Skip to content

Commit 45cd57c

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 508d6a2 commit 45cd57c

9 files changed

Lines changed: 707 additions & 141 deletions

File tree

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

Lines changed: 161 additions & 13 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,27 @@ 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+
#hasUpdatedBundles = false;
44+
2945
#singleProgramCache?: SingleProgramCache;
3046
#compilationMode?: 'partial' | 'full';
47+
#hasEmittedTslib = false;
3148

3249
/** Map of entry point names to their previous bundle results. */
3350
readonly previousBundleResults = new Map<string, BundleResult>();
@@ -44,18 +61,12 @@ export class BuilderState {
4461
/** Whether package.json and secondary entry point manifests have been emitted. */
4562
hasEmittedManifests = false;
4663

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-
5364
/** Whether entry points in package.json changed since the last compilation. */
5465
hasEntryPointsChanges = false;
5566

5667
constructor(
5768
options?: Partial<
58-
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints'>
69+
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints' | 'assets'>
5970
>,
6071
) {
6172
if (options?.tsConfigPath) {
@@ -69,6 +80,8 @@ export class BuilderState {
6980
this.#watchedCompilationFiles.add(entryPoint.entryFilePath);
7081
}
7182
}
83+
84+
this.#assetMatchers = options?.assets ? createAssetMatchers(options.assets) : undefined;
7285
}
7386

7487
/** Cached unified TypeScript and Angular compilation state. */
@@ -81,6 +94,11 @@ export class BuilderState {
8194
return this.#compilationMode;
8295
}
8396

97+
/** Whether the last emitted root package.json included a tslib dependency. */
98+
get hasEmittedTslib(): boolean {
99+
return this.#hasEmittedTslib;
100+
}
101+
84102
/** Whether any active entry point bundle imports `tslib`. */
85103
get hasTslibImport(): boolean {
86104
for (const result of this.previousBundleResults.values()) {
@@ -109,12 +127,13 @@ export class BuilderState {
109127
}
110128

111129
/**
112-
* Resets the execution result for a new build iteration.
130+
* Resets the execution result and staged output state for a new build iteration.
113131
*
114132
* @returns The fresh `ExecutionResult` instance for the current build iteration.
115133
*/
116134
resetExecutionResult(): ExecutionResult {
117135
this.#executionResult = new ExecutionResult();
136+
this.#hasUpdatedBundles = false;
118137

119138
return this.#executionResult;
120139
}
@@ -221,6 +240,9 @@ export class BuilderState {
221240
if (file !== packageJsonPath) {
222241
this.#pendingCompilationFiles.add(file);
223242
}
243+
if (this.#hasEmittedAssets) {
244+
this.#pendingAssetFiles.add(file);
245+
}
224246
}
225247
}
226248

@@ -294,12 +316,13 @@ export class BuilderState {
294316
}
295317

296318
/**
297-
* Records active bundle results across entry points after compilation and bundling succeed,
298-
* clearing pending compilation/bundle flags.
319+
* Records active bundle results across entry points after compilation and bundling succeed
320+
* and clears pending compilation/bundle flags.
299321
*
300322
* @param bundleResults Newly bundled results keyed by entry point name.
301323
*/
302324
recordBundleOutput(bundleResults: ReadonlyMap<string, BundleResult>): void {
325+
const hadEntryPointsChanges = this.hasEntryPointsChanges;
303326
this.hasCompilationError = false;
304327
this.hasEntryPointsChanges = false;
305328
this.pendingChangedEsmFiles.clear();
@@ -308,6 +331,126 @@ export class BuilderState {
308331
for (const [name, bundleResult] of bundleResults) {
309332
this.previousBundleResults.set(name, bundleResult);
310333
}
334+
335+
if (bundleResults.size > 0 || hadEntryPointsChanges || this.#emittedBundleFiles.size === 0) {
336+
this.#hasUpdatedBundles = true;
337+
}
338+
}
339+
340+
/**
341+
* Resolves and collects configured library assets to be emitted to or deleted from disk.
342+
*
343+
* @param options Normalized library options containing `assets` and `workspaceRoot`.
344+
* @param modifiedFiles Optional set of modified POSIX file paths in watch mode.
345+
* @returns The collected asset files to emit and delete.
346+
*/
347+
collectAssets(
348+
options: Pick<NormalizedLibraryOptions, 'assets' | 'workspaceRoot'>,
349+
modifiedFiles?: ReadonlySet<string>,
350+
): Promise<CollectedAssets> {
351+
let effectiveModifiedFiles: ReadonlySet<string> | undefined;
352+
if (this.#hasEmittedAssets && modifiedFiles) {
353+
for (const file of modifiedFiles) {
354+
this.#pendingAssetFiles.add(file);
355+
}
356+
effectiveModifiedFiles = this.#pendingAssetFiles;
357+
}
358+
359+
return collectAssetsToEmit(
360+
options.assets,
361+
options.workspaceRoot,
362+
this.#emittedAssets,
363+
effectiveModifiedFiles,
364+
this.#assetMatchers,
365+
);
366+
}
367+
368+
/**
369+
* Records emitted bundle, manifest, and asset output files after disk emission,
370+
* returning any stale or deleted output paths to remove from disk.
371+
*
372+
* @param collectedAssets Collected asset files emitted and deleted in this iteration.
373+
* @param manifestFiles Optional manifest output files generated in this iteration.
374+
* @returns Read-only set of relative POSIX output paths to delete from disk.
375+
*/
376+
recordEmittedOutputs(
377+
collectedAssets: CollectedAssets,
378+
manifestFiles?: readonly OutputFile[],
379+
): ReadonlySet<string> {
380+
const filesToDelete = new Set<string>();
381+
382+
if (this.#hasUpdatedBundles) {
383+
this.#hasUpdatedBundles = false;
384+
const nextBundleFiles = new Set<string>();
385+
for (const result of this.previousBundleResults.values()) {
386+
for (const file of result.esmOutputFiles) {
387+
nextBundleFiles.add(file);
388+
}
389+
for (const file of result.dtsOutputFiles) {
390+
nextBundleFiles.add(file);
391+
}
392+
}
393+
394+
for (const prevFile of this.#emittedBundleFiles) {
395+
if (!nextBundleFiles.has(prevFile)) {
396+
filesToDelete.add(prevFile);
397+
}
398+
}
399+
400+
this.#emittedBundleFiles = nextBundleFiles;
401+
}
402+
403+
if (manifestFiles) {
404+
const nextManifestFiles = new Set(manifestFiles.map((file) => file.path));
405+
for (const prevFile of this.#emittedManifestFiles) {
406+
if (!nextManifestFiles.has(prevFile)) {
407+
filesToDelete.add(prevFile);
408+
}
409+
}
410+
411+
this.#emittedManifestFiles = nextManifestFiles;
412+
}
413+
414+
const { filesToDelete: toDelete, filesToEmit: toEmit } = collectedAssets;
415+
416+
for (const emittedAsset of toEmit) {
417+
let outputs = this.#emittedAssets.get(emittedAsset.source);
418+
if (!outputs) {
419+
outputs = new Set();
420+
this.#emittedAssets.set(emittedAsset.source, outputs);
421+
}
422+
outputs.add(emittedAsset.path);
423+
}
424+
425+
if (toDelete.length) {
426+
const candidateDeletes = new Set<string>();
427+
for (const source of toDelete) {
428+
const outputs = this.#emittedAssets.get(source);
429+
if (outputs) {
430+
for (const output of outputs) {
431+
candidateDeletes.add(output);
432+
}
433+
this.#emittedAssets.delete(source);
434+
}
435+
}
436+
437+
for (const outputs of this.#emittedAssets.values()) {
438+
for (const output of outputs) {
439+
candidateDeletes.delete(output);
440+
}
441+
}
442+
443+
for (const output of candidateDeletes) {
444+
filesToDelete.add(output);
445+
}
446+
}
447+
448+
this.hasEmittedManifests = true;
449+
this.#hasEmittedTslib = this.hasTslibImport;
450+
this.#hasEmittedAssets = true;
451+
this.#pendingAssetFiles.clear();
452+
453+
return filesToDelete;
311454
}
312455

313456
/**
@@ -317,17 +460,22 @@ export class BuilderState {
317460
await this.#singleProgramCache?.compilationInstance.close?.();
318461
this.#singleProgramCache = undefined;
319462
this.#compilationMode = undefined;
320-
this.hasEmittedTslib = false;
321-
this.hasEmittedAssets = false;
463+
this.#hasEmittedTslib = false;
464+
this.#hasEmittedAssets = false;
322465
this.hasCompilationError = false;
323466
this.hasEmittedManifests = false;
324467
this.hasEntryPointsChanges = false;
325468
this.previousBundleResults.clear();
326469
this.pendingChangedEsmFiles.clear();
327470
this.pendingChangedDtsFiles.clear();
328471
this.#pendingCompilationFiles.clear();
472+
this.#pendingAssetFiles.clear();
329473
this.#referencingFiles.clear();
330474
this.#referencedByFile.clear();
331475
this.#watchedCompilationFiles.clear();
476+
this.#emittedBundleFiles.clear();
477+
this.#emittedManifestFiles.clear();
478+
this.#emittedAssets.clear();
479+
this.#hasUpdatedBundles = false;
332480
}
333481
}

0 commit comments

Comments
 (0)