Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ interface InternalOptions {
*/
disableCodeSplitting?: boolean;

/**
* Marks top-level await as supported by the browser code bundle regardless of the browsers the
* project targets.
*
* The test bundle contains a generated TestBed initializer that imports `zone.js/testing` behind
* a top-level await so that the import is skipped when Zone.js is not loaded. esbuild rejects the
* syntax outright when the project's Browserslist configuration resolves to a browser released
* before top-level await was available, so the build fails before the guard can run.
*
* That configuration describes the browsers an application is deployed to. A test bundle is only
* ever 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.
*
* Used exclusively for tests and shouldn't be used for other kinds of builds.
*/
supportTopLevelAwait?: boolean;

/**
* An array of files to restrict the TypeScript compilation root names to.
*/
Expand Down Expand Up @@ -463,6 +480,7 @@ export async function normalizeOptions(
externalRuntimeStyles,
instrumentForCoverage,
disableCodeSplitting,
supportTopLevelAwait,
} = options;

// Return all the normalized options
Expand Down Expand Up @@ -500,6 +518,7 @@ export async function normalizeOptions(
workspaceRoot,
entryPoints,
disableCodeSplitting,
supportTopLevelAwait,
rootFiles: rootFiles?.map((file: string) => path.resolve(workspaceRoot, file)),
optimizationOptions,
outputOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ export async function getVitestBuildOptions(
// live ESM bindings across chunk boundaries. This can cause uninitialized exports or break mocking.
// Disabling code splitting avoids shared chunks, but increases build and coverage memory/time.
disableCodeSplitting: !options.splitting,
// The generated TestBed initializer can import 'zone.js/testing' behind a top-level await.
// The test bundle is loaded by the test runner, not by the browsers the project targets.
supportTopLevelAwait: true,
// Enable support for vitest browser prebundling. Excludes can be controlled with a runnerConfig
// and the `optimizeDeps.exclude` option.
externalPackages: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { execute } from '../../index';
import {
BASE_OPTIONS,
describeBuilder,
UNIT_TEST_BUILDER_INFO,
setupApplicationTarget,
} from '../setup';

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Behavior: "Vitest top-level await"', () => {
it('builds the test bundle when the project targets browsers without top-level await', async () => {
// Zone.js is not a polyfill but is resolvable as a transitive dependency, so the generated
// TestBed initializer imports 'zone.js/testing' behind a top-level await.
setupApplicationTarget(harness, { polyfills: [] });

await harness.writeFile('.browserslistrc', 'Chrome 88');

harness.useTarget('test', {
...BASE_OPTIONS,
});

await harness.writeFile(
'src/app/app.component.spec.ts',
`
import { describe, it, expect } from 'vitest';

describe('Top-level await', () => {
it('runs', () => {
expect(true).toBe(true);
});
});
`,
);

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function createBrowserCodeBundleOptions(
entryNames: outputNames.bundles,
entryPoints,
target,
supported: getFeatureSupport(zoneless),
supported: getFeatureSupport(zoneless, options.supportTopLevelAwait),
};

if (options.disableCodeSplitting) {
Expand Down
9 changes: 8 additions & 1 deletion packages/angular/build/src/tools/esbuild/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,17 @@ export async function withNoProgress<T>(text: string, action: () => T | Promise<
* Generates a syntax feature object map for Angular applications.
* A full set of feature names can be found here: https://esbuild.github.io/api/#supported
* @param nativeAsyncAwait Indicate whether to support native async/await.
* @param topLevelAwait Indicate whether to support top-level await regardless of the target.
* @returns An object that can be used with the esbuild build `supported` option.
*/
export function getFeatureSupport(nativeAsyncAwait: boolean): BuildOptions['supported'] {
export function getFeatureSupport(
nativeAsyncAwait: boolean,
topLevelAwait = false,
): BuildOptions['supported'] {
return {
// Top-level await has no downleveled form, so esbuild rejects it outright for a target without
// it. Only enabled for bundles that are not deployed to the browsers a project targets.
...(topLevelAwait ? { 'top-level-await': true } : {}),
// Native async/await is not supported with Zone.js. Disabling support here will cause
// esbuild to downlevel async/await, async generators, and for await...of to a Zone.js supported form.
'async-await': nativeAsyncAwait,
Expand Down
Loading