Skip to content

Commit

Permalink
feat(@schematics/angular): add migration to update tslint to version 6
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alan-agius4 authored and mgechev committed Mar 17, 2020
1 parent df4382f commit a144e82
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
"version": "9.0.2",
"factory": "./update-9/schematic-options",
"description": "Replace deprecated 'styleext' and 'spec' Angular schematic options."
},
"tslint-version-6": {
"version": "10.0.0-beta.0",
"factory": "./update-10/update-tslint",
"description": "Update tslint to version 6."
}
}
}
156 changes: 156 additions & 0 deletions packages/schematics/angular/migrations/update-10/update-tslint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* @license
* Copyright Google Inc. 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 { JsonAstObject, JsonValue } from '@angular-devkit/core';
import { Rule } from '@angular-devkit/schematics';
import { addPackageJsonDependency, getPackageJsonDependency } from '../../utility/dependencies';
import { findPropertyInAstObject, insertPropertyInAstObjectInOrder, removePropertyInAstObject } from '../../utility/json-utils';
import { readJsonFileAsAstObject } from '../update-9/utils';

export const TSLINT_VERSION = '~6.1.0';
const TSLINT_CONFIG_PATH = '/tslint.json';

const RULES_TO_DELETE: string[] = [
'no-use-before-declare',
'no-unused-variable',
];

const RULES_TO_ADD: Record<string, JsonValue> = {
align: {
options: ['parameters', 'statements'],
},
'arrow-return-shorthand': true,
curly: true,
eofline: true,
'import-spacing': true,
indent: {
options: ['spaces'],
},
'variable-name': {
options: ['ban-keywords', 'check-format', 'allow-pascal-case'],
},
semicolon: { options: ['always'] },
'space-before-function-paren': {
options: {
anonymous: 'never',
asyncArrow: 'always',
constructor: 'never',
method: 'never',
named: 'never',
},
},
'typedef-whitespace': {
options: [
{
'call-signature': 'nospace',
'index-signature': 'nospace',
parameter: 'nospace',
'property-declaration': 'nospace',
'variable-declaration': 'nospace',
},
{
'call-signature': 'onespace',
'index-signature': 'onespace',
parameter: 'onespace',
'property-declaration': 'onespace',
'variable-declaration': 'onespace',
},
],
},
whitespace: {
options: [
'check-branch',
'check-decl',
'check-operator',
'check-separator',
'check-type',
'check-typecast',
],
},
};

export default function (): Rule {
return (tree, context) => {
const logger = context.logger;

// Update tslint dependency
const current = getPackageJsonDependency(tree, 'tslint');

if (!current) {
logger.info('"tslint" in not a dependency of this workspace.');

return;
}

if (current.version !== TSLINT_VERSION) {
addPackageJsonDependency(tree, {
type: current.type,
name: 'tslint',
version: TSLINT_VERSION,
overwrite: true,
});
}

// Update tslint config.
const tslintJsonAst = readJsonFileAsAstObject(tree, TSLINT_CONFIG_PATH);
if (!tslintJsonAst) {
const config = ['tslint.js', 'tslint.yaml'].find(c => tree.exists(c));
if (config) {
logger.warn(`Expected a JSON configuration file but found "${config}".`);
} else {
logger.warn('Cannot find "tslint.json" configuration file.');
}

return;
}

// Remove old/deprecated rules.
for (const rule of RULES_TO_DELETE) {
const tslintJsonAst = readJsonFileAsAstObject(tree, TSLINT_CONFIG_PATH) as JsonAstObject;
const rulesAst = findPropertyInAstObject(tslintJsonAst, 'rules');
if (rulesAst?.kind !== 'object') {
break;
}

const recorder = tree.beginUpdate(TSLINT_CONFIG_PATH);
removePropertyInAstObject(recorder, rulesAst, rule);
tree.commitUpdate(recorder);
}

// Add new rules only iif the configuration extends 'tslint:recommended'.
// This is because some rules conflict with prettier or other tools.
const extendsAst = findPropertyInAstObject(tslintJsonAst, 'extends');
if (
!extendsAst ||
(extendsAst.kind === 'string' && extendsAst.value !== 'tslint:recommended') ||
(extendsAst.kind === 'array' && extendsAst.elements.some(e => e.value !== 'tslint:recommended'))
) {
logger.warn(`tslint configuration does not extend "tslint:recommended".`
+ '\nMigration will terminate as some rules might conflict.');

return;
}

for (const [name, value] of Object.entries(RULES_TO_ADD)) {
const tslintJsonAst = readJsonFileAsAstObject(tree, TSLINT_CONFIG_PATH) as JsonAstObject;
const rulesAst = findPropertyInAstObject(tslintJsonAst, 'rules');
if (rulesAst?.kind !== 'object') {
break;
}

if (findPropertyInAstObject(rulesAst, name)) {
// Skip as rule already exists.
continue;
}

const recorder = tree.beginUpdate(TSLINT_CONFIG_PATH);
insertPropertyInAstObjectInOrder(recorder, rulesAst, name, value, 4);
tree.commitUpdate(recorder);
}

};
}
114 changes: 114 additions & 0 deletions packages/schematics/angular/migrations/update-10/update-tslint_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @license
* Copyright Google Inc. 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 { TSLINT_VERSION } from './update-tslint';

describe('Migration of tslint to version 6', () => {
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);

let tree: UnitTestTree;
const TSLINT_PATH = '/tslint.json';
const PACKAGE_JSON_PATH = '/package.json';

const TSLINT_CONFIG = {
extends: 'tslint:recommended',
rules: {
'no-use-before-declare': true,
'arrow-return-shorthand': false,
'label-position': true,
},
};

const PACKAGE_JSON = {
devDependencies: {
tslint: '~5.1.0',
},
};

beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
tree.create(PACKAGE_JSON_PATH, JSON.stringify(PACKAGE_JSON, null, 2));
tree.create(TSLINT_PATH, JSON.stringify(TSLINT_CONFIG, null, 2));
});

it('should update tslint dependency', async () => {
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const packageJson = JSON.parse(newTree.readContent(PACKAGE_JSON_PATH));
expect(packageJson.devDependencies.tslint).toBe(TSLINT_VERSION);
});

it('should remove old/deprecated rules', async () => {
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
expect(rules['no-use-before-declare']).toBeUndefined();
});

it('should add new rules', async () => {
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
expect(rules['eofline']).toBe(true);
});

it('should not update already present rules', async () => {
const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
expect(rules['arrow-return-shorthand']).toBe(false);
});

it(`should not add new rules when not extending 'tslint:recommended'`, async () => {
tree.overwrite(
TSLINT_PATH,
JSON.stringify({
...TSLINT_CONFIG,
extends: 'tslint-config-prettier',
}, null, 2),
);

const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
expect(rules['eofline']).toBeUndefined();
});

it(`should not add new rules when extending multiple configs`, async () => {
tree.overwrite(
TSLINT_PATH,
JSON.stringify({
...TSLINT_CONFIG,
extends: [
'tslint:recommended',
'tslint-config-prettier',
],
}, null, 2),
);

const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
expect(rules['eofline']).toBeUndefined();
});

it(`should remove old/deprecated rules when extending multiple configs`, async () => {
tree.overwrite(
TSLINT_PATH,
JSON.stringify({
...TSLINT_CONFIG,
extends: [
'tslint:recommended',
'tslint-config-prettier',
],
}, null, 2),
);

const newTree = await schematicRunner.runSchematicAsync('tslint-version-6', {}, tree).toPromise();
const { rules } = JSON.parse(newTree.readContent(TSLINT_PATH));
expect(rules['no-use-before-declare']).toBeUndefined();
});
});

0 comments on commit a144e82

Please sign in to comment.