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
486 lines
12 KiB
TypeScript
486 lines
12 KiB
TypeScript
// 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
|
|
}
|
|
}
|