Skip to content

Commit 2d2eac2

Browse files
committed
[segment explorer] display navigation error boundaries
1 parent 48bff8b commit 2d2eac2

File tree

9 files changed

+160
-29
lines changed

9 files changed

+160
-29
lines changed

packages/next/src/next-devtools/dev-overlay/components/overview/segment-explorer.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,10 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
251251
background-color: var(--color-blue-300);
252252
color: var(--color-blue-800);
253253
}
254+
.segment-explorer-file-label--not-found,
255+
.segment-explorer-file-label--forbidden,
256+
.segment-explorer-file-label--unauthorized {
257+
background-color: var(--color-amber-300);
258+
color: var(--color-amber-900);
259+
}
254260
`

packages/next/src/server/app-render/create-component-tree.tsx

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,13 @@ async function createComponentTreeInternal({
392392

393393
// Resolve the segment param
394394
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
395+
const isSegmentViewEnabled =
396+
process.env.NODE_ENV === 'development' &&
397+
ctx.renderOpts.devtoolSegmentExplorer
398+
const dir =
399+
process.env.NEXT_RUNTIME === 'edge'
400+
? process.env.__NEXT_EDGE_PROJECT_DIR!
401+
: ctx.renderOpts.dir || ''
395402

396403
// Use the same condition to render metadataOutlet as metadata
397404
const metadataOutlet = StreamingMetadataOutlet ? (
@@ -400,35 +407,30 @@ async function createComponentTreeInternal({
400407
<MetadataOutlet ready={getMetadataReady} />
401408
)
402409

403-
const notFoundElement = NotFound ? (
404-
<>
405-
<NotFound />
406-
{notFoundStyles}
407-
</>
408-
) : undefined
409-
410-
const forbiddenElement = Forbidden ? (
411-
<>
412-
<Forbidden />
413-
{forbiddenStyles}
414-
</>
415-
) : undefined
410+
const notFoundElement = await createBoundaryConventionElement({
411+
ctx,
412+
conventionName: 'not-found',
413+
Component: NotFound,
414+
styles: notFoundStyles,
415+
tree,
416+
})
416417

417-
const unauthorizedElement = Unauthorized ? (
418-
<>
419-
<Unauthorized />
420-
{unauthorizedStyles}
421-
</>
422-
) : undefined
418+
const forbiddenElement = await createBoundaryConventionElement({
419+
ctx,
420+
conventionName: 'forbidden',
421+
Component: Forbidden,
422+
styles: forbiddenStyles,
423+
tree,
424+
})
423425

424-
const dir =
425-
process.env.NEXT_RUNTIME === 'edge'
426-
? process.env.__NEXT_EDGE_PROJECT_DIR!
427-
: ctx.renderOpts.dir || ''
426+
const unauthorizedElement = await createBoundaryConventionElement({
427+
ctx,
428+
conventionName: 'unauthorized',
429+
Component: Unauthorized,
430+
styles: unauthorizedStyles,
431+
tree,
432+
})
428433

429-
const isSegmentViewEnabled =
430-
process.env.NODE_ENV === 'development' &&
431-
ctx.renderOpts.devtoolSegmentExplorer
432434
const nodeName = modType ?? 'page'
433435

434436
// TODO: Combine this `map` traversal with the loop below that turns the array
@@ -881,12 +883,12 @@ async function createComponentTreeInternal({
881883
<HTTPAccessFallbackBoundary
882884
key={cacheNodeKey}
883885
notFound={
884-
NotFound ? (
886+
notFoundElement ? (
885887
<>
886888
{layerAssets}
887889
<SegmentComponent params={params}>
888890
{notFoundStyles}
889-
<NotFound />
891+
{notFoundElement}
890892
</SegmentComponent>
891893
</>
892894
) : undefined
@@ -1028,6 +1030,54 @@ function getRootParamsImpl(
10281030
}
10291031
}
10301032

1033+
async function createBoundaryConventionElement({
1034+
ctx,
1035+
conventionName,
1036+
Component,
1037+
styles,
1038+
tree,
1039+
}: {
1040+
ctx: AppRenderContext
1041+
conventionName:
1042+
| 'not-found'
1043+
| 'error'
1044+
| 'loading'
1045+
| 'forbidden'
1046+
| 'unauthorized'
1047+
Component: React.ComponentType<any> | undefined
1048+
styles: React.ReactNode | undefined
1049+
tree: LoaderTree
1050+
}) {
1051+
const isSegmentViewEnabled =
1052+
process.env.NODE_ENV === 'development' &&
1053+
ctx.renderOpts.devtoolSegmentExplorer
1054+
const dir =
1055+
process.env.NEXT_RUNTIME === 'edge'
1056+
? process.env.__NEXT_EDGE_PROJECT_DIR!
1057+
: ctx.renderOpts.dir || ''
1058+
const { SegmentViewNode } = ctx.componentMod
1059+
const element = Component ? (
1060+
<>
1061+
<Component />
1062+
{styles}
1063+
</>
1064+
) : undefined
1065+
1066+
const wrappedElement =
1067+
isSegmentViewEnabled && element ? (
1068+
<SegmentViewNode
1069+
type={conventionName}
1070+
pagePath={getConventionPathByType(tree, dir, conventionName)!}
1071+
>
1072+
{element}
1073+
</SegmentViewNode>
1074+
) : (
1075+
element
1076+
)
1077+
1078+
return wrappedElement
1079+
}
1080+
10311081
function normalizeConventionFilePath(
10321082
projectDir: string,
10331083
conventionPath: string | undefined
@@ -1057,7 +1107,15 @@ function normalizeConventionFilePath(
10571107
function getConventionPathByType(
10581108
tree: LoaderTree,
10591109
dir: string,
1060-
conventionType: 'layout' | 'template' | 'page'
1110+
conventionType:
1111+
| 'layout'
1112+
| 'template'
1113+
| 'page'
1114+
| 'not-found'
1115+
| 'error'
1116+
| 'loading'
1117+
| 'forbidden'
1118+
| 'unauthorized'
10611119
) {
10621120
const modules = tree[2]
10631121
const conventionPath = modules[conventionType]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Forbidden() {
2+
return <h2>Custom Forbidden</h2>
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Layout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<div>
4+
<h1>Boundary Layout</h1>
5+
{children}
6+
</div>
7+
)
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function NotFound() {
2+
return <h2>Custom Not Found</h2>
3+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { forbidden, notFound, unauthorized } from 'next/navigation'
2+
3+
export default async function Page({
4+
searchParams,
5+
}: {
6+
searchParams: Promise<{ [key: string]: string }>
7+
}) {
8+
const search = await searchParams
9+
switch (search.name) {
10+
case 'not-found':
11+
return notFound()
12+
case 'forbidden':
13+
return forbidden()
14+
case 'unauthorized':
15+
return unauthorized()
16+
default:
17+
break
18+
}
19+
return <p>{'Visit /boundary?name=<boundary>'}</p>
20+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Unauthorized() {
2+
return <h2>Custom Unauthorized</h2>
3+
}

test/development/app-dir/segment-explorer/next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const nextConfig = {
55
experimental: {
66
devtoolSegmentExplorer: true,
7+
authInterrupts: true,
78
},
89
}
910

test/development/app-dir/segment-explorer/segment-explorer.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,33 @@ describe('segment-explorer', () => {
131131
global-error.js"
132132
`)
133133
})
134+
135+
it('should show navigation boundaries of the segment', async () => {
136+
const browser = await next.browser('/boundary?name=not-found')
137+
expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
138+
"app/
139+
layout.tsx
140+
boundary/
141+
layout.tsx
142+
not-found.tsx"
143+
`)
144+
145+
await browser.loadPage(`${next.url}/boundary?name=forbidden`)
146+
expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
147+
"app/
148+
layout.tsx
149+
boundary/
150+
layout.tsx
151+
forbidden.tsx"
152+
`)
153+
154+
await browser.loadPage(`${next.url}/boundary?name=unauthorized`)
155+
expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
156+
"app/
157+
layout.tsx
158+
boundary/
159+
layout.tsx
160+
unauthorized.tsx"
161+
`)
162+
})
134163
})

0 commit comments

Comments
 (0)