Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 6 additions & 48 deletions packages/angular/build/src/builders/application/chunk-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import { BundleMergedContextResult } from '../../tools/esbuild/bundler-context';
import {
type BuildOutputFile,
BuildOutputFileType,
type InitialFileRecord,
createOutputFile,
findTransitiveInitialFiles,
} from '../../tools/esbuild/bundler-files';
import { useRolldownChunks } from '../../utils/environment-options';
import { assertIsError } from '../../utils/error';
Expand Down Expand Up @@ -166,21 +166,6 @@ function bundleOutputToEsbuildMetafile(
return newMetafile;
}

/**
* Creates an InitialFileRecord object with a specified depth.
* @param depth The depth of the file in the dependency graph.
* @returns An InitialFileRecord object.
*/
function createInitialFileRecord(depth: number): InitialFileRecord {
return {
type: 'script',
entrypoint: false,
external: false,
serverFile: false,
depth,
};
}

/**
* Creates an esbuild message object for a chunk optimization failure.
* @param message The error message detailing the cause of the failure.
Expand Down Expand Up @@ -369,14 +354,11 @@ export async function optimizeChunks(
);

// Add new optimized chunks
const importsPerFile: Record<string, string[]> = {};
for (const optimizedFile of optimizedOutput) {
if (optimizedFile.type !== 'chunk') {
continue;
}

importsPerFile[optimizedFile.fileName] = optimizedFile.imports;

original.outputFiles.push(
createOutputFile(optimizedFile.fileName, optimizedFile.code, BuildOutputFileType.Browser),
);
Expand All @@ -392,41 +374,17 @@ export async function optimizeChunks(
}

// Update initial files to reflect optimized chunks
const entriesToAnalyze: [string, InitialFileRecord][] = [];
for (const usedFile of usedChunks) {
// Leave the main file since its information did not change
if (usedFile === mainFile) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
entriesToAnalyze.push([mainFile, original.initialFiles.get(mainFile)!]);
continue;
if (usedFile !== mainFile) {
original.initialFiles.delete(usedFile);
}

// Remove all other used chunks
original.initialFiles.delete(usedFile);
}

// Analyze for transitive initial files
let currentEntry;
while ((currentEntry = entriesToAnalyze.pop())) {
const [entryPath, entryRecord] = currentEntry;

for (const importPath of importsPerFile[entryPath]) {
const existingRecord = original.initialFiles.get(importPath);
if (existingRecord) {
// Store the smallest value depth
if (existingRecord.depth > entryRecord.depth + 1) {
existingRecord.depth = entryRecord.depth + 1;
}

continue;
}

const record = createInitialFileRecord(entryRecord.depth + 1);

original.initialFiles.set(importPath, record);
entriesToAnalyze.push([importPath, record]);
}
}
const mainRecord = original.initialFiles.get(mainFile);
assert(mainRecord, `Main file record should always be present in initialFiles [${mainFile}].`);
findTransitiveInitialFiles(newMetafile, original.initialFiles, [[mainFile, mainRecord]]);

return original;
}
37 changes: 2 additions & 35 deletions packages/angular/build/src/tools/esbuild/bundler-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
BuildOutputFileType,
type InitialFileRecord,
convertOutputFile,
findTransitiveInitialFiles,
} from './bundler-files';
import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache';

Expand Down Expand Up @@ -363,41 +364,7 @@ export class BundlerContext {
}

// Analyze for transitive initial files
const entriesToAnalyze = [...initialFiles];
let currentEntry;
while ((currentEntry = entriesToAnalyze.pop())) {
const [entryPath, entryRecord] = currentEntry;

for (const initialImport of result.metafile.outputs[entryPath].imports) {
const existingRecord = initialFiles.get(initialImport.path);
if (existingRecord) {
// Store the smallest value depth
if (existingRecord.depth > entryRecord.depth + 1) {
existingRecord.depth = entryRecord.depth + 1;
}

continue;
}

if (initialImport.kind === 'import-statement' || initialImport.kind === 'import-rule') {
const record: InitialFileRecord = {
type: initialImport.kind === 'import-rule' ? 'style' : 'script',
entrypoint: false,
external: initialImport.external,
serverFile: isPlatformServer,
depth: entryRecord.depth + 1,
};

if (!this.initialFilter || this.initialFilter(record)) {
initialFiles.set(initialImport.path, record);
}

if (!initialImport.external) {
entriesToAnalyze.push([initialImport.path, record]);
}
}
}
}
findTransitiveInitialFiles(result.metafile, initialFiles, undefined, this.initialFilter);

// Collect all external package names
const externalImports = new Set<string>();
Expand Down
53 changes: 52 additions & 1 deletion packages/angular/build/src/tools/esbuild/bundler-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import type { OutputFile } from 'esbuild';
import type { Metafile, OutputFile } from 'esbuild';
import { calculateHash } from '../../utils/hash';

export interface InitialFileRecord {
Expand All @@ -18,6 +18,57 @@ export interface InitialFileRecord {
depth: number;
}

/**
* Analyzes a metafile to find and record all transitive initial files starting from the given
* initial entries using breadth-first traversal so each file is recorded at its minimum depth.
*
* @param metafile The esbuild metafile containing output import relationships.
* @param initialFiles The map of initial files to populate.
* @param entriesToAnalyze Optional starting entries to traverse; defaults to all current `initialFiles`.
* Entries must share the same starting depth (or be ordered by non-decreasing depth) to guarantee
* that each transitive file is visited at its minimum depth first.
* @param initialFilter Optional filter predicate to determine whether a discovered record should be added.
*/
export function findTransitiveInitialFiles(
metafile: Metafile,
initialFiles: Map<string, InitialFileRecord>,
entriesToAnalyze: Iterable<[string, InitialFileRecord]> = initialFiles,
initialFilter?: (initial: Readonly<InitialFileRecord>) => boolean,
): void {
const queue = [...entriesToAnalyze];
for (let i = 0; i < queue.length; i++) {
const [entryPath, entryRecord] = queue[i];
const entryOutput = metafile.outputs[entryPath];
if (!entryOutput) {
continue;
}

for (const initialImport of entryOutput.imports) {
if (initialFiles.has(initialImport.path)) {
continue;
}

if (initialImport.kind === 'import-statement' || initialImport.kind === 'import-rule') {
const record: InitialFileRecord = {
type: initialImport.kind === 'import-rule' ? 'style' : 'script',
entrypoint: false,
external: initialImport.external,
serverFile: entryRecord.serverFile,
depth: entryRecord.depth + 1,
};

if (!initialFilter || initialFilter(record)) {
initialFiles.set(initialImport.path, record);

if (!initialImport.external) {
queue.push([initialImport.path, record]);
}
}
}
}
}
}

export enum BuildOutputFileType {
Browser,
Media,
Expand Down
Loading
Loading