Skip to content

@angular/build:unit-test (vitest): zoneless project fails to compile with "Top-level await is not available" when zone.js is a transitive dep #33324

@rLamonAC

Description

@rLamonAC

Command

test

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

21.2.13

Description

Description

A zoneless Angular project (provideZonelessChangeDetection() in main.ts, no zone.js in the polyfills array) fails to compile its unit-test bundle with:
X [ERROR] Top-level await is not available in the configured target environment ("chrome145.0", "ios17.0", "safari17.0" + 2 overrides)

Root cause

In @angular/build/src/builders/unit-test/runners/vitest/build-options.js, getZoneTestingStrategy() decides what to emit into the virtual angular:test-bed-init module:

function getZoneTestingStrategy(buildOptions, projectSourceRoot) {
  if (buildOptions.polyfills?.includes('zone.js/testing')) return 'none';
  if (buildOptions.polyfills?.includes('zone.js'))         return 'static';
  try {
    projectRequire.resolve('zone.js');
    return 'dynamic';                       // ← falls through to here for zoneless projects
  } catch { return 'none'; }
}

For a zoneless app, polyfills correctly contains neither 'zone.js' nor 'zone.js/testing' — but zone.js is still resolvable on disk because it's an optional/peer dep of @angular/core (and several Angular test utilities like fakeAsync / tick still reference it). The strategy therefore resolves to 'dynamic', which emits this into the virtual module:

if (typeof Zone !== 'undefined') {
  // 'zone.js/testing' is used to initialize the ZoneJS testing environment.
  // It must be imported dynamically to avoid a static dependency on 'zone.js'.
  await import('zone.js/testing');
}

The runtime guard typeof Zone !== 'undefined' would correctly skip the import on a zoneless app, but esbuild evaluates the top-level await at compile time and rejects the syntax — regardless of whether the branch can actually run. The build fails before the guard ever executes.

Expected

A zoneless project should resolve the strategy to 'none' and emit no zone-init code. The unit-test bundle should compile cleanly.

Actual

Strategy resolves to 'dynamic' because zone.js is transitively installed. The virtual angular:test-bed-init module contains a top-level await, which esbuild rejects against the configured browser targets — even though those targets (Chrome 145, iOS 17, Safari 17) actually do support TLA. The compile-time rejection appears to be tied to the output format guarantees rather than the targets themselves, so tightening browserslist does not help.

Minimal Reproduction

  1. Generate a zoneless Angular 22 app (or upgrade an existing zoneless app).
  2. Confirm main.ts uses provideZonelessChangeDetection() and polyfills in angular.json does not include 'zone.js' or 'zone.js/testing'.
  3. Configure test to use @angular/build:unit-test with the vitest runner.
  4. Run ng test.

Exception or Error

Top-level await is not available

Your Environment

_                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI       : 22.0.0
Angular           : 22.0.0
Node.js           : 24.16.0
Package Manager   : npm 11.13.0
Operating System  : win32 x64

┌────────────────────────────────────┬───────────────────┬───────────────────┐
│ Package                            │ Installed Version │ Requested Version │
├────────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular-devkit/build-angular      │ 22.0.0            │ 22.0.0            │
│ @angular/animations                │ 22.0.0            │ 22.0.0            │
│ @angular/build                     │ 22.0.0            │ 22.0.0            │
│ @angular/cdk                       │ 21.2.13           │ 21.2.13           │
│ @angular/cli                       │ 22.0.0            │ 22.0.0            │
│ @angular/common                    │ 22.0.0            │ 22.0.0            │
│ @angular/compiler                  │ 22.0.0            │ 22.0.0            │
│ @angular/compiler-cli              │ 22.0.0            │ 22.0.0            │
│ @angular/core                      │ 22.0.0            │ 22.0.0            │
│ @angular/forms                     │ 22.0.0            │ 22.0.0            │
│ @angular/google-maps               │ 21.2.13           │ 21.2.13           │
│ @angular/language-service          │ 22.0.0            │ 22.0.0            │
│ @angular/material                  │ 21.2.13           │ 21.2.13           │
│ @angular/material-date-fns-adapter │ 21.2.13           │ 21.2.13           │
│ @angular/platform-browser          │ 22.0.0            │ 22.0.0            │
│ @angular/platform-browser-dynamic  │ 22.0.0            │ 22.0.0            │
│ @angular/platform-server           │ 22.0.0            │ 22.0.0            │
│ @angular/router                    │ 22.0.0            │ 22.0.0            │
│ @angular/service-worker            │ 22.0.0            │ 22.0.0            │
│ @angular/ssr                       │ 22.0.0            │ 22.0.0            │
│ rxjs                               │ 7.8.2             │ 7.8.2             │
│ typescript                         │ 6.0.3             │ 6.0.3             │
│ vitest                             │ 4.1.7             │ 4.1.7             │

Anything else relevant?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions