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 (
)
}
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 (
)
}