diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/browser-support_spec.ts index 49597d3fdbda..b7710fb5b94a 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/browser-support_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/browser-support_spec.ts @@ -44,7 +44,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.expectFile('dist/browser/main.js.map').content.toContain('Promise'); }); - 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', @@ -53,6 +53,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.useTarget('build', { ...BASE_OPTIONS, + polyfills: ['zone.js'], }); const { result } = await harness.executeOnce(); @@ -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 { console.log("from-async-function"); }\ntest();', + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: [], + }); + + 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', @@ -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', @@ -104,6 +124,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { harness.useTarget('build', { ...BASE_OPTIONS, + polyfills: ['zone.js'], }); const { result } = await harness.executeOnce(); @@ -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"'); + }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts index a34bbb7cfd4a..94f258fb88d7 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts @@ -17,7 +17,11 @@ import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundle import { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result'; import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer'; import { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin'; -import { getFeatureSupport, transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils'; +import { + getFeatureSupport, + isZonelessApp, + transformSupportedBrowsersToTargets, +} from '../../tools/esbuild/utils'; import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin'; import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin'; import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils'; @@ -248,6 +252,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( @@ -259,6 +266,7 @@ export async function* serveWithVite( !!browserOptions.ssr, prebundleTransformer, target, + isZonelessApp(polyfills), browserOptions.loader as EsbuildLoaderOption | undefined, extensions?.middleware, transformers?.indexHtml, @@ -443,6 +451,7 @@ export async function setupServer( ssr: boolean, prebundleTransformer: JavaScriptTransformer, target: string[], + zoneless: boolean, prebundleLoaderExtensions: EsbuildLoaderOption | undefined, extensionMiddleware?: Connect.NextHandleFunction[], indexHtmlTransformer?: (content: string) => Promise, @@ -540,6 +549,7 @@ export async function setupServer( include: externalMetadata.implicitServer, ssr: true, prebundleTransformer, + zoneless, target, loader: prebundleLoaderExtensions, thirdPartySourcemaps, @@ -570,6 +580,7 @@ export async function setupServer( ssr: false, prebundleTransformer, target, + zoneless, loader: prebundleLoaderExtensions, thirdPartySourcemaps, }), @@ -605,6 +616,7 @@ function getDepOptimizationConfig({ exclude, include, target, + zoneless, prebundleTransformer, ssr, loader, @@ -616,6 +628,7 @@ function getDepOptimizationConfig({ target: string[]; prebundleTransformer: JavaScriptTransformer; ssr: boolean; + zoneless: boolean; loader?: EsbuildLoaderOption; thirdPartySourcemaps: boolean; }): DepOptimizationConfig { @@ -650,7 +663,7 @@ function getDepOptimizationConfig({ esbuildOptions: { // Set esbuild supported targets. target, - supported: getFeatureSupport(target), + supported: getFeatureSupport(target, zoneless), plugins, loader, }, diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts index a107cd16b38b..94f64dcdea21 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts @@ -21,7 +21,7 @@ import { createExternalPackagesPlugin } from './external-packages-plugin'; import { createAngularLocaleDataPlugin } from './i18n-locale-plugin'; import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin'; import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin'; -import { getFeatureSupport } from './utils'; +import { getFeatureSupport, isZonelessApp } from './utils'; import { createVirtualModulePlugin } from './virtual-module-plugin'; export function createBrowserCodeBundleOptions( @@ -29,7 +29,7 @@ export function createBrowserCodeBundleOptions( target: string[], sourceFileCache?: SourceFileCache, ): BuildOptions { - const { entryPoints, outputNames } = options; + const { entryPoints, outputNames, polyfills } = options; const { pluginOptions, styleOptions } = createCompilerPluginOptions( options, @@ -48,7 +48,7 @@ export function createBrowserCodeBundleOptions( entryNames: outputNames.bundles, entryPoints, target, - supported: getFeatureSupport(target), + supported: getFeatureSupport(target, isZonelessApp(polyfills)), plugins: [ createSourcemapIgnorelistPlugin(), createCompilerPlugin( @@ -154,8 +154,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, @@ -195,7 +202,7 @@ export function createServerCodeBundleOptions( js: `import './polyfills.server.mjs';`, }, entryPoints, - supported: getFeatureSupport(target), + supported: getFeatureSupport(target, isZonelessApp(polyfills)), plugins: [ createSourcemapIgnorelistPlugin(), createCompilerPlugin( @@ -260,27 +267,26 @@ export function createServerPolyfillBundleOptions( target: string[], sourceFileCache?: SourceFileCache, ): BundlerOptionsFactory | undefined { - const polyfills: string[] = []; + const serverPolyfills: string[] = []; const polyfillsFromConfig = new Set(options.polyfills); - - if (polyfillsFromConfig.has('zone.js')) { - polyfills.push('zone.js/node'); + if (!isZonelessApp(options.polyfills)) { + serverPolyfills.push('zone.js/node'); } if ( polyfillsFromConfig.has('@angular/localize') || polyfillsFromConfig.has('@angular/localize/init') ) { - polyfills.push('@angular/localize/init'); + serverPolyfills.push('@angular/localize/init'); } - polyfills.push('@angular/platform-server/init'); + serverPolyfills.push('@angular/platform-server/init'); const namespace = 'angular:polyfills-server'; const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions( { ...options, - polyfills, + polyfills: serverPolyfills, }, namespace, false, diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts index 09cfa6a3d04d..94ebfd4ede8f 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts @@ -170,18 +170,24 @@ export async function withNoProgress(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 = { // 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. // For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536 'object-rest-spread': false, + // Using top-level-await is not guaranteed to be safe with some code optimizations. + 'top-level-await': false, }; // Detect Safari browser versions that have a class field behavior bug @@ -479,3 +485,14 @@ export async function logMessages( logger.error((await formatMessages(errors, { kind: 'error', color })).join('\n')); } } + +/** + * Ascertain whether the application operates without `zone.js`, we currently rely on the polyfills setting to determine its status. + * If a file with an extension is provided or if `zone.js` is included in the polyfills, the application is deemed as not zoneless. + * @param polyfills An array of polyfills + * @returns true, when the application is considered as zoneless. + */ +export function isZonelessApp(polyfills: string[] | undefined): boolean { + // TODO: Instead, we should rely on the presence of zone.js in the polyfills build metadata. + return !polyfills?.some((p) => p === 'zone.js' || /\.[mc]?[jt]s$/.test(p)); +}