mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
0e6f52e99b
We have no testing infrastructure, unfortunately <img width="2254" height="1260" alt="Bildschirmfoto 2026-05-28 um 20 29 58" src="https://github.com/user-attachments/assets/a9f1b646-994d-4417-98d7-160ce5249032" />
291 lines
7.4 KiB
TypeScript
291 lines
7.4 KiB
TypeScript
import type { AnalyzeData, SourceIndex } from './analyze-data'
|
|
import { layoutTreemap } from './layout-treemap'
|
|
import { SpecialModule } from './types'
|
|
import { getSpecialModuleType } from './utils'
|
|
|
|
export interface LayoutRect {
|
|
x: number
|
|
y: number
|
|
width: number
|
|
height: number
|
|
}
|
|
|
|
export interface LayoutNodeInfo {
|
|
name: string
|
|
size: number
|
|
server?: boolean
|
|
client?: boolean
|
|
traced?: boolean
|
|
}
|
|
|
|
export interface LayoutNode extends LayoutNodeInfo {
|
|
size: number
|
|
rect: LayoutRect
|
|
type: 'file' | 'directory' | 'collapsed-directory'
|
|
specialModuleType: SpecialModule | null
|
|
titleBarHeight?: number
|
|
children?: LayoutNode[]
|
|
itemCount?: number
|
|
js?: boolean
|
|
css?: boolean
|
|
json?: boolean
|
|
asset?: boolean
|
|
sourceIndex?: SourceIndex // Track which source this node represents
|
|
}
|
|
|
|
interface SourceMetadata {
|
|
filtered: boolean
|
|
size: number
|
|
compressedSize: number
|
|
}
|
|
|
|
export enum SizeMode {
|
|
Compressed = 'compressed',
|
|
Uncompressed = 'uncompressed',
|
|
}
|
|
|
|
function precomputeSourceMetadata(
|
|
analyzeData: AnalyzeData,
|
|
filterSource?: (sourceIndex: SourceIndex) => boolean
|
|
): SourceMetadata[] {
|
|
const sourceCount = analyzeData.sourceCount()
|
|
const metadata: SourceMetadata[] = new Array(sourceCount)
|
|
|
|
for (let i = sourceCount - 1; i >= 0; i--) {
|
|
const children = analyzeData.sourceChildren(i)
|
|
const ownSize = analyzeData.getOwnSizes(i)
|
|
|
|
if (children.length === 0) {
|
|
// file
|
|
metadata[i] = {
|
|
size: ownSize.size,
|
|
compressedSize: ownSize.compressedSize,
|
|
filtered: filterSource ? !filterSource(i) : false,
|
|
}
|
|
} else {
|
|
// directory
|
|
metadata[i] = {
|
|
filtered: true,
|
|
size: ownSize.size,
|
|
compressedSize: ownSize.compressedSize,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Top-down pass: aggregate child sizes and filtered status for directories
|
|
function processDirectory(idx: SourceIndex) {
|
|
const children = analyzeData.sourceChildren(idx)
|
|
if (children.length === 0) return // Already processed as leaf
|
|
|
|
let totalUncompressedSize = metadata[idx].size
|
|
let totalCompressedSize = metadata[idx].compressedSize
|
|
let hasVisibleChild = false
|
|
|
|
for (const childIdx of children) {
|
|
processDirectory(childIdx) // Process child first
|
|
if (!metadata[childIdx].filtered) {
|
|
// Only add size of visible (non-filtered) children
|
|
totalUncompressedSize += metadata[childIdx].size
|
|
totalCompressedSize += metadata[childIdx].compressedSize
|
|
hasVisibleChild = true
|
|
}
|
|
}
|
|
|
|
metadata[idx].size = totalUncompressedSize
|
|
metadata[idx].compressedSize = totalCompressedSize
|
|
metadata[idx].filtered = !hasVisibleChild // Directory filtered if no visible children
|
|
}
|
|
|
|
// Process from root sources
|
|
const roots = analyzeData.sourceRoots()
|
|
for (const rootIdx of roots) {
|
|
processDirectory(rootIdx)
|
|
}
|
|
|
|
return metadata
|
|
}
|
|
|
|
// Internal function that uses precomputed metadata
|
|
function computeTreemapLayoutFromAnalyzeInternal(
|
|
analyzeData: AnalyzeData,
|
|
sourceIndex: SourceIndex,
|
|
foldedPath: string,
|
|
rect: LayoutRect,
|
|
metadata: SourceMetadata[],
|
|
sizeMode: SizeMode
|
|
): LayoutNode {
|
|
const source = analyzeData.source(sourceIndex)
|
|
if (!source) {
|
|
throw new Error(`Source at index ${sourceIndex} not found`)
|
|
}
|
|
|
|
const isDirectory = source.path.endsWith('/') || !source.path
|
|
|
|
const childrenIndices = analyzeData.sourceChildren(sourceIndex)
|
|
|
|
// Fold single-child directories
|
|
if (
|
|
childrenIndices.length === 1 &&
|
|
isDirectory &&
|
|
(foldedPath + source.path).length <= 40
|
|
) {
|
|
const childIndex = childrenIndices[0]
|
|
const child = analyzeData.source(childIndex)
|
|
if (child?.path.endsWith('/')) {
|
|
return computeTreemapLayoutFromAnalyzeInternal(
|
|
analyzeData,
|
|
childIndex,
|
|
foldedPath + source.path,
|
|
rect,
|
|
metadata,
|
|
sizeMode
|
|
)
|
|
}
|
|
}
|
|
|
|
const totalSize =
|
|
sizeMode === SizeMode.Compressed
|
|
? metadata[sourceIndex].compressedSize
|
|
: metadata[sourceIndex].size
|
|
|
|
// If this is a file (no children), create a file node
|
|
if (!isDirectory || childrenIndices.length === 0) {
|
|
return {
|
|
name: source.path,
|
|
size: totalSize,
|
|
type: 'file',
|
|
rect,
|
|
sourceIndex,
|
|
specialModuleType: getSpecialModuleType(analyzeData, sourceIndex),
|
|
...analyzeData.getSourceFlags(sourceIndex),
|
|
}
|
|
}
|
|
|
|
const directoryName = foldedPath + source.path || 'All Route Modules'
|
|
|
|
// Directory with children
|
|
const titleBarHeight = Math.round(
|
|
Math.max(12, Math.min(24, rect.height * 0.1))
|
|
)
|
|
const isCollapsed = rect.height < 30
|
|
|
|
const contentRect: LayoutRect = {
|
|
x: Math.round(rect.x),
|
|
y: Math.round(rect.y + titleBarHeight),
|
|
width: Math.max(0, Math.round(rect.width - 2)),
|
|
height: Math.max(0, Math.round(rect.height - titleBarHeight - 2)),
|
|
}
|
|
|
|
if (isCollapsed) {
|
|
// Count all descendant files
|
|
function countDescendants(idx: SourceIndex): number {
|
|
const children = analyzeData.sourceChildren(idx)
|
|
if (children.length === 0) return 1
|
|
return children.reduce(
|
|
(sum, childIdx) => sum + countDescendants(childIdx),
|
|
0
|
|
)
|
|
}
|
|
|
|
return {
|
|
name: directoryName,
|
|
size: totalSize,
|
|
type: 'collapsed-directory',
|
|
rect,
|
|
titleBarHeight,
|
|
itemCount: countDescendants(sourceIndex),
|
|
children: [],
|
|
sourceIndex,
|
|
specialModuleType: null,
|
|
}
|
|
}
|
|
|
|
// Recursively build children with their sizes
|
|
const childrenData: { index: number; size: number }[] = []
|
|
|
|
for (const childIndex of childrenIndices) {
|
|
const childSource = analyzeData.source(childIndex)
|
|
if (!childSource) continue
|
|
|
|
// Use precomputed filter status
|
|
if (metadata[childIndex].filtered) {
|
|
continue
|
|
}
|
|
|
|
// Use precomputed size based on mode
|
|
const childSize =
|
|
sizeMode === SizeMode.Compressed
|
|
? metadata[childIndex].compressedSize
|
|
: metadata[childIndex].size
|
|
|
|
childrenData.push({
|
|
index: childIndex,
|
|
size: childSize || 1, // Fallback to 1 for visibility
|
|
})
|
|
}
|
|
|
|
if (childrenData.length === 0) {
|
|
return {
|
|
name: directoryName,
|
|
size: totalSize,
|
|
type: 'directory',
|
|
rect,
|
|
titleBarHeight,
|
|
children: [],
|
|
sourceIndex,
|
|
specialModuleType: null,
|
|
}
|
|
}
|
|
|
|
// Sort by size (descending)
|
|
childrenData.sort((a, b) => b.size - a.size)
|
|
|
|
// Compute layout
|
|
const sizes = childrenData.map((c) => c.size)
|
|
const childRects = layoutTreemap(sizes, contentRect)
|
|
|
|
const layoutChildren: LayoutNode[] = childrenData.map((child, i) =>
|
|
computeTreemapLayoutFromAnalyzeInternal(
|
|
analyzeData,
|
|
child.index,
|
|
'',
|
|
childRects[i],
|
|
metadata,
|
|
sizeMode
|
|
)
|
|
)
|
|
|
|
return {
|
|
name: directoryName,
|
|
size: totalSize,
|
|
type: 'directory',
|
|
rect,
|
|
titleBarHeight,
|
|
children: layoutChildren,
|
|
sourceIndex,
|
|
specialModuleType: null,
|
|
}
|
|
}
|
|
|
|
// Public function that precomputes metadata and calls internal function
|
|
export function computeTreemapLayoutFromAnalyze(
|
|
analyzeData: AnalyzeData,
|
|
sourceIndex: SourceIndex,
|
|
rect: LayoutRect,
|
|
filterSource?: (sourceIndex: SourceIndex) => boolean,
|
|
sizeMode: SizeMode = SizeMode.Compressed
|
|
): LayoutNode {
|
|
// Precompute metadata once for entire tree
|
|
const metadata = precomputeSourceMetadata(analyzeData, filterSource)
|
|
|
|
// Use internal function with precomputed metadata
|
|
return computeTreemapLayoutFromAnalyzeInternal(
|
|
analyzeData,
|
|
sourceIndex,
|
|
'',
|
|
rect,
|
|
metadata,
|
|
sizeMode
|
|
)
|
|
}
|