Skip to content

Commit 81fcdd7

Browse files
committed
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 angular#22191
1 parent 6313036 commit 81fcdd7

File tree

4 files changed

+80
-11
lines changed

4 files changed

+80
-11
lines changed

packages/angular_devkit/build_angular/src/builders/application/tests/behavior/browser-support_spec.ts

Lines changed: 48 additions & 2 deletions
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
});

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ export async function* serveWithVite(
248248
const projectRoot = join(context.workspaceRoot, root as string);
249249
const browsers = getSupportedBrowsers(projectRoot, context.logger);
250250
const target = transformSupportedBrowsersToTargets(browsers);
251+
const polyfills = Array.isArray(browserOptions.polyfills)
252+
? browserOptions.polyfills
253+
: [browserOptions.polyfills];
251254

252255
// Setup server and start listening
253256
const serverConfiguration = await setupServer(
@@ -259,6 +262,7 @@ export async function* serveWithVite(
259262
!!browserOptions.ssr,
260263
prebundleTransformer,
261264
target,
265+
!polyfills.some((p) => p === 'zone.js'),
262266
browserOptions.loader as EsbuildLoaderOption | undefined,
263267
extensions?.middleware,
264268
transformers?.indexHtml,
@@ -443,6 +447,7 @@ export async function setupServer(
443447
ssr: boolean,
444448
prebundleTransformer: JavaScriptTransformer,
445449
target: string[],
450+
zoneless: boolean,
446451
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
447452
extensionMiddleware?: Connect.NextHandleFunction[],
448453
indexHtmlTransformer?: (content: string) => Promise<string>,
@@ -540,6 +545,7 @@ export async function setupServer(
540545
include: externalMetadata.implicitServer,
541546
ssr: true,
542547
prebundleTransformer,
548+
zoneless,
543549
target,
544550
loader: prebundleLoaderExtensions,
545551
thirdPartySourcemaps,
@@ -570,6 +576,7 @@ export async function setupServer(
570576
ssr: false,
571577
prebundleTransformer,
572578
target,
579+
zoneless,
573580
loader: prebundleLoaderExtensions,
574581
thirdPartySourcemaps,
575582
}),
@@ -605,6 +612,7 @@ function getDepOptimizationConfig({
605612
exclude,
606613
include,
607614
target,
615+
zoneless,
608616
prebundleTransformer,
609617
ssr,
610618
loader,
@@ -616,6 +624,7 @@ function getDepOptimizationConfig({
616624
target: string[];
617625
prebundleTransformer: JavaScriptTransformer;
618626
ssr: boolean;
627+
zoneless: boolean;
619628
loader?: EsbuildLoaderOption;
620629
thirdPartySourcemaps: boolean;
621630
}): DepOptimizationConfig {
@@ -650,7 +659,7 @@ function getDepOptimizationConfig({
650659
esbuildOptions: {
651660
// Set esbuild supported targets.
652661
target,
653-
supported: getFeatureSupport(target),
662+
supported: getFeatureSupport(target, zoneless),
654663
plugins,
655664
loader,
656665
},

packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ export function createBrowserCodeBundleOptions(
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,
3636
target,
3737
sourceFileCache,
3838
);
3939

40+
const isZoneless = !polyfills?.some((p) => p === 'zone.js');
4041
const buildOptions: BuildOptions = {
4142
...getEsBuildCommonOptions(options),
4243
platform: 'browser',
@@ -48,7 +49,7 @@ export function createBrowserCodeBundleOptions(
4849
entryNames: outputNames.bundles,
4950
entryPoints,
5051
target,
51-
supported: getFeatureSupport(target),
52+
supported: getFeatureSupport(target, isZoneless),
5253
plugins: [
5354
createSourcemapIgnorelistPlugin(),
5455
createCompilerPlugin(
@@ -154,8 +155,15 @@ export function createServerCodeBundleOptions(
154155
target: string[],
155156
sourceFileCache: SourceFileCache,
156157
): BuildOptions {
157-
const { serverEntryPoint, workspaceRoot, ssrOptions, watch, externalPackages, prerenderOptions } =
158-
options;
158+
const {
159+
serverEntryPoint,
160+
workspaceRoot,
161+
ssrOptions,
162+
watch,
163+
externalPackages,
164+
prerenderOptions,
165+
polyfills,
166+
} = options;
159167

160168
assert(
161169
serverEntryPoint,
@@ -179,6 +187,7 @@ export function createServerCodeBundleOptions(
179187
entryPoints['server'] = ssrEntryPoint;
180188
}
181189

190+
const isZoneless = !polyfills?.some((p) => p === 'zone.js');
182191
const buildOptions: BuildOptions = {
183192
...getEsBuildCommonOptions(options),
184193
platform: 'node',
@@ -195,7 +204,7 @@ export function createServerCodeBundleOptions(
195204
js: `import './polyfills.server.mjs';`,
196205
},
197206
entryPoints,
198-
supported: getFeatureSupport(target),
207+
supported: getFeatureSupport(target, isZoneless),
199208
plugins: [
200209
createSourcemapIgnorelistPlugin(),
201210
createCompilerPlugin(
@@ -262,7 +271,6 @@ export function createServerPolyfillBundleOptions(
262271
): BundlerOptionsFactory | undefined {
263272
const polyfills: string[] = [];
264273
const polyfillsFromConfig = new Set(options.polyfills);
265-
266274
if (polyfillsFromConfig.has('zone.js')) {
267275
polyfills.push('zone.js/node');
268276
}

packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts

Lines changed: 8 additions & 2 deletions
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

0 commit comments

Comments
 (0)