Skip to content

Commit

Permalink
[Fast Refresh] Redesigned Runtime Error Experience (vercel#12222)
Browse files Browse the repository at this point in the history
  • Loading branch information
Timer authored and rokinsky committed Jul 11, 2020
1 parent 6ab0cc4 commit 2df983c
Show file tree
Hide file tree
Showing 52 changed files with 2,395 additions and 11 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ examples/with-ioc/**
examples/with-kea/**
packages/next/compiled/**/*
packages/react-refresh-utils/**/*.js
packages/react-dev-overlay/lib/**
**/__tmp__/**
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ node_modules
packages/next/compiled/**
packages/react-refresh-utils/**/*.js
packages/react-refresh-utils/**/*.d.ts
packages/react-dev-overlay/lib/**
**/__tmp__/**
2 changes: 1 addition & 1 deletion examples/z-experimental-refresh/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ code:after {
hr {
border: none;
border-bottom: 1px solid #efefef;
margin: 6em auto;
margin: 5em auto;
}
25 changes: 25 additions & 0 deletions examples/z-experimental-refresh/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { useCallback, useEffect, useState } from 'react'
import ClickCount from '../components/ClickCount'
import styles from '../components/ClickCount.module.css'

function a() {
console.log(
// hello
document.body()
)
}

function foo() {
a()
}

function Home() {
const [count, setCount] = useState(0)
Expand Down Expand Up @@ -28,6 +40,19 @@ function Home() {
<p>Component with State</p>
<ClickCount />
</div>
<hr />
<div>
<button
className={styles.btn}
type="button"
onClick={e => {
setTimeout(() => document.parentNode(), 0)
foo()
}}
>
Throw an Error
</button>
</div>
</main>
)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_FID_POLYFILL': JSON.stringify(
config.experimental.measureFid
),
'process.env.__NEXT_FAST_REFRESH': JSON.stringify(hasReactRefresh),
...(isServer
? {
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
Expand Down Expand Up @@ -1031,6 +1032,7 @@ export default async function getBaseWebpackConfig(
customAppFile,
isDevelopment: dev,
isServer,
hasReactRefresh,
assetPrefix: config.assetPrefix || '',
sassOptions: config.experimental.sassOptions,
})
Expand Down
10 changes: 9 additions & 1 deletion packages/next/build/webpack/config/blocks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ export const base = curry(function base(
config.name = ctx.isServer ? 'server' : 'client'
config.target = ctx.isServer ? 'node' : 'web'

// https://webpack.js.org/configuration/devtool/#development
config.devtool = ctx.isDevelopment
? 'cheap-module-source-map'
? ctx.hasReactRefresh
? // `eval-source-map` results in the fastest rebuilds during dev. The
// only drawback is cold boot time, but this is mitigated by the fact
// that we load entries on-demand.
'eval-source-map'
: // `cheap-module-source-map` is the old preferred format that was
// required for `react-error-overlay`.
'cheap-module-source-map'
: ctx.isProduction
? false
: false
Expand Down
3 changes: 3 additions & 0 deletions packages/next/build/webpack/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export async function build(
customAppFile,
isDevelopment,
isServer,
hasReactRefresh,
assetPrefix,
sassOptions,
}: {
rootDirectory: string
customAppFile: string | null
isDevelopment: boolean
isServer: boolean
hasReactRefresh: boolean
assetPrefix: string
sassOptions: any
}
Expand All @@ -26,6 +28,7 @@ export async function build(
customAppFile,
isDevelopment,
isProduction: !isDevelopment,
hasReactRefresh,
isServer,
isClient: !isServer,
assetPrefix: assetPrefix
Expand Down
1 change: 1 addition & 0 deletions packages/next/build/webpack/config/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type ConfigurationContext = {

isDevelopment: boolean
isProduction: boolean
hasReactRefresh: boolean

isServer: boolean
isClient: boolean
Expand Down
41 changes: 34 additions & 7 deletions packages/next/client/dev/error-overlay/hot-dev-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
// can be found here:
// https://github.com/facebook/create-react-app/blob/v3.4.1/packages/react-dev-utils/webpackHotDevClient.js

import * as DevOverlay from '@next/react-dev-overlay/lib/client'
import fetch from 'next/dist/build/polyfills/unfetch'
import * as ErrorOverlay from 'next/dist/compiled/react-error-overlay'
import stripAnsi from 'next/dist/compiled/strip-ansi'
Expand Down Expand Up @@ -61,6 +62,10 @@ export default function connect(options) {
)
})

if (process.env.__NEXT_FAST_REFRESH) {
DevOverlay.register()
}

// We need to keep track of if there has been a runtime error.
// Essentially, we cannot guarantee application state was not corrupted by the
// runtime error. To prevent confusing behavior, we forcibly reload the entire
Expand All @@ -69,6 +74,10 @@ export default function connect(options) {
// See https://github.com/facebook/create-react-app/issues/3096
ErrorOverlay.startReportingRuntimeErrors({
onError: function() {
if (process.env.__NEXT_FAST_REFRESH) {
return
}

hadRuntimeError = true
},
})
Expand Down Expand Up @@ -97,7 +106,15 @@ export default function connect(options) {
customHmrEventHandler = handler
},
reportRuntimeError(err) {
ErrorOverlay.reportRuntimeError(err)
if (process.env.__NEXT_FAST_REFRESH) {
// FIXME: this code branch should be eliminated
setTimeout(() => {
// An unhandled rendering error occurred
throw err
})
} else {
ErrorOverlay.reportRuntimeError(err)
}
},
prepareError(err) {
// Temporary workaround for https://github.com/facebook/create-react-app/issues/4760
Expand Down Expand Up @@ -139,10 +156,10 @@ function handleSuccess() {

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onHotUpdateSuccess() {
tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay()
onFastRefresh(hasUpdates)
})
}
}
Expand Down Expand Up @@ -180,10 +197,10 @@ function handleWarnings(warnings) {

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate() {
tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay()
onFastRefresh(hasUpdates)
})
}
}
Expand Down Expand Up @@ -227,6 +244,15 @@ function tryDismissErrorOverlay() {
}
}

function onFastRefresh(hasUpdates) {
tryDismissErrorOverlay()
if (hasUpdates) {
if (process.env.__NEXT_FAST_REFRESH) {
DevOverlay.onRefresh()
}
}
}

// There is a newer version of the code available.
function handleAvailableHash(hash) {
// Update last known compilation hash.
Expand Down Expand Up @@ -363,14 +389,15 @@ function tryApplyUpdates(onHotUpdateSuccess) {
return
}

const hasUpdates = Boolean(updatedModules.length)
if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess()
onHotUpdateSuccess(hasUpdates)
}

if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates()
tryApplyUpdates(hasUpdates ? undefined : onHotUpdateSuccess)
} else {
if (process.env.__NEXT_TEST_MODE) {
afterApplyUpdates(() => {
Expand Down
13 changes: 12 additions & 1 deletion packages/next/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,18 @@ class Container extends React.Component {
}

render() {
return this.props.children
if (process.env.NODE_ENV === 'production') {
return this.props.children
}
if (process.env.NODE_ENV !== 'production') {
if (process.env.__NEXT_FAST_REFRESH) {
const {
ReactDevOverlay,
} = require('@next/react-dev-overlay/lib/client')
return <ReactDevOverlay>{this.props.children}</ReactDevOverlay>
}
return this.props.children
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"@babel/preset-react": "7.7.0",
"@babel/preset-typescript": "7.7.2",
"@babel/runtime": "7.7.2",
"@next/react-dev-overlay": "9.3.7-canary.1",
"@next/react-refresh-utils": "9.3.7-canary.1",
"babel-plugin-syntax-jsx": "6.18.0",
"babel-plugin-transform-define": "2.0.0",
Expand Down
5 changes: 5 additions & 0 deletions packages/next/server/hot-reloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { route } from '../next-server/server/router'
import errorOverlayMiddleware from './lib/error-overlay-middleware'
import { findPageFile } from './lib/find-page-file'
import onDemandEntryHandler, { normalizePage } from './on-demand-entry-handler'
import reactDevOverlayMiddleware from '@next/react-dev-overlay/lib/middleware'

export async function renderScriptError(res: ServerResponse, error: Error) {
// Asks CDNs and others to not to cache the errored page
Expand Down Expand Up @@ -353,6 +354,10 @@ export default class HotReloader {
onDemandEntries.middleware(),
webpackHotMiddleware,
errorOverlayMiddleware({ dir: this.dir }),
reactDevOverlayMiddleware({
rootDirectory: this.dir,
stats: () => this.stats,
}),
]
}

Expand Down
1 change: 1 addition & 0 deletions packages/react-dev-overlay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/lib/
3 changes: 3 additions & 0 deletions packages/react-dev-overlay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@next/react-dev-overlay`

A development-only overlay for developing React applications.
31 changes: 31 additions & 0 deletions packages/react-dev-overlay/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@next/react-dev-overlay",
"version": "9.3.7-canary.1",
"description": "A development-only overlay for developing React applications.",
"repository": {
"url": "zeit/next.js",
"directory": "packages/react-dev-overlay"
},
"files": [
"lib/"
],
"author": "Joe Haddad <timer@vercel.com>",
"license": "MIT",
"scripts": {
"prepublish": "tsc -d -p tsconfig.json",
"build": "tsc -d -w -p tsconfig.json"
},
"dependencies": {
"@babel/code-frame": "7.8.3",
"anser": "1.4.9",
"classnames": "2.2.6",
"source-map": "0.7.3",
"stacktrace-parser": "0.1.9",
"strip-ansi": "6.0.0"
},
"peerDependencies": {
"react": "^16.9.0",
"react-dom": "^16.9.0"
},
"devDependencies": {}
}
79 changes: 79 additions & 0 deletions packages/react-dev-overlay/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { parse as parseStack } from 'stacktrace-parser'
import * as Bus from './internal/bus'

let isRegistered = false
let stackTraceLimit: number | undefined = undefined

function onUnhandledError(ev: ErrorEvent) {
const error = ev?.error
if (!error || !(error instanceof Error) || typeof error.stack !== 'string') {
// A non-error was thrown, we don't have anything to show. :-(
return
}

const e = error
Bus.emit({
type: Bus.TYPE_UNHANDLED_ERROR,
reason: error,
frames: parseStack(e.stack),
})
}

function onUnhandledRejection(ev: PromiseRejectionEvent) {
const reason = ev?.reason
if (
!reason ||
!(reason instanceof Error) ||
typeof reason.stack !== 'string'
) {
// A non-error was thrown, we don't have anything to show. :-(
return
}

const e = reason
Bus.emit({
type: Bus.TYPE_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(e.stack),
})
}

function register() {
if (isRegistered) {
return
}
isRegistered = true

try {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 50
stackTraceLimit = limit
} catch {}

window.addEventListener('error', onUnhandledError)
window.addEventListener('unhandledrejection', onUnhandledRejection)
}

function unregister() {
if (!isRegistered) {
return
}
isRegistered = false

if (stackTraceLimit !== undefined) {
try {
Error.stackTraceLimit = stackTraceLimit
} catch {}
stackTraceLimit = undefined
}

window.removeEventListener('error', onUnhandledError)
window.removeEventListener('unhandledrejection', onUnhandledRejection)
}

function onRefresh() {
Bus.emit({ type: Bus.TYPE_REFFRESH })
}

export { default as ReactDevOverlay } from './internal/ReactDevOverlay'
export { register, unregister, onRefresh }
Loading

0 comments on commit 2df983c

Please sign in to comment.