Skip to content

Commit ce4e53f

Browse files
committed
[segment explorer] capture defined boundaries
1 parent cb1aed8 commit ce4e53f

File tree

7 files changed

+115
-28
lines changed

7 files changed

+115
-28
lines changed

packages/next/src/client/components/layout-router.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ export default function OuterLayoutRouter({
507507
forbidden,
508508
unauthorized,
509509
gracefullyDegrade,
510+
segmentViewBoundaries,
510511
}: {
511512
parallelRouterKey: string
512513
error: ErrorComponent | undefined
@@ -519,6 +520,7 @@ export default function OuterLayoutRouter({
519520
forbidden: React.ReactNode | undefined
520521
unauthorized: React.ReactNode | undefined
521522
gracefullyDegrade?: boolean
523+
segmentViewBoundaries?: React.ReactNode
522524
}) {
523525
const context = useContext(LayoutRouterContext)
524526
if (!context) {
@@ -659,13 +661,14 @@ export default function OuterLayoutRouter({
659661
)
660662

661663
if (process.env.NODE_ENV !== 'production') {
662-
const SegmentStateProvider = (
664+
const { SegmentStateProvider } =
663665
require('../../next-devtools/userspace/app/segment-explorer-node') as typeof import('../../next-devtools/userspace/app/segment-explorer-node')
664-
)
665-
.SegmentStateProvider as typeof import('../../next-devtools/userspace/app/segment-explorer-node').SegmentStateProvider as typeof import('../../next-devtools/userspace/app/segment-explorer-node').SegmentStateProvider
666666

667667
child = (
668-
<SegmentStateProvider key={stateKey}>{child}</SegmentStateProvider>
668+
<SegmentStateProvider key={stateKey}>
669+
{child}
670+
{segmentViewBoundaries}
671+
</SegmentStateProvider>
669672
)
670673
}
671674

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import type { SegmentNodeState } from '../../../userspace/app/segment-explorer-n
55
export function SegmentBoundaryTrigger({
66
onSelectBoundary,
77
offset,
8+
boundaries,
89
}: {
910
onSelectBoundary: SegmentNodeState['setBoundaryType']
1011
offset: number
12+
boundaries: Record<'not-found' | 'loading' | 'error', string | null>
1113
}) {
1214
const [shadowRoot] = useState<ShadowRoot>(() => {
1315
const ownerDocument = document
@@ -17,9 +19,24 @@ export function SegmentBoundaryTrigger({
1719
const shadowRootRef = useRef<ShadowRoot>(shadowRoot)
1820

1921
const triggerOptions = [
20-
{ label: 'Trigger Loading', value: 'loading', icon: <LoadingIcon /> },
21-
{ label: 'Trigger Error', value: 'error', icon: <ErrorIcon /> },
22-
{ label: 'Trigger Not Found', value: 'not-found', icon: <NotFoundIcon /> },
22+
{
23+
label: 'Trigger Loading',
24+
value: 'loading',
25+
icon: <LoadingIcon />,
26+
disabled: !boundaries.loading,
27+
},
28+
{
29+
label: 'Trigger Error',
30+
value: 'error',
31+
icon: <ErrorIcon />,
32+
disabled: !boundaries.error,
33+
},
34+
{
35+
label: 'Trigger Not Found',
36+
value: 'not-found',
37+
icon: <NotFoundIcon />,
38+
disabled: !boundaries['not-found'],
39+
},
2340
]
2441

2542
const resetOption = {
@@ -74,6 +91,7 @@ export function SegmentBoundaryTrigger({
7491
key={option.value}
7592
className="segment-boundary-dropdown-item"
7693
onClick={() => handleSelect(option.value)}
94+
disabled={option.disabled}
7795
>
7896
{option.icon}
7997
{option.label}
@@ -274,9 +292,14 @@ export const styles = `
274292
width: 100%;
275293
}
276294
295+
.segment-boundary-dropdown-item[data-disabled] {
296+
color: var(--color-gray-400);
297+
cursor: not-allowed;
298+
}
299+
277300
.segment-boundary-dropdown-item svg {
278301
margin-right: 12px;
279-
color: var(--color-gray-900);
302+
color: currentColor;
280303
}
281304
282305
.segment-boundary-dropdown-item:hover {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,23 @@ function PageSegmentTreeLayerPresentation({
133133
}
134134

135135
const hasFilesChildren = filesChildrenKeys.length > 0
136+
const boundaries: Record<'not-found' | 'loading' | 'error', string | null> = {
137+
'not-found': null,
138+
loading: null,
139+
error: null,
140+
}
141+
142+
filesChildrenKeys.forEach((childKey) => {
143+
const childNode = node.children[childKey]
144+
if (!childNode || !childNode.value) return
145+
if (childNode.value.type.startsWith('boundary:')) {
146+
const boundaryType = childNode.value.type.split(':')[1] as
147+
| 'not-found'
148+
| 'loading'
149+
| 'error'
150+
boundaries[boundaryType] = childNode.value.pagePath || null
151+
}
152+
})
136153

137154
return (
138155
<>
@@ -165,6 +182,11 @@ function PageSegmentTreeLayerPresentation({
165182
if (!childNode || !childNode.value) {
166183
return null
167184
}
185+
// If it's boundary node, which marks the existence of the boundary not the rendered status,
186+
// we don't need to present in the rendered files.
187+
if (childNode.value.type.startsWith('boundary:')) {
188+
return null
189+
}
168190
const filePath = childNode.value.pagePath
169191
const lastSegment = filePath.split('/').pop() || ''
170192
const isBuiltin = filePath.startsWith(BUILTIN_PREFIX)
@@ -208,6 +230,7 @@ function PageSegmentTreeLayerPresentation({
208230
<SegmentBoundaryTrigger
209231
offset={6}
210232
onSelectBoundary={pageChild.value.setBoundaryType}
233+
boundaries={boundaries}
211234
/>
212235
)}
213236
</div>

packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function createTrie<Value = string>({
9797
function remove(value: Value) {
9898
let currentNode = root
9999
const segments = getCharacters(value)
100+
100101
const stack: TrieNode<Value>[] = []
101102
let found = true
102103
for (const segment of segments) {

packages/next/src/next-devtools/userspace/app/segment-explorer-node.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,14 @@ function SegmentTrieNode({
3131
pagePath: string
3232
}): React.ReactNode {
3333
const { boundaryType, setBoundaryType } = useSegmentState()
34-
const nodeState: SegmentNodeState = useMemo(
35-
() => ({
34+
const nodeState: SegmentNodeState = useMemo(() => {
35+
return {
3636
type,
3737
pagePath,
3838
boundaryType,
3939
setBoundaryType,
40-
}),
41-
[type, pagePath, boundaryType, setBoundaryType]
42-
)
40+
}
41+
}, [type, pagePath, boundaryType, setBoundaryType])
4342

4443
// Use `useLayoutEffect` to ensure the state is updated during suspense.
4544
// `useEffect` won't work as the state is preserved during suspense.
@@ -143,7 +142,10 @@ export function SegmentStateProvider({ children }: { children: ReactNode }) {
143142
return (
144143
<SegmentStateContext.Provider
145144
key={errorBoundaryKey}
146-
value={{ boundaryType, setBoundaryType: setBoundaryTypeAndReload }}
145+
value={{
146+
boundaryType,
147+
setBoundaryType: setBoundaryTypeAndReload,
148+
}}
147149
>
148150
{children}
149151
</SegmentStateContext.Provider>

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -410,23 +410,24 @@ async function createComponentTreeInternal({
410410
<MetadataOutlet ready={getMetadataReady} />
411411
)
412412

413-
const notFoundElement = await createBoundaryConventionElement({
414-
ctx,
415-
conventionName: 'not-found',
416-
Component: NotFound,
417-
styles: notFoundStyles,
418-
tree,
419-
})
420-
421-
const forbiddenElement = await createBoundaryConventionElement({
413+
const [notFoundElement, notFoundFilePath] =
414+
await createBoundaryConventionElement({
415+
ctx,
416+
conventionName: 'not-found',
417+
Component: NotFound,
418+
styles: notFoundStyles,
419+
tree,
420+
})
421+
422+
const [forbiddenElement] = await createBoundaryConventionElement({
422423
ctx,
423424
conventionName: 'forbidden',
424425
Component: Forbidden,
425426
styles: forbiddenStyles,
426427
tree,
427428
})
428429

429-
const unauthorizedElement = await createBoundaryConventionElement({
430+
const [unauthorizedElement] = await createBoundaryConventionElement({
430431
ctx,
431432
conventionName: 'unauthorized',
432433
Component: Unauthorized,
@@ -546,8 +547,8 @@ async function createComponentTreeInternal({
546547
)
547548

548549
const templateFilePath = getConventionPathByType(tree, dir, 'template')
549-
550550
const errorFilePath = getConventionPathByType(tree, dir, 'error')
551+
const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
551552

552553
const wrappedErrorStyles =
553554
isSegmentViewEnabled && errorFilePath ? (
@@ -558,6 +559,34 @@ async function createComponentTreeInternal({
558559
errorStyles
559560
)
560561

562+
// Add a suffix to avoid conflict with the segment view node representing rendered file.
563+
// existence: not-found.tsx@boundary
564+
// rendered: not-found.tsx
565+
const fileNameSuffix = '@boundary'
566+
const segmentViewBoundaries = isSegmentViewEnabled ? (
567+
<>
568+
{notFoundFilePath && (
569+
<SegmentViewNode
570+
type={'boundary:not-found'}
571+
pagePath={notFoundFilePath + fileNameSuffix}
572+
/>
573+
)}
574+
{loadingFilePath && (
575+
<SegmentViewNode
576+
type={'boundary:loading'}
577+
pagePath={loadingFilePath + fileNameSuffix}
578+
/>
579+
)}
580+
{errorFilePath && (
581+
<SegmentViewNode
582+
type={'boundary:error'}
583+
pagePath={errorFilePath + fileNameSuffix}
584+
/>
585+
)}
586+
{/* do not surface forbidden and unauthorized boundaries yet as they're unstable */}
587+
</>
588+
) : null
589+
561590
return [
562591
parallelRouteKey,
563592
<LayoutRouter
@@ -581,6 +610,7 @@ async function createComponentTreeInternal({
581610
notFound={notFoundComponent}
582611
forbidden={forbiddenComponent}
583612
unauthorized={unauthorizedComponent}
613+
{...(isSegmentViewEnabled && { segmentViewBoundaries })}
584614
// Since gracefullyDegrade only applies to bots, only
585615
// pass it when we're in a bot context to avoid extra bytes.
586616
{...(gracefullyDegrade && { gracefullyDegrade })}
@@ -603,8 +633,8 @@ async function createComponentTreeInternal({
603633
}
604634

605635
let loadingElement = Loading ? <Loading key="l" /> : null
636+
const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
606637
if (isSegmentViewEnabled && loadingElement) {
607-
const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
608638
if (loadingFilePath) {
609639
loadingElement = (
610640
<SegmentViewNode
@@ -1098,18 +1128,20 @@ async function createBoundaryConventionElement({
10981128
</>
10991129
) : undefined
11001130

1131+
const pagePath = getConventionPathByType(tree, dir, conventionName)
1132+
11011133
const wrappedElement =
11021134
isSegmentViewEnabled && element ? (
11031135
<SegmentViewNode
11041136
key={cacheNodeKey + '-' + conventionName}
11051137
type={conventionName}
1106-
pagePath={getConventionPathByType(tree, dir, conventionName)!}
1138+
pagePath={pagePath!}
11071139
>
11081140
{element}
11091141
</SegmentViewNode>
11101142
) : (
11111143
element
11121144
)
11131145

1114-
return wrappedElement
1146+
return [wrappedElement, pagePath] as const
11151147
}
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 <div>Not Found @foo</div>
3+
}

0 commit comments

Comments
 (0)