Skip to content

Commit bc5b7d5

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): improve initial file analysis for esbuild builder
When using the esbuild-based browser application builder, the set of initially loaded files for the application is now calculated by analyzing potential transitively loading JavaScript and/or CSS files. This ensures that the full set of bundled files is available for bundle size calculations as well as further analysis in areas such as link-based hint generation in the application's index HTML. This also fixes a bug where non-injected `scripts` where incorrectly shown as initial files.
1 parent 7155cbe commit bc5b7d5

File tree

2 files changed

+69
-29
lines changed

2 files changed

+69
-29
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/esbuild.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
formatMessages,
2121
} from 'esbuild';
2222
import { basename, extname, relative } from 'node:path';
23-
import { FileInfo } from '../../utils/index-file/augment-index-html';
2423

2524
export type BundleContextResult =
2625
| { errors: Message[]; warnings: Message[] }
@@ -29,7 +28,7 @@ export type BundleContextResult =
2928
warnings: Message[];
3029
metafile: Metafile;
3130
outputFiles: OutputFile[];
32-
initialFiles: FileInfo[];
31+
initialFiles: Map<string, InitialFileRecord>;
3332
};
3433

3534
/**
@@ -41,11 +40,23 @@ export function isEsBuildFailure(value: unknown): value is BuildFailure {
4140
return !!value && typeof value === 'object' && 'errors' in value && 'warnings' in value;
4241
}
4342

43+
export interface InitialFileRecord {
44+
entrypoint: boolean;
45+
name?: string;
46+
type: 'script' | 'style';
47+
external?: boolean;
48+
}
49+
4450
export class BundlerContext {
4551
#esbuildContext?: BuildContext<{ metafile: true; write: false }>;
4652
#esbuildOptions: BuildOptions & { metafile: true; write: false };
4753

48-
constructor(private workspaceRoot: string, private incremental: boolean, options: BuildOptions) {
54+
constructor(
55+
private workspaceRoot: string,
56+
private incremental: boolean,
57+
options: BuildOptions,
58+
private initialFilter?: (initial: Readonly<InitialFileRecord>) => boolean,
59+
) {
4960
this.#esbuildOptions = {
5061
...options,
5162
metafile: true,
@@ -64,7 +75,7 @@ export class BundlerContext {
6475
let errors: Message[] | undefined;
6576
const warnings: Message[] = [];
6677
const metafile: Metafile = { inputs: {}, outputs: {} };
67-
const initialFiles = [];
78+
const initialFiles = new Map<string, InitialFileRecord>();
6879
const outputFiles = [];
6980
for (const result of individualResults) {
7081
warnings.push(...result.warnings);
@@ -80,7 +91,7 @@ export class BundlerContext {
8091
metafile.outputs = { ...metafile.outputs, ...result.metafile.outputs };
8192
}
8293

83-
initialFiles.push(...result.initialFiles);
94+
result.initialFiles.forEach((value, key) => initialFiles.set(key, value));
8495
outputFiles.push(...result.outputFiles);
8596
}
8697

@@ -139,27 +150,59 @@ export class BundlerContext {
139150
}
140151

141152
// Find all initial files
142-
const initialFiles: FileInfo[] = [];
153+
const initialFiles = new Map<string, InitialFileRecord>();
143154
for (const outputFile of result.outputFiles) {
144155
// Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
145156
const relativeFilePath = relative(this.workspaceRoot, outputFile.path);
146-
const entryPoint = result.metafile?.outputs[relativeFilePath]?.entryPoint;
157+
const entryPoint = result.metafile.outputs[relativeFilePath]?.entryPoint;
147158

148159
outputFile.path = relativeFilePath;
149160

150161
if (entryPoint) {
151162
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
152-
const name = basename(outputFile.path).split('.', 1)[0];
163+
const name = basename(relativeFilePath).split('.', 1)[0];
164+
// Entry points are only styles or scripts
165+
const type = extname(relativeFilePath) === '.css' ? 'style' : 'script';
153166

154167
// Only entrypoints with an entry in the options are initial files.
155168
// Dynamic imports also have an entryPoint value in the meta file.
156169
if ((this.#esbuildOptions.entryPoints as Record<string, string>)?.[name]) {
157170
// An entryPoint value indicates an initial file
158-
initialFiles.push({
159-
file: outputFile.path,
171+
const record: InitialFileRecord = {
160172
name,
161-
extension: extname(outputFile.path),
162-
});
173+
type,
174+
entrypoint: true,
175+
};
176+
177+
if (!this.initialFilter || this.initialFilter(record)) {
178+
initialFiles.set(relativeFilePath, record);
179+
}
180+
}
181+
}
182+
}
183+
184+
// Analyze for transitive initial files
185+
const files = [...initialFiles.keys()];
186+
for (const file of files) {
187+
for (const initialImport of result.metafile.outputs[file].imports) {
188+
if (initialFiles.has(initialImport.path)) {
189+
continue;
190+
}
191+
192+
if (initialImport.kind === 'import-statement' || initialImport.kind === 'import-rule') {
193+
const record: InitialFileRecord = {
194+
type: initialImport.kind === 'import-rule' ? 'style' : 'script',
195+
entrypoint: false,
196+
external: initialImport.external,
197+
};
198+
199+
if (!this.initialFilter || this.initialFilter(record)) {
200+
initialFiles.set(initialImport.path, record);
201+
}
202+
203+
if (!initialImport.external) {
204+
files.push(initialImport.path);
205+
}
163206
}
164207
}
165208
}

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { brotliCompress } from 'node:zlib';
1616
import { copyAssets } from '../../utils/copy-assets';
1717
import { assertIsError } from '../../utils/error';
1818
import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets';
19-
import { FileInfo } from '../../utils/index-file/augment-index-html';
2019
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
2120
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
2221
import { Spinner } from '../../utils/spinner';
@@ -25,7 +24,7 @@ import { BundleStats, generateBuildStatsTable } from '../../webpack/utils/stats'
2524
import { SourceFileCache, createCompilerPlugin } from './angular/compiler-plugin';
2625
import { logBuilderStatusWarnings } from './builder-status-warnings';
2726
import { checkCommonJSModules } from './commonjs-checker';
28-
import { BundlerContext, logMessages } from './esbuild';
27+
import { BundlerContext, InitialFileRecord, logMessages } from './esbuild';
2928
import { createGlobalScriptsBundleOptions } from './global-scripts';
3029
import { createGlobalStylesBundleOptions } from './global-styles';
3130
import { extractLicenses } from './license-extractor';
@@ -141,7 +140,9 @@ async function execute(
141140
codeBundleCache?.loadResultCache,
142141
);
143142
if (bundleOptions) {
144-
bundlerContexts.push(new BundlerContext(workspaceRoot, !!options.watch, bundleOptions));
143+
bundlerContexts.push(
144+
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
145+
);
145146
}
146147
}
147148
}
@@ -151,7 +152,9 @@ async function execute(
151152
for (const initial of [true, false]) {
152153
const bundleOptions = createGlobalScriptsBundleOptions(options, initial);
153154
if (bundleOptions) {
154-
bundlerContexts.push(new BundlerContext(workspaceRoot, !!options.watch, bundleOptions));
155+
bundlerContexts.push(
156+
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
157+
);
155158
}
156159
}
157160
}
@@ -169,15 +172,6 @@ async function execute(
169172
return executionResult;
170173
}
171174

172-
// Filter global stylesheet initial files. Currently all initial CSS files are from the global styles option.
173-
if (options.globalStyles.length > 0) {
174-
bundlingResult.initialFiles = bundlingResult.initialFiles.filter(
175-
({ file, name }) =>
176-
!file.endsWith('.css') ||
177-
options.globalStyles.find((style) => style.name === name)?.initial,
178-
);
179-
}
180-
181175
const { metafile, initialFiles, outputFiles } = bundlingResult;
182176

183177
executionResult.outputFiles.push(...outputFiles);
@@ -216,7 +210,11 @@ async function execute(
216210
baseHref: options.baseHref,
217211
lang: undefined,
218212
outputPath: virtualOutputPath,
219-
files: initialFiles,
213+
files: [...initialFiles].map(([file, record]) => ({
214+
name: record.name ?? '',
215+
file,
216+
extension: path.extname(file),
217+
})),
220218
});
221219

222220
for (const error of errors) {
@@ -758,10 +756,9 @@ export default createBuilder(buildEsbuildBrowser);
758756
function logBuildStats(
759757
context: BuilderContext,
760758
metafile: Metafile,
761-
initialFiles: FileInfo[],
759+
initial: Map<string, InitialFileRecord>,
762760
estimatedTransferSizes?: Map<string, number>,
763761
) {
764-
const initial = new Map(initialFiles.map((info) => [info.file, info.name]));
765762
const stats: BundleStats[] = [];
766763
for (const [file, output] of Object.entries(metafile.outputs)) {
767764
// Only display JavaScript and CSS files
@@ -778,7 +775,7 @@ function logBuildStats(
778775
initial: initial.has(file),
779776
stats: [
780777
file,
781-
initial.get(file) ?? '-',
778+
initial.get(file)?.name ?? '-',
782779
output.bytes,
783780
estimatedTransferSizes?.get(file) ?? '-',
784781
],

0 commit comments

Comments
 (0)