Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,5 +695,7 @@
"694": "createPrerenderPathname was called inside a client component scope.",
"695": "Expected workUnitAsyncStorage to have a store.",
"696": "Next DevTools: Can't dispatch %s in this environment. This is a bug in Next.js",
"697": "Next DevTools: Can't render in this environment. This is a bug in Next.js"
"697": "Next DevTools: Can't render in this environment. This is a bug in Next.js",
"698": "Next DevTools: App Dev Overlay is already mounted. This is a bug in Next.js",
"699": "Next DevTools: Pages Dev Overlay is already mounted. This is a bug in Next.js"
}
2 changes: 1 addition & 1 deletion packages/next/next-devtools.webpack-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = ({ dev, ...rest }) => {
return {
entry: path.join(
__dirname,
'src/client/components/react-dev-overlay/app/entrypoint.js'
'src/client/components/react-dev-overlay/entrypoint.js'
),
target,
mode: dev ? 'development' : 'production',
Expand Down
2 changes: 1 addition & 1 deletion packages/next/next-runtime.webpack-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const sharedExternals = [
const externalsMap = {
'./web/sandbox': 'next/dist/server/web/sandbox',
'next/dist/compiled/next-devtools':
'commonjs next/dist/client/components/react-dev-overlay/app/app-dev-overlay.shim.js',
'commonjs next/dist/client/components/react-dev-overlay/dev-overlay.shim.js',
}

const externalsRegexMap = {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import {
ACTION_BUILDING_INDICATOR_SHOW,
ACTION_RENDERING_INDICATOR_HIDE,
ACTION_RENDERING_INDICATOR_SHOW,
} from '../shared'
} from './shared'

import { startTransition, useInsertionEffect } from 'react'
import { createRoot } from 'react-dom/client'
import { FontStyles } from '../font/font-styles'
import type { DebugInfo } from '../types'
import { DevOverlay } from '../ui/dev-overlay'
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
import { FontStyles } from './font/font-styles'
import type { HydrationErrorState } from './pages/hydration-error-state'
import type { DebugInfo } from './types'
import { DevOverlay } from './ui/dev-overlay'
import type { DevIndicatorServerState } from '../../../server/dev/dev-indicator-server-state'
import type { VersionInfo } from '../../../server/dev/parse-version-info'

export interface Dispatcher {
onBuildOk(): void
Expand Down Expand Up @@ -143,22 +144,21 @@ function replayQueuedEvents(dispatch: NonNullable<typeof maybeDispatch>) {
}
}

function getSquashedHydrationErrorDetails() {
// We don't squash hydration errors in the App Router.
return null
}

function AppDevOverlay({
function DevOverlayRoot({
getComponentStack,
getOwnerStack,
getSquashedHydrationErrorDetails,
isRecoverableError,
routerType,
}: {
getComponentStack: (error: Error) => string | undefined
getOwnerStack: (error: Error) => string | null | undefined
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
isRecoverableError: (error: Error) => boolean
routerType: 'app' | 'pages'
}) {
const [state, dispatch] = useErrorOverlayReducer(
'app',
routerType,
getComponentStack,
getOwnerStack,
isRecoverableError
Expand Down Expand Up @@ -193,13 +193,28 @@ function AppDevOverlay({
)
}

let isMounted = false
let isPagesMounted = false
let isAppMounted = false

function getSquashedHydrationErrorDetailsApp() {
// We don't squash hydration errors in the App Router.
return null
}

export function renderAppDevOverlay(
getComponentStack: (error: Error) => string | undefined,
getOwnerStack: (error: Error) => string | null | undefined,
isRecoverableError: (error: Error) => boolean
): void {
if (!isMounted) {
if (isPagesMounted) {
// Switching between App and Pages Router is always a hard navigation
// TODO: Support soft navigation between App and Pages Router
throw new Error(
'Next DevTools: Pages Dev Overlay is already mounted. This is a bug in Next.js'
)
}

if (!isAppMounted) {
// React 19 will not throw away `<script>` elements in a container it owns.
// This ensures the actual user-space React does not unmount the Dev Overlay.
const script = document.createElement('script')
Expand All @@ -225,14 +240,80 @@ export function renderAppDevOverlay(
// TODO: Dedicated error boundary or root error callbacks?
// At least it won't unmount any user code if it errors.
root.render(
<AppDevOverlay
<DevOverlayRoot
getComponentStack={getComponentStack}
getOwnerStack={getOwnerStack}
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetailsApp}
isRecoverableError={isRecoverableError}
routerType="app"
/>
)
})

isAppMounted = true
}
}

export function renderPagesDevOverlay(
getComponentStack: (error: Error) => string | undefined,
getOwnerStack: (error: Error) => string | null | undefined,
getSquashedHydrationErrorDetails: (
error: Error
) => HydrationErrorState | null,
isRecoverableError: (error: Error) => boolean
): void {
if (isAppMounted) {
// Switching between App and Pages Router is always a hard navigation
// TODO: Support soft navigation between App and Pages Router
throw new Error(
'Next DevTools: App Dev Overlay is already mounted. This is a bug in Next.js'
)
}

if (!isPagesMounted) {
const container = document.createElement('nextjs-portal')
// Although the style applied to the shadow host is isolated,
// the element that attached the shadow host (i.e. "script")
// is still affected by the parent's style (e.g. "body"). This may
// occur style conflicts like "display: flex", with other children
// elements therefore give the shadow host an absolute position.
container.style.position = 'absolute'

// Pages Router runs with React 18 or 19 so we can't use the same trick as with
// App Router. We just reconnect the container if React wipes it e.g. when
// we recover from a shell error via createRoot()
new MutationObserver((records) => {
for (const record of records) {
if (record.type === 'childList') {
for (const node of record.removedNodes) {
if (node === container) {
// Reconnect the container to the body
document.body.appendChild(container)
}
}
}
}
}).observe(document.body, {
childList: true,
})
document.body.appendChild(container)

const root = createRoot(container)

startTransition(() => {
// TODO: Dedicated error boundary or root error callbacks?
// At least it won't unmount any user code if it errors.
root.render(
<DevOverlayRoot
getComponentStack={getComponentStack}
getOwnerStack={getOwnerStack}
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails}
isRecoverableError={isRecoverableError}
routerType="pages"
/>
)
})

isMounted = true
isPagesMounted = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ export function renderAppDevOverlay() {
)
}

export function renderPagesDevOverlay() {
throw new Error(
"Next DevTools: Can't render in this environment. This is a bug in Next.js"
)
}

// TODO: Extract into separate functions that are imported
export const dispatcher = new Proxy(
{},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dev-overlay.browser'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dev-overlay.browser'
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import * as Bus from './bus'
import { dispatcher } from 'next/dist/compiled/next-devtools'
import {
attachHydrationErrorState,
storeHydrationErrorStateFromConsoleArgs,
} from './hydration-error-state'
import {
ACTION_BEFORE_REFRESH,
ACTION_BUILDING_INDICATOR_HIDE,
ACTION_BUILD_ERROR,
ACTION_BUILD_OK,
ACTION_BUILDING_INDICATOR_SHOW,
ACTION_DEV_INDICATOR,
ACTION_REFRESH,
ACTION_STATIC_INDICATOR,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
ACTION_VERSION_INFO,
} from '../shared'
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'

Expand All @@ -35,10 +22,7 @@ function handleError(error: unknown) {
error.name !== 'ModuleBuildError' &&
error.name !== 'ModuleNotFoundError'
) {
Bus.emit({
type: ACTION_UNHANDLED_ERROR,
reason: error,
})
dispatcher.onUnhandledError(error)
}
}

Expand Down Expand Up @@ -68,10 +52,7 @@ function onUnhandledRejection(ev: PromiseRejectionEvent) {
return
}

Bus.emit({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
})
dispatcher.onUnhandledRejection(reason)
}

export function register() {
Expand All @@ -90,39 +71,39 @@ export function register() {
}

export function onBuildOk() {
Bus.emit({ type: ACTION_BUILD_OK })
dispatcher.onBuildOk()
}

export function onBuildError(message: string) {
Bus.emit({ type: ACTION_BUILD_ERROR, message })
dispatcher.onBuildError(message)
}

export function onRefresh() {
Bus.emit({ type: ACTION_REFRESH })
dispatcher.onRefresh()
}

export function onBeforeRefresh() {
Bus.emit({ type: ACTION_BEFORE_REFRESH })
dispatcher.onBeforeRefresh()
}

export function onVersionInfo(versionInfo: VersionInfo) {
Bus.emit({ type: ACTION_VERSION_INFO, versionInfo })
dispatcher.onVersionInfo(versionInfo)
}

export function onStaticIndicator(isStatic: boolean) {
Bus.emit({ type: ACTION_STATIC_INDICATOR, staticIndicator: isStatic })
dispatcher.onStaticIndicator(isStatic)
}

export function onDevIndicator(devIndicatorsState: DevIndicatorServerState) {
Bus.emit({ type: ACTION_DEV_INDICATOR, devIndicator: devIndicatorsState })
dispatcher.onDevIndicator(devIndicatorsState)
}

export function buildingIndicatorShow() {
Bus.emit({ type: ACTION_BUILDING_INDICATOR_SHOW })
dispatcher.buildingIndicatorShow()
}

export function buildingIndicatorHide() {
Bus.emit({ type: ACTION_BUILDING_INDICATOR_HIDE })
dispatcher.buildingIndicatorHide()
}

export { getErrorByType } from '../utils/get-error-by-type'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import React from 'react'
import * as Bus from './bus'
import { useErrorOverlayReducer } from '../shared'
import { Router } from '../../../router'
import { renderPagesDevOverlay } from 'next/dist/compiled/next-devtools'
import { getComponentStack, getOwnerStack } from '../../errors/stitched-error'
import { isRecoverableError } from '../../../react-client-callbacks/on-recoverable-error'
import { getSquashedHydrationErrorDetails } from './hydration-error-state'

export const usePagesDevOverlay = () => {
const [state, dispatch] = useErrorOverlayReducer(
'pages',
getComponentStack,
getOwnerStack,
isRecoverableError
)
export const usePagesDevOverlayBridge = () => {
React.useInsertionEffect(() => {
// NDT uses a different React instance so it's not technically a state update
// scheduled from useInsertionEffect.
renderPagesDevOverlay(
getComponentStack,
getOwnerStack,
getSquashedHydrationErrorDetails,
isRecoverableError
)
}, [])

React.useEffect(() => {
Bus.on(dispatch)

const { handleStaticIndicator } =
require('./hot-reloader-client') as typeof import('./hot-reloader-client')

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

return function () {
Router.events.off('routeChangeComplete', handleStaticIndicator)
Bus.off(dispatch)
}
}, [dispatch])

return {
state,
dispatch,
}
}, [])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PagesDevOverlayErrorBoundary } from './pages-dev-overlay-error-boundary'
import { usePagesDevOverlayBridge } from './hooks'

export type ErrorType = 'runtime' | 'build'

export type PagesDevOverlayBridgeType = typeof PagesDevOverlayBridge

interface PagesDevOverlayBridgeProps {
children?: React.ReactNode
}

export function PagesDevOverlayBridge({
children,
}: PagesDevOverlayBridgeProps) {
usePagesDevOverlayBridge()

return <PagesDevOverlayErrorBoundary>{children}</PagesDevOverlayErrorBoundary>
}
Loading
Loading