// 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 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 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() 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 } }