Skip to content

Commit 6979e35

Browse files
arunodarauchg
authored andcommitted
Add content based HASH to main.js and common.js (vercel#1336)
* Use file hashes instead of BUILD_ID. Now JSON pages also not prefixed with a hash and doesn't support immutable caching. Instead it supports Etag bases caching. * Remove appUpdated Router Events hook. Becuase now we don't need it because there's no buildId validation. * Remove buildId generation. * Turn off hash checks in the dev mode. * Update tests. * Revert "Remove buildId generation." This reverts commit fdd36a5. * Bring back the buildId validation. * Handle buildId validation only in production. * Add BUILD_ID to path again. * Remove duplicate immutable header. * Fix tests.
1 parent 4057331 commit 6979e35

File tree

6 files changed

+62
-36
lines changed

6 files changed

+62
-36
lines changed

lib/router/index.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global window, location */
1+
/* global window */
22
import _Router from './router'
33

44
const SingletonRouter = {
@@ -81,6 +81,7 @@ export function _notifyBuildIdMismatch (nextRoute) {
8181
if (SingletonRouter.onAppUpdated) {
8282
SingletonRouter.onAppUpdated(nextRoute)
8383
} else {
84-
location.href = nextRoute
84+
console.warn(`An app update detected. Loading the SSR version of "${nextRoute}"`)
85+
window.location.href = nextRoute
8586
}
8687
}

lib/router/router.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { parse, format } from 'url'
22
import { EventEmitter } from 'events'
3+
import fetch from 'unfetch'
34
import evalScript from '../eval-script'
45
import shallowEquals from '../shallow-equals'
56
import PQueue from '../p-queue'
67
import { loadGetInitialProps, getURL } from '../utils'
78
import { _notifyBuildIdMismatch } from './'
8-
import fetch from 'unfetch'
99

1010
if (typeof window !== 'undefined' && typeof navigator.serviceWorker !== 'undefined') {
1111
navigator.serviceWorker.getRegistrations()
@@ -340,6 +340,7 @@ export default class Router extends EventEmitter {
340340
doFetchRoute (route) {
341341
const { buildId } = window.__NEXT_DATA__
342342
const url = `/_next/${encodeURIComponent(buildId)}/pages${route}`
343+
343344
return fetch(url, {
344345
method: 'GET',
345346
headers: { 'Accept': 'application/json' }

server/build/index.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export default async function build (dir) {
1111
const compiler = await webpack(dir, { buildDir })
1212

1313
try {
14-
await runCompiler(compiler)
14+
const webpackStats = await runCompiler(compiler)
15+
await writeBuildStats(buildDir, webpackStats)
1516
await writeBuildId(buildDir)
1617
} catch (err) {
1718
console.error(`> Failed to build on ${buildDir}`)
@@ -30,18 +31,32 @@ function runCompiler (compiler) {
3031
if (err) return reject(err)
3132

3233
const jsonStats = stats.toJson()
34+
3335
if (jsonStats.errors.length > 0) {
3436
const error = new Error(jsonStats.errors[0])
3537
error.errors = jsonStats.errors
3638
error.warnings = jsonStats.warnings
3739
return reject(error)
3840
}
3941

40-
resolve()
42+
resolve(jsonStats)
4143
})
4244
})
4345
}
4446

47+
async function writeBuildStats (dir, webpackStats) {
48+
const chunkHashMap = {}
49+
webpackStats.chunks
50+
// We are not interested about pages
51+
.filter(({ files }) => !/^bundles/.test(files[0]))
52+
.forEach(({ hash, files }) => {
53+
chunkHashMap[files[0]] = { hash }
54+
})
55+
56+
const buildStatsPath = join(dir, '.next', 'build-stats.json')
57+
await fs.writeFile(buildStatsPath, JSON.stringify(chunkHashMap), 'utf8')
58+
}
59+
4560
async function writeBuildId (dir) {
4661
const buildIdPath = join(dir, '.next', 'BUILD_ID')
4762
const buildId = uuid.v4()

server/document.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,25 @@ export class NextScript extends Component {
5959
_documentProps: PropTypes.any
6060
}
6161

62+
getChunkScript (filename) {
63+
const { __NEXT_DATA__ } = this.context._documentProps
64+
let { buildStats } = __NEXT_DATA__
65+
const hash = buildStats ? buildStats[filename].hash : '-'
66+
67+
return (
68+
<script type='text/javascript' src={`/_next/${hash}/${filename}`} />
69+
)
70+
}
71+
6272
render () {
6373
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
64-
let { buildId } = __NEXT_DATA__
6574

6675
return <div>
6776
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
6877
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
6978
}} />}
70-
{ staticMarkup ? null : <script type='text/javascript' src={`/_next/${buildId}/commons.js`} /> }
71-
{ staticMarkup ? null : <script type='text/javascript' src={`/_next/${buildId}/main.js`} /> }
79+
{ staticMarkup ? null : this.getChunkScript('commons.js') }
80+
{ staticMarkup ? null : this.getChunkScript('main.js') }
7281
</div>
7382
}
7483
}

server/index.js

+26-28
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { resolve, join } from 'path'
22
import { parse as parseUrl } from 'url'
33
import { parse as parseQs } from 'querystring'
4-
import fs from 'mz/fs'
4+
import fs from 'fs'
55
import http, { STATUS_CODES } from 'http'
66
import {
77
renderToHTML,
@@ -25,9 +25,18 @@ export default class Server {
2525
this.quiet = quiet
2626
this.router = new Router()
2727
this.hotReloader = dev ? new HotReloader(this.dir, { quiet }) : null
28-
this.renderOpts = { dir: this.dir, dev, staticMarkup, hotReloader: this.hotReloader }
2928
this.http = null
3029
this.config = getConfig(this.dir)
30+
this.buildStats = !dev ? require(join(this.dir, '.next', 'build-stats.json')) : null
31+
this.buildId = !dev ? this.readBuildId() : '-'
32+
this.renderOpts = {
33+
dev,
34+
staticMarkup,
35+
dir: this.dir,
36+
hotReloader: this.hotReloader,
37+
buildStats: this.buildStats,
38+
buildId: this.buildId
39+
}
3140

3241
this.defineRoutes()
3342
}
@@ -57,8 +66,6 @@ export default class Server {
5766
if (this.hotReloader) {
5867
await this.hotReloader.start()
5968
}
60-
61-
this.renderOpts.buildId = await this.readBuildId()
6269
}
6370

6471
async close () {
@@ -83,20 +90,14 @@ export default class Server {
8390
await this.serveStatic(req, res, p)
8491
},
8592

86-
'/_next/:buildId/main.js': async (req, res, params) => {
87-
if (!this.handleBuildId(params.buildId, res)) {
88-
throwBuildIdMismatchError()
89-
}
90-
93+
'/_next/:hash/main.js': async (req, res, params) => {
94+
this.handleBuildHash('main.js', params.hash, res)
9195
const p = join(this.dir, '.next/main.js')
9296
await this.serveStatic(req, res, p)
9397
},
9498

95-
'/_next/:buildId/commons.js': async (req, res, params) => {
96-
if (!this.handleBuildId(params.buildId, res)) {
97-
throwBuildIdMismatchError()
98-
}
99-
99+
'/_next/:hash/commons.js': async (req, res, params) => {
100+
this.handleBuildHash('commons.js', params.hash, res)
100101
const p = join(this.dir, '.next/commons.js')
101102
await this.serveStatic(req, res, p)
102103
},
@@ -277,18 +278,10 @@ export default class Server {
277278
}
278279
}
279280

280-
async readBuildId () {
281+
readBuildId () {
281282
const buildIdPath = join(this.dir, '.next', 'BUILD_ID')
282-
try {
283-
const buildId = await fs.readFile(buildIdPath, 'utf8')
284-
return buildId.trim()
285-
} catch (err) {
286-
if (err.code === 'ENOENT') {
287-
return '-'
288-
} else {
289-
throw err
290-
}
291-
}
283+
const buildId = fs.readFileSync(buildIdPath, 'utf8')
284+
return buildId.trim()
292285
}
293286

294287
handleBuildId (buildId, res) {
@@ -311,8 +304,13 @@ export default class Server {
311304
const p = resolveFromList(id, errors.keys())
312305
if (p) return errors.get(p)[0]
313306
}
314-
}
315307

316-
function throwBuildIdMismatchError () {
317-
throw new Error('BUILD_ID Mismatched!')
308+
handleBuildHash (filename, hash, res) {
309+
if (this.dev) return
310+
if (hash !== this.buildStats[filename].hash) {
311+
throw new Error(`Invalid Build File Hash(${hash}) for chunk: ${filename}`)
312+
}
313+
314+
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
315+
}
318316
}

server/render.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ async function doRender (req, res, pathname, query, {
3232
err,
3333
page,
3434
buildId,
35+
buildStats,
3536
hotReloader,
3637
dir = process.cwd(),
3738
dev = false,
@@ -94,6 +95,7 @@ async function doRender (req, res, pathname, query, {
9495
pathname,
9596
query,
9697
buildId,
98+
buildStats,
9799
err: (err && dev) ? errorToJSON(err) : null
98100
},
99101
dev,

0 commit comments

Comments
 (0)