Conversation
The Vitest runner generates a virtual `angular:test-bed-init` module for the test bundle. When Zone.js is resolvable but is not one of the project's polyfills, that module imports `zone.js/testing` behind a top-level await, so that the import is skipped by a project which never loads Zone.js. That bundle is compiled against the targets derived from the project's Browserslist configuration, and esbuild has no downleveled form for top-level await. A project which targets a browser released before the syntax was available therefore fails to build its tests with `Top-level await is not available in the configured target environment`, before the runtime guard in the generated module can run. Browserslist describes the browsers an application is deployed to. A test bundle is never deployed: it is loaded by the test runner's module runner, or by a browser the runner launches, and every browser Angular supports has had top-level await since 2021. The unit-test builder now marks the feature as supported through an internal option, which applies to the test bundle alone and leaves application builds bound to their configured targets. Related to angular#33324
There was a problem hiding this comment.
Code Review
This pull request introduces a new internal option, supportTopLevelAwait, to allow marking top-level await as supported in browser code bundles regardless of the target browsers. This is primarily used for test bundles (such as those run by Vitest) where the test runner itself supports top-level await, preventing esbuild from failing the build when targeting older browsers. The option is integrated into the build options normalization, the Vitest runner, and the esbuild feature support utility, and is accompanied by a new behavior test. I have no feedback to provide as there are no review comments.
|
Thank you for looking into this issue and opening the PR. However, the approach of enabling top level await in the bundle options is not the right direction. Native async/await cannot be used when zone.js is loaded because native async/await microtasks bypass Zone.js execution context tracking. To support Zone.js, esbuild must downlevel async/await. Because top level await cannot be downleveled, it is fundamentally incompatible with Zone.js, and esbuild will reject top level await whenever async/await is disabled, regardless of whether top level await is marked as supported. The actual root cause is in getZoneTestingStrategy(). When an application is zoneless (polyfills is defined, for example [], without zone.js), resolving zone.js in node_modules causes this function to fall through and return 'dynamic'. In a zoneless application, Zone is never defined at runtime, so this import is never executed. However, the syntactic presence of top level await forces the bundle to require top level await support in esbuild. For zoneless applications, this should likely return 'none' instead. I do not recall why dynamic was originally added here in the first place. @clydin would have more info on why it was introduced. Closing this PR in favor of addressing the strategy in |
|
@alan-agius4 thank you. i've investigate a bit.
So returning 'none' for a zoneless app fixes the reported case but not other two. Running each polyfills shape against 22.1.8:
That last one carries no design decision: static imports there are semantically identical and drops await. I can send that as a separate PR if it is useful. Worth knowing about the coverage: |
|
Thanks for the additional information now I remember why! I do have an idea how we can address this and will try to open a PR shortly. I will also address the two top-level awaits. |
PR Checklist
Please check to confirm your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: #33324
The Vitest runner generates a virtual
angular:test-bed-initmodule. When Zone.js is resolvable but is not one of the project's polyfills, that module importszone.js/testingbehind a top-level await, so the import is skipped by a project that never loads Zone.js.The test bundle is compiled against the targets derived from the project's Browserslist configuration, and esbuild has no downleveled form for top-level await. A project that targets a browser released before the syntax was available therefore fails to build its tests with
Top-level await is not available in the configured target environment, before the runtime guard in the generated module can run.What is the new behavior?
The unit-test builder marks top-level await as supported for the test bundle, through an internal option that applies to the browser code bundle of that build alone.
Browserslist describes the browsers an application is deployed to. A test bundle is never deployed: it is loaded by the test runner's module runner, or by a browser the runner launches. Top-level await has been available in Chrome and Edge 89, Firefox 89, and Safari and iOS 15 since 2021, so it is supported by every browser within Angular's support window, browser mode included. Application builds keep their configured targets and still reject the syntax for a target that lacks it.
Does this PR introduce a breaking change?
Other information
Regression test:
packages/angular/build/src/builders/unit-test/tests/behavior/vitest-top-level-await_spec.ts. It fails onmainwith the esbuild error above and passes with this change.The exact targets in the issue —
chrome145.0,ios17.0,safari17.0— build onmain: all three support the syntax in the pinned esbuild 0.28.2 (and in 0.28.0, which 22.0.0 shipped), so the regression test reaches the same code path through an older Browserslist target instead.One shape this does not cover, in case someone reports it on the same issue: when the project's polyfills are a local file,
isZonelessApp()returns false,async-awaitis set tofalse, and esbuild then rejects top-level await for every target — the error reads+ 6 overridesrather than+ 2. Nosupportedentry helps there, since esbuild has no downleveled form to fall back to; that one needs the generated module to stop using the syntax, which changes TestBed initialization ordering rather than build options.