Skip to content

Commit d1c632a

Browse files
committedApr 18, 2024·
feat(@angular-devkit/build-angular): support native async/await when 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 #22191
1 parent 0155afc commit d1c632a

File tree

4 files changed

+101
-19
lines changed

4 files changed

+101
-19
lines changed
 

Diff for: ‎packages/angular_devkit/build_angular/src/builders/application/tests/behavior/browser-support_spec.ts

+48-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
4444
harness.expectFile('dist/browser/main.js.map').content.toContain('Promise<Void123>');
4545
});
4646

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

5454
harness.useTarget('build', {
5555
...BASE_OPTIONS,
56+
polyfills: ['zone.js'],
5657
});
5758

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

66+
it('does not downlevels async functions when zone.js is not included as a polyfill', async () => {
67+
// Add an async function to the project
68+
await harness.writeFile(
69+
'src/main.ts',
70+
'async function test(): Promise<void> { console.log("from-async-function"); }\ntest();',
71+
);
72+
73+
harness.useTarget('build', {
74+
...BASE_OPTIONS,
75+
polyfills: [],
76+
});
77+
78+
const { result } = await harness.executeOnce();
79+
80+
expect(result?.success).toBe(true);
81+
harness.expectFile('dist/browser/main.js').content.toMatch(/\sasync\s/);
82+
harness.expectFile('dist/browser/main.js').content.toContain('"from-async-function"');
83+
});
84+
6585
it('warns when IE is present in browserslist', async () => {
6686
await harness.appendToFile(
6787
'.browserslistrc',
@@ -89,7 +109,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
89109
);
90110
});
91111

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

105125
harness.useTarget('build', {
106126
...BASE_OPTIONS,
127+
polyfills: ['zone.js'],
107128
});
108129

109130
const { result } = await harness.executeOnce();
@@ -112,5 +133,30 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
112133
harness.expectFile('dist/browser/main.js').content.not.toMatch(/\sawait\s/);
113134
harness.expectFile('dist/browser/main.js').content.toContain('"for await...of"');
114135
});
136+
137+
it('does not downlevel "for await...of" when zone.js is not included as a polyfill', async () => {
138+
// Add an async function to the project
139+
await harness.writeFile(
140+
'src/main.ts',
141+
`
142+
(async () => {
143+
for await (const o of [1, 2, 3]) {
144+
console.log("for await...of");
145+
}
146+
})();
147+
`,
148+
);
149+
150+
harness.useTarget('build', {
151+
...BASE_OPTIONS,
152+
polyfills: [],
153+
});
154+
155+
const { result } = await harness.executeOnce();
156+
157+
expect(result?.success).toBe(true);
158+
harness.expectFile('dist/browser/main.js').content.toMatch(/\sawait\s/);
159+
harness.expectFile('dist/browser/main.js').content.toContain('"for await...of"');
160+
});
115161
});
116162
});

Diff for: ‎packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundle
1717
import { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result';
1818
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
1919
import { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
20-
import { getFeatureSupport, transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils';
20+
import {
21+
getFeatureSupport,
22+
isZonelessApp,
23+
transformSupportedBrowsersToTargets,
24+
} from '../../tools/esbuild/utils';
2125
import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin';
2226
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
2327
import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
@@ -248,6 +252,9 @@ export async function* serveWithVite(
248252
const projectRoot = join(context.workspaceRoot, root as string);
249253
const browsers = getSupportedBrowsers(projectRoot, context.logger);
250254
const target = transformSupportedBrowsersToTargets(browsers);
255+
const polyfills = Array.isArray((browserOptions.polyfills ??= []))
256+
? browserOptions.polyfills
257+
: [browserOptions.polyfills];
251258

252259
// Setup server and start listening
253260
const serverConfiguration = await setupServer(
@@ -259,6 +266,7 @@ export async function* serveWithVite(
259266
!!browserOptions.ssr,
260267
prebundleTransformer,
261268
target,
269+
isZonelessApp(polyfills),
262270
browserOptions.loader as EsbuildLoaderOption | undefined,
263271
extensions?.middleware,
264272
transformers?.indexHtml,
@@ -443,6 +451,7 @@ export async function setupServer(
443451
ssr: boolean,
444452
prebundleTransformer: JavaScriptTransformer,
445453
target: string[],
454+
zoneless: boolean,
446455
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
447456
extensionMiddleware?: Connect.NextHandleFunction[],
448457
indexHtmlTransformer?: (content: string) => Promise<string>,
@@ -540,6 +549,7 @@ export async function setupServer(
540549
include: externalMetadata.implicitServer,
541550
ssr: true,
542551
prebundleTransformer,
552+
zoneless,
543553
target,
544554
loader: prebundleLoaderExtensions,
545555
thirdPartySourcemaps,
@@ -570,6 +580,7 @@ export async function setupServer(
570580
ssr: false,
571581
prebundleTransformer,
572582
target,
583+
zoneless,
573584
loader: prebundleLoaderExtensions,
574585
thirdPartySourcemaps,
575586
}),
@@ -605,6 +616,7 @@ function getDepOptimizationConfig({
605616
exclude,
606617
include,
607618
target,
619+
zoneless,
608620
prebundleTransformer,
609621
ssr,
610622
loader,
@@ -616,6 +628,7 @@ function getDepOptimizationConfig({
616628
target: string[];
617629
prebundleTransformer: JavaScriptTransformer;
618630
ssr: boolean;
631+
zoneless: boolean;
619632
loader?: EsbuildLoaderOption;
620633
thirdPartySourcemaps: boolean;
621634
}): DepOptimizationConfig {
@@ -650,7 +663,7 @@ function getDepOptimizationConfig({
650663
esbuildOptions: {
651664
// Set esbuild supported targets.
652665
target,
653-
supported: getFeatureSupport(target),
666+
supported: getFeatureSupport(target, zoneless),
654667
plugins,
655668
loader,
656669
},

Diff for: ‎packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

+19-13
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import { createExternalPackagesPlugin } from './external-packages-plugin';
2121
import { createAngularLocaleDataPlugin } from './i18n-locale-plugin';
2222
import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin';
2323
import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin';
24-
import { getFeatureSupport } from './utils';
24+
import { getFeatureSupport, isZonelessApp } from './utils';
2525
import { createVirtualModulePlugin } from './virtual-module-plugin';
2626

2727
export function createBrowserCodeBundleOptions(
2828
options: NormalizedApplicationBuildOptions,
2929
target: string[],
3030
sourceFileCache?: SourceFileCache,
3131
): BuildOptions {
32-
const { entryPoints, outputNames } = options;
32+
const { entryPoints, outputNames, polyfills } = options;
3333

3434
const { pluginOptions, styleOptions } = createCompilerPluginOptions(
3535
options,
@@ -48,7 +48,7 @@ export function createBrowserCodeBundleOptions(
4848
entryNames: outputNames.bundles,
4949
entryPoints,
5050
target,
51-
supported: getFeatureSupport(target),
51+
supported: getFeatureSupport(target, isZonelessApp(polyfills)),
5252
plugins: [
5353
createSourcemapIgnorelistPlugin(),
5454
createCompilerPlugin(
@@ -154,8 +154,15 @@ export function createServerCodeBundleOptions(
154154
target: string[],
155155
sourceFileCache: SourceFileCache,
156156
): BuildOptions {
157-
const { serverEntryPoint, workspaceRoot, ssrOptions, watch, externalPackages, prerenderOptions } =
158-
options;
157+
const {
158+
serverEntryPoint,
159+
workspaceRoot,
160+
ssrOptions,
161+
watch,
162+
externalPackages,
163+
prerenderOptions,
164+
polyfills,
165+
} = options;
159166

160167
assert(
161168
serverEntryPoint,
@@ -195,7 +202,7 @@ export function createServerCodeBundleOptions(
195202
js: `import './polyfills.server.mjs';`,
196203
},
197204
entryPoints,
198-
supported: getFeatureSupport(target),
205+
supported: getFeatureSupport(target, isZonelessApp(polyfills)),
199206
plugins: [
200207
createSourcemapIgnorelistPlugin(),
201208
createCompilerPlugin(
@@ -260,27 +267,26 @@ export function createServerPolyfillBundleOptions(
260267
target: string[],
261268
sourceFileCache?: SourceFileCache,
262269
): BundlerOptionsFactory | undefined {
263-
const polyfills: string[] = [];
270+
const serverPolyfills: string[] = [];
264271
const polyfillsFromConfig = new Set(options.polyfills);
265-
266-
if (polyfillsFromConfig.has('zone.js')) {
267-
polyfills.push('zone.js/node');
272+
if (!isZonelessApp(options.polyfills)) {
273+
serverPolyfills.push('zone.js/node');
268274
}
269275

270276
if (
271277
polyfillsFromConfig.has('@angular/localize') ||
272278
polyfillsFromConfig.has('@angular/localize/init')
273279
) {
274-
polyfills.push('@angular/localize/init');
280+
serverPolyfills.push('@angular/localize/init');
275281
}
276282

277-
polyfills.push('@angular/platform-server/init');
283+
serverPolyfills.push('@angular/platform-server/init');
278284

279285
const namespace = 'angular:polyfills-server';
280286
const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions(
281287
{
282288
...options,
283-
polyfills,
289+
polyfills: serverPolyfills,
284290
},
285291
namespace,
286292
false,

Diff for: ‎packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,24 @@ export async function withNoProgress<T>(text: string, action: () => T | Promise<
170170
* Generates a syntax feature object map for Angular applications based on a list of targets.
171171
* A full set of feature names can be found here: https://esbuild.github.io/api/#supported
172172
* @param target An array of browser/engine targets in the format accepted by the esbuild `target` option.
173+
* @param nativeAsyncAwait Indicate whether to support native async/await.
173174
* @returns An object that can be used with the esbuild build `supported` option.
174175
*/
175-
export function getFeatureSupport(target: string[]): BuildOptions['supported'] {
176+
export function getFeatureSupport(
177+
target: string[],
178+
nativeAsyncAwait: boolean,
179+
): BuildOptions['supported'] {
176180
const supported: Record<string, boolean> = {
177181
// Native async/await is not supported with Zone.js. Disabling support here will cause
178182
// esbuild to downlevel async/await, async generators, and for await...of to a Zone.js supported form.
179-
'async-await': false,
183+
'async-await': nativeAsyncAwait,
180184
// V8 currently has a performance defect involving object spread operations that can cause signficant
181185
// degradation in runtime performance. By not supporting the language feature here, a downlevel form
182186
// will be used instead which provides a workaround for the performance issue.
183187
// For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536
184188
'object-rest-spread': false,
189+
// Using top-level-await is not guaranteed to be safe with some code optimizations.
190+
'top-level-await': false,
185191
};
186192

187193
// Detect Safari browser versions that have a class field behavior bug
@@ -479,3 +485,14 @@ export async function logMessages(
479485
logger.error((await formatMessages(errors, { kind: 'error', color })).join('\n'));
480486
}
481487
}
488+
489+
/**
490+
* Ascertain whether the application operates without `zone.js`, we currently rely on the polyfills setting to determine its status.
491+
* If a file with an extension is provided or if `zone.js` is included in the polyfills, the application is deemed as not zoneless.
492+
* @param polyfills An array of polyfills
493+
* @returns true, when the application is considered as zoneless.
494+
*/
495+
export function isZonelessApp(polyfills: string[] | undefined): boolean {
496+
// TODO: Instead, we should rely on the presence of zone.js in the polyfills build metadata.
497+
return !polyfills?.some((p) => p === 'zone.js' || /\.[mc]?[jt]s$/.test(p));
498+
}

0 commit comments

Comments
 (0)
Please sign in to comment.