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

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

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