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 #22191
  • Loading branch information
alan-agius4 committed Apr 18, 2024
1 parent 0155afc commit d1c632a
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 19 deletions.
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: [],
});

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"');
});
});
});
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand All @@ -259,6 +266,7 @@ export async function* serveWithVite(
!!browserOptions.ssr,
prebundleTransformer,
target,
isZonelessApp(polyfills),
browserOptions.loader as EsbuildLoaderOption | undefined,
extensions?.middleware,
transformers?.indexHtml,
Expand Down Expand Up @@ -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<string>,
Expand Down Expand Up @@ -540,6 +549,7 @@ export async function setupServer(
include: externalMetadata.implicitServer,
ssr: true,
prebundleTransformer,
zoneless,
target,
loader: prebundleLoaderExtensions,
thirdPartySourcemaps,
Expand Down Expand Up @@ -570,6 +580,7 @@ export async function setupServer(
ssr: false,
prebundleTransformer,
target,
zoneless,
loader: prebundleLoaderExtensions,
thirdPartySourcemaps,
}),
Expand Down Expand Up @@ -605,6 +616,7 @@ function getDepOptimizationConfig({
exclude,
include,
target,
zoneless,
prebundleTransformer,
ssr,
loader,
Expand All @@ -616,6 +628,7 @@ function getDepOptimizationConfig({
target: string[];
prebundleTransformer: JavaScriptTransformer;
ssr: boolean;
zoneless: boolean;
loader?: EsbuildLoaderOption;
thirdPartySourcemaps: boolean;
}): DepOptimizationConfig {
Expand Down Expand Up @@ -650,7 +663,7 @@ function getDepOptimizationConfig({
esbuildOptions: {
// Set esbuild supported targets.
target,
supported: getFeatureSupport(target),
supported: getFeatureSupport(target, zoneless),
plugins,
loader,
},
Expand Down
Expand Up @@ -21,15 +21,15 @@ 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(
options: NormalizedApplicationBuildOptions,
target: string[],
sourceFileCache?: SourceFileCache,
): BuildOptions {
const { entryPoints, outputNames } = options;
const { entryPoints, outputNames, polyfills } = options;

const { pluginOptions, styleOptions } = createCompilerPluginOptions(
options,
Expand All @@ -48,7 +48,7 @@ export function createBrowserCodeBundleOptions(
entryNames: outputNames.bundles,
entryPoints,
target,
supported: getFeatureSupport(target),
supported: getFeatureSupport(target, isZonelessApp(polyfills)),
plugins: [
createSourcemapIgnorelistPlugin(),
createCompilerPlugin(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -195,7 +202,7 @@ export function createServerCodeBundleOptions(
js: `import './polyfills.server.mjs';`,
},
entryPoints,
supported: getFeatureSupport(target),
supported: getFeatureSupport(target, isZonelessApp(polyfills)),
plugins: [
createSourcemapIgnorelistPlugin(),
createCompilerPlugin(
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 19 additions & 2 deletions packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts
Expand Up @@ -170,18 +170,24 @@ 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.
// 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
Expand Down Expand Up @@ -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));
}

0 comments on commit d1c632a

Please sign in to comment.