Skip to content

Commit

Permalink
feat: add customized-copy schematic for creating a customization copy (
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Jan 19, 2020
1 parent a582ecd commit c6eb1df
Show file tree
Hide file tree
Showing 10 changed files with 544 additions and 53 deletions.
16 changes: 16 additions & 0 deletions CUSTOMIZING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<!--` and `-->` for HTML and `/*` and `*/` for SCSS and TypeScript.
Expand Down
8 changes: 8 additions & 0 deletions e2e/test-schematics.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions schematics/src/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
73 changes: 73 additions & 0 deletions schematics/src/customized-copy/factory.js
Original file line number Diff line number Diff line change
@@ -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;
96 changes: 96 additions & 0 deletions schematics/src/customized-copy/factory.ts
Original file line number Diff line number Diff line change
@@ -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);
};
}
Loading

0 comments on commit c6eb1df

Please sign in to comment.