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'sexport *barrel graph when the library'spackage.jsondeclares"sideEffects": false. The namespace re-export accessors for those modules survive and returnundefined.When one of the dropped exports is a standalone component referenced from a retained
imports: [...]array literal of another component,TestBed.configureTestingModulethrows: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
undefinedunder 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:238setsdisableCodeSplitting: truesrc/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=falsethenng generate library lib,@angular/build>= 22.1.5, TypeScript 6.0.3, Node 22."sideEffects": falsetoprojects/lib/package.json.FooComponentand 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 ofexport *matter; a single flat barrel did not reproduce for us.projects/lib/sub/with its ownng-package.jsonandpublic-api.ts), mapped in the workspacetsconfig.jsonpathsto its sourcepublic-api.ts, alongside"@lib": ["./projects/lib/src/public-api.ts"].BarComponentwithimports: [FooComponent], importing it asimport {FooComponent} from '@lib'.BarComponenta spec doingTestBed.configureTestingModule({imports: [BarComponent]}).@angular/build:unit-test,buildTargetpointing at the library build, andincludecovering 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.jsonto"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:
(globalThis as any).__RAN = trueat the top of an affected module leaves__RANundefinedat 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.import * as barrel from '@lib', the affected names are present as ACCESSOR property descriptors and still returnundefinedafter both a microtask and a macrotask tick.directImport === barrel.Fooistrue.undefinedis the component class. In our failing component,ɵcmp.dependencieswas["UNDEFINED", "TranslatePipe"]— the third-party pipe resolved, the barrel-imported component did not.--includepasses; only the full multi-file run drops the module. The set ofundefinedexports also changes with import order — consistent with per-entry retention analysis rather than a fixed graph defect.undefinedexports. 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
Please provide the environment you discovered this bug in (run: ng version)
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/*.mjsplus per-entrypackage.jsonstubs, 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 tsconfigpaths— is treated as impure.Related but distinct: #33467 (unit-test builder resolves
includerelative tosourceRoot, also a multi-entry-point blind spot) and #33893 (sideEffects: falseinteracting with optimizer pure annotations inside published FESMs — statement-level rather than whole-module).