Command
test
Is this a regression?
Not known to be — this is the @angular/build:unit-test builder's default (jsdom) runner; I have no earlier version where this worked.
Description
Under the unit-test builder's default jsdom runner, a module that esbuild places in a chunk shared between spec entry points is wrapped in a lazy __esm initialiser, and a consumer in a different chunk reads its exports as undefined during class-field initialisation. The same specs pass when the runner is switched to browser mode with the same bundles, so this looks like the Node-side module evaluation path rather than a bundling fault.
The distinction that makes it precise:
- a service reads the shared const inside a method → always correct;
- a component reads the same const in a class-field initialiser → undefined.
The binding is live — replacing the fields with getters (reading the same import later) makes the failing test pass with no other change. So the value arrives; at construction time it is not yet visible to the importer, even though the emitted code calls init_env_config() earlier in the same module body and construction happens later still.
Trigger conditions — each toggled on its own with everything else constant; all four are required:
| # |
Required condition |
Evidence |
| 1 |
≥ 2 spec entries import the module (shared chunk, lazy __esm wrappers) |
a single spec file inlines everything and passes |
| 2 |
a consumer in another chunk reads the export during class-field initialisation |
getters pass |
| 3 |
the consuming spec file contains an async test callback |
making that one test synchronous passes; async with no await still fails |
| 4 |
zone.js is in the unit-test polyfills |
deleting "polyfills": ["zone.js"] from the unit-test build configuration makes the same suite pass; restoring it fails again |
Not required (removed with the failure surviving): TestBed.configureTestingModule() / compileComponents() (a bare TestBed.createComponent() inside an async test fails), provider overrides, barrel files, @angular/core patch level (fails on 21.2.18 and 21.2.19).
Conditions 3 and 4 are one mechanism: with zone.js present the builder downlevels async/await, so a spec containing any async function imports the __async helper — and esbuild then emits that spec entry CommonJS-wrapped, with its entry-local ESM dependencies (the component module) as lazy __esm initialisers:
// spec-…-lead-generator.container.js (dumped with ng test --dump-virtual-files)
import { DEALERS, LeadService, init_env_config, init_lead_service } from "./chunk-N64URYIK.js";
import { __async, __commonJS, __esm } from "./chunk-EFV3TZQO.js";
var LeadGeneratorContainer;
var init_lead_generator_container = __esm({
"…/lead-generator.container.ts"() {
init_services();
init_env_config(); // runs before the class statement below
LeadGeneratorContainer = class _LeadGeneratorContainer {
leadService = inject(LeadService);
dealers = DEALERS; // undefined at construction time
dealerOptions = this.dealers.map(…); // throws
};
}
});
var require_lead_generator_container_spec = __commonJS({
"…/lead-generator.container.spec.ts"(exports) {
init_lead_generator_container();
describe("LeadGeneratorContainer", () => { … });
}
});
export default require_lead_generator_container_spec();
The shared chunk assigns DEALERS inside init_env_config()'s __esm wrapper and exports the binding; the emitted code is valid ESM and runs correctly in browser mode.
One caveat for anyone building a paraphrased fixture: the failure is sensitive to inert content — adding a top-level console.log to either the shared or the importing module defuses it, and one hand-written structural clone of the same shape passes. The linked reproduction is the exact shape that fails deterministically.
Minimal Reproduction
https://github.com/jonmarozick/ng-shared-chunk-repro
npm install
npm test # ng test --no-watch
Expected: 2 passed. Actual: Test Files 1 failed | 1 passed (2) — lead-generator.container.spec.ts fails, lead.service.spec.ts (the control reading the same const in a method) passes.
Nine source files, dependencies are only Angular 21.2.19 + Vitest 4 + jsdom, package-lock.json committed. The module graph:
src/environments/env-config.ts exports `const DEALERS`
├── lead.service.ts reads DEALERS in a method
│ ├── lead.service.spec.ts ← spec entry 1 (passes)
│ └── lead-generator.container.ts
└── lead-generator.container.ts reads DEALERS in a field initialiser
└── lead-generator.container.spec.ts ← spec entry 2 (fails)
Also reproduced by copying these source files unchanged into a fresh ng new workspace on CLI 21.2.19 (@angular/core 21.2.19) with "polyfills": ["zone.js"] added to the unit-test build configuration — so nothing about it depends on the original project. The emitted bundles can be inspected via ng test --dump-virtual-files (they land in .angular/cache/<version>/<project>/unit-test/output-files/).
Exception or Error
TypeError: Cannot read properties of undefined (reading 'map')
❯ _LeadGeneratorContainer.<instance_members_initializer> src/app/features/lead-generator/lead-generator.container.ts:291:43
❯ new _LeadGeneratorContainer src/app/features/lead-generator/lead-generator.container.ts:336:5
❯ NodeInjectorFactory.LeadGeneratorContainer_Factory [as factory] spec-app-features-lead-generator-lead-generator.container.js:1138:16
❯ getNodeInjectable packages/core/src/render3/di.ts:785:37
❯ instantiateAllDirectives packages/core/src/render3/instructions/shared.ts:122:2
❯ createComponentRef packages/core/src/render3/component_ref.ts:301:20
❯ create packages/core/testing/src/test_bed.ts:695:44
Your Environment
Angular CLI: 21.2.19
Node: 24.18.0
Package Manager: npm 11.16.0
OS: win32 x64
@angular/build 21.2.19
@angular/cli 21.2.19
@angular/core 21.2.18
@angular/compiler-cli 21.2.18
typescript 5.9.3
vitest 4.1.10
zone.js 0.16.2
jsdom (builder default)
Also reproduced on Linux CI (ubuntu-latest, Node 24) and in a fresh ng new workspace with @angular/core 21.2.19.
Anything else relevant?
Command
test
Is this a regression?
Not known to be — this is the @angular/build:unit-test builder's default (jsdom) runner; I have no earlier version where this worked.
Description
Under the unit-test builder's default jsdom runner, a module that esbuild places in a chunk shared between spec entry points is wrapped in a lazy __esm initialiser, and a consumer in a different chunk reads its exports as undefined during class-field initialisation. The same specs pass when the runner is switched to browser mode with the same bundles, so this looks like the Node-side module evaluation path rather than a bundling fault.
The distinction that makes it precise:
The binding is live — replacing the fields with getters (reading the same import later) makes the failing test pass with no other change. So the value arrives; at construction time it is not yet visible to the importer, even though the emitted code calls init_env_config() earlier in the same module body and construction happens later still.
Trigger conditions — each toggled on its own with everything else constant; all four are required:
Not required (removed with the failure surviving): TestBed.configureTestingModule() / compileComponents() (a bare TestBed.createComponent() inside an async test fails), provider overrides, barrel files, @angular/core patch level (fails on 21.2.18 and 21.2.19).
Conditions 3 and 4 are one mechanism: with zone.js present the builder downlevels async/await, so a spec containing any async function imports the __async helper — and esbuild then emits that spec entry CommonJS-wrapped, with its entry-local ESM dependencies (the component module) as lazy __esm initialisers:
The shared chunk assigns DEALERS inside init_env_config()'s __esm wrapper and exports the binding; the emitted code is valid ESM and runs correctly in browser mode.
One caveat for anyone building a paraphrased fixture: the failure is sensitive to inert content — adding a top-level console.log to either the shared or the importing module defuses it, and one hand-written structural clone of the same shape passes. The linked reproduction is the exact shape that fails deterministically.
Minimal Reproduction
https://github.com/jonmarozick/ng-shared-chunk-repro
Expected: 2 passed. Actual: Test Files 1 failed | 1 passed (2) — lead-generator.container.spec.ts fails, lead.service.spec.ts (the control reading the same const in a method) passes.
Nine source files, dependencies are only Angular 21.2.19 + Vitest 4 + jsdom, package-lock.json committed. The module graph:
src/environments/env-config.ts exports `const DEALERS` ├── lead.service.ts reads DEALERS in a method │ ├── lead.service.spec.ts ← spec entry 1 (passes) │ └── lead-generator.container.ts └── lead-generator.container.ts reads DEALERS in a field initialiser └── lead-generator.container.spec.ts ← spec entry 2 (fails)Also reproduced by copying these source files unchanged into a fresh ng new workspace on CLI 21.2.19 (@angular/core 21.2.19) with "polyfills": ["zone.js"] added to the unit-test build configuration — so nothing about it depends on the original project. The emitted bundles can be inspected via ng test --dump-virtual-files (they land in .angular/cache/<version>/<project>/unit-test/output-files/).
Exception or Error
Your Environment
Also reproduced on Linux CI (ubuntu-latest, Node 24) and in a fresh ng new workspace with @angular/core 21.2.19.
Anything else relevant?