Which @angular/* package(s) are the source of the bug?
build
Is this a regression?
Yes, this behaviour used to work in version 21.2.19
Description
In a multi-entry-point library built with ng-packagr, @angular/build:unit-test (Vitest + jsdom) elides module bodies from the primary entry point's export * barrel graph when the library's package.json declares "sideEffects": false. The namespace re-export accessors for those modules survive and return undefined.
When one of the dropped exports is a standalone component referenced from a retained imports: [...] array literal of another component, TestBed.configureTestingModule throws:
TypeError: Cannot read properties of undefined (reading 'ɵcmp')
at getComponentDef (core/testing/src/test_bed_compiler.ts:1169)
at isStandaloneComponent (core/testing/src/test_bed_compiler.ts:1162)
at queueTypesFromModulesArrayRecur
at TestBedCompiler.queueTypesFromModulesArray
at TestBedCompiler.configureTestingModule
Dropping a module whose export is statically referenced from a retained array literal looks incorrect regardless of sideEffects, which is a module-level assertion about import-for-side-effect semantics — not a licence to remove a module that is still statically reachable.
In our library (761 barrel exports across 4 entry points) this fails 36 specs in 3 files, all in one secondary entry point. On Angular 21.2.19 the identical suite is 389/389 green.
Not a duplicate of #33728
#33728 describes the same symptom (an export from a shared chunk reading as undefined under the jsdom runner) and was fixed by #33729 "disable code splitting for unit test builds", released in v22.1.5.
That fix is installed and active here and the failure still reproduces. Verified in the installed package:
- src/builders/unit-test/runners/vitest/build-options.js:238 sets disableCodeSplitting: true
- honoured at src/tools/esbuild/application-code-bundle.js:55-58 (buildOptions.splitting = false)
So this is a distinct cause: not cross-chunk lazy initialisers, but whole-module elision driven by sideEffects.
Please provide a link to a minimal reproduction of the bug
Unable to share the affected codebase. Reproduction recipe below — it is deterministic and the toggle is a single field.
Setup
- ng new repro --no-standalone=false then ng generate library lib, @angular/build >= 22.1.5, TypeScript 6.0.3, Node 22.
- Add "sideEffects": false to projects/lib/package.json.
- In the primary entry, create a standalone FooComponent and export it through a multi-level barrel: public-api.ts → export * from './lib/components/public-api-components' → export * from './foo/foo.component'. At least two levels of export * matter; a single flat barrel did not reproduce for us.
- Add a secondary entry point (projects/lib/sub/ with its own ng-package.json and public-api.ts), mapped in the workspace tsconfig.json paths to its source public-api.ts, alongside "@lib": ["./projects/lib/src/public-api.ts"].
- In the secondary entry, create a standalone BarComponent with imports: [FooComponent], importing it as import {FooComponent} from '@lib'.
- Give BarComponent a spec doing TestBed.configureTestingModule({imports: [BarComponent]}).
- Configure a test target with @angular/build:unit-test, buildTarget pointing at the library build, and include covering the secondary entry's specs. Run the whole suite, not a single file — see below.
Expected: spec passes.
Actual: TypeError: Cannot read properties of undefined (reading 'ɵcmp').
Toggle: change projects/lib/package.json to "sideEffects": true (or a glob matching the source tree, e.g. ["./src/**", "./*/src/**"]) → passes. Change it back → fails. Deterministic in both directions.
Diagnostics that pin the mechanism
These are the observations that distinguish elision from TDZ, resolution failure, or interop snapshotting:
- The module body never runs. Injecting an unconditional (globalThis as any).__RAN = true at the top of an affected module leaves __RAN undefined at runtime, while a control module in the same barrel has its marker set. A module containing an unconditional global assignment can only be removed if it is declared side-effect-free.
- Not TDZ. On import * as barrel from '@lib', the affected names are present as ACCESSOR property descriptors and still return undefined after both a microtask and a macrotask tick.
- Not duplicate module identity. directImport === barrel.Foo is true.
- Only classes are affected in practice. Plain functions exported from the same barrel in the same import statement resolve correctly; the value read as undefined is the component class. In our failing component, ɵcmp.dependencies was ["UNDEFINED", "TranslatePipe"] — the third-party pipe resolved, the barrel-imported component did not.
- Whole-suite dependent. Running the affected spec alone via --include passes; only the full multi-file run drops the module. The set of undefined exports also changes with import order — consistent with per-entry retention analysis rather than a fixed graph defect.
- Not specific to one entry point. A barrel probe placed in a different, CommonJS-free secondary entry returns the identical set of undefined exports. Whether a given spec fails depends on which barrel modules that entry's retention set happens to keep.
Please provide the exception or error you saw
TypeError: Cannot read properties of undefined (reading 'ɵcmp')
at getComponentDef (../packages/core/testing/src/test_bed_compiler.ts:1169:24)
at isStandaloneComponent (../packages/core/testing/src/test_bed_compiler.ts:1162:14)
at queueTypesFromModulesArrayRecur (../packages/core/testing/src/test_bed_compiler.ts:847:23)
at TestBedCompiler.queueTypesFromModulesArray (../packages/core/testing/src/test_bed_compiler.ts:861:4)
at TestBedCompiler.configureTestingModule (../packages/core/testing/src/test_bed_compiler.ts:226:11)
at TestBedImpl.configureTestingModule (../packages/core/testing/src/test_bed.ts:594:18)
Please provide the environment you discovered this bug in (run: ng version)
Angular CLI: 22.1.5
Node: 22.23.1
Package Manager: npm 10.x
OS: linux x64
Angular: 22.1.3
... animations, cdk, common, compiler, compiler-cli, core, forms,
platform-browser, router
Package Version
---------------------------------------------------------
@angular/build 22.1.5
@angular/cli 22.1.5
@angular-devkit/build-angular 22.1.3
ng-packagr 22.1.1
typescript 6.0.3
vitest (via @angular/build:unit-test)
Anything else?
Workaround that keeps published tree-shaking intact: instead of "sideEffects": false, use globs that match only the workspace source tree and nothing in the build output, e.g.
"sideEffects": ["./src/**", "./*/src/**"]
ng-packagr emits fesm2022/*.mjs plus per-entry package.json stubs, so these globs match zero emitted files. The published package therefore keeps effective side-effect-free semantics for consumers, while the workspace source — which is what the unit-test build reads through tsconfig paths — is treated as impure.
Related but distinct: #33467 (unit-test builder resolves include relative to sourceRoot, also a multi-entry-point blind spot) and #33893 (sideEffects: false interacting with optimizer pure annotations inside published FESMs — statement-level rather than whole-module).
Which @angular/* package(s) are the source of the bug?
build
Is this a regression?
Yes, this behaviour used to work in version 21.2.19
Description
In a multi-entry-point library built with ng-packagr, @angular/build:unit-test (Vitest + jsdom) elides module bodies from the primary entry point's export * barrel graph when the library's package.json declares "sideEffects": false. The namespace re-export accessors for those modules survive and return undefined.
When one of the dropped exports is a standalone component referenced from a retained imports: [...] array literal of another component, TestBed.configureTestingModule throws:
Dropping a module whose export is statically referenced from a retained array literal looks incorrect regardless of sideEffects, which is a module-level assertion about import-for-side-effect semantics — not a licence to remove a module that is still statically reachable.
In our library (761 barrel exports across 4 entry points) this fails 36 specs in 3 files, all in one secondary entry point. On Angular 21.2.19 the identical suite is 389/389 green.
Not a duplicate of #33728
#33728 describes the same symptom (an export from a shared chunk reading as undefined under the jsdom runner) and was fixed by #33729 "disable code splitting for unit test builds", released in v22.1.5.
That fix is installed and active here and the failure still reproduces. Verified in the installed package:
So this is a distinct cause: not cross-chunk lazy initialisers, but whole-module elision driven by sideEffects.
Please provide a link to a minimal reproduction of the bug
Unable to share the affected codebase. Reproduction recipe below — it is deterministic and the toggle is a single field.
Setup
Expected: spec passes.
Actual: TypeError: Cannot read properties of undefined (reading 'ɵcmp').
Toggle: change projects/lib/package.json to "sideEffects": true (or a glob matching the source tree, e.g. ["./src/**", "./*/src/**"]) → passes. Change it back → fails. Deterministic in both directions.
Diagnostics that pin the mechanism
These are the observations that distinguish elision from TDZ, resolution failure, or interop snapshotting:
Please provide the exception or error you saw
TypeError: Cannot read properties of undefined (reading 'ɵcmp') at getComponentDef (../packages/core/testing/src/test_bed_compiler.ts:1169:24) at isStandaloneComponent (../packages/core/testing/src/test_bed_compiler.ts:1162:14) at queueTypesFromModulesArrayRecur (../packages/core/testing/src/test_bed_compiler.ts:847:23) at TestBedCompiler.queueTypesFromModulesArray (../packages/core/testing/src/test_bed_compiler.ts:861:4) at TestBedCompiler.configureTestingModule (../packages/core/testing/src/test_bed_compiler.ts:226:11) at TestBedImpl.configureTestingModule (../packages/core/testing/src/test_bed.ts:594:18)Please provide the environment you discovered this bug in (run: ng version)
Angular CLI: 22.1.5 Node: 22.23.1 Package Manager: npm 10.x OS: linux x64 Angular: 22.1.3 ... animations, cdk, common, compiler, compiler-cli, core, forms, platform-browser, router Package Version --------------------------------------------------------- @angular/build 22.1.5 @angular/cli 22.1.5 @angular-devkit/build-angular 22.1.3 ng-packagr 22.1.1 typescript 6.0.3 vitest (via @angular/build:unit-test)Anything else?
Workaround that keeps published tree-shaking intact: instead of "sideEffects": false, use globs that match only the workspace source tree and nothing in the build output, e.g.
ng-packagr emits fesm2022/*.mjs plus per-entry package.json stubs, so these globs match zero emitted files. The published package therefore keeps effective side-effect-free semantics for consumers, while the workspace source — which is what the unit-test build reads through tsconfig paths — is treated as impure.
Related but distinct: #33467 (unit-test builder resolves include relative to sourceRoot, also a multi-entry-point blind spot) and #33893 (sideEffects: false interacting with optimizer pure annotations inside published FESMs — statement-level rather than whole-module).