Skip to content

feat(@angular-devkit/build-angular): support native async/await when app is zoneless #27486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: [],
});

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 @@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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));
}