Skip to content

Commit 3d8bea0

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 01241a1 commit 3d8bea0

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
}
@@ -221,6 +244,9 @@ export class BuilderState {
221244
if (file !== packageJsonPath) {
222245
this.#pendingCompilationFiles.add(file);
223246
}
247+
if (this.#hasEmittedAssets) {
248+
this.#pendingAssetFiles.add(file);
249+
}
224250
}
225251
}
226252

@@ -295,11 +321,13 @@ export class BuilderState {
295321

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

313458
/**
@@ -317,17 +462,24 @@ export class BuilderState {
317462
await this.#singleProgramCache?.compilationInstance.close?.();
318463
this.#singleProgramCache = undefined;
319464
this.#compilationMode = undefined;
320-
this.hasEmittedTslib = false;
321-
this.hasEmittedAssets = false;
465+
this.#hasEmittedTslib = false;
466+
this.#hasEmittedAssets = false;
322467
this.hasCompilationError = false;
323468
this.hasEmittedManifests = false;
324469
this.hasEntryPointsChanges = false;
325470
this.previousBundleResults.clear();
326471
this.pendingChangedEsmFiles.clear();
327472
this.pendingChangedDtsFiles.clear();
328473
this.#pendingCompilationFiles.clear();
474+
this.#pendingAssetFiles.clear();
329475
this.#referencingFiles.clear();
330476
this.#referencedByFile.clear();
331477
this.#watchedCompilationFiles.clear();
478+
this.#emittedBundleFiles.clear();
479+
this.#emittedManifestFiles.clear();
480+
this.#emittedAssets.clear();
481+
this.#nextBundleFiles = undefined;
482+
this.#nextManifestFiles = undefined;
483+
this.#filesToDelete.clear();
332484
}
333485
}

0 commit comments

Comments
 (0)