mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Load chunks in SSR mode only if they exists in the filesystem (#2196)
* Always check with the fs when gettings chunks. * Add a new set of test cases for dynamic imports in dev. * Add dynamic import test cases for production. * Add availableChunks support for static exports.
This commit is contained in:
parent
9c1898f2c8
commit
0c4362e440
|
@ -6,12 +6,13 @@ import { resolve, join, dirname, sep } from 'path'
|
|||
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
||||
import getConfig from './config'
|
||||
import { renderToHTML } from './render'
|
||||
import { getAvailableChunks } from './utils'
|
||||
import { printAndExit } from '../lib/utils'
|
||||
|
||||
export default async function (dir, options) {
|
||||
dir = resolve(dir)
|
||||
const config = getConfig(dir)
|
||||
const nextDir = join(dir, config.distDir || '.next')
|
||||
const nextDir = join(dir, config.distDir)
|
||||
|
||||
log(` using build directory: ${nextDir}`)
|
||||
|
||||
|
@ -79,7 +80,8 @@ export default async function (dir, options) {
|
|||
assetPrefix: config.assetPrefix.replace(/\/$/, ''),
|
||||
dev: false,
|
||||
staticMarkup: false,
|
||||
hotReloader: null
|
||||
hotReloader: null,
|
||||
availableChunks: getAvailableChunks(dir, config.distDir)
|
||||
}
|
||||
|
||||
// We need this for server rendering the Link component.
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import Router from './router'
|
||||
import HotReloader from './hot-reloader'
|
||||
import { resolveFromList } from './resolve'
|
||||
import { getAvailableChunks } from './utils'
|
||||
import getConfig from './config'
|
||||
// We need to go up one more level since we are in the `dist` directory
|
||||
import pkg from '../../package'
|
||||
|
@ -42,7 +43,8 @@ export default class Server {
|
|||
hotReloader: this.hotReloader,
|
||||
buildStats: this.buildStats,
|
||||
buildId: this.buildId,
|
||||
assetPrefix: this.config.assetPrefix.replace(/\/$/, '')
|
||||
assetPrefix: this.config.assetPrefix.replace(/\/$/, ''),
|
||||
availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist)
|
||||
}
|
||||
|
||||
this.defineRoutes()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { join } from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
import { createElement } from 'react'
|
||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import send from 'send'
|
||||
|
@ -40,6 +41,7 @@ async function doRender (req, res, pathname, query, {
|
|||
buildStats,
|
||||
hotReloader,
|
||||
assetPrefix,
|
||||
availableChunks,
|
||||
dir = process.cwd(),
|
||||
dev = false,
|
||||
staticMarkup = false,
|
||||
|
@ -81,7 +83,7 @@ async function doRender (req, res, pathname, query, {
|
|||
} finally {
|
||||
head = Head.rewind() || defaultHead()
|
||||
}
|
||||
const chunks = flushChunks()
|
||||
const chunks = loadChunks({ dev, dir, dist, availableChunks })
|
||||
|
||||
if (err && dev) {
|
||||
errorHtml = render(createElement(ErrorDebug, { error: err }))
|
||||
|
@ -242,3 +244,18 @@ async function ensurePage (page, { dir, hotReloader }) {
|
|||
|
||||
await hotReloader.ensurePage(page)
|
||||
}
|
||||
|
||||
function loadChunks ({ dev, dir, dist, availableChunks }) {
|
||||
const flushedChunks = flushChunks()
|
||||
const validChunks = []
|
||||
|
||||
for (var chunk of flushedChunks) {
|
||||
const filename = join(dir, dist, 'chunks', chunk)
|
||||
const exists = dev ? existsSync(filename) : availableChunks[chunk]
|
||||
if (exists) {
|
||||
validChunks.push(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
return validChunks
|
||||
}
|
||||
|
|
|
@ -1,2 +1,21 @@
|
|||
import { join } from 'path'
|
||||
import { readdirSync, existsSync } from 'fs'
|
||||
|
||||
export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.js$/
|
||||
export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.js$/
|
||||
|
||||
export function getAvailableChunks (dir, dist) {
|
||||
const chunksDir = join(dir, dist, 'chunks')
|
||||
if (!existsSync(chunksDir)) return {}
|
||||
|
||||
const chunksMap = {}
|
||||
const chunkFiles = readdirSync(chunksDir)
|
||||
|
||||
chunkFiles.forEach(filename => {
|
||||
if (/\.js$/.test(filename)) {
|
||||
chunksMap[filename] = true
|
||||
}
|
||||
})
|
||||
|
||||
return chunksMap
|
||||
}
|
||||
|
|
19
test/integration/basic/components/welcome.js
Normal file
19
test/integration/basic/components/welcome.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
|
||||
export default class Welcome extends React.Component {
|
||||
state = { name: null }
|
||||
|
||||
componentDidMount () {
|
||||
const { name } = this.props
|
||||
this.setState({ name })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name } = this.state
|
||||
if (!name) return null
|
||||
|
||||
return (
|
||||
<p>Welcome, {name}</p>
|
||||
)
|
||||
}
|
||||
}
|
7
test/integration/basic/pages/dynamic/index.js
Normal file
7
test/integration/basic/pages/dynamic/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Link href='/dynamic/no-chunk'><a>No Chunk</a></Link>
|
||||
</div>
|
||||
)
|
11
test/integration/basic/pages/dynamic/no-chunk.js
Normal file
11
test/integration/basic/pages/dynamic/no-chunk.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import Welcome from '../../components/welcome'
|
||||
|
||||
const Welcome2 = dynamic(import('../../components/welcome'))
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Welcome name='normal' />
|
||||
<Welcome2 name='dynamic' />
|
||||
</div>
|
||||
)
|
61
test/integration/basic/test/dynamic.js
Normal file
61
test/integration/basic/test/dynamic.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/* global describe, it, expect */
|
||||
import webdriver from 'next-webdriver'
|
||||
import cheerio from 'cheerio'
|
||||
import { waitFor } from 'next-test-utils'
|
||||
|
||||
export default (context, render) => {
|
||||
describe('Dynamic import', () => {
|
||||
describe('with SSR', () => {
|
||||
async function get$ (path, query) {
|
||||
const html = await render(path, query)
|
||||
return cheerio.load(html)
|
||||
}
|
||||
|
||||
it('should render dynmaic import components', async () => {
|
||||
const $ = await get$('/dynamic/ssr')
|
||||
expect($('p').text()).toBe('Hello World 1')
|
||||
})
|
||||
|
||||
it('should stop render dynmaic import components', async () => {
|
||||
const $ = await get$('/dynamic/no-ssr')
|
||||
expect($('p').text()).toBe('loading...')
|
||||
})
|
||||
|
||||
it('should stop render dynmaic import components with custom loading', async () => {
|
||||
const $ = await get$('/dynamic/no-ssr-custom-loading')
|
||||
expect($('p').text()).toBe('LOADING')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with browser', () => {
|
||||
it('should render the page client side', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/no-ssr-custom-loading')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (/Hello World 1/.test(bodyText)) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should render even there are no physical chunk exists', async () => {
|
||||
const browser = await webdriver(context.appPort, '/dynamic/no-chunk')
|
||||
|
||||
while (true) {
|
||||
const bodyText = await browser
|
||||
.elementByCss('body').text()
|
||||
if (
|
||||
/Welcome, normal/.test(bodyText) &&
|
||||
/Welcome, dynamic/.test(bodyText)
|
||||
) break
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
|
@ -15,6 +15,7 @@ import rendering from './rendering'
|
|||
import misc from './misc'
|
||||
import clientNavigation from './client-navigation'
|
||||
import hmr from './hmr'
|
||||
import dynamic from './dynamic'
|
||||
|
||||
const context = {}
|
||||
context.app = nextServer({
|
||||
|
@ -64,5 +65,6 @@ describe('Basic Features', () => {
|
|||
xPoweredBy(context)
|
||||
misc(context)
|
||||
clientNavigation(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||
dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||
hmr(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||
})
|
||||
|
|
|
@ -79,20 +79,5 @@ export default function ({ app }, suiteName, render) {
|
|||
expect($('h1').text()).toBe('404')
|
||||
expect($('h2').text()).toBe('This page could not be found.')
|
||||
})
|
||||
|
||||
test('render dynmaic import components via SSR', async () => {
|
||||
const $ = await get$('/dynamic/ssr')
|
||||
expect($('p').text()).toBe('Hello World 1')
|
||||
})
|
||||
|
||||
test('stop render dynmaic import components in SSR', async () => {
|
||||
const $ = await get$('/dynamic/no-ssr')
|
||||
expect($('p').text()).toBe('loading...')
|
||||
})
|
||||
|
||||
test('stop render dynmaic import components in SSR with custom loading', async () => {
|
||||
const $ = await get$('/dynamic/no-ssr-custom-loading')
|
||||
expect($('p').text()).toBe('LOADING')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
3
test/integration/production/components/hello1.js
Normal file
3
test/integration/production/components/hello1.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<p>Hello World 1</p>
|
||||
)
|
19
test/integration/production/components/welcome.js
Normal file
19
test/integration/production/components/welcome.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
|
||||
export default class Welcome extends React.Component {
|
||||
state = { name: null }
|
||||
|
||||
componentDidMount () {
|
||||
const { name } = this.props
|
||||
this.setState({ name })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name } = this.state
|
||||
if (!name) return null
|
||||
|
||||
return (
|
||||
<p>Welcome, {name}</p>
|
||||
)
|
||||
}
|
||||
}
|
7
test/integration/production/pages/dynamic/index.js
Normal file
7
test/integration/production/pages/dynamic/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Link href='/dynamic/no-chunk'><a>No Chunk</a></Link>
|
||||
</div>
|
||||
)
|
11
test/integration/production/pages/dynamic/no-chunk.js
Normal file
11
test/integration/production/pages/dynamic/no-chunk.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import Welcome from '../../components/welcome'
|
||||
|
||||
const Welcome2 = dynamic(import('../../components/welcome'))
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Welcome name='normal' />
|
||||
<Welcome2 name='dynamic' />
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Hello = dynamic(import('../../components/hello1'), {
|
||||
ssr: false,
|
||||
loading: () => (<p>LOADING</p>)
|
||||
})
|
||||
|
||||
export default Hello
|
5
test/integration/production/pages/dynamic/no-ssr.js
Normal file
5
test/integration/production/pages/dynamic/no-ssr.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Hello = dynamic(import('../../components/hello1'), { ssr: false })
|
||||
|
||||
export default Hello
|
5
test/integration/production/pages/dynamic/ssr.js
Normal file
5
test/integration/production/pages/dynamic/ssr.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Hello = dynamic(import('../../components/hello1'))
|
||||
|
||||
export default Hello
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from 'next-test-utils'
|
||||
import webdriver from 'next-webdriver'
|
||||
import fetch from 'node-fetch'
|
||||
import dynamicImportTests from '../../basic/test/dynamic'
|
||||
|
||||
const appDir = join(__dirname, '../')
|
||||
let appPort
|
||||
|
@ -18,6 +19,8 @@ let server
|
|||
let app
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000
|
||||
|
||||
const context = {}
|
||||
|
||||
describe('Production Usage', () => {
|
||||
beforeAll(async () => {
|
||||
await nextBuild(appDir)
|
||||
|
@ -28,7 +31,7 @@ describe('Production Usage', () => {
|
|||
})
|
||||
|
||||
server = await startApp(app)
|
||||
appPort = server.address().port
|
||||
context.appPort = appPort = server.address().port
|
||||
})
|
||||
afterAll(() => stopApp(server))
|
||||
|
||||
|
@ -77,4 +80,6 @@ describe('Production Usage', () => {
|
|||
browser.close()
|
||||
})
|
||||
})
|
||||
|
||||
dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue