diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts index 53159f520fb2..2508c1279cff 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts @@ -253,19 +253,7 @@ function createCodeBundleOptions( entryNames: outputNames.bundles, assetNames: outputNames.media, target, - supported: { - // Native async/await is not supported with Zone.js. Disabling support here will cause - // esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild - // does not currently support downleveling async generators. Instead babel is used within the JS/TS - // loader to perform the downlevel transformation. - // NOTE: If esbuild adds support in the future, the babel support for async generators can be disabled. - 'async-await': false, - // 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, - }, + supported: getFeatureSupport(target), mainFields: ['es2020', 'browser', 'module', 'main'], conditions: ['es2020', 'es2015', 'module'], resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'], @@ -314,6 +302,57 @@ function createCodeBundleOptions( }; } +/** + * 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. + * @returns An object that can be used with the esbuild build `supported` option. + */ +function getFeatureSupport(target: string[]): BuildOptions['supported'] { + const supported: Record = { + // Native async/await is not supported with Zone.js. Disabling support here will cause + // esbuild to downlevel async/await and for await...of to a Zone.js supported form. However, esbuild + // does not currently support downleveling async generators. Instead babel is used within the JS/TS + // loader to perform the downlevel transformation. + // NOTE: If esbuild adds support in the future, the babel support for async generators can be disabled. + 'async-await': false, + // 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, + }; + + // Detect Safari browser versions that have a class field behavior bug + // See: https://github.com/angular/angular-cli/issues/24355#issuecomment-1333477033 + // See: https://github.com/WebKit/WebKit/commit/e8788a34b3d5f5b4edd7ff6450b80936bff396f2 + let safariClassFieldScopeBug = false; + for (const browser of target) { + let majorVersion; + if (browser.startsWith('ios')) { + majorVersion = Number(browser.slice(3, 5)); + } else if (browser.startsWith('safari')) { + majorVersion = Number(browser.slice(6, 8)); + } else { + continue; + } + // Technically, 14.0 is not broken but rather does not have support. However, the behavior + // is identical since it would be set to false by esbuild if present as a target. + if (majorVersion === 14 || majorVersion === 15) { + safariClassFieldScopeBug = true; + break; + } + } + // If class field support cannot be used set to false; otherwise leave undefined to allow + // esbuild to use `target` to determine support. + if (safariClassFieldScopeBug) { + supported['class-field'] = false; + supported['class-static-field'] = false; + } + + return supported; +} + function createGlobalStylesBundleOptions( options: NormalizedBrowserOptions, target: string[], diff --git a/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts b/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts index 276adf234690..1147738e93a9 100644 --- a/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts +++ b/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts @@ -14,10 +14,19 @@ export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]) const transformed: string[] = []; // https://esbuild.github.io/api/#target - const esBuildSupportedBrowsers = new Set(['safari', 'firefox', 'edge', 'chrome', 'ios', 'node']); + const esBuildSupportedBrowsers = new Set([ + 'chrome', + 'edge', + 'firefox', + 'ie', + 'ios', + 'node', + 'opera', + 'safari', + ]); for (const browser of supportedBrowsers) { - let [browserName, version] = browser.split(' '); + let [browserName, version] = browser.toLowerCase().split(' '); // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` if (browserName === 'ios_saf') { @@ -33,6 +42,11 @@ export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]) // esbuild only supports numeric versions so `TP` is converted to a high number (999) since // a Technology Preview (TP) of Safari is assumed to support all currently known features. version = '999'; + } else if (!version.includes('.')) { + // A lone major version is considered by esbuild to include all minor versions. However, + // browserslist does not and is also inconsistent in its `.0` version naming. For example, + // Safari 15.0 is named `safari 15` but Safari 16.0 is named `safari 16.0`. + version += '.0'; } transformed.push(browserName + version);