🤹

[React × Plop] CLI を使ってコンポーネントを一括作成

2024/08/21に公開

React での開発において、コンポーネントや Storybook やテストのファイルを 1 つ 1 つ手作業で作ると、たまにミスをするので歯痒く感じていました。タイポによるエラーや手動での調整は時間をロスしてしまうこともありますね。

そこで、CLI で一括で作ってしまいたいと考えました。

この記事では、Plop というライブラリを使うことで以下の GIf のようにコンポーネントを大量出力できるようにしています。

サンプルのリポジトリも用意しました。もしよければ試してみてください。

https://github.com/koji-koji/plop-app

Plop を使ってファイルを作成できるようにする

Plop はテキストファイルを生成してくれるライブラリです。

Plop では手順を踏むことにより、対話形式でコンポーネントファイルを作成できるようになります。

手順

  • plopfile.mjs 作成し、対話形式のプロンプトを定義
  • 雛形となる .hbs ファイルの定義
  • コマンドの実行

React を Vite で作成し、Storybook を導入済みの前提で進めます。

とりあえず Plop をインストールしましょう。

npm install --save-dev plop

plopfile.mjs を作成し、対話形式のプロンプトを定義

まずは Plop でコンポーネントファイルを作成するところまで進めます。

プロジェクトのルートディレクトリに plopfile.mjs を作成します。

export default function (plop) {
  // Plop ジェネレーターの定義。この設定により、CLI コマンドで `component` ジェネレーターが使用可能になる
  plop.setGenerator("component", {
    description: "React component generator",
    // 対話型プロンプトの定義
    prompts: [
      [
        {
          type: "input",
          name: "name",
          message: "Component name?",
        },
      ],
    ],
    // 入力内容に対するアクションの定義
    actions: function (data) {

      const actions = [
        {
          type: "add",
          // 出力先のパス設定。必要に応じて書き換えてください。
          path: "src/components/{{pascalCase name}}/{{pascalCase name}}.tsx",
          // 雛形となるファイルパス設定
          templateFile: "plop-templates/component/component.tsx.hbs",
        },
        {
          type: "add",
          path: "src/components/{{pascalCase name}}/{{pascalCase name}}.stories.tsx",
          templateFile: "plop-templates/component/component.stories.tsx.hbs",
        },
      ];

      return actions;
    },
  });
}

雛形となる .hbs ファイルの定義

plop-templates/component ディレクトリを作成し、component.tsx.hbs と component.stories.tsx.hbs を作成します。

component.tsx.hbs

import { memo } from 'react';

type Props = {
  className?: string;
};

const {{pascalCase name}} = ({ className = '' }: Props) => {
  return (
    <div className="{className}">{{pascalCase name}}</div>
  );
};

export default memo({{pascalCase name}});

component.stories.tsx.hbs

import type { Meta, StoryObj } from '@storybook/react';
import {{pascalCase name}} from './{{pascalCase name}}';

const meta: Meta<typeof {{pascalCase name}}> = {
  title: '{{pascalCase type}}/{{pascalCase name}}',
  component: {{pascalCase name}},
  tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof {{pascalCase name}}>;

export const Default: Story = {
  args: {
    // ここにデフォルトのプロップを追加
  },
};

.hbs 拡張子のコードは、 Prettier により予期しない形に自動整形されてしまう場合もあるかもしれません。その場合は、Prettier を無効化しましょう。

.prettierignore

# .prettierignore

# Plop.js templates
**/plop-templates/**/*.hbs

コマンドの実行

package.json にコマンドを登録してから、コマンドを実行をします。

npm run generate:component

対話形式でファイル名が求められるので入力します。

コンポーネントファイルと storybook ファイルが雛形通りに作成されました。

actions を変更して雛形ファイルを追加すれば、テストファイルも作成可能です。

コンポーネントが配置される場所を選択できるようにする

component ディレクトリ直下ではなく、分類したいケースが多そうですよね。
ここでは作りたいコンポーネントの種類に応じて配置される場所を選べるように調整します。

export default function (plop) {
  plop.setGenerator("component", {
    description: "React component generator",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "Component name?",
      },
      // 追加。コンポーネントのパスを選択させるプロンプト
      {
        type: "list",
        name: "type",
        message: "Choose the component path:",
        choices: [
          "common",
          "features",
          "layout"
        ]
      }
    ],
    actions: function (data) {
      const actions = [
        {
          type: "add",
          // 調整
          // 出力先のパス設定。プロンプトで選択されたパスを使用
          path: "src/components/{{type}}/{{pascalCase name}}.tsx",
          templateFile: "plop-templates/component/component.tsx.hbs",
        },
        {
          type: "add",
          // 調整
          // 出力先のパス設定。プロンプトで選択されたパスを使用
          path: "src/components/{{type}}/{{pascalCase name}}.stories.tsx",
          templateFile: "plop-templates/component/component.stories.tsx.hbs",
        },
      ];

      return actions;
    },
  });
}

これで、選択したパスの通りに配置されるようになります。

Plop オプションを設定することでプロンプトの入力を無視するようにする

複数のコンポーネントを作成したい場合は、わざわざ対話型プロンプトで 1 つ 1 つ作っていくのは効率が悪いです。一括で作りたいです。

そこで、コマンドのオプションを読み込み、オプションがある場合には対話型プロンプトが実行されないように調整します。

オプションの解析には minimist というライブラリを使います。

npm install minimist

plopfile.mjs を書き換えます。

import minimist from "minimist";

export default function (plop) {
  // 追加
  // コマンドライン引数を解析するために minimist を使用
  // process.argv はコマンドライン引数の配列で、最初の2つの要素は Node.js の実行バイナリとスクリプトのパスなので、slice(2) を使って実際の引数を取得する
  const args = minimist(process.argv.slice(2));

  plop.setGenerator("component", {
    description: "React component generator",
    prompts: [
      // 追加
      // コマンドライン引数に name が含まれていない場合のみ、プロンプトを表示する
      ...(!args.name
        ? [
            {
              type: "input",
              name: "name",
              message: "Component name?",
            },
          ]
        : []),
      ...(!args.type
        ? [
            {
              type: "list",
              name: "type",
              message: "Choose the component path:",
              choices: [
                "common",
                "features",
                "layout"
              ]
            }
          ]
        : []),
    ],
    actions: function (data) {
      // 追加
      data.name = data.name || args.name;
      data.type = data.type || args.type;

      const actions = [
        {
          type: "add",
          path: "src/components/{{type}}/{{pascalCase name}}.tsx",
          templateFile: "plop-templates/component/component.tsx.hbs",
        },
        {
          type: "add",
          path: "src/components/{{type}}/{{pascalCase name}}.stories.tsx",
          templateFile: "plop-templates/component/component.stories.tsx.hbs",
        },
      ];

      return actions;
    },
  });
}

オプションを読み込んで、name や path がある場合は無視をするように調整をしました。

今まで通り name をオプションと渡さなければ、対話型プロンプトは起動してくれます。

一方で、以下のコマンドように name を設定すれば対話型プロンプトが起動しないようになります。

// プロンプトが起動しない
npm run generate:component -- --name=sample_component

ここまでで調整は完了です。

あとは、コンポーネント設計をして名前を決め、実行するコマンドを作成していきます。

npm run generate:component -- --name=component_A_1 --type=layout
npm run generate:component -- --name=component_A_2 --type=layout
npm run generate:component -- --name=component_B_1 --type=common
npm run generate:component -- --name=component_B_2 --type=common
npm run generate:component -- --name=component_B_3 --type=common

コピーしてターミナルに流し込めば、一気にコンポーネントを作成することができます。

Discussion