From 3c5d24e19e593a8e302c8182c547b978bedd3d52 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Wed, 8 Jan 2020 19:11:47 +0800 Subject: [PATCH] feat(schematics): add v9 migration rules for icon and calendar (#4467) --- schematics/ng-update/data/input-names.ts | 60 ++++++++++ schematics/ng-update/index.ts | 14 ++- .../v9/input-names-calendar.spec.ts | 85 +++++++++++++++ .../test-cases/v9/input-names-icon.spec.ts | 103 ++++++++++++++++++ .../checks/calendar-input-rule.ts | 20 ++++ .../checks/icon-template-rule.ts | 20 ++++ schematics/utils/ng-update/elements.ts | 25 +++++ 7 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 schematics/ng-update/test-cases/v9/input-names-calendar.spec.ts create mode 100644 schematics/ng-update/test-cases/v9/input-names-icon.spec.ts create mode 100644 schematics/ng-update/upgrade-rules/checks/calendar-input-rule.ts create mode 100644 schematics/ng-update/upgrade-rules/checks/icon-template-rule.ts diff --git a/schematics/ng-update/data/input-names.ts b/schematics/ng-update/data/input-names.ts index aa4f44da366..0673b561316 100644 --- a/schematics/ng-update/data/input-names.ts +++ b/schematics/ng-update/data/input-names.ts @@ -135,6 +135,66 @@ export const inputNames: VersionChanges = { } } ] + }, + { + pr : 'https://github.com/NG-ZORRO/ng-zorro-antd/pull/4375', + changes: [ + { + replace : 'type', + replaceWith: 'nzType', + whitelist : { + attributes: ['nz-icon'] + } + } + ] + }, + { + pr : 'https://github.com/NG-ZORRO/ng-zorro-antd/pull/4375', + changes: [ + { + replace : 'iconfont', + replaceWith: 'nzIconfont', + whitelist : { + attributes: ['nz-icon'] + } + } + ] + }, + { + pr : 'https://github.com/NG-ZORRO/ng-zorro-antd/pull/4375', + changes: [ + { + replace : 'spin', + replaceWith: 'nzSpin', + whitelist : { + attributes: ['nz-icon'] + } + } + ] + }, + { + pr : 'https://github.com/NG-ZORRO/ng-zorro-antd/pull/4375', + changes: [ + { + replace : 'theme', + replaceWith: 'nzTheme', + whitelist : { + attributes: ['nz-icon'] + } + } + ] + }, + { + pr : 'https://github.com/NG-ZORRO/ng-zorro-antd/pull/4375', + changes: [ + { + replace : 'twoToneColor', + replaceWith: 'nzTwoToneColor', + whitelist : { + attributes: ['nz-icon'] + } + } + ] } ] }; diff --git a/schematics/ng-update/index.ts b/schematics/ng-update/index.ts index f196110277e..c9b255481ec 100644 --- a/schematics/ng-update/index.ts +++ b/schematics/ng-update/index.ts @@ -2,8 +2,10 @@ import { Rule } from '@angular-devkit/schematics'; import { createUpgradeRule, TargetVersion } from '@angular/cdk/schematics'; import chalk from 'chalk'; import { ruleUpgradeData } from './upgrade-data'; +import { CalendarTemplateRule } from './upgrade-rules/checks/calendar-input-rule'; import { DropdownClassRule } from './upgrade-rules/checks/dropdown-class-rule'; import { DropdownTemplateRule } from './upgrade-rules/checks/dropdown-template-rule'; +import { IconTemplateRule } from './upgrade-rules/checks/icon-template-rule'; import { TooltipLikeTemplateRule } from './upgrade-rules/checks/tooltip-like-template-rule'; /** Entry point for the migration schematics with target of NG-ZORRO v7 */ @@ -14,11 +16,17 @@ export function updateToV7(): Rule { /** Entry point for the migration schematics with target of NG-ZORRO v9 */ export function updateToV9(): Rule { return createUpgradeRule( - TargetVersion.V9, [ + TargetVersion.V9, + [ TooltipLikeTemplateRule, DropdownTemplateRule, - DropdownClassRule - ], ruleUpgradeData, postUpdate); + DropdownClassRule, + IconTemplateRule, + CalendarTemplateRule + ], + ruleUpgradeData, + postUpdate + ); } /** Post-update schematic to be called when update is finished. */ diff --git a/schematics/ng-update/test-cases/v9/input-names-calendar.spec.ts b/schematics/ng-update/test-cases/v9/input-names-calendar.spec.ts new file mode 100644 index 00000000000..8ad20d02900 --- /dev/null +++ b/schematics/ng-update/test-cases/v9/input-names-calendar.spec.ts @@ -0,0 +1,85 @@ +import { getSystemPath, normalize, virtualFs } from '@angular-devkit/core'; +import { TempScopedNodeJsSyncHost } from '@angular-devkit/core/node/testing'; +import { HostTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as shx from 'shelljs'; + +describe('calendar migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + let previousWorkingDir: string; + let warnOutput: string[]; + let errorOutput: string[]; + + beforeEach(() => { + runner = new SchematicTestRunner('test', require.resolve('../../../migration.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile('/tsconfig.json', JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + lib: ['es2015'] + } + })); + writeFile('/angular.json', JSON.stringify({ + projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} + })); + + warnOutput = []; + errorOutput = []; + runner.logger.subscribe(logEntry => { + if (logEntry.level === 'warn') { + warnOutput.push(logEntry.message); + } else if (logEntry.level === 'error') { + errorOutput.push(logEntry.message); + } + }); + + previousWorkingDir = shx.pwd(); + tmpDirPath = getSystemPath(host.root); + + shx.cd(tmpDirPath); + + writeFakeAngular(); + }); + + afterEach(() => { + shx.cd(previousWorkingDir); + shx.rm('-r', tmpDirPath); + }); + + function writeFakeAngular(): void { writeFile('/node_modules/@angular/core/index.d.ts', ``); } + + function writeFile(filePath: string, contents: string): void { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + // tslint:disable-next-line:no-any + async function runMigration(): Promise { + await runner.runSchematicAsync('migration-v9', {}, tree).toPromise(); + } + + describe('Calendar', () => { + + it('should properly report invalid deprecated input', async() => { + writeFile('/index.ts', `; + import {Component} from '@angular/core' + @Component({ + template: \` + + \` + }) + export class MyComp { + }`); + await runMigration(); + + expect(warnOutput).toContain( 'index.ts@5:24 - Found deprecated input "nzCard" component. ' + + 'Use "nzFullscreen" to instead please.'); + }); + + }); + +}); diff --git a/schematics/ng-update/test-cases/v9/input-names-icon.spec.ts b/schematics/ng-update/test-cases/v9/input-names-icon.spec.ts new file mode 100644 index 00000000000..8c322b2a6f4 --- /dev/null +++ b/schematics/ng-update/test-cases/v9/input-names-icon.spec.ts @@ -0,0 +1,103 @@ +import { getSystemPath, normalize, virtualFs } from '@angular-devkit/core'; +import { TempScopedNodeJsSyncHost } from '@angular-devkit/core/node/testing'; +import { HostTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as shx from 'shelljs'; + +describe('icon migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + let previousWorkingDir: string; + let warnOutput: string[]; + let errorOutput: string[]; + + beforeEach(() => { + runner = new SchematicTestRunner('test', require.resolve('../../../migration.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile('/tsconfig.json', JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + lib: ['es2015'] + } + })); + writeFile('/angular.json', JSON.stringify({ + projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} + })); + + warnOutput = []; + errorOutput = []; + runner.logger.subscribe(logEntry => { + if (logEntry.level === 'warn') { + warnOutput.push(logEntry.message); + } else if (logEntry.level === 'error') { + errorOutput.push(logEntry.message); + } + }); + + previousWorkingDir = shx.pwd(); + tmpDirPath = getSystemPath(host.root); + + shx.cd(tmpDirPath); + + writeFakeAngular(); + }); + + afterEach(() => { + shx.cd(previousWorkingDir); + shx.rm('-r', tmpDirPath); + }); + + function writeFakeAngular(): void { writeFile('/node_modules/@angular/core/index.d.ts', ``); } + + function writeFile(filePath: string, contents: string): void { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + // tslint:disable-next-line:no-any + async function runMigration(): Promise { + await runner.runSchematicAsync('migration-v9', {}, tree).toPromise(); + } + + describe('Icon', () => { + + it('should rename deprecated input names', async() => { + writeFile('/index.ts', ` + import {Component} from '@angular/core'; + @Component({template: \` + + \`}) + export class MyComp { + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).toContain(`[nzIconfont]="'value'"`); + expect(content).toContain(`[nzSpin]="true"`); + expect(content).toContain(`[nzType]="'play'"`); + expect(content).toContain(`nzTheme="o"`); + }); + + it('should properly report invalid deprecated css selector', async() => { + writeFile('/index.ts', `; + import {Component} from '@angular/core' + @Component({ + template: \` + + \` + }) + export class MyComp { + }`); + await runMigration(); + + expect(warnOutput).toContain( 'index.ts@5:14 - Found deprecated css selector "i.anticon" component. ' + + 'Use "i[nz-icon]" to instead please.'); + }); + + }); + +}); diff --git a/schematics/ng-update/upgrade-rules/checks/calendar-input-rule.ts b/schematics/ng-update/upgrade-rules/checks/calendar-input-rule.ts new file mode 100644 index 00000000000..d3e6d4b8f6b --- /dev/null +++ b/schematics/ng-update/upgrade-rules/checks/calendar-input-rule.ts @@ -0,0 +1,20 @@ +import { findInputsOnElementWithTag, MigrationRule, ResolvedResource, TargetVersion } from '@angular/cdk/schematics'; +import { findElementWithClassName } from '../../../utils/ng-update/elements'; + +export class CalendarTemplateRule extends MigrationRule { + + ruleEnabled = this.targetVersion === TargetVersion.V9; + + visitTemplate(template: ResolvedResource): void { + + findInputsOnElementWithTag(template.content, 'nzCard', ['nz-calendar']) + .forEach(offset => { + this.failures.push({ + filePath: template.filePath, + position: template.getCharacterAndLineOfPosition(offset), + message: `Found deprecated input "nzCard" component. Use "nzFullscreen" to instead please.` + }); + }) + + } +} diff --git a/schematics/ng-update/upgrade-rules/checks/icon-template-rule.ts b/schematics/ng-update/upgrade-rules/checks/icon-template-rule.ts new file mode 100644 index 00000000000..dfbb78ebc55 --- /dev/null +++ b/schematics/ng-update/upgrade-rules/checks/icon-template-rule.ts @@ -0,0 +1,20 @@ +import { MigrationRule, ResolvedResource, TargetVersion } from '@angular/cdk/schematics'; +import { findElementWithClassName } from '../../../utils/ng-update/elements'; + +export class IconTemplateRule extends MigrationRule { + + ruleEnabled = this.targetVersion === TargetVersion.V9; + + visitTemplate(template: ResolvedResource): void { + + findElementWithClassName(template.content, 'anticon', 'i') + .forEach(offset => { + this.failures.push({ + filePath: template.filePath, + position: template.getCharacterAndLineOfPosition(offset), + message: `Found deprecated css selector "i.anticon" component. Use "i[nz-icon]" to instead please.` + }); + }) + + } +} diff --git a/schematics/utils/ng-update/elements.ts b/schematics/utils/ng-update/elements.ts index 67ac1f3a7c9..8d6a419858a 100644 --- a/schematics/utils/ng-update/elements.ts +++ b/schematics/utils/ng-update/elements.ts @@ -1,5 +1,9 @@ import { parseFragment, DefaultTreeDocument, DefaultTreeElement } from 'parse5'; +const hasClassName = (node: DefaultTreeElement, className: string) => { + return Array.isArray(node.attrs) && node.attrs.find(attr => attr.name === 'class' && attr.value.indexOf(className) !== -1) +}; + export function findElementWithTag(html: string, tagName: string): number[] { const document = parseFragment(html, {sourceCodeLocationInfo: true}) as DefaultTreeDocument; const elements: DefaultTreeElement[] = []; @@ -20,3 +24,24 @@ export function findElementWithTag(html: string, tagName: string): number[] { return elements.map(element => element.sourceCodeLocation.startTag.startOffset) } + +export function findElementWithClassName(html: string, className: string, tagName: string): number[] { + const document = parseFragment(html, {sourceCodeLocationInfo: true}) as DefaultTreeDocument; + const elements: DefaultTreeElement[] = []; + + const visitNodes = nodes => { + nodes.forEach(node => { + if (node.childNodes) { + visitNodes(node.childNodes); + } + + if (hasClassName(node, className) && node.tagName && node.tagName.toLowerCase() === tagName.toLowerCase()) { + elements.push(node); + } + }); + }; + + visitNodes(document.childNodes); + + return elements.map(element => element.sourceCodeLocation.attrs.class.startOffset) +}