Skip to content

Commit c257959

Browse files
committed
Simplify build and js serving file structures
A number of different changes here that are unfortunately tied. 1. Move all client source into .next/bundles dir 2. Simplify serving logic where possible to map directly to these paths 3. Define buildId at the start of the build and tag dynamic chunk loading with this. 4. Avoid creating duplicate chunks with the same content when loaded via different explicit require paths. These changes also make it much easier for Potentially breaking changes: 1. Requests for page javascript are now *.js. / is special cased to be index.js. 2. Composite source files are not filtered based on dev or not 3. _error/index.js -> _error.js
1 parent e2fb50c commit c257959

File tree

10 files changed

+58
-107
lines changed

10 files changed

+58
-107
lines changed

server/build/babel/plugins/handle-import.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// We've added support for SSR with this version
33
import template from 'babel-template'
44
import syntax from 'babel-plugin-syntax-dynamic-import'
5-
import UUID from 'uuid'
5+
import { dirname, relative, resolve } from 'path'
66

77
const TYPE_IMPORT = 'Import'
88

@@ -43,10 +43,15 @@ export default () => ({
4343
visitor: {
4444
CallExpression (path) {
4545
if (path.node.callee.type === TYPE_IMPORT) {
46+
const { opts } = path.hub.file
47+
4648
const moduleName = path.node.arguments[0].value
47-
const name = `${moduleName.replace(/[^\w]/g, '-')}-${UUID.v4()}`
49+
const currentDir = dirname(opts.filename)
50+
const modulePath = resolve(currentDir, moduleName)
51+
const chunkName = relative(opts.sourceRoot, modulePath).replace(/[^\w]/g, '-')
52+
4853
const newImport = buildImport({
49-
name
54+
name: chunkName
5055
})({
5156
SOURCE: path.node.arguments
5257
})

server/build/index.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import md5File from 'md5-file/promise'
99

1010
export default async function build (dir, conf = null) {
1111
const buildDir = join(tmpdir(), uuid.v4())
12-
const compiler = await webpack(dir, { buildDir, conf })
12+
const buildId = uuid.v4()
13+
14+
const compiler = await webpack(dir, { buildDir, buildId, conf })
1315

1416
try {
1517
await runCompiler(compiler)
1618
await writeBuildStats(buildDir)
17-
await writeBuildId(buildDir)
19+
await writeBuildId(buildDir, buildId)
1820
} catch (err) {
1921
console.error(`> Failed to build on ${buildDir}`)
2022
throw err
@@ -52,15 +54,14 @@ async function writeBuildStats (dir) {
5254
// So, we need to generate the hash ourself.
5355
const assetHashMap = {
5456
'app.js': {
55-
hash: await md5File(join(dir, '.next', 'app.js'))
57+
hash: await md5File(join(dir, '.next', 'bundles', 'app.js'))
5658
}
5759
}
5860
const buildStatsPath = join(dir, '.next', 'build-stats.json')
5961
await fs.writeFile(buildStatsPath, JSON.stringify(assetHashMap), 'utf8')
6062
}
6163

62-
async function writeBuildId (dir) {
64+
async function writeBuildId (dir, buildId) {
6365
const buildIdPath = join(dir, '.next', 'BUILD_ID')
64-
const buildId = uuid.v4()
6566
await fs.writeFile(buildIdPath, buildId, 'utf8')
6667
}

server/build/plugins/dynamic-chunks-plugin.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ export default class PagesPlugin {
2626
source: () => newContent,
2727
size: () => newContent.length
2828
}
29-
30-
// This is to support, webpack dynamic import support with HMR
31-
compilation.assets[`chunks/${chunk.id}`] = {
32-
source: () => newContent,
33-
size: () => newContent.length
34-
}
3529
})
3630
callback()
3731
})

server/build/plugins/watch-pages-plugin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default class WatchPagesPlugin {
1010
compilation.plugin('optimize-assets', (assets, callback) => {
1111
// transpile pages/_document.js and descendants,
1212
// but don't need the bundle file
13-
delete assets[join('bundles', 'pages', '_document.js')]
13+
delete assets[join('pages', '_document.js')]
1414
callback()
1515
})
1616
})

server/build/webpack.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const interpolateNames = new Map(defaultPages.map((p) => {
2727

2828
const relativeResolve = rootModuleRelativePath(require)
2929

30-
export default async function createCompiler (dir, { dev = false, quiet = false, buildDir, conf = null } = {}) {
30+
export default async function createCompiler (dir, { dev = false, quiet = false, buildDir, buildId = 'development', conf = null } = {}) {
3131
dir = resolve(dir)
3232
const config = getConfig(dir, conf)
3333
const defaultEntries = dev ? [
@@ -54,16 +54,16 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
5454
// managing pages.
5555
if (dev) {
5656
for (const p of devPages) {
57-
entries[join('bundles', p)] = [`./${p}?entry`]
57+
entries[p] = [`./${p}?entry`]
5858
}
5959
} else {
6060
for (const p of pages) {
61-
entries[join('bundles', p)] = [`./${p}?entry`]
61+
entries[p] = [`./${p}?entry`]
6262
}
6363
}
6464

6565
for (const p of defaultPages) {
66-
const entryName = join('bundles', 'pages', p)
66+
const entryName = join('pages', p)
6767
if (!entries[entryName]) {
6868
entries[entryName] = [join(nextPagesDir, p) + '?entry']
6969
}
@@ -207,7 +207,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
207207
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0
208208
},
209209
options: {
210-
name: 'dist/[path][name].[ext]',
210+
name: '../dist/[path][name].[ext]',
211211
// By default, our babel config does not transpile ES2015 module syntax because
212212
// webpack knows how to handle them. (That's how it can do tree-shaking)
213213
// But Node.js doesn't know how to handle them. So, we have to transpile them here.
@@ -279,10 +279,10 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
279279
context: dir,
280280
entry,
281281
output: {
282-
path: buildDir ? join(buildDir, '.next') : join(dir, config.distDir),
282+
path: buildDir ? join(buildDir, '.next', 'bundles') : join(dir, config.distDir, 'bundles'),
283283
filename: '[name]',
284284
libraryTarget: 'commonjs2',
285-
publicPath: '/_next/webpack/',
285+
publicPath: `/_next/${buildId}/`,
286286
strictModuleExceptionHandling: true,
287287
devtoolModuleFilenameTemplate ({ resourcePath }) {
288288
const hash = createHash('sha1')

server/document.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ export class Head extends Component {
6767

6868
getPreloadDynamicChunks () {
6969
const { chunks, __NEXT_DATA__ } = this.context._documentProps
70-
let { assetPrefix } = __NEXT_DATA__
70+
let { assetPrefix, buildId } = __NEXT_DATA__
7171
return chunks.map((chunk) => (
7272
<link
7373
key={chunk}
7474
rel='preload'
75-
href={`${assetPrefix}/_next/webpack/chunks/${chunk}`}
75+
href={`${assetPrefix}/_next/${buildId}/chunks/${chunk}`}
7676
as='script'
7777
/>
7878
))
@@ -85,7 +85,7 @@ export class Head extends Component {
8585

8686
return <head {...this.props}>
8787
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' />
88-
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error/index.js`} as='script' />
88+
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error.js`} as='script' />
8989
{this.getPreloadDynamicChunks()}
9090
{this.getPreloadMainLinks()}
9191
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
@@ -148,15 +148,15 @@ export class NextScript extends Component {
148148

149149
getDynamicChunks () {
150150
const { chunks, __NEXT_DATA__ } = this.context._documentProps
151-
let { assetPrefix } = __NEXT_DATA__
151+
let { assetPrefix, buildId } = __NEXT_DATA__
152152
return (
153153
<div>
154154
{chunks.map((chunk) => (
155155
<script
156156
async
157157
key={chunk}
158158
type='text/javascript'
159-
src={`${assetPrefix}/_next/webpack/chunks/${chunk}`}
159+
src={`${assetPrefix}/_next/${buildId}/chunks/${chunk}`}
160160
/>
161161
))}
162162
</div>
@@ -188,15 +188,16 @@ export class NextScript extends Component {
188188
`
189189
}} />}
190190
<script async id={`__NEXT_PAGE__${pathname}`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} />
191-
<script async id={`__NEXT_PAGE__/_error`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error/index.js`} />
191+
<script async defer id={`__NEXT_PAGE__/_error`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error.js`} />
192192
{staticMarkup ? null : this.getDynamicChunks()}
193193
{staticMarkup ? null : this.getScripts()}
194194
</div>
195195
}
196196
}
197197

198198
function getPagePathname (pathname, nextExport) {
199-
if (!nextExport) return pathname
200199
if (pathname === '/') return '/index.js'
200+
201+
if (!nextExport) return `${pathname}.js`
201202
return `${pathname}/index.js`
202203
}

server/hot-reloader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default class HotReloader {
4040

4141
async start () {
4242
const [compiler] = await Promise.all([
43-
webpack(this.dir, { dev: true, quiet: this.quiet }),
43+
webpack(this.dir, { dev: true, buildId: 'hmr', quiet: this.quiet }),
4444
clean(this.dir)
4545
])
4646

@@ -66,7 +66,7 @@ export default class HotReloader {
6666
this.stats = null
6767

6868
const [compiler] = await Promise.all([
69-
webpack(this.dir, { dev: true, quiet: this.quiet }),
69+
webpack(this.dir, { dev: true, buildId: 'hmr', quiet: this.quiet }),
7070
clean(this.dir)
7171
])
7272

@@ -173,7 +173,7 @@ export default class HotReloader {
173173
]
174174

175175
let webpackDevMiddlewareConfig = {
176-
publicPath: '/_next/webpack/',
176+
publicPath: '/_next/hmr/',
177177
noInfo: true,
178178
quiet: true,
179179
clientLogLevel: 'warning',

server/index.js

Lines changed: 22 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -114,68 +114,12 @@ export default class Server {
114114
await this.serveStatic(req, res, p)
115115
},
116116

117-
// This is to support, webpack dynamic imports in production.
118-
'/_next/webpack/chunks/:name': async (req, res, params) => {
119-
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
120-
const p = join(this.dir, this.dist, 'chunks', params.name)
121-
await this.serveStatic(req, res, p)
122-
},
123-
124-
// This is to support, webpack dynamic import support with HMR
125-
'/_next/webpack/:id': async (req, res, params) => {
126-
const p = join(this.dir, this.dist, 'chunks', params.id)
127-
await this.serveStatic(req, res, p)
128-
},
129-
130-
'/_next/:hash/manifest.js': async (req, res, params) => {
131-
if (!this.dev) return this.send404(res)
132-
133-
this.handleBuildHash('manifest.js', params.hash, res)
134-
const p = join(this.dir, this.dist, 'manifest.js')
135-
await this.serveStatic(req, res, p)
136-
},
137-
138-
'/_next/:hash/main.js': async (req, res, params) => {
139-
if (!this.dev) return this.send404(res)
140-
141-
this.handleBuildHash('main.js', params.hash, res)
142-
const p = join(this.dir, this.dist, 'main.js')
143-
await this.serveStatic(req, res, p)
144-
},
145-
146-
'/_next/:hash/commons.js': async (req, res, params) => {
147-
if (!this.dev) return this.send404(res)
148-
149-
this.handleBuildHash('commons.js', params.hash, res)
150-
const p = join(this.dir, this.dist, 'commons.js')
151-
await this.serveStatic(req, res, p)
152-
},
153-
154-
'/_next/:hash/app.js': async (req, res, params) => {
155-
if (this.dev) return this.send404(res)
156-
157-
this.handleBuildHash('app.js', params.hash, res)
158-
const p = join(this.dir, this.dist, 'app.js')
159-
await this.serveStatic(req, res, p)
160-
},
161-
162-
'/_next/:buildId/page/_error*': async (req, res, params) => {
163-
if (!this.handleBuildId(params.buildId, res)) {
164-
const error = new Error('INVALID_BUILD_ID')
165-
const customFields = { buildIdMismatched: true }
166-
167-
return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts)
168-
}
169-
170-
const p = join(this.dir, `${this.dist}/bundles/pages/_error.js`)
171-
await this.serveStatic(req, res, p)
172-
},
173-
174-
'/_next/:buildId/page/:path*': async (req, res, params) => {
117+
'/_next/:hash/page/:path*': async (req, res, params) => {
175118
const paths = params.path || ['']
176119
const page = `/${paths.join('/')}`
120+
const filename = `pages/${page.replace(/\.js$/, '')}.js`
177121

178-
if (!this.handleBuildId(params.buildId, res)) {
122+
if (!this.handleBuildHash(filename, params.hash, res)) {
179123
const error = new Error('INVALID_BUILD_ID')
180124
const customFields = { buildIdMismatched: true }
181125

@@ -199,6 +143,20 @@ export default class Server {
199143
await renderScript(req, res, page, this.renderOpts)
200144
},
201145

146+
'/_next/:hash/:name': async (req, res, params) => {
147+
if (!this.dev) return this.send404(res)
148+
149+
if (!this.handleBuildHash(params.name, params.hash, res)) {
150+
const error = new Error('INVALID_BUILD_ID')
151+
const customFields = { buildIdMismatched: true }
152+
153+
return await renderScriptError(req, res, params.name, error, customFields, this.renderOpts)
154+
}
155+
156+
const p = join(this.dir, this.dist, 'bundles', params.name)
157+
await this.serveStatic(req, res, p)
158+
},
159+
202160
// It's very important keep this route's param optional.
203161
// (but it should support as many as params, seperated by '/')
204162
// Othewise this will lead to a pretty simple DOS attack.
@@ -379,16 +337,6 @@ export default class Server {
379337
return buildId.trim()
380338
}
381339

382-
handleBuildId (buildId, res) {
383-
if (this.dev) return true
384-
if (buildId !== this.renderOpts.buildId) {
385-
return false
386-
}
387-
388-
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
389-
return true
390-
}
391-
392340
async getCompilationError () {
393341
if (!this.hotReloader) return
394342

@@ -400,13 +348,15 @@ export default class Server {
400348
}
401349

402350
handleBuildHash (filename, hash, res) {
403-
if (this.dev) return
351+
if (this.dev) return true
404352

405-
if (hash !== this.buildStats[filename].hash) {
406-
throw new Error(`Invalid Build File Hash(${hash}) for chunk: ${filename}`)
353+
if (hash !== this.renderOpts.buildId &&
354+
hash !== (this.buildStats[filename] || {}).hash) {
355+
return false
407356
}
408357

409358
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
359+
return true
410360
}
411361

412362
send404 (res) {

server/on-demand-entry-handler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
129129

130130
const pagePath = join(dir, 'pages', page)
131131
const pathname = await resolvePath(pagePath)
132-
const name = join('bundles', pathname.substring(dir.length))
132+
const name = join('.', pathname.substring(dir.length))
133133

134134
const entry = [`${pathname}?entry`]
135135

server/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { join } from 'path'
22
import { readdirSync, existsSync } from 'fs'
33

4-
export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.js$/
5-
export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.js$/
4+
export const IS_BUNDLED_PAGE = /^pages.*\.js$/
5+
export const MATCH_ROUTE_NAME = /^pages[/\\](.*)\.js$/
66

77
export function getAvailableChunks (dir, dist) {
88
const chunksDir = join(dir, dist, 'chunks')

0 commit comments

Comments
 (0)