Skip to content

fix(@angular-devkit/build-angular): downlevel class fields with Safari <= v15 for esbuild #24363

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 2 commits into from
Dec 2, 2022
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 @@ -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'],
Expand Down Expand Up @@ -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<string, boolean> = {
// 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[],
Expand Down
18 changes: 16 additions & 2 deletions packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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);
Expand Down