'use client' import { ArrowUp, ChevronLeft, ChevronRight, Box, File, PanelTop, SquareFunction, Server, Globe, MessageCircleQuestion, } from 'lucide-react' import { useMemo, useState } from 'react' import type { AnalyzeData, ModuleIndex, ModulesData, SourceIndex, } from '@/lib/analyze-data' import { splitIdent } from '@/lib/utils' import clsx from 'clsx' import { Button } from '@/components/ui/button' interface ImportChainProps { startFileId: number analyzeData: AnalyzeData modulesData: ModulesData depthMap: Map environmentFilter: 'client' | 'server' } interface ChainLevel { moduleIndex: ModuleIndex sourceIndex: SourceIndex | undefined path: string depth: number fullPath?: string templateArgs?: string layer?: string moduleType?: string treeShaking?: string selectedIndex: number totalCount: number // Info about this level's relationship to parent (undefined for root) info?: DependentInfo } interface DependentInfo { moduleIndex: number sourceIndex: number | undefined ident: string isAsync: boolean depth: number } type PathPart = { segment: string isCommon: boolean isLastCommon: boolean isPackageName: boolean isInfrastructure: boolean } function spitPathSegments(path: string): string[] { return Array.from(path.matchAll(/(.+?(?:\/|$))/g)).map(([i]) => i) } function getPathParts( currentPath: string, previousPath: string | null ): PathPart[] { const currentSegments = spitPathSegments(currentPath) let commonCount = 0 if (previousPath) { const previousSegments = spitPathSegments(previousPath) const minLength = Math.min(currentSegments.length, previousSegments.length) for (let i = 0; i < minLength; i++) { if (currentSegments[i] === previousSegments[i]) { commonCount++ } else { break } } } let infrastructureCount = 0 let packageNameCount = 0 let nodeModulesIndex = currentSegments.lastIndexOf('node_modules/') if (nodeModulesIndex === -1) { nodeModulesIndex = currentSegments.length } else { infrastructureCount = nodeModulesIndex + 1 if (currentSegments[nodeModulesIndex + 1]?.startsWith('@')) { packageNameCount = 2 } else { packageNameCount = 1 } } return currentSegments.map((segment, i) => ({ segment, isCommon: i < commonCount, isLastCommon: i === commonCount - 1, isInfrastructure: i < infrastructureCount, isPackageName: i >= infrastructureCount && i < infrastructureCount + packageNameCount, })) } function getTitle(level: ChainLevel) { const parts = [] if (level.fullPath) parts.push(`Full Path: ${level.fullPath}`) else parts.push(`Path: ${level.path}`) if (level.layer) parts.push(`Layer: ${level.layer}`) if (level.moduleType) parts.push(`Module Type: ${level.moduleType}`) if (level.treeShaking) parts.push(`Tree Shaking: ${level.treeShaking}`) if (level.templateArgs) parts.push(`Template Args: ${level.templateArgs}`) return parts.join('\n') } export function ImportChain({ startFileId, analyzeData, modulesData, depthMap, environmentFilter, }: ImportChainProps) { // Filter to include only the current route const [currentRouteOnly, setCurrentRouteOnly] = useState(true) // Track which dependent is selected at each level const [selectedIndices, setSelectedIndices] = useState([]) // Helper function to get module indices from source path const getModuleIndicesFromSourceIndex = (sourceIndex: number) => { const path = analyzeData.getFullSourcePath(sourceIndex) return modulesData.getModuleIndiciesFromPath(path) } // Helper function to get source index from module path const getSourceIndexFromModuleIndex = (moduleIndex: number) => { const module = modulesData.module(moduleIndex) if (!module) return undefined // Search through all sources to find one with matching path const modulePath = module.path for (let i = 0; i < analyzeData.sourceCount(); i++) { if (analyzeData.getFullSourcePath(i) === modulePath) { return i } } return undefined } // Build the import chain based on current selections const chain = useMemo(() => { const result: ChainLevel[] = [] const visitedModules = new Set() const startPath = analyzeData.getFullSourcePath(startFileId) if (!startPath) return result // Get all module indices for the starting source const startModuleIndices = getModuleIndicesFromSourceIndex( startFileId ).filter((moduleIndex) => { if (currentRouteOnly && !depthMap.has(moduleIndex)) { return false } let module = modulesData.module(moduleIndex) let layer = splitIdent(module?.ident || '').layer if (layer) { if (environmentFilter === 'client' && /ssr|rsc|route|api/.test(layer)) { return false } if (environmentFilter === 'server' && /client/.test(layer)) { return false } } return true }) if (startModuleIndices.length === 0) return result // Get the selected index for the start modules (default to 0) const selectedStartIdx = selectedIndices[0] ?? 0 const actualStartIdx = Math.min( selectedStartIdx, startModuleIndices.length - 1 ) const startModuleIndex = startModuleIndices[actualStartIdx] const startIdent = modulesData.module(startModuleIndex)?.ident ?? '' result.push({ moduleIndex: startModuleIndex, sourceIndex: startFileId, path: startPath, ...splitIdent(startIdent), depth: depthMap.get(startModuleIndex) ?? Infinity, selectedIndex: actualStartIdx, totalCount: startModuleIndices.length, }) visitedModules.add(startModuleIndex) // Build chain by following selected dependents let levelIndex = 1 let currentModuleIndex = startModuleIndex while (true) { // Get dependents at the module level (sync and async) const dependentModuleIndices = [ ...modulesData .moduleDependents(currentModuleIndex) .map((index: number) => ({ index, async: false, depth: depthMap.get(index) ?? Infinity, })), ...modulesData .asyncModuleDependents(currentModuleIndex) .map((index: number) => ({ index, async: true, depth: depthMap.get(index) ?? Infinity, })), ] // Filter out dependents that would create a cycle const validDependents = dependentModuleIndices.filter( ({ index, depth }) => !visitedModules.has(index) && (isFinite(depth) || !currentRouteOnly) ) if (validDependents.length === 0) { // No more dependents or all would create cycles break } // Build info for each dependent const dependentsInfo: DependentInfo[] = validDependents.map( ({ index: moduleIndex, async: isAsync, depth }) => { const sourceIndex = getSourceIndexFromModuleIndex(moduleIndex) let ident = modulesData.module(moduleIndex)?.ident || '' return { moduleIndex, sourceIndex, ident, isAsync, depth, } } ) // Sort: sync first, async second, then by source presence, then by depth dependentsInfo.sort((a, b) => { // Sort by depth (smallest first) if (a.depth !== b.depth) { return a.depth - b.depth } // Sort by ident length (shortest first) if (a.ident.length !== b.ident.length) { return a.ident.length - b.ident.length } // Sort by ident return a.ident.localeCompare(b.ident) }) // Get the selected index for this level (default to 0) const selectedIdx = selectedIndices[levelIndex] ?? 0 const actualIdx = Math.min(selectedIdx, dependentsInfo.length - 1) const selectedDepInfo = dependentsInfo[actualIdx] const selectedDepModule = modulesData.module(selectedDepInfo.moduleIndex) if (!selectedDepModule || selectedDepModule.ident == null) break result.push({ moduleIndex: selectedDepInfo.moduleIndex, sourceIndex: selectedDepInfo.sourceIndex, path: selectedDepModule.path, depth: depthMap.get(selectedDepInfo.moduleIndex) ?? Infinity, ...splitIdent(selectedDepModule.ident), selectedIndex: actualIdx, totalCount: dependentsInfo.length, info: selectedDepInfo, }) visitedModules.add(selectedDepInfo.moduleIndex) currentModuleIndex = selectedDepInfo.moduleIndex levelIndex++ // Safety check to prevent infinite loops if (levelIndex > 100) break } return result // eslint-disable-next-line react-hooks/exhaustive-deps }, [ startFileId, analyzeData, modulesData, selectedIndices, currentRouteOnly, depthMap, environmentFilter, ]) const handlePrevious = (levelIndex: number) => { setSelectedIndices((prev) => { const newIndices = [...prev] const currentIdx = newIndices[levelIndex] ?? 0 const level = chain[levelIndex] newIndices[levelIndex] = currentIdx > 0 ? currentIdx - 1 : level.totalCount - 1 return newIndices.slice(0, levelIndex + 1) }) } const handleNext = (levelIndex: number) => { setSelectedIndices((prev) => { const newIndices = [...prev] const currentIdx = newIndices[levelIndex] ?? 0 const level = chain[levelIndex] newIndices[levelIndex] = currentIdx < level.totalCount - 1 ? currentIdx + 1 : 0 return newIndices.slice(0, levelIndex + 1) }) } const startPath = analyzeData.getFullSourcePath(startFileId) if (!startPath) return null return (

Import Chain

{chain.map((level, index) => { const previousPath = index > 0 ? chain[index - 1].path : null const parts = getPathParts(level.path, previousPath) const flags = level.sourceIndex !== undefined ? analyzeData.getSourceFlags(level.sourceIndex) : undefined // Get the current item's info from the level itself const currentItemInfo = level.info return (
{currentItemInfo?.isAsync &&
}
{currentItemInfo?.isAsync && ( (async) )} {index > 0 ? ( ) : undefined} {level.totalCount > 1 && (
{level.selectedIndex + 1}/{level.totalCount}
)}
{currentItemInfo?.isAsync &&
}
{!level.layer ? (
) : /app/.test(level.layer || '') ? (
) : (
)}
{parts.map( ( { segment, isCommon, isLastCommon, isInfrastructure, isPackageName, }, i ) => ( 20 && 'break-all', isCommon && !isLastCommon && !isPackageName && !isInfrastructure && 'text-muted-foreground/80', !isCommon && !isInfrastructure && 'font-bold', isInfrastructure && 'text-muted-foreground/50', isPackageName && 'text-orange-500' )} > {segment} ) )}
{/* Show icons for current item if we have flag info or no source */}
{/client/.test(level.layer || '') && (flags?.client ? (
) : (
))} {/ssr/.test(level.layer || '') && (flags?.server ? (
) : (
))} {/rsc/.test(level.layer || '') && (flags?.server ? (
) : (
))} {/route|api/.test(level.layer || '') && (flags?.server ? (
) : (
))}
{level.treeShaking && (
{level.treeShaking === 'locals' ? '(local declarations only)' : level.treeShaking === 'module evaluation' ? '(only the module evaluation part of the module)' : `(${level.treeShaking})`}
)}
) })} {chain.length === 0 && (

No dependents found.{' '} {currentRouteOnly ? ( ) : null}

)}
) }