From 8e3a161c76962acf378f11f59cb850f13980d90c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 8 Jan 2021 15:42:56 -0500 Subject: [PATCH] feat(@angular-devkit/build-angular): support targeting ES2017 with Zone.js This change causes native async functions to be downleveled when an application targets ES2017 within its TypeScript configuration. Any source file that contains the async keyword will be processed including libraries. Since Zone.js does not support native async, this processing allows Zone.js to function with an ES2017 target. --- .../angular_devkit/build_angular/package.json | 3 +- .../src/babel/presets/application.ts | 1 - .../build_angular/src/babel/webpack-loader.ts | 15 ++- .../tests/behavior/typescript-target_spec.ts | 93 +++++++++++++++++++ .../src/webpack/configs/common.ts | 2 +- .../src/webpack/configs/typescript.ts | 2 + yarn.lock | 2 +- 7 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 725a359dab1d..522e98c6606d 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -13,7 +13,8 @@ "@angular-devkit/core": "0.0.0", "@babel/core": "7.12.10", "@babel/generator": "7.12.11", - "@babel/plugin-transform-runtime": "7.12.10", + "@babel/plugin-transform-async-to-generator": "7.12.1", + "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", "@babel/runtime": "7.12.5", "@babel/template": "7.12.7", 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 f0e12ec93a9e..bd5ffcc6b52f 100644 --- a/packages/angular_devkit/build_angular/src/babel/presets/application.ts +++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts @@ -170,7 +170,6 @@ export default function (api: unknown, options: ApplicationPresetOptions) { if (options.forceAsyncTransformation) { // Always transform async/await to support Zone.js - // tslint:disable-next-line: no-implicit-dependencies plugins.push(require('@babel/plugin-transform-async-to-generator').default); needRuntimeTransform = true; } 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 e18d81ed4d2d..56187e719a9e 100644 --- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -9,6 +9,7 @@ import { custom } from 'babel-loader'; import { ScriptTarget } from 'typescript'; interface AngularCustomOptions { + forceAsyncTransformation: boolean; forceES5: boolean; shouldLink: boolean; } @@ -27,7 +28,8 @@ async function checkLinking( source: string, ): Promise<{ hasLinkerSupport?: boolean; requiresLinking: boolean }> { // @angular/core and @angular/compiler will cause false positives - if (/[\\\/]@angular[\\\/](?:compiler|core)/.test(path)) { + // Also, TypeScript files do not require linking + if (/[\\\/]@angular[\\\/](?:compiler|core)|\.tsx?$/.test(path)) { return { requiresLinking: false }; } @@ -82,11 +84,15 @@ export default custom(() => { // Analyze for ES target processing let forceES5 = false; + let forceAsyncTransformation = false; const esTarget = scriptTarget as ScriptTarget; if (esTarget < ScriptTarget.ES2015) { - forceES5 = true; + // TypeScript files will have already been downlevelled + forceES5 = !/\.tsx?$/.test(this.resourcePath); + } else if (esTarget >= ScriptTarget.ES2017) { + forceAsyncTransformation = source.includes('async'); } - shouldProcess ||= forceES5; + shouldProcess ||= forceAsyncTransformation || forceES5; // Add provided loader options to default base options const options: Record = { @@ -100,7 +106,7 @@ export default custom(() => { options.ignore = [() => true]; } - return { custom: { forceES5, shouldLink }, loader: options }; + return { custom: { forceAsyncTransformation, forceES5, shouldLink }, loader: options }; }, config(configuration, { customOptions }) { return { @@ -112,6 +118,7 @@ export default custom(() => { { angularLinker: customOptions.shouldLink, forceES5: customOptions.forceES5, + forceAsyncTransformation: customOptions.forceAsyncTransformation, diagnosticReporter: (type, message) => { switch (type) { case 'error': diff --git a/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts new file mode 100644 index 000000000000..5de9f6166f9e --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Behavior: "TypeScript Configuration - target"', () => { + it('downlevels async functions when targetting ES2017', async () => { + // Set TypeScript configuration target to ES2017 to enable native async + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + tsconfig.compilerOptions.target = 'es2017'; + + return JSON.stringify(tsconfig); + }); + + // Add a JavaScript file with async code + await harness.writeFile( + 'src/async-test.js', + 'async function testJs() { console.log("from-async-js-function"); }', + ); + + // Add an async function to the project as well as JavaScript file + await harness.modifyFile( + 'src/main.ts', + (content) => + 'import "./async-test";\n' + + content + + `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Zone.js does not support native async/await in ES2017+'), + }), + ); + + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js').content.toContain('"from-async-app-function"'); + harness.expectFile('dist/main.js').content.toContain('"from-async-js-function"'); + }); + + it('downlevels async functions when targetting greater than ES2017', async () => { + // Set TypeScript configuration target greater than ES2017 to enable native async + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + tsconfig.compilerOptions.target = 'es2020'; + + return JSON.stringify(tsconfig); + }); + + // Add an async function to the project + await harness.writeFile( + 'src/main.ts', + 'async function test(): Promise { console.log("from-async-function"); }', + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Zone.js does not support native async/await in ES2017+'), + }), + ); + + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js').content.toContain('"from-async-function"'); + }); + }); +}); 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 781e4e64d70b..41dda40f1221 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -544,7 +544,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { sideEffects: true, }, { - test: /\.[cm]?js$/, + test: /\.[cm]?js$|\.tsx?$/, exclude: [/[\/\\](?:core-js|\@babel|tslib|web-animations-js)[\/\\]/, /(ngfactory|ngstyle)\.js$/], use: [ { diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts index aa3c0317b2f5..11986ccd7f9b 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts @@ -79,6 +79,7 @@ function createIvyPlugin( compilerOptions, fileReplacements, emitNgModuleScope: !optimize, + suppressZoneJsIncompatibilityWarning: true, }); } @@ -159,6 +160,7 @@ function _createAotPlugin( directTemplateLoading: true, ...options, compilerOptions, + suppressZoneJsIncompatibilityWarning: true, }; pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions); diff --git a/yarn.lock b/yarn.lock index 6d8d387ee942..1be38e8142d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,7 +645,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.12.1": +"@babel/plugin-transform-async-to-generator@7.12.1", "@babel/plugin-transform-async-to-generator@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==