提交 e775721f 编写于 作者: N Naoyuki Kanezawa 提交者: GitHub

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
上级 7186fe8b
......@@ -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$/, '')
}
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())
}
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,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册