diff --git a/lib/router/index.js b/lib/router/index.js index 1df0ef8a..550ef110 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -1,3 +1,4 @@ +/* global window, location */ import _Router from './router' const SingletonRouter = { @@ -75,3 +76,11 @@ export const createRouter = function (...args) { // Export the actual Router class, which is usually used inside the server export const Router = _Router + +export function _notifyBuildIdMismatch (nextRoute) { + if (SingletonRouter.onAppUpdated) { + SingletonRouter.onAppUpdated(nextRoute) + } else { + location.href = nextRoute + } +} diff --git a/lib/router/router.js b/lib/router/router.js index 30f7ab5f..e3141a35 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -6,6 +6,7 @@ import evalScript from '../eval-script' import shallowEquals from '../shallow-equals' import PQueue from '../p-queue' import { loadGetInitialProps, getLocationOrigin } from '../utils' +import { _notifyBuildIdMismatch } from './' // Add "fetch" polyfill for older browsers if (typeof window !== 'undefined') { @@ -75,7 +76,7 @@ export default class Router extends EventEmitter { data, props, error - } = await this.getRouteInfo(route, pathname, query) + } = await this.getRouteInfo(route, pathname, query, as) if (error && error.cancelled) { this.emit('routeChangeError', error, as) @@ -116,7 +117,7 @@ export default class Router extends EventEmitter { data, props, error - } = await this.getRouteInfo(route, pathname, query) + } = await this.getRouteInfo(route, pathname, query, url) if (error && error.cancelled) { this.emit('routeChangeError', error, url) @@ -162,7 +163,7 @@ export default class Router extends EventEmitter { this.emit('routeChangeStart', as) const { data, props, error - } = await this.getRouteInfo(route, pathname, query) + } = await this.getRouteInfo(route, pathname, query, as) if (error && error.cancelled) { this.emit('routeChangeError', error, as) @@ -189,11 +190,16 @@ export default class Router extends EventEmitter { } } - async getRouteInfo (route, pathname, query) { + async getRouteInfo (route, pathname, query, as) { const routeInfo = {} try { - const { Component, err, jsonPageRes } = routeInfo.data = await this.fetchComponent(route) + routeInfo.data = await this.fetchComponent(route, as) + if (!routeInfo.data) { + return null + } + + const { Component, err, jsonPageRes } = routeInfo.data const ctx = { err, pathname, query, jsonPageRes } routeInfo.props = await this.getInitialProps(Component, ctx) } catch (err) { @@ -229,7 +235,7 @@ export default class Router extends EventEmitter { return this.prefetchQueue.add(() => this.fetchRoute(route)) } - async fetchComponent (route) { + async fetchComponent (route, as) { let data = this.components[route] if (data) return data @@ -240,6 +246,15 @@ export default class Router extends EventEmitter { const jsonPageRes = await this.fetchRoute(route) const jsonData = await jsonPageRes.json() + + if (jsonData.buildIdMismatch) { + _notifyBuildIdMismatch(as) + + const error = Error('Abort due to BUILD_ID mismatch') + error.cancelled = true + throw error + } + const newData = { ...loadComponent(jsonData), jsonPageRes diff --git a/readme.md b/readme.md index f6604d22..4b6e27c1 100644 --- a/readme.md +++ b/readme.md @@ -310,6 +310,7 @@ Here's a list of supported events: - `routeChangeStart(url)` - Fires when a route starts to change - `routeChangeComplete(url)` - Fires when a route changed completely - `routeChangeError(err, url)` - Fires when there's an error when changing routes +- `appUpdated(nextRoute)` - Fires when switching pages and there's a new version of the app > Here `url` is the URL shown in the browser. If you call `Router.push(url, as)` (or similar), then the value of `url` will be `as`. @@ -337,6 +338,17 @@ Router.onRouteChangeError = (err, url) => { } ``` +If you change a route while in between a new deployment, we can't navigate the app via client side. We need to do a full browser navigation. We do it automatically for you. + +But you can customize that via `Route.onAppUpdated` event like this: + +```js +Router.onAppUpdated = (nextUrl) => { + // persist the local state + location.href = nextUrl +} +``` + ### Prefetching Pages

diff --git a/server/index.js b/server/index.js index dcaf6424..a11c5046 100644 --- a/server/index.js +++ b/server/index.js @@ -81,19 +81,30 @@ export default class Server { }, '/_next/:buildId/main.js': async (req, res, params) => { - this.handleBuildId(params.buildId, res) + if (!this.handleBuildId(params.buildId, res)) { + throwBuildIdMismatchError() + } + const p = join(this.dir, '.next/main.js') await this.serveStatic(req, res, p) }, '/_next/:buildId/commons.js': async (req, res, params) => { - this.handleBuildId(params.buildId, res) + if (!this.handleBuildId(params.buildId, res)) { + throwBuildIdMismatchError() + } + const p = join(this.dir, '.next/commons.js') await this.serveStatic(req, res, p) }, '/_next/:buildId/pages/:path*': async (req, res, params) => { - this.handleBuildId(params.buildId, res) + if (!this.handleBuildId(params.buildId, res)) { + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ buildIdMismatch: true })) + return + } + const paths = params.path || ['index'] const pathname = `/${paths.join('/')}` await this.renderJSON(req, res, pathname) @@ -277,14 +288,13 @@ export default class Server { } handleBuildId (buildId, res) { - if (this.dev) return + if (this.dev) return true if (buildId !== this.renderOpts.buildId) { - const errorMessage = 'Build id mismatch!' + - 'Seems like the server and the client version of files are not the same.' - throw new Error(errorMessage) + return false } res.setHeader('Cache-Control', 'max-age=365000000, immutable') + return true } getCompilationError (page) { @@ -298,3 +308,7 @@ export default class Server { if (p) return errors.get(p)[0] } } + +function throwBuildIdMismatchError () { + throw new Error('BUILD_ID Mismatched!') +} diff --git a/test/integration/basic/test/misc.js b/test/integration/basic/test/misc.js index 61c7f64e..29926790 100644 --- a/test/integration/basic/test/misc.js +++ b/test/integration/basic/test/misc.js @@ -1,6 +1,6 @@ /* global describe, test, expect */ -export default function ({ app }) { +export default function (context) { describe('Misc', () => { test('finishes response', async () => { const res = { @@ -9,7 +9,7 @@ export default function ({ app }) { this.finished = true } } - const html = await app.renderToHTML({}, res, '/finish-response', {}) + const html = await context.app.renderToHTML({}, res, '/finish-response', {}) expect(html).toBeFalsy() }) })