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
11 changes: 7 additions & 4 deletions packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ export default function OuterLayoutRouter({
forbidden,
unauthorized,
gracefullyDegrade,
segmentViewBoundaries,
}: {
parallelRouterKey: string
error: ErrorComponent | undefined
Expand All @@ -519,6 +520,7 @@ export default function OuterLayoutRouter({
forbidden: React.ReactNode | undefined
unauthorized: React.ReactNode | undefined
gracefullyDegrade?: boolean
segmentViewBoundaries?: React.ReactNode
}) {
const context = useContext(LayoutRouterContext)
if (!context) {
Expand Down Expand Up @@ -659,13 +661,14 @@ export default function OuterLayoutRouter({
)

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

child = (
<SegmentStateProvider key={stateKey}>{child}</SegmentStateProvider>
<SegmentStateProvider key={stateKey}>
{child}
{segmentViewBoundaries}
</SegmentStateProvider>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import type { SegmentNodeState } from '../../../userspace/app/segment-explorer-n
export function SegmentBoundaryTrigger({
onSelectBoundary,
offset,
boundaries,
}: {
onSelectBoundary: SegmentNodeState['setBoundaryType']
offset: number
boundaries: Record<'not-found' | 'loading' | 'error', string | null>
}) {
const [shadowRoot] = useState<ShadowRoot>(() => {
const ownerDocument = document
Expand All @@ -17,9 +19,24 @@ export function SegmentBoundaryTrigger({
const shadowRootRef = useRef<ShadowRoot>(shadowRoot)

const triggerOptions = [
{ label: 'Trigger Loading', value: 'loading', icon: <LoadingIcon /> },
{ label: 'Trigger Error', value: 'error', icon: <ErrorIcon /> },
{ label: 'Trigger Not Found', value: 'not-found', icon: <NotFoundIcon /> },
{
label: 'Trigger Loading',
value: 'loading',
icon: <LoadingIcon />,
disabled: !boundaries.loading,
},
{
label: 'Trigger Error',
value: 'error',
icon: <ErrorIcon />,
disabled: !boundaries.error,
},
{
label: 'Trigger Not Found',
value: 'not-found',
icon: <NotFoundIcon />,
disabled: !boundaries['not-found'],
},
]

const resetOption = {
Expand Down Expand Up @@ -74,6 +91,7 @@ export function SegmentBoundaryTrigger({
key={option.value}
className="segment-boundary-dropdown-item"
onClick={() => handleSelect(option.value)}
disabled={option.disabled}
>
{option.icon}
{option.label}
Expand Down Expand Up @@ -274,9 +292,14 @@ export const styles = `
width: 100%;
}

.segment-boundary-dropdown-item[data-disabled] {
color: var(--color-gray-400);
cursor: not-allowed;
}

.segment-boundary-dropdown-item svg {
margin-right: 12px;
color: var(--color-gray-900);
color: currentColor;
}

.segment-boundary-dropdown-item:hover {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import {
} from './segment-boundary-trigger'
import { Tooltip } from '../../../components/tooltip'
import { useRef, useState } from 'react'

const BUILTIN_PREFIX = '__next_builtin__'
import {
BUILTIN_PREFIX,
getBoundaryOriginFileType,
isBoundaryFile,
normalizeBoundaryFilename,
} from '../../../../server/app-render/segment-explorer-path'

const isFileNode = (node: SegmentTrieNode) => {
return !!node.value?.type && !!node.value?.pagePath
Expand Down Expand Up @@ -133,6 +137,24 @@ function PageSegmentTreeLayerPresentation({
}

const hasFilesChildren = filesChildrenKeys.length > 0
const boundaries: Record<'not-found' | 'loading' | 'error', string | null> = {
'not-found': null,
loading: null,
error: null,
}

filesChildrenKeys.forEach((childKey) => {
const childNode = node.children[childKey]
if (!childNode || !childNode.value) return
if (isBoundaryFile(childNode.value.type)) {
const boundaryType = getBoundaryOriginFileType(childNode.value.type)

if (boundaryType in boundaries) {
boundaries[boundaryType as keyof typeof boundaries] =
childNode.value.pagePath || null
}
}
})

return (
<>
Expand Down Expand Up @@ -165,10 +187,15 @@ function PageSegmentTreeLayerPresentation({
if (!childNode || !childNode.value) {
return null
}
// If it's boundary node, which marks the existence of the boundary not the rendered status,
// we don't need to present in the rendered files.
if (isBoundaryFile(childNode.value.type)) {
return null
}
const filePath = childNode.value.pagePath
const lastSegment = filePath.split('/').pop() || ''
const isBuiltin = filePath.startsWith(BUILTIN_PREFIX)
const fileName = lastSegment.replace(BUILTIN_PREFIX, '')
const fileName = normalizeBoundaryFilename(lastSegment)

return (
<span
Expand Down Expand Up @@ -208,6 +235,7 @@ function PageSegmentTreeLayerPresentation({
<SegmentBoundaryTrigger
offset={6}
onSelectBoundary={pageChild.value.setBoundaryType}
boundaries={boundaries}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function createTrie<Value = string>({
function remove(value: Value) {
let currentNode = root
const segments = getCharacters(value)

const stack: TrieNode<Value>[] = []
let found = true
for (const segment of segments) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@ function SegmentTrieNode({
pagePath: string
}): React.ReactNode {
const { boundaryType, setBoundaryType } = useSegmentState()
const nodeState: SegmentNodeState = useMemo(
() => ({
const nodeState: SegmentNodeState = useMemo(() => {
return {
type,
pagePath,
boundaryType,
setBoundaryType,
}),
[type, pagePath, boundaryType, setBoundaryType]
)
}
}, [type, pagePath, boundaryType, setBoundaryType])

// Use `useLayoutEffect` to ensure the state is updated during suspense.
// `useEffect` won't work as the state is preserved during suspense.
Expand Down Expand Up @@ -143,7 +142,10 @@ export function SegmentStateProvider({ children }: { children: ReactNode }) {
return (
<SegmentStateContext.Provider
key={errorBoundaryKey}
value={{ boundaryType, setBoundaryType: setBoundaryTypeAndReload }}
value={{
boundaryType,
setBoundaryType: setBoundaryTypeAndReload,
}}
>
{children}
</SegmentStateContext.Provider>
Expand Down
63 changes: 49 additions & 14 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import type {
UseCachePageComponentProps,
} from '../use-cache/use-cache-wrapper'
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
import { getConventionPathByType } from './segment-explorer-path'
import {
BOUNDARY_PREFIX,
getConventionPathByType,
} from './segment-explorer-path'

/**
* Use the provided loader tree to create the React Component tree.
Expand Down Expand Up @@ -410,23 +413,24 @@ async function createComponentTreeInternal({
<MetadataOutlet ready={getMetadataReady} />
)

const notFoundElement = await createBoundaryConventionElement({
ctx,
conventionName: 'not-found',
Component: NotFound,
styles: notFoundStyles,
tree,
})
const [notFoundElement, notFoundFilePath] =
await createBoundaryConventionElement({
ctx,
conventionName: 'not-found',
Component: NotFound,
styles: notFoundStyles,
tree,
})

const forbiddenElement = await createBoundaryConventionElement({
const [forbiddenElement] = await createBoundaryConventionElement({
ctx,
conventionName: 'forbidden',
Component: Forbidden,
styles: forbiddenStyles,
tree,
})

const unauthorizedElement = await createBoundaryConventionElement({
const [unauthorizedElement] = await createBoundaryConventionElement({
ctx,
conventionName: 'unauthorized',
Component: Unauthorized,
Expand Down Expand Up @@ -546,8 +550,8 @@ async function createComponentTreeInternal({
)

const templateFilePath = getConventionPathByType(tree, dir, 'template')

const errorFilePath = getConventionPathByType(tree, dir, 'error')
const loadingFilePath = getConventionPathByType(tree, dir, 'loading')

const wrappedErrorStyles =
isSegmentViewEnabled && errorFilePath ? (
Expand All @@ -558,6 +562,34 @@ async function createComponentTreeInternal({
errorStyles
)

// Add a suffix to avoid conflict with the segment view node representing rendered file.
// existence: not-found.tsx@boundary
// rendered: not-found.tsx
const fileNameSuffix = '@boundary'
const segmentViewBoundaries = isSegmentViewEnabled ? (
<>
{notFoundFilePath && (
<SegmentViewNode
type={`${BOUNDARY_PREFIX}not-found`}
pagePath={notFoundFilePath + fileNameSuffix}
/>
)}
{loadingFilePath && (
<SegmentViewNode
type={`${BOUNDARY_PREFIX}loading`}
pagePath={loadingFilePath + fileNameSuffix}
/>
)}
{errorFilePath && (
<SegmentViewNode
type={`${BOUNDARY_PREFIX}error`}
pagePath={errorFilePath + fileNameSuffix}
/>
)}
{/* do not surface forbidden and unauthorized boundaries yet as they're unstable */}
</>
) : null

return [
parallelRouteKey,
<LayoutRouter
Expand All @@ -581,6 +613,7 @@ async function createComponentTreeInternal({
notFound={notFoundComponent}
forbidden={forbiddenComponent}
unauthorized={unauthorizedComponent}
{...(isSegmentViewEnabled && { segmentViewBoundaries })}
// Since gracefullyDegrade only applies to bots, only
// pass it when we're in a bot context to avoid extra bytes.
{...(gracefullyDegrade && { gracefullyDegrade })}
Expand All @@ -603,8 +636,8 @@ async function createComponentTreeInternal({
}

let loadingElement = Loading ? <Loading key="l" /> : null
const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
if (isSegmentViewEnabled && loadingElement) {
const loadingFilePath = getConventionPathByType(tree, dir, 'loading')
if (loadingFilePath) {
loadingElement = (
<SegmentViewNode
Expand Down Expand Up @@ -1098,18 +1131,20 @@ async function createBoundaryConventionElement({
</>
) : undefined

const pagePath = getConventionPathByType(tree, dir, conventionName)

const wrappedElement =
isSegmentViewEnabled && element ? (
<SegmentViewNode
key={cacheNodeKey + '-' + conventionName}
type={conventionName}
pagePath={getConventionPathByType(tree, dir, conventionName)!}
pagePath={pagePath!}
>
{element}
</SegmentViewNode>
) : (
element
)

return wrappedElement
return [wrappedElement, pagePath] as const
}
20 changes: 19 additions & 1 deletion packages/next/src/server/app-render/segment-explorer-path.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { LoaderTree } from '../lib/app-dir-module'

export const BUILTIN_PREFIX = '__next_builtin__'

export function normalizeConventionFilePath(
projectDir: string,
conventionPath: string | undefined
Expand All @@ -22,12 +24,28 @@ export function normalizeConventionFilePath(
if (nextInternalPrefixRegex.test(relativePath)) {
relativePath = relativePath.replace(nextInternalPrefixRegex, '')
// Add a special prefix to let segment explorer know it's a built-in component
relativePath = `__next_builtin__${relativePath}`
relativePath = `${BUILTIN_PREFIX}${relativePath}`
}

return relativePath
}

export const BOUNDARY_SUFFIX = '@boundary'
export function normalizeBoundaryFilename(filename: string) {
return filename
.replace(new RegExp(`^${BUILTIN_PREFIX}`), '')
.replace(new RegExp(`${BOUNDARY_SUFFIX}$`), '')
}

export const BOUNDARY_PREFIX = 'boundary:'
export function isBoundaryFile(fileType: string) {
return fileType.startsWith(BOUNDARY_PREFIX)
}

export function getBoundaryOriginFileType(fileType: string) {
return fileType.replace(BOUNDARY_PREFIX, '')
}

export function getConventionPathByType(
tree: LoaderTree,
dir: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function NotFound() {
return <div>Not Found @foo</div>
}
Loading