Skip to content

Commit 01ab16c

Browse files
clydindgp1130
authored andcommitted
perf(@angular-devkit/build-angular): fully avoid rebuild of component stylesheets when unchanged
With the full set of dependencies and watch files tracked within the bundler context object for component stylesheets, the entire bundler output can be cached in memory and reused when none of the relevant files have changed since the last rebuild. This is particularly useful for scenarios when a large tree of components are considered affected and must be recompiled by the AOT compiler. (cherry picked from commit 9020890)
1 parent f66f9cf commit 01ab16c

File tree

1 file changed

+54
-1
lines changed

1 file changed

+54
-1
lines changed

packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
BuildContext,
1111
BuildFailure,
1212
BuildOptions,
13+
BuildResult,
1314
Message,
1415
Metafile,
1516
OutputFile,
@@ -66,7 +67,9 @@ function isEsBuildFailure(value: unknown): value is BuildFailure {
6667
export class BundlerContext {
6768
#esbuildContext?: BuildContext<{ metafile: true; write: false }>;
6869
#esbuildOptions?: BuildOptions & { metafile: true; write: false };
70+
#esbuildResult?: BundleContextResult;
6971
#optionsFactory: BundlerOptionsFactory<BuildOptions & { metafile: true; write: false }>;
72+
#shouldCacheResult: boolean;
7073

7174
#loadCache?: MemoryLoadResultCache;
7275
readonly watchFiles = new Set<string>();
@@ -77,6 +80,8 @@ export class BundlerContext {
7780
options: BuildOptions | BundlerOptionsFactory,
7881
private initialFilter?: (initial: Readonly<InitialFileRecord>) => boolean,
7982
) {
83+
// To cache the results an option factory is needed to capture the full set of dependencies
84+
this.#shouldCacheResult = incremental && typeof options === 'function';
8085
this.#optionsFactory = (...args) => {
8186
const baseOptions = typeof options === 'function' ? options(...args) : options;
8287

@@ -142,6 +147,20 @@ export class BundlerContext {
142147
* warnings and errors for the attempted build.
143148
*/
144149
async bundle(): Promise<BundleContextResult> {
150+
// Return existing result if present
151+
if (this.#esbuildResult) {
152+
return this.#esbuildResult;
153+
}
154+
155+
const result = await this.#performBundle();
156+
if (this.#shouldCacheResult) {
157+
this.#esbuildResult = result;
158+
}
159+
160+
return result;
161+
}
162+
163+
async #performBundle() {
145164
// Create esbuild options if not present
146165
if (this.#esbuildOptions === undefined) {
147166
if (this.incremental) {
@@ -150,6 +169,10 @@ export class BundlerContext {
150169
this.#esbuildOptions = this.#optionsFactory(this.#loadCache);
151170
}
152171

172+
if (this.incremental) {
173+
this.watchFiles.clear();
174+
}
175+
153176
let result;
154177
try {
155178
if (this.#esbuildContext) {
@@ -167,6 +190,8 @@ export class BundlerContext {
167190
} catch (failure) {
168191
// Build failures will throw an exception which contains errors/warnings
169192
if (isEsBuildFailure(failure)) {
193+
this.#addErrorsToWatch(failure);
194+
170195
return failure;
171196
} else {
172197
throw failure;
@@ -177,7 +202,6 @@ export class BundlerContext {
177202
// While this should technically not be linked to incremental mode, incremental is only
178203
// currently enabled with watch mode where watch files are needed.
179204
if (this.incremental) {
180-
this.watchFiles.clear();
181205
// Add input files except virtual angular files which do not exist on disk
182206
Object.keys(result.metafile.inputs)
183207
.filter((input) => !input.startsWith('angular:'))
@@ -194,6 +218,8 @@ export class BundlerContext {
194218

195219
// Return if the build encountered any errors
196220
if (result.errors.length) {
221+
this.#addErrorsToWatch(result);
222+
197223
return {
198224
errors: result.errors,
199225
warnings: result.warnings,
@@ -281,6 +307,28 @@ export class BundlerContext {
281307
};
282308
}
283309

310+
#addErrorsToWatch(result: BuildFailure | BuildResult): void {
311+
for (const error of result.errors) {
312+
let file = error.location?.file;
313+
if (file) {
314+
this.watchFiles.add(join(this.workspaceRoot, file));
315+
}
316+
for (const note of error.notes) {
317+
file = note.location?.file;
318+
if (file) {
319+
this.watchFiles.add(join(this.workspaceRoot, file));
320+
}
321+
}
322+
}
323+
}
324+
325+
/**
326+
* Invalidate a stored bundler result based on the previous watch files
327+
* and a list of changed files.
328+
* The context must be created with incremental mode enabled for results
329+
* to be stored.
330+
* @returns True, if the result was invalidated; False, otherwise.
331+
*/
284332
invalidate(files: Iterable<string>): boolean {
285333
if (!this.incremental) {
286334
return false;
@@ -296,6 +344,10 @@ export class BundlerContext {
296344
invalid ||= this.watchFiles.has(file);
297345
}
298346

347+
if (invalid) {
348+
this.#esbuildResult = undefined;
349+
}
350+
299351
return invalid;
300352
}
301353

@@ -307,6 +359,7 @@ export class BundlerContext {
307359
async dispose(): Promise<void> {
308360
try {
309361
this.#esbuildOptions = undefined;
362+
this.#esbuildResult = undefined;
310363
this.#loadCache = undefined;
311364
await this.#esbuildContext?.dispose();
312365
} finally {

0 commit comments

Comments
 (0)