diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index a69058fe89e7b0..937eb6d037d149 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -435,6 +435,9 @@ function Router({ parentCacheNode: cache, parentSegmentPath: null, parentParams: {}, + // This is the "name" that shows up in the Suspense DevTools. + // It represents the root of the app. + debugNameContext: '/', // Root node always has `url` // Provided in AppTreeContext to ensure it can be overwritten in layout-router url: canonicalUrl, diff --git a/packages/next/src/client/components/layout-router.tsx b/packages/next/src/client/components/layout-router.tsx index 188a9994746bfc..3ae70d20ab354a 100644 --- a/packages/next/src/client/components/layout-router.tsx +++ b/packages/next/src/client/components/layout-router.tsx @@ -8,6 +8,7 @@ import type { LoadingModuleData } from '../../shared/lib/app-router-types' import type { FlightRouterState, FlightSegmentPath, + Segment, } from '../../shared/lib/app-router-types' import type { ErrorComponent } from './error-boundary' import { @@ -23,6 +24,7 @@ import React, { Suspense, useDeferredValue, type JSX, + type ActivityProps, } from 'react' import ReactDOM from 'react-dom' import { @@ -338,6 +340,7 @@ function ScrollAndFocusHandler({ function InnerLayoutRouter({ tree, segmentPath, + debugNameContext, cacheNode, params, url, @@ -345,6 +348,7 @@ function InnerLayoutRouter({ }: { tree: FlightRouterState segmentPath: FlightSegmentPath + debugNameContext: ActivityProps['name'] cacheNode: CacheNode params: Params url: string @@ -469,6 +473,7 @@ function InnerLayoutRouter({ parentCacheNode: cacheNode, parentSegmentPath: segmentPath, parentParams: params, + debugNameContext: debugNameContext, // TODO-APP: overriding of url for parallel routes url: url, @@ -487,9 +492,11 @@ function InnerLayoutRouter({ * If no loading property is provided it renders the children without a suspense boundary. */ function LoadingBoundary({ + name, loading, children, }: { + name: ActivityProps['name'] loading: LoadingModuleData | Promise children: React.ReactNode }): JSX.Element { @@ -519,7 +526,7 @@ function LoadingBoundary({ const loadingScripts = loadingModuleData[2] return ( {loadingStyles} @@ -577,6 +584,7 @@ export default function OuterLayoutRouter({ parentParams, url, isActive, + debugNameContext: parentDebugNameContext, } = context // Get the CacheNode for this segment by reading it from the parent segment's @@ -696,6 +704,12 @@ export default function OuterLayoutRouter({ } } + const debugName = getBoundaryDebugNameFromSegment(segment) + // `debugNameContext` represents the nearest non-"virtual" parent segment. + // `getBoundaryDebugNameFromSegment` returns null for virtual segments. + // So if `debugName` is null, the context is passed through unchanged. + const debugNameContext = debugName ?? parentDebugNameContext + // TODO: The loading module data for a segment is stored on the parent, then // applied to each of that parent segment's parallel route slots. In the // simple case where there's only one parallel route (the `children` slot), @@ -715,7 +729,7 @@ export default function OuterLayoutRouter({ errorStyles={errorStyles} errorScripts={errorScripts} > - + {segmentBoundaryTriggerNode} @@ -758,10 +773,17 @@ export default function OuterLayoutRouter({ } if (process.env.__NEXT_CACHE_COMPONENTS) { - const boundaryName = getBoundaryNameForSuspenseDevTools(tree) child = ( @@ -778,49 +800,33 @@ export default function OuterLayoutRouter({ return children } -function getBoundaryNameForSuspenseDevTools( - subtree: FlightRouterState -): string | undefined { - const segment = subtree[0] - +function getBoundaryDebugNameFromSegment(segment: Segment): string | undefined { + if (segment === '/') { + // Reached the root + return '/' + } if (typeof segment === 'string') { - const children = subtree[1] - const isPage = Object.keys(children).length === 0 - if (isPage) { - // Page segment - return '/' - } - - // Layout segment - - // Skip over "virtual" layouts that don't correspond to app- - // defined components. - if ( - segment === '' || - // For some reason, the loader tree sometimes includes extra __PAGE__ - // "layouts" when part of a parallel route. But it's not a leaf node. - // Otherwise, we wouldn't need this special case because pages are - // always leaf nodes. - // TODO: Investigate why the loader produces these fake page segments. - segment.startsWith(PAGE_SEGMENT_KEY) || - // This is inserted by the loader. We should consider encoding these - // in a more special way instead of checking the name, to distinguish them - // from app-defined groups. - segment[0] === '(virtual)' - ) { + if (isVirtualLayout(segment)) { return undefined + } else { + return segment + '/' } - - if (segment === '/_not-found') { - // Special case. For some reason, the name itself already has a - // leading slash. - return '/_not-found/' - } - - return `/${segment}/` } - - // Parameterized segments are always layouts const paramCacheKey = segment[1] - return `/${paramCacheKey}/` + return paramCacheKey + '/' +} + +function isVirtualLayout(segment: string): boolean { + return ( + // This is inserted by the loader. We should consider encoding these + // in a more special way instead of checking the name, to distinguish them + // from app-defined groups. + segment === '(slot)' || + // For some reason, the loader tree sometimes includes extra __PAGE__ + // "layouts" when part of a parallel route. But it's not a leaf node. + // Otherwise, we wouldn't need this special case because pages are + // always leaf nodes. + // TODO: Investigate why the loader produces these fake page segments. + segment.startsWith(PAGE_SEGMENT_KEY) + ) } diff --git a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts index 8051359166fe0d..10de1aac05f1f7 100644 --- a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts +++ b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts @@ -63,6 +63,7 @@ export const LayoutRouterContext = React.createContext<{ parentCacheNode: CacheNode parentSegmentPath: FlightSegmentPath | null parentParams: Params + debugNameContext: string | undefined url: string isActive: boolean } | null>(null) diff --git a/test/development/acceptance-app/hydration-error.test.ts b/test/development/acceptance-app/hydration-error.test.ts index 2f9673fca742d6..d266ef6d86127f 100644 --- a/test/development/acceptance-app/hydration-error.test.ts +++ b/test/development/acceptance-app/hydration-error.test.ts @@ -45,7 +45,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -107,7 +107,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -181,7 +181,7 @@ describe('Error overlay for hydration errors in App router', () => { - + } forbidden={undefined} unauthorized={undefined}> } ...> @@ -221,7 +221,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -262,7 +262,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -300,7 +300,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -342,7 +342,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -376,7 +376,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -415,7 +415,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -453,7 +453,7 @@ describe('Error overlay for hydration errors in App router', () => { "componentStack": "... - + @@ -510,7 +510,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -566,7 +566,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -625,7 +625,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -682,7 +682,7 @@ describe('Error overlay for hydration errors in App router', () => { - + @@ -966,7 +966,7 @@ describe('Error overlay for hydration errors in App router', () => { - + } forbidden={undefined} unauthorized={undefined}> } ...> diff --git a/test/development/app-dir/hydration-error-count/hydration-error-count.test.ts b/test/development/app-dir/hydration-error-count/hydration-error-count.test.ts index ed652792cb97dd..ba470b5b3359d8 100644 --- a/test/development/app-dir/hydration-error-count/hydration-error-count.test.ts +++ b/test/development/app-dir/hydration-error-count/hydration-error-count.test.ts @@ -17,7 +17,7 @@ describe('hydration-error-count', () => { - + @@ -66,7 +66,7 @@ describe('hydration-error-count', () => { - + @@ -120,7 +120,7 @@ describe('hydration-error-count', () => { - + @@ -155,7 +155,7 @@ describe('hydration-error-count', () => { - + @@ -197,7 +197,7 @@ describe('hydration-error-count', () => { - + @@ -229,7 +229,7 @@ describe('hydration-error-count', () => { - + @@ -267,7 +267,7 @@ describe('hydration-error-count', () => { - + @@ -299,7 +299,7 @@ describe('hydration-error-count', () => { - + @@ -343,7 +343,7 @@ describe('hydration-error-count', () => { - + diff --git a/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts b/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts index 9cef229eff676d..8c119c650d2ff6 100644 --- a/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts +++ b/test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts @@ -238,13 +238,13 @@ describe('Cache Components Errors', () => { at ScrollAndFocusHandler (bundler:///) at RenderFromTemplateContext (bundler:///) at OuterLayoutRouter (bundler:///) - 337 | */ - 338 | function InnerLayoutRouter({ - > 339 | tree, + 340 | */ + 341 | function InnerLayoutRouter({ + > 342 | tree, | ^ - 340 | segmentPath, - 341 | cacheNode, - 342 | params, + 343 | segmentPath, + 344 | debugNameContext, + 345 | cacheNode, To get a more detailed stack trace and pinpoint the issue, start the app in development mode by running \`next dev\`, then open "/dynamic-metadata-error-route" in your browser to investigate the error. Error occurred prerendering page "/dynamic-metadata-error-route". Read more: https://nextjs.org/docs/messages/prerender-error @@ -745,13 +745,13 @@ describe('Cache Components Errors', () => { at ScrollAndFocusHandler (bundler:///) at RenderFromTemplateContext (bundler:///) at OuterLayoutRouter (bundler:///) - 337 | */ - 338 | function InnerLayoutRouter({ - > 339 | tree, + 340 | */ + 341 | function InnerLayoutRouter({ + > 342 | tree, | ^ - 340 | segmentPath, - 341 | cacheNode, - 342 | params, + 343 | segmentPath, + 344 | debugNameContext, + 345 | cacheNode, To get a more detailed stack trace and pinpoint the issue, start the app in development mode by running \`next dev\`, then open "/dynamic-root" in your browser to investigate the error. Error occurred prerendering page "/dynamic-root". Read more: https://nextjs.org/docs/messages/prerender-error @@ -2141,13 +2141,13 @@ describe('Cache Components Errors', () => { at ScrollAndFocusHandler (bundler:///) at RenderFromTemplateContext () at OuterLayoutRouter (bundler:///) - 337 | */ - 338 | function InnerLayoutRouter({ - > 339 | tree, + 340 | */ + 341 | function InnerLayoutRouter({ + > 342 | tree, | ^ - 340 | segmentPath, - 341 | cacheNode, - 342 | params, + 343 | segmentPath, + 344 | debugNameContext, + 345 | cacheNode, To get a more detailed stack trace and pinpoint the issue, start the app in development mode by running \`next dev\`, then open "/sync-attribution/unguarded-async-guarded-clientsync" in your browser to investigate the error. Error occurred prerendering page "/sync-attribution/unguarded-async-guarded-clientsync". Read more: https://nextjs.org/docs/messages/prerender-error @@ -3157,13 +3157,13 @@ describe('Cache Components Errors', () => { at ScrollAndFocusHandler (bundler:///) at RenderFromTemplateContext (bundler:///) at OuterLayoutRouter (bundler:///) - 337 | */ - 338 | function InnerLayoutRouter({ - > 339 | tree, + 340 | */ + 341 | function InnerLayoutRouter({ + > 342 | tree, | ^ - 340 | segmentPath, - 341 | cacheNode, - 342 | params, + 343 | segmentPath, + 344 | debugNameContext, + 345 | cacheNode, To get a more detailed stack trace and pinpoint the issue, start the app in development mode by running \`next dev\`, then open "/use-cache-private-without-suspense" in your browser to investigate the error. Error occurred prerendering page "/use-cache-private-without-suspense". Read more: https://nextjs.org/docs/messages/prerender-error