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 } export interface LayoutNode extends LayoutNodeInfo { size: number rect: LayoutRect type: 'file' | 'directory' | 'collapsed-directory' specialModuleType: SpecialModule | null titleBarHeight?: number children?: LayoutNode[] itemCount?: number traced?: boolean 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[], filterSource: ((sourceIndex: SourceIndex) => boolean) | undefined, 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, filterSource, 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, filterSource, 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, filterSource, sizeMode ) }