diff --git a/client/webpack-dev-client/index.js b/client/webpack-dev-client/index.js index 6bf4cdbb4bc1046c5ea069454cd7e48734860212..94c465640af02f5d1d3a566802b02e6713b25fc1 100644 --- a/client/webpack-dev-client/index.js +++ b/client/webpack-dev-client/index.js @@ -107,6 +107,18 @@ const onSocketMsg = { } }, reload (route) { + if (route === '/_error') { + for (const r of Object.keys(next.router.components)) { + const { Component } = next.router.components[r] + if (Component.__route === '/_error-debug') { + // reload all '/_error-debug' + // which are expected to be errors of '/_error' routes + next.router.reload(r) + } + } + return + } + next.router.reload(route) }, close () { diff --git a/server/build/loaders/hot-self-accept-loader.js b/server/build/loaders/hot-self-accept-loader.js index 38a6b8978f6f2ff5160d94059249aba2afdc3b9b..e5ed1d1b72dac27b888e0ffda71061dc62b3756c 100644 --- a/server/build/loaders/hot-self-accept-loader.js +++ b/server/build/loaders/hot-self-accept-loader.js @@ -5,19 +5,34 @@ module.exports = function (content) { const route = getRoute(this) - return content + ` + return `${content} if (module.hot) { module.hot.accept() + + var Component = module.exports.default || module.exports + Component.__route = ${JSON.stringify(route)} + if (module.hot.status() !== 'idle') { - var Component = module.exports.default || module.exports - next.router.update('${route}', Component) + var components = next.router.components + for (var r in components) { + if (!components.hasOwnProperty(r)) continue + + if (components[r].Component.__route === ${JSON.stringify(route)}) { + next.router.update(r, Component) + } + } } } ` } +const nextPagesDir = resolve(__dirname, '..', '..', '..', 'pages') + function getRoute (loaderContext) { const pagesDir = resolve(loaderContext.options.context, 'pages') - const path = loaderContext.resourcePath - return '/' + relative(pagesDir, path).replace(/((^|\/)index)?\.js$/, '') + const { resourcePath } = loaderContext + const dir = [pagesDir, nextPagesDir] + .find((d) => resourcePath.indexOf(d) === 0) + const path = relative(dir, resourcePath) + return '/' + path.replace(/((^|\/)index)?\.js$/, '') } diff --git a/server/build/plugins/detach-plugin.js b/server/build/plugins/detach-plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..dd2b41fb8111a633a08009ca10e2699b929abced --- /dev/null +++ b/server/build/plugins/detach-plugin.js @@ -0,0 +1,73 @@ + +export default class DetachPlugin { + apply (compiler) { + compiler.pluginDetachFns = new Map() + compiler.plugin = plugin(compiler.plugin) + compiler.apply = apply + compiler.detach = detach + compiler.getDetachablePlugins = getDetachablePlugins + } +} + +export function detachable (Plugin) { + const { apply } = Plugin.prototype + + Plugin.prototype.apply = function (compiler) { + const fns = [] + + const { plugin } = compiler + compiler.plugin = function (name, fn) { + fns.push(plugin.call(this, name, fn)) + } + + // collect the result of `plugin` call in `apply` + apply.call(this, compiler) + + compiler.plugin = plugin + + return fns + } +} + +function plugin (original) { + return function (name, fn) { + original.call(this, name, fn) + + return () => { + const names = Array.isArray(name) ? name : [name] + for (const n of names) { + const plugins = this._plugins[n] || [] + const i = plugins.indexOf(fn) + if (i >= 0) plugins.splice(i, 1) + } + } + } +} + +function apply (...plugins) { + for (const p of plugins) { + const fn = p.apply(this) + if (!fn) continue + + const fns = this.pluginDetachFns.get(p) || new Set() + + const _fns = Array.isArray(fn) ? fn : [fn] + for (const f of _fns) fns.add(f) + + this.pluginDetachFns.set(p, fns) + } +} + +function detach (...plugins) { + for (const p of plugins) { + const fns = this.pluginDetachFns.get(p) || new Set() + for (const fn of fns) { + if (typeof fn === 'function') fn() + } + this.pluginDetachFns.delete(p) + } +} + +function getDetachablePlugins () { + return new Set(this.pluginDetachFns.keys()) +} diff --git a/server/build/plugins/dynamic-entry-plugin.js b/server/build/plugins/dynamic-entry-plugin.js index 8e2c6b5dfbd9397a91d206cb4362cab3c3555748..6ff2cb74cffd8c768e1a067127b8c95fb9e863a3 100644 --- a/server/build/plugins/dynamic-entry-plugin.js +++ b/server/build/plugins/dynamic-entry-plugin.js @@ -1,5 +1,9 @@ import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin' import MultiEntryPlugin from 'webpack/lib/MultiEntryPlugin' +import { detachable } from './detach-plugin' + +detachable(SingleEntryPlugin) +detachable(MultiEntryPlugin) export default class DynamicEntryPlugin { apply (compiler) { @@ -8,8 +12,9 @@ export default class DynamicEntryPlugin { compiler.removeEntry = removeEntry compiler.hasEntry = hasEntry - compiler.plugin('compilation', (compilation) => { - compilation.addEntry = compilationAddEntry(compilation.addEntry) + compiler.plugin('emit', (compilation, callback) => { + compiler.cache = compilation.cache + callback() }) } } @@ -37,21 +42,27 @@ function addEntry (entry, name = 'main') { } function removeEntry (name = 'main') { + for (const p of this.getDetachablePlugins()) { + if (!(p instanceof SingleEntryPlugin || p instanceof MultiEntryPlugin)) continue + if (p.name !== name) continue + + if (this.cache) { + for (const id of Object.keys(this.cache)) { + const m = this.cache[id] + if (m.name === name) { + // cache of `MultiModule` is based on `name`, + // so delete it here for the case + // a new entry is added with the same name later + delete this.cache[id] + } + } + } + + this.detach(p) + } this.entryNames.delete(name) } function hasEntry (name = 'main') { return this.entryNames.has(name) } - -function compilationAddEntry (original) { - return function (context, entry, name, callback) { - if (!this.compiler.entryNames.has(name)) { - // skip removed entry - callback() - return - } - - return original.call(this, context, entry, name, callback) - } -} diff --git a/server/build/plugins/watch-pages-plugin.js b/server/build/plugins/watch-pages-plugin.js index 1d9df3209d4d34d8e892c4373814540a1b31a558..2eb1af9e90bd30c9216d4551d6a24ae67ebe3050 100644 --- a/server/build/plugins/watch-pages-plugin.js +++ b/server/build/plugins/watch-pages-plugin.js @@ -3,6 +3,7 @@ import { resolve, relative, join, extname } from 'path' export default class WatchPagesPlugin { constructor (dir) { this.dir = resolve(dir, 'pages') + this.prevFileDependencies = null } apply (compiler) { @@ -11,6 +12,8 @@ export default class WatchPagesPlugin { compilation.contextDependencies = compilation.contextDependencies.concat([this.dir]) + this.prevFileDependencies = compilation.fileDependencies + callback() }) @@ -18,12 +21,18 @@ export default class WatchPagesPlugin { const getEntryName = (f) => { return join('bundles', relative(compiler.options.context, f)) } + const errorPageName = join('bundles', 'pages', '_error.js') compiler.plugin('watch-run', (watching, callback) => { Object.keys(compiler.fileTimestamps) .filter(isPageFile) + .filter((f) => this.prevFileDependencies.indexOf(f) < 0) .forEach((f) => { const name = getEntryName(f) + if (name === errorPageName) { + compiler.removeEntry(name) + } + if (compiler.hasEntry(name)) return const entries = ['webpack/hot/dev-server', f] @@ -35,6 +44,13 @@ export default class WatchPagesPlugin { .forEach((f) => { const name = getEntryName(f) compiler.removeEntry(name) + + if (name === errorPageName) { + compiler.addEntry([ + 'webpack/hot/dev-server', + join(__dirname, '..', '..', '..', 'pages', '_error.js') + ], name) + } }) callback() diff --git a/server/build/webpack.js b/server/build/webpack.js index a7f9bb625a0d2849bbe69bd0a2857cea8ff03dc4..a3be55d60dcb6ea5282be9652eae5137047fb9b6 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -6,6 +6,7 @@ import UnlinkFilePlugin from './plugins/unlink-file-plugin' import WatchPagesPlugin from './plugins/watch-pages-plugin' import WatchRemoveEventPlugin from './plugins/watch-remove-event-plugin' import DynamicEntryPlugin from './plugins/dynamic-entry-plugin' +import DetachPlugin from './plugins/detach-plugin' export default async function createCompiler (dir, { hotReload = false } = {}) { dir = resolve(dir) @@ -22,7 +23,9 @@ export default async function createCompiler (dir, { hotReload = false } = {}) { const errorEntry = join('bundles', 'pages', '_error.js') const defaultErrorPath = join(nextPagesDir, '_error.js') - if (!entry[errorEntry]) entry[errorEntry] = defaultErrorPath + if (!entry[errorEntry]) { + entry[errorEntry] = defaultEntries.concat([defaultErrorPath]) + } const errorDebugEntry = join('bundles', 'pages', '_error-debug.js') const errorDebugPath = join(nextPagesDir, '_error-debug.js') @@ -42,6 +45,7 @@ export default async function createCompiler (dir, { hotReload = false } = {}) { }) ].concat(hotReload ? [ new webpack.HotModuleReplacementPlugin(), + new DetachPlugin(), new DynamicEntryPlugin(), new UnlinkFilePlugin(), new WatchRemoveEventPlugin(), @@ -70,7 +74,10 @@ export default async function createCompiler (dir, { hotReload = false } = {}) { .concat(hotReload ? [{ test: /\.js$/, loader: 'hot-self-accept-loader', - include: join(dir, 'pages') + include: [ + join(dir, 'pages'), + nextPagesDir + ] }] : []) .concat([{ test: /\.js$/, diff --git a/server/index.js b/server/index.js index 5463d4b4e9a3fde89d7f998eaf368e5a15e7458d..41a234d6fb782ec4b095b3fa41e84fecc9c4883f 100644 --- a/server/index.js +++ b/server/index.js @@ -83,13 +83,19 @@ export default class Server { try { html = await render(req.url, ctx, opts) } catch (err) { - if (err.code === 'ENOENT') { - res.statusCode = 404 - } else { - console.error(err) + const _err = this.getCompilationError('/_error') + if (_err) { res.statusCode = 500 + html = await render('/_error-debug', { ...ctx, err: _err }, opts) + } else { + if (err.code === 'ENOENT') { + res.statusCode = 404 + } else { + console.error(err) + res.statusCode = 500 + } + html = await render('/_error', { ...ctx, err }, opts) } - html = await render('/_error', { ...ctx, err }, opts) } } @@ -111,13 +117,20 @@ export default class Server { try { json = await renderJSON(req.url, opts) } catch (err) { - if (err.code === 'ENOENT') { - res.statusCode = 404 - } else { - console.error(err) + const _err = this.getCompilationError('/_error.json') + if (_err) { res.statusCode = 500 + json = await renderJSON('/_error-debug.json', opts) + json = { ...json, err: errorToJSON(_err) } + } else { + if (err.code === 'ENOENT') { + res.statusCode = 404 + } else { + console.error(err) + res.statusCode = 500 + } + json = await renderJSON('/_error.json', opts) } - json = await renderJSON('/_error.json', opts) } } @@ -129,9 +142,19 @@ export default class Server { async render404 (req, res) { const { dir, dev } = this + const opts = { dir, dev } + + let html + + const err = this.getCompilationError('/_error') + if (err) { + res.statusCode = 500 + html = await render('/_error-debug', { req, res, err }, opts) + } else { + res.statusCode = 404 + html = await render('/_error', { req, res }, opts) + } - res.statusCode = 404 - const html = await render('/_error', { req, res }, { dir, dev }) sendHTML(res, html) } diff --git a/server/render.js b/server/render.js index f06c012cc623e8a5ea88c497a9d4c47a27aaa8d8..777a9f35ac9d89ecff7f33c0451a3d5bf273d4e0 100644 --- a/server/render.js +++ b/server/render.js @@ -44,7 +44,7 @@ export async function render (url, ctx = {}, { component, props, ids: ids, - err: ctx.err ? errorToJSON(ctx.err) : null + err: (ctx.err && dev) ? errorToJSON(ctx.err) : null }, dev, staticMarkup,