From 69ecddaa7d8b01aa7a9e61c403a4b9a8669e34c4 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 28 Jan 2022 13:36:29 +0100 Subject: [PATCH] feat(@schematics/angular): update new and existing projects compilation target to `ES2020` With this change we update the TypeScript compilation target to `ES2020` for both new and existing projects. This is because all browsers that Angular supports (https://angular.io/guide/browser-support) support `ES2020` features without the need for polyfills. --- .../builders/browser/specs/allow-js_spec.ts | 6 +- .../browser/specs/lazy-module_spec.ts | 4 +- .../browser/specs/resolve-json-module_spec.ts | 2 +- .../test/hello-world-app/tsconfig.json | 4 +- .../test/hello-world-lib/tsconfig.json | 2 +- .../test/angular-app/tsconfig.json | 4 +- .../test/basic-app/tsconfig.json | 4 +- .../migrations/migration-collection.json | 5 + .../update-14/update-tsconfig-target.ts | 56 ++++++++ .../update-14/update-tsconfig-target_spec.ts | 127 ++++++++++++++++++ .../workspace/files/tsconfig.json.template | 2 +- .../legacy-cli/e2e/tests/build/prod-build.ts | 6 +- .../e2e/tests/i18n/ivy-localize-es2015.ts | 5 - 13 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 packages/schematics/angular/migrations/update-14/update-tsconfig-target.ts create mode 100644 packages/schematics/angular/migrations/update-14/update-tsconfig-target_spec.ts 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 2f846fb77b..6f856d5441 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,7 @@ describe('Browser Builder allow js', () => { 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, }); - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5", "allowJs": true'); + host.replaceInFile('tsconfig.json', '"target": "es2020"', '"target": "es5", "allowJs": true'); const run = await architect.scheduleTarget(targetSpec); const output = (await run.result) as BrowserBuilderOutput; @@ -50,7 +50,7 @@ describe('Browser Builder allow js', () => { 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, }); - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5", "allowJs": true'); + host.replaceInFile('tsconfig.json', '"target": "es2020"', '"target": "es5", "allowJs": true'); const overrides = { aot: true }; @@ -73,7 +73,7 @@ describe('Browser Builder allow js', () => { 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, }); - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5", "allowJs": true'); + host.replaceInFile('tsconfig.json', '"target": "es2020"', '"target": "es5", "allowJs": true'); const overrides = { watch: true }; 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 0631d4a610..016632fecc 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 @@ -152,7 +152,7 @@ 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['default-node_modules_angular_common_fesm2015_http_mjs.js']).toBeDefined(); + expect(files['default-node_modules_angular_common_fesm2020_http_mjs.js']).toBeDefined(); }); it(`supports disabling the common bundle`, async () => { @@ -165,6 +165,6 @@ 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['default-node_modules_angular_common_fesm2015_http_mjs.js']).toBeUndefined(); + 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 9c8f90bca5..52ff4f9fcb 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,7 +29,7 @@ describe('Browser Builder resolve json module', () => { host.replaceInFile( 'tsconfig.json', - '"target": "es2017"', + '"target": "es2020"', '"target": "es5", "resolveJsonModule": true', ); 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 956e76c763..91d00e2ae8 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,13 @@ "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es2017", + "target": "es2020", "module": "es2020", "typeRoots": [ "node_modules/@types" ], "lib": [ - "es2017", + "es2020", "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 2c5d470a72..455f55ed0b 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 @@ -13,7 +13,7 @@ "node_modules/@types" ], "lib": [ - "es2017", + "es2020", "dom" ] }, 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 1807c9908f..aa337c9e75 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": "es2017", + "target": "es2020", "typeRoots": ["node_modules/@types"], - "lib": ["es2017", "dom"] + "lib": ["es2020", "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 890391f42f..caa9637b15 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": "es2017", + "target": "es2020", "module": "esnext", "typeRoots": ["node_modules/@types"], - "lib": ["es2017", "dom"] + "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "disableTypeScriptVersionCheck": true diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index e2a7e796d1..14b6324f0d 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -4,6 +4,11 @@ "version": "14.0.0", "factory": "./update-14/angular-packages-version-prefix", "description": "Update Angular packages 'dependencies' and 'devDependencies' version prefix to '^' instead of '~'." + }, + "update-tsconfig-target": { + "version": "14.0.0", + "factory": "./update-14/update-tsconfig-target", + "description": "Update TypeScript compilation target to 'ES2020'." } } } diff --git a/packages/schematics/angular/migrations/update-14/update-tsconfig-target.ts b/packages/schematics/angular/migrations/update-14/update-tsconfig-target.ts new file mode 100644 index 0000000000..9ffaf3a198 --- /dev/null +++ b/packages/schematics/angular/migrations/update-14/update-tsconfig-target.ts @@ -0,0 +1,56 @@ +/** + * @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 { Rule } from '@angular-devkit/schematics'; +import { JSONFile } from '../../utility/json-file'; +import { getWorkspace } from '../../utility/workspace'; +import { Builders } from '../../utility/workspace-models'; + +/** Migration to update tsconfig compilation target option to es2020. */ +export default function (): Rule { + return async (host) => { + /** Builders for which the migration will run. */ + const supportedBuilders = [Builders.Karma, Builders.NgPackagr, Builders.Browser]; + + /** Compilation targets values that should not be amended. */ + const skipTargets = ['es2020', 'es2021', 'es2022', 'esnext']; + + const uniqueTsConfigs = new Set(['/tsconfig.json']); + + // Find all tsconfig files which are refereced by the builders. + const workspace = await getWorkspace(host); + for (const project of workspace.projects.values()) { + for (const target of project.targets.values()) { + if (!supportedBuilders.includes(target.builder as Builders)) { + // Unknown builder. + continue; + } + + // Update all other known CLI builders that use a tsconfig. + const allOptions = [target.options ?? {}, ...Object.values(target.configurations ?? {})]; + for (const opt of allOptions) { + if (typeof opt?.tsConfig === 'string') { + uniqueTsConfigs.add(opt.tsConfig); + } + } + } + } + + // Modify tsconfig files + const targetJsonPath = ['compilerOptions', 'target']; + for (const tsConfigPath of uniqueTsConfigs) { + const json = new JSONFile(host, tsConfigPath); + const target = json.get(targetJsonPath); + + // Update compilation target when it's current set lower than es2020. + if (typeof target === 'string' && !skipTargets.includes(target.toLowerCase())) { + json.modify(targetJsonPath, 'es2020'); + } + } + }; +} diff --git a/packages/schematics/angular/migrations/update-14/update-tsconfig-target_spec.ts b/packages/schematics/angular/migrations/update-14/update-tsconfig-target_spec.ts new file mode 100644 index 0000000000..63c303244a --- /dev/null +++ b/packages/schematics/angular/migrations/update-14/update-tsconfig-target_spec.ts @@ -0,0 +1,127 @@ +/** + * @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 { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { parse as parseJson } from 'jsonc-parser'; +import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +describe('Migration to update target compiler options', () => { + const schematicName = 'update-tsconfig-target'; + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + function createJsonFile(tree: UnitTestTree, filePath: string, content: {}) { + tree.create(filePath, JSON.stringify(content, undefined, 2)); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function readJsonFile(tree: UnitTestTree, filePath: string): any { + return parseJson(tree.readContent(filePath).toString()); + } + + let tree: UnitTestTree; + + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + + // Workspace configuration + 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', + }, + }, + server: { + builder: Builders.Server, + options: { + tsConfig: 'src/tsconfig.server.json', + outputPath: '', + main: '', + }, + }, + }, + }, + }, + }; + + createJsonFile(tree, 'angular.json', angularConfig); + + // Create tsconfigs + const compilerOptions = { target: 'es5', module: 'esnext' }; + + // Workspace + createJsonFile(tree, 'tsconfig.json', { compilerOptions }); + + // Application + createJsonFile(tree, 'src/tsconfig.app.json', { compilerOptions }); + createJsonFile(tree, 'src/tsconfig.app.prod.json', { compilerOptions }); + createJsonFile(tree, 'src/tsconfig.spec.json', { compilerOptions }); + + // Server + createJsonFile(tree, 'src/tsconfig.server.json', { compilerOptions }); + }); + + it(`should update target in workspace 'tsconfig.json'`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { target } = readJsonFile(newTree, 'tsconfig.json').compilerOptions; + expect(target).toBe('es2020'); + }); + + it(`should update target in 'tsconfig.json' which is referenced in option`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { target } = readJsonFile(newTree, 'src/tsconfig.spec.json').compilerOptions; + expect(target).toBe('es2020'); + }); + + it(`should update target in 'tsconfig.json' which is referenced in a configuration`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { target } = readJsonFile(newTree, 'src/tsconfig.app.prod.json').compilerOptions; + expect(target).toBe('es2020'); + }); + + it(`should not update target in 'tsconfig.server.json'`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { target } = readJsonFile(newTree, 'src/tsconfig.server.json').compilerOptions; + expect(target).toBe('es5'); + }); + + it('should not update target if it is greater than es2020', async () => { + const tsConfigPath = 'src/tsconfig.app.json'; + tree.delete(tsConfigPath); + createJsonFile(tree, tsConfigPath, { compilerOptions: { target: 'es2021' } }); + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + const { target } = readJsonFile(newTree, tsConfigPath).compilerOptions; + expect(target).toBe('es2021'); + }); +}); diff --git a/packages/schematics/angular/workspace/files/tsconfig.json.template b/packages/schematics/angular/workspace/files/tsconfig.json.template index 977a7292be..81dfa6bef4 100644 --- a/packages/schematics/angular/workspace/files/tsconfig.json.template +++ b/packages/schematics/angular/workspace/files/tsconfig.json.template @@ -16,7 +16,7 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2017", + "target": "es2020", "module": "es2020", "lib": [ "es2020", diff --git a/tests/legacy-cli/e2e/tests/build/prod-build.ts b/tests/legacy-cli/e2e/tests/build/prod-build.ts index 38850965ea..22cdf5d254 100644 --- a/tests/legacy-cli/e2e/tests/build/prod-build.ts +++ b/tests/legacy-cli/e2e/tests/build/prod-build.ts @@ -37,11 +37,11 @@ export default async function () { await expectFileToMatch('dist/test-project/3rdpartylicenses.txt', /MIT/); const indexContent = await readFile('dist/test-project/index.html'); - const mainES2017Path = indexContent.match(/src="(main\.[a-z0-9]{0,32}\.js)"/)[1]; + const mainPath = indexContent.match(/src="(main\.[a-z0-9]{0,32}\.js)"/)[1]; // Content checks - await expectFileToMatch(`dist/test-project/${mainES2017Path}`, bootstrapRegExp); + await expectFileToMatch(`dist/test-project/${mainPath}`, bootstrapRegExp); // Size checks in bytes - verifySize(mainES2017Path, 141032); + verifySize(mainPath, 141032); } diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts index 0c9171dc74..287f826ff4 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts @@ -1,6 +1,5 @@ import { expectFileNotToExist, expectFileToMatch, readFile, writeFile } from '../../utils/fs'; import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; import { expectToFail } from '../../utils/utils'; import { externalServer, langTranslations, setupI18nConfig } from './setup'; @@ -8,11 +7,7 @@ export default async function () { // Setup i18n tests and config. await setupI18nConfig(); - // Ensure a es2017 build is used. await writeFile('.browserslistrc', 'Chrome 65'); - await updateJsonFile('tsconfig.json', (config) => { - config.compilerOptions.target = 'es2017'; - }); await ng('build', '--source-map'); for (const { lang, outputPath, translation } of langTranslations) {