diff --git a/lib/router/index.js b/lib/router/index.js
index 550ef110d3e2..aa9d4439b872 100644
--- a/lib/router/index.js
+++ b/lib/router/index.js
@@ -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
}
}
diff --git a/lib/router/router.js b/lib/router/router.js
index 518d1faff774..ed6f1e82b529 100644
--- a/lib/router/router.js
+++ b/lib/router/router.js
@@ -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()
@@ -322,6 +322,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' }
diff --git a/server/build/index.js b/server/build/index.js
index 65dcf31da8f4..0fcff294423b 100644
--- a/server/build/index.js
+++ b/server/build/index.js
@@ -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()
diff --git a/server/document.js b/server/document.js
index 588da328f7b8..a2682aa1b0f5 100644
--- a/server/document.js
+++ b/server/document.js
@@ -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 (
+
+ )
+ }
+
render () {
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
- let { buildId } = __NEXT_DATA__
return
{staticMarkup ? null : }
- { staticMarkup ? null : }
- { staticMarkup ? null : }
+ { staticMarkup ? null : this.getChunkScript('commons.js') }
+ { staticMarkup ? null : this.getChunkScript('main.js') }
}
}
diff --git a/server/index.js b/server/index.js
index 27978aa6e6d0..f08a9fec8ed9 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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')
+ }
}
diff --git a/server/render.js b/server/render.js
index e4551197465e..fac5dffb61bc 100644
--- a/server/render.js
+++ b/server/render.js
@@ -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,