Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): support native async/await when …
Browse files Browse the repository at this point in the history
…app is zoneless

This commit updates the esbuild based builders to emit native async/await when `zone.js` is not added as a polyfill.

Closes angular#22191
  • Loading branch information
alan-agius4 committed Apr 17, 2024
1 parent 6313036 commit 50a1bbe
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/main.js.map').content.toContain('Promise<Void123>');
});

it('downlevels async functions ', async () => {
it('downlevels async functions when zone.js is included as a polyfill', async () => {
// Add an async function to the project
await harness.writeFile(
'src/main.ts',
Expand All @@ -53,6 +53,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {

harness.useTarget('build', {
...BASE_OPTIONS,
polyfills: ['zone.js'],
});

const { result } = await harness.executeOnce();
Expand All @@ -62,6 +63,25 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/main.js').content.toContain('"from-async-function"');
});

it('does not downlevels async functions when zone.js is not included as a polyfill', async () => {
// Add an async function to the project
await harness.writeFile(
'src/main.ts',
'async function test(): Promise<void> { console.log("from-async-function"); }\ntest();',
);

harness.useTarget('build', {
...BASE_OPTIONS,
polyfills: ['zone.js'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toMatch(/\sasync\s/);
harness.expectFile('dist/browser/main.js').content.toContain('"from-async-function"');
});

it('warns when IE is present in browserslist', async () => {
await harness.appendToFile(
'.browserslistrc',
Expand Down Expand Up @@ -89,7 +109,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
);
});

it('downlevels "for await...of"', async () => {
it('downlevels "for await...of" when zone.js is included as a polyfill', async () => {
// Add an async function to the project
await harness.writeFile(
'src/main.ts',
Expand All @@ -104,6 +124,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {

harness.useTarget('build', {
...BASE_OPTIONS,
polyfills: ['zone.js'],
});

const { result } = await harness.executeOnce();
Expand All @@ -112,5 +133,30 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/main.js').content.not.toMatch(/\sawait\s/);
harness.expectFile('dist/browser/main.js').content.toContain('"for await...of"');
});

it('does not downlevel "for await...of" when zone.js is not included as a polyfill', async () => {
// Add an async function to the project
await harness.writeFile(
'src/main.ts',
`
(async () => {
for await (const o of [1, 2, 3]) {
console.log("for await...of");
}
})();
`,
);

harness.useTarget('build', {
...BASE_OPTIONS,
polyfills: [],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toMatch(/\sawait\s/);
harness.expectFile('dist/browser/main.js').content.toContain('"for await...of"');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ export async function* serveWithVite(
const projectRoot = join(context.workspaceRoot, root as string);
const browsers = getSupportedBrowsers(projectRoot, context.logger);
const target = transformSupportedBrowsersToTargets(browsers);
const polyfills = Array.isArray(browserOptions.polyfills)
? browserOptions.polyfills
: [browserOptions.polyfills];

// Setup server and start listening
const serverConfiguration = await setupServer(
Expand All @@ -259,6 +262,7 @@ export async function* serveWithVite(
!!browserOptions.ssr,
prebundleTransformer,
target,
!polyfills.some((p) => p === 'zone.js'),
browserOptions.loader as EsbuildLoaderOption | undefined,
extensions?.middleware,
transformers?.indexHtml,
Expand Down Expand Up @@ -443,6 +447,7 @@ export async function setupServer(
ssr: boolean,
prebundleTransformer: JavaScriptTransformer,
target: string[],
zoneless: boolean,
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
extensionMiddleware?: Connect.NextHandleFunction[],
indexHtmlTransformer?: (content: string) => Promise<string>,
Expand Down Expand Up @@ -540,6 +545,7 @@ export async function setupServer(
include: externalMetadata.implicitServer,
ssr: true,
prebundleTransformer,
zoneless,
target,
loader: prebundleLoaderExtensions,
thirdPartySourcemaps,
Expand Down Expand Up @@ -570,6 +576,7 @@ export async function setupServer(
ssr: false,
prebundleTransformer,
target,
zoneless,
loader: prebundleLoaderExtensions,
thirdPartySourcemaps,
}),
Expand Down Expand Up @@ -605,6 +612,7 @@ function getDepOptimizationConfig({
exclude,
include,
target,
zoneless,
prebundleTransformer,
ssr,
loader,
Expand All @@ -616,6 +624,7 @@ function getDepOptimizationConfig({
target: string[];
prebundleTransformer: JavaScriptTransformer;
ssr: boolean;
zoneless: boolean;
loader?: EsbuildLoaderOption;
thirdPartySourcemaps: boolean;
}): DepOptimizationConfig {
Expand Down Expand Up @@ -650,7 +659,7 @@ function getDepOptimizationConfig({
esbuildOptions: {
// Set esbuild supported targets.
target,
supported: getFeatureSupport(target),
supported: getFeatureSupport(target, zoneless),
plugins,
loader,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ export function createBrowserCodeBundleOptions(
target: string[],
sourceFileCache?: SourceFileCache,
): BuildOptions {
const { entryPoints, outputNames } = options;
const { entryPoints, outputNames, polyfills } = options;

const { pluginOptions, styleOptions } = createCompilerPluginOptions(
options,
target,
sourceFileCache,
);

const isZoneless = !polyfills?.some((p) => p === 'zone.js');
const buildOptions: BuildOptions = {
...getEsBuildCommonOptions(options),
platform: 'browser',
Expand All @@ -48,7 +49,7 @@ export function createBrowserCodeBundleOptions(
entryNames: outputNames.bundles,
entryPoints,
target,
supported: getFeatureSupport(target),
supported: getFeatureSupport(target, isZoneless),
plugins: [
createSourcemapIgnorelistPlugin(),
createCompilerPlugin(
Expand Down Expand Up @@ -154,8 +155,15 @@ export function createServerCodeBundleOptions(
target: string[],
sourceFileCache: SourceFileCache,
): BuildOptions {
const { serverEntryPoint, workspaceRoot, ssrOptions, watch, externalPackages, prerenderOptions } =
options;
const {
serverEntryPoint,
workspaceRoot,
ssrOptions,
watch,
externalPackages,
prerenderOptions,
polyfills,
} = options;

assert(
serverEntryPoint,
Expand All @@ -179,6 +187,7 @@ export function createServerCodeBundleOptions(
entryPoints['server'] = ssrEntryPoint;
}

const isZoneless = !polyfills?.some((p) => p === 'zone.js');
const buildOptions: BuildOptions = {
...getEsBuildCommonOptions(options),
platform: 'node',
Expand All @@ -195,7 +204,7 @@ export function createServerCodeBundleOptions(
js: `import './polyfills.server.mjs';`,
},
entryPoints,
supported: getFeatureSupport(target),
supported: getFeatureSupport(target, isZoneless),
plugins: [
createSourcemapIgnorelistPlugin(),
createCompilerPlugin(
Expand Down Expand Up @@ -262,7 +271,6 @@ export function createServerPolyfillBundleOptions(
): BundlerOptionsFactory | undefined {
const polyfills: string[] = [];
const polyfillsFromConfig = new Set(options.polyfills);

if (polyfillsFromConfig.has('zone.js')) {
polyfills.push('zone.js/node');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,17 @@ export async function withNoProgress<T>(text: string, action: () => T | Promise<
* Generates a syntax feature object map for Angular applications based on a list of targets.
* A full set of feature names can be found here: https://esbuild.github.io/api/#supported
* @param target An array of browser/engine targets in the format accepted by the esbuild `target` option.
* @param nativeAsyncAwait Indicate whether to support native async/await.
* @returns An object that can be used with the esbuild build `supported` option.
*/
export function getFeatureSupport(target: string[]): BuildOptions['supported'] {
export function getFeatureSupport(
target: string[],
nativeAsyncAwait: boolean,
): BuildOptions['supported'] {
const supported: Record<string, boolean> = {
// 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': false,
'async-await': nativeAsyncAwait,
// V8 currently has a performance defect involving object spread operations that can cause signficant
// degradation in runtime performance. By not supporting the language feature here, a downlevel form
// will be used instead which provides a workaround for the performance issue.
Expand Down

0 comments on commit 50a1bbe

Please sign in to comment.