1
0
Fork 0
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:
Tim Neutkens 2018-04-18 21:32:01 +02:00
commit 467ec85572
19 changed files with 321 additions and 108 deletions

View file

@ -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 })
} }

View file

@ -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()

View 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
}

View file

@ -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>
)
}
} }
} }

View file

@ -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: {

View file

@ -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",

View file

@ -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)
}) })

View file

@ -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: {

View file

@ -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)

View 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
}

View file

@ -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)
} }

View file

@ -0,0 +1,5 @@
if (typeof window !== 'undefined') {
throw new Error('An Expected error occured')
}
export default () => <div />

View file

@ -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 />
}
}

View file

@ -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')

View file

@ -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 () => {

View file

@ -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 />
}
}

View file

@ -0,0 +1,6 @@
import React from 'react'
export default class ErrorInRenderPage extends React.Component {
render () {
throw new Error('An Expected error occured')
}
}

View file

@ -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 = {

View file

@ -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"