diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index e0dd458bcf89..c5b8912e3c34 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -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." } } } diff --git a/packages/schematics/angular/migrations/update-10/update-tslint.ts b/packages/schematics/angular/migrations/update-10/update-tslint.ts new file mode 100644 index 000000000000..992386bac605 --- /dev/null +++ b/packages/schematics/angular/migrations/update-10/update-tslint.ts @@ -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 = { + 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); + } + + }; +} diff --git a/packages/schematics/angular/migrations/update-10/update-tslint_spec.ts b/packages/schematics/angular/migrations/update-10/update-tslint_spec.ts new file mode 100644 index 000000000000..c97aa2533041 --- /dev/null +++ b/packages/schematics/angular/migrations/update-10/update-tslint_spec.ts @@ -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(); + }); +});