Skip to content

Commit 55a2618

Browse files
committed
fix(@angular/build): support incremental asset updates and output cleanup in library watch mode
Ensure the library builder properly handles created, modified, and deleted assets during watch mode rebuilds, and removes stale bundle, sourcemap, and manifest output files from disk when entry points or exports are removed.
1 parent c6b5165 commit 55a2618

9 files changed

Lines changed: 713 additions & 111 deletions

File tree

‎packages/angular/build/src/builders/library/builder-state.ts‎

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
import { toPosixPath } from '../../utils/path';
1010
import { ExecutionResult } from './execution-result';
11+
import {
12+
type AssetMatcher,
13+
type CollectedAssets,
14+
collectAssetsToEmit,
15+
createAssetMatchers,
16+
} from './pipeline/assets';
17+
import type { OutputFile } from './pipeline/utils';
1118
import type {
1219
BundleResult,
1320
LibraryCompilationOutput,
@@ -17,17 +24,29 @@ import type {
1724

1825
/**
1926
* Encapsulates incremental compilation state, transitive resource reference tracking,
20-
* watched compilation file tracking, and execution results across watch-mode rebuilds.
27+
* asset matchers, and emitted output file tracking across watch-mode rebuilds.
2128
*/
2229
export class BuilderState {
2330
readonly #referencingFiles = new Map<string, Set<string>>();
2431
readonly #referencedByFile = new Map<string, Set<string>>();
2532
readonly #watchedCompilationFiles = new Set<string>();
2633
readonly #pendingCompilationFiles = new Set<string>();
34+
readonly #pendingAssetFiles = new Set<string>();
35+
readonly #assetMatchers?: readonly AssetMatcher[];
36+
37+
#emittedBundleFiles = new Set<string>();
38+
#emittedManifestFiles = new Set<string>();
39+
readonly #emittedAssets = new Map<string, Set<string>>();
40+
#hasEmittedAssets = false;
2741

2842
#executionResult = new ExecutionResult();
43+
#nextBundleFiles?: Set<string>;
44+
#nextManifestFiles?: Set<string>;
45+
readonly #filesToDelete = new Set<string>();
46+
2947
#singleProgramCache?: SingleProgramCache;
3048
#compilationMode?: 'partial' | 'full';
49+
#hasEmittedTslib = false;
3150

3251
/** Map of entry point names to their previous bundle results. */
3352
readonly previousBundleResults = new Map<string, BundleResult>();
@@ -44,18 +63,12 @@ export class BuilderState {
4463
/** Whether package.json and secondary entry point manifests have been emitted. */
4564
hasEmittedManifests = false;
4665

47-
/** Whether the last emitted root package.json included a tslib dependency. */
48-
hasEmittedTslib = false;
49-
50-
/** Whether static assets have been emitted in a prior iteration. */
51-
hasEmittedAssets = false;
52-
5366
/** Whether entry points in package.json changed since the last compilation. */
5467
hasEntryPointsChanges = false;
5568

5669
constructor(
5770
options?: Partial<
58-
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints'>
71+
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints' | 'assets'>
5972
>,
6073
) {
6174
if (options?.tsConfigPath) {
@@ -69,6 +82,8 @@ export class BuilderState {
6982
this.#watchedCompilationFiles.add(entryPoint.entryFilePath);
7083
}
7184
}
85+
86+
this.#assetMatchers = options?.assets ? createAssetMatchers(options.assets) : undefined;
7287
}
7388

7489
/** Cached unified TypeScript and Angular compilation state. */
@@ -81,6 +96,11 @@ export class BuilderState {
8196
return this.#compilationMode;
8297
}
8398

99+
/** Whether the last emitted root package.json included a tslib dependency. */
100+
get hasEmittedTslib(): boolean {
101+
return this.#hasEmittedTslib;
102+
}
103+
84104
/** Whether any active entry point bundle imports `tslib`. */
85105
get hasTslibImport(): boolean {
86106
for (const result of this.previousBundleResults.values()) {
@@ -109,12 +129,15 @@ export class BuilderState {
109129
}
110130

111131
/**
112-
* Resets the execution result for a new build iteration.
132+
* Resets the execution result and staged output state for a new build iteration.
113133
*
114134
* @returns The fresh `ExecutionResult` instance for the current build iteration.
115135
*/
116136
resetExecutionResult(): ExecutionResult {
117137
this.#executionResult = new ExecutionResult();
138+
this.#nextBundleFiles = undefined;
139+
this.#nextManifestFiles = undefined;
140+
this.#filesToDelete.clear();
118141

119142
return this.#executionResult;
120143
}
@@ -209,6 +232,9 @@ export class BuilderState {
209232
if (file !== packageJsonPath) {
210233
this.#pendingCompilationFiles.add(file);
211234
}
235+
if (this.#hasEmittedAssets) {
236+
this.#pendingAssetFiles.add(file);
237+
}
212238
}
213239
}
214240

@@ -281,11 +307,13 @@ export class BuilderState {
281307

282308
/**
283309
* Records active bundle results across entry points after compilation and bundling succeed,
284-
* clearing pending compilation/bundle flags.
310+
* clearing pending compilation/bundle flags and queuing any previously emitted bundle files
311+
* that are no longer present for deletion.
285312
*
286313
* @param bundleResults Newly bundled results keyed by entry point name.
287314
*/
288315
recordBundleOutput(bundleResults: ReadonlyMap<string, BundleResult>): void {
316+
const hadEntryPointsChanges = this.hasEntryPointsChanges;
289317
this.hasCompilationError = false;
290318
this.hasEntryPointsChanges = false;
291319
this.pendingChangedEsmFiles.clear();
@@ -294,6 +322,123 @@ export class BuilderState {
294322
for (const [name, bundleResult] of bundleResults) {
295323
this.previousBundleResults.set(name, bundleResult);
296324
}
325+
326+
if (bundleResults.size === 0 && !hadEntryPointsChanges && this.#emittedBundleFiles.size > 0) {
327+
return;
328+
}
329+
330+
const nextBundleFiles = new Set<string>();
331+
for (const result of this.previousBundleResults.values()) {
332+
for (const file of result.esmOutputFiles) {
333+
nextBundleFiles.add(file);
334+
}
335+
for (const file of result.dtsOutputFiles) {
336+
nextBundleFiles.add(file);
337+
}
338+
}
339+
340+
for (const prevFile of this.#emittedBundleFiles) {
341+
if (!nextBundleFiles.has(prevFile)) {
342+
this.#filesToDelete.add(prevFile);
343+
}
344+
}
345+
346+
this.#nextBundleFiles = nextBundleFiles;
347+
}
348+
349+
/**
350+
* Computes the active manifest output file set and queues any previously emitted
351+
* manifest files that are no longer present for deletion.
352+
*
353+
* @param manifestFiles Manifest output files generated in this iteration.
354+
*/
355+
recordManifestOutput(manifestFiles: readonly OutputFile[]): void {
356+
const nextManifestFiles = new Set(manifestFiles.map((file) => file.path));
357+
for (const prevFile of this.#emittedManifestFiles) {
358+
if (!nextManifestFiles.has(prevFile)) {
359+
this.#filesToDelete.add(prevFile);
360+
}
361+
}
362+
363+
this.#nextManifestFiles = nextManifestFiles;
364+
}
365+
366+
/**
367+
* Resolves and collects configured library assets to be emitted to or deleted from disk.
368+
*
369+
* @param options Normalized library options containing `assets` and `workspaceRoot`.
370+
* @param modifiedFiles Optional set of modified POSIX file paths in watch mode.
371+
* @returns The collected asset files to emit and delete.
372+
*/
373+
collectAssets(
374+
options: Pick<NormalizedLibraryOptions, 'assets' | 'workspaceRoot'>,
375+
modifiedFiles?: ReadonlySet<string>,
376+
): Promise<CollectedAssets> {
377+
let effectiveModifiedFiles: ReadonlySet<string> | undefined;
378+
if (this.#hasEmittedAssets && modifiedFiles) {
379+
for (const file of modifiedFiles) {
380+
this.#pendingAssetFiles.add(file);
381+
}
382+
effectiveModifiedFiles = this.#pendingAssetFiles;
383+
}
384+
385+
return collectAssetsToEmit(
386+
options.assets,
387+
options.workspaceRoot,
388+
this.#emittedAssets,
389+
effectiveModifiedFiles,
390+
this.#assetMatchers,
391+
);
392+
}
393+
394+
/**
395+
* Records emitted bundle, manifest, and asset output files after disk emission,
396+
* queuing any stale or deleted output paths for removal from disk.
397+
*
398+
* @param collectedAssets Collected asset files emitted and deleted in this iteration.
399+
* @returns Read-only set of relative POSIX output paths queued for deletion from disk.
400+
*/
401+
recordEmittedOutputs(collectedAssets: CollectedAssets): ReadonlySet<string> {
402+
const { filesToDelete: toDelete, filesToEmit: toEmit } = collectedAssets;
403+
404+
for (const deletedAsset of toDelete) {
405+
this.#emittedAssets.delete(deletedAsset.source);
406+
}
407+
408+
for (const emittedAsset of toEmit) {
409+
let outputs = this.#emittedAssets.get(emittedAsset.source);
410+
if (!outputs) {
411+
outputs = new Set();
412+
this.#emittedAssets.set(emittedAsset.source, outputs);
413+
}
414+
outputs.add(emittedAsset.path);
415+
}
416+
417+
if (toDelete.length) {
418+
const activeAssetOutputs = new Set<string>();
419+
for (const outputs of this.#emittedAssets.values()) {
420+
for (const output of outputs) {
421+
activeAssetOutputs.add(output);
422+
}
423+
}
424+
425+
for (const deletedAsset of toDelete) {
426+
if (!activeAssetOutputs.has(deletedAsset.path)) {
427+
this.#filesToDelete.add(deletedAsset.path);
428+
}
429+
}
430+
}
431+
432+
this.#emittedBundleFiles = this.#nextBundleFiles ?? this.#emittedBundleFiles;
433+
this.#emittedManifestFiles = this.#nextManifestFiles ?? this.#emittedManifestFiles;
434+
this.#nextBundleFiles = undefined;
435+
this.#nextManifestFiles = undefined;
436+
this.hasEmittedManifests = true;
437+
this.#hasEmittedTslib = this.hasTslibImport;
438+
this.#hasEmittedAssets = true;
439+
this.#pendingAssetFiles.clear();
440+
441+
return this.#filesToDelete;
297442
}
298443

299444
/**
@@ -303,17 +448,24 @@ export class BuilderState {
303448
await this.#singleProgramCache?.compilationInstance.close?.();
304449
this.#singleProgramCache = undefined;
305450
this.#compilationMode = undefined;
306-
this.hasEmittedTslib = false;
307-
this.hasEmittedAssets = false;
451+
this.#hasEmittedTslib = false;
452+
this.#hasEmittedAssets = false;
308453
this.hasCompilationError = false;
309454
this.hasEmittedManifests = false;
310455
this.hasEntryPointsChanges = false;
311456
this.previousBundleResults.clear();
312457
this.pendingChangedEsmFiles.clear();
313458
this.pendingChangedDtsFiles.clear();
314459
this.#pendingCompilationFiles.clear();
460+
this.#pendingAssetFiles.clear();
315461
this.#referencingFiles.clear();
316462
this.#referencedByFile.clear();
317463
this.#watchedCompilationFiles.clear();
464+
this.#emittedBundleFiles.clear();
465+
this.#emittedManifestFiles.clear();
466+
this.#emittedAssets.clear();
467+
this.#nextBundleFiles = undefined;
468+
this.#nextManifestFiles = undefined;
469+
this.#filesToDelete.clear();
318470
}
319471
}

0 commit comments

Comments
 (0)