import './segment-explorer.css' import { useSegmentTree, type SegmentTrieNode, } from '../../segment-explorer-trie' import { cx } from '../../utils/cx' import { SegmentBoundaryTrigger } from './segment-boundary-trigger' import { Tooltip } from '../tooltip/tooltip' import { useCallback, useMemo } from 'react' import { BUILTIN_PREFIX, getBoundaryOriginFileType, isBoundaryFile, isBuiltinBoundaryFile, normalizeBoundaryFilename, } from '../../../../server/app-render/segment-explorer-path' import { SegmentSuggestion } from './segment-suggestion' import type { SegmentBoundaryType } from '../../../userspace/app/segment-explorer-node' const isFileNode = (node: SegmentTrieNode) => { return !!node.value?.type && !!node.value?.pagePath } // Utility functions for global boundary management function traverseTreeAndResetBoundaries(node: SegmentTrieNode) { // Reset this node's boundary if it has setBoundaryType function if (node.value?.setBoundaryType) { node.value.setBoundaryType(null) } // Recursively traverse children Object.values(node.children).forEach((child) => { if (child) { traverseTreeAndResetBoundaries(child) } }) } function countActiveBoundaries(node: SegmentTrieNode): number { let count = 0 // Count this node's boundary override if it's active // Only count when there's a non ":boundary" type and it has an active override (boundaryType is not null) // This means the file is showing an overridden boundary instead of its original file if ( node.value?.setBoundaryType && node.value.boundaryType !== null && !isBoundaryFile(node.value.type) ) { count++ } // Recursively count children Object.values(node.children).forEach((child) => { if (child) { count += countActiveBoundaries(child) } }) return count } function PageRouteBar({ page }: { page: string }) { return (
{page}
) } function SegmentExplorerFooter({ activeBoundariesCount, onGlobalReset, }: { activeBoundariesCount: number onGlobalReset: () => void }) { const hasActiveOverrides = activeBoundariesCount > 0 return (
) } function FilePill({ type, isBuiltin, isOverridden, filePath, fileName, }: { type: string isBuiltin: boolean isOverridden: boolean filePath: string fileName: string }) { return ( { openInEditor({ filePath }) }} > {fileName} {isBuiltin ? : } ) } export function PageSegmentTree({ page }: { page: string }) { const tree = useSegmentTree() // Count active boundaries for the badge const activeBoundariesCount = useMemo(() => { return countActiveBoundaries(tree) }, [tree]) // Global reset handler const handleGlobalReset = useCallback(() => { traverseTreeAndResetBoundaries(tree) }, [tree]) return (
) } const GLOBAL_ERROR_BOUNDARY_TYPE = 'global-error' function PageSegmentTreeLayerPresentation({ segment, node, level, }: { segment: string node: SegmentTrieNode level: number }) { const childrenKeys = useMemo( () => Object.keys(node.children), [node.children] ) const missingGlobalError = useMemo(() => { const existingBoundaries: string[] = [] childrenKeys.forEach((key) => { const childNode = node.children[key] if (!childNode || !childNode.value) return const boundaryType = getBoundaryOriginFileType(childNode.value.type) const isGlobalConvention = boundaryType === GLOBAL_ERROR_BOUNDARY_TYPE if ( // If global-* convention is not built-in, it's existed (isGlobalConvention && !isBuiltinBoundaryFile(childNode.value.pagePath)) || (!isGlobalConvention && // If it's non global boundary, we check if file is boundary type isBoundaryFile(childNode.value.type)) ) { existingBoundaries.push(boundaryType) } }) return ( level === 0 && !existingBoundaries.includes(GLOBAL_ERROR_BOUNDARY_TYPE) ) }, [node.children, childrenKeys, level]) const sortedChildrenKeys = childrenKeys.sort((a, b) => { // Prioritize files with extensions over directories const aHasExt = a.includes('.') const bHasExt = b.includes('.') if (aHasExt && !bHasExt) return -1 if (!aHasExt && bHasExt) return 1 // For files, sort by priority: layout > template > page > boundaries > others if (aHasExt && bHasExt) { const aType = node.children[a]?.value?.type const bType = node.children[b]?.value?.type // Define priority order const getTypePriority = (type: string | undefined): number => { if (!type) return 5 if (type === 'layout') return 1 if (type === 'template') return 2 if (type === 'page') return 3 if (isBoundaryFile(type)) return 4 return 5 } const aPriority = getTypePriority(aType) const bPriority = getTypePriority(bType) // Sort by priority first if (aPriority !== bPriority) { return aPriority - bPriority } // If same priority, sort by file path const aFilePath = node.children[a]?.value?.pagePath || '' const bFilePath = node.children[b]?.value?.pagePath || '' return aFilePath.localeCompare(bFilePath) } // For directories, sort alphabetically return a.localeCompare(b) }) // If it's the 1st level and contains a file, use 'app' as the folder name const folderName = level === 0 && !segment ? 'app' : segment const folderChildrenKeys: string[] = [] const filesChildrenKeys: string[] = [] for (const childKey of sortedChildrenKeys) { const childNode = node.children[childKey] if (!childNode) continue // If it's a file node, add it to filesChildrenKeys if (isFileNode(childNode)) { filesChildrenKeys.push(childKey) continue } // Otherwise, it's a folder node, add it to folderChildrenKeys folderChildrenKeys.push(childKey) } const possibleExtension = normalizeBoundaryFilename(filesChildrenKeys[0] || '') .split('.') .pop() || 'js' let firstChild = null for (let i = sortedChildrenKeys.length - 1; i >= 0; i--) { const childNode = node.children[sortedChildrenKeys[i]] if (!childNode || !childNode.value) continue const isBoundary = isBoundaryFile(childNode.value.type) if (!firstChild && !isBoundary) { firstChild = childNode break } } let firstBoundaryChild = null for (const childKey of sortedChildrenKeys) { const childNode = node.children[childKey] if (!childNode || !childNode.value) continue if (isBoundaryFile(childNode.value.type)) { firstBoundaryChild = childNode break } } firstChild = firstChild || firstBoundaryChild const hasFilesChildren = filesChildrenKeys.length > 0 const boundaries: Record = { 'not-found': null, loading: null, error: null, 'global-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 ( <> {hasFilesChildren && (
{folderName && ( {folderName} {/* hidden slashes for testing snapshots */} {'/'} )} {missingGlobalError && ( )} {/* display all the file segments in this level */} {filesChildrenKeys.length > 0 && ( {filesChildrenKeys.map((fileChildSegment) => { const childNode = node.children[fileChildSegment] 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 } // If it's a page/default file, don't show it as a separate label since it's represented by the dropdown button // if ( // childNode.value.type === 'page' || // childNode.value.type === 'default' // ) { // return null // } const filePath = childNode.value.pagePath const lastSegment = filePath.split('/').pop() || '' const isBuiltin = filePath.startsWith(BUILTIN_PREFIX) const fileName = normalizeBoundaryFilename(lastSegment) const tooltipMessage = isBuiltin ? `The default Next.js ${childNode.value.type} is being shown. You can customize this page by adding your own ${fileName} file to the app/ directory.` : null const isOverridden = childNode.value.boundaryType !== null return ( ) })} )} {firstChild && firstChild.value && ( )}
)} {folderChildrenKeys.map((childSegment) => { const child = node.children[childSegment] if (!child) { return null } // If it's an folder segment without any files under it, // merge it with the segment in the next level. const nextSegment = hasFilesChildren ? childSegment : segment + ' / ' + childSegment return ( ) })} ) } function openInEditor({ filePath }: { filePath: string }) { const params = new URLSearchParams({ file: filePath, // Mark the file path is relative to the app directory, // The editor launcher will complete the full path for it. isAppRelativePath: '1', }) fetch( `${ process.env.__NEXT_ROUTER_BASEPATH || '' }/__nextjs_launch-editor?${params.toString()}` ) } export function InfoIcon(props: React.SVGProps) { return ( ) } function BackArrowIcon() { return ( ) } function CodeIcon(props: React.SVGProps) { return ( ) }