From 12931ba8c3772b1dd65846cbd6146804b08eab31 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 15 Sep 2022 18:03:23 +0000 Subject: [PATCH] refactor(@angular-devkit/build-angular): remove deprecated ES5 support Remove deprecated support for ES5 output. BREAKING CHANGE: Producing ES5 output is no longer possible. This was needed for Internet Explorer which is no longer supported. All browsers that Angular supports work with ES2015+ --- .../src/babel/presets/application.ts | 10 +- .../build_angular/src/babel/webpack-loader.ts | 10 +- .../src/builders/browser/index.ts | 25 +- .../builders/browser/specs/allow-js_spec.ts | 26 +- .../browser/specs/browser-support_spec.ts | 41 --- .../browser/specs/optimization-level_spec.ts | 8 - .../browser/specs/resolve-json-module_spec.ts | 2 +- .../src/builders/browser/specs/styles_spec.ts | 55 ++-- .../tests/behavior/browser-support_spec.ts | 119 ++++++++ .../tests/behavior/typescript-target_spec.ts | 253 ------------------ .../browser/tests/options/tsconfig_spec.ts | 27 -- .../src/builders/server/index.ts | 24 +- .../build_angular/src/utils/i18n-inlining.ts | 4 - .../src/utils/normalize-builder-schema.ts | 5 +- .../build_angular/src/utils/process-bundle.ts | 91 ++----- .../src/utils/supported-browsers.ts | 27 +- .../src/utils/webpack-browser-config.ts | 10 +- .../plugins/javascript-optimizer-worker.ts | 62 ++--- .../src/webpack/plugins/typescript.ts | 18 +- .../e2e/tests/i18n/ivy-localize-es5.ts | 30 --- .../e2e/tests/misc/es2015-nometa.ts | 5 +- .../e2e/tests/misc/forwardref-es2015.ts | 5 +- 22 files changed, 268 insertions(+), 589 deletions(-) delete mode 100644 packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts create mode 100644 packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/browser-support_spec.ts delete mode 100644 packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts delete mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts 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 ca1a6caf960d..366f07e48a10 100644 --- a/packages/angular_devkit/build_angular/src/babel/presets/application.ts +++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts @@ -11,7 +11,6 @@ import type { DiagnosticHandlingStrategy, Diagnostics, makeEs2015TranslatePlugin, - makeEs5TranslatePlugin, makeLocalePlugin, } from '@angular/localize/tools'; import { strict as assert } from 'assert'; @@ -28,7 +27,6 @@ export type DiagnosticReporter = (type: 'error' | 'warning' | 'info', message: s */ export interface I18nPluginCreators { makeEs2015TranslatePlugin: typeof makeEs2015TranslatePlugin; - makeEs5TranslatePlugin: typeof makeEs5TranslatePlugin; makeLocalePlugin: typeof makeLocalePlugin; } @@ -117,7 +115,7 @@ function createI18nPlugins( const diagnostics = createI18nDiagnostics(diagnosticReporter); const plugins = []; - const { makeEs5TranslatePlugin, makeEs2015TranslatePlugin, makeLocalePlugin } = pluginCreators; + const { makeEs2015TranslatePlugin, makeLocalePlugin } = pluginCreators; if (translation) { plugins.push( @@ -125,12 +123,6 @@ function createI18nPlugins( missingTranslation: missingTranslationBehavior, }), ); - - plugins.push( - makeEs5TranslatePlugin(diagnostics, translation, { - missingTranslation: missingTranslationBehavior, - }), - ); } plugins.push(makeLocalePlugin(locale)); 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 14b0e817da8b..6d23d24c25cb 100644 --- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -117,15 +117,7 @@ export default custom(() => { const esTarget = scriptTarget as ScriptTarget | undefined; 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 && customOptions.supportedBrowsers?.length) { + if (isJsFile && customOptions.supportedBrowsers?.length) { // 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. diff --git a/packages/angular_devkit/build_angular/src/builders/browser/index.ts b/packages/angular_devkit/build_angular/src/builders/browser/index.ts index 23c391a3608a..6a310a4d45a6 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/index.ts @@ -8,12 +8,10 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { EmittedFiles, WebpackLoggingCallback, runWebpack } from '@angular-devkit/build-webpack'; -import { logging } from '@angular-devkit/core'; import * as fs from 'fs'; import * as path from 'path'; import { Observable, from } from 'rxjs'; import { concatMap, map, switchMap } from 'rxjs/operators'; -import { ScriptTarget } from 'typescript'; import webpack from 'webpack'; import { ExecutionTransformer } from '../../transforms'; import { @@ -43,7 +41,6 @@ import { generateEntryPoints } from '../../utils/package-chunk-sort'; import { purgeStaleBuildCache } from '../../utils/purge-cache'; import { augmentAppWithServiceWorker } from '../../utils/service-worker'; import { Spinner } from '../../utils/spinner'; -import { getSupportedBrowsers } from '../../utils/supported-browsers'; import { assertCompatibleAngularVersion } from '../../utils/version'; import { generateI18nBrowserWebpackConfigFromContext, @@ -98,14 +95,13 @@ async function initialize( projectRoot: string; projectSourceRoot?: string; i18n: I18nOptions; - target: ScriptTarget; }> { const originalOutputPath = options.outputPath; // Assets are processed directly by the builder except when watching const adjustedOptions = options.watch ? options : { ...options, assets: [] }; - const { config, projectRoot, projectSourceRoot, i18n, target } = + const { config, projectRoot, projectSourceRoot, i18n } = await generateI18nBrowserWebpackConfigFromContext(adjustedOptions, context, (wco) => [ getCommonConfig(wco), getStylesConfig(wco), @@ -135,7 +131,7 @@ async function initialize( deleteOutputDir(context.workspaceRoot, originalOutputPath); } - return { config: transformedConfig || config, projectRoot, projectSourceRoot, i18n, target }; + return { config: transformedConfig || config, projectRoot, projectSourceRoot, i18n }; } /** @@ -170,9 +166,6 @@ export function buildWebpackBrowser( // Initialize builder const initialization = await initialize(options, context, transforms.webpackConfiguration); - // Check and warn about IE browser support - checkInternetExplorerSupport(initialization.projectRoot, context.logger); - // Add index file to watched files. if (options.watch) { const indexInputFile = path.join(context.workspaceRoot, getIndexInputFile(options.index)); @@ -193,7 +186,7 @@ export function buildWebpackBrowser( }), switchMap( // eslint-disable-next-line max-lines-per-function - ({ config, projectRoot, projectSourceRoot, i18n, target, cacheOptions }) => { + ({ config, projectRoot, projectSourceRoot, i18n, cacheOptions }) => { const normalizedOptimization = normalizeOptimization(options.optimization); return runWebpack(config, context, { @@ -255,7 +248,6 @@ export function buildWebpackBrowser( Array.from(outputPaths.values()), scriptsEntryPointName, webpackOutputPath, - target <= ScriptTarget.ES5, options.i18nMissingTranslation, ); if (!success) { @@ -458,15 +450,4 @@ function mapEmittedFilesToFileInfo(files: EmittedFiles[] = []): FileInfo[] { return filteredFiles; } -function checkInternetExplorerSupport(projectRoot: string, logger: logging.LoggerApi): void { - const supportedBrowsers = getSupportedBrowsers(projectRoot); - if (supportedBrowsers.some((b) => b === 'ie 9' || b === 'ie 10' || b === 'ie 11')) { - logger.warn( - `Warning: Support was requested for Internet Explorer in the project's browserslist configuration. ` + - 'Internet Explorer is no longer officially supported.' + - '\nFor more information, see https://angular.io/guide/browser-support', - ); - } -} - export default createBuilder(buildWebpackBrowser); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/allow-js_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/allow-js_spec.ts index 6f856d5441ad..4f5ab84c0eff 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/allow-js_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/allow-js_spec.ts @@ -29,7 +29,11 @@ describe('Browser Builder allow js', () => { 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, }); - host.replaceInFile('tsconfig.json', '"target": "es2020"', '"target": "es5", "allowJs": true'); + host.replaceInFile( + 'tsconfig.json', + '"target": "es2020"', + '"target": "es2020", "allowJs": true', + ); const run = await architect.scheduleTarget(targetSpec); const output = (await run.result) as BrowserBuilderOutput; @@ -39,7 +43,7 @@ describe('Browser Builder allow js', () => { await host.read(join(normalize(output.outputPath), 'main.js')).toPromise(), ); - expect(content).toContain('var a = 2'); + expect(content).toContain('const a = 2'); await run.stop(); }); @@ -50,7 +54,11 @@ describe('Browser Builder allow js', () => { 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, }); - host.replaceInFile('tsconfig.json', '"target": "es2020"', '"target": "es5", "allowJs": true'); + host.replaceInFile( + 'tsconfig.json', + '"target": "es2020"', + '"target": "es2020", "allowJs": true', + ); const overrides = { aot: true }; @@ -62,7 +70,7 @@ describe('Browser Builder allow js', () => { await host.read(join(normalize(output.outputPath), 'main.js')).toPromise(), ); - expect(content).toContain('var a = 2'); + expect(content).toContain('const a = 2'); await run.stop(); }); @@ -73,7 +81,11 @@ describe('Browser Builder allow js', () => { 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, }); - host.replaceInFile('tsconfig.json', '"target": "es2020"', '"target": "es5", "allowJs": true'); + host.replaceInFile( + 'tsconfig.json', + '"target": "es2020"', + '"target": "es2020", "allowJs": true', + ); const overrides = { watch: true }; @@ -88,13 +100,13 @@ describe('Browser Builder allow js', () => { switch (buildCount) { case 1: - expect(content).toContain('var a = 2'); + expect(content).toContain('const a = 2'); host.writeMultipleFiles({ 'src/my-js-file.js': `console.log(1); export const a = 1;`, }); break; case 2: - expect(content).toContain('var a = 1'); + expect(content).toContain('const a = 1'); break; } diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts deleted file mode 100644 index c812c3227671..000000000000 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { Architect } from '@angular-devkit/architect'; -import { logging } from '@angular-devkit/core'; -import { createArchitect, host } from '../../../testing/test-utils'; - -describe('Browser Builder browser support', () => { - const targetSpec = { project: 'app', target: 'build' }; - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - afterEach(async () => host.restore().toPromise()); - - it('warns when IE is present in browserslist', async () => { - host.writeMultipleFiles({ - '.browserslistrc': '\nIE 9', - }); - - const logger = new logging.Logger(''); - const logs: string[] = []; - logger.subscribe((e) => logs.push(e.message)); - - const run = await architect.scheduleTarget(targetSpec, undefined, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - - expect(logs.join()).toContain( - "Warning: Support was requested for Internet Explorer in the project's browserslist configuration", - ); - await run.stop(); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/optimization-level_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/optimization-level_spec.ts index 6db2db347594..f5450529db01 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/optimization-level_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/optimization-level_spec.ts @@ -26,14 +26,6 @@ describe('Browser Builder optimization level', () => { expect(await files['main.js']).not.toContain('AppComponent'); }); - it('tsconfig target changes optimizations to use es2017', async () => { - host.replaceInFile('tsconfig.json', '"target": "es5"', '"target": "es2017"'); - - const overrides = { optimization: true }; - const { files } = await browserBuild(architect, host, target, overrides); - expect(await files['vendor.js']).toMatch(/class \w{1,3}{constructor\(\){/); - }); - it('supports styles only optimizations', async () => { const overrides = { optimization: { diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/resolve-json-module_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/resolve-json-module_spec.ts index 7f24650c806e..7cee64ef8497 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/resolve-json-module_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/resolve-json-module_spec.ts @@ -30,7 +30,7 @@ describe('Browser Builder resolve json module', () => { host.replaceInFile( 'tsconfig.json', '"target": "es2020"', - '"target": "es5", "resolveJsonModule": true', + '"target": "es2020", "resolveJsonModule": true', ); const overrides = { watch: true }; diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts index 2d5cc91a3269..1cde4893122a 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts @@ -94,18 +94,22 @@ describe('Browser Builder styles', () => { @Component({ selector: 'app-root', templateUrl: './app.component.html', - styles: ['div { flex: 1 }'], + styles: ['div { mask-composite: add; }'], }) export class AppComponent { title = 'app'; } `, - '.browserslistrc': 'IE 10', + '.browserslistrc': ` + Safari 15.4 + Edge 104 + Firefox 91 + `, }); const { files } = await browserBuild(architect, host, target, { aot: false }); - expect(await files['main.js']).toContain('-ms-flex: 1;'); + expect(await files['main.js']).toContain('-webkit-mask-composite'); }); it('supports autoprefixer with inline component styles in AOT mode', async () => { @@ -116,18 +120,22 @@ describe('Browser Builder styles', () => { @Component({ selector: 'app-root', templateUrl: './app.component.html', - styles: ['div { flex: 1 }'], + styles: ['div { mask-composite: add; }'], }) export class AppComponent { title = 'app'; } `, - '.browserslistrc': 'IE 10', + '.browserslistrc': ` + Safari 15.4 + Edge 104 + Firefox 91 + `, }); const { files } = await browserBuild(architect, host, target, { aot: true }); - expect(await files['main.js']).toContain('-ms-flex: 1;'); + expect(await files['main.js']).toContain('-webkit-mask-composite'); }); extensionsWithImportSupport.forEach((ext) => { @@ -302,12 +310,16 @@ describe('Browser Builder styles', () => { @import url(imported-styles.css); /* normal-comment */ /*! important-comment */ - div { flex: 1 }`, + div { mask-composite: add; }`, 'src/imported-styles.css': tags.stripIndents` /* normal-comment */ /*! important-comment */ - section { flex: 1 }`, - '.browserslistrc': 'IE 10', + section { mask-composite: add; }`, + '.browserslistrc': ` + Safari 15.4 + Edge 104 + Firefox 91 + `, }); const overrides = { optimization: false }; @@ -315,10 +327,10 @@ describe('Browser Builder styles', () => { expect(await files['styles.css']).toContain(tags.stripIndents` /* normal-comment */ /*! important-comment */ - section { -ms-flex: 1; flex: 1 } + section { -webkit-mask-composite: source-over; mask-composite: add; } /* normal-comment */ /*! important-comment */ - div { -ms-flex: 1; flex: 1 }`); + div { -webkit-mask-composite: source-over; mask-composite: add; }`); }); it(`minimizes css`, async () => { @@ -334,24 +346,6 @@ describe('Browser Builder styles', () => { expect(await files['styles.css']).toContain('/*! important-comment */'); }); - it('supports autoprefixer grid comments in SCSS with optimization true', async () => { - host.writeMultipleFiles({ - 'src/styles.scss': tags.stripIndents` - /* autoprefixer grid: autoplace */ - .css-grid-container { - display: grid; - row-gap: 10px; - grid-template-columns: 100px; - } - `, - '.browserslistrc': 'IE 10', - }); - - const overrides = { optimization: true, styles: ['src/styles.scss'] }; - const { files } = await browserBuild(architect, host, target, overrides); - expect(await files['styles.css']).toContain('-ms-grid-columns:100px;'); - }); - // TODO: consider making this a unit test in the url processing plugins. it( `supports baseHref/deployUrl in resource urls`, @@ -665,10 +659,11 @@ describe('Browser Builder styles', () => { 'src/styles.css': ` div { box-shadow: 0 3px 10px, rgba(0, 0, 0, 0.15); } `, - '.browserslistrc': 'edge 17', + '.browserslistrc': `edge 18`, }); result = await browserBuild(architect, host, target, { optimization: true }); + expect(await result.files['styles.css']).toContain('rgba(0,0,0,.15)'); }); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/browser-support_spec.ts new file mode 100644 index 000000000000..e1f46f8efc69 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/browser-support_spec.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright Google LLC 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: "Browser support"', () => { + it('creates correct sourcemaps when downleveling async functions', async () => { + // 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 + // The type `Void123` is used as a unique identifier for the final sourcemap + // If sourcemaps are not properly propagated then it will not be in the final sourcemap + await harness.modifyFile( + 'src/main.ts', + (content) => + 'import "./async-test";\n' + + content + + '\ntype Void123 = void;' + + `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + vendorChunk: true, + sourceMap: { + scripts: true, + }, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js.map').content.toContain('Promise'); + }); + + it('downlevels async functions ', async () => { + // 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, + vendorChunk: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js').content.toContain('"from-async-function"'); + }); + + it('warns when IE is present in browserslist', async () => { + await harness.writeFile( + '.browserslistrc', + ` + IE 9 + IE 11 + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + expect(logs).toContain( + jasmine.objectContaining({ + level: 'warn', + message: + `One or more browsers which are configured in the project's Browserslist configuration ` + + 'will be ignored as ES5 output is not supported by the Angular CLI.\n' + + `Ignored browsers: ie 11, ie 9`, + }), + ); + }); + + it('downlevels "for await...of"', 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, + vendorChunk: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toMatch(/\sawait\s/); + harness.expectFile('dist/main.js').content.toContain('"for await...of"'); + }); + }); +}); 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 deleted file mode 100644 index 0b1f1877cc03..000000000000 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/behavior/typescript-target_spec.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { logging } from '@angular-devkit/core'; -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, - vendorChunk: true, - }); - - const { result } = await harness.executeOnce(); - - expect(result?.success).toBe(true); - 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('creates correct sourcemaps when downleveling async functions', 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 - // The type `Void123` is used as a unique identifier for the final sourcemap - // If sourcemaps are not properly propagated then it will not be in the final sourcemap - await harness.modifyFile( - 'src/main.ts', - (content) => - 'import "./async-test";\n' + - content + - '\ntype Void123 = void;' + - `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`, - ); - - harness.useTarget('build', { - ...BASE_OPTIONS, - vendorChunk: true, - sourceMap: { - scripts: true, - }, - }); - - const { result } = await harness.executeOnce(); - - expect(result?.success).toBe(true); - harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); - harness.expectFile('dist/main.js.map').content.toContain('Promise'); - }); - - 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, - vendorChunk: true, - }); - - const { result } = await harness.executeOnce(); - - expect(result?.success).toBe(true); - harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); - harness.expectFile('dist/main.js').content.toContain('"from-async-function"'); - }); - - it('downlevels "for await...of" when targetting ES2018+', 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 () => { - for await (const o of [1, 2, 3]) { - console.log("for await...of"); - } - })(); - `, - ); - - harness.useTarget('build', { - ...BASE_OPTIONS, - vendorChunk: true, - }); - - const { result } = await harness.executeOnce(); - - expect(result?.success).toBe(true); - harness.expectFile('dist/main.js').content.not.toMatch(/\sawait\s/); - harness.expectFile('dist/main.js').content.toContain('"for await...of"'); - }); - - it('allows optimizing global scripts with ES2015+ syntax 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); - }); - - // Add global script with ES2015+ syntax to the project - await harness.writeFile( - 'src/es2015-syntax.js', - ` - class foo { - bar() { - console.log('baz'); - } - } - - (new foo()).bar(); - `, - ); - - harness.useTarget('build', { - ...BASE_OPTIONS, - optimization: { - scripts: true, - }, - scripts: ['src/es2015-syntax.js'], - }); - - const { result } = await harness.executeOnce(); - - 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(); - - expect(logs).toContain( - jasmine.objectContaining({ - message: jasmine.stringMatching('DEPRECATED: ES5 output is deprecated'), - }), - ); - }); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/tsconfig_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/tsconfig_spec.ts index 06ceb617be02..77bc192af4b9 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/tsconfig_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/tsconfig_spec.ts @@ -11,33 +11,6 @@ import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { describe('Option: "tsConfig"', () => { - it('uses a provided TypeScript configuration file', async () => { - // Setup a TS file that uses ES2015+ const and then target ES5. - // The const usage should be downleveled in the output if the TS config is used. - await harness.writeFile('src/main.ts', 'const a = 5; console.log(a);'); - await harness.writeFile( - 'src/tsconfig.option.json', - JSON.stringify({ - compilerOptions: { - target: 'es5', - types: [], - }, - files: ['main.ts'], - }), - ); - - harness.useTarget('build', { - ...BASE_OPTIONS, - tsConfig: 'src/tsconfig.option.json', - }); - - const { result } = await harness.executeOnce(); - - expect(result?.success).toBe(true); - - harness.expectFile('dist/main.js').content.not.toContain('const'); - }); - it('throws an exception when TypeScript Configuration file does not exist', async () => { harness.useTarget('build', { ...BASE_OPTIONS, diff --git a/packages/angular_devkit/build_angular/src/builders/server/index.ts b/packages/angular_devkit/build_angular/src/builders/server/index.ts index 7d6cba729c9d..e9f424769f43 100644 --- a/packages/angular_devkit/build_angular/src/builders/server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/server/index.ts @@ -12,7 +12,6 @@ import { tags } from '@angular-devkit/core'; import * as path from 'path'; import { Observable, from } from 'rxjs'; import { concatMap, map } from 'rxjs/operators'; -import { ScriptTarget } from 'typescript'; import webpack from 'webpack'; import { ExecutionTransformer } from '../../transforms'; import { NormalizedBrowserBuilderSchema, deleteOutputDir } from '../../utils'; @@ -67,7 +66,7 @@ export function execute( let outputPaths: undefined | Map; return from(initialize(options, context, transforms.webpackConfiguration)).pipe( - concatMap(({ config, i18n, target }) => { + concatMap(({ config, i18n }) => { return runWebpack(config, context, { webpackFactory: require('webpack') as typeof webpack, logging: (stats, config) => { @@ -94,7 +93,6 @@ export function execute( Array.from(outputPaths.values()), [], outputPath, - target <= ScriptTarget.ES5, options.i18nMissingTranslation, ); } @@ -136,13 +134,13 @@ async function initialize( ): Promise<{ config: webpack.Configuration; i18n: I18nOptions; - target: ScriptTarget; }> { // Purge old build disk cache. await purgeStaleBuildCache(context); + const browserslist = (await import('browserslist')).default; const originalOutputPath = options.outputPath; - const { config, i18n, target } = await generateI18nBrowserWebpackConfigFromContext( + const { config, i18n } = await generateI18nBrowserWebpackConfigFromContext( { ...options, buildOptimizer: false, @@ -150,17 +148,19 @@ async function initialize( platform: 'server', } as NormalizedBrowserBuilderSchema, context, - (wco) => [getCommonConfig(wco), getStylesConfig(wco)], - ); + (wco) => { + // We use the platform to determine the JavaScript syntax output. + wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions')); - let transformedConfig; - if (webpackConfigurationTransform) { - transformedConfig = await webpackConfigurationTransform(config); - } + return [getCommonConfig(wco), getStylesConfig(wco)]; + }, + ); if (options.deleteOutputPath) { deleteOutputDir(context.workspaceRoot, originalOutputPath); } - return { config: transformedConfig || config, i18n, target }; + const transformedConfig = (await webpackConfigurationTransform?.(config)) ?? config; + + return { config: transformedConfig, i18n }; } diff --git a/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts b/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts index fec3322f168d..beb5734db539 100644 --- a/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts +++ b/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts @@ -22,7 +22,6 @@ function emittedFilesToInlineOptions( scriptsEntryPointName: string[], emittedPath: string, outputPath: string, - es5: boolean, missingTranslation: 'error' | 'warning' | 'ignore' | undefined, context: BuilderContext, ): { options: InlineOptions[]; originalFiles: string[] } { @@ -41,7 +40,6 @@ function emittedFilesToInlineOptions( const action: InlineOptions = { filename: emittedFile.file, code: fs.readFileSync(originalPath, 'utf8'), - es5, outputPath, missingTranslation, setLocale: emittedFile.name === 'main' || emittedFile.name === 'vendor', @@ -75,7 +73,6 @@ export async function i18nInlineEmittedFiles( outputPaths: string[], scriptsEntryPointName: string[], emittedPath: string, - es5: boolean, missingTranslation: 'error' | 'warning' | 'ignore' | undefined, ): Promise { const executor = new BundleActionExecutor({ i18n }); @@ -89,7 +86,6 @@ export async function i18nInlineEmittedFiles( scriptsEntryPointName, emittedPath, baseOutputPath, - es5, missingTranslation, context, ); diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-builder-schema.ts b/packages/angular_devkit/build_angular/src/utils/normalize-builder-schema.ts index 1c3af36dded5..138b25bc4aaa 100644 --- a/packages/angular_devkit/build_angular/src/utils/normalize-builder-schema.ts +++ b/packages/angular_devkit/build_angular/src/utils/normalize-builder-schema.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { json } from '@angular-devkit/core'; +import { json, logging } from '@angular-devkit/core'; import { AssetPatternClass, Schema as BrowserBuilderSchema, @@ -40,6 +40,7 @@ export function normalizeBrowserSchema( projectSourceRoot: string | undefined, options: BrowserBuilderSchema, metadata: json.JsonObject, + logger: logging.LoggerApi, ): NormalizedBrowserBuilderSchema { const normalizedSourceMapOptions = normalizeSourceMaps(options.sourceMap || false); @@ -71,6 +72,6 @@ export function normalizeBrowserSchema( // A value of 0 is falsy and will disable polling rather then enable // 500 ms is a sensible default in this case poll: options.poll === 0 ? 500 : options.poll, - supportedBrowsers: getSupportedBrowsers(projectRoot), + supportedBrowsers: getSupportedBrowsers(projectRoot, logger), }; } diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index e9fe997d02b4..5faecf6acde2 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -17,7 +17,7 @@ import { types, } from '@babel/core'; import templateBuilder from '@babel/template'; -import * as fs from 'fs'; +import * as fs from 'fs/promises'; import * as path from 'path'; import { workerData } from 'worker_threads'; import { allowMinify, shouldBeautify } from './environment-options'; @@ -72,8 +72,7 @@ export async function createI18nPlugins( shouldInline: boolean, localeDataContent?: string, ) { - const { Diagnostics, makeEs2015TranslatePlugin, makeEs5TranslatePlugin, makeLocalePlugin } = - await loadLocalizeTools(); + const { Diagnostics, makeEs2015TranslatePlugin, makeLocalePlugin } = await loadLocalizeTools(); const plugins = []; const diagnostics = new Diagnostics(); @@ -85,13 +84,6 @@ export async function createI18nPlugins( missingTranslation: translation === undefined ? 'ignore' : missingTranslation, }), ); - - plugins.push( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeEs5TranslatePlugin(diagnostics, (translation || {}) as any, { - missingTranslation: translation === undefined ? 'ignore' : missingTranslation, - }), - ); } plugins.push(makeLocalePlugin(locale)); @@ -113,7 +105,6 @@ export interface InlineOptions { filename: string; code: string; map?: string; - es5: boolean; outputPath: string; missingTranslation?: 'warning' | 'error' | 'ignore'; setLocale?: boolean; @@ -180,7 +171,7 @@ export async function inlineLocales(options: InlineOptions) { // If locale data is provided, load it and prepend to file const localeDataPath = i18n.locales[locale]?.dataPath; if (localeDataPath) { - localeDataContent = await loadLocaleData(localeDataPath, true, options.es5); + localeDataContent = await loadLocaleData(localeDataPath, true); } } @@ -215,12 +206,12 @@ export async function inlineLocales(options: InlineOptions) { i18n.flatOutput ? '' : locale, options.filename, ); - fs.writeFileSync(outputPath, transformResult.code); + await fs.writeFile(outputPath, transformResult.code); if (options.map && transformResult.map) { const outputMap = remapping([transformResult.map as SourceMapInput, options.map], () => null); - fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap)); + await fs.writeFile(outputPath + '.map', JSON.stringify(outputMap)); } } @@ -287,7 +278,7 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) { let localeDataSource; const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath; if (localeDataPath) { - const localeDataContent = await loadLocaleData(localeDataPath, true, options.es5); + const localeDataContent = await loadLocaleData(localeDataPath, true); localeDataSource = new OriginalSource(localeDataContent, path.basename(localeDataPath)); } @@ -306,21 +297,21 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) { i18n.flatOutput ? '' : locale, options.filename, ); - fs.writeFileSync(outputPath, outputCode); + await fs.writeFile(outputPath, outputCode); if (inputMap && outputMap) { outputMap.file = options.filename; if (mapSourceRoot) { outputMap.sourceRoot = mapSourceRoot; } - fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap)); + await fs.writeFile(outputPath + '.map', JSON.stringify(outputMap)); } } return { file: options.filename, diagnostics: diagnostics.messages, count: positions.length }; } -function inlineCopyOnly(options: InlineOptions) { +async function inlineCopyOnly(options: InlineOptions) { if (!i18n) { throw new Error('i18n options are missing'); } @@ -331,9 +322,9 @@ function inlineCopyOnly(options: InlineOptions) { i18n.flatOutput ? '' : locale, options.filename, ); - fs.writeFileSync(outputPath, options.code); + await fs.writeFile(outputPath, options.code); if (options.map) { - fs.writeFileSync(outputPath + '.map', options.map); + await fs.writeFile(outputPath + '.map', options.map); } } @@ -351,44 +342,21 @@ function findLocalizePositions( const { File } = require('@babel/core'); const file = new File({}, { code: options.code, ast }); - if (options.es5) { - traverse(file.ast, { - CallExpression(path) { - const callee = path.get('callee'); - if ( - callee.isIdentifier() && - callee.node.name === localizeName && - utils.isGlobalIdentifier(callee) - ) { - const [messageParts, expressions] = unwrapLocalizeCall(path, utils); - positions.push({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - start: path.node.start!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - end: path.node.end!, - messageParts, - expressions, - }); - } - }, - }); - } else { - traverse(file.ast, { - TaggedTemplateExpression(path) { - if (types.isIdentifier(path.node.tag) && path.node.tag.name === localizeName) { - const [messageParts, expressions] = unwrapTemplateLiteral(path, utils); - positions.push({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - start: path.node.start!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - end: path.node.end!, - messageParts, - expressions, - }); - } - }, - }); - } + traverse(file.ast, { + TaggedTemplateExpression(path) { + if (types.isIdentifier(path.node.tag) && path.node.tag.name === localizeName) { + const [messageParts, expressions] = unwrapTemplateLiteral(path, utils); + positions.push({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + start: path.node.start!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + end: path.node.end!, + messageParts, + expressions, + }); + } + }, + }); return positions; } @@ -415,9 +383,9 @@ function unwrapLocalizeCall( return [messageParts, expressions]; } -async function loadLocaleData(path: string, optimize: boolean, es5: boolean): Promise { +async function loadLocaleData(path: string, optimize: boolean): Promise { // The path is validated during option processing before the build starts - const content = fs.readFileSync(path, 'utf8'); + const content = await fs.readFile(path, 'utf8'); // Downlevel and optimize the data const transformResult = await transformAsync(content, { @@ -432,8 +400,7 @@ async function loadLocaleData(path: string, optimize: boolean, es5: boolean): Pr require.resolve('@babel/preset-env'), { bugfixes: true, - // IE 11 is the oldest supported browser - targets: es5 ? { ie: '11' } : { esmodules: true }, + targets: { esmodules: true }, }, ], ], diff --git a/packages/angular_devkit/build_angular/src/utils/supported-browsers.ts b/packages/angular_devkit/build_angular/src/utils/supported-browsers.ts index a3e750a29806..2aa537e8b2a1 100644 --- a/packages/angular_devkit/build_angular/src/utils/supported-browsers.ts +++ b/packages/angular_devkit/build_angular/src/utils/supported-browsers.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import { logging } from '@angular-devkit/core'; import browserslist from 'browserslist'; -export function getSupportedBrowsers(projectRoot: string): string[] { +export function getSupportedBrowsers(projectRoot: string, logger: logging.LoggerApi): string[] { browserslist.defaults = [ 'last 1 Chrome version', 'last 1 Firefox version', @@ -18,5 +19,27 @@ export function getSupportedBrowsers(projectRoot: string): string[] { 'Firefox ESR', ]; - return browserslist(undefined, { path: projectRoot }); + // Get browsers from config or default. + const browsersFromConfigOrDefault = new Set(browserslist(undefined, { path: projectRoot })); + + // Get browsers that support ES6 modules. + const browsersThatSupportEs6 = new Set(browserslist('supports es6-module')); + + const unsupportedBrowsers: string[] = []; + for (const browser of browsersFromConfigOrDefault) { + if (!browsersThatSupportEs6.has(browser)) { + browsersFromConfigOrDefault.delete(browser); + unsupportedBrowsers.push(browser); + } + } + + if (unsupportedBrowsers.length) { + logger.warn( + `One or more browsers which are configured in the project's Browserslist configuration ` + + 'will be ignored as ES5 output is not supported by the Angular CLI.\n' + + `Ignored browsers: ${unsupportedBrowsers.join(', ')}`, + ); + } + + return Array.from(browsersFromConfigOrDefault); } diff --git a/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts b/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts index 850beeffc8b8..b0d2686dbd36 100644 --- a/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts +++ b/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts @@ -9,7 +9,6 @@ import { BuilderContext } from '@angular-devkit/architect'; import { logging } from '@angular-devkit/core'; import * as path from 'path'; -import { ScriptTarget } from 'typescript'; import { Configuration, javascript } from 'webpack'; import { merge as webpackMerge } from 'webpack-merge'; import { Schema as BrowserBuilderSchema } from '../builders/browser/schema'; @@ -44,7 +43,7 @@ export async function generateWebpackConfig( const tsConfig = await readTsconfig(tsConfigPath); const ts = await import('typescript'); - const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES5; + const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES2015; const buildOptions: NormalizedBrowserBuilderSchema = { ...options, ...extraBuildOptions }; const wco: BrowserWebpackConfigOptions = { @@ -77,16 +76,12 @@ export async function generateI18nBrowserWebpackConfigFromContext( projectRoot: string; projectSourceRoot?: string; i18n: I18nOptions; - target: ScriptTarget; }> { const { buildOptions, i18n } = await configureI18nBuild(context, options); - let target = ScriptTarget.ES5; const result = await generateBrowserWebpackConfigFromContext( buildOptions, context, (wco) => { - target = wco.scriptTarget; - return webpackPartialGenerator(wco); }, extraBuildOptions, @@ -133,7 +128,7 @@ export async function generateI18nBrowserWebpackConfigFromContext( }); } - return { ...result, i18n, target }; + return { ...result, i18n }; } export async function generateBrowserWebpackConfigFromContext( options: BrowserBuilderSchema, @@ -158,6 +153,7 @@ export async function generateBrowserWebpackConfigFromContext( projectSourceRoot, options, projectMetadata, + context.logger, ); const config = await generateWebpackConfig( diff --git a/packages/angular_devkit/build_angular/src/webpack/plugins/javascript-optimizer-worker.ts b/packages/angular_devkit/build_angular/src/webpack/plugins/javascript-optimizer-worker.ts index 8ac77a8400e7..3fedc9daad40 100644 --- a/packages/angular_devkit/build_angular/src/webpack/plugins/javascript-optimizer-worker.ts +++ b/packages/angular_devkit/build_angular/src/webpack/plugins/javascript-optimizer-worker.ts @@ -7,7 +7,7 @@ */ import remapping from '@ampproject/remapping'; -import type { TransformFailure, TransformResult } from 'esbuild'; +import type { TransformResult } from 'esbuild'; import { minify } from 'terser'; import { EsbuildExecutor } from './esbuild-executor'; @@ -150,49 +150,23 @@ async function optimizeWithEsbuild( esbuild = new EsbuildExecutor(options.alwaysUseWasm); } - let result: TransformResult; - try { - result = await esbuild.transform(content, { - minifyIdentifiers: !options.keepIdentifierNames, - minifySyntax: true, - // NOTE: Disabling whitespace ensures unused pure annotations are kept - minifyWhitespace: false, - pure: ['forwardRef'], - legalComments: options.removeLicenses ? 'none' : 'inline', - sourcefile: name, - sourcemap: options.sourcemap && 'external', - define: options.define, - // This option should always be disabled for browser builds as we don't rely on `.name` - // and causes deadcode to be retained which makes `NG_BUILD_MANGLE` unusable to investigate tree-shaking issues. - // We enable `keepNames` only for server builds as Domino relies on `.name`. - // Once we no longer rely on Domino for SSR we should be able to remove this. - keepNames: options.keepNames, - target: `es${options.target}`, - }); - } catch (error) { - const failure = error as TransformFailure; - - // If esbuild fails with only ES5 support errors, fallback to just terser. - // This will only happen if ES5 is the output target and a global script contains ES2015+ syntax. - // In that case, the global script is technically already invalid for the target environment but - // this is and has been considered a configuration issue. Global scripts must be compatible with - // the target environment. - if ( - failure.errors?.every((error) => - error.text.includes('to the configured target environment ("es5") is not supported yet'), - ) - ) { - result = { - code: content, - map: '', - warnings: [], - }; - } else { - throw error; - } - } - - return result; + return esbuild.transform(content, { + minifyIdentifiers: !options.keepIdentifierNames, + minifySyntax: true, + // NOTE: Disabling whitespace ensures unused pure annotations are kept + minifyWhitespace: false, + pure: ['forwardRef'], + legalComments: options.removeLicenses ? 'none' : 'inline', + sourcefile: name, + sourcemap: options.sourcemap && 'external', + define: options.define, + // This option should always be disabled for browser builds as we don't rely on `.name` + // and causes deadcode to be retained which makes `NG_BUILD_MANGLE` unusable to investigate tree-shaking issues. + // We enable `keepNames` only for server builds as Domino relies on `.name`. + // Once we no longer rely on Domino for SSR we should be able to remove this. + keepNames: options.keepNames, + target: `es${options.target}`, + }); } /** diff --git a/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts b/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts index 6ef15d705e34..7aa644732d05 100644 --- a/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts @@ -16,7 +16,7 @@ export function createIvyPlugin( aot: boolean, tsconfig: string, ): AngularWebpackPlugin { - const { buildOptions } = wco; + const { buildOptions, tsConfig } = wco; const optimize = buildOptions.optimization.scripts; const compilerOptions: CompilerOptions = { @@ -25,18 +25,14 @@ export function createIvyPlugin( declarationMap: false, }; - if (buildOptions.preserveSymlinks !== undefined) { - compilerOptions.preserveSymlinks = buildOptions.preserveSymlinks; + if (tsConfig.options.target === undefined || tsConfig.options.target <= ScriptTarget.ES5) { + throw new Error( + 'ES output older than ES2015 is not supported. Please update TypeScript "target" compiler option to ES2015 or later.', + ); } - // Outputting ES2015 from TypeScript is the required minimum for the build optimizer passes. - // Downleveling to ES5 will occur after the build optimizer passes via babel which is the same - // as for third-party libraries. This greatly reduces the complexity of static analysis. - if (wco.scriptTarget < ScriptTarget.ES2015) { - compilerOptions.target = ScriptTarget.ES2015; - wco.logger.warn( - 'DEPRECATED: ES5 output is deprecated. Please update TypeScript `target` compiler option to ES2015 or later.', - ); + if (buildOptions.preserveSymlinks !== undefined) { + compilerOptions.preserveSymlinks = buildOptions.preserveSymlinks; } const fileReplacements: Record = {}; diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts deleted file mode 100644 index 88bfac2d9cc1..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { langTranslations, setupI18nConfig } from './setup'; - -export default async function () { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Ensure a es5 build is used. - await updateJsonFile('tsconfig.json', (config) => { - config.compilerOptions.target = 'es5'; - }); - - // Build each locale and verify the output. - await ng('build'); - - for (const { lang, outputPath, translation } of langTranslations) { - await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial); - await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`')); - - // Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references) - // The only reference in a new application is in @angular/core - await expectFileToMatch(`${outputPath}/vendor.js`, lang); - - // Verify the HTML lang attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts b/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts index 8926df2445ab..3973636e07f5 100644 --- a/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts +++ b/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts @@ -1,10 +1,7 @@ -import { prependToFile, replaceInFile, writeFile } from '../../utils/fs'; +import { prependToFile, replaceInFile } from '../../utils/fs'; import { ng } from '../../utils/process'; export default async function () { - // Ensure an ES2015 build is used in test - await writeFile('.browserslistrc', 'Chrome 65'); - await ng('generate', 'service', 'user'); // Update the application to use the new service diff --git a/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts b/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts index a17859577cee..cdf3eef6a313 100644 --- a/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts +++ b/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts @@ -1,11 +1,8 @@ -import { appendToFile, replaceInFile, writeFile } from '../../utils/fs'; +import { appendToFile, replaceInFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; export default async function () { - // Ensure an ES2015 build is used in test - await writeFile('.browserslistrc', 'Chrome 65'); - // Update the application to use a forward reference await replaceInFile( 'src/app/app.component.ts',