Schematicsを作ってみよう

この記事は [Angular Advent Calendar 2018](https://qiita.com/advent-calendar/2018/angular) の 3 日目の記事です。

みなさん今日もAngular CLI使っていますか？

Angular CLIは、ビルド周りや開発サーバの面倒を見てくれるだけでなく、`ng generate`や`ng add`といったコマンドを実行するだけで必要なファイルの生成はもちろん、モジュールの追加や設定の変更も行ってくれるので非常に便利ですよね。

これらの処理はAngular Schematicsによって定義されており、自分で作ることもできます。また、既存のSchematicsを拡張することも可能です。

どうでしょう？試してみたくありませんか？
それでは、早速やってみましょう！

## はじめよう

まずは`schematics-cli`をインストールしましょう。

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

新規プロジェクトを作成します。

```
$ schematics blank my-schematics
$ cd my-schematics
```
※`blank`の他に`schematic`を指定すると実装サンプル付きのプロジェクトが作られます

生成されたファイルを見てみましょう。

```json:package.json
{
  "name": "my-schematics",
  "schematics": "./src/collection.json",
  "dependencies": {
  }
}
```

`package.json`の`schematics`に指定されている`collection.json`に実行対象となるSchematicsが入っています。

```json:src/collection.json
{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "my-schematics": {
      "description": "A blank schematic.",
      "factory": "./my-schematics/index#mySchematics"
    }
  }
}
```

`my-schematics`が呼ばれたときに実行されるメソッドが`./my-schematics/index.ts`の`mySchematics`という事がわかります。

`$schema`はIDEでの補完等に使われるものなので必須ではありません。

Schematicsの中身は`Rules`（= `Tree`を返す関数）を返す関数です。`Tree`はファイルシステムやファイルの変更が含まれています。

```ts:src/my-schematics/index.ts
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

export function mySchematics(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    return tree;
  };
}
```

## 作ってみよう

基本となるのは`Tree`の操作です。

```ts:src/my-schematics/index.ts
export function mySchematics(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    tree.create(_options.path + '/hi', 'Hello world!');
    return tree;
  };
}
```

ビルドして実行すると、Schematicsの出力を見ることができます。

```shell
$ npm run build
$ schematics .:my-schematics --path=./
CREATE /hi (12 bytes)
```

デフォルトではプレビューだけですが、`--dry-run=false`を指定することで実際にファイルが生成されるようになります。

```shell
$ schematics .:my-schematics --path=./ --dry-run=false
CREATE /hi (12 bytes)
$ cat hi
Hello world!
```

`.:my-schematics`の部分は`:`より前が`collection.json`へのパス、後が`collection.json`における定義名となります。そのため、他のディレクトリで呼び出す場合は`schematics ./path/to/collection.json:my-schematics`のようになります。

### ログ出力

進捗やエラー表示などには`logger`を用います。用途に応じて、

- debug
- info
- warn
- error

などがあります。

```ts
export function mySchematics(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    _context.logger.info('🎉 Hello, schematics!');
    return tree;
  };
}
```

### テンプレート

コンポーネント等に使うファイル一式を生成する際はテンプレートを利用しましょう。

```ts:src/my-schematics/index.ts
import { strings } from '@angular-devkit/core';
import {
  Rule,
  SchematicContext,
  applyTemplates,
  mergeWith,
  apply,
  url,
} from '@angular-devkit/schematics';

export function mySchematics(_options: any): Rule {
  return (_, _context: SchematicContext) => {
    return mergeWith(apply(url('./files'), [
      applyTemplates({
        ...strings,
        name: _options.name,
      }),
    ]));
  };
}
```

`url()`でテンプレートのパスを指定し、`applyTemplates()`を実行しましょう。

ファイル名は`__name__`のようにアンダースコアで囲まれた部分が置換されます。

```ejs:src/my-schematics/files/__name__.component.css.template
/* styles */
```

テンプレートはEJSのような構文で、 `<% %>`で囲んだ部分に条件式を書いたり、`<%= %>`で囲んだ部分に文字列を代入することができます。

```ejs:src/my-schematics/files/__name__.component.ts.template
<% if (name) { %>
  Hello <%= name %>, I'm a schematic.
<% } else { %>
  Why don't you give me your name with --name?
<% } %>
```

`@angular-devkit/core`には、セレクタ→クラス名の変換（`classify`）やケバブケースへの変換（`dasherize`）といった処理のための便利なユーティリティもあるので是非活用しましょう。

```ejs:src/my-schematics/files/__name__.component.ts.template
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'my-<%= name %>',
  templateUrl: './<%= name %>.component.html',
  styleUrls: ['./<%= name %>.component.css'] }
})
export class <%= classify(name) %>Component implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}
```

それでは、実行してみましょう。

```sh
$ schematics .:my-schematics --name=sample --dry-run=false
CREATE /sample.component.css (12 bytes)
CREATE /sample.component.html (35 bytes)
CREATE /sample.component.ts (270 bytes)
```

コンポーネントが生成されました :tada:


## 公開する

いい感じのSchematicsができたら是非公開してみましょう！

```
$ npm run build
$ npm publish
```

## おわりに

今回は単純なサンプルの紹介でしたが、`ng add`など他のSchematicsの実装例は以下のリポジトリを参考にすると良いと思います。

https://github.com/angular/material2/tree/master/src/lib/schematics
https://github.com/ng-bootstrap/schematics
https://github.com/ionic-team/ionic/tree/master/angular/src/schematics

Onsen UIのSchematicsも開発中です！（宣伝）
https://github.com/OnsenUI/OnsenUI/pull/2591

ちなみにschematics-cliのヘルプは↓で表示できます。

```
$ schematics --help
```

利用可能なAPIについてはGitHubのドキュメントを読みましょう。
https://github.com/angular/angular-cli/tree/master/packages/angular_devkit/schematics

明日は@MasanobuAkibaさんです！

## 参考文献
https://medium.com/@michael.warneke/merging-custom-angular-schematics-c14a303f63b6
https://github.com/angular/angular-cli/tree/master/packages/schematics/angular
https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2