Skip to content

unit-test builder: "sideEffects": false elides statically-referenced barrel modules, component reads as undefined in TestBed #33910

Description

@PatVandyke

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

  1. ng new repro --no-standalone=false then ng generate library lib, @angular/build >= 22.1.5, TypeScript 6.0.3, Node 22.
  2. Add "sideEffects": false to projects/lib/package.json.
  3. In the primary entry, create a standalone FooComponent and export it through a multi-level barrel: public-api.tsexport * 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.
  4. 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"].
  5. In the secondary entry, create a standalone BarComponent with imports: [FooComponent], importing it as import {FooComponent} from '@lib'.
  6. Give BarComponent a spec doing TestBed.configureTestingModule({imports: [BarComponent]}).
  7. 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:

  1. 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.
  2. 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.
  3. Not duplicate module identity. directImport === barrel.Foo is true.
  4. 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.
  5. 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.
  6. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions