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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 17 additions & 2 deletions
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

Lines changed: 12 additions & 3 deletions
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

Lines changed: 26 additions & 28 deletions
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

Lines changed: 2 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy