From 301eb0ffd6a6ba3cf60a1e8ff5bd0fbd2b7bee02 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:51:05 +0000 Subject: [PATCH 1/3] fix(@angular/build): deduplicate spurious chokidar polling change events When using Chokidar in polling mode, non-atomic file writes (such as open with O_TRUNC followed by write) can span across poll intervals and cause Chokidar to emit two 'change' events for a single file modification. If the initial rebuild completes quickly (e.g. in JIT mode), the trailing poll event can flush a second rebuild batch with the same file state. Track the last known mtimeMs and size of watched files in createChokidarWatcher and ignore 'modified' events when the file's mtimeMs and size have not changed since the previous queued or flushed event. --- packages/angular/build/src/utils/watcher.ts | 59 +++++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/angular/build/src/utils/watcher.ts b/packages/angular/build/src/utils/watcher.ts index 8d47ba48e907..f85ceb874b4e 100644 --- a/packages/angular/build/src/utils/watcher.ts +++ b/packages/angular/build/src/utils/watcher.ts @@ -752,6 +752,7 @@ async function createChokidarWatcher( ): Promise { const chokidar = chokidarModule ?? (await import('chokidar')); const watchedFiles = new Set(); + const fileStats = new Map(); let queue: WatcherQueue; if (options?.polling) { @@ -811,16 +812,36 @@ async function createChokidarWatcher( return; } - if (type !== 'removed') { - const stat = fs.statSync(rawPath, { throwIfNoEntry: false }); - // Ignore historical events from before watcher initialization, but allow a 1000 ms window - // to account for coarse filesystem timestamp resolution (e.g., ext4/overlayfs integer second - // mtime truncation on Linux) where files modified during startup may have truncated .000 ms mtimes. - if (stat && stat.mtimeMs < initTime - 1000) { - return; - } + if (type === 'removed') { + fileStats.delete(lookupKey); + queue.addChange(type, rawPath); + + return; + } + + const stat = fs.statSync(rawPath, { throwIfNoEntry: false }); + if (!stat) { + return; } + // Ignore historical events from before watcher initialization, but allow a 1000 ms window + // to account for coarse filesystem timestamp resolution (e.g., ext4/overlayfs integer second + // mtime truncation on Linux) where files modified during startup may have truncated .000 ms mtimes. + if (stat.mtimeMs < initTime - 1000) { + return; + } + + const previousStat = fileStats.get(lookupKey); + if ( + type === 'modified' && + previousStat && + previousStat.mtimeMs === stat.mtimeMs && + previousStat.size === stat.size + ) { + return; + } + + fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size }); queue.addChange(type, rawPath); }; @@ -833,8 +854,20 @@ async function createChokidarWatcher( return this; }, - next() { - return queue.next(); + async next() { + const result = await queue.next(); + if (result.value) { + for (const file of [...result.value.added, ...result.value.modified]) { + const stat = fs.statSync(file, { throwIfNoEntry: false }); + if (stat?.isFile()) { + const posixPath = toPosixPathNormalized(file); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size }); + } + } + } + + return result; }, add(paths) { @@ -846,6 +879,11 @@ async function createChokidarWatcher( if (!watchedFiles.has(lookupKey)) { watchedFiles.add(lookupKey); + const stat = fs.statSync(p, { throwIfNoEntry: false }); + if (stat?.isFile()) { + fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size }); + } + // For files inside node_modules, register their package directory. Only newly encountered // package directories need to be added to Chokidar. const { isPackage, newPkgDir } = nodeModulesManager.registerPackage(posixPath, lookupKey); @@ -873,6 +911,7 @@ async function createChokidarWatcher( const lookupKey = toLookupKey(posixPath, isCaseSensitive); if (watchedFiles.has(lookupKey)) { watchedFiles.delete(lookupKey); + fileStats.delete(lookupKey); // When the last watched file in a package is removed, unwatch the package directory. const { isPackage, unwatchPkgDir } = nodeModulesManager.removePackageFile( From e7de4ed60fcd0438e8f7fe782adb7b5fcd889476 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:15:45 +0000 Subject: [PATCH 2/3] fixup! fix(@angular/build): deduplicate spurious chokidar polling change events --- packages/angular/build/src/utils/watcher.ts | 24 ++++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/angular/build/src/utils/watcher.ts b/packages/angular/build/src/utils/watcher.ts index f85ceb874b4e..382367a57033 100644 --- a/packages/angular/build/src/utils/watcher.ts +++ b/packages/angular/build/src/utils/watcher.ts @@ -857,14 +857,17 @@ async function createChokidarWatcher( async next() { const result = await queue.next(); if (result.value) { - for (const file of [...result.value.added, ...result.value.modified]) { - const stat = fs.statSync(file, { throwIfNoEntry: false }); - if (stat?.isFile()) { - const posixPath = toPosixPathNormalized(file); - const lookupKey = toLookupKey(posixPath, isCaseSensitive); - fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size }); - } - } + const files = [...result.value.added, ...result.value.modified]; + await Promise.all( + files.map(async (file) => { + const stat = await fs.promises.stat(file).catch(() => undefined); + if (stat?.isFile()) { + const posixPath = toPosixPathNormalized(file); + const lookupKey = toLookupKey(posixPath, isCaseSensitive); + fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size }); + } + }), + ); } return result; @@ -879,11 +882,6 @@ async function createChokidarWatcher( if (!watchedFiles.has(lookupKey)) { watchedFiles.add(lookupKey); - const stat = fs.statSync(p, { throwIfNoEntry: false }); - if (stat?.isFile()) { - fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size }); - } - // For files inside node_modules, register their package directory. Only newly encountered // package directories need to be added to Chokidar. const { isPackage, newPkgDir } = nodeModulesManager.registerPackage(posixPath, lookupKey); From ae097a0fe90014c28c0ce626acc1167b7fb4a75e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:42:06 +0000 Subject: [PATCH 3/3] test(@angular/build): remove flaky attribute from integration test targets --- packages/angular/build/BUILD.bazel | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index d648331b70de..e77d03277780 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -291,7 +291,6 @@ jasmine_test( name = "application_integration_tests", size = "medium", data = [":application_integration_test_lib"], - flaky = True, shard_count = 25, ) @@ -303,7 +302,6 @@ jasmine_test( # Force IPv4 to resolve RBE resolution issues "NODE_OPTIONS": "--dns-result-order=ipv4first", }, - flaky = True, shard_count = 10, ) @@ -316,7 +314,6 @@ jasmine_test( # or standardize to avoid complex configuration like this! "PUPPETEER_DOWNLOAD_PATH": "../../../node_modules/puppeteer/downloads", }, - flaky = True, shard_count = 10, ) @@ -324,7 +321,6 @@ jasmine_test( name = "unit-test_integration_tests", size = "medium", data = [":unit-test_integration_test_lib"], - flaky = True, shard_count = 5, )