Skip to content

fix(@angular/build): avoid top-level await for Zone.js injection in Vitest runner - #34150

Merged
alan-agius4 merged 1 commit into
angular:mainfrom
alan-agius4:fix-vitest-zoneless
Sep 24, 2026
Merged

alan-agius4 merged 1 commit into
angular:mainfrom
alan-agius4:fix-vitest-zoneless

Conversation

@alan-agius4

@alan-agius4 alan-agius4 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Previously, the Vitest unit-test runner used dynamic import strategies ('dynamic' and 'dynamic-zone') within the generated TestBed initialization virtual file (createTestBedInitVirtualFile) to load zone.js and zone.js/testing.

This logic was fundamentally flawed:

  1. When Zone.js was loaded dynamically at runtime via the 'dynamic' strategy, esbuild did not downlevel async/await because isZonelessApp checked the build target's polyfills configuration (which did not explicitly list zone.js). Consequently, native async/await microtasks bypassed Zone.js context tracking, breaking Zone.js at runtime.
  2. If a project used a local polyfills file (e.g. polyfills: ["src/polyfills.ts"]), isZonelessApp considered the application zoneful and disabled async-await support in esbuild. However, esbuild cannot downlevel top-level await when async/await is downleveled, causing esbuild to reject top-level await unconditionally. Hence, the 'dynamic' strategy never worked as intended.
  3. For zoneless applications (such as polyfills: []), the syntactic presence of top-level await in the virtual file caused esbuild builds to fail when targeting older browsers or Browserslist targets that lack top-level await support, even though Zone was never present at runtime.

Issue Number: Fixes #33324

What is the new behavior?

  • Eliminates top-level await import() from createTestBedInitVirtualFile entirely.
  • Inverts the polyfill strategy so that zone.js and zone.js/testing are injected directly into buildOptions.polyfills before bundling based on the configured polyfills option (from the test target or inherited from the build target).
  • For library targets where polyfills is undefined, zone.js and zone.js/testing are injected if zone.js is installed, accompanied by a deprecation warning advising users to configure the polyfills option in their test configuration ([] for zoneless projects or ["zone.js"] for Zone.js projects).

Does this PR introduce a breaking change?

  • Yes
  • No

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new 'zoneless' option for the Vitest unit-test runner in Angular, allowing developers to explicitly configure whether tests should run in zoneless mode. It refactors the Zone.js testing initialization logic by injecting the necessary polyfills directly into the build options and deprecates the automatic injection of Zone.js based on dependency detection. The review feedback suggests improving robustness by adding a fallback empty array when initializing the polyfills Set and updating the deprecation warning message to provide clearer guidance for library projects that lack a polyfills configuration option.

Comment thread packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts Outdated
Comment thread packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts Outdated
@alan-agius4
alan-agius4 force-pushed the fix-vitest-zoneless branch 2 times, most recently from d64f95d to 3ebcbbf Compare September 23, 2026 13:07
@alan-agius4 alan-agius4 added the target: patch This PR is targeted for the next patch release label Sep 23, 2026
@alan-agius4
alan-agius4 force-pushed the fix-vitest-zoneless branch 3 times, most recently from 920587f to 887c542 Compare September 23, 2026 13:20
@thekhegay

Copy link
Copy Markdown
Contributor

@alan-agius4
ran your branch on installed @angular/build 22.1.8, patched by hand. few polyfills shapes, default browserslist and firefox 88. before on ff88 only [zone.js] and [zone.js + local file] built, rest died on top-level await. now all build and generated module has no await. zoneless true/false work as you describe.

two cases still broken:

  • ["src/polyfills.ts"] where that file does import 'zone.js'. builds now, Zone is there at runtime, but zone.js/testing is not added, so fakeAsync fails with "zone-testing.js is needed for the fakeAsync() test helper but could not be found". with polyfills: ["zone.js"] same test gives "Expected to be running in 'ProxyZone'", so there testing part is loaded. zoneless: false fixes it. looks like case 2 from your description.

  • ["zone.js/testing"] alone. function returns polyfills untouched so zone.js itself never loads, test dies with ReferenceError: Zone is not defined at zone-testing.js:1989. same before and after your change.

@alan-agius4

Copy link
Copy Markdown
Collaborator Author

zone.js/testing should never be added to the application's polyfills as test utilities should not be included in app production bundles.

@thekhegay

Copy link
Copy Markdown
Contributor

my bad, out of scope for this pr.
place it comes from is karma: library schematic writes polyfills: ["zone.js", "zone.js/testing"] into karma test target (@schematics/angular/library), so people migrating karma to vitest move that array over

@alan-agius4
alan-agius4 requested a review from clydin September 23, 2026 13:50
@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Sep 23, 2026
@alan-agius4 alan-agius4 changed the title fix(@angular/build): avoid top-level await and add zoneless option for Vitest runner fix(@angular/build): avoid top-level await for Zone.js injection in Vitest runner Sep 24, 2026
@alan-agius4
alan-agius4 force-pushed the fix-vitest-zoneless branch 2 times, most recently from 935f6cc to 41f0666 Compare September 24, 2026 07:53
…itest runner

Previously, the Vitest unit-test runner used dynamic import strategies (`'dynamic'` and `'dynamic-zone'`) within the generated TestBed initialization virtual file (`createTestBedInitVirtualFile`) to load `zone.js` and `zone.js/testing`.

This logic was fundamentally flawed:
1. When Zone.js was loaded dynamically at runtime via the `'dynamic'` strategy, esbuild did not downlevel `async`/`await` because `isZonelessApp` checked the build target's `polyfills` configuration (which did not explicitly list `zone.js`). Consequently, native async/await microtasks bypassed Zone.js context tracking, breaking Zone.js at runtime.
2. If a project used a local polyfills file (e.g. `polyfills: ["src/polyfills.ts"]`), `isZonelessApp` considered the application zoneful and disabled `async-await` support in esbuild. However, esbuild cannot downlevel top-level await when async/await is downleveled, causing esbuild to reject top-level await unconditionally. Hence, the `'dynamic'` strategy never worked as intended.
3. For zoneless applications (such as `polyfills: []`), the syntactic presence of top-level `await` in the virtual file caused esbuild builds to fail when targeting older browsers or Browserslist targets that lack top-level await support, even though Zone was never present at runtime.

This commit resolves these issues by:
- Eliminating top-level `await import()` from `createTestBedInitVirtualFile` entirely.
- Inverting the polyfill strategy so that `zone.js` and `zone.js/testing` are injected directly into `buildOptions.polyfills` before bundling based on the configured `polyfills` option (from the `test` target or inherited from the `build` target).
- For library targets where `polyfills` is undefined, `zone.js` and `zone.js/testing` are injected if `zone.js` is installed, accompanied by a deprecation warning advising users to configure the `polyfills` option in their test configuration (`[]` for zoneless projects or `["zone.js"]` for Zone.js projects).

Fixes angular#33324
@alan-agius4 alan-agius4 added action: merge The PR is ready for merge by the caretaker target: minor This PR is targeted for the next minor release and removed action: review The PR is still awaiting reviews from at least one requested reviewer target: patch This PR is targeted for the next patch release labels Sep 24, 2026
@alan-agius4
alan-agius4 merged commit d09f98e into angular:main Sep 24, 2026
40 of 41 checks passed
@alan-agius4

Copy link
Copy Markdown
Collaborator Author

This PR was merged into the repository. The changes were merged into the following branches:

@alan-agius4
alan-agius4 deleted the fix-vitest-zoneless branch September 24, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: @angular/build target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants