Hi,
We were routinely receiving build errors on Azure DevOps (using ubuntu image) for our native-federation-v4 app:
The confusing thing was, it if we retried the failed job, it would sometimes succeed, but we could never reproduce locally (all devs use mac or win). After some analysis with the help of Claude, we discovered a race condition was happening. Here are the findings:
Package: @angular-architects/native-federation-v4 v20.4.1
Angular: 20.x | @angular/build: (latest)
Summary
When buildForFederation runs before buildApplication, it inadvertently calls markAsReady() on Angular's internal globalSharedCompilationState singleton. This leaves #pendingCompilation = false when buildApplication starts. The polyfills bundle (NoopCompilation) then skips its wait for the browser bundle's AotCompilation to populate typeScriptFileCache, resulting in a hard build failure.
The bug is Linux-only (CI) and does not reproduce on macOS due to a difference in OS-level I/O scheduling (epoll vs kqueue).
Root Cause
Causal chain:
- buildForFederation calls bundleExposedAndMappings → createAngularEsbuildContext → createAwaitableCompilerPlugin, which wraps Angular's createCompilerPlugin from @angular/build/private.
- That plugin's onStart hook calls markAsInProgress() at the start and markAsReady() when done — leaving globalSharedCompilationState.#pendingCompilation = false after buildForFederation completes.
- This is the same singleton that buildApplication uses to coordinate its concurrent browser and polyfills bundles. buildForFederation is not supposed to interact with it at all.
- When buildApplication subsequently starts, BundlerContext.bundleAll fires two concurrent context.build() calls: - Browser bundle (AotCompilation): onStart calls markAsInProgress() synchronously, before any await - Polyfills bundle (NoopCompilation): onStart does await compilation.initialize() first (a microtask), then checks waitUntilReady
- On Linux (epoll + Go goroutine scheduling), the polyfills bundle's IPC callback arrives and resolves its microtask before the browser bundle's synchronous markAsInProgress() runs. It sees #pendingCompilation = false (the stale value from step 2), concludes compilation is already done, and proceeds — but typeScriptFileCache is empty, so polyfills.ts is reported as missing.
- On macOS (kqueue), the ordering consistently favours the browser bundle going first, so markAsInProgress() always runs before the polyfills bundle checks the flag. The race never triggers.
Workaround (applied via patch-package)
We've implemented a patch package workaround and the error has gone away. The solution was to reset the state immediately after buildForFederation completes, in src/builders/build/builder.js:
import { createRequire } from 'node:module';
// After buildForFederation / bundleExposedAndMappings completes (src/builders/build/builder.ts:349):
try {
const req = createRequire(import.meta.url);
const angularBuildDir = path.dirname(path.dirname(req.resolve('@angular/build')));
const compilationStatePath = path.join(
angularBuildDir,
'src/tools/esbuild/angular/compilation-state.js'
);
req(compilationStatePath).getSharedCompilationState().markAsInProgress();
} catch (_e) {}
This resets #pendingCompilation = true so buildApplication manages the state from a clean slate.
Reproduction
Consistent on any Linux CI environment (tested on GitHub Actions ubuntu-latest). Does not reproduce on macOS.
Hi,
We were routinely receiving build errors on Azure DevOps (using ubuntu image) for our native-federation-v4 app:
The confusing thing was, it if we retried the failed job, it would sometimes succeed, but we could never reproduce locally (all devs use mac or win). After some analysis with the help of Claude, we discovered a race condition was happening. Here are the findings:
Package: @angular-architects/native-federation-v4 v20.4.1
Angular: 20.x | @angular/build: (latest)
Summary
When buildForFederation runs before buildApplication, it inadvertently calls markAsReady() on Angular's internal globalSharedCompilationState singleton. This leaves #pendingCompilation = false when buildApplication starts. The polyfills bundle (NoopCompilation) then skips its wait for the browser bundle's AotCompilation to populate typeScriptFileCache, resulting in a hard build failure.
The bug is Linux-only (CI) and does not reproduce on macOS due to a difference in OS-level I/O scheduling (epoll vs kqueue).
Root Cause
Causal chain:
Workaround (applied via patch-package)
We've implemented a patch package workaround and the error has gone away. The solution was to reset the state immediately after buildForFederation completes, in src/builders/build/builder.js:
This resets #pendingCompilation = true so buildApplication manages the state from a clean slate.
Reproduction
Consistent on any Linux CI environment (tested on GitHub Actions ubuntu-latest). Does not reproduce on macOS.