From c3cde703d5f407487ef21abd8ec94f070b254edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Arif=20=C5=9Ei=C5=9Fman?= Date: Wed, 29 Jul 2026 01:05:37 +0300 Subject: [PATCH] fix(builder): watch the files the federation build actually tracked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit syncNfFileWatcher derived its watch list from `bundlerCache.keys()`. That cache is Angular's SourceFileCache, which extends Map but keeps tracked files in `typeScriptFileCache` (.ts) and `referencedFiles` (templates and styles) rather than in the outer Map. Instrumented on a running dev server: outerMap.size 0, typeScriptFileCache.size 197, referencedFiles.length 3119. Reading only `keys()` therefore watched nothing, so shared-mapping and exposed sources never invalidated the cache and the dev server kept serving stale bundles until restart. Read the two properties that actually hold the tracked files, and wake the rebuild loop for them too — they are externals for the app build, so Angular's own rebuild iterator never emits for them. Fixes #94. --- src/builders/build/builder.ts | 60 ++++++++++++++++++++++++---------- src/builders/remote/builder.ts | 32 +++++++++++++----- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/builders/build/builder.ts b/src/builders/build/builder.ts index 0ed503e..81eb22f 100644 --- a/src/builders/build/builder.ts +++ b/src/builders/build/builder.ts @@ -425,6 +425,38 @@ export async function* runBuilder( const isUnderLinkedDir = (p: string): boolean => linkedDirs.some((d) => p === d || p.startsWith(d + path.sep)); + // Angular's SourceFileCache extends Map but keeps what it actually tracked in + // `typeScriptFileCache` (.ts) and `referencedFiles` (templates/styles); the + // outer Map stays empty. Reading only `keys()` therefore watches NOTHING, so + // shared-mapping and exposed sources never invalidate and the dev server keeps + // serving stale bundles until it is restarted. + const federationSourceFiles = (cache: { + keys(): IterableIterator; + typeScriptFileCache?: Map; + referencedFiles?: readonly string[]; + }): string[] => + [ + ...new Set([ + ...cache.keys(), + ...(cache.typeScriptFileCache?.keys() ?? []), + ...(cache.referencedFiles ?? []), + ]), + ].filter((file) => !file.includes("node_modules")); + + const federationWatchedFiles = new Set(); + const syncFederationWatcher = (): void => { + if (!nfWatcher) return; + const files = federationSourceFiles( + normalized.options.federationCache.bundlerCache, + ); + for (const file of files) federationWatchedFiles.add(path.normalize(file)); + syncNfFileWatcher( + nfWatcher, + { keys: () => files[Symbol.iterator]() }, + linkedDirs, + ); + }; + // watcherRef lets onChange reach the watcher without a const self-reference. const watcherRef: { current?: NfFileWatcher } = {}; const nfWatcher: NfFileWatcher | undefined = watch @@ -433,10 +465,16 @@ export async function* runBuilder( debounceMs: 100, onChange: (p) => { // Core stops filling the dirty buffer once onChange is set, so refill it - // here (Set.add stays idempotent if core is later fixed). Only wake the - // loop for linked edits; others ride the next Angular-driven rebuild. + // here (Set.add stays idempotent if core is later fixed). Wake the loop + // for edits the Angular-driven rebuild will NOT cover: linked dirs and + // the federation's own tracked sources, which are externals to the app + // build and so never reach Angular's rebuild iterator. watcherRef.current?.mutate((s) => s.add(p)); - if (isUnderLinkedDir(p)) notifyChange(); + if ( + isUnderLinkedDir(p) || + federationWatchedFiles.has(path.normalize(p)) + ) + notifyChange(); }, }) : undefined; @@ -474,13 +512,7 @@ export async function* runBuilder( await adapter.dispose("mapping-or-exposed").catch(() => undefined); } - if (nfWatcher) { - syncNfFileWatcher( - nfWatcher, - normalized.options.federationCache.bundlerCache, - linkedDirs, - ); - } + syncFederationWatcher(); const hasLocales = i18n?.locales && Object.keys(i18n.locales).length > 0; if (hasLocales && localeFilter) { @@ -582,13 +614,7 @@ export async function* runBuilder( signal, ); - if (nfWatcher) { - syncNfFileWatcher( - nfWatcher, - normalized.options.federationCache.bundlerCache, - linkedDirs, - ); - } + syncFederationWatcher(); if (signal?.aborted) { throw new AbortedError("[builder] After federation build."); diff --git a/src/builders/remote/builder.ts b/src/builders/remote/builder.ts index 482e267..f72c4f3 100644 --- a/src/builders/remote/builder.ts +++ b/src/builders/remote/builder.ts @@ -135,13 +135,33 @@ export async function* runRemoteBuilder( await copyAllAssets(assetEntries, absoluteBrowserOutput, context.workspaceRoot); - if (changeWatcher) { + // Angular's SourceFileCache extends Map but keeps what it actually tracked in + // `typeScriptFileCache` (.ts) and `referencedFiles` (templates/styles); the + // outer Map stays empty. Reading only `keys()` therefore watches NOTHING, so + // shared-mapping and exposed sources never invalidate and the dev server keeps + // serving stale bundles until it is restarted. + const syncFederationWatcher = (): void => { + if (!changeWatcher) return; + const cache = normalized.options.federationCache.bundlerCache as { + keys(): IterableIterator; + typeScriptFileCache?: Map; + referencedFiles?: readonly string[]; + }; + const files = [ + ...new Set([ + ...cache.keys(), + ...(cache.typeScriptFileCache?.keys() ?? []), + ...(cache.referencedFiles ?? []) + ]) + ].filter((file) => !file.includes('node_modules')); syncNfFileWatcher( changeWatcher.watcher, - normalized.options.federationCache.bundlerCache, + { keys: () => files[Symbol.iterator]() }, linkedDirs ); - } + }; + + syncFederationWatcher(); const rebuildQueue = new RebuildQueue(); @@ -197,11 +217,7 @@ export async function* runRemoteBuilder( // remain in pendingPaths and will drive the next iteration. for (const p of changedFiles) changeWatcher.pendingPaths.delete(p); - syncNfFileWatcher( - changeWatcher.watcher, - normalized.options.federationCache.bundlerCache, - linkedDirs - ); + syncFederationWatcher(); if (signal?.aborted) { throw new AbortedError('[remote-builder] After federation build.');