| | import { |
| | ACTION_BEFORE_REFRESH, |
| | ACTION_BUILD_ERROR, |
| | ACTION_BUILD_OK, |
| | ACTION_DEBUG_INFO, |
| | ACTION_DEV_INDICATOR, |
| | ACTION_REFRESH, |
| | ACTION_ERROR_OVERLAY_CLOSE, |
| | ACTION_ERROR_OVERLAY_OPEN, |
| | ACTION_ERROR_OVERLAY_TOGGLE, |
| | ACTION_STATIC_INDICATOR, |
| | ACTION_UNHANDLED_ERROR, |
| | ACTION_UNHANDLED_REJECTION, |
| | ACTION_VERSION_INFO, |
| | useErrorOverlayReducer, |
| | ACTION_BUILDING_INDICATOR_HIDE, |
| | ACTION_BUILDING_INDICATOR_SHOW, |
| | ACTION_RENDERING_INDICATOR_HIDE, |
| | ACTION_RENDERING_INDICATOR_SHOW, |
| | ACTION_DEVTOOL_UPDATE_ROUTE_STATE, |
| | ACTION_DEVTOOLS_CONFIG, |
| | type OverlayState, |
| | type DispatcherEvent, |
| | ACTION_CACHE_INDICATOR, |
| | } from './dev-overlay/shared' |
| |
|
| | import { |
| | createContext, |
| | startTransition, |
| | useContext, |
| | useEffect, |
| | useInsertionEffect, |
| | useLayoutEffect, |
| | type ActionDispatch, |
| | } from 'react' |
| | import { createRoot } from 'react-dom/client' |
| | import type { CacheIndicatorState } from './dev-overlay/cache-indicator' |
| | import { FontStyles } from './dev-overlay/font/font-styles' |
| | import type { HydrationErrorState } from './shared/hydration-error' |
| | import type { DebugInfo } from './shared/types' |
| | import { DevOverlay } from './dev-overlay/dev-overlay' |
| | import type { DevIndicatorServerState } from '../server/dev/dev-indicator-server-state' |
| | import type { VersionInfo } from '../server/dev/parse-version-info' |
| | import { |
| | insertSegmentNode, |
| | removeSegmentNode, |
| | getSegmentTrieRoot, |
| | } from './dev-overlay/segment-explorer-trie' |
| | import type { SegmentNodeState } from './userspace/app/segment-explorer-node' |
| | import type { DevToolsConfig } from './dev-overlay/shared' |
| | import type { SegmentTrieData } from '../shared/lib/mcp-page-metadata-types' |
| |
|
| | export interface Dispatcher { |
| | onBuildOk(): void |
| | onBuildError(message: string): void |
| | onVersionInfo(versionInfo: VersionInfo): void |
| | onDebugInfo(debugInfo: DebugInfo): void |
| | onBeforeRefresh(): void |
| | onRefresh(): void |
| | onCacheIndicator(status: CacheIndicatorState): void |
| | onStaticIndicator(status: 'pending' | 'static' | 'dynamic' | 'disabled'): void |
| | onDevIndicator(devIndicator: DevIndicatorServerState): void |
| | onDevToolsConfig(config: DevToolsConfig): void |
| | onUnhandledError(reason: Error): void |
| | onUnhandledRejection(reason: Error): void |
| | openErrorOverlay(): void |
| | closeErrorOverlay(): void |
| | toggleErrorOverlay(): void |
| | buildingIndicatorHide(): void |
| | buildingIndicatorShow(): void |
| | renderingIndicatorHide(): void |
| | renderingIndicatorShow(): void |
| | segmentExplorerNodeAdd(nodeState: SegmentNodeState): void |
| | segmentExplorerNodeRemove(nodeState: SegmentNodeState): void |
| | segmentExplorerUpdateRouteState(page: string): void |
| | } |
| |
|
| | type Dispatch = ReturnType<typeof useErrorOverlayReducer>[1] |
| | let maybeDispatch: Dispatch | null = null |
| | const queue: Array<(dispatch: Dispatch) => void> = [] |
| |
|
| | |
| | type OverlayStateWithRouter = OverlayState & { routerType: 'pages' | 'app' } |
| |
|
| | let currentOverlayState: OverlayStateWithRouter | null = null |
| |
|
| | export function getSerializedOverlayState(): OverlayStateWithRouter | null { |
| | |
| | |
| | if (!currentOverlayState) return null |
| |
|
| | return { |
| | ...currentOverlayState, |
| | errors: currentOverlayState.errors.map((errorEvent: any) => ({ |
| | ...errorEvent, |
| | error: errorEvent.error |
| | ? { |
| | name: errorEvent.error.name, |
| | message: errorEvent.error.message, |
| | stack: errorEvent.error.stack, |
| | } |
| | : null, |
| | })), |
| | } |
| | } |
| |
|
| | export function getSegmentTrieData(): SegmentTrieData | null { |
| | if (!currentOverlayState) { |
| | return null |
| | } |
| | const trieRoot = getSegmentTrieRoot() |
| | return { |
| | segmentTrie: trieRoot, |
| | routerType: currentOverlayState.routerType, |
| | } |
| | } |
| |
|
| | |
| | |
| | function createQueuable<Args extends any[]>( |
| | queueableFunction: (dispatch: Dispatch, ...args: Args) => void |
| | ) { |
| | return (...args: Args) => { |
| | if (maybeDispatch) { |
| | queueableFunction(maybeDispatch, ...args) |
| | } else { |
| | queue.push((dispatch: Dispatch) => { |
| | queueableFunction(dispatch, ...args) |
| | }) |
| | } |
| | } |
| | } |
| |
|
| | |
| | export const dispatcher: Dispatcher = { |
| | onBuildOk: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_BUILD_OK }) |
| | }), |
| | onBuildError: createQueuable((dispatch: Dispatch, message: string) => { |
| | dispatch({ type: ACTION_BUILD_ERROR, message }) |
| | }), |
| | onBeforeRefresh: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_BEFORE_REFRESH }) |
| | }), |
| | onRefresh: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_REFRESH }) |
| | }), |
| | onVersionInfo: createQueuable( |
| | (dispatch: Dispatch, versionInfo: VersionInfo) => { |
| | dispatch({ type: ACTION_VERSION_INFO, versionInfo }) |
| | } |
| | ), |
| | onCacheIndicator: createQueuable( |
| | (dispatch: Dispatch, status: CacheIndicatorState) => { |
| | dispatch({ type: ACTION_CACHE_INDICATOR, cacheIndicator: status }) |
| | } |
| | ), |
| | onStaticIndicator: createQueuable( |
| | ( |
| | dispatch: Dispatch, |
| | status: 'pending' | 'static' | 'dynamic' | 'disabled' |
| | ) => { |
| | dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status }) |
| | } |
| | ), |
| | onDebugInfo: createQueuable((dispatch: Dispatch, debugInfo: DebugInfo) => { |
| | dispatch({ type: ACTION_DEBUG_INFO, debugInfo }) |
| | }), |
| | onDevIndicator: createQueuable( |
| | (dispatch: Dispatch, devIndicator: DevIndicatorServerState) => { |
| | dispatch({ type: ACTION_DEV_INDICATOR, devIndicator }) |
| | } |
| | ), |
| | onDevToolsConfig: createQueuable( |
| | (dispatch: Dispatch, devToolsConfig: DevToolsConfig) => { |
| | dispatch({ type: ACTION_DEVTOOLS_CONFIG, devToolsConfig }) |
| | } |
| | ), |
| | onUnhandledError: createQueuable((dispatch: Dispatch, error: Error) => { |
| | dispatch({ |
| | type: ACTION_UNHANDLED_ERROR, |
| | reason: error, |
| | }) |
| | }), |
| | onUnhandledRejection: createQueuable((dispatch: Dispatch, error: Error) => { |
| | dispatch({ |
| | type: ACTION_UNHANDLED_REJECTION, |
| | reason: error, |
| | }) |
| | }), |
| | openErrorOverlay: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_ERROR_OVERLAY_OPEN }) |
| | }), |
| | closeErrorOverlay: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_ERROR_OVERLAY_CLOSE }) |
| | }), |
| | toggleErrorOverlay: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_ERROR_OVERLAY_TOGGLE }) |
| | }), |
| | buildingIndicatorHide: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_BUILDING_INDICATOR_HIDE }) |
| | }), |
| | buildingIndicatorShow: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_BUILDING_INDICATOR_SHOW }) |
| | }), |
| | renderingIndicatorHide: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_RENDERING_INDICATOR_HIDE }) |
| | }), |
| | renderingIndicatorShow: createQueuable((dispatch: Dispatch) => { |
| | dispatch({ type: ACTION_RENDERING_INDICATOR_SHOW }) |
| | }), |
| | segmentExplorerNodeAdd: createQueuable( |
| | (_: Dispatch, nodeState: SegmentNodeState) => { |
| | insertSegmentNode(nodeState) |
| | } |
| | ), |
| | segmentExplorerNodeRemove: createQueuable( |
| | (_: Dispatch, nodeState: SegmentNodeState) => { |
| | removeSegmentNode(nodeState) |
| | } |
| | ), |
| | segmentExplorerUpdateRouteState: createQueuable( |
| | (dispatch: Dispatch, page: string) => { |
| | dispatch({ type: ACTION_DEVTOOL_UPDATE_ROUTE_STATE, page }) |
| | } |
| | ), |
| | } |
| |
|
| | function replayQueuedEvents(dispatch: NonNullable<typeof maybeDispatch>) { |
| | try { |
| | for (const queuedFunction of queue) { |
| | queuedFunction(dispatch) |
| | } |
| | } finally { |
| | |
| | queue.length = 0 |
| | } |
| | } |
| |
|
| | function DevOverlayRoot({ |
| | enableCacheIndicator, |
| | getOwnerStack, |
| | getSquashedHydrationErrorDetails, |
| | isRecoverableError, |
| | routerType, |
| | shadowRoot, |
| | }: { |
| | enableCacheIndicator: boolean |
| | getOwnerStack: (error: Error) => string | null | undefined |
| | getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null |
| | isRecoverableError: (error: Error) => boolean |
| | routerType: 'app' | 'pages' |
| | shadowRoot: ShadowRoot |
| | }) { |
| | const [state, dispatch] = useErrorOverlayReducer( |
| | routerType, |
| | getOwnerStack, |
| | isRecoverableError, |
| | enableCacheIndicator |
| | ) |
| |
|
| | useEffect(() => { |
| | currentOverlayState = { ...state, routerType } |
| | }, [state, routerType]) |
| |
|
| | useLayoutEffect(() => { |
| | const portalNode = shadowRoot.host |
| | if (state.theme === 'dark') { |
| | portalNode.classList.add('dark') |
| | portalNode.classList.remove('light') |
| | } else if (state.theme === 'light') { |
| | portalNode.classList.add('light') |
| | portalNode.classList.remove('dark') |
| | } else { |
| | portalNode.classList.remove('dark') |
| | portalNode.classList.remove('light') |
| | } |
| | }, [shadowRoot, state.theme]) |
| |
|
| | useInsertionEffect(() => { |
| | maybeDispatch = dispatch |
| |
|
| | |
| | |
| | |
| | const replayTimeout = setTimeout(() => { |
| | replayQueuedEvents(dispatch) |
| | }) |
| |
|
| | return () => { |
| | maybeDispatch = null |
| | clearTimeout(replayTimeout) |
| | } |
| | }, []) |
| |
|
| | return ( |
| | <> |
| | {/* Fonts can only be loaded outside the Shadow DOM. */} |
| | <FontStyles /> |
| | <DevOverlayContext |
| | value={{ |
| | dispatch, |
| | getSquashedHydrationErrorDetails, |
| | shadowRoot, |
| | state, |
| | }} |
| | > |
| | <DevOverlay /> |
| | </DevOverlayContext> |
| | </> |
| | ) |
| | } |
| | export const DevOverlayContext = createContext<{ |
| | shadowRoot: ShadowRoot |
| | state: OverlayState & { |
| | routerType: 'pages' | 'app' |
| | } |
| | dispatch: ActionDispatch<[action: DispatcherEvent]> |
| | getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null |
| | }>(null!) |
| | export const useDevOverlayContext = () => useContext(DevOverlayContext) |
| |
|
| | let isPagesMounted = false |
| | let isAppMounted = false |
| |
|
| | function getSquashedHydrationErrorDetailsApp() { |
| | |
| | return null |
| | } |
| |
|
| | export function renderAppDevOverlay( |
| | getOwnerStack: (error: Error) => string | null | undefined, |
| | isRecoverableError: (error: Error) => boolean, |
| | enableCacheIndicator: boolean |
| | ): void { |
| | if (isPagesMounted) { |
| | |
| | |
| | throw new Error( |
| | 'Next DevTools: Pages Dev Overlay is already mounted. This is a bug in Next.js' |
| | ) |
| | } |
| |
|
| | if (!isAppMounted) { |
| | |
| | |
| | const script = document.createElement('script') |
| | script.style.display = 'block' |
| | |
| | |
| | |
| | |
| | |
| | script.style.position = 'absolute' |
| | script.setAttribute('data-nextjs-dev-overlay', 'true') |
| |
|
| | const container = document.createElement('nextjs-portal') |
| |
|
| | script.appendChild(container) |
| | document.body.appendChild(script) |
| |
|
| | const root = createRoot(container, { |
| | identifierPrefix: 'ndt-', |
| | |
| | |
| | onDefaultTransitionIndicator: () => () => {}, |
| | }) |
| |
|
| | const shadowRoot = container.attachShadow({ mode: 'open' }) |
| |
|
| | startTransition(() => { |
| | |
| | |
| | root.render( |
| | <DevOverlayRoot |
| | enableCacheIndicator={enableCacheIndicator} |
| | getOwnerStack={getOwnerStack} |
| | getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetailsApp} |
| | isRecoverableError={isRecoverableError} |
| | routerType="app" |
| | shadowRoot={shadowRoot} |
| | /> |
| | ) |
| | }) |
| |
|
| | isAppMounted = true |
| | } |
| | } |
| |
|
| | export function renderPagesDevOverlay( |
| | getOwnerStack: (error: Error) => string | null | undefined, |
| | getSquashedHydrationErrorDetails: ( |
| | error: Error |
| | ) => HydrationErrorState | null, |
| | isRecoverableError: (error: Error) => boolean |
| | ): void { |
| | if (isAppMounted) { |
| | |
| | |
| | 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') |
| | |
| | |
| | |
| | |
| | |
| | container.style.position = 'absolute' |
| |
|
| | |
| | |
| | |
| | new MutationObserver((records) => { |
| | for (const record of records) { |
| | if (record.type === 'childList') { |
| | for (const node of record.removedNodes) { |
| | if (node === container) { |
| | |
| | document.body.appendChild(container) |
| | } |
| | } |
| | } |
| | } |
| | }).observe(document.body, { |
| | childList: true, |
| | }) |
| | document.body.appendChild(container) |
| |
|
| | const root = createRoot(container, { identifierPrefix: 'ndt-' }) |
| |
|
| | const shadowRoot = container.attachShadow({ mode: 'open' }) |
| |
|
| | startTransition(() => { |
| | |
| | |
| | root.render( |
| | <DevOverlayRoot |
| | // Pages Router does not support Cache Components |
| | enableCacheIndicator={false} |
| | getOwnerStack={getOwnerStack} |
| | getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails} |
| | isRecoverableError={isRecoverableError} |
| | routerType="pages" |
| | shadowRoot={shadowRoot} |
| | /> |
| | ) |
| | }) |
| |
|
| | isPagesMounted = true |
| | } |
| | } |
| |
|