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'
|
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')
|
||||||
|
|
|
@ -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 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
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 + `
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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$/, '')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue