diff --git a/CUSTOMIZING.md b/CUSTOMIZING.md index d1aa721409..0adf1e7867 100644 --- a/CUSTOMIZING.md +++ b/CUSTOMIZING.md @@ -55,6 +55,22 @@ When **adding new functionality**, it is better to encapsulate it in **new compo When **heavily customizing** existing components it is better to **copy components** and change all references. If 20% of the component has to be changed it is already a good idea to duplicate it. That way incoming changes will not affect your customizations. Typical hot-spots where copying is a good idea are header-related or product-detail page related customizations. +We are supplying a the schematic `customized-copy` for copying components and replacing all usages. + +```bash +$ node schematics/customization custom +$ ng g customized-copy shared/components/product/product-price +CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.html (1591 bytes) +CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.spec.ts (7632 bytes) +CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.ts (1370 bytes) +UPDATE src/app/shared/shared.module.ts (12676 bytes) +UPDATE src/app/shared/components/product/product-row/product-row.component.html (4110 bytes) +UPDATE src/app/shared/components/product/product-row/product-row.component.spec.ts (5038 bytes) +UPDATE src/app/shared/components/product/product-tile/product-tile.component.html (2140 bytes) +UPDATE src/app/shared/components/product/product-tile/product-tile.component.spec.ts (4223 bytes) +... +``` + ### Existing Features When you want to **disable code** provided by Intershop, it is better to **comment it out instead of deleting** it. This allows Git to merge changes more predictably since original and incoming passages are still similar to each other. Commenting out should be done in the form of block comments starting a line above and ending in an additional line below the code. Use `` for HTML and `/*` and `*/` for SCSS and TypeScript. diff --git a/e2e/test-schematics.sh b/e2e/test-schematics.sh index 639c3b37d6..e182172208 100644 --- a/e2e/test-schematics.sh +++ b/e2e/test-schematics.sh @@ -74,6 +74,14 @@ stat src/app/shared/cms/components/audio/audio.component.ts grep "AudioComponent" src/app/shared/cms/cms.module.ts grep "AudioComponent" src/app/shared/shared.module.ts + +node schematics/customization custom +npx ng g customized-copy shell/footer/footer + +stat src/app/shell/footer/custom-footer/custom-footer.component.ts +grep 'custom-footer' src/app/app.component.html + + git add -A npx lint-staged npx tsc --project tsconfig.spec.json diff --git a/schematics/src/collection.json b/schematics/src/collection.json index d6f7eb8d39..7aa21b4610 100644 --- a/schematics/src/collection.json +++ b/schematics/src/collection.json @@ -80,6 +80,11 @@ "description": "Perform de-containerization from 0.15 to 0.16.", "schema": "./migration/decontainerize-0.16-to-0.17/schema.json", "hidden": true + }, + "customized-copy": { + "factory": "./customized-copy/factory#customize", + "description": "Create a copy of Component for customization.", + "schema": "./customized-copy/schema.json" } } } diff --git a/schematics/src/customized-copy/factory.js b/schematics/src/customized-copy/factory.js new file mode 100644 index 0000000000..0e99a7f22e --- /dev/null +++ b/schematics/src/customized-copy/factory.js @@ -0,0 +1,73 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@angular-devkit/core"); +const schematics_1 = require("@angular-devkit/schematics"); +const tsquery_1 = require("@phenomnomnominal/tsquery"); +const project_1 = require("@schematics/angular/utility/project"); +const path_1 = require("path"); +const factory_1 = require("../move-component/factory"); +const common_1 = require("../utils/common"); +const filesystem_1 = require("../utils/filesystem"); +const registration_1 = require("../utils/registration"); +function customize(options) { + return host => { + if (!options.project) { + throw new schematics_1.SchematicsException('Option (project) is required.'); + } + if (!options.from) { + throw new schematics_1.SchematicsException('Option (from) is required.'); + } + const project = project_1.getProject(host, options.project); + const from = options.from.replace(/\/$/, ''); + const sourceRoot = project.sourceRoot; + const dir = host.getDir(`${sourceRoot}/app/${from}`); + const fromName = path_1.basename(dir.path); + if (!dir || !dir.subfiles.length || !dir.subfiles.find(v => v.endsWith('component.ts'))) { + throw new schematics_1.SchematicsException('Option (from) is not pointing to a component folder.'); + } + dir.subfiles.forEach(file => { + host.create(path_1.join(dir.parent.path, `${project.prefix}-${fromName}`, `${project.prefix}-${file}`), host.read(dir.file(file).path)); + }); + const toName = `${project.prefix}-${fromName}`; + host.visit(file => { + if (file.startsWith(`/${sourceRoot}/app/`) && !file.includes(`/${fromName}/${fromName}.component`)) { + if (file.includes(`/${project.prefix}-${fromName}/`) && file.endsWith('.component.ts')) { + factory_1.updateComponentDecorator(host, file, `ish-${fromName}`, fromName); + factory_1.updateComponentDecorator(host, file, fromName, toName); + } + if (file.endsWith('.ts')) { + factory_1.updateComponentClassName(host, file, core_1.strings.classify(fromName) + 'Component', core_1.strings.classify(toName) + 'Component'); + const imports = tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), file.includes(toName) ? `ImportDeclaration` : `ImportDeclaration[text=/.*${fromName}.*/]`).filter((x) => file.includes(fromName) || x.getText().includes(`/${fromName}/`)); + if (imports.length) { + const updates = []; + imports.forEach(importDeclaration => { + tsquery_1.tsquery(importDeclaration, 'StringLiteral').forEach(node => { + const replacement = node + .getFullText() + .replace(new RegExp(`/${fromName}/${fromName}.component`), `/${toName}/${toName}.component`) + .replace(new RegExp(`/${fromName}.component`), `/${toName}.component`); + if (node.getFullText() !== replacement) { + updates.push({ node, replacement }); + } + }); + }); + if (updates.length) { + const updater = host.beginUpdate(file); + updates.forEach(({ node, replacement }) => { + updater.remove(node.pos, node.end - node.pos).insertLeft(node.pos, replacement); + }); + host.commitUpdate(updater); + } + } + } + factory_1.updateComponentSelector(host, file, fromName, `${project.prefix}-${fromName}`, false); + } + }); + let options2 = { name: from, project: options.project }; + options2 = common_1.applyNameAndPath('component', host, options2); + options2 = common_1.determineArtifactName('component', host, options2); + options2 = common_1.findDeclaringModule(host, options2); + return registration_1.addDeclarationToNgModule(options2); + }; +} +exports.customize = customize; diff --git a/schematics/src/customized-copy/factory.ts b/schematics/src/customized-copy/factory.ts new file mode 100644 index 0000000000..0ffa2adb9d --- /dev/null +++ b/schematics/src/customized-copy/factory.ts @@ -0,0 +1,96 @@ +import { strings } from '@angular-devkit/core'; +import { Rule, SchematicsException } from '@angular-devkit/schematics'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { getProject } from '@schematics/angular/utility/project'; +import { basename, join } from 'path'; +import * as ts from 'typescript'; + +import { updateComponentClassName, updateComponentDecorator, updateComponentSelector } from '../move-component/factory'; +import { applyNameAndPath, determineArtifactName, findDeclaringModule } from '../utils/common'; +import { readIntoSourceFile } from '../utils/filesystem'; +import { addDeclarationToNgModule } from '../utils/registration'; + +import { CustomizedCopyOptionsSchema as Options } from './schema'; + +export function customize(options: Options): Rule { + return host => { + if (!options.project) { + throw new SchematicsException('Option (project) is required.'); + } + + if (!options.from) { + throw new SchematicsException('Option (from) is required.'); + } + + const project = getProject(host, options.project); + + const from = options.from.replace(/\/$/, ''); + const sourceRoot = project.sourceRoot; + const dir = host.getDir(`${sourceRoot}/app/${from}`); + + const fromName = basename(dir.path); + + if (!dir || !dir.subfiles.length || !dir.subfiles.find(v => v.endsWith('component.ts'))) { + throw new SchematicsException('Option (from) is not pointing to a component folder.'); + } + + dir.subfiles.forEach(file => { + host.create( + join(dir.parent.path, `${project.prefix}-${fromName}`, `${project.prefix}-${file}`), + host.read(dir.file(file).path) + ); + }); + + const toName = `${project.prefix}-${fromName}`; + host.visit(file => { + if (file.startsWith(`/${sourceRoot}/app/`) && !file.includes(`/${fromName}/${fromName}.component`)) { + if (file.includes(`/${project.prefix}-${fromName}/`) && file.endsWith('.component.ts')) { + updateComponentDecorator(host, file, `ish-${fromName}`, fromName); + updateComponentDecorator(host, file, fromName, toName); + } + if (file.endsWith('.ts')) { + updateComponentClassName( + host, + file, + strings.classify(fromName) + 'Component', + strings.classify(toName) + 'Component' + ); + + const imports = tsquery( + readIntoSourceFile(host, file), + file.includes(toName) ? `ImportDeclaration` : `ImportDeclaration[text=/.*${fromName}.*/]` + ).filter((x: ts.ImportDeclaration) => file.includes(fromName) || x.getText().includes(`/${fromName}/`)); + if (imports.length) { + const updates: { node: ts.Node; replacement: string }[] = []; + imports.forEach(importDeclaration => { + tsquery(importDeclaration, 'StringLiteral').forEach(node => { + const replacement = node + .getFullText() + .replace(new RegExp(`/${fromName}/${fromName}.component`), `/${toName}/${toName}.component`) + .replace(new RegExp(`/${fromName}.component`), `/${toName}.component`); + if (node.getFullText() !== replacement) { + updates.push({ node, replacement }); + } + }); + }); + if (updates.length) { + const updater = host.beginUpdate(file); + updates.forEach(({ node, replacement }) => { + updater.remove(node.pos, node.end - node.pos).insertLeft(node.pos, replacement); + }); + host.commitUpdate(updater); + } + } + } + updateComponentSelector(host, file, fromName, `${project.prefix}-${fromName}`, false); + } + }); + + let options2: unknown = { name: from, project: options.project }; + options2 = applyNameAndPath('component', host, options2); + options2 = determineArtifactName('component', host, options2); + options2 = findDeclaringModule(host, options2); + + return addDeclarationToNgModule(options2); + }; +} diff --git a/schematics/src/customized-copy/factory_spec.ts b/schematics/src/customized-copy/factory_spec.ts new file mode 100644 index 0000000000..bf17ac6197 --- /dev/null +++ b/schematics/src/customized-copy/factory_spec.ts @@ -0,0 +1,236 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +import { createApplication, createModule, createSchematicRunner } from '../utils/testHelper'; + +describe('customized-copy Schematic', () => { + const schematicRunner = createSchematicRunner(); + + let appTree: UnitTestTree; + beforeEach(async () => { + appTree = await createApplication(schematicRunner) + .pipe(createModule(schematicRunner, { name: 'shared' })) + .toPromise(); + appTree.overwrite('/projects/bar/src/app/app.component.html', ''); + appTree = await schematicRunner + .runSchematicAsync('component', { project: 'bar', name: 'foo/dummy' }, appTree) + .toPromise(); + appTree = await schematicRunner + .runSchematicAsync('component', { project: 'bar', name: 'shared/dummy-two' }, appTree) + .toPromise(); + + appTree.overwrite( + '/projects/bar/src/app/shared/dummy-two/dummy-two.component.ts', + `import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { DummyComponent } from '../../../foo/dummy/dummy.component'; + +@Component({ + selector: 'ish-dummy-two', + templateUrl: './dummy-two.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DummyTwoComponent {} +` + ); + + const angularJson = JSON.parse(appTree.readContent('/angular.json')); + angularJson.projects.bar.prefix = 'custom'; + appTree.overwrite('/angular.json', JSON.stringify(angularJson)); + }); + + it('should be created', () => { + expect(appTree.files.filter(f => f.endsWith('component.ts'))).toMatchInlineSnapshot(` + Array [ + "/projects/bar/src/app/app.component.ts", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.ts", + "/projects/bar/src/app/foo/dummy/dummy.component.ts", + ] + `); + expect(appTree.readContent('/projects/bar/src/app/shared/dummy-two/dummy-two.component.ts')).toMatchInlineSnapshot(` + "import { ChangeDetectionStrategy, Component } from '@angular/core'; + import { DummyComponent } from '../../../foo/dummy/dummy.component'; + + @Component({ + selector: 'ish-dummy-two', + templateUrl: './dummy-two.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + export class DummyTwoComponent {} + " + `); + + expect(JSON.parse(appTree.readContent('/angular.json')).projects.bar.prefix).toMatchInlineSnapshot(`"custom"`); + + expect(appTree.readContent('/projects/bar/src/app/app.module.ts')).toMatchInlineSnapshot(` + "import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppRoutingModule } from './app-routing.module'; + import { AppComponent } from './app.component'; + import { DummyComponent } from './foo/dummy/dummy.component'; + + @NgModule({ + declarations: [ + AppComponent, + DummyComponent + ], + imports: [ + BrowserModule, + AppRoutingModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + " + `); + expect(appTree.readContent('/projects/bar/src/app/shared/shared.module.ts')).toMatchInlineSnapshot(` + "import { NgModule } from '@angular/core'; + import { DummyTwoComponent } from './dummy-two/dummy-two.component'; + + @NgModule({ + imports: [], + declarations: [DummyTwoComponent], + exports: [], + entryComponents: [] + }) + export class SharedModule { } + " + `); + }); + + it('should customize component in root module', async () => { + appTree = await schematicRunner + .runSchematicAsync('customized-copy', { project: 'bar', from: 'foo/dummy' }, appTree) + .toPromise(); + + expect(appTree.files.filter(x => x.includes('/src/app/'))).toMatchInlineSnapshot(` + Array [ + "/projects/bar/src/app/app-routing.module.ts", + "/projects/bar/src/app/app.module.ts", + "/projects/bar/src/app/app.component.css", + "/projects/bar/src/app/app.component.html", + "/projects/bar/src/app/app.component.spec.ts", + "/projects/bar/src/app/app.component.ts", + "/projects/bar/src/app/shared/shared.module.ts", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.ts", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.html", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.spec.ts", + "/projects/bar/src/app/foo/dummy/dummy.component.ts", + "/projects/bar/src/app/foo/dummy/dummy.component.html", + "/projects/bar/src/app/foo/dummy/dummy.component.spec.ts", + "/projects/bar/src/app/foo/custom-dummy/custom-dummy.component.ts", + "/projects/bar/src/app/foo/custom-dummy/custom-dummy.component.html", + "/projects/bar/src/app/foo/custom-dummy/custom-dummy.component.spec.ts", + ] + `); + + expect(appTree.readContent('/projects/bar/src/app/app.module.ts')).toMatchInlineSnapshot(` + "import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppRoutingModule } from './app-routing.module'; + import { AppComponent } from './app.component'; + import { CustomDummyComponent } from './foo/custom-dummy/custom-dummy.component'; + import { DummyComponent } from './foo/dummy/dummy.component'; + + @NgModule({ + declarations: [ + AppComponent, + CustomDummyComponent, + DummyComponent + ], + imports: [ + BrowserModule, + AppRoutingModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + " + `); + + expect(appTree.readContent('/projects/bar/src/app/foo/custom-dummy/custom-dummy.component.ts')) + .toMatchInlineSnapshot(` + "import { ChangeDetectionStrategy, Component } from '@angular/core'; + + @Component({ + selector: 'custom-dummy', + templateUrl: './custom-dummy.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + export class CustomDummyComponent {} + " + `); + + const specFile = appTree.readContent('/projects/bar/src/app/foo/custom-dummy/custom-dummy.component.spec.ts'); + expect(specFile).toContain("import { CustomDummyComponent } from './custom-dummy.component'"); + expect(specFile).toContain('let fixture: ComponentFixture'); + }); + + it('should customize component in shared module', async () => { + appTree = await schematicRunner + .runSchematicAsync('customized-copy', { project: 'bar', from: 'shared/dummy-two' }, appTree) + .toPromise(); + + expect(appTree.files.filter(x => x.includes('/src/app/'))).toMatchInlineSnapshot(` + Array [ + "/projects/bar/src/app/app-routing.module.ts", + "/projects/bar/src/app/app.module.ts", + "/projects/bar/src/app/app.component.css", + "/projects/bar/src/app/app.component.html", + "/projects/bar/src/app/app.component.spec.ts", + "/projects/bar/src/app/app.component.ts", + "/projects/bar/src/app/shared/shared.module.ts", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.ts", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.html", + "/projects/bar/src/app/shared/dummy-two/dummy-two.component.spec.ts", + "/projects/bar/src/app/shared/custom-dummy-two/custom-dummy-two.component.ts", + "/projects/bar/src/app/shared/custom-dummy-two/custom-dummy-two.component.html", + "/projects/bar/src/app/shared/custom-dummy-two/custom-dummy-two.component.spec.ts", + "/projects/bar/src/app/foo/dummy/dummy.component.ts", + "/projects/bar/src/app/foo/dummy/dummy.component.html", + "/projects/bar/src/app/foo/dummy/dummy.component.spec.ts", + ] + `); + + expect(appTree.readContent('/projects/bar/src/app/app.module.ts')).not.toContain( + 'import { CustomDummyTwoComponent }' + ); + + expect(appTree.readContent('/projects/bar/src/app/shared/shared.module.ts')).toMatchInlineSnapshot(` + "import { NgModule } from '@angular/core'; + import { CustomDummyTwoComponent } from './custom-dummy-two/custom-dummy-two.component'; + import { DummyTwoComponent } from './dummy-two/dummy-two.component'; + + @NgModule({ + imports: [], + declarations: [CustomDummyTwoComponent, DummyTwoComponent], + exports: [], + entryComponents: [] + }) + export class SharedModule { } + " + `); + + expect(appTree.readContent('/projects/bar/src/app/shared/custom-dummy-two/custom-dummy-two.component.ts')) + .toMatchInlineSnapshot(` + "import { ChangeDetectionStrategy, Component } from '@angular/core'; + import { DummyComponent } from '../../../foo/dummy/dummy.component'; + + @Component({ + selector: 'custom-dummy-two', + templateUrl: './custom-dummy-two.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + export class CustomDummyTwoComponent {} + " + `); + + const specFile = appTree.readContent( + '/projects/bar/src/app/shared/custom-dummy-two/custom-dummy-two.component.spec.ts' + ); + expect(specFile).toContain("import { CustomDummyTwoComponent } from './custom-dummy-two.component'"); + expect(specFile).toContain('let fixture: ComponentFixture'); + }); +}); diff --git a/schematics/src/customized-copy/schema.d.ts b/schematics/src/customized-copy/schema.d.ts new file mode 100644 index 0000000000..d9b7afc44d --- /dev/null +++ b/schematics/src/customized-copy/schema.d.ts @@ -0,0 +1,13 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface CustomizedCopyOptionsSchema { + project?: string; + /** + * The folder of the component source. + */ + from?: string; +} diff --git a/schematics/src/customized-copy/schema.json b/schematics/src/customized-copy/schema.json new file mode 100644 index 0000000000..42b488ec6e --- /dev/null +++ b/schematics/src/customized-copy/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "CustomizedCopy", + "title": "Customized Copy Options Schema", + "type": "object", + "properties": { + "project": { + "type": "string", + "$default": { + "$source": "projectName" + }, + "visible": false + }, + "from": { + "type": "string", + "description": "The folder of the component source.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What source folder has the component? (relative to src/app/)" + } + } +} diff --git a/schematics/src/move-component/factory.js b/schematics/src/move-component/factory.js index 56daf90c75..fe890e9ffb 100644 --- a/schematics/src/move-component/factory.js +++ b/schematics/src/move-component/factory.js @@ -94,29 +94,12 @@ function move(options) { if (file.includes(from + '/')) { renames.push([file, replacePath(file)]); if (fromName !== toName && file.endsWith('.component.ts')) { - const updater = host.beginUpdate(file); - tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), 'Decorator Identifier[name=Component]') - .map(x => x.parent) - .forEach(componentDecorator => { - tsquery_1.tsquery(componentDecorator, 'PropertyAssignment') - .map((pa) => pa.initializer) - .forEach(x => { - updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromName, toName)); - }); - }); - host.commitUpdate(updater); + updateComponentDecorator(host, file, fromName, toName); } } if (file.endsWith('.ts')) { if (fromClassName !== toClassName) { - const identifiers = tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), `Identifier[name=${fromClassName}]`); - if (identifiers.length) { - const updater = host.beginUpdate(file); - identifiers.forEach(x => updater - .remove(x.pos, x.end - x.pos) - .insertLeft(x.pos, x.getFullText().replace(fromClassName, toClassName))); - host.commitUpdate(updater); - } + updateComponentClassName(host, file, fromClassName, toClassName); } const imports = tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), file.includes(fromName) ? `ImportDeclaration` : `ImportDeclaration[text=/.*${fromName}.*/]`).filter((x) => file.includes(fromName) || x.getText().includes(`/${fromName}/`)); if (imports.length) { @@ -139,11 +122,7 @@ function move(options) { } } else if (fromName !== toName && file.endsWith('.html')) { - const content = host.read(file).toString(); - const replacement = content.replace(new RegExp(`(?!.*${fromName}[a-z-]+.*)ish-${fromName}`, 'g'), 'ish-' + toName); - if (content !== replacement) { - host.overwrite(file, replacement); - } + updateComponentSelector(host, file, fromName, toName); } } }); @@ -154,3 +133,34 @@ function move(options) { }; } exports.move = move; +function updateComponentSelector(host, file, fromName, toName, includePrefix = true) { + const content = host.read(file).toString(); + const replacement = content.replace(new RegExp(`(?!.*${fromName}[a-z-]+.*)ish-${fromName}`, 'g'), (includePrefix ? 'ish-' : '') + toName); + if (content !== replacement) { + host.overwrite(file, replacement); + } +} +exports.updateComponentSelector = updateComponentSelector; +function updateComponentClassName(host, file, fromClassName, toClassName) { + const identifiers = tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), `Identifier[name=${fromClassName}]`); + if (identifiers.length) { + const updater = host.beginUpdate(file); + identifiers.forEach(x => updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromClassName, toClassName))); + host.commitUpdate(updater); + } +} +exports.updateComponentClassName = updateComponentClassName; +function updateComponentDecorator(host, file, fromName, toName) { + const updater = host.beginUpdate(file); + tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), 'Decorator Identifier[name=Component]') + .map(x => x.parent) + .forEach(componentDecorator => { + tsquery_1.tsquery(componentDecorator, 'PropertyAssignment') + .map((pa) => pa.initializer) + .forEach(x => { + updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromName, toName)); + }); + }); + host.commitUpdate(updater); +} +exports.updateComponentDecorator = updateComponentDecorator; diff --git a/schematics/src/move-component/factory.ts b/schematics/src/move-component/factory.ts index eef66db03b..bb75971e60 100644 --- a/schematics/src/move-component/factory.ts +++ b/schematics/src/move-component/factory.ts @@ -115,31 +115,12 @@ export function move(options: Options): Rule { renames.push([file, replacePath(file)]); if (fromName !== toName && file.endsWith('.component.ts')) { - const updater = host.beginUpdate(file); - tsquery(readIntoSourceFile(host, file), 'Decorator Identifier[name=Component]') - .map(x => x.parent) - .forEach(componentDecorator => { - tsquery(componentDecorator, 'PropertyAssignment') - .map((pa: ts.PropertyAssignment) => pa.initializer) - .forEach(x => { - updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromName, toName)); - }); - }); - host.commitUpdate(updater); + updateComponentDecorator(host, file, fromName, toName); } } if (file.endsWith('.ts')) { if (fromClassName !== toClassName) { - const identifiers = tsquery(readIntoSourceFile(host, file), `Identifier[name=${fromClassName}]`); - if (identifiers.length) { - const updater = host.beginUpdate(file); - identifiers.forEach(x => - updater - .remove(x.pos, x.end - x.pos) - .insertLeft(x.pos, x.getFullText().replace(fromClassName, toClassName)) - ); - host.commitUpdate(updater); - } + updateComponentClassName(host, file, fromClassName, toClassName); } const imports = tsquery( @@ -165,14 +146,7 @@ export function move(options: Options): Rule { } } } else if (fromName !== toName && file.endsWith('.html')) { - const content = host.read(file).toString(); - const replacement = content.replace( - new RegExp(`(?!.*${fromName}[a-z-]+.*)ish-${fromName}`, 'g'), - 'ish-' + toName - ); - if (content !== replacement) { - host.overwrite(file, replacement); - } + updateComponentSelector(host, file, fromName, toName); } } }); @@ -183,3 +157,39 @@ export function move(options: Options): Rule { }); }; } + +export function updateComponentSelector(host, file, fromName: string, toName: string, includePrefix: boolean = true) { + const content = host.read(file).toString(); + const replacement = content.replace( + new RegExp(`(?!.*${fromName}[a-z-]+.*)ish-${fromName}`, 'g'), + (includePrefix ? 'ish-' : '') + toName + ); + if (content !== replacement) { + host.overwrite(file, replacement); + } +} + +export function updateComponentClassName(host, file, fromClassName: string, toClassName: string) { + const identifiers = tsquery(readIntoSourceFile(host, file), `Identifier[name=${fromClassName}]`); + if (identifiers.length) { + const updater = host.beginUpdate(file); + identifiers.forEach(x => + updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromClassName, toClassName)) + ); + host.commitUpdate(updater); + } +} + +export function updateComponentDecorator(host, file, fromName: string, toName: string) { + const updater = host.beginUpdate(file); + tsquery(readIntoSourceFile(host, file), 'Decorator Identifier[name=Component]') + .map(x => x.parent) + .forEach(componentDecorator => { + tsquery(componentDecorator, 'PropertyAssignment') + .map((pa: ts.PropertyAssignment) => pa.initializer) + .forEach(x => { + updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromName, toName)); + }); + }); + host.commitUpdate(updater); +}