Skip to content

Commit 33a6418

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 63b6471 commit 33a6418

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

@@ -283,11 +309,13 @@ export class BuilderState {
283309

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

301446
/**
@@ -305,17 +450,24 @@ export class BuilderState {
305450
await this.#singleProgramCache?.compilationInstance.close?.();
306451
this.#singleProgramCache = undefined;
307452
this.#compilationMode = undefined;
308-
this.hasEmittedTslib = false;
309-
this.hasEmittedAssets = false;
453+
this.#hasEmittedTslib = false;
454+
this.#hasEmittedAssets = false;
310455
this.hasCompilationError = false;
311456
this.hasEmittedManifests = false;
312457
this.hasEntryPointsChanges = false;
313458
this.previousBundleResults.clear();
314459
this.pendingChangedEsmFiles.clear();
315460
this.pendingChangedDtsFiles.clear();
316461
this.#pendingCompilationFiles.clear();
462+
this.#pendingAssetFiles.clear();
317463
this.#referencingFiles.clear();
318464
this.#referencedByFile.clear();
319465
this.#watchedCompilationFiles.clear();
466+
this.#emittedBundleFiles.clear();
467+
this.#emittedManifestFiles.clear();
468+
this.#emittedAssets.clear();
469+
this.#nextBundleFiles = undefined;
470+
this.#nextManifestFiles = undefined;
471+
this.#filesToDelete.clear();
320472
}
321473
}

0 commit comments

Comments
 (0)