| | |
| |
|
| | |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | export class ModulesData { |
| | private modulesHeader: ModulesDataHeader |
| | private modulesBinaryData: DataView |
| | private pathToModuleIndex: Map<string, ModuleIndex[]> |
| |
|
| | constructor(modulesArrayBuffer: ArrayBuffer) { |
| | |
| | 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 |
| | ) |
| |
|
| | |
| | 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) ?? [] |
| | } |
| |
|
| | |
| | private readEdgesDataAtIndex( |
| | reference: EdgesDataReference, |
| | index: ModuleIndex |
| | ): ModuleIndex[] { |
| | const { offset, length } = reference |
| |
|
| | if (length === 0) { |
| | return [] |
| | } |
| |
|
| | |
| | const numOffsets = this.modulesBinaryData.getUint32(offset, false) |
| |
|
| | if (index < 0 || index >= numOffsets) { |
| | return [] |
| | } |
| |
|
| | |
| | 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 [] |
| | } |
| |
|
| | |
| | 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 |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | export class AnalyzeData { |
| | private analyzeHeader: AnalyzeDataHeader |
| | private analyzeBinaryData: DataView |
| | private pathToSourceIndex: Map<string, SourceIndex> |
| |
|
| | constructor(analyzeArrayBuffer: ArrayBuffer) { |
| | |
| | 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 |
| | ) |
| |
|
| | |
| | this.pathToSourceIndex = new Map() |
| | for (let i = 0; i < this.analyzeHeader.sources.length; i++) { |
| | const fullPath = this.getFullSourcePath(i) |
| | this.pathToSourceIndex.set(fullPath, i) |
| | } |
| | } |
| |
|
| | |
| |
|
| | 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 |
| | } |
| |
|
| | |
| |
|
| | |
| | private readEdgesDataAtIndex( |
| | reference: EdgesDataReference, |
| | index: SourceIndex |
| | ): SourceIndex[] { |
| | const { offset, length } = reference |
| |
|
| | if (length === 0) { |
| | return [] |
| | } |
| |
|
| | |
| | const numOffsets = this.analyzeBinaryData.getUint32(offset, false) |
| |
|
| | if (index < 0 || index >= numOffsets) { |
| | return [] |
| | } |
| |
|
| | |
| | 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 [] |
| | } |
| |
|
| | |
| | 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) |
| | } |
| |
|
| | |
| | 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' |
| | ) |
| | } |
| |
|
| | |
| | getRawAnalyzeHeader(): AnalyzeDataHeader { |
| | return this.analyzeHeader |
| | } |
| | } |
| |
|