From 5df577b64134c8bd40ef6406627fd0b1fb82e010 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 24 May 2022 14:30:56 +0000 Subject: [PATCH] fix(@angular-devkit/build-angular): downlevel libraries based on the browserslist configurations There is no standard for library authors to ship their library in different ES versions, which can result in vendor libraries to ship ES features which are not supported by one or more browsers that the user's application supports. With this change, we will be downlevelling libraries based on the list of supported browsers which is configured in the browserslist configuration. Previously, we only downlevelled libraries when targeting ES5. The TypeScript target option will not effect how the libraries get downlevelled. Closes #23126 --- .../src/babel/presets/application.ts | 8 +- .../build_angular/src/babel/webpack-loader.ts | 53 +++++++++---- .../tests/behavior/typescript-target_spec.ts | 79 +++++++++++++++---- .../build_localize_replaced_watch_spec.ts | 2 +- .../src/webpack/configs/common.ts | 1 + .../src/webpack/plugins/typescript.ts | 8 ++ 6 files changed, 117 insertions(+), 34 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/babel/presets/application.ts b/packages/angular_devkit/build_angular/src/babel/presets/application.ts index 234f8fa7052c..ca1a6caf960d 100644 --- a/packages/angular_devkit/build_angular/src/babel/presets/application.ts +++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts @@ -47,7 +47,7 @@ export interface ApplicationPresetOptions { linkerPluginCreator: typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin; }; - forceES5?: boolean; + forcePresetEnv?: boolean; forceAsyncTransformation?: boolean; instrumentCode?: { includedBasePath: string; @@ -59,6 +59,7 @@ export interface ApplicationPresetOptions { wrapDecorators: boolean; }; + supportedBrowsers?: string[]; diagnosticReporter?: DiagnosticReporter; } @@ -178,14 +179,13 @@ export default function (api: unknown, options: ApplicationPresetOptions) { ); } - if (options.forceES5) { + if (options.forcePresetEnv) { presets.push([ require('@babel/preset-env').default, { bugfixes: true, modules: false, - // Comparable behavior to tsconfig target of ES5 - targets: { ie: 9 }, + targets: options.supportedBrowsers, exclude: ['transform-typeof-symbol'], }, ]); diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts index a19f499141b9..aa2845fd666e 100644 --- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -72,18 +72,26 @@ export default custom(() => { return { async customOptions(options, { source, map }) { - const { i18n, scriptTarget, aot, optimize, instrumentCode, ...rawOptions } = - options as AngularBabelLoaderOptions; + const { + i18n, + scriptTarget, + aot, + optimize, + instrumentCode, + supportedBrowsers, + ...rawOptions + } = options as AngularBabelLoaderOptions; // Must process file if plugins are added let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0; const customOptions: ApplicationPresetOptions = { forceAsyncTransformation: false, - forceES5: false, + forcePresetEnv: false, angularLinker: undefined, i18n: undefined, instrumentCode: undefined, + supportedBrowsers, }; // Analyze file for linking @@ -107,20 +115,35 @@ export default custom(() => { // Analyze for ES target processing const esTarget = scriptTarget as ScriptTarget | undefined; - if (esTarget !== undefined) { - if (esTarget < ScriptTarget.ES2015) { - customOptions.forceES5 = true; - } else if (esTarget >= ScriptTarget.ES2017 || /\.[cm]?js$/.test(this.resourcePath)) { - // Application code (TS files) will only contain native async if target is ES2017+. - // However, third-party libraries can regardless of the target option. - // APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and - // will not have native async. - customOptions.forceAsyncTransformation = - !/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async'); - } - shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5 || false; + const isJsFile = /\.[cm]?js$/.test(this.resourcePath); + + // The below should be dropped when we no longer support ES5 TypeScript output. + if (esTarget === ScriptTarget.ES5) { + // This is needed because when target is ES5 we change the TypeScript target to ES2015 + // because it simplifies build-optimization passes. + // @see https://github.com/angular/angular-cli/blob/22af6520834171d01413d4c7e4a9f13fb752252e/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts#L51-L56 + customOptions.forcePresetEnv = true; + // Comparable behavior to tsconfig target of ES5 + customOptions.supportedBrowsers = ['IE 9']; + } else if (isJsFile) { + // Applications code ES version can be controlled using TypeScript's `target` option. + // However, this doesn't effect libraries and hence we use preset-env to downlevel ES fetaures + // based on the supported browsers in browserlist. + customOptions.forcePresetEnv = true; } + if ((esTarget !== undefined && esTarget >= ScriptTarget.ES2017) || isJsFile) { + // Application code (TS files) will only contain native async if target is ES2017+. + // However, third-party libraries can regardless of the target option. + // APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and + // will not have native async. + customOptions.forceAsyncTransformation = + !/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async'); + } + + shouldProcess ||= + customOptions.forceAsyncTransformation || customOptions.forcePresetEnv || false; + // Analyze for i18n inlining if ( i18n && diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts index 538d30a40aa4..68cd9304c2f8 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import { logging } from '@angular-devkit/core'; import { buildWebpackBrowser } from '../../index'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; @@ -141,12 +142,12 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { await harness.writeFile( 'src/main.ts', ` - (async () => { - for await (const o of [1, 2, 3]) { - console.log("for await...of"); - } - })(); - `, + (async () => { + for await (const o of [1, 2, 3]) { + console.log("for await...of"); + } + })(); + `, ); harness.useTarget('build', { @@ -176,14 +177,14 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { await harness.writeFile( 'src/es2015-syntax.js', ` - class foo { - bar() { - console.log('baz'); - } - } - - (new foo()).bar(); - `, + class foo { + bar() { + console.log('baz'); + } + } + + (new foo()).bar(); + `, ); harness.useTarget('build', { @@ -198,5 +199,55 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); }); + + it('a deprecation warning should be issued when targetting ES5', async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + tsconfig.compilerOptions.target = 'es5'; + + return JSON.stringify(tsconfig); + }); + await harness.writeFiles({ + 'src/tsconfig.worker.json': `{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/worker", + "lib": [ + "es2018", + "webworker" + ], + "types": [] + }, + "include": [ + "**/*.worker.ts", + ] + }`, + 'src/app/app.worker.ts': ` + /// + + const prefix: string = 'Data: '; + addEventListener('message', ({ data }) => { + postMessage(prefix + data); + }); + `, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + webWorkerTsConfig: 'src/tsconfig.worker.json', + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const deprecationMessages = logs.filter(({ message }) => + message.startsWith('DEPRECATED: ES5 output is deprecated'), + ); + + expect(deprecationMessages).toHaveSize(1); + }); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts index 5ada84dcc828..cc6981723e5a 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts @@ -55,7 +55,7 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => { const buildCount = await harness .execute() .pipe( - timeout(BUILD_TIMEOUT), + timeout(BUILD_TIMEOUT * 2), concatMap(async ({ result }, index) => { expect(result?.success).toBe(true); diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index d81636c09d45..2c33d03bae47 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -378,6 +378,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise = {};