mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Add content based HASH to main.js and common.js (#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 fdd36a5a0a307becdbd1d85ae3881b3a15b03d26. * Bring back the buildId validation. * Handle buildId validation only in production. * Add BUILD_ID to path again. * Remove duplicate immutable header. * Fix tests.
This commit is contained in:
parent
40573317f7
commit
6979e35947
|
@ -1,4 +1,4 @@
|
||||||
/* global window, location */
|
/* global window */
|
||||||
import _Router from './router'
|
import _Router from './router'
|
||||||
|
|
||||||
const SingletonRouter = {
|
const SingletonRouter = {
|
||||||
|
@ -81,6 +81,7 @@ export function _notifyBuildIdMismatch (nextRoute) {
|
||||||
if (SingletonRouter.onAppUpdated) {
|
if (SingletonRouter.onAppUpdated) {
|
||||||
SingletonRouter.onAppUpdated(nextRoute)
|
SingletonRouter.onAppUpdated(nextRoute)
|
||||||
} else {
|
} else {
|
||||||
location.href = nextRoute
|
console.warn(`An app update detected. Loading the SSR version of "${nextRoute}"`)
|
||||||
|
window.location.href = nextRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { parse, format } from 'url'
|
import { parse, format } from 'url'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
|
import fetch from 'unfetch'
|
||||||
import evalScript from '../eval-script'
|
import evalScript from '../eval-script'
|
||||||
import shallowEquals from '../shallow-equals'
|
import shallowEquals from '../shallow-equals'
|
||||||
import PQueue from '../p-queue'
|
import PQueue from '../p-queue'
|
||||||
import { loadGetInitialProps, getURL } from '../utils'
|
import { loadGetInitialProps, getURL } from '../utils'
|
||||||
import { _notifyBuildIdMismatch } from './'
|
import { _notifyBuildIdMismatch } from './'
|
||||||
import fetch from 'unfetch'
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && typeof navigator.serviceWorker !== 'undefined') {
|
if (typeof window !== 'undefined' && typeof navigator.serviceWorker !== 'undefined') {
|
||||||
navigator.serviceWorker.getRegistrations()
|
navigator.serviceWorker.getRegistrations()
|
||||||
|
@ -340,6 +340,7 @@ export default class Router extends EventEmitter {
|
||||||
doFetchRoute (route) {
|
doFetchRoute (route) {
|
||||||
const { buildId } = window.__NEXT_DATA__
|
const { buildId } = window.__NEXT_DATA__
|
||||||
const url = `/_next/${encodeURIComponent(buildId)}/pages${route}`
|
const url = `/_next/${encodeURIComponent(buildId)}/pages${route}`
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Accept': 'application/json' }
|
headers: { 'Accept': 'application/json' }
|
||||||
|
|
|
@ -11,7 +11,8 @@ export default async function build (dir) {
|
||||||
const compiler = await webpack(dir, { buildDir })
|
const compiler = await webpack(dir, { buildDir })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runCompiler(compiler)
|
const webpackStats = await runCompiler(compiler)
|
||||||
|
await writeBuildStats(buildDir, webpackStats)
|
||||||
await writeBuildId(buildDir)
|
await writeBuildId(buildDir)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`> Failed to build on ${buildDir}`)
|
console.error(`> Failed to build on ${buildDir}`)
|
||||||
|
@ -30,6 +31,7 @@ function runCompiler (compiler) {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
|
|
||||||
const jsonStats = stats.toJson()
|
const jsonStats = stats.toJson()
|
||||||
|
|
||||||
if (jsonStats.errors.length > 0) {
|
if (jsonStats.errors.length > 0) {
|
||||||
const error = new Error(jsonStats.errors[0])
|
const error = new Error(jsonStats.errors[0])
|
||||||
error.errors = jsonStats.errors
|
error.errors = jsonStats.errors
|
||||||
|
@ -37,11 +39,24 @@ function runCompiler (compiler) {
|
||||||
return reject(error)
|
return reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve()
|
resolve(jsonStats)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeBuildStats (dir, webpackStats) {
|
||||||
|
const chunkHashMap = {}
|
||||||
|
webpackStats.chunks
|
||||||
|
// We are not interested about pages
|
||||||
|
.filter(({ files }) => !/^bundles/.test(files[0]))
|
||||||
|
.forEach(({ hash, files }) => {
|
||||||
|
chunkHashMap[files[0]] = { hash }
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildStatsPath = join(dir, '.next', 'build-stats.json')
|
||||||
|
await fs.writeFile(buildStatsPath, JSON.stringify(chunkHashMap), 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
async function writeBuildId (dir) {
|
async function writeBuildId (dir) {
|
||||||
const buildIdPath = join(dir, '.next', 'BUILD_ID')
|
const buildIdPath = join(dir, '.next', 'BUILD_ID')
|
||||||
const buildId = uuid.v4()
|
const buildId = uuid.v4()
|
||||||
|
|
|
@ -59,16 +59,25 @@ export class NextScript extends Component {
|
||||||
_documentProps: PropTypes.any
|
_documentProps: PropTypes.any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChunkScript (filename) {
|
||||||
|
const { __NEXT_DATA__ } = this.context._documentProps
|
||||||
|
let { buildStats } = __NEXT_DATA__
|
||||||
|
const hash = buildStats ? buildStats[filename].hash : '-'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<script type='text/javascript' src={`/_next/${hash}/${filename}`} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
|
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
|
||||||
let { buildId } = __NEXT_DATA__
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
|
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
|
||||||
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
|
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
|
||||||
}} />}
|
}} />}
|
||||||
{ staticMarkup ? null : <script type='text/javascript' src={`/_next/${buildId}/commons.js`} /> }
|
{ staticMarkup ? null : this.getChunkScript('commons.js') }
|
||||||
{ staticMarkup ? null : <script type='text/javascript' src={`/_next/${buildId}/main.js`} /> }
|
{ staticMarkup ? null : this.getChunkScript('main.js') }
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import { parse as parseUrl } from 'url'
|
import { parse as parseUrl } from 'url'
|
||||||
import { parse as parseQs } from 'querystring'
|
import { parse as parseQs } from 'querystring'
|
||||||
import fs from 'mz/fs'
|
import fs from 'fs'
|
||||||
import http, { STATUS_CODES } from 'http'
|
import http, { STATUS_CODES } from 'http'
|
||||||
import {
|
import {
|
||||||
renderToHTML,
|
renderToHTML,
|
||||||
|
@ -25,9 +25,18 @@ export default class Server {
|
||||||
this.quiet = quiet
|
this.quiet = quiet
|
||||||
this.router = new Router()
|
this.router = new Router()
|
||||||
this.hotReloader = dev ? new HotReloader(this.dir, { quiet }) : null
|
this.hotReloader = dev ? new HotReloader(this.dir, { quiet }) : null
|
||||||
this.renderOpts = { dir: this.dir, dev, staticMarkup, hotReloader: this.hotReloader }
|
|
||||||
this.http = null
|
this.http = null
|
||||||
this.config = getConfig(this.dir)
|
this.config = getConfig(this.dir)
|
||||||
|
this.buildStats = !dev ? require(join(this.dir, '.next', 'build-stats.json')) : null
|
||||||
|
this.buildId = !dev ? this.readBuildId() : '-'
|
||||||
|
this.renderOpts = {
|
||||||
|
dev,
|
||||||
|
staticMarkup,
|
||||||
|
dir: this.dir,
|
||||||
|
hotReloader: this.hotReloader,
|
||||||
|
buildStats: this.buildStats,
|
||||||
|
buildId: this.buildId
|
||||||
|
}
|
||||||
|
|
||||||
this.defineRoutes()
|
this.defineRoutes()
|
||||||
}
|
}
|
||||||
|
@ -57,8 +66,6 @@ export default class Server {
|
||||||
if (this.hotReloader) {
|
if (this.hotReloader) {
|
||||||
await this.hotReloader.start()
|
await this.hotReloader.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderOpts.buildId = await this.readBuildId()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async close () {
|
async close () {
|
||||||
|
@ -83,20 +90,14 @@ export default class Server {
|
||||||
await this.serveStatic(req, res, p)
|
await this.serveStatic(req, res, p)
|
||||||
},
|
},
|
||||||
|
|
||||||
'/_next/:buildId/main.js': async (req, res, params) => {
|
'/_next/:hash/main.js': async (req, res, params) => {
|
||||||
if (!this.handleBuildId(params.buildId, res)) {
|
this.handleBuildHash('main.js', params.hash, res)
|
||||||
throwBuildIdMismatchError()
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = join(this.dir, '.next/main.js')
|
const p = join(this.dir, '.next/main.js')
|
||||||
await this.serveStatic(req, res, p)
|
await this.serveStatic(req, res, p)
|
||||||
},
|
},
|
||||||
|
|
||||||
'/_next/:buildId/commons.js': async (req, res, params) => {
|
'/_next/:hash/commons.js': async (req, res, params) => {
|
||||||
if (!this.handleBuildId(params.buildId, res)) {
|
this.handleBuildHash('commons.js', params.hash, res)
|
||||||
throwBuildIdMismatchError()
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = join(this.dir, '.next/commons.js')
|
const p = join(this.dir, '.next/commons.js')
|
||||||
await this.serveStatic(req, res, p)
|
await this.serveStatic(req, res, p)
|
||||||
},
|
},
|
||||||
|
@ -277,18 +278,10 @@ export default class Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readBuildId () {
|
readBuildId () {
|
||||||
const buildIdPath = join(this.dir, '.next', 'BUILD_ID')
|
const buildIdPath = join(this.dir, '.next', 'BUILD_ID')
|
||||||
try {
|
const buildId = fs.readFileSync(buildIdPath, 'utf8')
|
||||||
const buildId = await fs.readFile(buildIdPath, 'utf8')
|
return buildId.trim()
|
||||||
return buildId.trim()
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
return '-'
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBuildId (buildId, res) {
|
handleBuildId (buildId, res) {
|
||||||
|
@ -311,8 +304,13 @@ export default class Server {
|
||||||
const p = resolveFromList(id, errors.keys())
|
const p = resolveFromList(id, errors.keys())
|
||||||
if (p) return errors.get(p)[0]
|
if (p) return errors.get(p)[0]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function throwBuildIdMismatchError () {
|
handleBuildHash (filename, hash, res) {
|
||||||
throw new Error('BUILD_ID Mismatched!')
|
if (this.dev) return
|
||||||
|
if (hash !== this.buildStats[filename].hash) {
|
||||||
|
throw new Error(`Invalid Build File Hash(${hash}) for chunk: ${filename}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ async function doRender (req, res, pathname, query, {
|
||||||
err,
|
err,
|
||||||
page,
|
page,
|
||||||
buildId,
|
buildId,
|
||||||
|
buildStats,
|
||||||
hotReloader,
|
hotReloader,
|
||||||
dir = process.cwd(),
|
dir = process.cwd(),
|
||||||
dev = false,
|
dev = false,
|
||||||
|
@ -94,6 +95,7 @@ async function doRender (req, res, pathname, query, {
|
||||||
pathname,
|
pathname,
|
||||||
query,
|
query,
|
||||||
buildId,
|
buildId,
|
||||||
|
buildStats,
|
||||||
err: (err && dev) ? errorToJSON(err) : null
|
err: (err && dev) ? errorToJSON(err) : null
|
||||||
},
|
},
|
||||||
dev,
|
dev,
|
||||||
|
|
Loading…
Reference in a new issue