From 31a08f64b924770899a3310c6288c4d83df2dc59 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 20 Sep 2022 09:59:23 +0000 Subject: [PATCH] feat(@angular-devkit/build-angular): use Browserslist to determine ECMA output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed. This is done by passing the transpiled TypeScript to Babel preset-env. **Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: https://github.com/microsoft/TypeScript/issues/45995. BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. --- .../build_angular/src/babel/webpack-loader.ts | 31 ++-- .../browser-esbuild/compiler-plugin.ts | 17 +- .../src/builders/browser-esbuild/index.ts | 14 +- .../builders/browser-esbuild/stylesheets.ts | 2 + .../builders/browser/specs/allow-js_spec.ts | 12 +- .../src/builders/browser/specs/aot_spec.ts | 6 +- .../browser/specs/lazy-module_spec.ts | 15 +- .../browser/specs/resolve-json-module_spec.ts | 4 +- .../tests/behavior/browser-support_spec.ts | 26 ++-- .../behavior/serve_service-worker_spec.ts | 6 +- .../src/builders/server/index.ts | 1 + .../build_angular/src/utils/build-options.ts | 3 +- .../src/utils/esbuild-targets.ts | 43 +++++ .../src/utils/webpack-browser-config.ts | 4 - .../src/webpack/configs/common.ts | 25 +-- .../webpack/plugins/css-optimizer-plugin.ts | 35 +---- .../plugins/javascript-optimizer-plugin.ts | 30 ++-- .../plugins/javascript-optimizer-worker.ts | 14 +- .../src/webpack/plugins/typescript.ts | 13 +- .../src/webpack/utils/helpers.ts | 21 --- .../test/hello-world-app/tsconfig.json | 7 +- .../test/hello-world-lib/tsconfig.json | 5 +- .../test/angular-app/src/tsconfig.app.json | 2 +- .../test/angular-app/tsconfig.json | 4 +- .../test/basic-app/tsconfig.json | 4 +- .../migrations/migration-collection.json | 7 + .../update-15/update-typescript-target.ts | 77 +++++++++ .../update-typescript-target_spec.ts | 147 ++++++++++++++++++ .../workspace/files/tsconfig.json.template | 7 +- .../e2e/assets/12.0-project/tsconfig.json | 7 +- .../e2e/assets/webpack/test-app/tsconfig.json | 2 +- .../e2e/tests/build/scripts-output-hashing.ts | 20 ++- 32 files changed, 412 insertions(+), 199 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts create mode 100644 packages/schematics/angular/migrations/update-15/update-typescript-target.ts create mode 100644 packages/schematics/angular/migrations/update-15/update-typescript-target_spec.ts 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 6d23d24c25cb..78af1b28d480 100644 --- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -7,7 +7,6 @@ */ import { custom } from 'babel-loader'; -import { ScriptTarget } from 'typescript'; import { loadEsmModule } from '../utils/load-esm'; import { VERSION } from '../utils/package-version'; import { ApplicationPresetOptions, I18nPluginCreators } from './presets/application'; @@ -72,15 +71,8 @@ export default custom(() => { return { async customOptions(options, { source, map }) { - const { - i18n, - scriptTarget, - aot, - optimize, - instrumentCode, - supportedBrowsers, - ...rawOptions - } = options as AngularBabelLoaderOptions; + const { i18n, 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; @@ -114,24 +106,19 @@ export default custom(() => { } // Analyze for ES target processing - const esTarget = scriptTarget as ScriptTarget | undefined; - const isJsFile = /\.[cm]?js$/.test(this.resourcePath); - - if (isJsFile && customOptions.supportedBrowsers?.length) { + if (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. 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'); - } + // 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; diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts index 9125b013228a..bc6b300d9d0f 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts @@ -182,16 +182,13 @@ export function createCompilerPlugin( enableResourceInlining: false, }); - // Adjust the esbuild output target based on the tsconfig target - if ( - compilerOptions.target === undefined || - compilerOptions.target <= ts.ScriptTarget.ES2015 - ) { - build.initialOptions.target = 'es2015'; - } else if (compilerOptions.target >= ts.ScriptTarget.ESNext) { - build.initialOptions.target = 'esnext'; - } else { - build.initialOptions.target = ts.ScriptTarget[compilerOptions.target].toLowerCase(); + if (compilerOptions.target === undefined || compilerOptions.target < ts.ScriptTarget.ES2022) { + // If 'useDefineForClassFields' is already defined in the users project leave the value as is. + // Othereise fallback to false to due https://github.com/microsoft/TypeScript/issues/45995 + // which breaks the deprecated @Effects NGRX decorator. + compilerOptions.target = ts.ScriptTarget.ES2022; + compilerOptions.useDefineForClassFields ??= false; + // TODO: show warning about this override when we have access to the logger. } // The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files 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 07a1880e2bb2..fb858a9e3e33 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 @@ -14,10 +14,12 @@ import * as path from 'path'; import { NormalizedOptimizationOptions, deleteOutputDir } from '../../utils'; import { copyAssets } from '../../utils/copy-assets'; import { assertIsError } from '../../utils/error'; +import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets'; import { FileInfo } from '../../utils/index-file/augment-index-html'; import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator'; import { generateEntryPoints } from '../../utils/package-chunk-sort'; import { augmentAppWithServiceWorker } from '../../utils/service-worker'; +import { getSupportedBrowsers } from '../../utils/supported-browsers'; import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config'; import { resolveGlobalStyles } from '../../webpack/configs'; import { createCompilerPlugin } from './compiler-plugin'; @@ -89,6 +91,10 @@ export async function buildEsbuildBrowser( return { success: false }; } + const target = transformSupportedBrowsersToTargets( + getSupportedBrowsers(projectRoot, context.logger), + ); + const [codeResults, styleResults] = await Promise.all([ // Execute esbuild to bundle the application code bundleCode( @@ -99,6 +105,7 @@ export async function buildEsbuildBrowser( optimizationOptions, sourcemapOptions, tsconfig, + target, ), // Execute esbuild to bundle the global stylesheets bundleGlobalStylesheets( @@ -107,6 +114,7 @@ export async function buildEsbuildBrowser( options, optimizationOptions, sourcemapOptions, + target, ), ]); @@ -248,6 +256,7 @@ async function bundleCode( optimizationOptions: NormalizedOptimizationOptions, sourcemapOptions: SourceMapClass, tsconfig: string, + target: string[], ) { let fileReplacements: Record | undefined; if (options.fileReplacements) { @@ -267,7 +276,7 @@ async function bundleCode( entryPoints, entryNames: outputNames.bundles, assetNames: outputNames.media, - target: 'es2020', + 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 @@ -313,6 +322,7 @@ async function bundleCode( outputNames, includePaths: options.stylePreprocessorOptions?.includePaths, externalDependencies: options.externalDependencies, + target, }, ), ], @@ -329,6 +339,7 @@ async function bundleGlobalStylesheets( options: BrowserBuilderOptions, optimizationOptions: NormalizedOptimizationOptions, sourcemapOptions: SourceMapClass, + target: string[], ) { const outputFiles: OutputFile[] = []; const initialFiles: FileInfo[] = []; @@ -360,6 +371,7 @@ async function bundleGlobalStylesheets( includePaths: options.stylePreprocessorOptions?.includePaths, preserveSymlinks: options.preserveSymlinks, externalDependencies: options.externalDependencies, + target, }, ); diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts index 0f932bd12849..2bfaf70f867c 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts @@ -20,6 +20,7 @@ export interface BundleStylesheetOptions { outputNames?: { bundles?: string; media?: string }; includePaths?: string[]; externalDependencies?: string[]; + target: string[]; } async function bundleStylesheet( @@ -43,6 +44,7 @@ async function bundleStylesheet( outdir: options.workspaceRoot, write: false, platform: 'browser', + target: options.target, preserveSymlinks: options.preserveSymlinks, external: options.externalDependencies, conditions: ['style', 'sass'], 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 4f5ab84c0eff..049b95ee8e42 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 @@ -31,8 +31,8 @@ describe('Browser Builder allow js', () => { host.replaceInFile( 'tsconfig.json', - '"target": "es2020"', - '"target": "es2020", "allowJs": true', + '"target": "es2022"', + '"target": "es2022", "allowJs": true', ); const run = await architect.scheduleTarget(targetSpec); @@ -56,8 +56,8 @@ describe('Browser Builder allow js', () => { host.replaceInFile( 'tsconfig.json', - '"target": "es2020"', - '"target": "es2020", "allowJs": true', + '"target": "es2022"', + '"target": "es2022", "allowJs": true', ); const overrides = { aot: true }; @@ -83,8 +83,8 @@ describe('Browser Builder allow js', () => { host.replaceInFile( 'tsconfig.json', - '"target": "es2020"', - '"target": "es2020", "allowJs": true', + '"target": "es2022"', + '"target": "es2022", "allowJs": true', ); const overrides = { watch: true }; diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/aot_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/aot_spec.ts index 44f2cba1eca4..ae6b620aa37c 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/aot_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/aot_spec.ts @@ -27,11 +27,11 @@ describe('Browser Builder AOT', () => { const run = await architect.scheduleTarget(targetSpec, overrides); const output = (await run.result) as BrowserBuilderOutput; - expect(output.success).toBe(true); + expect(output.success).toBeTrue(); - const fileName = join(normalize(output.outputPath), 'main.js'); + const fileName = join(normalize(output.outputs[0].path), 'main.js'); const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise()); - expect(content).toContain('AppComponent.ɵcmp'); + expect(content).toContain('AppComponent_Factory'); await run.stop(); }); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/lazy-module_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/lazy-module_spec.ts index 5e025a65cb81..fa0f7c381b1a 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/lazy-module_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/lazy-module_spec.ts @@ -65,8 +65,7 @@ describe('Browser Builder lazy modules', () => { const { files } = await browserBuild(architect, host, target, { aot: true }); const data = await files['src_app_lazy_lazy_module_ts.js']; - expect(data).not.toBeUndefined(); - expect(data).toContain('LazyModule.ɵmod'); + expect(data).toContain('this.ɵmod'); }); }); @@ -126,7 +125,7 @@ describe('Browser Builder lazy modules', () => { }); const { files } = await browserBuild(architect, host, target); - expect(files['src_lazy-module_ts.js']).not.toBeUndefined(); + expect(files['src_lazy-module_ts.js']).toBeDefined(); }); it(`supports lazy bundle for dynamic import() calls`, async () => { @@ -140,7 +139,7 @@ describe('Browser Builder lazy modules', () => { host.replaceInFile('src/tsconfig.app.json', '"main.ts"', `"main.ts","lazy-module.ts"`); const { files } = await browserBuild(architect, host, target); - expect(files['lazy-module.js']).not.toBeUndefined(); + expect(files['lazy-module.js']).toBeDefined(); }); it(`supports making a common bundle for shared lazy modules`, async () => { @@ -151,8 +150,8 @@ describe('Browser Builder lazy modules', () => { }); const { files } = await browserBuild(architect, host, target); - expect(files['src_one_ts.js']).not.toBeUndefined(); - expect(files['src_two_ts.js']).not.toBeUndefined(); + expect(files['src_one_ts.js']).toBeDefined(); + expect(files['src_two_ts.js']).toBeDefined(); expect(files['default-node_modules_angular_common_fesm2020_http_mjs.js']).toBeDefined(); }); @@ -164,8 +163,8 @@ describe('Browser Builder lazy modules', () => { }); const { files } = await browserBuild(architect, host, target, { commonChunk: false }); - expect(files['src_one_ts.js']).not.toBeUndefined(); - expect(files['src_two_ts.js']).not.toBeUndefined(); + expect(files['src_one_ts.js']).toBeDefined(); + expect(files['src_two_ts.js']).toBeDefined(); expect(files['default-node_modules_angular_common_fesm2020_http_mjs.js']).toBeUndefined(); }); }); 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 7cee64ef8497..c47fd03462e7 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 @@ -29,8 +29,8 @@ describe('Browser Builder resolve json module', () => { host.replaceInFile( 'tsconfig.json', - '"target": "es2020"', - '"target": "es2020", "resolveJsonModule": true', + '"target": "es2022"', + '"target": "es2022", "resolveJsonModule": true', ); const overrides = { watch: true }; 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 index e1f46f8efc69..7e488ddcbc2b 100644 --- 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 @@ -65,12 +65,12 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { }); it('warns when IE is present in browserslist', async () => { - await harness.writeFile( + await harness.appendToFile( '.browserslistrc', ` - IE 9 - IE 11 - `, + IE 9 + IE 11 + `, ); harness.useTarget('build', { @@ -84,9 +84,9 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { 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`, + `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', }), ); }); @@ -96,12 +96,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', { diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts index bbd5872ad711..51d663d61dcf 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts @@ -42,7 +42,11 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => { }; describe('Behavior: "dev-server builder serves service worker"', () => { - beforeEach(() => { + beforeEach(async () => { + // Application code is not needed for these tests + await harness.writeFile('src/main.ts', ''); + await harness.writeFile('src/polyfills.ts', ''); + harness.useProject('test', { root: '.', sourceRoot: 'src', 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 554d7d333e0a..62737cbbb14c 100644 --- a/packages/angular_devkit/build_angular/src/builders/server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/server/index.ts @@ -152,6 +152,7 @@ async function initialize( context, (wco) => { // We use the platform to determine the JavaScript syntax output. + wco.buildOptions.supportedBrowsers ??= []; wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions')); return [getPlatformServerExportsConfig(wco), getCommonConfig(wco), getStylesConfig(wco)]; diff --git a/packages/angular_devkit/build_angular/src/utils/build-options.ts b/packages/angular_devkit/build_angular/src/utils/build-options.ts index 9282353cc1a2..a986b9786415 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-options.ts @@ -72,7 +72,7 @@ export interface BuildOptions { cache: NormalizedCachedOptions; codeCoverage?: boolean; codeCoverageExclude?: string[]; - supportedBrowsers: string[]; + supportedBrowsers?: string[]; } export interface WebpackDevServerOptions @@ -87,6 +87,5 @@ export interface WebpackConfigOptions { buildOptions: T; tsConfig: ParsedConfiguration; tsConfigPath: string; - scriptTarget: import('typescript').ScriptTarget; projectName: 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 new file mode 100644 index 000000000000..276adf234690 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/esbuild-targets.ts @@ -0,0 +1,43 @@ +/** + * @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 + */ + +/** + * Transform browserlists result to esbuild target. + * @see https://esbuild.github.io/api/#target + */ +export function transformSupportedBrowsersToTargets(supportedBrowsers: string[]): string[] { + const transformed: string[] = []; + + // https://esbuild.github.io/api/#target + const esBuildSupportedBrowsers = new Set(['safari', 'firefox', 'edge', 'chrome', 'ios', 'node']); + + for (const browser of supportedBrowsers) { + let [browserName, version] = browser.split(' '); + + // browserslist uses the name `ios_saf` for iOS Safari whereas esbuild uses `ios` + if (browserName === 'ios_saf') { + browserName = 'ios'; + } + + // browserslist uses ranges `15.2-15.3` versions but only the lowest is required + // to perform minimum supported feature checks. esbuild also expects a single version. + [version] = version.split('-'); + + if (esBuildSupportedBrowsers.has(browserName)) { + if (browserName === 'safari' && version === 'TP') { + // 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'; + } + + transformed.push(browserName + version); + } + } + + return transformed; +} 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 b0d2686dbd36..abdcfa35c863 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 @@ -42,9 +42,6 @@ export async function generateWebpackConfig( const tsConfigPath = path.resolve(workspaceRoot, options.tsConfig); const tsConfig = await readTsconfig(tsConfigPath); - const ts = await import('typescript'); - const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES2015; - const buildOptions: NormalizedBrowserBuilderSchema = { ...options, ...extraBuildOptions }; const wco: BrowserWebpackConfigOptions = { root: workspaceRoot, @@ -55,7 +52,6 @@ export async function generateWebpackConfig( tsConfig, tsConfigPath, projectName, - scriptTarget, }; wco.buildOptions.progress = defaultProgress(wco.buildOptions.progress); 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 636e9ef073da..be04758a5bc6 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -40,7 +40,6 @@ import { externalizePackages, getCacheSettings, getInstrumentationExcludedPaths, - getMainFieldsAndConditionNames, getOutputHashFormat, getStatsOptions, globalScriptsByBundleName, @@ -50,16 +49,7 @@ const VENDORS_TEST = /[\\/]node_modules[\\/]/; // eslint-disable-next-line max-lines-per-function export async function getCommonConfig(wco: WebpackConfigOptions): Promise { - const { - root, - projectRoot, - buildOptions, - tsConfig, - projectName, - sourceRoot, - tsConfigPath, - scriptTarget, - } = wco; + const { root, projectRoot, buildOptions, tsConfig, projectName, sourceRoot, tsConfigPath } = wco; const { cache, codeCoverage, @@ -270,7 +260,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise, advanced: boolean | undefined, ): Promise<{ code: string; map?: object }> { const result = await minify( @@ -193,7 +190,8 @@ async function optimizeWithTerser( passes: advanced ? 2 : 1, pure_getters: advanced, }, - ecma: target, + // terser only supports up to ES2020 + ecma: 2020, // esbuild in the first pass is used to minify identifiers instead of mangle here mangle: false, // esbuild in the first pass is used to minify function names 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 7aa644732d05..31e5e453b934 100644 --- a/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/plugins/typescript.ts @@ -25,9 +25,16 @@ export function createIvyPlugin( declarationMap: false, }; - 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.', + if (tsConfig.options.target === undefined || tsConfig.options.target < ScriptTarget.ES2022) { + tsConfig.options.target = ScriptTarget.ES2022; + // If 'useDefineForClassFields' is already defined in the users project leave the value as is. + // Othereise fallback to false to due https://github.com/microsoft/TypeScript/issues/45995 + // which breaks the deprecated @Effects NGRX decorator. + tsConfig.options.useDefineForClassFields ??= false; + + wco.logger.warn( + 'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' + + '"false" respectively by the Angular CLI. To control ECMA version and features use the Browerslist configuration.', ); } diff --git a/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts b/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts index 9fc774bb8369..022842e0239a 100644 --- a/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts +++ b/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts @@ -11,7 +11,6 @@ import { createHash } from 'crypto'; import { existsSync } from 'fs'; import glob from 'glob'; import * as path from 'path'; -import { ScriptTarget } from 'typescript'; import type { Configuration, WebpackOptionsNormalized } from 'webpack'; import { AssetPatternClass, @@ -317,23 +316,3 @@ export function getStatsOptions(verbose = false): WebpackStatsOptions { ? { ...webpackOutputOptions, ...verboseWebpackOutputOptions } : webpackOutputOptions; } - -export function getMainFieldsAndConditionNames( - target: ScriptTarget, - platformServer: boolean, -): Pick { - const mainFields = platformServer - ? ['es2015', 'module', 'main'] - : ['es2015', 'browser', 'module', 'main']; - const conditionNames = ['es2015', '...']; - - if (target >= ScriptTarget.ES2020) { - mainFields.unshift('es2020'); - conditionNames.unshift('es2020'); - } - - return { - mainFields, - conditionNames, - }; -} diff --git a/packages/angular_devkit/build_angular/test/hello-world-app/tsconfig.json b/packages/angular_devkit/build_angular/test/hello-world-app/tsconfig.json index 91d00e2ae8f7..26bec6bf178a 100644 --- a/packages/angular_devkit/build_angular/test/hello-world-app/tsconfig.json +++ b/packages/angular_devkit/build_angular/test/hello-world-app/tsconfig.json @@ -8,13 +8,14 @@ "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es2020", - "module": "es2020", + "target": "es2022", + "module": "es2022", + "useDefineForClassFields": false, "typeRoots": [ "node_modules/@types" ], "lib": [ - "es2020", + "es2022", "dom" ] }, diff --git a/packages/angular_devkit/build_angular/test/hello-world-lib/tsconfig.json b/packages/angular_devkit/build_angular/test/hello-world-lib/tsconfig.json index 455f55ed0b41..26bfbc225506 100644 --- a/packages/angular_devkit/build_angular/test/hello-world-lib/tsconfig.json +++ b/packages/angular_devkit/build_angular/test/hello-world-lib/tsconfig.json @@ -8,12 +8,13 @@ "moduleResolution": "node", "experimentalDecorators": true, "target": "es2015", - "module": "es2020", + "module": "es2022", + "useDefineForClassFields": false, "typeRoots": [ "node_modules/@types" ], "lib": [ - "es2020", + "es2022", "dom" ] }, diff --git a/packages/angular_devkit/build_webpack/test/angular-app/src/tsconfig.app.json b/packages/angular_devkit/build_webpack/test/angular-app/src/tsconfig.app.json index c1ec1d179974..4191001bb4d4 100644 --- a/packages/angular_devkit/build_webpack/test/angular-app/src/tsconfig.app.json +++ b/packages/angular_devkit/build_webpack/test/angular-app/src/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", - "module": "es2020", + "module": "es2022", "types": [] }, "exclude": ["test.ts", "**/*.spec.ts"] diff --git a/packages/angular_devkit/build_webpack/test/angular-app/tsconfig.json b/packages/angular_devkit/build_webpack/test/angular-app/tsconfig.json index aa337c9e758d..5e7ab16f0c42 100644 --- a/packages/angular_devkit/build_webpack/test/angular-app/tsconfig.json +++ b/packages/angular_devkit/build_webpack/test/angular-app/tsconfig.json @@ -8,9 +8,9 @@ "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es2020", + "target": "es2022", "typeRoots": ["node_modules/@types"], - "lib": ["es2020", "dom"] + "lib": ["es2022", "dom"] }, "angularCompilerOptions": { "enableIvy": true, diff --git a/packages/angular_devkit/build_webpack/test/basic-app/tsconfig.json b/packages/angular_devkit/build_webpack/test/basic-app/tsconfig.json index caa9637b15dc..985646bca926 100644 --- a/packages/angular_devkit/build_webpack/test/basic-app/tsconfig.json +++ b/packages/angular_devkit/build_webpack/test/basic-app/tsconfig.json @@ -7,10 +7,10 @@ "declaration": false, "moduleResolution": "node", "experimentalDecorators": true, - "target": "es2020", + "target": "es2022", "module": "esnext", "typeRoots": ["node_modules/@types"], - "lib": ["es2020", "dom"] + "lib": ["es2022", "dom"] }, "angularCompilerOptions": { "disableTypeScriptVersionCheck": true diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index ac1dcc473d78..ff029d1852ce 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -5,10 +5,17 @@ "factory": "./update-15/remove-browserslist-config", "description": "Remove Browserslist configuration files that matches the Angular CLI default configuration." }, +<<<<<<< HEAD "remove-platform-server-exports": { "version": "15.0.0", "factory": "./update-15/remove-platform-server-exports", "description": "Remove exported `@angular/platform-server` `renderModule` method. The `renderModule` method is now exported by the Angular CLI." +======= + "update-typescript-target": { + "version": "15.0.0", + "factory": "./update-15/update-typescript-target", + "description": "Update TypeScript compiler `target` and set `useDefineForClassFields`. These changes are for IDE purposes as TypeScript compiler options `target` and `useDefineForClassFields` are set to `ES2022` and `false` respectively by the Angular CLI. To control ECMA version and features use the Browerslist configuration." +>>>>>>> aec56ac85 (feat(@angular-devkit/build-angular): use Browserslist to determine ECMA output) } } } diff --git a/packages/schematics/angular/migrations/update-15/update-typescript-target.ts b/packages/schematics/angular/migrations/update-15/update-typescript-target.ts new file mode 100644 index 000000000000..008bf26e893d --- /dev/null +++ b/packages/schematics/angular/migrations/update-15/update-typescript-target.ts @@ -0,0 +1,77 @@ +/** + * @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 { JsonObject } from '@angular-devkit/core'; +import { Rule, Tree } from '@angular-devkit/schematics'; +import { JSONFile } from '../../utility/json-file'; +import { getWorkspace } from '../../utility/workspace'; +import { Builders } from '../../utility/workspace-models'; + +export default function (): Rule { + return async (host) => { + // Workspace level tsconfig + updateTarget(host, 'tsconfig.json'); + + const workspace = await getWorkspace(host); + + // Find all tsconfig which are refereces used by builders + for (const [, project] of workspace.projects) { + for (const [, target] of project.targets) { + // Update all other known CLI builders that use a tsconfig + const tsConfigs = [target.options || {}, ...Object.values(target.configurations || {})] + .filter((opt) => typeof opt?.tsConfig === 'string') + .map((opt) => (opt as { tsConfig: string }).tsConfig); + + const uniqueTsConfigs = [...new Set(tsConfigs)]; + + if (uniqueTsConfigs.length < 1) { + continue; + } + + switch (target.builder as Builders) { + case Builders.Server: + case Builders.Karma: + case Builders.Browser: + case Builders.NgPackagr: + for (const tsConfig of uniqueTsConfigs) { + removeOrUpdateTarget(host, tsConfig); + } + break; + } + } + } + }; +} + +function removeOrUpdateTarget(host: Tree, tsConfigPath: string): void { + const json = new JSONFile(host, tsConfigPath); + if (typeof json.get(['extends']) === 'string') { + json.remove(['compilerOptions', 'target']); + } else { + updateTarget(host, tsConfigPath); + } +} + +const ESNEXT_ES2022_REGEXP = /^es(?:next|2022)$/i; +function updateTarget(host: Tree, tsConfigPath: string): void { + const json = new JSONFile(host, tsConfigPath); + const jsonPath = ['compilerOptions']; + const compilerOptions = json.get(jsonPath); + + if (compilerOptions && typeof compilerOptions === 'object') { + const { target } = compilerOptions as JsonObject; + + if (typeof target === 'string' && !ESNEXT_ES2022_REGEXP.test(target)) { + json.modify(jsonPath, { + ...compilerOptions, + 'target': 'ES2022', + 'useDefineForClassFields': false, + }); + } + } +} diff --git a/packages/schematics/angular/migrations/update-15/update-typescript-target_spec.ts b/packages/schematics/angular/migrations/update-15/update-typescript-target_spec.ts new file mode 100644 index 000000000000..87b4e0b9fa7d --- /dev/null +++ b/packages/schematics/angular/migrations/update-15/update-typescript-target_spec.ts @@ -0,0 +1,147 @@ +/** + * @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 { isJsonObject } from '@angular-devkit/core'; +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +describe('Migration to update target and add useDefineForClassFields', () => { + const schematicName = 'update-typescript-target'; + + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + function createJsonFile(tree: EmptyTree, filePath: string, content: {}): void { + const stringifiedContent = JSON.stringify(content, undefined, 2); + if (tree.exists(filePath)) { + tree.overwrite(filePath, stringifiedContent); + } else { + tree.create(filePath, stringifiedContent); + } + } + + function getCompilerOptionsValue(tree: UnitTestTree, filePath: string): Record { + const json = tree.readJson(filePath); + if (isJsonObject(json) && isJsonObject(json.compilerOptions)) { + return json.compilerOptions; + } + + throw new Error(`Cannot retrieve 'compilerOptions'.`); + } + + function createWorkSpaceConfig(tree: EmptyTree) { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + build: { + builder: Builders.Browser, + options: { + tsConfig: 'src/tsconfig.app.json', + main: '', + polyfills: '', + }, + configurations: { + production: { + tsConfig: 'src/tsconfig.app.prod.json', + }, + }, + }, + test: { + builder: Builders.Karma, + options: { + karmaConfig: '', + tsConfig: 'src/tsconfig.spec.json', + }, + }, + }, + }, + }, + }; + + createJsonFile(tree, 'angular.json', angularConfig); + } + + let tree: EmptyTree; + beforeEach(() => { + tree = new EmptyTree(); + createWorkSpaceConfig(tree); + + // Create tsconfigs + const compilerOptions = { target: 'es2015', module: 'es2020' }; + const configWithExtends = { extends: './tsconfig.json', compilerOptions }; + + // Workspace + createJsonFile(tree, 'tsconfig.json', { compilerOptions }); + + // Application + createJsonFile(tree, 'src/tsconfig.app.json', configWithExtends); + createJsonFile(tree, 'src/tsconfig.app.prod.json', configWithExtends); + createJsonFile(tree, 'src/tsconfig.spec.json', { compilerOptions }); + }); + + it(`should update target and add useDefineForClassFields in workspace 'tsconfig.json'`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const compilerOptions = getCompilerOptionsValue(newTree, 'tsconfig.json'); + expect(compilerOptions).toEqual( + jasmine.objectContaining({ + target: 'ES2022', + useDefineForClassFields: false, + }), + ); + }); + + it(`should remove target value from tsconfig referenced in options and configuration`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + { + const compilerOptions = getCompilerOptionsValue(newTree, 'src/tsconfig.app.prod.json'); + expect(compilerOptions['target']).toBeUndefined(); + expect(compilerOptions['useDefineForClassFields']).toBeUndefined(); + } + { + const compilerOptions = getCompilerOptionsValue(newTree, 'src/tsconfig.app.json'); + expect(compilerOptions['target']).toBeUndefined(); + expect(compilerOptions['useDefineForClassFields']).toBeUndefined(); + } + }); + + it('should add target and useDefineForClassFields when tsconfig is not extended', async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const compilerOptions = getCompilerOptionsValue(newTree, 'src/tsconfig.spec.json'); + expect(compilerOptions).toEqual( + jasmine.objectContaining({ + target: 'ES2022', + useDefineForClassFields: false, + }), + ); + }); + + it('should not add useDefineForClassFields when tsconfig target is ES2022', async () => { + createJsonFile(tree, 'tsconfig.json', { compilerOptions: { 'target': 'es2022' } }); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + + const compilerOptions = getCompilerOptionsValue(newTree, 'tsconfig.json'); + expect(compilerOptions).toEqual({ target: 'es2022' }); + }); + + it('should not add useDefineForClassFields when tsconfig target is ESNEXT', async () => { + createJsonFile(tree, 'tsconfig.json', { compilerOptions: { 'target': 'esnext' } }); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + + const compilerOptions = getCompilerOptionsValue(newTree, 'tsconfig.json'); + expect(compilerOptions).toEqual({ target: 'esnext' }); + }); +}); diff --git a/packages/schematics/angular/workspace/files/tsconfig.json.template b/packages/schematics/angular/workspace/files/tsconfig.json.template index 81dfa6bef428..cac9dd40cf28 100644 --- a/packages/schematics/angular/workspace/files/tsconfig.json.template +++ b/packages/schematics/angular/workspace/files/tsconfig.json.template @@ -16,10 +16,11 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2020", - "module": "es2020", + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, "lib": [ - "es2020", + "ES2022", "dom" ] }, diff --git a/tests/legacy-cli/e2e/assets/12.0-project/tsconfig.json b/tests/legacy-cli/e2e/assets/12.0-project/tsconfig.json index 6df828326e3f..4977e18060ed 100644 --- a/tests/legacy-cli/e2e/assets/12.0-project/tsconfig.json +++ b/tests/legacy-cli/e2e/assets/12.0-project/tsconfig.json @@ -14,10 +14,11 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2017", - "module": "es2020", + "target": "es2022", + "module": "es2022", + "useDefineForClassFields": false, "lib": [ - "es2018", + "ES2022", "dom" ] }, diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json b/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json index 46d83664cba4..0102307af01b 100644 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json +++ b/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "baseUrl": "", - "module": "es2020", + "module": "es2022", "moduleResolution": "node", "target": "es2015", "noImplicitAny": false, diff --git a/tests/legacy-cli/e2e/tests/build/scripts-output-hashing.ts b/tests/legacy-cli/e2e/tests/build/scripts-output-hashing.ts index 430b7a8478ac..925969b68da8 100644 --- a/tests/legacy-cli/e2e/tests/build/scripts-output-hashing.ts +++ b/tests/legacy-cli/e2e/tests/build/scripts-output-hashing.ts @@ -1,4 +1,9 @@ -import { expectFileMatchToExist, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; +import { + expectFileMatchToExist, + expectFileToMatch, + writeFile, + writeMultipleFiles, +} from '../../utils/fs'; import { ng } from '../../utils/process'; import { updateJsonFile, updateTsConfig } from '../../utils/project'; @@ -23,17 +28,16 @@ export default async function () { build.configurations['production'].outputHashing = 'all'; configJson['cli'] = { cache: { enabled: 'false' } }; }); - await updateTsConfig((json) => { - json['compilerOptions']['target'] = 'es2017'; - json['compilerOptions']['module'] = 'es2020'; - }); + + // Chrome 65 does not support optional catch in try/catch blocks. + await writeFile('.browserslistrc', 'Chrome 65'); + await ng('build', '--configuration=production'); const filenameBuild1 = await getScriptsFilename(); await expectFileToMatch(`dist/test-project/${filenameBuild1}`, 'try{console.log()}catch(c){}'); - await updateTsConfig((json) => { - json['compilerOptions']['target'] = 'es2019'; - }); + await writeFile('.browserslistrc', 'last 1 Chrome version'); + await ng('build', '--configuration=production'); const filenameBuild2 = await getScriptsFilename(); await expectFileToMatch(`dist/test-project/${filenameBuild2}`, 'try{console.log()}catch{}');