Skip to content

Commit

Permalink
feat: script for setting up customization (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Jan 19, 2020
1 parent ccf461c commit 538fb2d
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 30 deletions.
61 changes: 52 additions & 9 deletions CUSTOMIZING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,83 @@ When customizing the PWA, always keep in mind that you probably want to upgrade
It can be tempting to always modify existing templates, component and style files inline when doing customization. However, when merging incoming upgrades the number of merge conflicts can possibly be big. So if you want to upgrade to new PWA versions later, stick to the following recommendations.

## Components
## Start Customization

To start customizing **set a prefix** for your custom components with the script `node schematics/customization <prefix>`. After that we recommend to additionally use the prefix in every component to further help identifying customized components.

```bash
$ node schematics/customization custom
$ ng g c shared/components/basket/custom-basket-display
CREATE src/app/shared/components/basket/custom-basket-display/custom-basket-display.component.ts (275 bytes)
...
```

The script also creates an **additional theme** under `src/styles/themes`. Here you can adjust colors and add overrides to existing styling.

## Import Changes from new Release

> Prior to 0.16.1 the whole of the git history changed entirely. Please see https://github.com/intershop/intershop-pwa/issues/62 for suggestions on importing the new history.
Importing changes of new releases is done with `git` tooling. If you stick to the guidelines in this chapter the problems arising in the process of updating will not be as impacting as you might fear. Also remember to `npm install` after you are importing a change that modified the `package-lock.json` and run tests and linting in the process.

For importing changes from the current Release you can use different approaches:

### 1. Merge the new release in its entirety.

This way your development will not deviate that much from the current PWA development, but you will have to resolve all appearing conflicts at once.

Just add the Intershop PWA GitHub repository as a second remote in your project and `git merge` the release branch.

### 2. Cherry-Pick individual changes from the new release.

By cherry-picking changes individually you can decide to skip certain changes and you will get smaller amounts of merge conflicts. However, that way the histories will deviate more and more over time.

To execute this add the Intershop PWA GitHub repository as a second remote in your project and `git cherry-pick` the range of commits for this release.

### 3. Rebase your changes onto the new release.

This way your project code will always be up-to-date with the current Intershop PWA history, as this history will stay the base of the project over all releases.

To perform this update use `git rebase --onto <new release tag> <old release hash>` on your projects main branch. Your running feature branches must then be rebased the same way onto the new development branch.

As it may be the best way to keep the original history intact, the upgrade process can be quite challenging. By rebasing every single one of your customizations commits, every commit is virtually re-applied onto the current PWA history. You can imagine it as pretending to start the custom project anew onto the current version. If your projects history is clean and every commit is well-described and concise, this might be a way to go.

## Specific Concerns

### Components

When **adding new functionality**, it is better to encapsulate it in **new components** that handle every aspect of the customization and just **use them in existing templates**. That way the modifications on existing code is most often kept to a single line change only.

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.

## Existing Features
### 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.

Some of the provided components supply **configuration parameters** which can be used to handle customization in an easy way (i.e. disabling features or switching between them).

## New Features
### New Features

When adding new independent features to the PWA, it might be a good idea to **add an extension** first. Use the provided schematics `ng g extension <name>` to scaffold the extension. Adding all related pages, components, models and services here is less intrusive than adding them to the existing folder structure. Add additional artifacts to extensions by supplying the `--extension` flag to schematics calls.

## Data
### Data

When **adding new fields** to PWA data models, add them to interfaces and **map them as early as possible** in mapper classes to model classes. That way the data can be readily used on templates. If improving and parsing improper data too late it could lead to more modifications on components and templates which will be harder to upgrade later on.

## NgRx
### NgRx

Adding **new data** to the state should always almost exclusively be done by adding new stores in **store groups**. Add one with `ng g store-group <group>` and then add consecutive stores with `ng g store --feature <group> <store>`. Keep modifications to the existing store as little as possible. As NgRx is loosely coupled by nature, you can deactivate effects by simply commenting out the `@Effect` decorator.

## Testing
### Testing

When modifying components it is most likely that related test cases will fail. If possible, use the Jest update feature **update snapshots** when adapting test cases. When you upgrade the PWA to a new version, those snapshots will most likely have merge conflicts in them. Here you can just accept either modification and update the test snapshots.

## Styling
### Styling

Changing the styling of **existing components** can be done by changing relevant information in the global style files under `src/styles`. If too many changes have to be made, it is better to **add the styling in additional files on global or component level**.
Changing the styling of **existing components** should be done by adding overrides in the custom theme folder under `src/styles/themes`. You can also change relevant information in the global style files under `src/styles`. If too many changes have to be made, it is better to **add the styling in additional files on global or component level**.

When styling on component level, all styling is encapsulated to exactly this component (default behavior). You can re-use variables from global styling on component level by adding imports like `@import '~theme/variables.scss';`.

## Dependencies
### Dependencies

When updating dependencies and resolving conflicts inside of `package-lock.json`, always **accept Intershop's changes** first. After that run `npm install` to regenerate the file.
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"@types/jest": "^24.0.18",
"@types/node": "^12.12.7",
"codelyzer": "^5.1.0",
"comment-json": "^2.3.1",
"cpy-cli": "^2.0.0",
"husky": "^1.3.1",
"intershop-schematics": "file:schematics/src",
Expand Down
83 changes: 83 additions & 0 deletions schematics/customization/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const fs = require('fs');
const { parse, stringify } = require('comment-json');
const { execSync } = require('child_process');
const { Project, ts } = require('ts-morph');

if (process.argv.length < 3) {
console.warn('required prefix argument missing');
process.exit(1);
}

const prefix = process.argv[2];

// replace in angular.json
const angularJson = parse(fs.readFileSync('./angular.json', { encoding: 'UTF-8' }));
const project = Object.keys(angularJson.projects)[0];
angularJson.projects[project].prefix = prefix;

if (prefix != 'ish') {
// add style definition in angular.json
const styles = angularJson.projects[project].architect.build.options.styles;
if (!styles.find(style => style.bundleName === prefix)) {
styles.push({
input: `src/styles/themes/${prefix}/style.scss`,
lazy: true,
bundleName: prefix,
});
}

// add style definition files
if (!fs.existsSync(`src/styles/themes/${prefix}`)) {
execSync(`npx ncp src/styles/themes/default src/styles/themes/${prefix} --stopOnErr`);
}
}
fs.writeFileSync('./angular.json', stringify(angularJson, null, 2));
execSync('npx prettier --write angular.json');

// replace in tslint.json
// "directive-selector": [true, "attribute", "ish", "camelCase"],
// "component-selector": [true, "element", "ish", "kebab-case"],

const addPrefix = (target, prefix) => {
if (typeof target[2] === 'string') {
target[2] = ['ish'];
}
target[2].push(prefix);
target[2] = target[2].filter((val, idx, arr) => arr.indexOf(val) === idx);
};

const tslintJson = parse(fs.readFileSync('./tslint.json', { encoding: 'UTF-8' }));
addPrefix(tslintJson.rules['directive-selector'], prefix);
addPrefix(tslintJson.rules['component-selector'], prefix);
fs.writeFileSync('./tslint.json', stringify(tslintJson, null, 2));
execSync('npx prettier --write tslint.json');

// set default theme
const tsMorphProject = new Project();
tsMorphProject.addSourceFilesAtPaths('src/environments/environment*.ts');

tsMorphProject
.getSourceFiles()
.filter(file => !file.getBaseName().includes('.model.'))
.forEach(file => {
file
.forEachDescendantAsArray()
.filter(stm => stm.getKind() == ts.SyntaxKind.VariableDeclaration && stm.getName() === 'environment')
.map(stm => stm.getInitializer())
// .filter(ole => !ole.getProperty('theme'))
.forEach(objectLiteralExpression => {
const property = objectLiteralExpression.getProperty('theme');
if (property) {
property.remove();
}
if (prefix !== 'ish') {
objectLiteralExpression.addPropertyAssignment({
name: 'theme',
initializer: `'${prefix}'`,
});
}
});
});

tsMorphProject.saveSync();
execSync('npx prettier --write src/environments/*.*');
84 changes: 63 additions & 21 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"arrow-parens": [true, "ban-single-arg-parens"],
"no-unnecessary-callback-wrapper": true,
"no-unsafe-callback-scope": false,
"no-commented-out-code": { "severity": "warning" },
"no-commented-out-code": {
"severity": "warning"
},
"no-consecutive-blank-lines": true,
"callable-types": true,
"class-name": true,
Expand Down Expand Up @@ -64,8 +66,14 @@
"modifiers": ["export", "const"],
"format": "camelCase"
},
{ "type": "type", "format": "PascalCase" },
{ "type": "enumMember", "format": "PascalCase" }
{
"type": "type",
"format": "PascalCase"
},
{
"type": "enumMember",
"format": "PascalCase"
}
],
"no-arg": true,
"no-bitwise": true,
Expand Down Expand Up @@ -125,16 +133,28 @@
"rxjs-no-ignored-subscribe": true,
"rxjs-no-unsafe-switchmap": true,
"rxjs-no-unsafe-catch": true,
"rxjs-no-unsafe-first": { "severity": "off" },
"rxjs-no-unsafe-scope": { "severity": "off" },
"rxjs-no-unsafe-first": {
"severity": "off"
},
"rxjs-no-unsafe-scope": {
"severity": "off"
},
"rxjs-no-unsafe-takeuntil": true,
"rxjs-no-subject-value": true,
"rxjs-no-subject-unsubscribe": true,
"rxjs-no-exposed-subjects": { "severity": "off" },
"rxjs-no-explicit-generics": { "severity": "off" },
"rxjs-no-exposed-subjects": {
"severity": "off"
},
"rxjs-no-explicit-generics": {
"severity": "off"
},
"rxjs-no-ignored-replay-buffer": true,
"rxjs-prefer-async-pipe": { "severity": "off" },
"rxjs-throw-error": { "severity": "off" },
"rxjs-prefer-async-pipe": {
"severity": "off"
},
"rxjs-throw-error": {
"severity": "off"
},
"private-destroy-field": true,
"ngrx-use-empty-store-type": true,
"ngrx-use-oftype-with-type": true,
Expand Down Expand Up @@ -166,12 +186,14 @@
"check-separator",
"check-type"
],
"directive-selector": [true, "attribute", "ish", "camelCase"],
"component-selector": [true, "element", "ish", "kebab-case"],
"directive-selector": [true, "attribute", ["ish"], "camelCase"],
"component-selector": [true, "element", ["ish"], "kebab-case"],
"no-inputs-metadata-property": true,
"no-assignement-to-inputs": true,
"no-outputs-metadata-property": true,
"use-component-change-detection": { "severity": "warning" },
"use-component-change-detection": {
"severity": "warning"
},
"no-host-metadata-property": true,
"no-input-rename": true,
"no-output-rename": true,
Expand Down Expand Up @@ -213,7 +235,10 @@
"name": ["*", "toBeDefined"],
"message": "Most of the time this is the wrong assertion in tests!"
},
{ "name": ["spyOn"], "message": "Use ts-mockito instead!" },
{
"name": ["spyOn"],
"message": "Use ts-mockito instead!"
},
{
"name": ["*", "select"],
"message": "Use store.pipe(select()) instead!"
Expand Down Expand Up @@ -307,7 +332,10 @@
"filePattern": ".*src/app.*",
"message": "use star imports only for aggregation of deeper lying imports"
},
{ "starImport": true, "from": "lodash.*" },
{
"starImport": true,
"from": "lodash.*"
},
{
"import": "^(?!(range|uniq|memoize|once|groupBy|countBy|flatten|isEqual|intersection)$).*",
"from": "lodash.*"
Expand Down Expand Up @@ -412,7 +440,9 @@
"exclude": ["actions$"]
}
},
"meaningful-naming-in-tests": { "severity": "warning" },
"meaningful-naming-in-tests": {
"severity": "warning"
},
"prefer-method-signature": true,
"prefer-switch": true,
"prefer-template": {
Expand All @@ -421,10 +451,14 @@
},
"use-async-synchronisation-in-tests": true,
"use-jest-extended-matchers-in-tests": true,
"no-any": { "severity": "warning" },
"no-any": {
"severity": "warning"
},
"no-commented-out-tests": true,
"no-focused-tests": true,
"no-disabled-tests": { "severity": "warning" },
"no-disabled-tests": {
"severity": "warning"
},
"no-barrel-files": true,
"use-camel-case-environment-properties": true,
"component-creation-test": true,
Expand Down Expand Up @@ -553,7 +587,6 @@
"name": "^([A-Z].*)Service$",
"file": "(/utils.*|/services/([a-z][a-z0-9-]+/|))[a-z][a-z0-9-]+/<kebab>\\.service\\.ts$"
},

// modules and routing
{
"name": "^([A-Z].+)ExportsModule$",
Expand Down Expand Up @@ -583,7 +616,10 @@
"name": "^(.*)Module$",
"file": ".*(cypress.*|/<kebab>/<kebab>|/core/[a-z][a-z0-9-]+)\\.module\\.ts$"
},
{ "name": "^(.*)Routes$", "file": ".*/<kebab>\\.module\\.ts$" },
{
"name": "^(.*)Routes$",
"file": ".*/<kebab>\\.module\\.ts$"
},
// factory pattern
{
"name": "^([A-Z].*)Helper$",
Expand Down Expand Up @@ -623,12 +659,18 @@
"name": "^([A-Z].*)State$",
"file": ".*/store/([a-z][a-z0-9-]+/|)(<kebab>/<kebab>\\.reducer|(<kebab>/|)<kebab>-store)\\.ts$"
},
{ "name": "^(initialState)$", "file": ".*/store/.*\\.reducer\\.ts$" },
{
"name": "^(initialState)$",
"file": ".*/store/.*\\.reducer\\.ts$"
},
{
"name": "^([a-z].*)Reducer$",
"file": ".*/store/([a-z][a-z0-9-]+/|)<kebab>/<kebab>\\.reducer\\.ts$"
},
{ "name": "^(metaReducers)$", "file": ".*\\.module\\.ts$" },
{
"name": "^(metaReducers)$",
"file": ".*\\.module\\.ts$"
},
{
"name": "^([a-z].*)Reducers$",
"file": ".*/store/(<kebab>/|)<kebab>-store\\.module\\.ts$"
Expand Down

0 comments on commit 538fb2d

Please sign in to comment.