diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 8e7cb1dec5ca..e0dd458bcf89 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -49,6 +49,11 @@ "version": "9.0.0-next.6", "factory": "./update-8/#updateLazyModulePaths", "description": "Lazy loading syntax migration. Update lazy loading syntax to use dynamic imports." + }, + "schematic-options-9": { + "version": "9.0.2", + "factory": "./update-9/schematic-options", + "description": "Replace deprecated 'styleext' and 'spec' Angular schematic options." } } } diff --git a/packages/schematics/angular/migrations/update-9/schematic-options.ts b/packages/schematics/angular/migrations/update-9/schematic-options.ts new file mode 100644 index 000000000000..bd5a358756ac --- /dev/null +++ b/packages/schematics/angular/migrations/update-9/schematic-options.ts @@ -0,0 +1,90 @@ +/** + * @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 } from '@angular-devkit/core'; +import { Rule, UpdateRecorder } from '@angular-devkit/schematics'; +import { getWorkspacePath } from '../../utility/config'; +import { findPropertyInAstObject } from '../../utility/json-utils'; +import { getWorkspace } from './utils'; + +export default function(): Rule { + return tree => { + const workspacePath = getWorkspacePath(tree); + const workspace = getWorkspace(tree); + const recorder = tree.beginUpdate(workspacePath); + + const rootSchematics = findSchematicsField(workspace); + if (rootSchematics) { + updateSchematicsField(rootSchematics, recorder); + } + + const projects = findPropertyInAstObject(workspace, 'projects'); + if (!projects || projects.kind !== 'object' || !projects.properties) { + return; + } + + for (const { value } of projects.properties) { + if (value.kind !== 'object') { + continue; + } + + const projectSchematics = findSchematicsField(value); + if (!projectSchematics) { + continue; + } + + updateSchematicsField(projectSchematics, recorder); + } + + tree.commitUpdate(recorder); + + return tree; + }; +} + +function findSchematicsField(value: JsonAstObject): JsonAstObject | null { + const schematics = findPropertyInAstObject(value, 'schematics'); + if (schematics && schematics.kind == 'object') { + return schematics; + } + + return null; +} + +function updateSchematicsField(schematics: JsonAstObject, recorder: UpdateRecorder): void { + for (const { + key: { value: schematicName }, + value: schematicValue, + } of schematics.properties) { + if (schematicValue.kind !== 'object') { + continue; + } + + if (!schematicName.startsWith('@schematics/angular:')) { + continue; + } + + for (const { key: optionKey, value: optionValue } of schematicValue.properties) { + if (optionKey.value === 'styleext') { + // Rename `styleext` to `style + const offset = optionKey.start.offset + 1; + recorder.remove(offset, optionKey.value.length); + recorder.insertLeft(offset, 'style'); + } else if (optionKey.value === 'spec') { + // Rename `spec` to `skipTests` + const offset = optionKey.start.offset + 1; + recorder.remove(offset, optionKey.value.length); + recorder.insertLeft(offset, 'skipTests'); + + // invert value + const { start, end } = optionValue; + recorder.remove(start.offset, end.offset - start.offset); + recorder.insertLeft(start.offset, `${!optionValue.value}`); + } + } + } +} diff --git a/packages/schematics/angular/migrations/update-9/schematic-options_spec.ts b/packages/schematics/angular/migrations/update-9/schematic-options_spec.ts new file mode 100644 index 000000000000..bc6c9c3006ca --- /dev/null +++ b/packages/schematics/angular/migrations/update-9/schematic-options_spec.ts @@ -0,0 +1,175 @@ +/** + * @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'; + +const workspacePath = '/angular.json'; + +describe('Migration to version 9', () => { + describe('Migrate workspace config', () => { + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + + beforeEach(async () => { + tree = new UnitTestTree(new EmptyTree()); + tree = await schematicRunner + .runExternalSchematicAsync( + require.resolve('../../collection.json'), + 'ng-new', + { + name: 'migration-test', + version: '1.2.3', + directory: '.', + }, + tree, + ) + .toPromise(); + }); + + describe('schematic options', () => { + function getI18NConfig(localId: string): object { + return { + outputPath: `dist/my-project/${localId}/`, + i18nFile: `src/locale/messages.${localId}.xlf`, + i18nFormat: 'xlf', + i18nLocale: localId, + }; + } + + it('should replace styleext with style', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.schematics = { + '@schematics/angular:component': { + styleext: 'scss', + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { schematics } = JSON.parse(tree2.readContent(workspacePath)); + expect(schematics['@schematics/angular:component'].styleext).toBeUndefined(); + expect(schematics['@schematics/angular:component'].style).toBe('scss'); + }); + + it('should not replace styleext with style in non-Angular schematic', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.schematics = { + '@schematics/some-other:component': { + styleext: 'scss', + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { schematics } = JSON.parse(tree2.readContent(workspacePath)); + expect(schematics['@schematics/some-other:component'].styleext).toBe('scss'); + expect(schematics['@schematics/some-other:component'].style).toBeUndefined(); + }); + + it('should replace spec (false) with skipTests (true)', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.schematics = { + '@schematics/angular:component': { + spec: false, + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { schematics } = JSON.parse(tree2.readContent(workspacePath)); + expect(schematics['@schematics/angular:component'].spec).toBeUndefined(); + expect(schematics['@schematics/angular:component'].skipTests).toBe(true); + }); + + it('should replace spec (true) with skipTests (false)', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.schematics = { + '@schematics/angular:component': { + spec: true, + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { schematics } = JSON.parse(tree2.readContent(workspacePath)); + expect(schematics['@schematics/angular:component'].spec).toBeUndefined(); + expect(schematics['@schematics/angular:component'].skipTests).toBe(false); + }); + + it('should replace spec with skipTests for multiple Angular schematics', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.schematics = { + '@schematics/angular:component': { + spec: false, + }, + '@schematics/angular:directive': { + spec: false, + }, + '@schematics/angular:service': { + spec: true, + }, + '@schematics/angular:module': { + spec: false, + }, + '@schematics/angular:guard': { + spec: true, + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { schematics } = JSON.parse(tree2.readContent(workspacePath)); + for (const key of Object.keys(workspace.schematics)) { + expect(schematics[key].spec).toBeUndefined(); + expect(schematics[key].skipTests).toBe(!workspace.schematics[key].spec); + } + }); + + it('should replace both styleext with style and spec with skipTests', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.schematics = { + '@schematics/angular:component': { + styleext: 'scss', + spec: false, + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { schematics } = JSON.parse(tree2.readContent(workspacePath)); + expect(schematics['@schematics/angular:component'].styleext).toBeUndefined(); + expect(schematics['@schematics/angular:component'].style).toBe('scss'); + expect(schematics['@schematics/angular:component'].spec).toBeUndefined(); + expect(schematics['@schematics/angular:component'].skipTests).toBe(true); + }); + + it('should replace both styleext with style and spec with skipTests in a project', async () => { + const workspace = JSON.parse(tree.readContent(workspacePath)); + workspace.projects['migration-test'].schematics = { + '@schematics/angular:component': { + styleext: 'scss', + spec: false, + }, + }; + tree.overwrite(workspacePath, JSON.stringify(workspace, undefined, 2)); + + const tree2 = await schematicRunner.runSchematicAsync('schematic-options-9', {}, tree.branch()).toPromise(); + const { projects: { 'migration-test': { schematics } } } = JSON.parse(tree2.readContent(workspacePath)); + expect(schematics['@schematics/angular:component'].styleext).toBeUndefined(); + expect(schematics['@schematics/angular:component'].style).toBe('scss'); + expect(schematics['@schematics/angular:component'].spec).toBeUndefined(); + expect(schematics['@schematics/angular:component'].skipTests).toBe(true); + }); + + }); + }); +});