From 462815813f2bbe392fc53b808162f9a468cf0a04 Mon Sep 17 00:00:00 2001 From: MeAkib Date: Wed, 28 Oct 2026 18:51:13 +0600 Subject: [PATCH 1/2] fix(@angular/build): remove every output of a deleted asset in watch mode The previous asset state was keyed by source file, so an asset copied to several destinations (two asset entries with the same input, or one copy per locale with `localize`) kept only the last destination. Deleting the source during `ng build --watch` then removed only that copy and left the others in the output directory. Key the previous asset state by destination instead. --- .../src/builders/application/build-action.ts | 6 ++-- .../tests/behavior/rebuild-assets_spec.ts | 34 +++++++++++++++++++ .../tools/esbuild/bundler-execution-result.ts | 3 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index eaae50848a48..4589ff2e7b03 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -316,9 +316,9 @@ function* emitOutputResults( // Initially assume all previous assets files have been removed const removedAssetFiles = new Map(previousAssetsInfo); for (const { source, destination } of assetFiles) { - removedAssetFiles.delete(source); + removedAssetFiles.delete(destination); - if (!previousAssetsInfo.has(source)) { + if (!previousAssetsInfo.has(destination)) { incrementalResult.added.push(destination); incrementalResult.background = false; } else if (changes.modified.has(source)) { @@ -353,7 +353,7 @@ function* emitOutputResults( path, type, })), - ...Array.from(removedAssetFiles.values(), (path) => ({ + ...Array.from(removedAssetFiles.keys(), (path) => ({ path, type: BuildOutputFileType.Browser, })), diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts index 7bfcca94d242..1c36f64c1892 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts @@ -76,5 +76,39 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }, ]); }); + + it('remove all outputs of a deleted asset copied to multiple destinations', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { + glob: '**/*', + input: 'public', + }, + { + glob: '**/*', + input: 'public', + output: 'static', + }, + ], + watch: true, + }); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').toExist(); + harness.expectFile('dist/browser/static/asset.txt').toExist(); + + await harness.removeFile('public/asset.txt'); + }, + + ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').toNotExist(); + harness.expectFile('dist/browser/static/asset.txt').toNotExist(); + }, + ]); + }); }); }); diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index 991ad988662b..bebc98a43808 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -176,8 +176,9 @@ export class ExecutionResult { previousOutputInfo: new Map( this.outputFiles.map(({ path, hash, type }) => [`${type}:${path}`, { hash, type, path }]), ), + // Keyed by destination since one source can be copied to several outputs (e.g. per locale). previousAssetsInfo: new Map( - this.assetFiles.map(({ source, destination }) => [source, destination]), + this.assetFiles.map(({ source, destination }) => [destination, source]), ), templateUpdates: this.templateUpdates, }; From 6a5a552f4ec8f4a3476426d79b1b77ab58c95511 Mon Sep 17 00:00:00 2001 From: MeAkib Date: Wed, 28 Oct 2026 19:57:11 +0600 Subject: [PATCH 2/2] fixup! fix(@angular/build): remove every output of a deleted asset in watch mode --- .../build/src/builders/application/build-action.ts | 4 ++-- .../build/src/tools/esbuild/bundler-execution-result.ts | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index 4589ff2e7b03..8d06213833cd 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -314,7 +314,7 @@ function* emitOutputResults( } // Initially assume all previous assets files have been removed - const removedAssetFiles = new Map(previousAssetsInfo); + const removedAssetFiles = new Set(previousAssetsInfo); for (const { source, destination } of assetFiles) { removedAssetFiles.delete(destination); @@ -353,7 +353,7 @@ function* emitOutputResults( path, type, })), - ...Array.from(removedAssetFiles.keys(), (path) => ({ + ...Array.from(removedAssetFiles, (path) => ({ path, type: BuildOutputFileType.Browser, })), diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index bebc98a43808..3777d2835f5a 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -31,7 +31,7 @@ export interface RebuildState { string, { hash: string; type: BuildOutputFileType; path: string } >; - previousAssetsInfo: ReadonlyMap; + previousAssetsInfo: ReadonlySet; templateUpdates?: Map; } @@ -176,10 +176,8 @@ export class ExecutionResult { previousOutputInfo: new Map( this.outputFiles.map(({ path, hash, type }) => [`${type}:${path}`, { hash, type, path }]), ), - // Keyed by destination since one source can be copied to several outputs (e.g. per locale). - previousAssetsInfo: new Map( - this.assetFiles.map(({ source, destination }) => [destination, source]), - ), + // Tracked by destination since one source can be copied to several outputs (e.g. per locale). + previousAssetsInfo: new Set(this.assetFiles.map(({ destination }) => destination)), templateUpdates: this.templateUpdates, }; }