🦔

plopで作るコンポーネント生成ワークフロー

2024/12/07に公開

この記事はポート株式会社 サービス開発部 Advent Calendar 2024 の7日目です。

はじめに

ポート株式会社でフロントエンドエンジニアをしているminamiです。
普段はキャリアパーク就活BOXなどのサービス開発を担当しています。

この記事では私が現在開発時に利用しているplopというパッケージを紹介します。
plopとは、一貫性を持ったファイルを簡単に作成できるツールになります。
設定ファイルであるplopfile.jsを調整することで、様々なカスタマイズが可能です。
https://plopjs.com/
https://www.npmjs.com/package/plop

前回の問題点

実は去年のアドカレでもplopについて書きました。基本的な作成手順はほぼ同じです。
https://zenn.dev/minamisauce/articles/b0d8d985470f63

これはこれで便利だったのですが、以下のような問題点がありました。

  • 全部自由入力なので手間がかかり面倒😕
  • コンポーネントを作りたいディレクトリが、既に存在するのかわからない
    • 既に存在するディレクトリに作りたい場合も、手入力する必要がある
    • 誤字があっても気づけないので、不要なディレクトリを作ってしまう😣

結論VSCodeのUI上で作った方が一目でわかって楽!となり、直近はほぼ使っていませんでした。
今回は上記の問題点を踏まえて、より使いやすいものを目指しました。

今回つくったもの

今回私が作成したplopの動作は以下のとおりです。

  • npm run generate-component叩く
  • featurescomponentsどちらに作成するか選択する
  • featuresを選択した場合、既存ディレクトリまたは新規作成するかを選択する
  • コンポーネント名を入力する
  • 入力内容に応じたディレクトリ階層に、指定のファイルが作成される

このように質問に答えていくだけで、コンポーネントファイルの雛形を作成することができます🎉
上記画像の動作例では、app/src/features/AdventCalendar/components/Notificationというディレクトリが作成され、その中にindex.tsx、index.stories.tsx、index.test.tsxの雛形ファイルが作成されます。

plopfile.jsの中身

では今回作成したplopfile.jsの中身を解説します。

prompts

promptsではユーザの入力データを収集します。

const fs = require('fs');
const path = require('path');
const featuresPath = path.resolve(__dirname, './app/src/features');

module.exports = function (
  /** @type {import('plop').NodePlopAPI} */
  plop,
) {
  plop.setGenerator('component', {
    description: 'Create a new component',
    prompts: [
      {
        type: "list",
        name: "kind",
        message: "どちらに作成しますか?",
        choices: ["features", "components"],
      },
      {
        type: 'list',
        name: 'path',
        message: '既存機能に追加 または 新規で作成しますか?',
        choices: () => {
          const directories = fs.readdirSync(featuresPath, { withFileTypes: true })
            .filter(dirent => dirent.isDirectory())
            .map(dirent => dirent.name);
          directories.push('新規で作成');

          return directories.map(dir => ({
            name: dir,
            value: dir
          }));
        },
        when: ({ kind }) => kind === 'features',
      },
      {
        type: 'input',
        name: 'newPath',
        message: '機能名は何にしますか? (ex: Signup, Mypage)',
        when: ({ path }) => path === '新規で作成',
      },
      {
        type: 'input',
        name: 'name',
        message: 'コンポーネント名は何にしますか? (ex: Button)',
      },
      {
        type: 'input',
        name: 'name',
        message: 'コンポーネント名は何にしますか? (ex: Button)',
      },
    ],

typeは形式、nameは変数名、messageは質問文が入ります。type内容に応じてその他のプロパティが指定できます。
1つ目の質問では、type: "list"とし、choices: ["features", "components"]を指定することで、ディレクトリを選択できるようにしました。

{
    type: "list",
    name: "kind",
    message: "どちらに作成しますか?",
    choices: ["features", "components"],
},

2つ目の質問ではchoicesの配列を現在のディレクトリから取得するようにしています。
fsモジュールのreaddirSyncを用いて、指定ディレクトリの一覧を取得し、
また、whenでこの質問が表示される条件を指定しています。今回の場合は、1つ目の質問でfeaturesと回答した時のみ表示したいので、変数名kindfeaturesのときを指定しています。

{
    type: 'list',
    name: 'path',
    message: '既存機能に追加 または 新規で作成しますか?',
    choices: () => {
        const directories = fs.readdirSync(featuresPath, { withFileTypes: true })
            .filter(dirent => dirent.isDirectory())
            .map(dirent => dirent.name);
        directories.push('新規で作成');

        return directories.map(dir => ({
            name: dir,
            value: dir
        }));
    },
    when: ({ kind }) => kind === 'features',
},

3つ目以降の質問はtype: 'input'を指定して、自由入力で値を受け取るようにしています。

{
    type: 'input',
    name: 'newPath',
    message: '機能名は何にしますか? (ex: Signup, Mypage)',
    when: ({ path }) => path === '新規で作成',
},

actions

actionsでは、promptsの内容を元にファイルを作成します。

actions: function (data) {
    var actions = [];

    if (data.kind === 'features') {
        if (data.path === '新規で作成') {
            actions.push({
                type: 'add',
                path: 'app/src/features/{{pascalCase newPath}}/components/{{pascalCase name}}/index.tsx',
                templateFile: 'plop-templates/features/newFeatures/component.tsx.hbs',
            });
        } else {
            actions.push({
                type: 'add',
                path: 'app/src/features/{{pascalCase path}}/components/{{pascalCase name}}/index.tsx',
                templateFile: 'plop-templates/features/component.tsx.hbs',
            });
        }
    } else {
        actions.push({
            type: 'add',
            path: 'app/src/components/{{pascalCase name}}/index.tsx',
            templateFile: 'plop-templates/components/component.tsx.hbs',
        });
    }
    return actions;
}

promptsで指定した変数を条件として利用できるので、1つ目の質問の回答に応じて分岐させています。
typeには形式、pathにはファイルを作成する場所、templateFileにはファイルを作成する際のテンプレートファイルを指定します。
※ここでは省略していますが、テンプレートファイルの書き方や、index.tsx以外のpath指定は前回の記事に記載してあります🙏

さいごに

いかがでしたでしょうか?
私は最近、新規機能の開発を行うことが多いため、plopを使うことでスムーズにコンポーネント生成ができ満足しています。
メンバーが多いプロジェクトでも、plopにコンポーネント作成時のルールを組み込むことで、一貫性のあるコンポーネントディレクトリを構築することができると考えています。
今回紹介した以外にもカスタマイズは色々できそうなので、改善を重ねてより便利にしていきたいと思います🥳

ポート株式会社 エンジニアブログ

Discussion