Help us understand the problem. What is going on with this article?

@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-devkitschematics を提供しており、 @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-initng-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の場合は appangular.json で設定されている値です)

そして、その projectTypeapplication 以外の場合は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のルールがちょっとしんどい、もしくはもっと業務を効率化したいという人は、ぜひ自作ツールづくりにチャレンジしてみてください!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away