@capacitor/angularを事例にng addを読む（追記あり）

静的サイトをラッピングしてAppStoreやGoogle Playから配信するモバイルアプリにすることができるCapacitorというライブラリがあります。考え方としてはCordovaの後継であり、HTML5ハイブリッドアプリやガワアプリというようなアプリをつくることができます。

そのCapacitorが、`ng add` に対応する `@capacitor/angular` というパッケージを提供しているので、そのコードを読んでみましょう。

ionic-team/capacitor-angular-toolkit
https://github.com/ionic-team/capacitor-angular-toolkit/blob/master/schematics/add/index.ts

# コードリーディング
## 構成
まず、何によって `@capacitor/angular` パッケージができているかをみるために、package.jsonをみます（抜粋）

```package.json
{
..中略..
  "peerDependencies": {
    "@angular-devkit/core": "^8.0.0",
    "@angular-devkit/schematics": "^8.0.0",
    "rxjs": "~6.4.0",
    "typescript": "~3.4.3"
  },
..中略..
  "schematics": "./schematics/collection.json"
}
```

rxjsとtypescriptを利用している以外に、 `@angular-devkit` を利用しています。Angularは、Angular を拡張するための開発パッケージとして、 `@angular-devkit` 、 `schematics` を提供しており、 `@angular-devkit/core` はそのコアファイル、 `@angular-devkit/schematics` はAngular CLIの機能拡張などに利用することができます。今回取り上げて `ng add` の拡張だけではなく、 `ng generate` で任意のテンプレートを追加したりすることも可能です。

そして `schematics` キーで、このパッケージのschematicsの内容を記述した `collection.json` を指定しているので、続いてこちらをみることにします。

https://github.com/ionic-team/capacitor-angular-toolkit/blob/master/schematics/collection.json

```collection.json
{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
  "ng-add": {
      "description": "Add Capacitor to your project",
      "factory": "./add",
      "schema": "./add/schema.json"
    },
  "cap-init": {
      "description": "Run Cap init",
      "factory": "./cap-init",
      "schema": "./cap-init/schema.json"
    }
  }
}
```
`$scheme` キーは、このcollectionが何に従って記述されてるかを示しています（IDEの補完などに利用されます）。そして、2つの `schematics ` を定義しています。`ng add` が実行されると `ng-add` が自動的に実行され、 `cap-init` は `ng-add` の実行中に `RunSchematicTask` で実行されます。では、 `ng-add` で具体的にどんな処理が行われるかをみてみましょう。

## 処理

https://github.com/ionic-team/capacitor-angular-toolkit/blob/master/schematics/add/index.ts

100行近くありますが、関数が多く、実際に実行されているのはこの部分です。

```index.ts
export default function ngAdd(options: CapAddOptions): Rule {
  return async (host: Tree) => {
    const workspace = await getWorkspace(host);

    if (!options.project) {
      options.project = workspace.extensions.defaultProject as string;
    }

    const projectTree = workspace.projects.get(options.project);

    if (projectTree.extensions['projectType'] !== 'application') {
      throw new SchematicsException(
        `Capacitor Add requires a project type of "application".`
      );
    }

    const packageMgm = getPackageManager(projectTree.root);
    const distTarget = projectTree.targets.get('build').options[
      'outputPath'
    ] as string;
    const sourcePath = projectTree.sourceRoot;

    return chain([
      addCapacitorToPackageJson(),
      addCapPluginsToAppComponent(sourcePath),
      capInit(options.project, packageMgm, distTarget),
    ]);
  };
}
```

`getWorkspace()` は `@schematics/angular/utility/workspace` からAngularのworkspace（※ angular.jsonで設定されています）を取得することができます。このように、様々なメソッドが用意されています。

まずworkspaceを取得して、workspaceにprojectがない場合はデフォルトのものを追加しています。（Ionicの場合は `app` 。 `angular.json` で設定されている値です）

そして、その `projectType` が `application` 以外の場合はCapacitorを利用できないとエラーを返します。問題ない場合は、 `getPackageManager` メソッドからパッケージを取得してきて、またビルドの `outputPath` をprojectから取得してきます。

そして、自作している `addCapacitorToPackageJson()` （dependenciesに@capacitor/core、devDependenciesに@capacitor/cliを追加）と、 `addCapPluginsToAppComponent` メソッド（app.component.tsに `import { Plugins } from '@capacitor/core';` を追加）するメソッドとともに、Capacitor CLIをつかって `npx cap init` を行う `capInit` メソッドを実行しています。

# Prettierパッケージをつくってみる
何となくどのように `ng add` が実装されてるかがわかりました。では、 `@capacitor/angular` をベースに、Prettierの自作パッケージをつくってみましょう。

まず `@capacitor/angular` をForkします。パッケージ名は `prettier-angular-toolkit` とします。

https://github.com/rdlabo/prettier-angular-toolkit

## 1. パッケージ名などの変更
package.jsonの情報を変更します。ゼロからつくるより、コードリーディングしてOSSの他パッケージをForkしてつくった方が土台がしっかりしてるので、結構好みだったりします。自分で実装することによって、元コードの理解も深まりますし。

## 2. 不要なフォルダを削除
`schematics/cap-init` は今回使わないので削除します。

## 3. パッケージの変更
インストールするパッケージを変更します。 `schematics/add/index.ts` でインストールするパッケージを、Capacitor関係からPrettierまわりに変更します。今回は、 `dependencies` はないので当該行は削除して、 `devDependencies` を以下に書き換えます。

```schematics/add/index.ts
function addFormatterToPackageJson(): Rule {
  return (host: Tree) => {
    addPackageToPackageJson(
      host,
      'devDependencies',
      'prettier',
      'latest'
    );
    addPackageToPackageJson(
      host,
      'devDependencies',
      'lint-staged',
      'latest'
    );
    addPackageToPackageJson(
      host,
      'devDependencies',
      '@kaizenplatform/prettier-config',
      'latest'
    );
    return host;
  };
}
```

`addPackageToPackageJson` はIonicTeamの自作ライブラリですが、とても汎用性が高く便利です。
`lint-staged` はPrettierの範囲をGitでステージにあがったファイルに限定するために利用します（対象ファイル数が多くなるとフォーマットに時間がかかるため）。 `@kaizenplatform/prettier-config` はPrettierのフォーマットとして利用します（もっといいフォーマットがある場合コメントにて教えてください）

また、 `addCapPluginsToAppComponent` を変更して、 `addPrettierConfig` として、`prettier.config.js` 作成用のメソッドにします。hostのprojectRootに `prettier.config.js` があるかないかを判定して、ない場合追加する簡単なメソッドです。

```schematics/add/index.ts
function addPrettierConfig(projectRoot: string): Rule {
  return (host: Tree) => {
    const sourcePath = `${projectRoot}/prettier.config.js`;
    if (!host.exists(sourcePath)) {
      host.create(sourcePath, 'module.exports =require(\'@kaizenplatform/prettier-config\');')
    }
    return host;
  };
}
```

またPackage.jsonをアップデートして、タスクとlint-stagedを設定する必要があります。これはメソッドが存在しないので、自作しましょう。 `schematics/utils/package.ts` に追加します。

```schematics/utils/package.ts
/**
 * Adds a method to the package.json
 */
export function addMethodToPackageJson(host: Tree, scriptsName: string, method: string) {
  if (host.exists('package.json')) {
    const sourceText = host.read('package.json')!.toString('utf-8');
    const json = JSON.parse(sourceText);
    if (!json['scripts']) {
      json['scripts'] = {};
    }

    if (!json['scripts'][scriptsName]) {
      json['scripts'][scriptsName] = method;
    }

    host.overwrite('package.json', JSON.stringify(json, null, 2));
  }

  return host;
}

/**
 * Adds a method to the package.json
 */
export function addKeyToPackageJson(host: Tree, key: string, method: string | object) {
  if (host.exists('package.json')) {
    const sourceText = host.read('package.json')!.toString('utf-8');
    const json = JSON.parse(sourceText);

    if (!json[key]) {
      json[key] = method;
    }

    host.overwrite('package.json', JSON.stringify(json, null, 2));
  }

  return host;
}
```

似たようなメソッドですが、どちらもpackage.jsonへの追記のためのメソッドです。前者をオリジナルのscriptsの追加、後者をオリジナルのキーの追加に利用します。それでは、活用していきましょう。 `addFormatterToPackageJson()` に更に追加します。

```schematics/add/index.ts
addScriptsToPackageJson(
      host,
      'lint-staged',
      'lint-staged'
    );
    addScriptsToPackageJson(
      host,
      'formatter',
      'prettier --parser typescript --write \"./**/*.ts\" &&  prettier --parser html --write \"./**/*.html\"'
    );
    addKeyToPackageJson(
      host,
      'pre-commit',
      [
        'lint-staged',
      ]
    );
    addKeyToPackageJson(
      host,
      'lint-staged',
      {
        '*.ts': [
          'prettier --parser typescript --write',
          'git add',
        ],
        '*.html': [
          'prettier --parser html --write',
          'git add',
        ],
      }
    );
```

まだnpm installを実行していないので、最後にnpm installを行いましょう。 `spawn` を使って直接実行します。

```schematics/add/index.ts
function doNpmInstall(): Rule {
  return (host: Tree) => {
    return new Observable<Tree>(subscriber => {
      const child = spawn('npm', ['install'], { stdio: 'inherit' });
      child.on('error', error => {
        subscriber.error(error);
      });
      child.on('close', () => {
        subscriber.next(host);
        subscriber.complete();
      });
      return () => {
        child.kill();
        return host;
      };
    });
  };
}
```

実装できました。 `npm publish` で公開しましょう。これで、 `ng add @rdlabo/ng-add-formatter` が利用できるようになりました。簡単ですね。

# まとめ
Angularは、 `@schematics` や `@angular-devkit` などのパッケージを通して、開発者がAngularを土台にして自作ツールや開発の効率化を図りやすいように様々な支援をしています。
Angularのルールがちょっとしんどい、もしくはもっと業務を効率化したいという人は、ぜひ自作ツールづくりにチャレンジしてみてください！