1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

add error page for debug

This commit is contained in:
nkzawa 2016-10-19 21:41:45 +09:00
parent 400b75658b
commit 2e2db37ccb
10 changed files with 250 additions and 77 deletions

View file

@ -7,13 +7,13 @@ import DefaultApp from '../lib/app'
import evalScript from '../lib/eval-script' import evalScript from '../lib/eval-script'
const { const {
__NEXT_DATA__: { app, component, props, classNames } __NEXT_DATA__: { app, component, props, classNames, err }
} = window } = window
const App = app ? evalScript(app).default : DefaultApp const App = app ? evalScript(app).default : DefaultApp
const Component = evalScript(component).default 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 headManager = new HeadManager()
const container = document.getElementById('__next') const container = document.getElementById('__next')

View file

@ -123,7 +123,10 @@ export default class Router {
const xhr = loadComponent(componentUrl, (err, data) => { const xhr = loadComponent(componentUrl, (err, data) => {
if (err) return reject(err) 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) => { return loadJSON(url, (err, data) => {
if (err) return fn(err) if (err) return fn(err)
const { component } = data
let module let module
try { try {
module = evalScript(component) module = evalScript(data.component)
} catch (err) { } catch (err) {
return fn(err) return fn(err)
} }
const Component = module.default || module const Component = module.default || module
fn(null, { Component }) fn(null, { Component, err: data.err })
}) })
} }

View file

@ -40,8 +40,8 @@
"react": "15.3.2", "react": "15.3.2",
"react-dom": "15.3.2", "react-dom": "15.3.2",
"react-hot-loader": "3.0.0-beta.6", "react-hot-loader": "3.0.0-beta.6",
"resolve": "1.1.7",
"send": "0.14.1", "send": "0.14.1",
"strip-ansi": "3.0.1",
"url": "0.11.0", "url": "0.11.0",
"webpack": "1.13.2", "webpack": "1.13.2",
"webpack-dev-server": "1.16.2", "webpack-dev-server": "1.16.2",

68
pages/_error-debug.js Normal file
View file

@ -0,0 +1,68 @@
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'
}
})

View file

@ -8,8 +8,8 @@ module.exports = function (content) {
return content + ` return content + `
if (module.hot) { if (module.hot) {
module.hot.accept() module.hot.accept()
if ('idle' !== module.hot.status()) { if (module.hot.status() !== 'idle') {
const Component = module.exports.default || module.exports var Component = module.exports.default || module.exports
next.router.update('${route}', Component) next.router.update('${route}', Component)
} }
} }

View file

@ -14,10 +14,16 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
entry[join('bundles', p)] = defaultEntries.concat(['./' + p]) entry[join('bundles', p)] = defaultEntries.concat(['./' + p])
} }
const nextPagesDir = resolve(__dirname, '..', '..', 'pages')
const errorEntry = join('bundles', 'pages', '_error.js') 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 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 nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules')
const plugins = [ const plugins = [
@ -39,21 +45,21 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
const loaders = [{ const loaders = [{
test: /\.js$/, test: /\.js$/,
loader: 'emit-file-loader', loader: 'emit-file-loader',
include: [ include: [dir, nextPagesDir],
dir,
resolve(__dirname, '..', '..', 'pages')
],
exclude: /node_modules/, exclude: /node_modules/,
query: { query: {
name: 'dist/[path][name].[ext]' name: 'dist/[path][name].[ext]'
} }
}, { }]
.concat(hotReload ? [{
test: /\.js$/,
loader: 'hot-self-accept-loader',
include: resolve(dir, 'pages')
}] : [])
.concat([{
test: /\.js$/, test: /\.js$/,
loader: 'babel', loader: 'babel',
include: [ include: [dir, nextPagesDir],
dir,
resolve(__dirname, '..', '..', 'pages')
],
exclude: /node_modules/, exclude: /node_modules/,
query: { query: {
presets: ['es2015', 'react'], presets: ['es2015', 'react'],
@ -74,12 +80,12 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
] ]
] ]
} }
}] }])
.concat(hotReload ? [{
test: /\.js$/, const interpolateNames = new Map([
loader: 'hot-self-accept-loader', [defaultErrorPath, 'dist/pages/_error.js'],
include: resolve(dir, 'pages') [errorDebugPath, 'dist/pages/_error-debug.js']
}] : []) ])
return webpack({ return webpack({
context: dir, context: dir,
@ -120,10 +126,7 @@ export default async function createCompiler (dir, { hotReload = false } = {}) {
loaders loaders
}, },
customInterpolateName: function (url, name, opts) { customInterpolateName: function (url, name, opts) {
if (defaultErrorPath === this.resourcePath) { return interpolateNames.get(this.resourcePath) || url
return 'dist/pages/_error.js'
}
return url
} }
}) })
} }

View file

@ -1,3 +1,4 @@
import { join } from 'path'
import WebpackDevServer from 'webpack-dev-server' import WebpackDevServer from 'webpack-dev-server'
import webpack from './build/webpack' import webpack from './build/webpack'
import read from './read' import read from './read'
@ -6,11 +7,13 @@ export default class HotReloader {
constructor (dir) { constructor (dir) {
this.dir = dir this.dir = dir
this.server = null this.server = null
this.stats = null
this.compilationErrors = null
} }
async start () { async start () {
await this.prepareServer() await this.prepareServer()
await this.waitBuild() this.stats = await this.waitUntilValid()
await this.listen() await this.listen()
} }
@ -20,14 +23,16 @@ export default class HotReloader {
compiler.plugin('after-emit', (compilation, callback) => { compiler.plugin('after-emit', (compilation, callback) => {
const { assets } = compilation const { assets } = compilation
for (const f of Object.keys(assets)) { for (const f of Object.keys(assets)) {
const source = assets[f] deleteCache(assets[f].existsAt)
// delete updated file caches
delete require.cache[source.existsAt]
delete read.cache[source.existsAt]
} }
callback() callback()
}) })
compiler.plugin('done', (stats) => {
this.stats = stats
this.compilationErrors = null
})
this.server = new WebpackDevServer(compiler, { this.server = new WebpackDevServer(compiler, {
publicPath: '/', publicPath: '/',
hot: true, hot: true,
@ -52,18 +57,10 @@ export default class HotReloader {
}) })
} }
async waitBuild () { waitUntilValid () {
const stats = await new Promise((resolve) => { return new Promise((resolve) => {
this.server.middleware.waitUntilValid(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 () { 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 () { get fileSystem () {
return this.server.middleware.fileSystem return this.server.middleware.fileSystem
} }
} }
function deleteCache (path) {
delete require.cache[path]
delete read.cache[path]
}

View file

@ -1,9 +1,11 @@
import http from 'http' import http from 'http'
import { resolve } from 'path' import { resolve, join } from 'path'
import { parse } from 'url'
import send from 'send' import send from 'send'
import Router from './router' import Router from './router'
import { render, renderJSON } from './render' import { render, renderJSON, errorToJSON } from './render'
import HotReloader from './hot-reloader' import HotReloader from './hot-reloader'
import { resolveFromList } from './resolve'
export default class Server { export default class Server {
constructor ({ dir = '.', dev = false, hotReload = false }) { constructor ({ dir = '.', dev = false, hotReload = false }) {
@ -68,17 +70,27 @@ export default class Server {
async render (req, res) { async render (req, res) {
const { dir, dev } = this const { dir, dev } = this
const ctx = { req, res }
const opts = { dir, dev }
let html let html
try {
html = await render(req.url, { req, res }, { dir, dev }) const err = this.getCompilationError(req.url)
} catch (err) { if (err) {
if (err.code === 'ENOENT') { res.statusCode = 500
res.statusCode = 404 html = await render('/_error-debug', { ...ctx, err }, opts)
} else { } else {
console.error(err) try {
res.statusCode = 500 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) sendHTML(res, html)
@ -86,17 +98,27 @@ export default class Server {
async renderJSON (req, res) { async renderJSON (req, res) {
const { dir } = this const { dir } = this
const opts = { dir }
let json let json
try {
json = await renderJSON(req.url, { dir }) const err = this.getCompilationError(req.url)
} catch (err) { if (err) {
if (err.code === 'ENOENT') { res.statusCode = 500
res.statusCode = 404 json = await renderJSON('/_error-debug.json', opts)
} else { json = { ...json, err: errorToJSON(err) }
console.error(err) } else {
res.statusCode = 500 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) const data = JSON.stringify(json)
@ -127,6 +149,18 @@ export default class Server {
.on('finish', resolve) .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) { function sendHTML (res, html) {

View file

@ -41,7 +41,8 @@ export async function render (url, ctx = {}, {
data: { data: {
component, component,
props, props,
classNames: css.renderedClassNames classNames: css.renderedClassNames,
err: ctx.err ? errorToJSON(ctx.err) : null
}, },
hotReload: false, hotReload: false,
dev, dev,
@ -57,6 +58,19 @@ export async function renderJSON (url, { dir = process.cwd() } = {}) {
return { component } 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) { function getPath (url) {
return parse(url || '/').pathname.slice(1).replace(/\.json$/, '') return parse(url || '/').pathname.slice(1).replace(/\.json$/, '')
} }

View file

@ -1,14 +1,46 @@
import _resolve from 'resolve' import { join, sep } from 'path'
import fs from 'mz/fs'
export default function resolve (id, opts) { export default async function resolve (id) {
return new Promise((resolve, reject) => { const paths = getPaths(id)
_resolve(id, opts, (err, path) => { for (const p of paths) {
if (err) { if (await isFile(p)) {
err.code = 'ENOENT' return p
return reject(err) }
} }
resolve(path)
}) 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()
}