diff --git a/packages/next/client/components/hot-reloader.tsx b/packages/next/client/components/hot-reloader.tsx index f03e2f62c3ef3..0a0ecc11260ec 100644 --- a/packages/next/client/components/hot-reloader.tsx +++ b/packages/next/client/components/hot-reloader.tsx @@ -15,7 +15,7 @@ import { onBuildOk, onRefresh, ReactDevOverlay, -} from 'next/dist/compiled/@next/react-dev-overlay/dist/client' +} from './react-dev-overlay/src/client' import stripAnsi from 'next/dist/compiled/strip-ansi' import formatWebpackMessages from '../dev/error-overlay/format-webpack-messages' import { useRouter } from './hooks-client' diff --git a/packages/next/client/components/react-dev-overlay/src/client.ts b/packages/next/client/components/react-dev-overlay/src/client.ts new file mode 100644 index 0000000000000..a9eb0a496eea5 --- /dev/null +++ b/packages/next/client/components/react-dev-overlay/src/client.ts @@ -0,0 +1,95 @@ +import * as Bus from './internal/bus' +import { parseStack } from './internal/helpers/parseStack' + +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 + } + + if ( + error.message.match(/(hydration|content does not match|did not match)/i) + ) { + error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error` + } + + 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 onBuildOk() { + Bus.emit({ type: Bus.TYPE_BUILD_OK }) +} + +function onBuildError(message: string) { + Bus.emit({ type: Bus.TYPE_BUILD_ERROR, message }) +} + +function onRefresh() { + Bus.emit({ type: Bus.TYPE_REFRESH }) +} + +export { getErrorByType } from './internal/helpers/getErrorByType' +export { getServerError } from './internal/helpers/nodeStackFrames' +export { default as ReactDevOverlay } from './internal/ReactDevOverlay' +export { onBuildOk, onBuildError, register, unregister, onRefresh } diff --git a/packages/next/client/components/react-dev-overlay/src/internal/ErrorBoundary.tsx b/packages/next/client/components/react-dev-overlay/src/internal/ErrorBoundary.tsx new file mode 100644 index 0000000000000..7f836313c510b --- /dev/null +++ b/packages/next/client/components/react-dev-overlay/src/internal/ErrorBoundary.tsx @@ -0,0 +1,50 @@ +import React from 'react' + +type ErrorBoundaryProps = { + onError: (error: Error, componentStack: string | null) => void + globalOverlay?: boolean + isMounted?: boolean +} +type ErrorBoundaryState = { error: Error | null } + +class ErrorBoundary extends React.PureComponent< + ErrorBoundaryProps, + ErrorBoundaryState +> { + state = { error: null } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + componentDidCatch( + error: Error, + // Loosely typed because it depends on the React version and was + // accidentally excluded in some versions. + errorInfo?: { componentStack?: string | null } + ) { + this.props.onError(error, errorInfo?.componentStack || null) + if (!this.props.globalOverlay) { + this.setState({ error }) + } + } + + render() { + // The component has to be unmounted or else it would continue to error + return this.state.error || + (this.props.globalOverlay && this.props.isMounted) ? ( + // When the overlay is global for the application and it wraps a component rendering `` + // we have to render the html shell otherwise the shadow root will not be able to attach + this.props.globalOverlay ? ( + +
+ + + ) : null + ) : ( + this.props.children + ) + } +} + +export { ErrorBoundary } diff --git a/packages/next/client/components/react-dev-overlay/src/internal/ReactDevOverlay.tsx b/packages/next/client/components/react-dev-overlay/src/internal/ReactDevOverlay.tsx new file mode 100644 index 0000000000000..707e982dcb50a --- /dev/null +++ b/packages/next/client/components/react-dev-overlay/src/internal/ReactDevOverlay.tsx @@ -0,0 +1,127 @@ +import * as React from 'react' + +import * as Bus from './bus' +import { ShadowPortal } from './components/ShadowPortal' +import { BuildError } from './container/BuildError' +import { Errors, SupportedErrorEvent } from './container/Errors' +import { ErrorBoundary } from './ErrorBoundary' +import { Base } from './styles/Base' +import { ComponentStyles } from './styles/ComponentStyles' +import { CssReset } from './styles/CssReset' + +type OverlayState = { + nextId: number + buildError: string | null + errors: SupportedErrorEvent[] +} + +function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState { + switch (ev.type) { + case Bus.TYPE_BUILD_OK: { + return { ...state, buildError: null } + } + case Bus.TYPE_BUILD_ERROR: { + return { ...state, buildError: ev.message } + } + case Bus.TYPE_REFRESH: { + return { ...state, buildError: null, errors: [] } + } + case Bus.TYPE_UNHANDLED_ERROR: + case Bus.TYPE_UNHANDLED_REJECTION: { + return { + ...state, + nextId: state.nextId + 1, + errors: [ + ...state.errors.filter((err) => { + // Filter out duplicate errors + return err.event.reason !== ev.reason + }), + { id: state.nextId, event: ev }, + ], + } + } + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _: never = ev + return state + } + } +} + +type ErrorType = 'runtime' | 'build' + +const shouldPreventDisplay = ( + errorType?: ErrorType | null, + preventType?: ErrorType[] | null +) => { + if (!preventType || !errorType) { + return false + } + return preventType.includes(errorType) +} + +type ReactDevOverlayProps = { + children?: React.ReactNode + preventDisplay?: ErrorType[] + globalOverlay?: boolean +} + +const ReactDevOverlay: React.FunctionComponent+ + {getFrameSource(stackFrame)} @ {stackFrame.methodName} + + +
+
+ {decoded.map((entry, index) => (
+
+ {entry.content}
+
+ ))}
+
+ content
' + }, +} + +// elements with display:flex are focusable in IE10-11 +var focusFlexboxContainer = { + element: 'span', + mutate: function mutate(element) { + element.setAttribute( + 'style', + 'display: -webkit-flex; display: -ms-flexbox; display: flex;' + ) + element.innerHTML = 'hello' + }, +} + +// form[tabindex=0][disabled] should be focusable as the +// specification doesn't know the disabled attribute on the form element +// @specification https://www.w3.org/TR/html5/forms.html#the-form-element +var focusFormDisabled = { + element: 'form', + mutate: function mutate(element) { + element.setAttribute('tabindex', 0) + element.setAttribute('disabled', 'disabled') + }, +} + +// NOTE: https://github.com/medialize/ally.js/issues/35 +// fixes https://github.com/medialize/ally.js/issues/20 +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-ismap +var focusImgIsmap = { + element: 'a', + mutate: function mutate(element) { + element.href = '#void' + element.innerHTML = '' + return element.querySelector('img') + }, +} + +// NOTE: https://github.com/medialize/ally.js/issues/35 +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-usemap +var focusImgUsemapTabindex = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + return element.querySelector('img') + }, +} + +var focusInHiddenIframe = { + element: function element(wrapper, _document) { + var iframe = _document.createElement('iframe') + + // iframe must be part of the DOM before accessing the contentWindow is possible + wrapper.appendChild(iframe) + + // create the iframe's default document () + var iframeDocument = iframe.contentWindow.document + iframeDocument.open() + iframeDocument.close() + return iframe + }, + mutate: function mutate(iframe) { + iframe.style.visibility = 'hidden' + + var iframeDocument = iframe.contentWindow.document + var input = iframeDocument.createElement('input') + iframeDocument.body.appendChild(input) + return input + }, + validate: function validate(iframe) { + var iframeDocument = iframe.contentWindow.document + var focus = iframeDocument.querySelector('input') + return iframeDocument.activeElement === focus + }, +} + +var result = !platform.is.WEBKIT + +function focusInZeroDimensionObject() { + return result +} + +// Firefox allows *any* value and treats invalid values like tabindex="-1" +// @browser-issue Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 +var focusInvalidTabindex = { + element: 'div', + mutate: function mutate(element) { + element.setAttribute('tabindex', 'invalid-value') + }, +} + +var focusLabelTabindex = { + element: 'label', + mutate: function mutate(element) { + element.setAttribute('tabindex', '-1') + }, + validate: function validate(element, focusTarget, _document) { + // force layout in Chrome 49, otherwise the element won't be focusable + /* eslint-disable no-unused-vars */ + var variableToPreventDeadCodeElimination = element.offsetHeight + /* eslint-enable no-unused-vars */ + element.focus() + return _document.activeElement === element + }, +} + +var svg = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtb' + + 'G5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBpZD0ic3ZnIj48dGV4dCB4PSIxMCIgeT0iMjAiIGlkPSJ' + + 'zdmctbGluay10ZXh0Ij50ZXh0PC90ZXh0Pjwvc3ZnPg==' + +// Note: IE10 on BrowserStack does not like this test + +var focusObjectSvgHidden = { + element: 'object', + mutate: function mutate(element) { + element.setAttribute('type', 'image/svg+xml') + element.setAttribute('data', svg) + element.setAttribute('width', '200') + element.setAttribute('height', '50') + element.style.visibility = 'hidden' + }, +} + +// Note: IE10 on BrowserStack does not like this test + +var focusObjectSvg = { + name: 'can-focus-object-svg', + element: 'object', + mutate: function mutate(element) { + element.setAttribute('type', 'image/svg+xml') + element.setAttribute('data', svg) + element.setAttribute('width', '200') + element.setAttribute('height', '50') + }, + validate: function validate(element, focusTarget, _document) { + if (platform.is.GECKO) { + // Firefox seems to be handling the object creation asynchronously and thereby produces a false negative test result. + // Because we know Firefox is able to focus object elements referencing SVGs, we simply cheat by sniffing the user agent string + return true + } + + return _document.activeElement === element + }, +} + +// Every Environment except IE9 considers SWF objects focusable +var result$1 = !platform.is.IE9 + +function focusObjectSwf() { + return result$1 +} + +var focusRedirectImgUsemap = { + element: 'div', + mutate: function mutate(element) { + element.innerHTML = + '' + + '' + + // focus the , not the