Skip to content

Commit ee94be9

Browse files
authored
[dev-overlay] Use same bundle for Pages and App Router (#80019)
1 parent 9e9d13f commit ee94be9

22 files changed

+203
-174
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,5 +695,7 @@
695695
"694": "createPrerenderPathname was called inside a client component scope.",
696696
"695": "Expected workUnitAsyncStorage to have a store.",
697697
"696": "Next DevTools: Can't dispatch %s in this environment. This is a bug in Next.js",
698-
"697": "Next DevTools: Can't render in this environment. This is a bug in Next.js"
698+
"697": "Next DevTools: Can't render in this environment. This is a bug in Next.js",
699+
"698": "Next DevTools: App Dev Overlay is already mounted. This is a bug in Next.js",
700+
"699": "Next DevTools: Pages Dev Overlay is already mounted. This is a bug in Next.js"
699701
}

packages/next/next-devtools.webpack-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports = ({ dev, ...rest }) => {
2727
return {
2828
entry: path.join(
2929
__dirname,
30-
'src/client/components/react-dev-overlay/app/entrypoint.js'
30+
'src/client/components/react-dev-overlay/entrypoint.js'
3131
),
3232
target,
3333
mode: dev ? 'development' : 'production',

packages/next/next-runtime.webpack-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const sharedExternals = [
9393
const externalsMap = {
9494
'./web/sandbox': 'next/dist/server/web/sandbox',
9595
'next/dist/compiled/next-devtools':
96-
'commonjs next/dist/client/components/react-dev-overlay/app/app-dev-overlay.shim.js',
96+
'commonjs next/dist/client/components/react-dev-overlay/dev-overlay.shim.js',
9797
}
9898

9999
const externalsRegexMap = {

packages/next/src/client/components/react-dev-overlay/app/entrypoint.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/next/src/client/components/react-dev-overlay/app/entrypoint.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/next/src/client/components/react-dev-overlay/app/app-dev-overlay.browser.tsx renamed to packages/next/src/client/components/react-dev-overlay/dev-overlay.browser.tsx

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ import {
1717
ACTION_BUILDING_INDICATOR_SHOW,
1818
ACTION_RENDERING_INDICATOR_HIDE,
1919
ACTION_RENDERING_INDICATOR_SHOW,
20-
} from '../shared'
20+
} from './shared'
2121

2222
import { startTransition, useInsertionEffect } from 'react'
2323
import { createRoot } from 'react-dom/client'
24-
import { FontStyles } from '../font/font-styles'
25-
import type { DebugInfo } from '../types'
26-
import { DevOverlay } from '../ui/dev-overlay'
27-
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
28-
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
24+
import { FontStyles } from './font/font-styles'
25+
import type { HydrationErrorState } from './pages/hydration-error-state'
26+
import type { DebugInfo } from './types'
27+
import { DevOverlay } from './ui/dev-overlay'
28+
import type { DevIndicatorServerState } from '../../../server/dev/dev-indicator-server-state'
29+
import type { VersionInfo } from '../../../server/dev/parse-version-info'
2930

3031
export interface Dispatcher {
3132
onBuildOk(): void
@@ -143,22 +144,21 @@ function replayQueuedEvents(dispatch: NonNullable<typeof maybeDispatch>) {
143144
}
144145
}
145146

146-
function getSquashedHydrationErrorDetails() {
147-
// We don't squash hydration errors in the App Router.
148-
return null
149-
}
150-
151-
function AppDevOverlay({
147+
function DevOverlayRoot({
152148
getComponentStack,
153149
getOwnerStack,
150+
getSquashedHydrationErrorDetails,
154151
isRecoverableError,
152+
routerType,
155153
}: {
156154
getComponentStack: (error: Error) => string | undefined
157155
getOwnerStack: (error: Error) => string | null | undefined
156+
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
158157
isRecoverableError: (error: Error) => boolean
158+
routerType: 'app' | 'pages'
159159
}) {
160160
const [state, dispatch] = useErrorOverlayReducer(
161-
'app',
161+
routerType,
162162
getComponentStack,
163163
getOwnerStack,
164164
isRecoverableError
@@ -193,13 +193,28 @@ function AppDevOverlay({
193193
)
194194
}
195195

196-
let isMounted = false
196+
let isPagesMounted = false
197+
let isAppMounted = false
198+
199+
function getSquashedHydrationErrorDetailsApp() {
200+
// We don't squash hydration errors in the App Router.
201+
return null
202+
}
203+
197204
export function renderAppDevOverlay(
198205
getComponentStack: (error: Error) => string | undefined,
199206
getOwnerStack: (error: Error) => string | null | undefined,
200207
isRecoverableError: (error: Error) => boolean
201208
): void {
202-
if (!isMounted) {
209+
if (isPagesMounted) {
210+
// Switching between App and Pages Router is always a hard navigation
211+
// TODO: Support soft navigation between App and Pages Router
212+
throw new Error(
213+
'Next DevTools: Pages Dev Overlay is already mounted. This is a bug in Next.js'
214+
)
215+
}
216+
217+
if (!isAppMounted) {
203218
// React 19 will not throw away `<script>` elements in a container it owns.
204219
// This ensures the actual user-space React does not unmount the Dev Overlay.
205220
const script = document.createElement('script')
@@ -225,14 +240,80 @@ export function renderAppDevOverlay(
225240
// TODO: Dedicated error boundary or root error callbacks?
226241
// At least it won't unmount any user code if it errors.
227242
root.render(
228-
<AppDevOverlay
243+
<DevOverlayRoot
244+
getComponentStack={getComponentStack}
245+
getOwnerStack={getOwnerStack}
246+
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetailsApp}
247+
isRecoverableError={isRecoverableError}
248+
routerType="app"
249+
/>
250+
)
251+
})
252+
253+
isAppMounted = true
254+
}
255+
}
256+
257+
export function renderPagesDevOverlay(
258+
getComponentStack: (error: Error) => string | undefined,
259+
getOwnerStack: (error: Error) => string | null | undefined,
260+
getSquashedHydrationErrorDetails: (
261+
error: Error
262+
) => HydrationErrorState | null,
263+
isRecoverableError: (error: Error) => boolean
264+
): void {
265+
if (isAppMounted) {
266+
// Switching between App and Pages Router is always a hard navigation
267+
// TODO: Support soft navigation between App and Pages Router
268+
throw new Error(
269+
'Next DevTools: App Dev Overlay is already mounted. This is a bug in Next.js'
270+
)
271+
}
272+
273+
if (!isPagesMounted) {
274+
const container = document.createElement('nextjs-portal')
275+
// Although the style applied to the shadow host is isolated,
276+
// the element that attached the shadow host (i.e. "script")
277+
// is still affected by the parent's style (e.g. "body"). This may
278+
// occur style conflicts like "display: flex", with other children
279+
// elements therefore give the shadow host an absolute position.
280+
container.style.position = 'absolute'
281+
282+
// Pages Router runs with React 18 or 19 so we can't use the same trick as with
283+
// App Router. We just reconnect the container if React wipes it e.g. when
284+
// we recover from a shell error via createRoot()
285+
new MutationObserver((records) => {
286+
for (const record of records) {
287+
if (record.type === 'childList') {
288+
for (const node of record.removedNodes) {
289+
if (node === container) {
290+
// Reconnect the container to the body
291+
document.body.appendChild(container)
292+
}
293+
}
294+
}
295+
}
296+
}).observe(document.body, {
297+
childList: true,
298+
})
299+
document.body.appendChild(container)
300+
301+
const root = createRoot(container)
302+
303+
startTransition(() => {
304+
// TODO: Dedicated error boundary or root error callbacks?
305+
// At least it won't unmount any user code if it errors.
306+
root.render(
307+
<DevOverlayRoot
229308
getComponentStack={getComponentStack}
230309
getOwnerStack={getOwnerStack}
310+
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails}
231311
isRecoverableError={isRecoverableError}
312+
routerType="pages"
232313
/>
233314
)
234315
})
235316

236-
isMounted = true
317+
isPagesMounted = true
237318
}
238319
}

packages/next/src/client/components/react-dev-overlay/app/app-dev-overlay.shim.ts renamed to packages/next/src/client/components/react-dev-overlay/dev-overlay.shim.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ export function renderAppDevOverlay() {
44
)
55
}
66

7+
export function renderPagesDevOverlay() {
8+
throw new Error(
9+
"Next DevTools: Can't render in this environment. This is a bug in Next.js"
10+
)
11+
}
12+
713
// TODO: Extract into separate functions that are imported
814
export const dispatcher = new Proxy(
915
{},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dev-overlay.browser'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dev-overlay.browser'

packages/next/src/client/components/react-dev-overlay/pages/client.ts

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
1-
import * as Bus from './bus'
1+
import { dispatcher } from 'next/dist/compiled/next-devtools'
22
import {
33
attachHydrationErrorState,
44
storeHydrationErrorStateFromConsoleArgs,
55
} from './hydration-error-state'
6-
import {
7-
ACTION_BEFORE_REFRESH,
8-
ACTION_BUILDING_INDICATOR_HIDE,
9-
ACTION_BUILD_ERROR,
10-
ACTION_BUILD_OK,
11-
ACTION_BUILDING_INDICATOR_SHOW,
12-
ACTION_DEV_INDICATOR,
13-
ACTION_REFRESH,
14-
ACTION_STATIC_INDICATOR,
15-
ACTION_UNHANDLED_ERROR,
16-
ACTION_UNHANDLED_REJECTION,
17-
ACTION_VERSION_INFO,
18-
} from '../shared'
196
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
207
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
218

@@ -35,10 +22,7 @@ function handleError(error: unknown) {
3522
error.name !== 'ModuleBuildError' &&
3623
error.name !== 'ModuleNotFoundError'
3724
) {
38-
Bus.emit({
39-
type: ACTION_UNHANDLED_ERROR,
40-
reason: error,
41-
})
25+
dispatcher.onUnhandledError(error)
4226
}
4327
}
4428

@@ -68,10 +52,7 @@ function onUnhandledRejection(ev: PromiseRejectionEvent) {
6852
return
6953
}
7054

71-
Bus.emit({
72-
type: ACTION_UNHANDLED_REJECTION,
73-
reason: reason,
74-
})
55+
dispatcher.onUnhandledRejection(reason)
7556
}
7657

7758
export function register() {
@@ -90,39 +71,39 @@ export function register() {
9071
}
9172

9273
export function onBuildOk() {
93-
Bus.emit({ type: ACTION_BUILD_OK })
74+
dispatcher.onBuildOk()
9475
}
9576

9677
export function onBuildError(message: string) {
97-
Bus.emit({ type: ACTION_BUILD_ERROR, message })
78+
dispatcher.onBuildError(message)
9879
}
9980

10081
export function onRefresh() {
101-
Bus.emit({ type: ACTION_REFRESH })
82+
dispatcher.onRefresh()
10283
}
10384

10485
export function onBeforeRefresh() {
105-
Bus.emit({ type: ACTION_BEFORE_REFRESH })
86+
dispatcher.onBeforeRefresh()
10687
}
10788

10889
export function onVersionInfo(versionInfo: VersionInfo) {
109-
Bus.emit({ type: ACTION_VERSION_INFO, versionInfo })
90+
dispatcher.onVersionInfo(versionInfo)
11091
}
11192

11293
export function onStaticIndicator(isStatic: boolean) {
113-
Bus.emit({ type: ACTION_STATIC_INDICATOR, staticIndicator: isStatic })
94+
dispatcher.onStaticIndicator(isStatic)
11495
}
11596

11697
export function onDevIndicator(devIndicatorsState: DevIndicatorServerState) {
117-
Bus.emit({ type: ACTION_DEV_INDICATOR, devIndicator: devIndicatorsState })
98+
dispatcher.onDevIndicator(devIndicatorsState)
11899
}
119100

120101
export function buildingIndicatorShow() {
121-
Bus.emit({ type: ACTION_BUILDING_INDICATOR_SHOW })
102+
dispatcher.buildingIndicatorShow()
122103
}
123104

124105
export function buildingIndicatorHide() {
125-
Bus.emit({ type: ACTION_BUILDING_INDICATOR_HIDE })
106+
dispatcher.buildingIndicatorHide()
126107
}
127108

128109
export { getErrorByType } from '../utils/get-error-by-type'
Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
import React from 'react'
2-
import * as Bus from './bus'
3-
import { useErrorOverlayReducer } from '../shared'
42
import { Router } from '../../../router'
3+
import { renderPagesDevOverlay } from 'next/dist/compiled/next-devtools'
54
import { getComponentStack, getOwnerStack } from '../../errors/stitched-error'
65
import { isRecoverableError } from '../../../react-client-callbacks/on-recoverable-error'
6+
import { getSquashedHydrationErrorDetails } from './hydration-error-state'
77

8-
export const usePagesDevOverlay = () => {
9-
const [state, dispatch] = useErrorOverlayReducer(
10-
'pages',
11-
getComponentStack,
12-
getOwnerStack,
13-
isRecoverableError
14-
)
8+
export const usePagesDevOverlayBridge = () => {
9+
React.useInsertionEffect(() => {
10+
// NDT uses a different React instance so it's not technically a state update
11+
// scheduled from useInsertionEffect.
12+
renderPagesDevOverlay(
13+
getComponentStack,
14+
getOwnerStack,
15+
getSquashedHydrationErrorDetails,
16+
isRecoverableError
17+
)
18+
}, [])
1519

1620
React.useEffect(() => {
17-
Bus.on(dispatch)
18-
1921
const { handleStaticIndicator } =
2022
require('./hot-reloader-client') as typeof import('./hot-reloader-client')
2123

2224
Router.events.on('routeChangeComplete', handleStaticIndicator)
2325

2426
return function () {
2527
Router.events.off('routeChangeComplete', handleStaticIndicator)
26-
Bus.off(dispatch)
2728
}
28-
}, [dispatch])
29-
30-
return {
31-
state,
32-
dispatch,
33-
}
29+
}, [])
3430
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { PagesDevOverlayErrorBoundary } from './pages-dev-overlay-error-boundary'
2+
import { usePagesDevOverlayBridge } from './hooks'
3+
4+
export type ErrorType = 'runtime' | 'build'
5+
6+
export type PagesDevOverlayBridgeType = typeof PagesDevOverlayBridge
7+
8+
interface PagesDevOverlayBridgeProps {
9+
children?: React.ReactNode
10+
}
11+
12+
export function PagesDevOverlayBridge({
13+
children,
14+
}: PagesDevOverlayBridgeProps) {
15+
usePagesDevOverlayBridge()
16+
17+
return <PagesDevOverlayErrorBoundary>{children}</PagesDevOverlayErrorBoundary>
18+
}

0 commit comments

Comments
 (0)