Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions client/webpack-dev-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
25 changes: 20 additions & 5 deletions server/build/loaders/hot-self-accept-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 73 additions & 0 deletions server/build/plugins/detach-plugin.js
Original file line number Diff line number Diff line change
@@ -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())
}
39 changes: 25 additions & 14 deletions server/build/plugins/dynamic-entry-plugin.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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()
})
}
}
Expand Down Expand Up @@ -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)
}
}
16 changes: 16 additions & 0 deletions server/build/plugins/watch-pages-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -11,19 +12,27 @@ export default class WatchPagesPlugin {
compilation.contextDependencies =
compilation.contextDependencies.concat([this.dir])

this.prevFileDependencies = compilation.fileDependencies

callback()
})

const isPageFile = this.isPageFile.bind(this)
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]
Expand All @@ -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()
Expand Down
11 changes: 9 additions & 2 deletions server/build/webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
Expand All @@ -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(),
Expand Down Expand Up @@ -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$/,
Expand Down
47 changes: 35 additions & 12 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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)
}
}

Expand All @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down