Repository navigation
unit-test builder: "sideEffects": false elides statically-referenced barrel modules, component reads as undefined in TestBed #33910
Description
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:238setsdisableCodeSplitting: 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=falsethenng generate library lib,@angular/build>= 22.1.5, TypeScript 6.0.3, Node 22.- Add
"sideEffects": falsetoprojects/lib/package.json. - In the primary entry, create a standalone
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. - Add a secondary entry point (
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"]. - In the secondary entry, create a standalone
BarComponentwithimports: [FooComponent], importing it asimport {FooComponent} from '@lib'. - Give
BarComponenta spec doingTestBed.configureTestingModule({imports: [BarComponent]}). - Configure a test target with
@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.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 = 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. - Not TDZ. On
import * as barrel from '@lib', the affected names are present as ACCESSOR property descriptors and still returnundefinedafter both a microtask and a macrotask tick. - Not duplicate module identity.
directImport === barrel.Fooistrue. - Only classes are affected in practice. Plain functions exported from the same barrel in the same import statement resolve correctly; the value read as
undefinedis the component class. In our failing component,ɵcmp.dependencieswas["UNDEFINED", "TranslatePipe"]— the third-party pipe resolved, the barrel-imported component did not. - Whole-suite dependent. Running the affected spec alone via
--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. - Not specific to one entry point. A barrel probe placed in a different, CommonJS-free secondary entry returns the identical set of
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
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).
Activity
fix(@angular/build): ignore side-effect annotations in unit test builds
fix(@angular/build): ignore side-effect annotations in unit test builds
fix(@angular/build): ignore side-effect annotations in unit test builds