From 65d3ab706590474b79ef70949af4874d6e9ba138 Mon Sep 17 00:00:00 2001 From: MeAkib Date: Thu, 29 Oct 2026 00:17:20 +0600 Subject: [PATCH] fix(@angular/build): copy assets replaced by a rename in watch mode When an asset is replaced by renaming another file over it, as editors with atomic save do, the native file watcher on Linux reports the asset as added instead of modified. The incremental build only copied existing assets that were reported as modified, so the output kept the previous contents of the asset. Existing assets reported as added are now copied as well. The new spec disables polling so that the native watcher is used. --- .../src/builders/application/build-action.ts | 3 +- .../tests/behavior/rebuild-assets_spec.ts | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index 8d06213833cd..d03d2741b105 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -321,7 +321,8 @@ function* emitOutputResults( if (!previousAssetsInfo.has(destination)) { incrementalResult.added.push(destination); incrementalResult.background = false; - } else if (changes.modified.has(source)) { + } else if (changes.modified.has(source) || changes.added.has(source)) { + // A file replaced by a rename (e.g. an atomic save) can be reported as added. incrementalResult.modified.push(destination); incrementalResult.background = false; } else { 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 1c36f64c1892..04610625eb09 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 @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { rename } from 'node:fs/promises'; import { buildApplication } from '../../index'; import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; @@ -43,6 +44,39 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { ]); }); + it('emits an asset replaced by renaming another file over it', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { + glob: '**/*', + input: 'public', + }, + ], + watch: true, + // Polling reports a replaced file as modified, while native watchers can report it as added. + poll: undefined, + }); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('foo'); + + // Editors with atomic save write a temporary file and rename it over the original. + await harness.writeFile('asset.txt.tmp', 'bar'); + await rename( + harness.resolvePath('asset.txt.tmp'), + harness.resolvePath('public/asset.txt'), + ); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('bar'); + }, + ]); + }); + it('remove deleted asset from output', async () => { await Promise.all([ harness.writeFile('public/asset-two.txt', 'bar'),