From 13ee1538b7af88b3a5235ca4461e17a45d916c11 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Tue, 10 Jan 2017 11:22:51 +0900 Subject: [PATCH 1/3] render debug page as overlay --- client/next.js | 23 +++++--- client/webpack-hot-middleware-client.js | 10 ++-- lib/app.js | 78 +++++-------------------- lib/error-debug.js | 67 +++++++++++++++++++++ lib/router/router.js | 22 ++++--- pages/_error-debug.js | 74 ----------------------- server/build/webpack.js | 1 - server/render.js | 9 ++- test/integration.test.js | 2 +- 9 files changed, 116 insertions(+), 170 deletions(-) create mode 100644 lib/error-debug.js delete mode 100644 pages/_error-debug.js diff --git a/client/next.js b/client/next.js index a1897675e5b27..5ce2a6b4aec1d 100644 --- a/client/next.js +++ b/client/next.js @@ -20,20 +20,24 @@ const { const Component = evalScript(component).default const ErrorComponent = evalScript(errorComponent).default +let lastProps export const router = createRouter(pathname, query, { Component, ErrorComponent, - ctx: { err } + err }) const headManager = new HeadManager() const container = document.getElementById('__next') -const defaultProps = { Component, ErrorComponent, props, router, headManager } if (ids && ids.length) rehydrate(ids) -render() +router.subscribe(({ Component, props, err }) => { + render({ Component, props, err }) +}) + +render({ Component, props, err }) export function render (props = {}) { try { @@ -45,15 +49,20 @@ export function render (props = {}) { export async function renderError (err) { const { pathname, query } = router - const props = await ErrorComponent.getInitialProps({ err, pathname, query }) + const props = await getInitialProps(ErrorComponent, { err, pathname, query }) try { - doRender({ Component: ErrorComponent, props }) + doRender({ Component: ErrorComponent, props, err }) } catch (err2) { console.error(err2) } } -function doRender (props) { - const appProps = { ...defaultProps, ...props } +function doRender ({ Component, props = lastProps, err }) { + lastProps = props + const appProps = { Component, props, err, router, headManager } ReactDOM.render(createElement(App, appProps), container) } + +function getInitialProps (Component, ctx) { + return Component.getInitialProps ? Component.getInitialProps(ctx) : {} +} 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..d7cadf93e564b 100644 --- a/lib/app.js +++ b/lib/app.js @@ -2,82 +2,35 @@ import React, { Component, PropTypes } from 'react' import { AppContainer } from 'react-hot-loader' 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) - } - } - - componentDidMount () { - const { router } = this.props - - this.close = router.subscribe((data) => { - const props = data.props || this.state.props - const state = propsToState({ - ...data, - props, - router - }) - - try { - this.setState(state) - } catch (err) { - this.handleError(err) - } - }) - } - - componentWillUnmount () { - if (this.close) this.close() - } - getChildContext () { - const { router, headManager } = this.props - return { router, headManager } + const { headManager } = this.props + return { headManager } } render () { - const { Component, props } = this.state + const { Component, props, err, router } = this.props + const url = createUrl(router) return - +
+ + {ErrorDebug && err ? : null} +
} - 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 +51,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 () => {

From 1c5a327528917ba704b5c57ed3e4d817072b1151 Mon Sep 17 00:00:00 2001
From: nkzawa 
Date: Wed, 11 Jan 2017 20:36:39 +0900
Subject: [PATCH 2/3] handle errors occurrred on rendering cycle for HMR

---
 client/index.js    | 69 ++++++++++++++++++++++++++++++++++++++++++++++
 client/next-dev.js | 16 +++++++++--
 client/next.js     | 69 ++--------------------------------------------
 lib/app.js         | 27 ++++++++++++++----
 4 files changed, 106 insertions(+), 75 deletions(-)
 create mode 100644 client/index.js

diff --git a/client/index.js b/client/index.js
new file mode 100644
index 0000000000000..84bfd15678bf3
--- /dev/null
+++ b/client/index.js
@@ -0,0 +1,69 @@
+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 function render (props, onError = renderErrorComponent) {
+  try {
+    doRender(props)
+  } catch (err) {
+    onError(err)
+  }
+}
+
+async function renderErrorComponent (err) {
+  const { pathname, query } = router
+  const props = await getInitialProps(ErrorComponent, { err, pathname, query })
+  doRender({ Component: ErrorComponent, props, err })
+}
+
+function doRender ({ Component, props, err }) {
+  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 5ce2a6b4aec1d..5400a409161c3 100644
--- a/client/next.js
+++ b/client/next.js
@@ -1,68 +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
-let lastProps
-
-export const router = createRouter(pathname, query, {
-  Component,
-  ErrorComponent,
-  err
-})
-
-const headManager = new HeadManager()
-const container = document.getElementById('__next')
-
-if (ids && ids.length) rehydrate(ids)
-
-router.subscribe(({ Component, props, err }) => {
-  render({ Component, props, err })
-})
-
-render({ Component, props, err })
-
-export function render (props = {}) {
-  try {
-    doRender(props)
-  } catch (err) {
-    renderError(err)
-  }
-}
-
-export async function renderError (err) {
-  const { pathname, query } = router
-  const props = await getInitialProps(ErrorComponent, { err, pathname, query })
-  try {
-    doRender({ Component: ErrorComponent, props, err })
-  } catch (err2) {
-    console.error(err2)
-  }
-}
-
-function doRender ({ Component, props = lastProps, err }) {
-  lastProps = props
-  const appProps = { Component, props, err, router, headManager }
-  ReactDOM.render(createElement(App, appProps), container)
-}
-
-function getInitialProps (Component, ctx) {
-  return Component.getInitialProps ? Component.getInitialProps(ctx) : {}
-}
+next()
diff --git a/lib/app.js b/lib/app.js
index d7cadf93e564b..02864563a1fa0 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -1,5 +1,6 @@
 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'
@@ -17,16 +18,32 @@ export default class App extends Component {
 
   render () {
     const { Component, props, err, router } = this.props
+    const containerProps = { Component, props, router }
+
+    return 
+ + {ErrorDebug && err ? : null} +
+ } + +} + +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, router } = this.props const url = createUrl(router) + // includes AppContainer which bypasses shouldComponentUpdate method + // https://github.com/gaearon/react-hot-loader/issues/442 return -
- - {ErrorDebug && err ? : null} -
+
} - } function createUrl (router) { From 1e80a2ec4a16466201bd3aed22e75f8f037f7999 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Wed, 11 Jan 2017 22:39:57 +0900 Subject: [PATCH 3/3] retrieve props if required on HMR --- client/index.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/client/index.js b/client/index.js index 84bfd15678bf3..3c1fab58db77b 100644 --- a/client/index.js +++ b/client/index.js @@ -41,21 +41,29 @@ export default (onError) => { render({ Component, props, err }, onError) } -export function render (props, onError = renderErrorComponent) { +export async function render (props, onError = renderErrorComponent) { try { - doRender(props) + await doRender(props) } catch (err) { - onError(err) + await onError(err) } } async function renderErrorComponent (err) { const { pathname, query } = router const props = await getInitialProps(ErrorComponent, { err, pathname, query }) - doRender({ Component: ErrorComponent, props, err }) + await doRender({ Component: ErrorComponent, props, err }) } -function doRender ({ Component, 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