diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000000000..3c1fab58db77b --- /dev/null +++ b/client/index.js @@ -0,0 +1,77 @@ +import { createElement } from 'react' +import ReactDOM from 'react-dom' +import HeadManager from './head-manager' +import { rehydrate } from '../lib/css' +import { createRouter } from '../lib/router' +import App from '../lib/app' +import evalScript from '../lib/eval-script' + +const { + __NEXT_DATA__: { + component, + errorComponent, + props, + ids, + err, + pathname, + query + } +} = window + +const Component = evalScript(component).default +const ErrorComponent = evalScript(errorComponent).default +let lastAppProps + +export const router = createRouter(pathname, query, { + Component, + ErrorComponent, + err +}) + +const headManager = new HeadManager() +const container = document.getElementById('__next') + +export default (onError) => { + if (ids && ids.length) rehydrate(ids) + + router.subscribe(({ Component, props, err }) => { + render({ Component, props, err }, onError) + }) + + render({ Component, props, err }, onError) +} + +export async function render (props, onError = renderErrorComponent) { + try { + await doRender(props) + } catch (err) { + await onError(err) + } +} + +async function renderErrorComponent (err) { + const { pathname, query } = router + const props = await getInitialProps(ErrorComponent, { err, pathname, query }) + await doRender({ Component: ErrorComponent, props, err }) +} + +async function doRender ({ Component, props, err }) { + if (!props && Component && + Component !== ErrorComponent && + lastAppProps.Component === ErrorComponent) { + // fetch props if ErrorComponent was replaced with a page component by HMR + const { pathname, query } = router + props = await getInitialProps(Component, { err, pathname, query }) + } + + Component = Component || lastAppProps.Component + props = props || lastAppProps.props + + const appProps = { Component, props, err, router, headManager } + lastAppProps = appProps + ReactDOM.render(createElement(App, appProps), container) +} + +function getInitialProps (Component, ctx) { + return Component.getInitialProps ? Component.getInitialProps(ctx) : {} +} diff --git a/client/next-dev.js b/client/next-dev.js index 8453ebabd9d43..2ae0ba06f9bb3 100644 --- a/client/next-dev.js +++ b/client/next-dev.js @@ -3,10 +3,20 @@ import patch from './patch-react' // apply patch first patch((err) => { console.error(err) - next.renderError(err) + + Promise.resolve().then(() => { + onError(err) + }) }) require('react-hot-loader/patch') -const next = require('./next') -window.next = next +const next = window.next = require('./') + +next.default(onError) + +function onError (err) { + // just show the debug screen but don't render ErrorComponent + // so that the current component doesn't lose props + next.render({ err }) +} diff --git a/client/next.js b/client/next.js index a1897675e5b27..5400a409161c3 100644 --- a/client/next.js +++ b/client/next.js @@ -1,59 +1,3 @@ -import { createElement } from 'react' -import ReactDOM from 'react-dom' -import HeadManager from './head-manager' -import { rehydrate } from '../lib/css' -import { createRouter } from '../lib/router' -import App from '../lib/app' -import evalScript from '../lib/eval-script' +import next from './' -const { - __NEXT_DATA__: { - component, - errorComponent, - props, - ids, - err, - pathname, - query - } -} = window - -const Component = evalScript(component).default -const ErrorComponent = evalScript(errorComponent).default - -export const router = createRouter(pathname, query, { - Component, - ErrorComponent, - ctx: { err } -}) - -const headManager = new HeadManager() -const container = document.getElementById('__next') -const defaultProps = { Component, ErrorComponent, props, router, headManager } - -if (ids && ids.length) rehydrate(ids) - -render() - -export function render (props = {}) { - try { - doRender(props) - } catch (err) { - renderError(err) - } -} - -export async function renderError (err) { - const { pathname, query } = router - const props = await ErrorComponent.getInitialProps({ err, pathname, query }) - try { - doRender({ Component: ErrorComponent, props }) - } catch (err2) { - console.error(err2) - } -} - -function doRender (props) { - const appProps = { ...defaultProps, ...props } - ReactDOM.render(createElement(App, appProps), container) -} +next() diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js index adfe4081a7929..7dfc49492c12b 100644 --- a/client/webpack-hot-middleware-client.js +++ b/client/webpack-hot-middleware-client.js @@ -5,9 +5,9 @@ const handlers = { reload (route) { if (route === '/_error') { for (const r of Object.keys(Router.components)) { - const { Component } = Router.components[r] - if (Component.__route === '/_error-debug') { - // reload all '/_error-debug' + const { err } = Router.components[r] + if (err) { + // reload all error routes // which are expected to be errors of '/_error' routes Router.reload(r) } @@ -29,8 +29,8 @@ const handlers = { return } - const { Component } = Router.components[route] || {} - if (Component && Component.__route === '/_error-debug') { + const { err } = Router.components[route] || {} + if (err) { // reload to recover from runtime errors Router.reload(route) } diff --git a/lib/app.js b/lib/app.js index 5367ea05a619d..02864563a1fa0 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,83 +1,53 @@ import React, { Component, PropTypes } from 'react' import { AppContainer } from 'react-hot-loader' +import shallowEquals from './shallow-equals' import { warn } from './utils' +const ErrorDebug = process.env.NODE_ENV === 'production' + ? null : require('./error-debug').default + export default class App extends Component { static childContextTypes = { - router: PropTypes.object, headManager: PropTypes.object } - constructor (props) { - super(props) - this.state = propsToState(props) - this.close = null - } - - componentWillReceiveProps (nextProps) { - const state = propsToState(nextProps) - try { - this.setState(state) - } catch (err) { - this.handleError(err) - } + getChildContext () { + const { headManager } = this.props + return { headManager } } - componentDidMount () { - const { router } = this.props - - this.close = router.subscribe((data) => { - const props = data.props || this.state.props - const state = propsToState({ - ...data, - props, - router - }) + render () { + const { Component, props, err, router } = this.props + const containerProps = { Component, props, router } - try { - this.setState(state) - } catch (err) { - this.handleError(err) - } - }) + return
+ + {ErrorDebug && err ? : null} +
} - componentWillUnmount () { - if (this.close) this.close() - } +} - getChildContext () { - const { router, headManager } = this.props - return { router, headManager } +class Container extends Component { + shouldComponentUpdate (nextProps) { + // need this check not to rerender component which has already thrown an error + return !shallowEquals(this.props, nextProps) } render () { - const { Component, props } = this.state + const { Component, props, router } = this.props + const url = createUrl(router) + // includes AppContainer which bypasses shouldComponentUpdate method + // https://github.com/gaearon/react-hot-loader/issues/442 return - + } - - async handleError (err) { - console.error(err) - - const { router, ErrorComponent } = this.props - const { pathname, query } = router - const props = await ErrorComponent.getInitialProps({ err, pathname, query }) - const state = propsToState({ Component: ErrorComponent, props, router }) - - try { - this.setState(state) - } catch (err2) { - console.error(err2) - } - } } -function propsToState (props) { - const { Component, router } = props - const url = { +function createUrl (router) { + return { query: router.query, pathname: router.pathname, back: () => router.back(), @@ -98,9 +68,4 @@ function propsToState (props) { return router.replace(replaceRoute, replaceUrl) } } - - return { - Component, - props: { ...props.props, url } - } } diff --git a/lib/error-debug.js b/lib/error-debug.js new file mode 100644 index 0000000000000..04d58180d0fa7 --- /dev/null +++ b/lib/error-debug.js @@ -0,0 +1,67 @@ +import React from 'react' +import ansiHTML from 'ansi-html' +import Head from './head' + +export default ({ err: { name, message, stack, module } }) => ( +
+ + + + {module ?
Error in {module.rawRequest}
: null} + { + name === 'ModuleBuildError' + ?
+      : 
{stack}
+ } +
+) + +const styles = { + errorDebug: { + background: '#a6004c', + boxSizing: 'border-box', + overflow: 'auto', + padding: '16px', + position: 'fixed', + left: 0, + right: 0, + top: 0, + bottom: 0, + zIndex: 9999 + }, + + message: { + fontFamily: '"SF Mono", "Roboto Mono", "Fira Mono", menlo-regular, monospace', + fontSize: '10px', + color: '#fbe7f1', + margin: 0, + whiteSpace: 'pre-wrap', + wordWrap: 'break-word' + }, + + heading: { + fontFamily: '-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif', + fontSize: '13px', + fontWeight: 'bold', + color: '#ff84bf', + marginBottom: '20px' + } +} + +const encodeHtml = str => { + return str.replace(//g, '>') +} + +// see color definitions of babel-code-frame: +// https://github.com/babel/babel/blob/master/packages/babel-code-frame/src/index.js + +ansiHTML.setColors({ + reset: ['fff', 'a6004c'], + darkgrey: 'e54590', + yellow: 'ee8cbb', + green: 'f2a2c7', + magenta: 'fbe7f1', + blue: 'fff', + cyan: 'ef8bb9', + red: 'fff' +}) diff --git a/lib/router/router.js b/lib/router/router.js index 02ae798254cb9..2cc6e1f6bc3d8 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -5,13 +5,13 @@ import { EventEmitter } from 'events' import { reloadIfPrefetched } from '../prefetch' export default class Router extends EventEmitter { - constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) { + constructor (pathname, query, { Component, ErrorComponent, err } = {}) { super() // represents the current component key this.route = toRoute(pathname) // set up the component cache (by route keys) - this.components = { [this.route]: { Component, ctx } } + this.components = { [this.route]: { Component, err } } this.ErrorComponent = ErrorComponent this.pathname = pathname @@ -166,17 +166,18 @@ export default class Router extends EventEmitter { const routeInfo = {} try { - const data = routeInfo.data = await this.fetchComponent(route) - const ctx = { ...data.ctx, pathname, query } - routeInfo.props = await this.getInitialProps(data.Component, ctx) + const { Component, err, xhr } = routeInfo.data = await this.fetchComponent(route) + const ctx = { err, xhr, pathname, query } + routeInfo.props = await this.getInitialProps(Component, ctx) } catch (err) { if (err.cancelled) { return { error: err } } - const data = routeInfo.data = { Component: this.ErrorComponent, ctx: { err } } - const ctx = { ...data.ctx, pathname, query } - routeInfo.props = await this.getInitialProps(data.Component, ctx) + const Component = this.ErrorComponent + routeInfo.data = { Component, err } + const ctx = { err, pathname, query } + routeInfo.props = await this.getInitialProps(Component, ctx) routeInfo.error = err console.error(err) @@ -213,10 +214,7 @@ export default class Router extends EventEmitter { const url = `/_next/pages${route}` const xhr = loadComponent(url, (err, data) => { if (err) return reject(err) - resolve({ - Component: data.Component, - ctx: { xhr, err: data.err } - }) + resolve({ ...data, xhr }) }) }) diff --git a/pages/_error-debug.js b/pages/_error-debug.js deleted file mode 100644 index 42c1a320e446c..0000000000000 --- a/pages/_error-debug.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react' -import Head from 'next/head' -import ansiHTML from 'ansi-html' - -export default class ErrorDebug extends React.Component { - static getInitialProps ({ err }) { - const { name, message, stack, module } = err - return { name, message, stack, path: module ? module.rawRequest : null } - } - - render () { - const { name, message, stack, path } = this.props - - return
- - - - {path ?
Error in {path}
: null} - { - name === 'ModuleBuildError' - ?
-        : 
{stack}
- } - - -
- } -} - -const encodeHtml = str => { - return str.replace(//g, '>') -} - -// see color definitions of babel-code-frame: -// https://github.com/babel/babel/blob/master/packages/babel-code-frame/src/index.js - -ansiHTML.setColors({ - reset: ['fff', 'a6004c'], - darkgrey: 'e54590', - yellow: 'ee8cbb', - green: 'f2a2c7', - magenta: 'fbe7f1', - blue: 'fff', - cyan: 'ef8bb9', - red: 'fff' -}) diff --git a/server/build/webpack.js b/server/build/webpack.js index f3fc98053748f..dc4abc691d935 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -17,7 +17,6 @@ import getConfig from '../config' const documentPage = join('pages', '_document.js') const defaultPages = [ '_error.js', - '_error-debug.js', '_document.js' ] diff --git a/server/render.js b/server/render.js index 6d26eaeec89f8..3b05c00df5909 100644 --- a/server/render.js +++ b/server/render.js @@ -27,8 +27,7 @@ export async function renderError (err, req, res, pathname, query, opts) { } export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) { - const page = err && opts.dev ? '/_error-debug' : '/_error' - return doRender(req, res, pathname, query, { ...opts, err, page }) + return doRender(req, res, pathname, query, { ...opts, err, page: '_error' }) } async function doRender (req, res, pathname, query, { @@ -54,7 +53,7 @@ async function doRender (req, res, pathname, query, { ] = await Promise.all([ Component.getInitialProps ? Component.getInitialProps(ctx) : {}, readPage(join(dir, '.next', 'bundles', 'pages', page)), - readPage(join(dir, '.next', 'bundles', 'pages', dev ? '_error-debug' : '_error')) + readPage(join(dir, '.next', 'bundles', 'pages', '_error')) ]) // the response might be finshed on the getinitialprops call @@ -64,6 +63,7 @@ async function doRender (req, res, pathname, query, { const app = createElement(App, { Component, props, + err, router: new Router(pathname, query) }) @@ -104,8 +104,7 @@ export async function renderJSON (req, res, page, { dir = process.cwd() } = {}) } export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) { - const page = err && dev ? '/_error-debug' : '/_error' - const component = await readPage(join(dir, '.next', 'bundles', 'pages', page)) + const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error')) sendJSON(res, { component, diff --git a/test/integration.test.js b/test/integration.test.js index a7b04491f537a..cecbf383fdb6d 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -63,7 +63,7 @@ describe('integration tests', () => { test('error', async () => { const html = await render('/error') - expect(html).toMatch(/
Error: This is an expected error\n[^]+<\/pre>/)
+    expect(html).toMatch(/
Error: This is an expected error\n[^]+<\/pre>/)
   })
 
   test('error 404', async () => {