Skip to content

Commit a144e82

Browse files
alan-agius4mgechev
authored andcommitted
feat(@schematics/angular): add migration to update tslint to version 6
In tslint version 6, several recommanded rules have been removed in the following PRs: palantir/tslint#4871 palantir/tslint#4312 With this migration we update the tslint depedency to 6+ and add back these rules in the user workspace `tslint.json` also we remove some deprecated rules. Until version 10, this is an opt-in migration and users will need to run this manually with the below command: ``` ng update @angular/cli --migrate-only tslint-version-6 ``` Closes: #17117 Reference: TOOL-1348
1 parent df4382f commit a144e82

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
"version": "9.0.2",
5555
"factory": "./update-9/schematic-options",
5656
"description": "Replace deprecated 'styleext' and 'spec' Angular schematic options."
57+
},
58+
"tslint-version-6": {
59+
"version": "10.0.0-beta.0",
60+
"factory": "./update-10/update-tslint",
61+
"description": "Update tslint to version 6."
5762
}
5863
}
5964
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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.io/license
7+
*/
8+
import { JsonAstObject, JsonValue } from '@angular-devkit/core';
9+
import { Rule } from '@angular-devkit/schematics';
10+
import { addPackageJsonDependency, getPackageJsonDependency } from '../../utility/dependencies';
11+
import { findPropertyInAstObject, insertPropertyInAstObjectInOrder, removePropertyInAstObject } from '../../utility/json-utils';
12+
import { readJsonFileAsAstObject } from '../update-9/utils';
13+
14+
export const TSLINT_VERSION = '~6.1.0';
15+
const TSLINT_CONFIG_PATH = '/tslint.json';
16+
17+
const RULES_TO_DELETE: string[] = [
18+
'no-use-before-declare',
19+
'no-unused-variable',
20+
];
21+
22+
const RULES_TO_ADD: Record<string, JsonValue> = {
23+
align: {
24+
options: ['parameters', 'statements'],
25+
},
26+
'arrow-return-shorthand': true,
27+
curly: true,
28+
eofline: true,
29+
'import-spacing': true,
30+
indent: {
31+
options: ['spaces'],
32+
},
33+
'variable-name': {
34+
options: ['ban-keywords', 'check-format', 'allow-pascal-case'],
35+
},
36+
semicolon: { options: ['always'] },
37+
'space-before-function-paren': {
38+
options: {
39+
anonymous: 'never',
40+
asyncArrow: 'always',
41+
constructor: 'never',
42+
method: 'never',
43+
named: 'never',
44+
},
45+
},
46+
'typedef-whitespace': {
47+
options: [
48+
{
49+
'call-signature': 'nospace',
50+
'index-signature': 'nospace',
51+
parameter: 'nospace',
52+
'property-declaration': 'nospace',
53+
'variable-declaration': 'nospace',
54+
},
55+
{
56+
'call-signature': 'onespace',
57+
'index-signature': 'onespace',
58+
parameter: 'onespace',
59+
'property-declaration': 'onespace',
60+
'variable-declaration': 'onespace',
61+
},
62+
],
63+
},
64+
whitespace: {
65+
options: [
66+
'check-branch',
67+
'check-decl',
68+
'check-operator',
69+
'check-separator',
70+
'check-type',
71+
'check-typecast',
72+
],
73+
},
74+
};
75+
76+
export default function (): Rule {
77+
return (tree, context) => {
78+
const logger = context.logger;
79+
80+
// Update tslint dependency
81+
const current = getPackageJsonDependency(tree, 'tslint');
82+
83+
if (!current) {
84+
logger.info('"tslint" in not a dependency of this workspace.');
85+
86+
return;
87+
}
88+
89+
if (current.version !== TSLINT_VERSION) {
90+
addPackageJsonDependency(tree, {
91+
type: current.type,
92+
name: 'tslint',
93+
version: TSLINT_VERSION,
94+
overwrite: true,
95+
});
96+
}
97+
98+
// Update tslint config.
99+
const tslintJsonAst = readJsonFileAsAstObject(tree, TSLINT_CONFIG_PATH);
100+
if (!tslintJsonAst) {
101+
const config = ['tslint.js', 'tslint.yaml'].find(c => tree.exists(c));
102+
if (config) {
103+
logger.warn(`Expected a JSON configuration file but found "${config}".`);
104+
} else {
105+
logger.warn('Cannot find "tslint.json" configuration file.');
106+
}
107+
108+
return;
109+
}
110+
111+
// Remove old/deprecated rules.
112+
for (const rule of RULES_TO_DELETE) {
113+
const tslintJsonAst = readJsonFileAsAstObject(tree, TSLINT_CONFIG_PATH) as JsonAstObject;
114+
const rulesAst = findPropertyInAstObject(tslintJsonAst, 'rules');
115+
if (rulesAst?.kind !== 'object') {
116+
break;
117+
}
118+
119+
const recorder = tree.beginUpdate(TSLINT_CONFIG_PATH);
120+
removePropertyInAstObject(recorder, rulesAst, rule);
121+
tree.commitUpdate(recorder);
122+
}
123+
124+
// Add new rules only iif the configuration extends 'tslint:recommended'.
125+
// This is because some rules conflict with prettier or other tools.
126+
const extendsAst = findPropertyInAstObject(tslintJsonAst, 'extends');
127+
if (
128+
!extendsAst ||
129+
(extendsAst.kind === 'string' && extendsAst.value !== 'tslint:recommended') ||
130+
(extendsAst.kind === 'array' && extendsAst.elements.some(e => e.value !== 'tslint:recommended'))
131+
) {
132+
logger.warn(`tslint configuration does not extend "tslint:recommended".`
133+
+ '\nMigration will terminate as some rules might conflict.');
134+
135+
return;
136+
}
137+
138+
for (const [name, value] of Object.entries(RULES_TO_ADD)) {
139+
const tslintJsonAst = readJsonFileAsAstObject(tree, TSLINT_CONFIG_PATH) as JsonAstObject;
140+
const rulesAst = findPropertyInAstObject(tslintJsonAst, 'rules');
141+
if (rulesAst?.kind !== 'object') {
142+
break;
143+
}
144+
145+
if (findPropertyInAstObject(rulesAst, name)) {
146+
// Skip as rule already exists.
147+
continue;
148+
}
149+
150+
const recorder = tree.beginUpdate(TSLINT_CONFIG_PATH);
151+
insertPropertyInAstObjectInOrder(recorder, rulesAst, name, value, 4);
152+
tree.commitUpdate(recorder);
153+
}
154+
155+
};
156+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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.io/license
7+
*/
8+
import { EmptyTree } from '@angular-devkit/schematics';
9+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
10+
import { TSLINT_VERSION } from './update-tslint';
11+
12+
describe('Migration of tslint to version 6', () => {
13+
const schematicRunner = new SchematicTestRunner(
14+
'migrations',
15+
require.resolve('../migration-collection.json'),
16+
);
17+
18+
let tree: UnitTestTree;
19+
const TSLINT_PATH = '/tslint.json';
20+
const PACKAGE_JSON_PATH = '/package.json';
21+
22+
const TSLINT_CONFIG = {
23+
extends: 'tslint:recommended',
24+
rules: {
25+
'no-use-before-declare': true,
26+
'arrow-return-shorthand': false,
27+
'label-position': true,
28+
},
29+
};
30+
31+
const PACKAGE_JSON = {
32+
devDependencies: {
33+
tslint: '~5.1.0',
34+
},
35+
};
36+
37+
beforeEach(() => {
38+
tree = new UnitTestTree(new EmptyTree());
39+
tree.create(PACKAGE_JSON_PATH, JSON.stringify(PACKAGE_JSON, null, 2));
40+
tree.create(TSLINT_PATH, JSON.stringify(TSLINT_CONFIG, null, 2));
41+
});
42+
43+
it('should update tslint dependency', async () => {
44+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
45+
const packageJson = JSON.parse(newTree.readContent(PACKAGE_JSON_PATH));
46+
expect(packageJson.devDependencies.tslint).toBe(TSLINT_VERSION);
47+
});
48+
49+
it('should remove old/deprecated rules', async () => {
50+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
51+
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
52+
expect(rules['no-use-before-declare']).toBeUndefined();
53+
});
54+
55+
it('should add new rules', async () => {
56+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
57+
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
58+
expect(rules['eofline']).toBe(true);
59+
});
60+
61+
it('should not update already present rules', async () => {
62+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
63+
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
64+
expect(rules['arrow-return-shorthand']).toBe(false);
65+
});
66+
67+
it(`should not add new rules when not extending 'tslint:recommended'`, async () => {
68+
tree.overwrite(
69+
TSLINT_PATH,
70+
JSON.stringify({
71+
...TSLINT_CONFIG,
72+
extends: 'tslint-config-prettier',
73+
}, null, 2),
74+
);
75+
76+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
77+
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
78+
expect(rules['eofline']).toBeUndefined();
79+
});
80+
81+
it(`should not add new rules when extending multiple configs`, async () => {
82+
tree.overwrite(
83+
TSLINT_PATH,
84+
JSON.stringify({
85+
...TSLINT_CONFIG,
86+
extends: [
87+
'tslint:recommended',
88+
'tslint-config-prettier',
89+
],
90+
}, null, 2),
91+
);
92+
93+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
94+
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
95+
expect(rules['eofline']).toBeUndefined();
96+
});
97+
98+
it(`should remove old/deprecated rules when extending multiple configs`, async () => {
99+
tree.overwrite(
100+
TSLINT_PATH,
101+
JSON.stringify({
102+
...TSLINT_CONFIG,
103+
extends: [
104+
'tslint:recommended',
105+
'tslint-config-prettier',
106+
],
107+
}, null, 2),
108+
);
109+
110+
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
111+
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
112+
expect(rules['no-use-before-declare']).toBeUndefined();
113+
});
114+
});

0 commit comments

Comments
 (0)