Skip to content

Commit 77741f5

Browse files
committed
fix(@schematics/angular): add 'update-typescript-lib' migration
This migration updates the TypeScript `lib` configuration to `es2022` or a newer version if a more modern one is already in use. This change ensures that projects are configured to support modern ECMAScript features that are commonly used in the ecosystem and required by recent versions of Angular and its dependencies. It helps prevent compilation errors related to newer language and library features.
1 parent afa2738 commit 77741f5

File tree

3 files changed

+258
-0
lines changed

3 files changed

+258
-0
lines changed

packages/schematics/angular/migrations/migration-collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
"version": "21.0.0",
1414
"factory": "./karma/migration",
1515
"description": "Remove any karma configuration files that only contain the default content. The default configuration is automatically available without a specific project file."
16+
},
17+
"update-typescript-lib": {
18+
"version": "21.0.0",
19+
"factory": "./update-typescript-lib/migration",
20+
"description": "Updates the 'lib' property in tsconfig files to use 'es2022' or a more modern version."
1621
}
1722
}
1823
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { Rule, Tree } from '@angular-devkit/schematics';
10+
import { JSONFile } from '../../utility/json-file';
11+
import { getWorkspace } from '../../utility/workspace';
12+
13+
export default function (): Rule {
14+
return async (host, context) => {
15+
// Workspace level tsconfig
16+
if (host.exists('tsconfig.json')) {
17+
updateLib(host, 'tsconfig.json');
18+
}
19+
20+
const workspace = await getWorkspace(host);
21+
22+
// Find all tsconfig which are references used by builders
23+
for (const [, project] of workspace.projects) {
24+
for (const [targetName, target] of project.targets) {
25+
if (!target.options) {
26+
continue;
27+
}
28+
29+
// Update all other known CLI builders that use a tsconfig
30+
const tsConfigs = [target.options, ...Object.values(target.configurations || {})]
31+
.filter((opt) => typeof opt?.tsConfig === 'string')
32+
.map((opt) => (opt as { tsConfig: string }).tsConfig);
33+
34+
const uniqueTsConfigs = new Set(tsConfigs);
35+
for (const tsConfig of uniqueTsConfigs) {
36+
if (host.exists(tsConfig)) {
37+
updateLib(host, tsConfig);
38+
} else {
39+
context.logger.warn(
40+
`'${tsConfig}' referenced in the '${targetName}' target does not exist.`,
41+
);
42+
}
43+
}
44+
}
45+
}
46+
};
47+
}
48+
49+
function updateLib(host: Tree, tsConfigPath: string): void {
50+
const json = new JSONFile(host, tsConfigPath);
51+
const jsonPath = ['compilerOptions', 'lib'];
52+
const lib = json.get(jsonPath) as string[] | undefined;
53+
54+
if (!lib || !Array.isArray(lib)) {
55+
return;
56+
}
57+
58+
const esLibs = lib.filter((l) => typeof l === 'string' && l.toLowerCase().startsWith('es'));
59+
const hasDom = lib.some((l) => typeof l === 'string' && l.toLowerCase() === 'dom');
60+
61+
if (esLibs.length === 0) {
62+
return;
63+
}
64+
65+
const esLibToVersion = new Map<string, number>();
66+
for (const l of esLibs) {
67+
const version = l.toLowerCase().match(/^es(next|(\d+))$/)?.[1];
68+
if (version) {
69+
esLibToVersion.set(l, version === 'next' ? Infinity : Number(version));
70+
}
71+
}
72+
73+
if (esLibToVersion.size === 0) {
74+
return;
75+
}
76+
77+
const latestEsLib = [...esLibToVersion.entries()].sort(([, v1], [, v2]) => v2 - v1)[0];
78+
const latestVersion = latestEsLib[1];
79+
80+
if (hasDom) {
81+
if (latestVersion <= 2022) {
82+
json.remove(jsonPath);
83+
}
84+
85+
return;
86+
}
87+
88+
// No 'dom' with 'es' libs, so update 'es' lib.
89+
if (latestVersion < 2022) {
90+
const newLibs = lib.filter((l) => !esLibToVersion.has(l));
91+
newLibs.push('es2022');
92+
json.modify(jsonPath, newLibs);
93+
}
94+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { isJsonObject } from '@angular-devkit/core';
10+
import { EmptyTree } from '@angular-devkit/schematics';
11+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
12+
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models';
13+
14+
describe('Migration to update TypeScript lib', () => {
15+
const schematicName = 'update-typescript-lib';
16+
17+
const schematicRunner = new SchematicTestRunner(
18+
'migrations',
19+
require.resolve('../migration-collection.json'),
20+
);
21+
22+
function createJsonFile(tree: UnitTestTree, filePath: string, content: {}): void {
23+
const stringifiedContent = JSON.stringify(content, undefined, 2);
24+
if (tree.exists(filePath)) {
25+
tree.overwrite(filePath, stringifiedContent);
26+
} else {
27+
tree.create(filePath, stringifiedContent);
28+
}
29+
}
30+
31+
function getCompilerOptions(tree: UnitTestTree, filePath: string): Record<string, unknown> {
32+
const json = tree.readJson(filePath);
33+
if (isJsonObject(json) && isJsonObject(json.compilerOptions)) {
34+
return json.compilerOptions;
35+
}
36+
37+
throw new Error(`Cannot retrieve 'compilerOptions'.`);
38+
}
39+
40+
function createWorkSpaceConfig(tree: UnitTestTree) {
41+
const angularConfig: WorkspaceSchema = {
42+
version: 1,
43+
projects: {
44+
app: {
45+
root: '',
46+
sourceRoot: 'src',
47+
projectType: ProjectType.Application,
48+
prefix: 'app',
49+
architect: {
50+
build: {
51+
builder: Builders.Browser,
52+
options: {
53+
tsConfig: 'src/tsconfig.app.json',
54+
main: '',
55+
polyfills: '',
56+
},
57+
configurations: {
58+
production: {
59+
tsConfig: 'src/tsconfig.app.prod.json',
60+
},
61+
},
62+
},
63+
test: {
64+
builder: Builders.Karma,
65+
options: {
66+
karmaConfig: '',
67+
tsConfig: 'src/tsconfig.spec.json',
68+
},
69+
},
70+
},
71+
},
72+
},
73+
};
74+
75+
createJsonFile(tree, 'angular.json', angularConfig);
76+
}
77+
78+
let tree: UnitTestTree;
79+
beforeEach(() => {
80+
tree = new UnitTestTree(new EmptyTree());
81+
createWorkSpaceConfig(tree);
82+
83+
// Create tsconfigs
84+
const compilerOptions = { lib: ['es2020', 'dom'] };
85+
const configWithExtends = { extends: './tsconfig.json', compilerOptions };
86+
87+
// Workspace
88+
createJsonFile(tree, 'tsconfig.json', { compilerOptions });
89+
90+
// Application
91+
createJsonFile(tree, 'src/tsconfig.app.json', configWithExtends);
92+
createJsonFile(tree, 'src/tsconfig.app.prod.json', configWithExtends);
93+
createJsonFile(tree, 'src/tsconfig.spec.json', { compilerOptions });
94+
});
95+
96+
it(`should remove 'lib' when 'dom' is present and ES version is less than 2022`, async () => {
97+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
98+
const compilerOptions = getCompilerOptions(newTree, 'tsconfig.json');
99+
expect(compilerOptions.lib).toBeUndefined();
100+
});
101+
102+
it(`should remove 'lib' when 'dom' is present and ES version is 2022`, async () => {
103+
createJsonFile(tree, 'tsconfig.json', { compilerOptions: { lib: ['es2022', 'dom'] } });
104+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
105+
const compilerOptions = getCompilerOptions(newTree, 'tsconfig.json');
106+
expect(compilerOptions.lib).toBeUndefined();
107+
});
108+
109+
it(`should not remove 'lib' when 'dom' is present and ES version is 'esnext'`, async () => {
110+
createJsonFile(tree, 'tsconfig.json', { compilerOptions: { lib: ['esnext', 'dom'] } });
111+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
112+
const compilerOptions = getCompilerOptions(newTree, 'tsconfig.json');
113+
expect(compilerOptions.lib).toEqual(['esnext', 'dom']);
114+
});
115+
116+
it(`should update 'lib' to 'es2022' when 'dom' is not present and ES version is less than 2022`, async () => {
117+
createJsonFile(tree, 'tsconfig.json', { compilerOptions: { lib: ['es2020'] } });
118+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
119+
const compilerOptions = getCompilerOptions(newTree, 'tsconfig.json');
120+
expect(compilerOptions.lib).toEqual(['es2022']);
121+
});
122+
123+
it(`should not update 'lib' when 'dom' is not present and ES version is 2022`, async () => {
124+
createJsonFile(tree, 'tsconfig.json', { compilerOptions: { lib: ['es2022'] } });
125+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
126+
const compilerOptions = getCompilerOptions(newTree, 'tsconfig.json');
127+
expect(compilerOptions.lib).toEqual(['es2022']);
128+
});
129+
130+
it(`should not update 'lib' when 'dom' is not present and ES version is 'esnext'`, async () => {
131+
createJsonFile(tree, 'tsconfig.json', { compilerOptions: { lib: ['esnext'] } });
132+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
133+
const compilerOptions = getCompilerOptions(newTree, 'tsconfig.json');
134+
expect(compilerOptions.lib).toEqual(['esnext']);
135+
});
136+
137+
it('should not error when a tsconfig is not found', async () => {
138+
tree.delete('src/tsconfig.spec.json');
139+
await schematicRunner.runSchematic(schematicName, {}, tree);
140+
});
141+
142+
it('should not error when compilerOptions is not defined', async () => {
143+
createJsonFile(tree, 'tsconfig.json', {});
144+
await schematicRunner.runSchematic(schematicName, {}, tree);
145+
});
146+
147+
it(`should not error when 'lib' is not defined`, async () => {
148+
createJsonFile(tree, 'tsconfig.json', { compilerOptions: {} });
149+
await schematicRunner.runSchematic(schematicName, {}, tree);
150+
});
151+
152+
it(`should remove 'lib' from all tsconfigs`, async () => {
153+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
154+
expect(getCompilerOptions(newTree, 'tsconfig.json').lib).toBeUndefined();
155+
expect(getCompilerOptions(newTree, 'src/tsconfig.app.json').lib).toBeUndefined();
156+
expect(getCompilerOptions(newTree, 'src/tsconfig.app.prod.json').lib).toBeUndefined();
157+
expect(getCompilerOptions(newTree, 'src/tsconfig.spec.json').lib).toBeUndefined();
158+
});
159+
});

0 commit comments

Comments
 (0)