Files
next.js/apps/bundle-analyzer/lib/treemap-layout.ts
Arian Tron 61f56f997c
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
first commit
2026-03-10 19:37:31 +03:30

295 lines
7.5 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
}
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
)
}