@@ -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
2222import { startTransition , useInsertionEffect } from 'react'
2323import { 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
3031export 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+
197204export 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}
0 commit comments