first commit
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

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,485 @@
// Type definitions matching the Rust structures from analyze.rs
// Type aliases for better readability
export type ModuleIndex = number
export type SourceIndex = number
export interface AnalyzeModule {
ident: string
path: string
}
export interface AnalyzeSource {
parent_source_index: number | null
path: string
}
export interface AnalyzeChunkPart {
source_index: number
output_file_index: number
size: number
compressed_size: number
}
export interface AnalyzeOutputFile {
filename: string
}
export interface AnalyzeLayer {
name: string
}
interface EdgesDataReference {
offset: number
length: number
}
interface AnalyzeDataHeader {
sources: AnalyzeSource[]
chunk_parts: AnalyzeChunkPart[]
output_files: AnalyzeOutputFile[]
output_file_chunk_parts: EdgesDataReference
source_chunk_parts: EdgesDataReference
source_children: EdgesDataReference
source_roots: number[]
}
interface ModulesDataHeader {
modules: AnalyzeModule[]
module_dependents: EdgesDataReference
async_module_dependents: EdgesDataReference
module_dependencies: EdgesDataReference
async_module_dependencies: EdgesDataReference
}
/**
* Represents the global modules data that is shared across all routes
*/
export class ModulesData {
private modulesHeader: ModulesDataHeader
private modulesBinaryData: DataView
private pathToModuleIndex: Map<string, ModuleIndex[]>
constructor(modulesArrayBuffer: ArrayBuffer) {
// Parse modules.data
const modulesDataView = new DataView(modulesArrayBuffer)
const modulesJsonLength = modulesDataView.getUint32(0, false)
const modulesJsonBytes = new Uint8Array(
modulesArrayBuffer,
4,
modulesJsonLength
)
const modulesJsonString = new TextDecoder('utf-8').decode(modulesJsonBytes)
this.modulesHeader = JSON.parse(modulesJsonString) as ModulesDataHeader
const modulesBinaryOffset = 4 + modulesJsonLength
this.modulesBinaryData = new DataView(
modulesArrayBuffer,
modulesBinaryOffset
)
// Build pathToModuleIndex map
this.pathToModuleIndex = new Map()
for (let i = 0; i < this.modulesHeader.modules.length; i++) {
const module = this.modulesHeader.modules[i]
const existing = this.pathToModuleIndex.get(module.path)
if (existing) {
existing.push(i)
} else {
this.pathToModuleIndex.set(module.path, [i])
}
}
}
module(index: ModuleIndex): AnalyzeModule | undefined {
return this.modulesHeader.modules[index]
}
moduleCount(): number {
return this.modulesHeader.modules.length
}
getModuleIndiciesFromPath(path: string): ModuleIndex[] {
return this.pathToModuleIndex.get(path) ?? []
}
// Read edges data for a specific index only
private readEdgesDataAtIndex(
reference: EdgesDataReference,
index: ModuleIndex
): ModuleIndex[] {
const { offset, length } = reference
if (length === 0) {
return []
}
// Read the number of offset entries (first u32)
const numOffsets = this.modulesBinaryData.getUint32(offset, false)
if (index < 0 || index >= numOffsets) {
return []
}
// Read only the two offsets we need
const offsetsStart = offset + 4
const prevOffset =
index === 0
? 0
: this.modulesBinaryData.getUint32(
offsetsStart + (index - 1) * 4,
false
)
const currentOffset = this.modulesBinaryData.getUint32(
offsetsStart + index * 4,
false
)
const edgeCount = currentOffset - prevOffset
if (edgeCount === 0) {
return []
}
// Read only the data for this index
const dataStart = offset + 4 + numOffsets * 4
const edges: number[] = []
for (let j = 0; j < edgeCount; j++) {
const edgeValue = this.modulesBinaryData.getUint32(
dataStart + (prevOffset + j) * 4,
false
)
edges.push(edgeValue)
}
return edges
}
moduleDependents(index: ModuleIndex): ModuleIndex[] {
return this.readEdgesDataAtIndex(
this.modulesHeader.module_dependents,
index
)
}
asyncModuleDependents(index: ModuleIndex): ModuleIndex[] {
return this.readEdgesDataAtIndex(
this.modulesHeader.async_module_dependents,
index
)
}
moduleDependencies(index: ModuleIndex): ModuleIndex[] {
return this.readEdgesDataAtIndex(
this.modulesHeader.module_dependencies,
index
)
}
asyncModuleDependencies(index: ModuleIndex): ModuleIndex[] {
return this.readEdgesDataAtIndex(
this.modulesHeader.async_module_dependencies,
index
)
}
getRawModulesHeader(): ModulesDataHeader {
return this.modulesHeader
}
}
/**
* Represents route-specific analyze data
*/
export class AnalyzeData {
private analyzeHeader: AnalyzeDataHeader
private analyzeBinaryData: DataView
private pathToSourceIndex: Map<string, SourceIndex>
constructor(analyzeArrayBuffer: ArrayBuffer) {
// Parse analyze.data
const analyzeDataView = new DataView(analyzeArrayBuffer)
const analyzeJsonLength = analyzeDataView.getUint32(0, false)
const analyzeJsonBytes = new Uint8Array(
analyzeArrayBuffer,
4,
analyzeJsonLength
)
const analyzeJsonString = new TextDecoder('utf-8').decode(analyzeJsonBytes)
this.analyzeHeader = JSON.parse(analyzeJsonString) as AnalyzeDataHeader
const analyzeBinaryOffset = 4 + analyzeJsonLength
this.analyzeBinaryData = new DataView(
analyzeArrayBuffer,
analyzeBinaryOffset
)
// Build pathToSourceIndex map
this.pathToSourceIndex = new Map()
for (let i = 0; i < this.analyzeHeader.sources.length; i++) {
const fullPath = this.getFullSourcePath(i)
this.pathToSourceIndex.set(fullPath, i)
}
}
// Accessor methods for header data
source(index: SourceIndex): AnalyzeSource | undefined {
return this.analyzeHeader.sources[index]
}
sourceCount(): number {
return this.analyzeHeader.sources.length
}
getSourceIndexFromPath(path: string): SourceIndex | undefined {
return this.pathToSourceIndex.get(path)
}
chunkPart(index: number): AnalyzeChunkPart | undefined {
return this.analyzeHeader.chunk_parts[index]
}
chunkPartCount(): number {
return this.analyzeHeader.chunk_parts.length
}
outputFile(index: number): AnalyzeOutputFile | undefined {
return this.analyzeHeader.output_files[index]
}
outputFileCount(): number {
return this.analyzeHeader.output_files.length
}
sourceRoots(): SourceIndex[] {
return this.analyzeHeader.source_roots
}
// Methods to read edges data from the binary section
// Read edges data for a specific index only
private readEdgesDataAtIndex(
reference: EdgesDataReference,
index: SourceIndex
): SourceIndex[] {
const { offset, length } = reference
if (length === 0) {
return []
}
// Read the number of offset entries (first u32)
const numOffsets = this.analyzeBinaryData.getUint32(offset, false)
if (index < 0 || index >= numOffsets) {
return []
}
// Read only the two offsets we need
const offsetsStart = offset + 4
const prevOffset =
index === 0
? 0
: this.analyzeBinaryData.getUint32(
offsetsStart + (index - 1) * 4,
false
)
const currentOffset = this.analyzeBinaryData.getUint32(
offsetsStart + index * 4,
false
)
const edgeCount = currentOffset - prevOffset
if (edgeCount === 0) {
return []
}
// Read only the data for this index
const dataStart = offset + 4 + numOffsets * 4
const edges: number[] = []
for (let j = 0; j < edgeCount; j++) {
const edgeValue = this.analyzeBinaryData.getUint32(
dataStart + (prevOffset + j) * 4,
false
)
edges.push(edgeValue)
}
return edges
}
outputFileChunkParts(index: number): number[] {
return this.readEdgesDataAtIndex(
this.analyzeHeader.output_file_chunk_parts,
index
)
}
sourceChunkParts(index: SourceIndex): number[] {
return this.readEdgesDataAtIndex(
this.analyzeHeader.source_chunk_parts,
index
)
}
sourceChildren(index: SourceIndex): SourceIndex[] {
return this.readEdgesDataAtIndex(this.analyzeHeader.source_children, index)
}
// Utility method to get the full path of a source by walking up the parent chain
getFullSourcePath(index: SourceIndex): string {
const source = this.source(index)
if (!source) return ''
if (source.parent_source_index === null) {
return source.path
}
const parentPath = this.getFullSourcePath(source.parent_source_index)
return parentPath + source.path
}
getOwnSizes(index: SourceIndex): {
size: number
compressedSize: number
} {
const chunkParts = this.sourceChunkParts(index)
let size = 0
let compressedSize = 0
for (const chunkPartIndex of chunkParts) {
const chunkPart = this.chunkPart(chunkPartIndex)
if (chunkPart) {
size += chunkPart.size
compressedSize += chunkPart.compressed_size
}
}
return { size, compressedSize }
}
getRecursiveModuleCount(
index: SourceIndex,
filterSource: (sourceIndex: SourceIndex) => boolean
): number {
const selfVisible = filterSource(index)
const selfCount =
selfVisible && this.sourceChunkParts(index).length > 0 ? 1 : 0
const children = this.sourceChildren(index)
if (children.length === 0) {
return selfCount
}
let totalCount = selfCount
for (const childIndex of children) {
totalCount += this.getRecursiveModuleCount(childIndex, filterSource)
}
return totalCount
}
sourceChunks(index: SourceIndex): string[] {
const chunkParts = this.sourceChunkParts(index)
const uniqueChunks = new Set<string>()
for (const chunkPartIndex of chunkParts) {
const chunkPart = this.chunkPart(chunkPartIndex)
if (chunkPart) {
const outputFile = this.outputFile(chunkPart.output_file_index)
if (outputFile) {
uniqueChunks.add(outputFile.filename)
}
}
}
return Array.from(uniqueChunks).sort()
}
getRecursiveSizes(
index: SourceIndex,
filterSource: (sourceIndex: SourceIndex) => boolean
): { size: number; compressedSize: number } {
let size = 0
let compressedSize = 0
if (filterSource(index)) {
const { size: ownUncompressedSize, compressedSize: ownCompressedSize } =
this.getOwnSizes(index)
size += ownUncompressedSize
compressedSize += ownCompressedSize
}
for (const childIndex of this.sourceChildren(index)) {
const {
size: childUncompressedSize,
compressedSize: childCompressedSize,
} = this.getRecursiveSizes(childIndex, filterSource)
size += childUncompressedSize
compressedSize += childCompressedSize
}
return {
size,
compressedSize,
}
}
getSourceFlags(index: SourceIndex): {
client: boolean
server: boolean
traced: boolean
js: boolean
css: boolean
json: boolean
asset: boolean
} {
let client = false
let server = false
let traced = false
let js = false
let css = false
let json = false
let asset = false
const chunkParts = this.sourceChunkParts(index)
for (const chunkPartIndex of chunkParts) {
const chunkPart = this.chunkPart(chunkPartIndex)
if (!chunkPart) continue
const outputFile = this.outputFile(chunkPart.output_file_index)
if (!outputFile) continue
if (outputFile.filename.startsWith('[client-fs]/')) {
client = true
} else if (outputFile.filename.startsWith('[project]/')) {
traced = true
} else {
server = true
}
if (outputFile.filename.endsWith('.js')) {
js = true
} else if (outputFile.filename.endsWith('.css')) {
css = true
} else if (outputFile.filename.endsWith('.json')) {
json = true
} else {
asset = true
}
}
return { client, server, traced, js, css, json, asset }
}
isPolyfillModule(index: SourceIndex): boolean {
const fullSourcePath = this.getFullSourcePath(index)
return fullSourcePath.endsWith(
'node_modules/next/dist/build/polyfills/polyfill-module.js'
)
}
isPolyfillNoModule(index: SourceIndex): boolean {
const fullSourcePath = this.getFullSourcePath(index)
return fullSourcePath.endsWith(
'node_modules/next/dist/build/polyfills/polyfill-nomodule.js'
)
}
// Get the raw header for debugging
getRawAnalyzeHeader(): AnalyzeDataHeader {
return this.analyzeHeader
}
}

View File

@@ -0,0 +1,10 @@
export class NetworkError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
super(message)
this.name = 'NetworkError'
// Preserve error cause when supported
if (options && 'cause' in options) {
;(this as any).cause = options.cause
}
}
}

View File

@@ -0,0 +1,121 @@
import type { LayoutRect } from './treemap-layout'
export function layoutTreemap(sizes: number[], rect: LayoutRect): LayoutRect[] {
if (sizes.length === 0) return []
if (sizes.length === 1) return [rect]
const totalSize = sizes.reduce((a, b) => a + b, 0)
const normalizedSizes = sizes.map(
(s) => (s / totalSize) * rect.width * rect.height
)
const result: LayoutRect[] = []
let remaining = [...normalizedSizes]
let currentRect = { ...rect }
let totalRemaining = remaining.reduce((a, b) => a + b, 0)
while (remaining.length > 1) {
// Decide orientation: vertical if wider, horizontal if taller
const vertical = currentRect.width >= currentRect.height
// Pick items until sum > total / count
const picked: number[] = []
let sum = 0
for (const size of remaining) {
picked.push(size)
sum += size
if (vertical) {
const width = (currentRect.width * sum) / totalRemaining
if (width > (currentRect.height / picked.length) * 0.9) {
break
}
} else {
const height = (currentRect.height * sum) / totalRemaining
if (height > (currentRect.width / picked.length) * 0.9) {
break
}
}
}
// Ensure at least one item is picked
if (picked.length === 0) {
picked.push(remaining[0])
sum = remaining[0]
}
// Calculate the space used by this row/column
const spaceRatio = sum / totalRemaining
totalRemaining -= sum
if (vertical) {
// Items stacked vertically, filling full width
const rowWidth = Math.round(spaceRatio * currentRect.width)
let offsetY = 0
for (let i = 0; i < picked.length; i++) {
const size = picked[i]
const itemHeight =
i === picked.length - 1
? Math.round(currentRect.height - offsetY)
: Math.round((size / sum) * currentRect.height)
result.push({
x: Math.round(currentRect.x),
y: Math.round(currentRect.y + offsetY),
width: rowWidth,
height: itemHeight,
})
offsetY += itemHeight
}
// Update remaining rectangle
currentRect = {
x: Math.round(currentRect.x + rowWidth),
y: Math.round(currentRect.y),
width: Math.round(currentRect.width - rowWidth),
height: Math.round(currentRect.height),
}
} else {
// Items placed horizontally, filling full height
const rowHeight = Math.round(spaceRatio * currentRect.height)
let offsetX = 0
for (let i = 0; i < picked.length; i++) {
const size = picked[i]
const itemWidth =
i === picked.length - 1
? Math.round(currentRect.width - offsetX)
: Math.round((size / sum) * currentRect.width)
result.push({
x: Math.round(currentRect.x + offsetX),
y: Math.round(currentRect.y),
width: itemWidth,
height: rowHeight,
})
offsetX += itemWidth
}
// Update remaining rectangle
currentRect = {
x: Math.round(currentRect.x),
y: Math.round(currentRect.y + rowHeight),
width: Math.round(currentRect.width),
height: Math.round(currentRect.height - rowHeight),
}
}
// Remove picked items from remaining
remaining = remaining.slice(picked.length)
}
// Last item fills remaining space
if (remaining.length === 1) {
result.push(currentRect)
}
return result
}

View File

@@ -0,0 +1,146 @@
import type { AnalyzeData, ModuleIndex, ModulesData } from './analyze-data'
/**
* Compute active entries from the current route's sources.
*
* It's a heuristic approach that looks for known entry module idents
* and traces their dependencies to find active modules.
*
* I don't like it as it has too much assumptions about next.js internals.
* It would be better if the source map contains idents instead of only paths.
*/
export function computeActiveEntries(
modulesData: ModulesData,
analyzeData: AnalyzeData
): ModuleIndex[] {
const potentialEntryDependents = [
'next/dist/esm/build/templates/pages.js',
'next/dist/esm/build/templates/pages-api.js',
'next/dist/esm/build/templates/pages-edge-api.js',
'next/dist/esm/build/templates/edge-ssr.js',
'next/dist/esm/build/templates/app-route.js',
'next/dist/esm/build/templates/edge-app-route.js',
'next/dist/esm/build/templates/app-page.js',
'next/dist/esm/build/templates/edge-ssr-app.js',
'next/dist/esm/build/templates/middleware.js',
'[next]/entry/page-loader.ts',
]
const potentialEntries = [
'next/dist/client/app-next-turbopack.js',
'next/dist/client/next-turbopack.js',
]
const activeEntries = new Set<ModuleIndex>()
for (
let moduleIndex = 0;
moduleIndex < modulesData.moduleCount();
moduleIndex++
) {
const ident = modulesData.module(moduleIndex)?.ident
if (ident == null) {
continue
}
if (
potentialEntryDependents.some((entryIdent) => ident.includes(entryIdent))
) {
const dependencies = modulesData.moduleDependencies(moduleIndex)
for (const dep of dependencies) {
const path = modulesData.module(dep)!.path
if (path.includes('next/dist/')) {
continue
}
const source = analyzeData.getSourceIndexFromPath(path)
if (source !== undefined) {
activeEntries.add(dep)
}
}
}
if (potentialEntries.some((entryIdent) => ident.includes(entryIdent))) {
activeEntries.add(moduleIndex)
}
}
return Array.from(activeEntries)
}
/**
* Compute module depth from active entries using BFS
* Returns a Map from ModuleIndex to depth
* Unreachable modules will not have an entry in the map
*/
export function computeModuleDepthMap(
modulesData: ModulesData,
activeEntries: ModuleIndex[]
): Map<ModuleIndex, number> {
const depthMap = new Map<ModuleIndex, number>()
const delayedModules = new Array<{ depth: number; queue: ModuleIndex[] }>()
// Initialize queue with active entries
for (const moduleIndex of activeEntries) {
depthMap.set(moduleIndex, 0)
}
// BFS to compute depth
// We need to insert new entries into the depth map in monotonic increasing order of depth
// so that we always process shallower modules before deeper ones
// This is important to avoid visiting modules multiple times and needing to decrease their depth
let i = 0
for (const [moduleIndex, depth] of depthMap) {
const newDepth = depth + 1
// Process regular dependencies
const dependencies = modulesData.moduleDependencies(moduleIndex)
for (const depIndex of dependencies) {
if (!depthMap.has(depIndex)) {
depthMap.set(depIndex, newDepth)
}
}
// Process async dependencies with higher depth penalty
const asyncDependencies = modulesData.asyncModuleDependencies(moduleIndex)
for (const depIndex of asyncDependencies) {
if (!depthMap.has(depIndex)) {
const newDepth = depth + 1000
// We can't directly insert async dependencies into the depth map
// because they might be processed before their parent module
// leading to incorrect depth assignment.
// Instead, we queue them to be processed later.
let delayedQueue = delayedModules.find((dq) => dq.depth === newDepth)
if (!delayedQueue) {
delayedQueue = { depth: newDepth, queue: [] }
delayedModules.push(delayedQueue)
// Keep delayed queues sorted by depth descending
delayedModules.sort((a, b) => b.depth - a.depth)
}
delayedQueue.queue.push(depIndex)
}
}
i++
// Check if we need to process the next delayed queue to insert its items into the depth map
// This happens when we reach the end of the current queue
// or the next delayed queue has the same depth so its items need to be processed now
while (
delayedModules.length > 0 &&
(i === depthMap.size ||
newDepth === delayedModules[delayedModules.length - 1].depth)
) {
const { depth, queue } = delayedModules.pop()!
for (const depIndex of queue) {
if (!depthMap.has(depIndex)) {
depthMap.set(depIndex, depth)
}
}
}
}
if (delayedModules.length > 0) {
throw new Error(
'Internal error: delayed modules remain after BFS processing'
)
}
return depthMap
}

View File

@@ -0,0 +1,294 @@
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
)
}

View File

@@ -0,0 +1,40 @@
export interface FileNode {
id: number
name: string
type: 'file'
size: number
outputType: 'code' | 'css' | 'asset'
server: boolean
client: boolean
dependencies: number[]
dependents: number[]
}
export interface DirectoryNode {
id: number
name: string
type: 'directory'
children: TreeNode[]
size?: number
}
export type TreeNode = FileNode | DirectoryNode
interface Route {
page: string
regex: string
routeKeys?: Record<string, string>
namedRegex?: string
}
export interface RouteManifest {
version: number
pages: Record<string, string>
staticRoutes: Array<Route>
dynamicRoutes: Array<Route>
}
export enum SpecialModule {
POLYFILL_MODULE,
POLYFILL_NOMODULE,
}

View File

@@ -0,0 +1,69 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { SpecialModule } from './types'
import { NetworkError } from './errors'
import { AnalyzeData, SourceIndex } from './analyze-data'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export async function fetchStrict(url: string): Promise<Response> {
let res: Response
try {
res = await fetch(url)
} catch (err) {
throw new NetworkError(`Failed to fetch ${url}`, { cause: err })
}
if (!res.ok) {
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`)
}
return res
}
export async function jsonFetcher<T>(url: string): Promise<T> {
const res = await fetchStrict(url)
return res.json() as Promise<T>
}
export function getSpecialModuleType(
analyzeData: AnalyzeData | undefined,
sourceIndex: SourceIndex | null
): SpecialModule | null {
if (!analyzeData || sourceIndex == null) return null
const path = analyzeData.source(sourceIndex)?.path || ''
if (path.endsWith('polyfill-module.js')) {
return SpecialModule.POLYFILL_MODULE
} else if (path.endsWith('polyfill-nomodule.js')) {
return SpecialModule.POLYFILL_NOMODULE
}
return null
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
}
let IDENT_ATTRIBUTES_REGEXP =
/^(.+?)(?: \{(.*)\})?(?: \[(.*)\])?(?: \((.*?)\))?(?: <(.*?)>)?$/
export function splitIdent(ident: string): {
fullPath: string
templateArgs: string
layer: string
moduleType: string
treeShaking: string
} {
let [match, fullPath, templateArgs, layer, moduleType, treeShaking] =
IDENT_ATTRIBUTES_REGEXP.exec(ident) || ['']
ident = ident.substring(0, ident.length - match.length)
return { fullPath, templateArgs, layer, moduleType, treeShaking }
}