提交 2e2db37c 编写于 作者: N nkzawa

add error page for debug

上级 400b7565
......@@ -7,13 +7,13 @@ import DefaultApp from '../lib/app'
import evalScript from '../lib/eval-script'
const {
__NEXT_DATA__: { app, component, props, classNames }
__NEXT_DATA__: { app, component, props, classNames, err }
} = window
const App = app ? evalScript(app).default : DefaultApp
const Component = evalScript(component).default
export const router = new Router(window.location.href, { Component })
export const router = new Router(window.location.href, { Component, ctx: { err } })
const headManager = new HeadManager()
const container = document.getElementById('__next')
......
......@@ -123,7 +123,10 @@ export default class Router {
const xhr = loadComponent(componentUrl, (err, data) => {
if (err) return reject(err)
resolve({ ...data, ctx: { xhr } })
resolve({
Component: data.Component,
ctx: { xhr, err: data.err }
})
})
})
......@@ -189,17 +192,15 @@ function loadComponent (url, fn) {
return loadJSON(url, (err, data) => {
if (err) return fn(err)
const { component } = data
let module
try {
module = evalScript(component)
module = evalScript(data.component)
} catch (err) {
return fn(err)
}
const Component = module.default || module
fn(null, { Component })
fn(null, { Component, err: data.err })
})
}
......
import React from 'react'
import stripAnsi from 'strip-ansi'
import Head from 'next/head'
import { css, StyleSheet } from 'next/css'
export default class ErrorDebug extends React.Component {
static getInitialProps ({ err }) {
const { message, module } = err
return { message, path: module.rawRequest }
}
render () {
const { message, path } = this.props
return <div className={css(styles.errorDebug)}>
<Head>
<style dangerouslySetInnerHTML={{ __html: `
body {
background: #dc0067;
margin: 0;
}
`}} />
</Head>
<div className={css(styles.heading)}>Error in {path}</div>
<pre className={css(styles.message)}>{stripAnsi(message)}</pre>
</div>
}
}
const styles = StyleSheet.create({
body: {
background: '#dc0067',
margin: 0
},
errorDebug: {
height: '100%',
padding: '16px',
boxSizing: 'border-box'
},
message: {
fontFamily: 'menlo-regular',
fontSize: '10px',
color: '#fff',
margin: 0
},
heading: {
fontFamily: 'sans-serif',
fontSize: '13px',
fontWeight: 'bold',
color: '#ff90c6',
marginBottom: '20px'
},
token: {
backgroundColor: '#000'
},
marker: {
color: '#000'
},
dim: {
color: '#e85b9b'
}
})
......@@ -8,8 +8,8 @@ module.exports = function (content) {
return content + `
if (module.hot) {
module.hot.accept()
if ('idle' !== module.hot.status()) {
const Component = module.exports.default || module.exports
if (module.hot.status() !== 'idle') {
var Component = module.exports.default || module.exports
next.router.update('${route}', Component)
}
}
......
......@@ -14,10 +14,16 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
entry[join('bundles', p)] = defaultEntries.concat(['./' + p])
}
const nextPagesDir = resolve(__dirname, '..', '..', 'pages')
const errorEntry = join('bundles', 'pages', '_error.js')
const defaultErrorPath = resolve(__dirname, '..', '..', 'pages', '_error.js')
const defaultErrorPath = resolve(nextPagesDir, '_error.js')
if (!entry[errorEntry]) entry[errorEntry] = defaultErrorPath
const errorDebugEntry = join('bundles', 'pages', '_error-debug.js')
const errorDebugPath = resolve(nextPagesDir, '_error-debug.js')
entry[errorDebugEntry] = errorDebugPath
const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules')
const plugins = [
......@@ -39,21 +45,21 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
const loaders = [{
test: /\.js$/,
loader: 'emit-file-loader',
include: [
dir,
resolve(__dirname, '..', '..', 'pages')
],
include: [dir, nextPagesDir],
exclude: /node_modules/,
query: {
name: 'dist/[path][name].[ext]'
}
}, {
}]
.concat(hotReload ? [{
test: /\.js$/,
loader: 'hot-self-accept-loader',
include: resolve(dir, 'pages')
}] : [])
.concat([{
test: /\.js$/,
loader: 'babel',
include: [
dir,
resolve(__dirname, '..', '..', 'pages')
],
include: [dir, nextPagesDir],
exclude: /node_modules/,
query: {
presets: ['es2015', 'react'],
......@@ -74,12 +80,12 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
]
]
}
}]
.concat(hotReload ? [{
test: /\.js$/,
loader: 'hot-self-accept-loader',
include: resolve(dir, 'pages')
}] : [])
}])
const interpolateNames = new Map([
[defaultErrorPath, 'dist/pages/_error.js'],
[errorDebugPath, 'dist/pages/_error-debug.js']
])
return webpack({
context: dir,
......@@ -120,10 +126,7 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
loaders
},
customInterpolateName: function (url, name, opts) {
if (defaultErrorPath === this.resourcePath) {
return 'dist/pages/_error.js'
}
return url
return interpolateNames.get(this.resourcePath) || url
}
})
}
import { join } from 'path'
import WebpackDevServer from 'webpack-dev-server'
import webpack from './build/webpack'
import read from './read'
......@@ -6,11 +7,13 @@ export default class HotReloader {
constructor (dir) {
this.dir = dir
this.server = null
this.stats = null
this.compilationErrors = null
}
async start () {
await this.prepareServer()
await this.waitBuild()
this.stats = await this.waitUntilValid()
await this.listen()
}
......@@ -20,14 +23,16 @@ export default class HotReloader {
compiler.plugin('after-emit', (compilation, callback) => {
const { assets } = compilation
for (const f of Object.keys(assets)) {
const source = assets[f]
// delete updated file caches
delete require.cache[source.existsAt]
delete read.cache[source.existsAt]
deleteCache(assets[f].existsAt)
}
callback()
})
compiler.plugin('done', (stats) => {
this.stats = stats
this.compilationErrors = null
})
this.server = new WebpackDevServer(compiler, {
publicPath: '/',
hot: true,
......@@ -52,18 +57,10 @@ export default class HotReloader {
})
}
async waitBuild () {
const stats = await new Promise((resolve) => {
waitUntilValid () {
return new Promise((resolve) => {
this.server.middleware.waitUntilValid(resolve)
})
const jsonStats = stats.toJson()
if (jsonStats.errors.length > 0) {
const err = new Error(jsonStats.errors[0])
err.errors = jsonStats.errors
err.warnings = jsonStats.warnings
throw err
}
}
listen () {
......@@ -75,7 +72,31 @@ export default class HotReloader {
})
}
getCompilationErrors () {
if (!this.compilationErrors) {
this.compilationErrors = new Map()
if (this.stats.hasErrors()) {
const entries = this.stats.compilation.entries
.filter((e) => e.context === this.dir)
.filter((e) => !!e.errors.length || !!e.dependenciesErrors.length)
for (const e of entries) {
const path = join(e.context, '.next', e.name)
const errors = e.errors.concat(e.dependenciesErrors)
this.compilationErrors.set(path, errors)
}
}
}
return this.compilationErrors
}
get fileSystem () {
return this.server.middleware.fileSystem
}
}
function deleteCache (path) {
delete require.cache[path]
delete read.cache[path]
}
import http from 'http'
import { resolve } from 'path'
import { resolve, join } from 'path'
import { parse } from 'url'
import send from 'send'
import Router from './router'
import { render, renderJSON } from './render'
import { render, renderJSON, errorToJSON } from './render'
import HotReloader from './hot-reloader'
import { resolveFromList } from './resolve'
export default class Server {
constructor ({ dir = '.', dev = false, hotReload = false }) {
......@@ -68,17 +70,27 @@ export default class Server {
async render (req, res) {
const { dir, dev } = this
const ctx = { req, res }
const opts = { dir, dev }
let html
try {
html = await render(req.url, { req, res }, { dir, dev })
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404
} else {
console.error(err)
res.statusCode = 500
const err = this.getCompilationError(req.url)
if (err) {
res.statusCode = 500
html = await render('/_error-debug', { ...ctx, err }, opts)
} else {
try {
html = await render(req.url, ctx, opts)
} catch (err) {
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', { req, res, err }, { dir, dev })
}
sendHTML(res, html)
......@@ -86,17 +98,27 @@ export default class Server {
async renderJSON (req, res) {
const { dir } = this
const opts = { dir }
let json
try {
json = await renderJSON(req.url, { dir })
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404
} else {
console.error(err)
res.statusCode = 500
const err = this.getCompilationError(req.url)
if (err) {
res.statusCode = 500
json = await renderJSON('/_error-debug.json', opts)
json = { ...json, err: errorToJSON(err) }
} else {
try {
json = await renderJSON(req.url, opts)
} catch (err) {
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', { dir })
}
const data = JSON.stringify(json)
......@@ -127,6 +149,18 @@ export default class Server {
.on('finish', resolve)
})
}
getCompilationError (url) {
if (!this.hotReloader) return
const errors = this.hotReloader.getCompilationErrors()
if (!errors.size) return
const p = parse(url || '/').pathname.replace(/\.json$/, '')
const id = join(this.dir, '.next', 'bundles', 'pages', p)
const path = resolveFromList(id, errors.keys())
if (path) return errors.get(path)[0]
}
}
function sendHTML (res, html) {
......
......@@ -41,7 +41,8 @@ export async function render (url, ctx = {}, {
data: {
component,
props,
classNames: css.renderedClassNames
classNames: css.renderedClassNames,
err: ctx.err ? errorToJSON(ctx.err) : null
},
hotReload: false,
dev,
......@@ -57,6 +58,19 @@ export async function renderJSON (url, { dir = process.cwd() } = {}) {
return { component }
}
export function errorToJSON (err) {
const { name, message, stack } = err
const json = { name, message, stack }
if (name === 'ModuleBuildError') {
// webpack compilation error
const { module: { rawRequest } } = err
json.module = { rawRequest }
}
return json
}
function getPath (url) {
return parse(url || '/').pathname.slice(1).replace(/\.json$/, '')
}
import _resolve from 'resolve'
import { join, sep } from 'path'
import fs from 'mz/fs'
export default function resolve (id, opts) {
return new Promise((resolve, reject) => {
_resolve(id, opts, (err, path) => {
if (err) {
err.code = 'ENOENT'
return reject(err)
}
resolve(path)
})
})
export default async function resolve (id) {
const paths = getPaths(id)
for (const p of paths) {
if (await isFile(p)) {
return p
}
}
const err = new Error(`Cannot find module ${id}`)
err.code = 'ENOENT'
throw err
}
export function resolveFromList (id, files) {
const paths = getPaths(id)
const set = new Set(files)
for (const p of paths) {
if (set.has(p)) return p
}
}
function getPaths (id) {
const i = sep === '/' ? id : id.replace(/\//g, sep)
if (i.slice(-3) === '.js') return [i]
if (i[i.length - 1] === sep) return [i + 'index.js']
return [
i + '.js',
join(i, 'index.js')
]
}
async function isFile (p) {
let stat
try {
stat = await fs.stat(p)
} catch (err) {
if (err.code === 'ENOENT') return false
throw err
}
return stat.isFile() || stat.isFIFO()
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册