Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
539 lines
18 KiB
TypeScript
539 lines
18 KiB
TypeScript
'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<ModuleIndex, number>
|
|
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<number[]>([])
|
|
|
|
// 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<number>()
|
|
|
|
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 (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<h3 className="text-xs font-semibold text-foreground flex-1">
|
|
Import Chain
|
|
</h3>
|
|
<label
|
|
className="inline-flex items-center space-x-2 text-xs cursor-pointer"
|
|
title="Only include dependent modules that are part of the current route's bundle"
|
|
>
|
|
<span>Current route only</span>
|
|
<input
|
|
type="checkbox"
|
|
className="form-checkbox h-4 w-4 text-primary"
|
|
checked={currentRouteOnly}
|
|
onChange={() => setCurrentRouteOnly((prev) => !prev)}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div className="space-y-0">
|
|
{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 (
|
|
<div key={`${level.path}-${index}`}>
|
|
{currentItemInfo?.isAsync && <div className="h-8" />}
|
|
<div className="flex items-center justify-center gap-2 py-1">
|
|
{currentItemInfo?.isAsync && (
|
|
<span className="text-xs text-muted-foreground italic">
|
|
(async)
|
|
</span>
|
|
)}
|
|
{index > 0 ? (
|
|
<ArrowUp className="w-4 h-4 text-muted-foreground" />
|
|
) : undefined}
|
|
{level.totalCount > 1 && (
|
|
<div className="flex items-center gap-1 flex-none">
|
|
<button
|
|
type="button"
|
|
onClick={() => handlePrevious(index)}
|
|
className="p-0.5 hover:bg-accent rounded transition-colors cursor-pointer"
|
|
title="Previous dependent"
|
|
>
|
|
<ChevronLeft className="w-3 h-3" />
|
|
</button>
|
|
<span className="text-muted-foreground text-xs min-w-[3ch] text-center">
|
|
{level.selectedIndex + 1}/{level.totalCount}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleNext(index)}
|
|
className="p-0.5 hover:bg-accent rounded transition-colors cursor-pointer"
|
|
title="Next dependent"
|
|
>
|
|
<ChevronRight className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{currentItemInfo?.isAsync && <div className="h-8" />}
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex flex-col gap-1 items-center">
|
|
{!level.layer ? (
|
|
<div title="Unknown">
|
|
<MessageCircleQuestion className="w-3 h-3 text-gray-500" />
|
|
</div>
|
|
) : /app/.test(level.layer || '') ? (
|
|
<div title="App Router">
|
|
<Box className="w-3 h-3 text-green-500" />
|
|
</div>
|
|
) : (
|
|
<div title="Pages Router">
|
|
<File className="w-3 h-3 text-purple-500" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 border border-border rounded px-2 py-1 bg-background">
|
|
<span
|
|
className="font-mono text-xs text-foreground text-center block break-words"
|
|
title={getTitle(level)}
|
|
>
|
|
{parts.map(
|
|
(
|
|
{
|
|
segment,
|
|
isCommon,
|
|
isLastCommon,
|
|
isInfrastructure,
|
|
isPackageName,
|
|
},
|
|
i
|
|
) => (
|
|
<span
|
|
key={i}
|
|
className={clsx(
|
|
segment.length > 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}
|
|
<wbr />
|
|
</span>
|
|
)
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Show icons for current item if we have flag info or no source */}
|
|
<div className="flex flex-col gap-1 items-center">
|
|
{/client/.test(level.layer || '') &&
|
|
(flags?.client ? (
|
|
<div title="Included on client-side of this route">
|
|
<PanelTop className="w-3 h-3 text-green-500" />
|
|
</div>
|
|
) : (
|
|
<div title="On client layer, but not included on client-side of this route (might be optimized away)">
|
|
<PanelTop className="w-3 h-3 text-gray-500" />
|
|
</div>
|
|
))}
|
|
{/ssr/.test(level.layer || '') &&
|
|
(flags?.server ? (
|
|
<div title="Included on server-side of this route for server-side rendering">
|
|
<Globe className="w-3 h-3 text-blue-500" />
|
|
</div>
|
|
) : (
|
|
<div title="On server-side rendering layer, but not included on server-side of this route (might be optimized away)">
|
|
<Globe className="w-3 h-3 text-gray-500" />
|
|
</div>
|
|
))}
|
|
{/rsc/.test(level.layer || '') &&
|
|
(flags?.server ? (
|
|
<div title="Included on server-side of this route as Server Component">
|
|
<Server className="w-3 h-3 text-orange-500" />
|
|
</div>
|
|
) : (
|
|
<div title="On Server Component layer, but not included on server-side of this route (might be optimized away)">
|
|
<Server className="w-3 h-3 text-gray-500" />
|
|
</div>
|
|
))}
|
|
{/route|api/.test(level.layer || '') &&
|
|
(flags?.server ? (
|
|
<div title="Included on server-side of this route for API routes">
|
|
<SquareFunction className="w-3 h-3 text-red-500" />
|
|
</div>
|
|
) : (
|
|
<div title="On API route layer, but not included on server-side of this route (might be optimized away)">
|
|
<SquareFunction className="w-3 h-3 text-gray-500" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="block text-center">
|
|
{level.treeShaking && (
|
|
<div className="text-xs text-foreground text-center">
|
|
{level.treeShaking === 'locals'
|
|
? '(local declarations only)'
|
|
: level.treeShaking === 'module evaluation'
|
|
? '(only the module evaluation part of the module)'
|
|
: `(${level.treeShaking})`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
{chain.length === 0 && (
|
|
<p className="text-muted-foreground italic text-xs">
|
|
No dependents found.{' '}
|
|
{currentRouteOnly ? (
|
|
<Button
|
|
variant="link"
|
|
className="inline p-0 text-xs h-auto"
|
|
onClick={() => setCurrentRouteOnly(false)}
|
|
>
|
|
Show all routes.
|
|
</Button>
|
|
) : null}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|