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'
|
||||
|
||||
const SingletonRouter = {
|
||||
|
@ -81,6 +81,7 @@ export function _notifyBuildIdMismatch (nextRoute) {
|
|||
if (SingletonRouter.onAppUpdated) {
|
||||
SingletonRouter.onAppUpdated(nextRoute)
|
||||
} 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 { EventEmitter } from 'events'
|
||||
import fetch from 'unfetch'
|
||||
import evalScript from '../eval-script'
|
||||
import shallowEquals from '../shallow-equals'
|
||||
import PQueue from '../p-queue'
|
||||
import { loadGetInitialProps, getURL } from '../utils'
|
||||
import { _notifyBuildIdMismatch } from './'
|
||||
import fetch from 'unfetch'
|
||||
|
||||
if (typeof window !== 'undefined' && typeof navigator.serviceWorker !== 'undefined') {
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
|
@ -340,6 +340,7 @@ export default class Router extends EventEmitter {
|
|||
doFetchRoute (route) {
|
||||
const { buildId } = window.__NEXT_DATA__
|
||||
const url = `/_next/${encodeURIComponent(buildId)}/pages${route}`
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
|
|
|
@ -11,7 +11,8 @@ export default async function build (dir) {
|
|||
const compiler = await webpack(dir, { buildDir })
|
||||
|
||||
try {
|
||||
await runCompiler(compiler)
|
||||
const webpackStats = await runCompiler(compiler)
|
||||
await writeBuildStats(buildDir, webpackStats)
|
||||
await writeBuildId(buildDir)
|
||||
} catch (err) {
|
||||
console.error(`> Failed to build on ${buildDir}`)
|
||||
|
@ -30,6 +31,7 @@ function runCompiler (compiler) {
|
|||
if (err) return reject(err)
|
||||
|
||||
const jsonStats = stats.toJson()
|
||||
|
||||
if (jsonStats.errors.length > 0) {
|
||||
const error = new Error(jsonStats.errors[0])
|
||||
error.errors = jsonStats.errors
|
||||
|
@ -37,11 +39,24 @@ function runCompiler (compiler) {
|
|||
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) {
|
||||
const buildIdPath = join(dir, '.next', 'BUILD_ID')
|
||||
const buildId = uuid.v4()
|
||||
|
|
|
@ -59,16 +59,25 @@ export class NextScript extends Component {
|
|||
_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 () {
|
||||
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
|
||||
let { buildId } = __NEXT_DATA__
|
||||
|
||||
return <div>
|
||||
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
|
||||
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
|
||||
}} />}
|
||||
{ staticMarkup ? null : <script type='text/javascript' src={`/_next/${buildId}/commons.js`} /> }
|
||||
{ staticMarkup ? null : <script type='text/javascript' src={`/_next/${buildId}/main.js`} /> }
|
||||
{ staticMarkup ? null : this.getChunkScript('commons.js') }
|
||||
{ staticMarkup ? null : this.getChunkScript('main.js') }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { resolve, join } from 'path'
|
||||
import { parse as parseUrl } from 'url'
|
||||
import { parse as parseQs } from 'querystring'
|
||||
import fs from 'mz/fs'
|
||||
import fs from 'fs'
|
||||
import http, { STATUS_CODES } from 'http'
|
||||
import {
|
||||
renderToHTML,
|
||||
|
@ -25,9 +25,18 @@ export default class Server {
|
|||
this.quiet = quiet
|
||||
this.router = new Router()
|
||||
this.hotReloader = dev ? new HotReloader(this.dir, { quiet }) : null
|
||||
this.renderOpts = { dir: this.dir, dev, staticMarkup, hotReloader: this.hotReloader }
|
||||
this.http = null
|
||||
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()
|
||||
}
|
||||
|
@ -57,8 +66,6 @@ export default class Server {
|
|||
if (this.hotReloader) {
|
||||
await this.hotReloader.start()
|
||||
}
|
||||
|
||||
this.renderOpts.buildId = await this.readBuildId()
|
||||
}
|
||||
|
||||
async close () {
|
||||
|
@ -83,20 +90,14 @@ export default class Server {
|
|||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
'/_next/:buildId/main.js': async (req, res, params) => {
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
throwBuildIdMismatchError()
|
||||
}
|
||||
|
||||
'/_next/:hash/main.js': async (req, res, params) => {
|
||||
this.handleBuildHash('main.js', params.hash, res)
|
||||
const p = join(this.dir, '.next/main.js')
|
||||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
'/_next/:buildId/commons.js': async (req, res, params) => {
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
throwBuildIdMismatchError()
|
||||
}
|
||||
|
||||
'/_next/:hash/commons.js': async (req, res, params) => {
|
||||
this.handleBuildHash('commons.js', params.hash, res)
|
||||
const p = join(this.dir, '.next/commons.js')
|
||||
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')
|
||||
try {
|
||||
const buildId = await fs.readFile(buildIdPath, 'utf8')
|
||||
return buildId.trim()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return '-'
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const buildId = fs.readFileSync(buildIdPath, 'utf8')
|
||||
return buildId.trim()
|
||||
}
|
||||
|
||||
handleBuildId (buildId, res) {
|
||||
|
@ -311,8 +304,13 @@ export default class Server {
|
|||
const p = resolveFromList(id, errors.keys())
|
||||
if (p) return errors.get(p)[0]
|
||||
}
|
||||
}
|
||||
|
||||
function throwBuildIdMismatchError () {
|
||||
throw new Error('BUILD_ID Mismatched!')
|
||||
handleBuildHash (filename, hash, res) {
|
||||
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,
|
||||
page,
|
||||
buildId,
|
||||
buildStats,
|
||||
hotReloader,
|
||||
dir = process.cwd(),
|
||||
dev = false,
|
||||
|
@ -94,6 +95,7 @@ async function doRender (req, res, pathname, query, {
|
|||
pathname,
|
||||
query,
|
||||
buildId,
|
||||
buildStats,
|
||||
err: (err && dev) ? errorToJSON(err) : null
|
||||
},
|
||||
dev,
|
||||
|
|
Loading…
Reference in a new issue