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..59202089ea47 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. + // Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995 + // which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well. + 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..043919f1e2cf 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. + // Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995 + // which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well. + 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..7ca61d6466de 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -9,6 +9,11 @@ "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. For more information, see https://github.com/browserslist/browserslist" } } } 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{}');