Angular Schematics を作成してアプリに適用するまでの手順

Angular アプリを「決まったパターンで量産」していて、お決まりのコードをいつもコピペするのが面倒なので、自動で出来ないかということで Schematics を調査したメモ。

## 0. schematics-cli をインストールする（初回のみ）

```terminal
npm install -g @angular-devkit/schematics-cli
```

尚、開発環境の node, Angular 等のバージョンは以下。

```
node v11.13.0
npm v6.9.0
@angular-devkit/core@8.3.23
@angular/cli@8.3.18
```

## 1. schematics プロジェクトを作る

```terminal
schematics blank my-schematics
```

## 2. schematics プロジェクトの編集

VSCode で ``./my-schematics/`` を開いて。

まずは ``package.json`` を開いてnpmパッケージ名を設定。バージョンも付けときますか。

**./package.json**

```terminal
{
  "name": "@amay077/my-schematics", ←編集
  "version": "1.0.0", ←編集
  "description": "A blank schematics",
＜以下略＞
```

次に ``tsconfig.json`` に以下を追加。
出力ディレクトリを ``./built`` に。
``tsc`` で出力ディレクトリやテンプレートがビルド対象にならないように ``exclude`` に追加。

**./tsconfig.json**

```json
    "types": [
      "jasmine",
      "node"
    ],
    "outDir": "built" ←追加
  },
  "exclude": [
    "src/*/files/**/*",
    "built",     ←追加
    "templates"  ←追加
  ]
}
```

次に ``collection.json`` を次のように編集。

``./src/collection.json``

名前を ``ng-add`` にしておくと、``ng add <schematicsの名前>`` で適用できるようになる（らしい）。
factory はビルドされた ``*.js`` ファイルが出力されるディレクトリ(``./built``)を指すようにする。

```terminal
{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": { ←編集
      "description": "A blank schematic.",
      "factory": "../build/my-schematics/index#mySchematics" ←編集
    }
  }
}
```

そして schematics 自体の実装をします。

まずは ``index.ts`` 。

やっていることをざざっと。

1. ``addDependencies`` で ``package.json`` に npm パッケージを追加して ``npm install`` を実行。
2. ``copyTemplateFiles`` で、用意されたファイル群をアプリプロジェクトに適用。

**./src/my-schematics/index.ts**

```typescript
import { Rule, chain, SchematicsException, Tree, SchematicContext, mergeWith, apply, url, applyTemplates, move, forEach } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { strings } from '@angular-devkit/core';
import { normalize } from 'path';

export function mySchematics(_options: any): Rule {
  return chain([
    addDependencies(_options), 
    copyTemplateFiles(_options),
  ]);
}

export function addDependencies(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {

    const buf = tree.read('package.json');
    if (!buf) {
      throw new SchematicsException('cannot find package.json');
    }
    const content = JSON.parse(buf.toString('utf-8'));

    content.dependencies = {
      ...content.dependencies, 
      'ngx-toastr': '^11.2.1',
    };

    tree.overwrite('package.json', JSON.stringify(content, null, 2));
    _context.addTask(new NodePackageInstallTask());
    return tree;
  };
}

export function copyTemplateFiles(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {

    return mergeWith(apply(url('../../templates/'), [
      applyTemplates({
        projectName: 'MYAPP', // ← TODO package.json やアプリディレクトリからプロジェクト名を取得した方が良さそう
      }),      
      move(normalize(`./`)),
      forEach((fileEntry) => {
        if (tree.exists(fileEntry.path)) {
          tree.overwrite(fileEntry.path, fileEntry.content);
          return null;
        }
        return fileEntry;
      })      
    ]));
  };
}
```

コピーするテンプレートファイル群は ``templates`` ディレクトリ以下に準備。

**./template/.node-version**

```text
v11.13.0
```

**./templates/src/app/app.component.ts.template**

```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <!--The content below is only a placeholder and can be replaced.-->
    <div style="text-align:center" class="content">
      <h1>
        こんにちは {{title}}!
      </h1>
    </div>
    <router-outlet></router-outlet>
  `,
  styles: []
})
export class AppComponent {
  title = '<%= projectName %>';
}
```

``index.ts`` 内 ``copyTemplateFiles`` の ``applyTemplates`` により、拡張子が ``.template`` のファイルは渡されたパラメータが埋め込まれる。 ``*.template`` 以外のファイルはそのまま上書きコピーされる。

``./vscode`` や ``.gitignore``, ``.editorconfig`` などを ``./templates`` に入れておくと、いちいち他からコピーしなくて済むので楽。

## 3. schematics プロジェクトのビルド

my-schematics ディレクトリで npm run build する。

```terminal
npm run build
```

ここまでで、schematics プロジェクトの構成は次の通り。

```
/hoge
 └/my-schematics
   ├─tsconfig.json
   ├─package.json
   ├─/src
   │  ├─collection.json
   │  └─/my-schematics
   │     └─index.ts
   ├─/templates
   │  ├─.node-version
   │  └─/src
   │     └─/app
   │        └─app.component.ts.template
   ├─/node_module ← schematics プロジェクトに必要な npm モジュール群
   └─/built
      └─/my-schematics
         └─*.* ← npm run build で生成された(主に *.js)ファイル
```

## 4. Angular アプリを作る

schematics を適用するアプリを ``ng new`` で作成する。オプションはお好みで。
アプリ作成直後で、一応 ``git commit`` しておく。

```terminal
ng new --minimal=true --routing=true --style=scss --name MyApp
git add .
git commit -m "initial commit"
```

アプリ作成後のディレクトリ構成は次の通り。

```
/hoge
 ├/my-schematics
 └/MyApp
```

5. schematics を適用する

```bash
cd MyApp
npm link ../my-schematics
ng add @amay077/my-schematics
```

``npm link`` で、ローカルの schematics NPM プロジェクトにリンクして使用できるようにする。
``ng add`` で、schematics をアプリプロジェクトに適用する。

実行後、``git status`` すると、適用による差分更新が把握できるでしょう。

```terminal
        modified:   package-lock.json
        modified:   package.json
        modified:   src/app/app.component.ts

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .node-version
```

## 6. 適用後のアプリをビルド＆実行する

```terminal
ng serve
```

をした後、ブラウザで ``http://localhost:4200`` を開く。
下図のような表示になれば適用成功。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/3f4124c8-da95-865e-179f-f49c6821ed41.png)

## 7. Schematics 開発のまわし方(模索中)

1. my-schematics プロジェクトの実装を修正する
2. ``./my-schematics`` ディレクトリで ``npm run build`` する
3. ``../MyApp`` に移動
4. 必要に応じて ``git reset --hard`` で、アプリプロジェクトへの変更を破棄する
5. ``npm link ../my-schematics`` と ``ng add @amay077/my-schematics`` を実行する

※ローカルに npm registry を立てておいて、そこに ``@amay077/my-schematics`` を publish すれば、``npm link`` は不要。
※npm link したパッケージの破棄は ``npm unlink -g @amay077/my-schematics`` を実行。


## 参考

* [Schematicsを作ってみよう - Qiita](https://qiita.com/puku0x/items/462a038133e7233dfaed)
* [実践！Schematics - Qiita](https://qiita.com/Quramy/items/a06d62132007807062df)
* [Angular - Schematics](https://angular.io/guide/schematics)
* [How to overwrite file with angular schematics? - Stack Overflow](https://stackoverflow.com/questions/48957132/how-to-overwrite-file-with-angular-schematics)

## 次にやりたいこと

* ``ng g component xxx`` すると、``app.module.ts`` に自動的にコード追加されるやつ
    * angular-cli の github から ``ng g component`` のコードを探せばできそう
* schematics プロジェクトを private な npm registry に publish して運用したい
    * verdaccio, GitHub Packages, Azure DevOps(Artifacts) など