1
0
Fork 0
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:
Arunoda Susiripala 2017-03-08 00:13:56 +05:30 committed by Guillermo Rauch
parent 40573317f7
commit 6979e35947
6 changed files with 62 additions and 36 deletions

View file

@ -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
}
}

View file

@ -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' }

View file

@ -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()

View file

@ -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>
}
}

View file

@ -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')
}
}

View file

@ -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,