fix(builder): watch the files the federation build actually tracked - #96
Conversation
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 native-federation#94.
d59d16a to
c3cde70
Compare
|
Thanks for PR! Have a look at these feedback points though. 1. The root cause holds only on the parallel-TS path — please pin down which one you were on. Upstream (22.0.x) confirms both halves. With
Worth noting what's load-bearing regardless of path: 2. The widened wake-up doubles dev-feedback latency on common saves.
The second pass isn't a cold rebuild — Can we narrow the wake set to sources reachable from 3. Type it against Both files already import You can merge main to receive the fixes for the audit |
…n-tracked-sources
- Extract the duplicated federationSourceFiles helper into src/utils, typed against SourceFileCache so a future Angular rename fails the build instead of silently regressing to no watching; cover it with a spec. - Reword the path-dependent claim: the outer Map is only guaranteed empty on the parallel-TS path — with in-process type checking (NG_BUILD_PARALLEL_TS=0) augmentHostWithCaching fills it, while templates/styles only ever live in referencedFiles. Log NG_BUILD_PARALLEL_TS and the observed cache shape at verbose level so the active compilation path is diagnosable at builder start. - Skip the Angular-driven federation rebuild when a watcher-driven rebuild already completed since the last consumed Angular output and the dirty buffer is empty: an ordinary save no longer pays a second rebuildDelay plus a redundant re-link of identical outputs.
Verifying the widened watch list on a real Nx workspace (macOS) surfaced a self-sustaining rebuild loop: FSEvents re-delivers "changed" events for recently modified files on a ~30s cadence with mtime and ctime untouched. With every federation-tracked source watched, one such file wakes the rebuild loop, the rebuild resyncs the watcher, the replay fires again — federation rebuilds run forever and pin a core until the dev server is stopped. Filter events through an mtime check before they reach the dirty buffer: a same-mtime event is a replay and is dropped; a real save advances mtime and passes; a failed stat (deletion) passes. Watch-list entries are seeded with their current mtime at sync time so a replay never triggers even one spurious rebuild. Applied to both builders.
|
Follow-up with the end-to-end results promised in the description, plus the three review points. Main is merged in. 1. Compilation path pinned down — your Nx load-order hypothesis was right. The builders now log the diagnostics at verbose level, and on the Nx workspace they print: Parallel path despite the env value: 2. Double rebuild eliminated. A completed watcher-driven rebuild now passes the same save's subsequent Angular output straight through when the dirty buffer is empty ( 3. Helper extracted and typed. New finding from the live verification — the widened watch list needs an event filter. On macOS, FSEvents re-delivers "changed" events for recently modified files on a ~30 s cadence with mtime and ctime untouched. Watching every federation-tracked source turned that into a self-sustaining loop: replay wakes the loop, the rebuild resyncs the watcher, the next replay fires — federation rebuilds ran forever and pinned a core. Raw Fixed in the latest commit: events are dropped unless the file's mtime actually advanced (baselines seeded at watcher-sync time; failed stats — deletions — pass through). With the filter, an 80 s watch window right after a save — the exact replay trigger condition — recorded zero spurious rebuilds; both builders are covered and the filter has its own spec.
|
13012ed to
e17fccf
Compare
|
Thanks for this! |
Fixes #94.
Problem
While the dev server runs, edits to shared-mapping libraries and exposed sources never reach the emitted federation bundles. The dev server keeps serving stale code until it is restarted; a hard browser reload does not help.
Root cause
syncNfFileWatcherdecides what to watch from the bundler cache:bundlerCacheis Angular'sSourceFileCache. That class extendsMap, but where a tracked file is recorded depends on the compilation mode:SourceFileCache(parallel-compilation.tsnever receives the outer one), so the outer Map stays empty.NG_BUILD_PARALLEL_TS=0, applied before anything imports@angular/build)augmentHostWithCachingdoes populate the outer Map with parsed.tssources..tsoutput lands intypeScriptFileCache, and templates/styles are listed only inreferencedFiles(SourceFileCache.invalidate()itself consultsreferencedFilesviaextraWatchFiles).So reading only
keys()always misses templates and styles, and on the parallel path misses everything.Which path was the instrumented run on? The parallel one — confirmed with the diagnostics this PR now logs at verbose level (
NG_BUILD_PARALLEL_TSat builder start plus the observed cache shape after each watcher sync). On the Nx workspace below the dev server prints:setup-builder-env-variables.tsdid setNG_BUILD_PARALLEL_TS=0, butuseParallelTsis a module-level const in@angular/build, and under Nx something imports@angular/buildbefore the builder module does — so the env assignment comes too late and the parallel path runs anyway. That makes part 1 load-bearing for Nx users regardless of the intended default, and it is a second bug worth filing separately (the env var silently not taking effect).useParallelTsis not re-exported through@angular/build/private, so the effective path cannot be read directly; the env value + cache shape pair in the logs is the reliable signal.There is a second gap on the same path (load-bearing on either compilation path): the rebuild loop is only woken for npm-linked dirs (
if (isUnderLinkedDir(p)) notifyChange()). Shared mappings and exposes are externals for the app build, so Angular's own rebuild iterator never emits for them — the "ride the next Angular-driven rebuild" fallback never arrives.Fix
syncNfFileWatchera key view over the union of the outerkeys(),typeScriptFileCacheandreferencedFiles— covers both compilation paths. The helper lives insrc/utils/federation-source-files.ts, is typed againstSourceFileCache(a future Angular rename now breaks the build instead of silently regressing to no watching) and is covered by a spec.rebuildDelayplus a re-link that rewrites identical outputs.src/utils/stale-watch-event-filter.ts, with spec — see below for why this is required, not optional, once the watch list is this wide).Points 1, 2 and 4 apply to both
build/builder.tsandremote/builder.ts; the remote builder has no Angular-output race, so point 3 only applies tobuild.Found while verifying: FSEvents replay turns the wide watch list into an infinite rebuild loop
Live-testing this branch on the workspace below surfaced a failure mode the original submission missed. On macOS, FSEvents re-delivers "changed" events for recently modified files on a ~30 s cadence even when nothing touched them again — mtime and ctime stay put. Instrumenting the raw
fs.watchcallbacks showed the pattern clearly:With every federation-tracked source watched (~1,900 files here), one recently-edited file is enough: the replay wakes the loop, the rebuild resyncs the watcher, the next replay fires again — federation rebuilds run forever and pin a core until the dev server is stopped. The original code never hit this because it effectively watched nothing.
The filter drops any event whose mtime matches the last value seen for that path; a real save advances mtime and passes, and a failed stat (deletion) passes. Watch-list entries are seeded with their current mtime at sync time, so a replay never triggers even one spurious rebuild.
Verification
Original round, measured on a real Nx + Angular 22.0.8 workspace (host + 3 remotes) by writing a marker into a source file while the dev server is running, then checking whether it reaches the emitted bundle:
.tsedit.htmleditThe
.htmlcase is the variant described in the issue comment (triggered throughexposesrather thansharedMappings). Both go through the samemapping-or-exposedcontext, so a single fix closes both.Review round, re-verified end-to-end with the built artifact (all four changes combined) dropped into the consuming workspace's
node_modules:.tssave produced exactly one federation rebuild: the marker reached the emitted_apex_platform_logging.jsand the rewrittenremoteEntry.jsonone second after the save, and was served by the dev middleware. No second cycle followed the same save's Angular output (point 3 above at work).npm run typecheck,npm run lint(0 errors; warning count identical to the unmodified baseline) andvitest run(16 files / 111 tests) all pass.