mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Merge branch 'canary' of github.com:zeit/next.js into canary
This commit is contained in:
commit
467ec85572
|
@ -1,4 +1,4 @@
|
||||||
import { createElement } from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import HeadManager from './head-manager'
|
import HeadManager from './head-manager'
|
||||||
import { createRouter } from '../lib/router'
|
import { createRouter } from '../lib/router'
|
||||||
|
@ -66,33 +66,44 @@ const errorContainer = document.getElementById('__next-error')
|
||||||
let lastAppProps
|
let lastAppProps
|
||||||
export let router
|
export let router
|
||||||
export let ErrorComponent
|
export let ErrorComponent
|
||||||
|
let HotAppContainer
|
||||||
let ErrorDebugComponent
|
let ErrorDebugComponent
|
||||||
let Component
|
let Component
|
||||||
let App
|
let App
|
||||||
let stripAnsi = (s) => s
|
let stripAnsi = (s) => s
|
||||||
|
let applySourcemaps = (e) => e
|
||||||
|
|
||||||
export const emitter = new EventEmitter()
|
export const emitter = new EventEmitter()
|
||||||
|
|
||||||
export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: passedStripAnsi } = {}) => {
|
export default async ({
|
||||||
|
HotAppContainer: passedHotAppContainer,
|
||||||
|
ErrorDebugComponent: passedDebugComponent,
|
||||||
|
stripAnsi: passedStripAnsi,
|
||||||
|
applySourcemaps: passedApplySourcemaps
|
||||||
|
} = {}) => {
|
||||||
// Wait for all the dynamic chunks to get loaded
|
// Wait for all the dynamic chunks to get loaded
|
||||||
for (const chunkName of chunks) {
|
for (const chunkName of chunks) {
|
||||||
await pageLoader.waitForChunk(chunkName)
|
await pageLoader.waitForChunk(chunkName)
|
||||||
}
|
}
|
||||||
|
|
||||||
stripAnsi = passedStripAnsi || stripAnsi
|
stripAnsi = passedStripAnsi || stripAnsi
|
||||||
|
applySourcemaps = passedApplySourcemaps || applySourcemaps
|
||||||
|
HotAppContainer = passedHotAppContainer
|
||||||
ErrorDebugComponent = passedDebugComponent
|
ErrorDebugComponent = passedDebugComponent
|
||||||
ErrorComponent = await pageLoader.loadPage('/_error')
|
ErrorComponent = await pageLoader.loadPage('/_error')
|
||||||
App = await pageLoader.loadPage('/_app')
|
App = await pageLoader.loadPage('/_app')
|
||||||
|
|
||||||
|
let initialErr = err
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Component = await pageLoader.loadPage(page)
|
Component = await pageLoader.loadPage(page)
|
||||||
|
|
||||||
if (typeof Component !== 'function') {
|
if (typeof Component !== 'function') {
|
||||||
throw new Error(`The default export is not a React Component in page: "${pathname}"`)
|
throw new Error(`The default export is not a React Component in page: "${pathname}"`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.error(stripAnsi(`${err.message}\n${err.stack}`))
|
// This catches errors like throwing in the top level of a module
|
||||||
Component = ErrorComponent
|
initialErr = error
|
||||||
}
|
}
|
||||||
|
|
||||||
router = createRouter(pathname, query, asPath, {
|
router = createRouter(pathname, query, asPath, {
|
||||||
|
@ -101,7 +112,7 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
|
||||||
App,
|
App,
|
||||||
Component,
|
Component,
|
||||||
ErrorComponent,
|
ErrorComponent,
|
||||||
err
|
err: initialErr
|
||||||
})
|
})
|
||||||
|
|
||||||
router.subscribe(({ Component, props, hash, err }) => {
|
router.subscribe(({ Component, props, hash, err }) => {
|
||||||
|
@ -109,14 +120,14 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
|
||||||
})
|
})
|
||||||
|
|
||||||
const hash = location.hash.substring(1)
|
const hash = location.hash.substring(1)
|
||||||
render({ Component, props, hash, err, emitter })
|
render({ Component, props, hash, err: initialErr, emitter })
|
||||||
|
|
||||||
return emitter
|
return emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function render (props) {
|
export async function render (props) {
|
||||||
if (props.err) {
|
if (props.err) {
|
||||||
await renderError(props.err)
|
await renderError(props)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,31 +135,37 @@ export async function render (props) {
|
||||||
await doRender(props)
|
await doRender(props)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.abort) return
|
if (err.abort) return
|
||||||
await renderError(err)
|
await renderError({...props, err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method handles all runtime and debug errors.
|
// This method handles all runtime and debug errors.
|
||||||
// 404 and 500 errors are special kind of errors
|
// 404 and 500 errors are special kind of errors
|
||||||
// and they are still handle via the main render method.
|
// and they are still handle via the main render method.
|
||||||
export async function renderError (error) {
|
export async function renderError (props) {
|
||||||
const prod = process.env.NODE_ENV === 'production'
|
const {err} = props
|
||||||
// We need to unmount the current app component because it's
|
|
||||||
// in the inconsistant state.
|
|
||||||
// Otherwise, we need to face issues when the issue is fixed and
|
|
||||||
// it's get notified via HMR
|
|
||||||
ReactDOM.unmountComponentAtNode(appContainer)
|
|
||||||
|
|
||||||
const errorMessage = `${error.message}\n${error.stack}`
|
// In development we apply sourcemaps to the error
|
||||||
console.error(stripAnsi(errorMessage))
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
await applySourcemaps(err)
|
||||||
if (prod) {
|
|
||||||
const initProps = {Component: ErrorComponent, router, ctx: {err: error, pathname, query, asPath}}
|
|
||||||
const props = await loadGetInitialProps(ErrorComponent, initProps)
|
|
||||||
renderReactElement(createElement(ErrorComponent, props), errorContainer)
|
|
||||||
} else {
|
|
||||||
renderReactElement(createElement(ErrorDebugComponent, { error }), errorContainer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const str = stripAnsi(`${err.message}\n${err.stack}${err.info ? `\n\n${err.info.componentStack}` : ''}`)
|
||||||
|
console.error(str)
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// We need to unmount the current app component because it's
|
||||||
|
// in the inconsistant state.
|
||||||
|
// Otherwise, we need to face issues when the issue is fixed and
|
||||||
|
// it's get notified via HMR
|
||||||
|
ReactDOM.unmountComponentAtNode(appContainer)
|
||||||
|
renderReactElement(<ErrorDebugComponent error={err} />, errorContainer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In production we do a normal render with the `ErrorComponent` as component.
|
||||||
|
// `App` will handle the calling of `getInitialProps`, which will include the `err` on the context
|
||||||
|
await doRender({...props, err, Component: ErrorComponent})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doRender ({ Component, props, hash, err, emitter: emitterProp = emitter }) {
|
async function doRender ({ Component, props, hash, err, emitter: emitterProp = emitter }) {
|
||||||
|
@ -172,7 +189,15 @@ async function doRender ({ Component, props, hash, err, emitter: emitterProp = e
|
||||||
|
|
||||||
// We need to clear any existing runtime error messages
|
// We need to clear any existing runtime error messages
|
||||||
ReactDOM.unmountComponentAtNode(errorContainer)
|
ReactDOM.unmountComponentAtNode(errorContainer)
|
||||||
renderReactElement(createElement(App, appProps), appContainer)
|
|
||||||
|
// In development we render react-hot-loader's wrapper component
|
||||||
|
if (HotAppContainer) {
|
||||||
|
renderReactElement(<HotAppContainer errorReporter={ErrorDebugComponent} warnings={false}>
|
||||||
|
<App {...appProps} />
|
||||||
|
</HotAppContainer>, appContainer)
|
||||||
|
} else {
|
||||||
|
renderReactElement(<App {...appProps} />, appContainer)
|
||||||
|
}
|
||||||
|
|
||||||
emitterProp.emit('after-reactdom-render', { Component, ErrorComponent, appProps })
|
emitterProp.emit('after-reactdom-render', { Component, ErrorComponent, appProps })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import initNext, * as next from './'
|
import initNext, * as next from './'
|
||||||
import ErrorDebugComponent from '../lib/error-debug'
|
import {ClientDebug} from '../lib/error-debug'
|
||||||
import initOnDemandEntries from './on-demand-entries-client'
|
import initOnDemandEntries from './on-demand-entries-client'
|
||||||
import initWebpackHMR from './webpack-hot-middleware-client'
|
import initWebpackHMR from './webpack-hot-middleware-client'
|
||||||
|
import {AppContainer as HotAppContainer} from 'react-hot-loader'
|
||||||
require('@zeit/source-map-support/browser-source-map-support')
|
import {applySourcemaps} from './source-map-support'
|
||||||
|
|
||||||
window.next = next
|
window.next = next
|
||||||
|
|
||||||
initNext({ ErrorDebugComponent, stripAnsi })
|
initNext({ HotAppContainer, ErrorDebugComponent: ClientDebug, applySourcemaps, stripAnsi })
|
||||||
.then((emitter) => {
|
.then((emitter) => {
|
||||||
initOnDemandEntries()
|
initOnDemandEntries()
|
||||||
initWebpackHMR()
|
initWebpackHMR()
|
||||||
|
|
54
client/source-map-support.js
Normal file
54
client/source-map-support.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// @flow
|
||||||
|
import fetch from 'unfetch'
|
||||||
|
const filenameRE = /\(([^)]+\.js):(\d+):(\d+)\)$/
|
||||||
|
|
||||||
|
export async function applySourcemaps (e: any): Promise<void> {
|
||||||
|
if (!e || typeof e.stack !== 'string' || e.sourceMapsApplied) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = e.stack.split('\n')
|
||||||
|
|
||||||
|
const result = await Promise.all(lines.map((line) => {
|
||||||
|
return rewriteTraceLine(line)
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.stack = result.join('\n')
|
||||||
|
// This is to make sure we don't apply the sourcemaps twice on the same object
|
||||||
|
e.sourceMapsApplied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rewriteTraceLine (trace: string): Promise<string> {
|
||||||
|
const m = trace.match(filenameRE)
|
||||||
|
if (m == null) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = m[1]
|
||||||
|
if (filePath.match(/node_modules/)) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapPath = `${filePath}.map`
|
||||||
|
|
||||||
|
const res = await fetch(mapPath)
|
||||||
|
if (res.status !== 200) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapContents = await res.json()
|
||||||
|
const {SourceMapConsumer} = require('source-map')
|
||||||
|
const map = new SourceMapConsumer(mapContents)
|
||||||
|
const originalPosition = map.originalPositionFor({
|
||||||
|
line: Number(m[2]),
|
||||||
|
column: Number(m[3])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (originalPosition.source != null) {
|
||||||
|
const { source, line, column } = originalPosition
|
||||||
|
const mappedPosition = `(${source.replace(/^webpack:\/\/\//, '')}:${String(line)}:${String(column)})`
|
||||||
|
return trace.replace(filenameRE, mappedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trace
|
||||||
|
}
|
45
lib/app.js
45
lib/app.js
|
@ -5,10 +5,6 @@ import { execOnce, warn, loadGetInitialProps } from './utils'
|
||||||
import { makePublicRouterInstance } from './router'
|
import { makePublicRouterInstance } from './router'
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
state = {
|
|
||||||
hasError: null
|
|
||||||
}
|
|
||||||
|
|
||||||
static displayName = 'App'
|
static displayName = 'App'
|
||||||
|
|
||||||
static async getInitialProps ({ Component, router, ctx }) {
|
static async getInitialProps ({ Component, router, ctx }) {
|
||||||
|
@ -24,18 +20,25 @@ export default class App extends Component {
|
||||||
|
|
||||||
getChildContext () {
|
getChildContext () {
|
||||||
const { headManager } = this.props
|
const { headManager } = this.props
|
||||||
const {hasError} = this.state
|
|
||||||
return {
|
return {
|
||||||
headManager,
|
headManager,
|
||||||
router: makePublicRouterInstance(this.props.router),
|
router: makePublicRouterInstance(this.props.router),
|
||||||
_containerProps: {...this.props, hasError}
|
_containerProps: {...this.props}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch (error, info) {
|
componentDidCatch (err, info) {
|
||||||
error.stack = `${error.stack}\n\n${info.componentStack}`
|
// To provide clearer stacktraces in error-debug.js in development
|
||||||
window.next.renderError(error)
|
// To provide clearer stacktraces in app.js in production
|
||||||
this.setState({ hasError: true })
|
err.info = info
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
// In production we render _error.js
|
||||||
|
window.next.renderError({err})
|
||||||
|
} else {
|
||||||
|
// In development we throw the error up to AppContainer from react-hot-loader
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -78,28 +81,8 @@ export class Container extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasError } = this.context._containerProps
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const {children} = this.props
|
const {children} = this.props
|
||||||
|
return <>{children}</>
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
return <>{children}</>
|
|
||||||
} else {
|
|
||||||
const ErrorDebug = require('./error-debug').default
|
|
||||||
const { AppContainer } = require('react-hot-loader')
|
|
||||||
|
|
||||||
// includes AppContainer which bypasses shouldComponentUpdate method
|
|
||||||
// https://github.com/gaearon/react-hot-loader/issues/442
|
|
||||||
return (
|
|
||||||
<AppContainer warnings={false} errorReporter={ErrorDebug}>
|
|
||||||
{children}
|
|
||||||
</AppContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,68 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ansiHTML from 'ansi-html'
|
import ansiHTML from 'ansi-html'
|
||||||
import Head from './head'
|
import Head from './head'
|
||||||
|
import {applySourcemaps} from '../client/source-map-support'
|
||||||
|
|
||||||
export default ({ error, error: { name, message, module } }) => (
|
// On the client side the error can come from multiple places for example react-hot-loader or client/index.js
|
||||||
<div style={styles.errorDebug}>
|
// `componentDidCatch` doesn't support asynchronous execution, so we have to handle sourcemap support here
|
||||||
<Head>
|
export class ClientDebug extends React.Component {
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
state = {
|
||||||
</Head>
|
mappedError: null
|
||||||
{module ? <h1 style={styles.heading}>Error in {module.rawRequest}</h1> : null}
|
}
|
||||||
{
|
componentDidMount () {
|
||||||
name === 'ModuleBuildError'
|
const {error} = this.props
|
||||||
? <pre style={styles.stack} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
|
|
||||||
: <StackTrace error={error} />
|
// If sourcemaps were already applied there is no need to set the state
|
||||||
|
if (error.sourceMapsApplied) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const StackTrace = ({ error: { name, message, stack } }) => (
|
// Since componentDidMount doesn't handle errors we use then/catch here
|
||||||
|
applySourcemaps(error).then(() => {
|
||||||
|
this.setState({mappedError: error})
|
||||||
|
}).catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {mappedError} = this.state
|
||||||
|
const {error} = this.props
|
||||||
|
if (!error.sourceMapsApplied && mappedError === null) {
|
||||||
|
return <div style={styles.errorDebug}>
|
||||||
|
<h1 style={styles.heading}>Loading stacktrace...</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ErrorDebug error={error} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the server side the error has sourcemaps already applied, so `ErrorDebug` is rendered directly.
|
||||||
|
export default function ErrorDebug ({error}) {
|
||||||
|
const { name, message, module } = error
|
||||||
|
return (
|
||||||
|
<div style={styles.errorDebug}>
|
||||||
|
<Head>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||||
|
</Head>
|
||||||
|
{module ? <h1 style={styles.heading}>Error in {module.rawRequest}</h1> : null}
|
||||||
|
{
|
||||||
|
name === 'ModuleBuildError'
|
||||||
|
? <pre style={styles.stack} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
|
||||||
|
: <StackTrace error={error} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StackTrace = ({ error: { name, message, stack, info } }) => (
|
||||||
<div>
|
<div>
|
||||||
<div style={styles.heading}>{message || name}</div>
|
<div style={styles.heading}>{message || name}</div>
|
||||||
<pre style={styles.stack}>
|
<pre style={styles.stack}>
|
||||||
{stack}
|
{stack}
|
||||||
</pre>
|
</pre>
|
||||||
|
{info && <pre style={styles.stack}>
|
||||||
|
{info.componentStack}
|
||||||
|
</pre>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,7 +77,8 @@ const styles = {
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 9999
|
zIndex: 9999,
|
||||||
|
color: '#b3adac'
|
||||||
},
|
},
|
||||||
|
|
||||||
stack: {
|
stack: {
|
||||||
|
|
|
@ -64,7 +64,6 @@
|
||||||
"@babel/runtime": "7.0.0-beta.42",
|
"@babel/runtime": "7.0.0-beta.42",
|
||||||
"@babel/template": "7.0.0-beta.42",
|
"@babel/template": "7.0.0-beta.42",
|
||||||
"@zeit/check-updates": "1.1.1",
|
"@zeit/check-updates": "1.1.1",
|
||||||
"@zeit/source-map-support": "0.6.2",
|
|
||||||
"ansi-html": "0.0.7",
|
"ansi-html": "0.0.7",
|
||||||
"babel-core": "7.0.0-bridge.0",
|
"babel-core": "7.0.0-bridge.0",
|
||||||
"babel-loader": "8.0.0-beta.2",
|
"babel-loader": "8.0.0-beta.2",
|
||||||
|
|
|
@ -23,17 +23,6 @@ function styledJsxOptions (opts) {
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
const envPlugins = {
|
|
||||||
'development': [
|
|
||||||
require('@babel/plugin-transform-react-jsx-source')
|
|
||||||
],
|
|
||||||
'production': [
|
|
||||||
require('babel-plugin-transform-react-remove-prop-types')
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins = envPlugins[process.env.NODE_ENV] || envPlugins['development']
|
|
||||||
|
|
||||||
module.exports = (context, opts = {}) => ({
|
module.exports = (context, opts = {}) => ({
|
||||||
presets: [
|
presets: [
|
||||||
[require('@babel/preset-env'), {
|
[require('@babel/preset-env'), {
|
||||||
|
@ -53,6 +42,6 @@ module.exports = (context, opts = {}) => ({
|
||||||
regenerator: true
|
regenerator: true
|
||||||
}],
|
}],
|
||||||
[require('styled-jsx/babel'), styledJsxOptions(opts['styled-jsx'])],
|
[require('styled-jsx/babel'), styledJsxOptions(opts['styled-jsx'])],
|
||||||
...plugins
|
process.env.NODE_ENV === 'production' && require('babel-plugin-transform-react-remove-prop-types')
|
||||||
]
|
].filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import BuildManifestPlugin from './plugins/build-manifest-plugin'
|
||||||
|
|
||||||
const presetItem = createConfigItem(require('./babel/preset'), {type: 'preset'})
|
const presetItem = createConfigItem(require('./babel/preset'), {type: 'preset'})
|
||||||
const hotLoaderItem = createConfigItem(require('react-hot-loader/babel'), {type: 'plugin'})
|
const hotLoaderItem = createConfigItem(require('react-hot-loader/babel'), {type: 'plugin'})
|
||||||
|
const reactJsxSourceItem = createConfigItem(require('@babel/plugin-transform-react-jsx-source'), {type: 'plugin'})
|
||||||
|
|
||||||
const nextDir = path.join(__dirname, '..', '..', '..')
|
const nextDir = path.join(__dirname, '..', '..', '..')
|
||||||
const nextNodeModulesDir = path.join(nextDir, 'node_modules')
|
const nextNodeModulesDir = path.join(nextDir, 'node_modules')
|
||||||
|
@ -34,7 +35,8 @@ function babelConfig (dir, {isServer, dev}) {
|
||||||
cacheDirectory: true,
|
cacheDirectory: true,
|
||||||
presets: [],
|
presets: [],
|
||||||
plugins: [
|
plugins: [
|
||||||
dev && !isServer && hotLoaderItem
|
dev && !isServer && hotLoaderItem,
|
||||||
|
dev && reactJsxSourceItem
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +121,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
let webpackConfig = {
|
let webpackConfig = {
|
||||||
devtool: dev ? 'source-map' : false,
|
devtool: dev ? 'cheap-module-source-map' : false,
|
||||||
name: isServer ? 'server' : 'client',
|
name: isServer ? 'server' : 'client',
|
||||||
cache: true,
|
cache: true,
|
||||||
target: isServer ? 'node' : 'web',
|
target: isServer ? 'node' : 'web',
|
||||||
|
@ -139,14 +141,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
|
||||||
libraryTarget: 'commonjs2',
|
libraryTarget: 'commonjs2',
|
||||||
// This saves chunks with the name given via require.ensure()
|
// This saves chunks with the name given via require.ensure()
|
||||||
chunkFilename: '[name]-[chunkhash].js',
|
chunkFilename: '[name]-[chunkhash].js',
|
||||||
strictModuleExceptionHandling: true,
|
strictModuleExceptionHandling: true
|
||||||
devtoolModuleFilenameTemplate (info) {
|
|
||||||
if (dev) {
|
|
||||||
return info.absoluteResourcePath
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${info.absoluteResourcePath.replace(dir, '.').replace(nextDir, './node_modules/next')}`
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
performance: { hints: false },
|
performance: { hints: false },
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-disable import/first, no-return-await */
|
/* eslint-disable import/first, no-return-await */
|
||||||
require('@zeit/source-map-support').install()
|
|
||||||
import { resolve, join, sep } from 'path'
|
import { resolve, join, sep } from 'path'
|
||||||
import { parse as parseUrl } from 'url'
|
import { parse as parseUrl } from 'url'
|
||||||
import { parse as parseQs } from 'querystring'
|
import { parse as parseQs } from 'querystring'
|
||||||
|
@ -337,6 +336,8 @@ export default class Server {
|
||||||
res.statusCode = 404
|
res.statusCode = 404
|
||||||
return this.renderErrorToHTML(null, req, res, pathname, query)
|
return this.renderErrorToHTML(null, req, res, pathname, query)
|
||||||
} else {
|
} else {
|
||||||
|
const {applySourcemaps} = require('./lib/source-map-support')
|
||||||
|
await applySourcemaps(err)
|
||||||
if (!this.quiet) console.error(err)
|
if (!this.quiet) console.error(err)
|
||||||
res.statusCode = 500
|
res.statusCode = 500
|
||||||
return this.renderErrorToHTML(err, req, res, pathname, query)
|
return this.renderErrorToHTML(err, req, res, pathname, query)
|
||||||
|
|
57
server/lib/source-map-support.js
Normal file
57
server/lib/source-map-support.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// @flow
|
||||||
|
const filenameRE = /\(([^)]+\.js):(\d+):(\d+)\)$/
|
||||||
|
|
||||||
|
export async function applySourcemaps (e: any): Promise<void> {
|
||||||
|
if (!e || typeof e.stack !== 'string' || e.sourceMapsApplied) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = e.stack.split('\n')
|
||||||
|
|
||||||
|
const result = await Promise.all(lines.map((line) => {
|
||||||
|
return rewriteTraceLine(line)
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.stack = result.join('\n')
|
||||||
|
// This is to make sure we don't apply the sourcemaps twice on the same object
|
||||||
|
e.sourceMapsApplied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rewriteTraceLine (trace: string): Promise<string> {
|
||||||
|
const m = trace.match(filenameRE)
|
||||||
|
if (m == null) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = m[1]
|
||||||
|
const mapPath = `${filePath}.map`
|
||||||
|
|
||||||
|
// Load these on demand.
|
||||||
|
const fs = require('fs')
|
||||||
|
const promisify = require('./promisify')
|
||||||
|
|
||||||
|
const readFile = promisify(fs.readFile)
|
||||||
|
const access = promisify(fs.access)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(mapPath, (fs.constants || fs).R_OK)
|
||||||
|
} catch (err) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapContents = await readFile(mapPath)
|
||||||
|
const {SourceMapConsumer} = require('source-map')
|
||||||
|
const map = new SourceMapConsumer(JSON.parse(mapContents))
|
||||||
|
const originalPosition = map.originalPositionFor({
|
||||||
|
line: Number(m[2]),
|
||||||
|
column: Number(m[3])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (originalPosition.source != null) {
|
||||||
|
const { source, line, column } = originalPosition
|
||||||
|
const mappedPosition = `(${source.replace(/^webpack:\/\/\//, '')}:${String(line)}:${String(column)})`
|
||||||
|
return trace.replace(filenameRE, mappedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trace
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import Head, { defaultHead } from '../lib/head'
|
||||||
import ErrorDebug from '../lib/error-debug'
|
import ErrorDebug from '../lib/error-debug'
|
||||||
import { flushChunks } from '../lib/dynamic'
|
import { flushChunks } from '../lib/dynamic'
|
||||||
import { BUILD_MANIFEST } from '../lib/constants'
|
import { BUILD_MANIFEST } from '../lib/constants'
|
||||||
|
import { applySourcemaps } from './lib/source-map-support'
|
||||||
|
|
||||||
const logger = console
|
const logger = console
|
||||||
|
|
||||||
|
@ -49,6 +50,8 @@ async function doRender (req, res, pathname, query, {
|
||||||
} = {}) {
|
} = {}) {
|
||||||
page = page || pathname
|
page = page || pathname
|
||||||
|
|
||||||
|
await applySourcemaps(err)
|
||||||
|
|
||||||
if (hotReloader) { // In dev mode we use on demand entries to compile the page before rendering
|
if (hotReloader) { // In dev mode we use on demand entries to compile the page before rendering
|
||||||
await ensurePage(page, { dir, hotReloader })
|
await ensurePage(page, { dir, hotReloader })
|
||||||
}
|
}
|
||||||
|
@ -95,7 +98,7 @@ async function doRender (req, res, pathname, query, {
|
||||||
if (err && dev) {
|
if (err && dev) {
|
||||||
errorHtml = render(createElement(ErrorDebug, { error: err }))
|
errorHtml = render(createElement(ErrorDebug, { error: err }))
|
||||||
} else if (err) {
|
} else if (err) {
|
||||||
errorHtml = render(app)
|
html = render(app)
|
||||||
} else {
|
} else {
|
||||||
html = render(app)
|
html = render(app)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
throw new Error('An Expected error occured')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => <div />
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react'
|
||||||
|
export default class ErrorInRenderPage extends React.Component {
|
||||||
|
render () {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
throw new Error('An Expected error occured')
|
||||||
|
}
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
/* global describe, it, expect */
|
/* global describe, it, expect */
|
||||||
|
|
||||||
import webdriver from 'next-webdriver'
|
import webdriver from 'next-webdriver'
|
||||||
|
import {waitFor} from 'next-test-utils'
|
||||||
|
|
||||||
export default (context, render) => {
|
export default (context, render) => {
|
||||||
describe('Client Navigation', () => {
|
describe('Client Navigation', () => {
|
||||||
|
@ -454,6 +455,24 @@ export default (context, render) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('runtime errors', () => {
|
||||||
|
it('should show ErrorDebug when a client side error is thrown inside a component', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/error-inside-browser-page')
|
||||||
|
await waitFor(2000)
|
||||||
|
const text = await browser.elementByCss('body').text()
|
||||||
|
expect(text).toMatch(/An Expected error occured/)
|
||||||
|
expect(text).toMatch(/pages\/error-inside-browser-page\.js:5:0/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show ErrorDebug when a client side error is thrown outside a component', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/error-in-the-browser-global-scope')
|
||||||
|
await waitFor(2000)
|
||||||
|
const text = await browser.elementByCss('body').text()
|
||||||
|
expect(text).toMatch(/An Expected error occured/)
|
||||||
|
expect(text).toMatch(/pages\/error-in-the-browser-global-scope\.js:2:0/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('with 404 pages', () => {
|
describe('with 404 pages', () => {
|
||||||
it('should 404 on not existent page', async () => {
|
it('should 404 on not existent page', async () => {
|
||||||
const browser = await webdriver(context.appPort, '/non-existent')
|
const browser = await webdriver(context.appPort, '/non-existent')
|
||||||
|
|
|
@ -104,11 +104,14 @@ export default function ({ app }, suiteName, render, fetch) {
|
||||||
test('error-inside-page', async () => {
|
test('error-inside-page', async () => {
|
||||||
const $ = await get$('/error-inside-page')
|
const $ = await get$('/error-inside-page')
|
||||||
expect($('pre').text()).toMatch(/This is an expected error/)
|
expect($('pre').text()).toMatch(/This is an expected error/)
|
||||||
|
// Check if the the source map line is correct
|
||||||
|
expect($('body').text()).toMatch(/pages\/error-inside-page\.js:2:0/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('error-in-the-global-scope', async () => {
|
test('error-in-the-global-scope', async () => {
|
||||||
const $ = await get$('/error-in-the-global-scope')
|
const $ = await get$('/error-in-the-global-scope')
|
||||||
expect($('pre').text()).toMatch(/aa is not defined/)
|
expect($('pre').text()).toMatch(/aa is not defined/)
|
||||||
|
expect($('body').text()).toMatch(/pages\/error-in-the-global-scope\.js:1:0/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('asPath', async () => {
|
test('asPath', async () => {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react'
|
||||||
|
export default class ErrorInRenderPage extends React.Component {
|
||||||
|
render () {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
throw new Error('An Expected error occured')
|
||||||
|
}
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
}
|
6
test/integration/production/pages/error-in-ssr-render.js
Normal file
6
test/integration/production/pages/error-in-ssr-render.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import React from 'react'
|
||||||
|
export default class ErrorInRenderPage extends React.Component {
|
||||||
|
render () {
|
||||||
|
throw new Error('An Expected error occured')
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,6 +74,26 @@ describe('Production Usage', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Runtime errors', () => {
|
||||||
|
it('should render a server side error on the client side', async () => {
|
||||||
|
const browser = await webdriver(appPort, '/error-in-ssr-render')
|
||||||
|
await waitFor(2000)
|
||||||
|
const text = await browser.elementByCss('body').text()
|
||||||
|
// this makes sure we don't leak the actual error to the client side in production
|
||||||
|
expect(text).toMatch(/Internal Server Error\./)
|
||||||
|
const headingText = await browser.elementByCss('h1').text()
|
||||||
|
// This makes sure we render statusCode on the client side correctly
|
||||||
|
expect(headingText).toBe('500')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render a client side component error', async () => {
|
||||||
|
const browser = await webdriver(appPort, '/error-in-browser-render')
|
||||||
|
await waitFor(2000)
|
||||||
|
const text = await browser.elementByCss('body').text()
|
||||||
|
expect(text).toMatch(/An unexpected error has occurred\./)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Misc', () => {
|
describe('Misc', () => {
|
||||||
it('should handle already finished responses', async () => {
|
it('should handle already finished responses', async () => {
|
||||||
const res = {
|
const res = {
|
||||||
|
|
|
@ -759,12 +759,6 @@
|
||||||
postcss-loader "^2.0.10"
|
postcss-loader "^2.0.10"
|
||||||
style-loader "^0.19.1"
|
style-loader "^0.19.1"
|
||||||
|
|
||||||
"@zeit/source-map-support@0.6.2":
|
|
||||||
version "0.6.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@zeit/source-map-support/-/source-map-support-0.6.2.tgz#0efd478f24a606726948165e53a8efe89e24036f"
|
|
||||||
dependencies:
|
|
||||||
source-map "^0.6.0"
|
|
||||||
|
|
||||||
abab@^1.0.3:
|
abab@^1.0.3:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
||||||
|
@ -6929,7 +6923,7 @@ source-map-url@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
||||||
|
|
||||||
source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
|
source-map@0.6.1, source-map@^0.6.1, source-map@~0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue