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:
parent
400b75658b
commit
2e2db37ccb
|
@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
"react": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"react-hot-loader": "3.0.0-beta.6",
|
||||
"resolve": "1.1.7",
|
||||
"send": "0.14.1",
|
||||
"strip-ansi": "3.0.1",
|
||||
"url": "0.11.0",
|
||||
"webpack": "1.13.2",
|
||||
"webpack-dev-server": "1.16.2",
|
||||
|
|
68
pages/_error-debug.js
Normal file
68
pages/_error-debug.js
Normal 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'
|
||||
}
|
||||
})
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
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]
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
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$/, '')
|
||||
}
|
||||
|
|
|
@ -1,14 +1,46 @@
|
|||
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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue