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.
This commit is contained in:
Alan Agius 2022-01-28 13:36:29 +01:00 committed by Douglas Parker
parent b3082355c0
commit 69ecddaa7d
13 changed files with 205 additions and 22 deletions

View File

@ -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 };

View File

@ -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();
});
});

View File

@ -29,7 +29,7 @@ describe('Browser Builder resolve json module', () => {
host.replaceInFile(
'tsconfig.json',
'"target": "es2017"',
'"target": "es2020"',
'"target": "es5", "resolveJsonModule": true',
);

View File

@ -8,13 +8,13 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2017",
"target": "es2020",
"module": "es2020",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2017",
"es2020",
"dom"
]
},

View File

@ -13,7 +13,7 @@
"node_modules/@types"
],
"lib": [
"es2017",
"es2020",
"dom"
]
},

View File

@ -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,

View File

@ -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

View File

@ -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'."
}
}
}

View File

@ -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');
}
}
};
}

View File

@ -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');
});
});

View File

@ -16,7 +16,7 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"target": "es2020",
"module": "es2020",
"lib": [
"es2020",

View File

@ -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);
}

View File

@ -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) {