Skip to content

fix(vite): lower async/await for zone.js like the Angular CLI - #642

Open
edbzn wants to merge 5 commits into
voidzero-dev:mainfrom
edbzn:fix/vite-lower-async
Open

edbzn wants to merge 5 commits into
voidzero-dev:mainfrom
edbzn:fix/vite-lower-async

Conversation

@edbzn

@edbzn edbzn commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #641.

Stacked on #607. The first three commits (a1a0cc5, dd27b7f, 6a5b004) are #607's, unchanged. This PR adds a59e630 and a merge of main, which resolves a one-line conflict next to vendorSourceMap. Please merge #607 first. I'll rebase this PR after that.

Problem

@oxc-angular/vite leaves async/await native. zone.js can't track a native await, so fakeAsync with tick()/flushMicrotasks() can't drive code that awaits, and change detection after an await doesn't run as a zone task. The Angular CLI lowers async to generators when zone.js is in the polyfills (esbuild supported: { 'async-await': false }, getFeatureSupport in @angular/build).

Fix

A new sub-plugin, @oxc-angular/vite-lower-async, runs #607's transformForBrowser with lowerAsync: true on application scripts that contain async (not node_modules).

  • Why feat(napi): transformForBrowser - TypeScript with Angular's settings, async lowered for zone.js #607 and not a config hook: Vite 8 transforms with Oxc. Oxc's target lowers by ES version, with no per-feature switch like esbuild's supported. The Angular CLI's own Vite dev server hits the same limit and falls back to es2016 for dependencies. That would also lower spread, optional chaining and class fields. esbuild.supported needs esbuild, and esbuild's inline var __async helper breaks under vi.mock hoisting (see vite: async/await is not lowered for zone.js, so fakeAsync/tick cannot control awaits #641).
  • Order: the hook is a transform with order: 'post', so it sees code after TypeScript is stripped. It passes that code as JavaScript, so only async is lowered and class fields are left alone. It also runs after Vitest hoists vi.mock(). If the lowering ran first, an async mock factory would become an eager _asyncToGenerator(...) call, and hoisting would move it above its helper's import: Cannot access '__vi_import_1__' before initialization.
  • Helpers are imported from @oxc-project/runtime. Vite 8 resolves them without the package being installed: its builtin oxc-runtime plugin covers dev and Vitest, and Rolldown inlines them in builds. No new dependency.
  • When: the existing zoneless option was unused. It now controls the lowering. When it is unset, an app is zoneless unless node_modules/zone.js exists above the Vite root. That replaces the CLI's signal, zone.js in the polyfills, which a Vite app doesn't declare. Server (SSR) transforms stay native, as feat(napi): transformForBrowser - TypeScript with Angular's settings, async lowered for zone.js #607's lowerAsync: false intends. Note that the CLI does lower its server bundle for zone apps.
  • Not covered: dependencies (node_modules and pre-bundled deps), which the CLI also lowers.

Checks

  • New test/lower-async.test.ts (6 tests):
    • async is lowered;
    • a class field stays a field;
    • async stays native when zoneless, and in SSR transforms;
    • the default follows whether zone.js is installed;
    • a nested Vitest run in a client environment, with an async vi.mock factory. Without order: 'post' it fails with Cannot access '__vi_import_1__' before initialization. Without the lowering, it fails its not.toContain('async') assertion.
  • Checked by hand, not committed because zone.js isn't a dependency of this package: the fakeAsync + flushMicrotasks() repro from vite: async/await is not lowered for zone.js, so fakeAsync/tick cannot control awaits #641 under Vitest with zone.js 0.16.3. It passes with the lowering. With zoneless: true it fails with expected "vi.fn()" to be called at least once.
  • Checked by hand: a vite build of a module with an async method and an async generator. The helpers are inlined from Rolldown's runtime and no async is left. An SSR build keeps it native.
  • On the branch (merged with main):
    • vitest run --dir ./test: 21 files, 387 tests pass.
    • tsc -p tsconfig.json --noEmit is clean.
    • oxfmt --check and oxlint --type-aware --type-check are clean on the changed files.

🤖 Generated with Claude Code

Rush and others added 5 commits October 21, 2026 20:13
zone.js cannot track native `await`, so `fakeAsync` with `tick()` or
`flushMicrotasks()` could not drive code that awaits. Unless the app is
zoneless, lower `async`, async generators and `for await` in application
code with `transformForBrowser`, as the Angular CLI does for apps with
zone.js in their polyfills. `zoneless` defaults to whether zone.js is
installed; server transforms stay native.

The transform runs after Vitest hoists `vi.mock()`, so an async mock
factory, now an eager `_asyncToGenerator(...)` call, still finds its
helper import.

Fixes voidzero-dev#641

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolves the conflict with main's vendorSourceMap option next to the removed zoneless default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

downleveling is maybe the term you're looking instead of "lower"

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vite: async/await is not lowered for zone.js, so fakeAsync/tick cannot control awaits

3 participants