will be formatted to empty string,
+ // we'll skip rendering the frame in this case.
+ const fileSource = getFrameSource(f)
+ if (!fileSource) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
{fileSource}
+
+
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/ComponentStackFrameRow.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/ComponentStackFrameRow.tsx
new file mode 100644
index 0000000000000..774d39d1c1d0d
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/ComponentStackFrameRow.tsx
@@ -0,0 +1,104 @@
+import React from 'react'
+import type { ComponentStackFrame } from '../../helpers/parse-component-stack'
+import { useOpenInEditor } from '../../helpers/use-open-in-editor'
+import { HotlinkedText } from '../../components/hot-linked-text'
+
+function EditorLink({
+ children,
+ componentStackFrame: { file, column, lineNumber },
+}: {
+ children: React.ReactNode
+ componentStackFrame: ComponentStackFrame
+}) {
+ const open = useOpenInEditor({
+ file,
+ column,
+ lineNumber,
+ })
+
+ return (
+
+ )
+}
+
+function formatLineNumber(lineNumber: number, column: number | undefined) {
+ if (!column) {
+ return lineNumber
+ }
+
+ return `${lineNumber}:${column}`
+}
+
+function LocationLine({
+ componentStackFrame,
+}: {
+ componentStackFrame: ComponentStackFrame
+}) {
+ const { file, lineNumber, column } = componentStackFrame
+ return (
+ <>
+ {file} {lineNumber ? `(${formatLineNumber(lineNumber, column)})` : ''}
+ >
+ )
+}
+
+function SourceLocation({
+ componentStackFrame,
+}: {
+ componentStackFrame: ComponentStackFrame
+}) {
+ const { file, canOpenInEditor } = componentStackFrame
+
+ if (file && canOpenInEditor) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+export function ComponentStackFrameRow({
+ componentStackFrame,
+}: {
+ componentStackFrame: ComponentStackFrame
+}) {
+ const { component } = componentStackFrame
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/component-stack-pseudo-html.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/component-stack-pseudo-html.tsx
new file mode 100644
index 0000000000000..5f893ae26e2d8
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/component-stack-pseudo-html.tsx
@@ -0,0 +1,309 @@
+import { useMemo, Fragment, useState } from 'react'
+import type { ComponentStackFrame } from '../../helpers/parse-component-stack'
+import { CollapseIcon } from '../../icons/CollapseIcon'
+
+function getAdjacentProps(isAdj: boolean) {
+ return { 'data-nextjs-container-errors-pseudo-html--tag-adjacent': isAdj }
+}
+
+/**
+ *
+ * Format component stack into pseudo HTML
+ * component stack is an array of strings, e.g.: ['p', 'p', 'Page', ...]
+ *
+ * For html tags mismatch, it will render it for the code block
+ *
+ * ```
+ *
+ * {`
+ *
+ *
+ *
+ * `}
+ *
+ * ```
+ *
+ * For text mismatch, it will render it for the code block
+ *
+ * ```
+ *
+ * {`
+ *
+ *
+ * "Server Text" (green)
+ * "Client Text" (red)
+ *
+ *
+ * `}
+ * ```
+ *
+ * For bad text under a tag it will render it for the code block,
+ * e.g. "Mismatched Text" under
+ *
+ * ```
+ *
+ * {`
+ *
+ *
+ *
+ * "Mismatched Text" (red)
+ *
+ *
+ *
+ * `}
+ * ```
+ *
+ */
+export function PseudoHtmlDiff({
+ componentStackFrames,
+ firstContent,
+ secondContent,
+ hydrationMismatchType,
+ reactOutputComponentDiff,
+ ...props
+}: {
+ componentStackFrames: ComponentStackFrame[]
+ firstContent: string
+ secondContent: string
+ reactOutputComponentDiff: string | undefined
+ hydrationMismatchType: 'tag' | 'text' | 'text-in-tag'
+} & React.HTMLAttributes) {
+ const isHtmlTagsWarning = hydrationMismatchType === 'tag'
+ const isReactHydrationDiff = !!reactOutputComponentDiff
+
+ // For text mismatch, mismatched text will take 2 rows, so we display 4 rows of component stack
+ const MAX_NON_COLLAPSED_FRAMES = isHtmlTagsWarning ? 6 : 4
+ const [isHtmlCollapsed, toggleCollapseHtml] = useState(true)
+
+ const htmlComponents = useMemo(() => {
+ const componentStacks: React.ReactNode[] = []
+ // React 19 unified mismatch
+ if (isReactHydrationDiff) {
+ let currentComponentIndex = componentStackFrames.length - 1
+ const reactComponentDiffLines = reactOutputComponentDiff.split('\n')
+ const diffHtmlStack: React.ReactNode[] = []
+ reactComponentDiffLines.forEach((line, index) => {
+ let trimmedLine = line.trim()
+ const isDiffLine = trimmedLine[0] === '+' || trimmedLine[0] === '-'
+ const spaces = ' '.repeat(Math.max(componentStacks.length * 2, 1))
+
+ if (isDiffLine) {
+ const sign = trimmedLine[0]
+ trimmedLine = trimmedLine.slice(1).trim() // trim spaces after sign
+ diffHtmlStack.push(
+
+ {sign}
+ {spaces}
+ {trimmedLine}
+ {'\n'}
+
+ )
+ } else if (currentComponentIndex >= 0) {
+ const isUserLandComponent = trimmedLine.startsWith(
+ '<' + componentStackFrames[currentComponentIndex].component
+ )
+ // If it's matched userland component or it's ... we will keep the component stack in diff
+ if (isUserLandComponent || trimmedLine === '...') {
+ currentComponentIndex--
+ componentStacks.push(
+
+ {spaces}
+ {trimmedLine}
+ {'\n'}
+
+ )
+ } else if (!isHtmlCollapsed) {
+ componentStacks.push(
+
+ {spaces}
+ {trimmedLine}
+ {'\n'}
+
+ )
+ }
+ } else if (!isHtmlCollapsed) {
+ // In general, if it's not collapsed, show the whole diff
+ componentStacks.push(
+
+ {spaces}
+ {trimmedLine}
+ {'\n'}
+
+ )
+ }
+ })
+ return componentStacks.concat(diffHtmlStack)
+ }
+
+ const nestedHtmlStack: React.ReactNode[] = []
+ const tagNames = isHtmlTagsWarning
+ ? // tags could have < or > in the name, so we always remove them to match
+ [firstContent.replace(/<|>/g, ''), secondContent.replace(/<|>/g, '')]
+ : []
+
+ let lastText = ''
+
+ const componentStack = componentStackFrames
+ .map((frame) => frame.component)
+ .reverse()
+
+ // [child index, parent index]
+ const matchedIndex = [-1, -1]
+ if (isHtmlTagsWarning) {
+ // Reverse search for the child tag
+ for (let i = componentStack.length - 1; i >= 0; i--) {
+ if (componentStack[i] === tagNames[0]) {
+ matchedIndex[0] = i
+ break
+ }
+ }
+ // Start searching parent tag from child tag above
+ for (let i = matchedIndex[0] - 1; i >= 0; i--) {
+ if (componentStack[i] === tagNames[1]) {
+ matchedIndex[1] = i
+ break
+ }
+ }
+ }
+
+ componentStack.forEach((component, index, componentList) => {
+ const spaces = ' '.repeat(nestedHtmlStack.length * 2)
+
+ // When component is the server or client tag name, highlight it
+ const isHighlightedTag = isHtmlTagsWarning
+ ? index === matchedIndex[0] || index === matchedIndex[1]
+ : tagNames.includes(component)
+ const isAdjacentTag =
+ isHighlightedTag ||
+ Math.abs(index - matchedIndex[0]) <= 1 ||
+ Math.abs(index - matchedIndex[1]) <= 1
+
+ const isLastFewFrames =
+ !isHtmlTagsWarning && index >= componentList.length - 6
+
+ const adjProps = getAdjacentProps(isAdjacentTag)
+
+ if ((isHtmlTagsWarning && isAdjacentTag) || isLastFewFrames) {
+ const codeLine = (
+
+ {spaces}
+
+ {`<${component}>\n`}
+
+
+ )
+ lastText = component
+
+ const wrappedCodeLine = (
+
+ {codeLine}
+ {/* Add ^^^^ to the target tags used for snapshots but not displayed for users */}
+ {isHighlightedTag && (
+
+ {spaces + '^'.repeat(component.length + 2) + '\n'}
+
+ )}
+
+ )
+ nestedHtmlStack.push(wrappedCodeLine)
+ } else {
+ if (
+ nestedHtmlStack.length >= MAX_NON_COLLAPSED_FRAMES &&
+ isHtmlCollapsed
+ ) {
+ return
+ }
+
+ if (!isHtmlCollapsed || isLastFewFrames) {
+ nestedHtmlStack.push(
+
+ {spaces}
+ {'<' + component + '>\n'}
+
+ )
+ } else if (isHtmlCollapsed && lastText !== '...') {
+ lastText = '...'
+ nestedHtmlStack.push(
+
+ {spaces}
+ {'...\n'}
+
+ )
+ }
+ }
+ })
+ // Hydration mismatch: text or text-tag
+ if (!isHtmlTagsWarning) {
+ const spaces = ' '.repeat(nestedHtmlStack.length * 2)
+ let wrappedCodeLine
+ if (hydrationMismatchType === 'text') {
+ // hydration type is "text", represent [server content, client content]
+ wrappedCodeLine = (
+
+
+ {spaces + `"${firstContent}"\n`}
+
+
+ {spaces + `"${secondContent}"\n`}
+
+
+ )
+ } else if (hydrationMismatchType === 'text-in-tag') {
+ // hydration type is "text-in-tag", represent [parent tag, mismatch content]
+ wrappedCodeLine = (
+
+
+ {spaces + `<${secondContent}>\n`}
+
+
+ {spaces + ` "${firstContent}"\n`}
+
+
+ )
+ }
+ nestedHtmlStack.push(wrappedCodeLine)
+ }
+
+ return nestedHtmlStack
+ }, [
+ componentStackFrames,
+ isHtmlCollapsed,
+ firstContent,
+ secondContent,
+ isHtmlTagsWarning,
+ hydrationMismatchType,
+ MAX_NON_COLLAPSED_FRAMES,
+ isReactHydrationDiff,
+ reactOutputComponentDiff,
+ ])
+
+ return (
+
+
+
+ {htmlComponents}
+
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/index.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/index.tsx
new file mode 100644
index 0000000000000..d1dfabbbac359
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RuntimeError/index.tsx
@@ -0,0 +1,231 @@
+import * as React from 'react'
+import { CodeFrame } from '../../components/CodeFrame'
+import type { ReadyRuntimeError } from '../../helpers/get-error-by-type'
+import { noop as css } from '../../helpers/noop-template'
+import { CallStackFrame } from './CallStackFrame'
+
+export type RuntimeErrorProps = { error: ReadyRuntimeError }
+
+export function RuntimeError({ error }: RuntimeErrorProps) {
+ const [isIgnoredExpanded, setIsIgnoredExpanded] = React.useState(false)
+ const {
+ firstFrame,
+ allLeadingFrames,
+ trailingCallStackFrames,
+ displayedFramesCount,
+ } = React.useMemo(() => {
+ const filteredFrames = error.frames.filter((frame) =>
+ isIgnoredExpanded ? true : !frame.ignored
+ )
+
+ const firstFirstPartyFrameIndex = filteredFrames.findIndex(
+ (entry) =>
+ !entry.ignored &&
+ Boolean(entry.originalCodeFrame) &&
+ Boolean(entry.originalStackFrame)
+ )
+
+ return {
+ displayedFramesCount: filteredFrames.length,
+ firstFrame: filteredFrames[firstFirstPartyFrameIndex] ?? null,
+ allLeadingFrames:
+ firstFirstPartyFrameIndex < 0
+ ? []
+ : filteredFrames.slice(0, firstFirstPartyFrameIndex),
+ trailingCallStackFrames: filteredFrames.slice(
+ firstFirstPartyFrameIndex + 1
+ ),
+ }
+ }, [error.frames, isIgnoredExpanded])
+
+ return (
+
+ {firstFrame ? (
+ <>
+ Source
+ {allLeadingFrames.map((frame, frameIndex) => (
+
+ ))}
+
+ >
+ ) : undefined}
+
+ {trailingCallStackFrames.map((frame, frameIndex) => (
+
+ ))}
+ {
+ // if the default displayed ignored frames count is equal equal to the total frames count, hide the button
+ displayedFramesCount === error.frames.length &&
+ !isIgnoredExpanded ? null : (
+
+ )
+ }
+
+ )
+}
+
+export const styles = css`
+ [data-nextjs-call-stack-frame]:not(:last-child),
+ [data-nextjs-component-stack-frame]:not(:last-child) {
+ margin-bottom: var(--size-gap-double);
+ }
+
+ [data-expand-ignore-button]:focus:not(:focus-visible),
+ [data-expand-ignore-button] {
+ background: none;
+ border: none;
+ color: var(--color-font);
+ cursor: pointer;
+ font-size: var(--size-font);
+ margin: var(--size-gap) 0;
+ padding: 0;
+ text-decoration: underline;
+ outline: none;
+ }
+
+ [data-nextjs-data-runtime-error-copy-button],
+ [data-nextjs-data-runtime-error-copy-button]:focus:not(:focus-visible) {
+ position: relative;
+ margin-left: var(--size-gap);
+ padding: 0;
+ border: none;
+ background: none;
+ outline: none;
+ }
+ [data-nextjs-data-runtime-error-copy-button] > svg {
+ vertical-align: middle;
+ }
+ .nextjs-data-runtime-error-copy-button {
+ color: inherit;
+ }
+ .nextjs-data-runtime-error-copy-button--initial:hover {
+ cursor: pointer;
+ }
+ .nextjs-data-runtime-error-copy-button[aria-disabled='true'] {
+ opacity: 0.3;
+ cursor: not-allowed;
+ }
+ .nextjs-data-runtime-error-copy-button--error,
+ .nextjs-data-runtime-error-copy-button--error:hover {
+ color: var(--color-ansi-red);
+ }
+ .nextjs-data-runtime-error-copy-button--success {
+ color: var(--color-ansi-green);
+ }
+
+ [data-nextjs-call-stack-frame] > h3,
+ [data-nextjs-component-stack-frame] > h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-family: var(--font-stack-monospace);
+ font-size: var(--size-font);
+ }
+ [data-nextjs-call-stack-frame] > h3[data-nextjs-frame-expanded='false'] {
+ color: #666;
+ display: inline-block;
+ }
+ [data-nextjs-call-stack-frame] > div,
+ [data-nextjs-component-stack-frame] > div {
+ display: flex;
+ align-items: center;
+ padding-left: calc(var(--size-gap) + var(--size-gap-half));
+ font-size: var(--size-font-small);
+ color: #999;
+ }
+ [data-nextjs-call-stack-frame] > div > svg,
+ [data-nextjs-component-stack-frame] > [role='link'] > svg {
+ width: auto;
+ height: var(--size-font-small);
+ margin-left: var(--size-gap);
+ flex-shrink: 0;
+ display: none;
+ }
+
+ [data-nextjs-call-stack-frame] > div[data-has-source],
+ [data-nextjs-component-stack-frame] > [role='link'] {
+ cursor: pointer;
+ }
+ [data-nextjs-call-stack-frame] > div[data-has-source]:hover,
+ [data-nextjs-component-stack-frame] > [role='link']:hover {
+ text-decoration: underline dotted;
+ }
+ [data-nextjs-call-stack-frame] > div[data-has-source] > svg,
+ [data-nextjs-component-stack-frame] > [role='link'] > svg {
+ display: unset;
+ }
+
+ [data-nextjs-call-stack-framework-icon] {
+ margin-right: var(--size-gap);
+ }
+ [data-nextjs-call-stack-framework-icon='next'] > mask {
+ mask-type: alpha;
+ }
+ [data-nextjs-call-stack-framework-icon='react'] {
+ color: rgb(20, 158, 202);
+ }
+ [data-nextjs-collapsed-call-stack-details][open]
+ [data-nextjs-call-stack-chevron-icon] {
+ transform: rotate(90deg);
+ }
+ [data-nextjs-collapsed-call-stack-details] summary {
+ display: flex;
+ align-items: center;
+ margin-bottom: var(--size-gap);
+ list-style: none;
+ }
+ [data-nextjs-collapsed-call-stack-details] summary::-webkit-details-marker {
+ display: none;
+ }
+
+ [data-nextjs-collapsed-call-stack-details] h3 {
+ color: #666;
+ }
+ [data-nextjs-collapsed-call-stack-details] [data-nextjs-call-stack-frame] {
+ margin-bottom: var(--size-gap-double);
+ }
+
+ [data-nextjs-container-errors-pseudo-html] {
+ position: relative;
+ }
+ [data-nextjs-container-errors-pseudo-html-collapse] {
+ position: absolute;
+ left: 10px;
+ top: 10px;
+ color: inherit;
+ background: none;
+ border: none;
+ padding: 0;
+ }
+ [data-nextjs-container-errors-pseudo-html--diff='add'] {
+ color: var(--color-ansi-green);
+ }
+ [data-nextjs-container-errors-pseudo-html--diff='remove'] {
+ color: var(--color-ansi-red);
+ }
+ [data-nextjs-container-errors-pseudo-html--tag-error] {
+ color: var(--color-ansi-red);
+ font-weight: bold;
+ }
+ /* hide but text are still accessible in DOM */
+ [data-nextjs-container-errors-pseudo-html--hint] {
+ display: inline-block;
+ font-size: 0;
+ }
+ [data-nextjs-container-errors-pseudo-html--tag-adjacent='false'] {
+ color: var(--color-accents-1);
+ }
+`
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/StaticIndicator.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/StaticIndicator.tsx
new file mode 100644
index 0000000000000..5d9a19fadc252
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/StaticIndicator.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react'
+import { Toast } from '../components/Toast'
+import { LightningBolt } from '../icons/LightningBolt'
+import { CloseIcon } from '../icons/CloseIcon'
+import type { Dispatcher } from '../../../app/hot-reloader-client'
+
+export function StaticIndicator({ dispatcher }: { dispatcher?: Dispatcher }) {
+ return (
+
+
+
+
+
+ Static route
+
+
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/root-layout-missing-tags-error.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/root-layout-missing-tags-error.tsx
new file mode 100644
index 0000000000000..407b09a5fd9a9
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/root-layout-missing-tags-error.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react'
+import type { VersionInfo } from '../../../../../../server/dev/parse-version-info'
+import { Dialog, DialogContent, DialogHeader } from '../components/Dialog'
+import { Overlay } from '../components/Overlay'
+import { VersionStalenessInfo } from '../components/VersionStalenessInfo'
+import { HotlinkedText } from '../components/hot-linked-text'
+
+type RootLayoutMissingTagsErrorProps = {
+ missingTags: string[]
+ versionInfo?: VersionInfo
+}
+
+export const RootLayoutMissingTagsError: React.FC =
+ function RootLayoutMissingTagsError({ missingTags, versionInfo }) {
+ const noop = React.useCallback(() => {}, [])
+ return (
+
+
+
+ )
+ }
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/attach-hydration-error-state.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/attach-hydration-error-state.ts
new file mode 100644
index 0000000000000..8ee6c80c707be
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/attach-hydration-error-state.ts
@@ -0,0 +1,46 @@
+import {
+ isHydrationError,
+ getDefaultHydrationErrorMessage,
+} from '../../../../is-hydration-error'
+import {
+ hydrationErrorState,
+ getReactHydrationDiffSegments,
+} from './hydration-error-info'
+
+export function attachHydrationErrorState(error: Error) {
+ if (
+ isHydrationError(error) &&
+ !error.message.includes(
+ 'https://nextjs.org/docs/messages/react-hydration-error'
+ )
+ ) {
+ const reactHydrationDiffSegments = getReactHydrationDiffSegments(
+ error.message
+ )
+ let parsedHydrationErrorState: typeof hydrationErrorState = {}
+ if (reactHydrationDiffSegments) {
+ parsedHydrationErrorState = {
+ ...(error as any).details,
+ ...hydrationErrorState,
+ warning: hydrationErrorState.warning || [
+ getDefaultHydrationErrorMessage(),
+ ],
+ notes: reactHydrationDiffSegments[0],
+ reactOutputComponentDiff: reactHydrationDiffSegments[1],
+ }
+ } else {
+ // If there's any extra information in the error message to display,
+ // append it to the error message details property
+ if (hydrationErrorState.warning) {
+ // The patched console.error found hydration errors logged by React
+ // Append the logged warning to the error message
+ parsedHydrationErrorState = {
+ ...(error as any).details,
+ // It contains the warning, component stack, server and client tag names
+ ...hydrationErrorState,
+ }
+ }
+ }
+ ;(error as any).details = parsedHydrationErrorState
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/console-error.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/console-error.ts
new file mode 100644
index 0000000000000..225452dcc4abd
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/console-error.ts
@@ -0,0 +1,29 @@
+// To distinguish from React error.digest, we use a different symbol here to determine if the error is from console.error or unhandled promise rejection.
+const digestSym = Symbol.for('next.console.error.digest')
+const consoleTypeSym = Symbol.for('next.console.error.type')
+
+// Represent non Error shape unhandled promise rejections or console.error errors.
+// Those errors will be captured and displayed in Error Overlay.
+type UnhandledError = Error & {
+ [digestSym]: 'NEXT_UNHANDLED_ERROR'
+ [consoleTypeSym]: 'string' | 'error'
+}
+
+export function createUnhandledError(message: string | Error): UnhandledError {
+ const error = (
+ typeof message === 'string' ? new Error(message) : message
+ ) as UnhandledError
+ error[digestSym] = 'NEXT_UNHANDLED_ERROR'
+ error[consoleTypeSym] = typeof message === 'string' ? 'string' : 'error'
+ return error
+}
+
+export const isUnhandledConsoleOrRejection = (
+ error: any
+): error is UnhandledError => {
+ return error && error[digestSym] === 'NEXT_UNHANDLED_ERROR'
+}
+
+export const getUnhandledErrorType = (error: UnhandledError) => {
+ return error[consoleTypeSym]
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/enqueue-client-error.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/enqueue-client-error.ts
new file mode 100644
index 0000000000000..8e2df164dddd2
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/enqueue-client-error.ts
@@ -0,0 +1,22 @@
+import { isHydrationError } from '../../../../is-hydration-error'
+
+// Dedupe the two consecutive errors: If the previous one is same as current one, ignore the current one.
+export function enqueueConsecutiveDedupedError(
+ queue: Array,
+ error: Error
+) {
+ const isFront = isHydrationError(error)
+ const previousError = isFront ? queue[0] : queue[queue.length - 1]
+ // Compare the error stack to dedupe the consecutive errors
+ if (previousError && previousError.stack === error.stack) {
+ return
+ }
+ // TODO: change all to push error into errorQueue,
+ // currently there's a async api error is always erroring while hydration error showing up.
+ // Move hydration error to the front of the queue to unblock.
+ if (isFront) {
+ queue.unshift(error)
+ } else {
+ queue.push(error)
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/format-webpack-messages.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/format-webpack-messages.ts
new file mode 100644
index 0000000000000..a1b27a8d29499
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/format-webpack-messages.ts
@@ -0,0 +1,223 @@
+/**
+MIT License
+
+Copyright (c) 2015-present, Facebook, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+import stripAnsi from 'next/dist/compiled/strip-ansi'
+// This file is based on https://github.com/facebook/create-react-app/blob/7b1a32be6ec9f99a6c9a3c66813f3ac09c4736b9/packages/react-dev-utils/formatWebpackMessages.js
+// It's been edited to remove chalk and CRA-specific logic
+
+const friendlySyntaxErrorLabel = 'Syntax error:'
+
+const WEBPACK_BREAKING_CHANGE_POLYFILLS =
+ '\n\nBREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.'
+
+function isLikelyASyntaxError(message: string) {
+ return stripAnsi(message).includes(friendlySyntaxErrorLabel)
+}
+
+let hadMissingSassError = false
+
+// Cleans up webpack error messages.
+function formatMessage(
+ message: any,
+ verbose?: boolean,
+ importTraceNote?: boolean
+) {
+ // TODO: Replace this once webpack 5 is stable
+ if (typeof message === 'object' && message.message) {
+ const filteredModuleTrace =
+ message.moduleTrace &&
+ message.moduleTrace.filter(
+ (trace: any) =>
+ !/next-(middleware|client-pages|route|edge-function)-loader\.js/.test(
+ trace.originName
+ )
+ )
+
+ let body = message.message
+ const breakingChangeIndex = body.indexOf(WEBPACK_BREAKING_CHANGE_POLYFILLS)
+ if (breakingChangeIndex >= 0) {
+ body = body.slice(0, breakingChangeIndex)
+ }
+
+ message =
+ (message.moduleName ? stripAnsi(message.moduleName) + '\n' : '') +
+ (message.file ? stripAnsi(message.file) + '\n' : '') +
+ body +
+ (message.details && verbose ? '\n' + message.details : '') +
+ (filteredModuleTrace && filteredModuleTrace.length
+ ? (importTraceNote || '\n\nImport trace for requested module:') +
+ filteredModuleTrace
+ .map((trace: any) => `\n${trace.moduleName}`)
+ .join('')
+ : '') +
+ (message.stack && verbose ? '\n' + message.stack : '')
+ }
+ let lines = message.split('\n')
+
+ // Strip Webpack-added headers off errors/warnings
+ // https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
+ lines = lines.filter((line: string) => !/Module [A-z ]+\(from/.test(line))
+
+ // Transform parsing error into syntax error
+ // TODO: move this to our ESLint formatter?
+ lines = lines.map((line: string) => {
+ const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(
+ line
+ )
+ if (!parsingError) {
+ return line
+ }
+ const [, errorLine, errorColumn, errorMessage] = parsingError
+ return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`
+ })
+
+ message = lines.join('\n')
+ // Smoosh syntax errors (commonly found in CSS)
+ message = message.replace(
+ /SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
+ `${friendlySyntaxErrorLabel} $3 ($1:$2)\n`
+ )
+ // Clean up export errors
+ message = message.replace(
+ /^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
+ `Attempted import error: '$1' is not exported from '$2'.`
+ )
+ message = message.replace(
+ /^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
+ `Attempted import error: '$2' does not contain a default export (imported as '$1').`
+ )
+ message = message.replace(
+ /^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
+ `Attempted import error: '$1' is not exported from '$3' (imported as '$2').`
+ )
+ lines = message.split('\n')
+
+ // Remove leading newline
+ if (lines.length > 2 && lines[1].trim() === '') {
+ lines.splice(1, 1)
+ }
+
+ // Cleans up verbose "module not found" messages for files and packages.
+ if (lines[1] && lines[1].startsWith('Module not found: ')) {
+ lines = [
+ lines[0],
+ lines[1]
+ .replace('Error: ', '')
+ .replace('Module not found: Cannot find file:', 'Cannot find file:'),
+ ...lines.slice(2),
+ ]
+ }
+
+ // Add helpful message for users trying to use Sass for the first time
+ if (lines[1] && lines[1].match(/Cannot find module.+sass/)) {
+ // ./file.module.scss (<>) => ./file.module.scss
+ const firstLine = lines[0].split('!')
+ lines[0] = firstLine[firstLine.length - 1]
+
+ lines[1] =
+ "To use Next.js' built-in Sass support, you first need to install `sass`.\n"
+ lines[1] += 'Run `npm i sass` or `yarn add sass` inside your workspace.\n'
+ lines[1] += '\nLearn more: https://nextjs.org/docs/messages/install-sass'
+
+ // dispose of unhelpful stack trace
+ lines = lines.slice(0, 2)
+ hadMissingSassError = true
+ } else if (
+ hadMissingSassError &&
+ message.match(/(sass-loader|resolve-url-loader: CSS error)/)
+ ) {
+ // dispose of unhelpful stack trace following missing sass module
+ lines = []
+ }
+
+ if (!verbose) {
+ message = lines.join('\n')
+ // Internal stacks are generally useless so we strip them... with the
+ // exception of stacks containing `webpack:` because they're normally
+ // from user code generated by Webpack. For more information see
+ // https://github.com/facebook/create-react-app/pull/1050
+ message = message.replace(
+ /^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,
+ ''
+ ) // at ... ...:x:y
+ message = message.replace(/^\s*at\s(\n|$)/gm, '') // at
+
+ message = message.replace(
+ /File was processed with these loaders:\n(.+[\\/](next[\\/]dist[\\/].+|@next[\\/]react-refresh-utils[\\/]loader)\.js\n)*You may need an additional loader to handle the result of these loaders.\n/g,
+ ''
+ )
+
+ lines = message.split('\n')
+ }
+
+ // Remove duplicated newlines
+ lines = (lines as string[]).filter(
+ (line, index, arr) =>
+ index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()
+ )
+
+ // Reassemble the message
+ message = lines.join('\n')
+ return message.trim()
+}
+
+export default function formatWebpackMessages(json: any, verbose?: boolean) {
+ const formattedErrors = json.errors.map((message: any) => {
+ const isUnknownNextFontError = message.message.includes(
+ 'An error occurred in `next/font`.'
+ )
+ return formatMessage(message, isUnknownNextFontError || verbose)
+ })
+ const formattedWarnings = json.warnings.map((message: any) => {
+ return formatMessage(message, verbose)
+ })
+
+ // Reorder errors to put the most relevant ones first.
+ let reactServerComponentsError = -1
+
+ for (let i = 0; i < formattedErrors.length; i++) {
+ const error = formattedErrors[i]
+ if (error.includes('ReactServerComponentsError')) {
+ reactServerComponentsError = i
+ break
+ }
+ }
+
+ // Move the reactServerComponentsError to the top if it exists
+ if (reactServerComponentsError !== -1) {
+ const error = formattedErrors.splice(reactServerComponentsError, 1)
+ formattedErrors.unshift(error[0])
+ }
+
+ const result = {
+ ...json,
+ errors: formattedErrors,
+ warnings: formattedWarnings,
+ }
+ if (!verbose && result.errors.some(isLikelyASyntaxError)) {
+ // If there are any syntax errors, show just them.
+ result.errors = result.errors.filter(isLikelyASyntaxError)
+ result.warnings = []
+ }
+ return result
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts
new file mode 100644
index 0000000000000..3521f27d6ecec
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts
@@ -0,0 +1,50 @@
+import {
+ ACTION_UNHANDLED_ERROR,
+ ACTION_UNHANDLED_REJECTION,
+} from '../../../shared'
+import type { SupportedErrorEvent } from '../container/Errors'
+import { getOriginalStackFrames } from './stack-frame'
+import type { OriginalStackFrame } from './stack-frame'
+import type { ComponentStackFrame } from './parse-component-stack'
+import { getErrorSource } from '../../../../../../shared/lib/error-source'
+
+export type ReadyRuntimeError = {
+ id: number
+ runtime: true
+ error: Error
+ frames: OriginalStackFrame[]
+ componentStackFrames?: ComponentStackFrame[]
+}
+
+export async function getErrorByType(
+ ev: SupportedErrorEvent,
+ isAppDir: boolean
+): Promise {
+ const { id, event } = ev
+ switch (event.type) {
+ case ACTION_UNHANDLED_ERROR:
+ case ACTION_UNHANDLED_REJECTION: {
+ const readyRuntimeError: ReadyRuntimeError = {
+ id,
+ runtime: true,
+ error: event.reason,
+ frames: await getOriginalStackFrames(
+ event.frames,
+ getErrorSource(event.reason),
+ isAppDir,
+ event.reason.toString()
+ ),
+ }
+ if (event.type === ACTION_UNHANDLED_ERROR) {
+ readyRuntimeError.componentStackFrames = event.componentStackFrames
+ }
+ return readyRuntimeError
+ }
+ default: {
+ break
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _: never = event as never
+ throw new Error('type system invariant violation')
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-socket-url.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-socket-url.ts
new file mode 100644
index 0000000000000..a129b35dde08d
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-socket-url.ts
@@ -0,0 +1,26 @@
+import { normalizedAssetPrefix } from '../../../../../../shared/lib/normalized-asset-prefix'
+
+function getSocketProtocol(assetPrefix: string): string {
+ let protocol = window.location.protocol
+
+ try {
+ // assetPrefix is a url
+ protocol = new URL(assetPrefix).protocol
+ } catch {}
+
+ return protocol === 'http:' ? 'ws:' : 'wss:'
+}
+
+export function getSocketUrl(assetPrefix: string | undefined): string {
+ const prefix = normalizedAssetPrefix(assetPrefix)
+ const protocol = getSocketProtocol(assetPrefix || '')
+
+ if (URL.canParse(prefix)) {
+ // since normalized asset prefix is ensured to be a URL format,
+ // we can safely replace the protocol
+ return prefix.replace(/^http/, 'ws')
+ }
+
+ const { hostname, port } = window.location
+ return `${protocol}//${hostname}${port ? `:${port}` : ''}${prefix}`
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-source-map-from-file.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-source-map-from-file.ts
new file mode 100644
index 0000000000000..4bbf9ad7af8c9
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-source-map-from-file.ts
@@ -0,0 +1,71 @@
+import fs from 'fs/promises'
+import path from 'path'
+import url from 'url'
+import type { RawSourceMap } from 'next/dist/compiled/source-map08'
+import dataUriToBuffer from 'next/dist/compiled/data-uri-to-buffer'
+import { getSourceMapUrl } from './get-source-map-url'
+
+export async function getSourceMapFromFile(
+ filename: string
+): Promise {
+ filename = filename.startsWith('file://')
+ ? url.fileURLToPath(filename)
+ : filename
+
+ let fileContents: string
+
+ try {
+ fileContents = await fs.readFile(filename, 'utf-8')
+ } catch (error) {
+ throw new Error(`Failed to read file contents of ${filename}.`, {
+ cause: error,
+ })
+ }
+
+ const sourceUrl = getSourceMapUrl(fileContents)
+
+ if (!sourceUrl) {
+ return undefined
+ }
+
+ if (sourceUrl.startsWith('data:')) {
+ let buffer: dataUriToBuffer.MimeBuffer
+
+ try {
+ buffer = dataUriToBuffer(sourceUrl)
+ } catch (error) {
+ throw new Error(`Failed to parse source map URL for ${filename}.`, {
+ cause: error,
+ })
+ }
+
+ if (buffer.type !== 'application/json') {
+ throw new Error(
+ `Unknown source map type for ${filename}: ${buffer.typeFull}.`
+ )
+ }
+
+ try {
+ return JSON.parse(buffer.toString())
+ } catch (error) {
+ throw new Error(`Failed to parse source map for ${filename}.`, {
+ cause: error,
+ })
+ }
+ }
+
+ const sourceMapFilename = path.resolve(
+ path.dirname(filename),
+ decodeURIComponent(sourceUrl)
+ )
+
+ try {
+ const sourceMapContents = await fs.readFile(sourceMapFilename, 'utf-8')
+
+ return JSON.parse(sourceMapContents.toString())
+ } catch (error) {
+ throw new Error(`Failed to parse source map ${sourceMapFilename}.`, {
+ cause: error,
+ })
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-source-map-url.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-source-map-url.ts
new file mode 100644
index 0000000000000..9a6049cf1cdbc
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-source-map-url.ts
@@ -0,0 +1,15 @@
+export function getSourceMapUrl(fileContents: string): string | null {
+ const regex = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/gm
+ let match = null
+ for (;;) {
+ let next = regex.exec(fileContents)
+ if (next == null) {
+ break
+ }
+ match = next
+ }
+ if (!(match && match[1])) {
+ return null
+ }
+ return match[1].toString()
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/hydration-error-info.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/hydration-error-info.ts
new file mode 100644
index 0000000000000..0b1fbb6389169
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/hydration-error-info.ts
@@ -0,0 +1,105 @@
+import { getHydrationErrorStackInfo } from '../../../../is-hydration-error'
+
+export type HydrationErrorState = {
+ // Hydration warning template format:
+ warning?: [string, string, string]
+ componentStack?: string
+ serverContent?: string
+ clientContent?: string
+ // React 19 hydration diff format:
+ notes?: string
+ reactOutputComponentDiff?: string
+}
+
+type NullableText = string | null | undefined
+
+export const hydrationErrorState: HydrationErrorState = {}
+
+// https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js used as a reference
+const htmlTagsWarnings = new Set([
+ 'Warning: In HTML, %s cannot be a child of <%s>.%s\nThis will cause a hydration error.%s',
+ 'Warning: In HTML, %s cannot be a descendant of <%s>.\nThis will cause a hydration error.%s',
+ 'Warning: In HTML, text nodes cannot be a child of <%s>.\nThis will cause a hydration error.',
+ "Warning: In HTML, whitespace text nodes cannot be a child of <%s>. Make sure you don't have any extra whitespace between tags on each line of your source code.\nThis will cause a hydration error.",
+ 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
+ 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
+])
+const textAndTagsMismatchWarnings = new Set([
+ 'Warning: Expected server HTML to contain a matching text node for "%s" in <%s>.%s',
+ 'Warning: Did not expect server HTML to contain the text node "%s" in <%s>.%s',
+])
+const textMismatchWarning =
+ 'Warning: Text content did not match. Server: "%s" Client: "%s"%s'
+
+export const getHydrationWarningType = (
+ message: NullableText
+): 'tag' | 'text' | 'text-in-tag' => {
+ if (typeof message !== 'string') {
+ // TODO: Doesn't make sense to treat no message as a hydration error message.
+ // We should bail out somewhere earlier.
+ return 'text'
+ }
+
+ const normalizedMessage = message.startsWith('Warning: ')
+ ? message
+ : `Warning: ${message}`
+
+ if (isHtmlTagsWarning(normalizedMessage)) return 'tag'
+ if (isTextInTagsMismatchWarning(normalizedMessage)) return 'text-in-tag'
+
+ return 'text'
+}
+
+const isHtmlTagsWarning = (message: string) => htmlTagsWarnings.has(message)
+
+const isTextMismatchWarning = (message: string) =>
+ textMismatchWarning === message
+const isTextInTagsMismatchWarning = (msg: string) =>
+ textAndTagsMismatchWarnings.has(msg)
+
+const isKnownHydrationWarning = (message: NullableText) => {
+ if (typeof message !== 'string') {
+ return false
+ }
+ // React 18 has the `Warning: ` prefix.
+ // React 19 does not.
+ const normalizedMessage = message.startsWith('Warning: ')
+ ? message
+ : `Warning: ${message}`
+
+ return (
+ isHtmlTagsWarning(normalizedMessage) ||
+ isTextInTagsMismatchWarning(normalizedMessage) ||
+ isTextMismatchWarning(normalizedMessage)
+ )
+}
+
+export const getReactHydrationDiffSegments = (msg: NullableText) => {
+ if (msg) {
+ const { message, diff } = getHydrationErrorStackInfo(msg)
+ if (message) return [message, diff]
+ }
+ return undefined
+}
+
+/**
+ * Patch console.error to capture hydration errors.
+ * If any of the knownHydrationWarnings are logged, store the message and component stack.
+ * When the hydration runtime error is thrown, the message and component stack are added to the error.
+ * This results in a more helpful error message in the error overlay.
+ */
+
+export function storeHydrationErrorStateFromConsoleArgs(...args: any[]) {
+ const [msg, serverContent, clientContent, componentStack] = args
+ if (isKnownHydrationWarning(msg)) {
+ hydrationErrorState.warning = [
+ // remove the last %s from the message
+ msg,
+ serverContent,
+ clientContent,
+ ]
+ hydrationErrorState.componentStack = componentStack
+ hydrationErrorState.serverContent = serverContent
+ hydrationErrorState.clientContent = clientContent
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/launchEditor.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/launchEditor.ts
new file mode 100644
index 0000000000000..889ff5ed1b443
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/launchEditor.ts
@@ -0,0 +1,417 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2015-present, Facebook, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import { cyan, green, red } from '../../../../../../lib/picocolors'
+import child_process from 'child_process'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+import shellQuote from 'next/dist/compiled/shell-quote'
+
+function isTerminalEditor(editor: string) {
+ switch (editor) {
+ case 'vi':
+ case 'vim':
+ case 'nvim':
+ case 'emacs':
+ case 'nano': {
+ return true
+ }
+ default: {
+ }
+ }
+ return false
+}
+
+// Map from full process name to binary that starts the process
+// We can't just re-use full process name, because it will spawn a new instance
+// of the app every time
+const COMMON_EDITORS_MACOS = {
+ '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
+ '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
+ '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
+ '/Applications/Brackets.app/Contents/MacOS/Brackets': 'brackets',
+ '/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
+ '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
+ '/Applications/Sublime Text Dev.app/Contents/MacOS/Sublime Text':
+ '/Applications/Sublime Text Dev.app/Contents/SharedSupport/bin/subl',
+ '/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
+ '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
+ '/Applications/Visual Studio Code.app/Contents/MacOS/Electron':
+ '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
+ '/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Electron':
+ '/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code',
+ '/Applications/VSCodium.app/Contents/MacOS/Electron':
+ '/Applications/VSCodium.app/Contents/Resources/app/bin/code',
+ '/Applications/AppCode.app/Contents/MacOS/appcode':
+ '/Applications/AppCode.app/Contents/MacOS/appcode',
+ '/Applications/CLion.app/Contents/MacOS/clion':
+ '/Applications/CLion.app/Contents/MacOS/clion',
+ '/Applications/IntelliJ IDEA.app/Contents/MacOS/idea':
+ '/Applications/IntelliJ IDEA.app/Contents/MacOS/idea',
+ '/Applications/PhpStorm.app/Contents/MacOS/phpstorm':
+ '/Applications/PhpStorm.app/Contents/MacOS/phpstorm',
+ '/Applications/PyCharm.app/Contents/MacOS/pycharm':
+ '/Applications/PyCharm.app/Contents/MacOS/pycharm',
+ '/Applications/PyCharm CE.app/Contents/MacOS/pycharm':
+ '/Applications/PyCharm CE.app/Contents/MacOS/pycharm',
+ '/Applications/RubyMine.app/Contents/MacOS/rubymine':
+ '/Applications/RubyMine.app/Contents/MacOS/rubymine',
+ '/Applications/WebStorm.app/Contents/MacOS/webstorm':
+ '/Applications/WebStorm.app/Contents/MacOS/webstorm',
+ '/Applications/MacVim.app/Contents/MacOS/MacVim': 'mvim',
+ '/Applications/GoLand.app/Contents/MacOS/goland':
+ '/Applications/GoLand.app/Contents/MacOS/goland',
+ '/Applications/Rider.app/Contents/MacOS/rider':
+ '/Applications/Rider.app/Contents/MacOS/rider',
+}
+
+const COMMON_EDITORS_LINUX = {
+ atom: 'atom',
+ Brackets: 'brackets',
+ code: 'code',
+ 'code-insiders': 'code-insiders',
+ vscodium: 'vscodium',
+ emacs: 'emacs',
+ gvim: 'gvim',
+ 'idea.sh': 'idea',
+ 'phpstorm.sh': 'phpstorm',
+ 'pycharm.sh': 'pycharm',
+ 'rubymine.sh': 'rubymine',
+ sublime_text: 'sublime_text',
+ vim: 'vim',
+ nvim: 'nvim',
+ 'webstorm.sh': 'webstorm',
+ 'goland.sh': 'goland',
+ 'rider.sh': 'rider',
+}
+
+const COMMON_EDITORS_WIN = [
+ 'Brackets.exe',
+ 'Code.exe',
+ 'Code - Insiders.exe',
+ 'VSCodium.exe',
+ 'atom.exe',
+ 'sublime_text.exe',
+ 'notepad++.exe',
+ 'clion.exe',
+ 'clion64.exe',
+ 'idea.exe',
+ 'idea64.exe',
+ 'phpstorm.exe',
+ 'phpstorm64.exe',
+ 'pycharm.exe',
+ 'pycharm64.exe',
+ 'rubymine.exe',
+ 'rubymine64.exe',
+ 'webstorm.exe',
+ 'webstorm64.exe',
+ 'goland.exe',
+ 'goland64.exe',
+ 'rider.exe',
+ 'rider64.exe',
+]
+
+// Transpiled version of: /^([A-Za-z]:[/\\])?[\p{L}0-9/.\-_\\]+$/u
+// Non-transpiled version requires support for Unicode property regex. Allows
+// alphanumeric characters, periods, dashes, slashes, and underscores.
+const WINDOWS_FILE_NAME_ACCESS_LIST =
+ /^([A-Za-z]:[/\\])?(?:[\x2D-9A-Z\\_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7B9\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDF00-\uDF1C\uDF27\uDF30-\uDF45]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF1A]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFF1]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D])+$/
+
+function getArgumentsForLineNumber(
+ editor: string,
+ fileName: string,
+ lineNumber: number,
+ colNumber: number
+): string[] {
+ const editorBasename = path.basename(editor).replace(/\.(exe|cmd|bat)$/i, '')
+ switch (editorBasename) {
+ case 'atom':
+ case 'Atom':
+ case 'Atom Beta':
+ case 'subl':
+ case 'sublime':
+ case 'sublime_text': {
+ return [fileName + ':' + lineNumber + ':' + colNumber]
+ }
+ case 'wstorm':
+ case 'charm': {
+ return [fileName + ':' + lineNumber]
+ }
+ case 'notepad++': {
+ return ['-n' + lineNumber, '-c' + colNumber, fileName]
+ }
+ case 'vim':
+ case 'nvim':
+ case 'mvim':
+ case 'joe':
+ case 'gvim': {
+ return ['+' + lineNumber, fileName]
+ }
+ case 'emacs':
+ case 'emacsclient': {
+ return ['+' + lineNumber + ':' + colNumber, fileName]
+ }
+ case 'rmate':
+ case 'mate':
+ case 'mine': {
+ return ['--line', lineNumber.toString(), fileName]
+ }
+ case 'code':
+ case 'Code':
+ case 'code-insiders':
+ case 'Code - Insiders':
+ case 'vscodium':
+ case 'VSCodium': {
+ return ['-g', fileName + ':' + lineNumber + ':' + colNumber]
+ }
+ case 'appcode':
+ case 'clion':
+ case 'clion64':
+ case 'idea':
+ case 'idea64':
+ case 'phpstorm':
+ case 'phpstorm64':
+ case 'pycharm':
+ case 'pycharm64':
+ case 'rubymine':
+ case 'rubymine64':
+ case 'webstorm':
+ case 'webstorm64':
+ case 'goland':
+ case 'goland64':
+ case 'rider':
+ case 'rider64': {
+ return ['--line', lineNumber.toString(), fileName]
+ }
+ default: {
+ // For all others, drop the lineNumber until we have
+ // a mapping above, since providing the lineNumber incorrectly
+ // can result in errors or confusing behavior.
+ return [fileName]
+ }
+ }
+}
+
+function guessEditor(): string[] {
+ // Explicit config always wins
+ if (process.env.REACT_EDITOR) {
+ return shellQuote.parse(process.env.REACT_EDITOR) as any
+ }
+
+ // We can find out which editor is currently running by:
+ // `ps x` on macOS and Linux
+ // `Get-Process` on Windows
+ try {
+ if (process.platform === 'darwin') {
+ const output = child_process.execSync('ps x').toString()
+ const processNames = Object.keys(COMMON_EDITORS_MACOS)
+ for (let i = 0; i < processNames.length; i++) {
+ const processName = processNames[i]
+ if (output.indexOf(processName) !== -1) {
+ return [(COMMON_EDITORS_MACOS as any)[processName]]
+ }
+ }
+ } else if (process.platform === 'win32') {
+ // Some processes need elevated rights to get its executable path.
+ // Just filter them out upfront. This also saves 10-20ms on the command.
+ const output = child_process
+ .execSync(
+ 'wmic process where "executablepath is not null" get executablepath'
+ )
+ .toString()
+ const runningProcesses = output.split('\r\n')
+ for (let i = 0; i < runningProcesses.length; i++) {
+ const processPath = runningProcesses[i].trim()
+ const processName = path.basename(processPath)
+ if (COMMON_EDITORS_WIN.indexOf(processName) !== -1) {
+ return [processPath]
+ }
+ }
+ } else if (process.platform === 'linux') {
+ // --no-heading No header line
+ // x List all processes owned by you
+ // -o comm Need only names column
+ const output = child_process
+ .execSync('ps x --no-heading -o comm --sort=comm')
+ .toString()
+ const processNames = Object.keys(COMMON_EDITORS_LINUX)
+ for (let i = 0; i < processNames.length; i++) {
+ const processName = processNames[i]
+ if (output.indexOf(processName) !== -1) {
+ return [(COMMON_EDITORS_LINUX as any)[processName] as string]
+ }
+ }
+ }
+ } catch (error) {
+ // Ignore...
+ }
+
+ // Last resort, use old skool env vars
+ if (process.env.VISUAL) {
+ return [process.env.VISUAL]
+ } else if (process.env.EDITOR) {
+ return [process.env.EDITOR]
+ }
+
+ return []
+}
+
+function printInstructions(fileName: string, errorMessage: string | null) {
+ console.log()
+ console.log(
+ red('Could not open ' + path.basename(fileName) + ' in the editor.')
+ )
+ if (errorMessage) {
+ if (errorMessage[errorMessage.length - 1] !== '.') {
+ errorMessage += '.'
+ }
+ console.log(red('The editor process exited with an error: ' + errorMessage))
+ }
+ console.log()
+ console.log(
+ 'To set up the editor integration, add something like ' +
+ cyan('REACT_EDITOR=atom') +
+ ' to the ' +
+ green('.env.local') +
+ ' file in your project folder ' +
+ 'and restart the development server.'
+ )
+ console.log()
+}
+
+function launchEditor(fileName: string, lineNumber: number, colNumber: number) {
+ if (!fs.existsSync(fileName)) {
+ return
+ }
+
+ // Sanitize lineNumber to prevent malicious use on win32
+ // via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
+ // and it should be a positive integer
+ if (!(Number.isInteger(lineNumber) && lineNumber > 0)) {
+ return
+ }
+
+ // colNumber is optional, but should be a positive integer too
+ // default is 1
+ if (!(Number.isInteger(colNumber) && colNumber > 0)) {
+ colNumber = 1
+ }
+
+ let [editor, ...args] = guessEditor()
+
+ if (!editor) {
+ printInstructions(fileName, null)
+ return
+ }
+
+ if (editor.toLowerCase() === 'none') {
+ return
+ }
+
+ if (
+ process.platform === 'linux' &&
+ fileName.startsWith('/mnt/') &&
+ /Microsoft/i.test(os.release())
+ ) {
+ // Assume WSL / "Bash on Ubuntu on Windows" is being used, and
+ // that the file exists on the Windows file system.
+ // `os.release()` is "4.4.0-43-Microsoft" in the current release
+ // build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
+ // When a Windows editor is specified, interop functionality can
+ // handle the path translation, but only if a relative path is used.
+ fileName = path.relative('', fileName)
+ }
+
+ // cmd.exe on Windows is vulnerable to RCE attacks given a file name of the
+ // form "C:\Users\myusername\Downloads\& curl 172.21.93.52". Use an access list
+ // to validate user-provided file names. This doesn't cover the entire range
+ // of valid file names but should cover almost all of them in practice.
+ if (
+ process.platform === 'win32' &&
+ !WINDOWS_FILE_NAME_ACCESS_LIST.test(fileName.trim())
+ ) {
+ console.log()
+ console.log(
+ red('Could not open ' + path.basename(fileName) + ' in the editor.')
+ )
+ console.log()
+ console.log(
+ 'When running on Windows, file names are checked against an access list ' +
+ 'to protect against remote code execution attacks. File names may ' +
+ 'consist only of alphanumeric characters (all languages), periods, ' +
+ 'dashes, slashes, and underscores.'
+ )
+ console.log()
+ return
+ }
+
+ if (lineNumber) {
+ args = args.concat(
+ getArgumentsForLineNumber(editor, fileName, lineNumber, colNumber)
+ )
+ } else {
+ args.push(fileName)
+ }
+
+ let p: child_process.ChildProcess | undefined = undefined
+ if (process.platform === 'win32') {
+ // On Windows, launch the editor in a shell because spawn can only
+ // launch .exe files.
+ p = child_process.spawn('cmd.exe', ['/C', editor].concat(args), {
+ stdio: 'inherit',
+ detached: true,
+ })
+ } else if (isTerminalEditor(editor)) {
+ if (process.platform === 'darwin') {
+ p = child_process.spawn(
+ 'osascript',
+ [
+ '-e',
+ `tell application "Terminal" to do script "${shellQuote.quote([
+ editor,
+ ...args,
+ ])}"`,
+ ],
+ { stdio: 'ignore' }
+ )
+ } else {
+ printInstructions(fileName, 'Terminal editors can only be used on macOS.')
+ }
+ } else {
+ p = child_process.spawn(editor, args, { stdio: 'inherit' })
+ }
+
+ if (p) {
+ p.on('exit', function (errorCode) {
+ if (errorCode) {
+ printInstructions(fileName, '(code ' + errorCode + ')')
+ }
+ })
+ p.on('error', function (error) {
+ printInstructions(fileName, error.message)
+ })
+ }
+}
+
+export { launchEditor }
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/node-stack-frames.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/node-stack-frames.ts
new file mode 100644
index 0000000000000..e6fb8b9922f08
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/node-stack-frames.ts
@@ -0,0 +1,70 @@
+import { parse } from 'next/dist/compiled/stacktrace-parser'
+import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
+import {
+ decorateServerError,
+ type ErrorSourceType,
+} from '../../../../../../shared/lib/error-source'
+
+export function getFilesystemFrame(frame: StackFrame): StackFrame {
+ const f: StackFrame = { ...frame }
+
+ if (typeof f.file === 'string') {
+ if (
+ // Posix:
+ f.file.startsWith('/') ||
+ // Win32:
+ /^[a-z]:\\/i.test(f.file) ||
+ // Win32 UNC:
+ f.file.startsWith('\\\\')
+ ) {
+ f.file = `file://${f.file}`
+ }
+ }
+
+ return f
+}
+
+export function getServerError(error: Error, type: ErrorSourceType): Error {
+ if (error.name === 'TurbopackInternalError') {
+ // If this is an internal Turbopack error we shouldn't show internal details
+ // to the user. These are written to a log file instead.
+ const turbopackInternalError = new Error(
+ 'An unexpected Turbopack error occurred. Please see the output of `next dev` for more details.'
+ )
+ decorateServerError(turbopackInternalError, type)
+ return turbopackInternalError
+ }
+
+ let n: Error
+ try {
+ throw new Error(error.message)
+ } catch (e) {
+ n = e as Error
+ }
+
+ n.name = error.name
+ try {
+ n.stack = `${n.toString()}\n${parse(error.stack!)
+ .map(getFilesystemFrame)
+ .map((f) => {
+ let str = ` at ${f.methodName}`
+ if (f.file) {
+ let loc = f.file
+ if (f.lineNumber) {
+ loc += `:${f.lineNumber}`
+ if (f.column) {
+ loc += `:${f.column}`
+ }
+ }
+ str += ` (${loc})`
+ }
+ return str
+ })
+ .join('\n')}`
+ } catch {
+ n.stack = error.stack
+ }
+
+ decorateServerError(n, type)
+ return n
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/noop-template.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/noop-template.ts
new file mode 100644
index 0000000000000..df41284da5b4b
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/noop-template.ts
@@ -0,0 +1,10 @@
+export function noop(
+ strings: TemplateStringsArray,
+ ...keys: readonly string[]
+) {
+ const lastIndex = strings.length - 1
+ return (
+ strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') +
+ strings[lastIndex]
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/parse-component-stack.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/parse-component-stack.ts
new file mode 100644
index 0000000000000..7d674c8f146a6
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/parse-component-stack.ts
@@ -0,0 +1,104 @@
+export type ComponentStackFrame = {
+ canOpenInEditor: boolean
+ component: string
+ file?: string
+ lineNumber?: number
+ column?: number
+}
+
+enum LocationType {
+ FILE = 'file',
+ WEBPACK_INTERNAL = 'webpack-internal',
+ HTTP = 'http',
+ PROTOCOL_RELATIVE = 'protocol-relative',
+ UNKNOWN = 'unknown',
+}
+
+/**
+ * Get the type of frame line based on the location
+ */
+function getLocationType(location: string): LocationType {
+ if (location.startsWith('file://')) {
+ return LocationType.FILE
+ }
+ if (location.includes('webpack-internal://')) {
+ return LocationType.WEBPACK_INTERNAL
+ }
+ if (location.startsWith('http://') || location.startsWith('https://')) {
+ return LocationType.HTTP
+ }
+ if (location.startsWith('//')) {
+ return LocationType.PROTOCOL_RELATIVE
+ }
+ return LocationType.UNKNOWN
+}
+
+function parseStackFrameLocation(
+ location: string
+): Omit {
+ const locationType = getLocationType(location)
+
+ const modulePath = location?.replace(
+ /^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/,
+ ''
+ )
+ const [, file, lineNumber, column] =
+ modulePath?.match(/^(.+):(\d+):(\d+)/) ?? []
+
+ switch (locationType) {
+ case LocationType.FILE:
+ case LocationType.WEBPACK_INTERNAL:
+ return {
+ canOpenInEditor: true,
+ file,
+ lineNumber: lineNumber ? Number(lineNumber) : undefined,
+ column: column ? Number(column) : undefined,
+ }
+ // When the location is a URL we only show the file
+ // TODO: Resolve http(s) URLs through sourcemaps
+ case LocationType.HTTP:
+ case LocationType.PROTOCOL_RELATIVE:
+ case LocationType.UNKNOWN:
+ default: {
+ return {
+ canOpenInEditor: false,
+ }
+ }
+ }
+}
+
+export function parseComponentStack(
+ componentStack: string
+): ComponentStackFrame[] {
+ const componentStackFrames: ComponentStackFrame[] = []
+ for (const line of componentStack.trim().split('\n')) {
+ // TODO: support safari stack trace
+ // Get component and file from the component stack line
+ const match = /at ([^ ]+)( \((.*)\))?/.exec(line)
+ if (match?.[1]) {
+ const component = match[1]
+ const location = match[3]
+
+ if (!location) {
+ componentStackFrames.push({
+ canOpenInEditor: false,
+ component,
+ })
+ continue
+ }
+
+ // Stop parsing the component stack if we reach a Next.js component
+ if (location?.includes('next/dist')) {
+ break
+ }
+
+ const frameLocation = parseStackFrameLocation(location)
+ componentStackFrames.push({
+ component,
+ ...frameLocation,
+ })
+ }
+ }
+
+ return componentStackFrames
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/parse-stack.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/parse-stack.ts
new file mode 100644
index 0000000000000..951b899c25758
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/parse-stack.ts
@@ -0,0 +1,52 @@
+import { parse } from 'next/dist/compiled/stacktrace-parser'
+import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
+import {
+ getHydrationErrorStackInfo,
+ isReactHydrationErrorMessage,
+} from '../../../../is-hydration-error'
+
+const regexNextStatic = /\/_next(\/static\/.+)/
+
+export function parseStack(stack: string | undefined): StackFrame[] {
+ if (!stack) return []
+ const messageAndStack = stack.replace(/^Error: /, '')
+ if (isReactHydrationErrorMessage(messageAndStack)) {
+ const { stack: parsedStack } = getHydrationErrorStackInfo(messageAndStack)
+ if (parsedStack) {
+ stack = parsedStack
+ }
+ }
+
+ // throw away eval information that stacktrace-parser doesn't support
+ // adapted from https://github.com/stacktracejs/error-stack-parser/blob/9f33c224b5d7b607755eb277f9d51fcdb7287e24/error-stack-parser.js#L59C33-L59C62
+ stack = stack
+ .split('\n')
+ .map((line) => {
+ if (line.includes('(eval ')) {
+ line = line
+ .replace(/eval code/g, 'eval')
+ .replace(/\(eval at [^()]* \(/, '(file://')
+ .replace(/\),.*$/g, ')')
+ }
+
+ return line
+ })
+ .join('\n')
+
+ const frames = parse(stack)
+ return frames.map((frame) => {
+ try {
+ const url = new URL(frame.file!)
+ const res = regexNextStatic.exec(url.pathname)
+ if (res) {
+ const distDir = process.env.__NEXT_DIST_DIR
+ ?.replace(/\\/g, '/')
+ ?.replace(/\/$/, '')
+ if (distDir) {
+ frame.file = 'file://' + distDir.concat(res.pop()!) + url.search
+ }
+ }
+ } catch {}
+ return frame
+ })
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts
new file mode 100644
index 0000000000000..36622efd1cb13
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts
@@ -0,0 +1,3 @@
+export const RuntimeErrorHandler = {
+ hadRuntimeError: false,
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/stack-frame.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/stack-frame.ts
new file mode 100644
index 0000000000000..68bb44681d17c
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/stack-frame.ts
@@ -0,0 +1,147 @@
+import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
+import type { OriginalStackFrameResponse } from '../../../server/shared'
+import {
+ isWebpackInternalResource,
+ formatFrameSourceFile,
+} from './webpack-module-path'
+export interface OriginalStackFrame extends OriginalStackFrameResponse {
+ error: boolean
+ reason: string | null
+ external: boolean
+ ignored: boolean
+ sourceStackFrame: StackFrame
+}
+
+function getOriginalStackFrame(
+ source: StackFrame,
+ type: 'server' | 'edge-server' | null,
+ isAppDir: boolean,
+ errorMessage: string
+): Promise {
+ async function _getOriginalStackFrame(): Promise {
+ const params = new URLSearchParams()
+ params.append('isServer', String(type === 'server'))
+ params.append('isEdgeServer', String(type === 'edge-server'))
+ params.append('isAppDirectory', String(isAppDir))
+ params.append('errorMessage', errorMessage)
+ for (const key in source) {
+ params.append(key, ((source as any)[key] ?? '').toString())
+ }
+
+ const controller = new AbortController()
+ const tm = setTimeout(() => controller.abort(), 3000)
+ const res = await self
+ .fetch(
+ `${
+ process.env.__NEXT_ROUTER_BASEPATH || ''
+ }/__nextjs_original-stack-frame?${params.toString()}`,
+ { signal: controller.signal }
+ )
+ .finally(() => {
+ clearTimeout(tm)
+ })
+ if (!res.ok || res.status === 204) {
+ return Promise.reject(new Error(await res.text()))
+ }
+
+ const body: OriginalStackFrameResponse = await res.json()
+ return {
+ error: false,
+ reason: null,
+ external: false,
+ sourceStackFrame: source,
+ originalStackFrame: body.originalStackFrame,
+ originalCodeFrame: body.originalCodeFrame || null,
+ sourcePackage: body.sourcePackage,
+ ignored: body.originalStackFrame?.ignored || false,
+ }
+ }
+
+ // TODO: merge this section into ignoredList handling
+ if (
+ source.file === 'file://' ||
+ source.file?.match(/^node:/) ||
+ source.file?.match(/https?:\/\//)
+ ) {
+ return Promise.resolve({
+ error: false,
+ reason: null,
+ external: true,
+ sourceStackFrame: source,
+ originalStackFrame: null,
+ originalCodeFrame: null,
+ sourcePackage: null,
+ ignored: true,
+ })
+ }
+
+ return _getOriginalStackFrame().catch((err: Error) => ({
+ error: true,
+ reason: err?.message ?? err?.toString() ?? 'Unknown Error',
+ external: false,
+ sourceStackFrame: source,
+ originalStackFrame: null,
+ originalCodeFrame: null,
+ sourcePackage: null,
+ ignored: false,
+ }))
+}
+
+export function getOriginalStackFrames(
+ frames: StackFrame[],
+ type: 'server' | 'edge-server' | null,
+ isAppDir: boolean,
+ errorMessage: string
+) {
+ return Promise.all(
+ frames.map((frame) =>
+ getOriginalStackFrame(frame, type, isAppDir, errorMessage)
+ )
+ )
+}
+
+export function getFrameSource(frame: StackFrame): string {
+ if (!frame.file) return ''
+
+ const isWebpackFrame = isWebpackInternalResource(frame.file)
+
+ let str = ''
+ // Skip URL parsing for webpack internal file paths.
+ if (isWebpackFrame) {
+ str = formatFrameSourceFile(frame.file)
+ } else {
+ try {
+ const u = new URL(frame.file)
+
+ let parsedPath = ''
+ // Strip the origin for same-origin scripts.
+ if (globalThis.location?.origin !== u.origin) {
+ // URLs can be valid without an `origin`, so long as they have a
+ // `protocol`. However, `origin` is preferred.
+ if (u.origin === 'null') {
+ parsedPath += u.protocol
+ } else {
+ parsedPath += u.origin
+ }
+ }
+
+ // Strip query string information as it's typically too verbose to be
+ // meaningful.
+ parsedPath += u.pathname
+ str = formatFrameSourceFile(parsedPath)
+ } catch {
+ str = formatFrameSourceFile(frame.file)
+ }
+ }
+
+ if (!isWebpackInternalResource(frame.file) && frame.lineNumber != null) {
+ if (str) {
+ if (frame.column != null) {
+ str += ` (${frame.lineNumber}:${frame.column})`
+ } else {
+ str += ` (${frame.lineNumber})`
+ }
+ }
+ }
+ return str
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/stitched-error.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/stitched-error.ts
new file mode 100644
index 0000000000000..fbe2ba6f2d7f8
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/stitched-error.ts
@@ -0,0 +1,49 @@
+import React from 'react'
+import isError from '../../../../../../lib/is-error'
+
+const REACT_ERROR_STACK_BOTTOM_FRAME = 'react-stack-bottom-frame'
+const REACT_ERROR_STACK_BOTTOM_FRAME_REGEX = new RegExp(
+ `(at ${REACT_ERROR_STACK_BOTTOM_FRAME} )|(${REACT_ERROR_STACK_BOTTOM_FRAME}\\@)`
+)
+
+const captureOwnerStack = (React as any).captureOwnerStack
+ ? (React as any).captureOwnerStack
+ : () => ''
+
+export function getReactStitchedError(err: T): Error | T {
+ if (typeof (React as any).captureOwnerStack !== 'function') {
+ return err
+ }
+ const isErrorInstance = isError(err)
+ const originStack = isErrorInstance ? err.stack || '' : ''
+ const originMessage = isErrorInstance ? err.message : ''
+ const stackLines = originStack.split('\n')
+ const indexOfSplit = stackLines.findIndex((line) =>
+ REACT_ERROR_STACK_BOTTOM_FRAME_REGEX.test(line)
+ )
+ const isOriginalReactError = indexOfSplit >= 0 // has the react-stack-bottom-frame
+ let newStack = isOriginalReactError
+ ? stackLines.slice(0, indexOfSplit).join('\n')
+ : originStack
+
+ const newError = new Error(originMessage)
+ // Copy all enumerable properties, e.g. digest
+ Object.assign(newError, err)
+ newError.stack = newStack
+
+ // Avoid duplicate overriding stack frames
+ appendOwnerStack(newError)
+
+ return newError
+}
+
+function appendOwnerStack(error: Error) {
+ let stack = error.stack || ''
+ // Avoid duplicate overriding stack frames
+ const ownerStack = captureOwnerStack()
+ if (ownerStack && stack.endsWith(ownerStack) === false) {
+ stack += ownerStack
+ // Override stack
+ error.stack = stack
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts
new file mode 100644
index 0000000000000..2f75fa8425a6b
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts
@@ -0,0 +1,111 @@
+import { useEffect } from 'react'
+import { attachHydrationErrorState } from './attach-hydration-error-state'
+import { isNextRouterError } from '../../../../is-next-router-error'
+import { storeHydrationErrorStateFromConsoleArgs } from './hydration-error-info'
+import { formatConsoleArgs } from '../../../../../lib/console'
+import isError from '../../../../../../lib/is-error'
+import { createUnhandledError } from './console-error'
+import { enqueueConsecutiveDedupedError } from './enqueue-client-error'
+import { getReactStitchedError } from './stitched-error'
+
+const queueMicroTask =
+ globalThis.queueMicrotask || ((cb: () => void) => Promise.resolve().then(cb))
+
+export type ErrorHandler = (error: Error) => void
+
+const errorQueue: Array = []
+const errorHandlers: Array = []
+const rejectionQueue: Array = []
+const rejectionHandlers: Array = []
+
+export function handleClientError(
+ originError: unknown,
+ consoleErrorArgs: any[],
+ capturedFromConsole: boolean = false
+) {
+ let error: Error
+ if (!originError || !isError(originError)) {
+ // If it's not an error, format the args into an error
+ const formattedErrorMessage = formatConsoleArgs(consoleErrorArgs)
+ error = createUnhandledError(formattedErrorMessage)
+ } else {
+ error = capturedFromConsole
+ ? createUnhandledError(originError)
+ : originError
+ }
+ error = getReactStitchedError(error)
+
+ storeHydrationErrorStateFromConsoleArgs(...consoleErrorArgs)
+ attachHydrationErrorState(error)
+
+ enqueueConsecutiveDedupedError(errorQueue, error)
+ for (const handler of errorHandlers) {
+ // Delayed the error being passed to React Dev Overlay,
+ // avoid the state being synchronously updated in the component.
+ queueMicroTask(() => {
+ handler(error)
+ })
+ }
+}
+
+export function useErrorHandler(
+ handleOnUnhandledError: ErrorHandler,
+ handleOnUnhandledRejection: ErrorHandler
+) {
+ useEffect(() => {
+ // Handle queued errors.
+ errorQueue.forEach(handleOnUnhandledError)
+ rejectionQueue.forEach(handleOnUnhandledRejection)
+
+ // Listen to new errors.
+ errorHandlers.push(handleOnUnhandledError)
+ rejectionHandlers.push(handleOnUnhandledRejection)
+
+ return () => {
+ // Remove listeners.
+ errorHandlers.splice(errorHandlers.indexOf(handleOnUnhandledError), 1)
+ rejectionHandlers.splice(
+ rejectionHandlers.indexOf(handleOnUnhandledRejection),
+ 1
+ )
+ }
+ }, [handleOnUnhandledError, handleOnUnhandledRejection])
+}
+
+function onUnhandledError(event: WindowEventMap['error']): void | boolean {
+ if (isNextRouterError(event.error)) {
+ event.preventDefault()
+ return false
+ }
+ handleClientError(event.error, [])
+}
+
+function onUnhandledRejection(ev: WindowEventMap['unhandledrejection']): void {
+ const reason = ev?.reason
+ if (isNextRouterError(reason)) {
+ ev.preventDefault()
+ return
+ }
+
+ let error = reason
+ if (error && !isError(error)) {
+ error = createUnhandledError(error + '')
+ }
+
+ rejectionQueue.push(error)
+ for (const handler of rejectionHandlers) {
+ handler(error)
+ }
+}
+
+export function handleGlobalErrors() {
+ if (typeof window !== 'undefined') {
+ try {
+ // Increase the number of stack frames on the client
+ Error.stackTraceLimit = 50
+ } catch {}
+
+ window.addEventListener('error', onUnhandledError)
+ window.addEventListener('unhandledrejection', onUnhandledRejection)
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-open-in-editor.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-open-in-editor.ts
new file mode 100644
index 0000000000000..8e2e9e43f4b95
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-open-in-editor.ts
@@ -0,0 +1,35 @@
+import { useCallback } from 'react'
+
+export function useOpenInEditor({
+ file,
+ lineNumber,
+ column,
+}: {
+ file?: string | null
+ lineNumber?: number | null
+ column?: number | null
+} = {}) {
+ const openInEditor = useCallback(() => {
+ if (file == null || lineNumber == null || column == null) return
+
+ const params = new URLSearchParams()
+ params.append('file', file)
+ params.append('lineNumber', String(lineNumber))
+ params.append('column', String(column))
+
+ self
+ .fetch(
+ `${
+ process.env.__NEXT_ROUTER_BASEPATH || ''
+ }/__nextjs_launch-editor?${params.toString()}`
+ )
+ .then(
+ () => {},
+ () => {
+ console.error('There was an issue opening this code in your editor.')
+ }
+ )
+ }, [file, lineNumber, column])
+
+ return openInEditor
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-websocket.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-websocket.ts
new file mode 100644
index 0000000000000..dd875fa571275
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-websocket.ts
@@ -0,0 +1,111 @@
+import { useCallback, useContext, useEffect, useRef } from 'react'
+import { GlobalLayoutRouterContext } from '../../../../../../shared/lib/app-router-context.shared-runtime'
+import { getSocketUrl } from './get-socket-url'
+import type { TurbopackMsgToBrowser } from '../../../../../../server/dev/hot-reloader-types'
+
+export function useWebsocket(assetPrefix: string) {
+ const webSocketRef = useRef(undefined)
+
+ useEffect(() => {
+ if (webSocketRef.current) {
+ return
+ }
+
+ const url = getSocketUrl(assetPrefix)
+
+ webSocketRef.current = new window.WebSocket(`${url}/_next/webpack-hmr`)
+ }, [assetPrefix])
+
+ return webSocketRef
+}
+
+export function useSendMessage(webSocketRef: ReturnType) {
+ const sendMessage = useCallback(
+ (data: string) => {
+ const socket = webSocketRef.current
+ if (!socket || socket.readyState !== socket.OPEN) {
+ return
+ }
+ return socket.send(data)
+ },
+ [webSocketRef]
+ )
+ return sendMessage
+}
+
+export function useTurbopack(
+ sendMessage: ReturnType,
+ onUpdateError: (err: unknown) => void
+) {
+ const turbopackState = useRef<{
+ init: boolean
+ queue: Array | undefined
+ callback: ((msg: TurbopackMsgToBrowser) => void) | undefined
+ }>({
+ init: false,
+ // Until the dynamic import resolves, queue any turbopack messages which will be replayed.
+ queue: [],
+ callback: undefined,
+ })
+
+ const processTurbopackMessage = useCallback((msg: TurbopackMsgToBrowser) => {
+ const { callback, queue } = turbopackState.current
+ if (callback) {
+ callback(msg)
+ } else {
+ queue!.push(msg)
+ }
+ }, [])
+
+ useEffect(() => {
+ const { current: initCurrent } = turbopackState
+ // TODO(WEB-1589): only install if `process.turbopack` set.
+ if (initCurrent.init) {
+ return
+ }
+ initCurrent.init = true
+
+ import(
+ // @ts-expect-error requires "moduleResolution": "node16" in tsconfig.json and not .ts extension
+ '@vercel/turbopack-ecmascript-runtime/browser/dev/hmr-client/hmr-client.ts'
+ ).then(({ connect }) => {
+ const { current } = turbopackState
+ connect({
+ addMessageListener(cb: (msg: TurbopackMsgToBrowser) => void) {
+ current.callback = cb
+
+ // Replay all Turbopack messages before we were able to establish the HMR client.
+ for (const msg of current.queue!) {
+ cb(msg)
+ }
+ current.queue = undefined
+ },
+ sendMessage,
+ onUpdateError,
+ })
+ })
+ }, [sendMessage, onUpdateError])
+
+ return processTurbopackMessage
+}
+
+export function useWebsocketPing(
+ websocketRef: ReturnType
+) {
+ const sendMessage = useSendMessage(websocketRef)
+ const { tree } = useContext(GlobalLayoutRouterContext)
+
+ useEffect(() => {
+ // Taken from on-demand-entries-client.js
+ const interval = setInterval(() => {
+ sendMessage(
+ JSON.stringify({
+ event: 'ping',
+ tree,
+ appDirRoute: true,
+ })
+ )
+ }, 2500)
+ return () => clearInterval(interval)
+ }, [tree, sendMessage])
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/webpack-module-path.test.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/webpack-module-path.test.ts
new file mode 100644
index 0000000000000..a0923c2c66161
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/webpack-module-path.test.ts
@@ -0,0 +1,86 @@
+import {
+ formatFrameSourceFile,
+ isWebpackInternalResource,
+} from './webpack-module-path'
+
+describe('webpack-module-path', () => {
+ describe('isWebpackInternalResource', () => {
+ it('should return true for webpack-internal paths', () => {
+ expect(
+ isWebpackInternalResource('webpack-internal:///./src/hello.tsx')
+ ).toBe(true)
+ expect(
+ isWebpackInternalResource(
+ 'rsc://React/Server/webpack-internal:///(rsc)/./src/hello.tsx?42'
+ )
+ ).toBe(true)
+ expect(
+ isWebpackInternalResource(
+ 'rsc://React/Server/webpack:///(rsc)/./src/hello.tsx?42'
+ )
+ ).toBe(true)
+ expect(
+ isWebpackInternalResource(
+ 'rsc://React/Server/webpack:///(app-pages-browser)/./src/hello.tsx?42'
+ )
+ ).toBe(true)
+ expect(
+ isWebpackInternalResource(
+ 'rsc://React/Server/webpack:///(app-pages-browser)/./src/hello.tsx?42dc'
+ )
+ ).toBe(true)
+ expect(isWebpackInternalResource('webpack://_N_E/./src/hello.tsx')).toBe(
+ true
+ )
+ expect(isWebpackInternalResource('webpack://./src/hello.tsx')).toBe(true)
+ expect(isWebpackInternalResource('webpack:///./src/hello.tsx')).toBe(true)
+ })
+
+ it('should return false for non-webpack-internal paths', () => {
+ expect(isWebpackInternalResource('')).toBe(false)
+ expect(isWebpackInternalResource('file:///src/hello.tsx')).toBe(false)
+ })
+ })
+
+ describe('formatFrameSourceFile', () => {
+ it('should return the original file path', () => {
+ expect(formatFrameSourceFile('webpack-internal:///./src/hello.tsx')).toBe(
+ './src/hello.tsx'
+ )
+ expect(
+ formatFrameSourceFile(
+ 'rsc://React/Server/webpack-internal:///(rsc)/./src/hello.tsx?42'
+ )
+ ).toBe('./src/hello.tsx')
+ expect(
+ formatFrameSourceFile(
+ 'rsc://React/Server/webpack:///(rsc)/./src/hello.tsx?42'
+ )
+ ).toBe('./src/hello.tsx')
+ expect(
+ formatFrameSourceFile(
+ 'rsc://React/Server/webpack:///(app-pages-browser)/./src/hello.tsx?42'
+ )
+ ).toBe('./src/hello.tsx')
+ expect(
+ formatFrameSourceFile(
+ 'rsc://React/Server/webpack:///(app-pages-browser)/./src/hello.tsx?42?0'
+ )
+ ).toBe('./src/hello.tsx')
+ expect(
+ formatFrameSourceFile(
+ 'rsc://React/Server/webpack:///(app-pages-browser)/./src/hello.tsx?42dc'
+ )
+ ).toBe('./src/hello.tsx')
+ expect(formatFrameSourceFile('webpack://_N_E/./src/hello.tsx')).toBe(
+ './src/hello.tsx'
+ )
+ expect(formatFrameSourceFile('webpack://./src/hello.tsx')).toBe(
+ './src/hello.tsx'
+ )
+ expect(formatFrameSourceFile('webpack:///./src/hello.tsx')).toBe(
+ './src/hello.tsx'
+ )
+ })
+ })
+})
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/webpack-module-path.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/webpack-module-path.ts
new file mode 100644
index 0000000000000..683a2fbc13e6f
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/webpack-module-path.ts
@@ -0,0 +1,35 @@
+const replacementRegExes = [
+ /^(rsc:\/\/React\/[^/]+\/)/,
+ /^webpack-internal:\/\/\/(\([\w-]+\)\/)?/,
+ /^(webpack:\/\/\/|webpack:\/\/(_N_E\/)?)(\([\w-]+\)\/)?/,
+ /\?\w+(\?\d+)?$/, // React replay error query param, .e.g. ?c69d?0, ?c69d
+ /\?\d+$/, // React's fakeFunctionIdx query param
+]
+
+export function isWebpackInternalResource(file: string) {
+ for (const regex of replacementRegExes) {
+ if (regex.test(file)) return true
+
+ file = file.replace(regex, '')
+ }
+
+ return false
+}
+
+/**
+ * Format the webpack internal id to original file path
+ *
+ * webpack-internal:///./src/hello.tsx => ./src/hello.tsx
+ * rsc://React/Server/webpack-internal:///(rsc)/./src/hello.tsx?42 => ./src/hello.tsx
+ * rsc://React/Server/webpack:///app/indirection.tsx?14cb?0 => app/indirection.tsx
+ * webpack://_N_E/./src/hello.tsx => ./src/hello.tsx
+ * webpack://./src/hello.tsx => ./src/hello.tsx
+ * webpack:///./src/hello.tsx => ./src/hello.tsx
+ */
+export function formatFrameSourceFile(file: string) {
+ for (const regex of replacementRegExes) {
+ file = file.replace(regex, '')
+ }
+
+ return file
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/hooks/use-on-click-outside.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/hooks/use-on-click-outside.ts
new file mode 100644
index 0000000000000..265f7a69a16e8
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/hooks/use-on-click-outside.ts
@@ -0,0 +1,31 @@
+import * as React from 'react'
+
+export function useOnClickOutside(
+ el: Node | null,
+ handler: ((e: MouseEvent | TouchEvent) => void) | undefined
+) {
+ React.useEffect(() => {
+ if (el == null || handler == null) {
+ return
+ }
+
+ const listener = (e: MouseEvent | TouchEvent) => {
+ // Do nothing if clicking ref's element or descendent elements
+ if (!el || el.contains(e.target as Element)) {
+ return
+ }
+
+ handler(e)
+ }
+
+ const root = el.getRootNode()
+ root.addEventListener('mousedown', listener as EventListener)
+ root.addEventListener('touchstart', listener as EventListener, {
+ passive: false,
+ })
+ return function () {
+ root.removeEventListener('mousedown', listener as EventListener)
+ root.removeEventListener('touchstart', listener as EventListener)
+ }
+ }, [handler, el])
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/CloseIcon.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/CloseIcon.tsx
new file mode 100644
index 0000000000000..e5a51a44a3eb5
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/CloseIcon.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react'
+
+const CloseIcon = () => {
+ return (
+
+ )
+}
+
+export { CloseIcon }
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/CollapseIcon.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/CollapseIcon.tsx
new file mode 100644
index 0000000000000..4e38bd16b2ddc
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/CollapseIcon.tsx
@@ -0,0 +1,24 @@
+export function CollapseIcon({ collapsed }: { collapsed?: boolean } = {}) {
+ return (
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/LightningBolt.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/LightningBolt.tsx
new file mode 100644
index 0000000000000..debf0181551f5
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/LightningBolt.tsx
@@ -0,0 +1,16 @@
+export const LightningBolt = (props: React.SVGProps) => (
+
+)
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/Base.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/Base.tsx
new file mode 100644
index 0000000000000..de83c0feb98c0
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/Base.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react'
+import { noop as css } from '../helpers/noop-template'
+
+export function Base() {
+ return (
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/ComponentStyles.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/ComponentStyles.tsx
new file mode 100644
index 0000000000000..7ab1259af53c2
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/ComponentStyles.tsx
@@ -0,0 +1,30 @@
+import { styles as codeFrame } from '../components/CodeFrame/styles'
+import { styles as dialog } from '../components/Dialog'
+import { styles as leftRightDialogHeader } from '../components/LeftRightDialogHeader/styles'
+import { styles as overlay } from '../components/Overlay/styles'
+import { styles as terminal } from '../components/Terminal/styles'
+import { styles as toast } from '../components/Toast'
+import { styles as versionStaleness } from '../components/VersionStalenessInfo'
+import { styles as buildErrorStyles } from '../container/BuildError'
+import { styles as containerErrorStyles } from '../container/Errors'
+import { styles as containerRuntimeErrorStyles } from '../container/RuntimeError'
+import { noop as css } from '../helpers/noop-template'
+
+export function ComponentStyles() {
+ return (
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/CssReset.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/CssReset.tsx
new file mode 100644
index 0000000000000..eb3e2ca7a6453
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/CssReset.tsx
@@ -0,0 +1,362 @@
+import * as React from 'react'
+import { noop as css } from '../helpers/noop-template'
+
+export function CssReset() {
+ return (
+
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/ReactDevOverlay.tsx
new file mode 100644
index 0000000000000..f5546fac1d364
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/ReactDevOverlay.tsx
@@ -0,0 +1,67 @@
+import * as React from 'react'
+
+import { ShadowPortal } from '../internal/components/ShadowPortal'
+import { BuildError } from '../internal/container/BuildError'
+import { Errors } from '../internal/container/Errors'
+import { Base } from '../internal/styles/Base'
+import { ComponentStyles } from '../internal/styles/ComponentStyles'
+import { CssReset } from '../internal/styles/CssReset'
+
+import { ErrorBoundary } from '../../pages/ErrorBoundary'
+import { usePagesReactDevOverlay } from '../../pages/hooks'
+
+export type ErrorType = 'runtime' | 'build'
+
+interface ReactDevOverlayProps {
+ children?: React.ReactNode
+ preventDisplay?: ErrorType[]
+ globalOverlay?: boolean
+}
+
+export default function ReactDevOverlay({
+ children,
+ preventDisplay,
+ globalOverlay,
+}: ReactDevOverlayProps) {
+ const {
+ isMounted,
+ displayPrevented,
+ hasBuildError,
+ hasRuntimeErrors,
+ state,
+ onComponentError,
+ } = usePagesReactDevOverlay(preventDisplay)
+
+ return (
+ <>
+
+ {children ?? null}
+
+ {isMounted ? (
+
+
+
+
+
+ {displayPrevented ? null : hasBuildError ? (
+
+ ) : hasRuntimeErrors ? (
+
+ ) : undefined}
+
+ ) : undefined}
+ >
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/app/OldReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/app/OldReactDevOverlay.tsx
new file mode 100644
index 0000000000000..2b46347d4fa2d
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/app/OldReactDevOverlay.tsx
@@ -0,0 +1,92 @@
+import React from 'react'
+import type { OverlayState } from '../shared'
+import { ShadowPortal } from '../internal/components/ShadowPortal'
+import { BuildError } from '../internal/container/BuildError'
+import { Errors } from '../internal/container/Errors'
+import { StaticIndicator } from '../internal/container/StaticIndicator'
+import { Base } from '../internal/styles/Base'
+import { ComponentStyles } from '../internal/styles/ComponentStyles'
+import { CssReset } from '../internal/styles/CssReset'
+import { RootLayoutMissingTagsError } from '../internal/container/root-layout-missing-tags-error'
+import type { Dispatcher } from './hot-reloader-client'
+import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler'
+
+interface ReactDevOverlayState {
+ isReactError: boolean
+}
+export default class ReactDevOverlay extends React.PureComponent<
+ {
+ state: OverlayState
+ dispatcher?: Dispatcher
+ children: React.ReactNode
+ },
+ ReactDevOverlayState
+> {
+ state = { isReactError: false }
+
+ static getDerivedStateFromError(error: Error): ReactDevOverlayState {
+ if (!error.stack) return { isReactError: false }
+
+ RuntimeErrorHandler.hadRuntimeError = true
+ return {
+ isReactError: true,
+ }
+ }
+
+ render() {
+ const { state, children, dispatcher } = this.props
+ const { isReactError } = this.state
+
+ const hasBuildError = state.buildError != null
+ const hasRuntimeErrors = Boolean(state.errors.length)
+ const hasStaticIndicator = state.staticIndicator
+ const debugInfo = state.debugInfo
+
+ return (
+ <>
+ {isReactError ? (
+
+
+
+
+ ) : (
+ children
+ )}
+
+
+
+
+ {state.rootLayoutMissingTags?.length ? (
+
+ ) : hasBuildError ? (
+
+ ) : (
+ <>
+ {hasRuntimeErrors ? (
+
+ ) : null}
+
+ {hasStaticIndicator && (
+
+ )}
+ >
+ )}
+
+ >
+ )
+ }
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx
index 2b46347d4fa2d..9bc320c9f2fb4 100644
--- a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx
@@ -1,92 +1,6 @@
-import React from 'react'
-import type { OverlayState } from '../shared'
-import { ShadowPortal } from '../internal/components/ShadowPortal'
-import { BuildError } from '../internal/container/BuildError'
-import { Errors } from '../internal/container/Errors'
-import { StaticIndicator } from '../internal/container/StaticIndicator'
-import { Base } from '../internal/styles/Base'
-import { ComponentStyles } from '../internal/styles/ComponentStyles'
-import { CssReset } from '../internal/styles/CssReset'
-import { RootLayoutMissingTagsError } from '../internal/container/root-layout-missing-tags-error'
-import type { Dispatcher } from './hot-reloader-client'
-import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler'
+import OldReactDevOverlay from './OldReactDevOverlay'
+import NewReactDevOverlay from '../_experimental/app/ReactDevOverlay'
-interface ReactDevOverlayState {
- isReactError: boolean
-}
-export default class ReactDevOverlay extends React.PureComponent<
- {
- state: OverlayState
- dispatcher?: Dispatcher
- children: React.ReactNode
- },
- ReactDevOverlayState
-> {
- state = { isReactError: false }
-
- static getDerivedStateFromError(error: Error): ReactDevOverlayState {
- if (!error.stack) return { isReactError: false }
-
- RuntimeErrorHandler.hadRuntimeError = true
- return {
- isReactError: true,
- }
- }
-
- render() {
- const { state, children, dispatcher } = this.props
- const { isReactError } = this.state
-
- const hasBuildError = state.buildError != null
- const hasRuntimeErrors = Boolean(state.errors.length)
- const hasStaticIndicator = state.staticIndicator
- const debugInfo = state.debugInfo
-
- return (
- <>
- {isReactError ? (
-
-
-
-
- ) : (
- children
- )}
-
-
-
-
- {state.rootLayoutMissingTags?.length ? (
-
- ) : hasBuildError ? (
-
- ) : (
- <>
- {hasRuntimeErrors ? (
-
- ) : null}
-
- {hasStaticIndicator && (
-
- )}
- >
- )}
-
- >
- )
- }
-}
+export default Boolean(process.env.__NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY)
+ ? NewReactDevOverlay
+ : OldReactDevOverlay
diff --git a/packages/next/src/client/components/react-dev-overlay/pages/OldReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/pages/OldReactDevOverlay.tsx
new file mode 100644
index 0000000000000..104a70a8136e5
--- /dev/null
+++ b/packages/next/src/client/components/react-dev-overlay/pages/OldReactDevOverlay.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react'
+
+import { ShadowPortal } from '../internal/components/ShadowPortal'
+import { BuildError } from '../internal/container/BuildError'
+import { Errors } from '../internal/container/Errors'
+import { ErrorBoundary } from './ErrorBoundary'
+import { Base } from '../internal/styles/Base'
+import { ComponentStyles } from '../internal/styles/ComponentStyles'
+import { CssReset } from '../internal/styles/CssReset'
+import { usePagesReactDevOverlay } from './hooks'
+
+export type ErrorType = 'runtime' | 'build'
+
+interface ReactDevOverlayProps {
+ children?: React.ReactNode
+ preventDisplay?: ErrorType[]
+ globalOverlay?: boolean
+}
+
+export default function ReactDevOverlay({
+ children,
+ preventDisplay,
+ globalOverlay,
+}: ReactDevOverlayProps) {
+ const {
+ isMounted,
+ displayPrevented,
+ hasBuildError,
+ hasRuntimeErrors,
+ state,
+ onComponentError,
+ } = usePagesReactDevOverlay(preventDisplay)
+
+ return (
+ <>
+
+ {children ?? null}
+
+ {isMounted ? (
+
+
+
+
+
+ {displayPrevented ? null : hasBuildError ? (
+
+ ) : hasRuntimeErrors ? (
+
+ ) : undefined}
+
+ ) : undefined}
+ >
+ )
+}
diff --git a/packages/next/src/client/components/react-dev-overlay/pages/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/pages/ReactDevOverlay.tsx
index 104a70a8136e5..1c7cc77874c74 100644
--- a/packages/next/src/client/components/react-dev-overlay/pages/ReactDevOverlay.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/pages/ReactDevOverlay.tsx
@@ -1,66 +1,6 @@
-import * as React from 'react'
+import OldReactDevOverlay from './OldReactDevOverlay'
+import NewReactDevOverlay from '../_experimental/pages/ReactDevOverlay'
-import { ShadowPortal } from '../internal/components/ShadowPortal'
-import { BuildError } from '../internal/container/BuildError'
-import { Errors } from '../internal/container/Errors'
-import { ErrorBoundary } from './ErrorBoundary'
-import { Base } from '../internal/styles/Base'
-import { ComponentStyles } from '../internal/styles/ComponentStyles'
-import { CssReset } from '../internal/styles/CssReset'
-import { usePagesReactDevOverlay } from './hooks'
-
-export type ErrorType = 'runtime' | 'build'
-
-interface ReactDevOverlayProps {
- children?: React.ReactNode
- preventDisplay?: ErrorType[]
- globalOverlay?: boolean
-}
-
-export default function ReactDevOverlay({
- children,
- preventDisplay,
- globalOverlay,
-}: ReactDevOverlayProps) {
- const {
- isMounted,
- displayPrevented,
- hasBuildError,
- hasRuntimeErrors,
- state,
- onComponentError,
- } = usePagesReactDevOverlay(preventDisplay)
-
- return (
- <>
-
- {children ?? null}
-
- {isMounted ? (
-
-
-
-
-
- {displayPrevented ? null : hasBuildError ? (
-
- ) : hasRuntimeErrors ? (
-
- ) : undefined}
-
- ) : undefined}
- >
- )
-}
+export default Boolean(process.env.__NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY)
+ ? NewReactDevOverlay
+ : OldReactDevOverlay
diff --git a/packages/next/src/client/components/react-dev-overlay/pages/hooks.ts b/packages/next/src/client/components/react-dev-overlay/pages/hooks.ts
index acf3c0704a82c..0ea0bee1cc4ce 100644
--- a/packages/next/src/client/components/react-dev-overlay/pages/hooks.ts
+++ b/packages/next/src/client/components/react-dev-overlay/pages/hooks.ts
@@ -1,4 +1,4 @@
-import type { ErrorType } from './ReactDevOverlay'
+import type { ErrorType } from './OldReactDevOverlay'
import React from 'react'
import * as Bus from './bus'
import { useErrorOverlayReducer } from '../shared'