mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Hot reload error page (#190)
* add detach-plugin * detach-plugin: remove unused property * watch-pages-plugin: replace _error.js when user defined one was added/removed * dynamic-entry-plugin: delete cache * fix HMR settings for _error.js * render: pass error only on dev * hot-reload: enable to hot-reload error page * server: check if /_error has compilation errors * webapck-dev-client: fix reloading /_error
This commit is contained in:
parent
7186fe8b7e
commit
e775721f34
|
@ -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 () {
|
||||
|
|
|
@ -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$/, '')
|
||||
}
|
||||
|
|
73
server/build/plugins/detach-plugin.js
Normal file
73
server/build/plugins/detach-plugin.js
Normal file
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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$/,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue