💧

Plopを使ってReactコンポーネントの雛形を自動作成する

2023/09/30に公開

React や Next.js で新規開発を進めていると、 「新しい機能を作るたびに何度も同じ構成のファイル群を作成するの面倒だなぁ」 と思うことは多いと思います。

例えば自分が携わっているプロジェクトでは、以下のように bullet-proof-react[1] を参考にしたディレクトリ構成を採用しています。しかし毎回毎回 /src/features/{機能名}/components/ からディレクトリを切って、 コンポーネントファイルを作成するのは少し面倒です。
(特にエントリーポイントとして毎度index.tsを作るのが結構だるい...)

/src/features/todo 
 ├─ api/
 ├─ components/
+│  ├─ TodoCreateModal/
+│  │   ├─ TodoCreateModal.tsx
+│  │   ├─ TodoCreateModal.stories.tsx
+│  │   ├─ TodoCreateModal.test.tsx
+│  │   └─ index.ts
 │  ├─ TodoItem/
 │  ├─ ...
 │  └─ index.ts
 ├─ hooks/
 └─ index.ts 

このような定型的なボイラープレート?はできる限りなくしたいのがエンジニアの性でしょう。

そこで今回は Plop という JavaScript コードジェネレーター作成ツールを用いて、事前定義したテンプレートファイルから React コンポーネントやテストファイルを自動生成する方法を紹介したいと思います。

https://github.com/plopjs/plop

Plop とは?

まず Plop について簡単に説明しておきましょう。

先程も少し触れましたが、Plop とはコードジェネレーター作成のためのツールです。公式では、"micro-generator framework." と呼ばれていたりします。

実態としては、inquire.jsからなるプロンプト部分とテンプレートエンジン Handlebars からなるテンプレートをもとに新しいファイルを作成するツールとなっています。

If you boil plop down to its core, it is basically glue code between inquirer prompts and handlebar templates.

(拙訳)
plop の核心を煮詰めれば、基本的には inquire.js のプロンプトと handlebar からなるテンプレートの間にある接着剤のようなコードである。

https://plopjs.com/documentation/#getting-started

このツールを使うことで、例えば 対話形式で React コードを自動生成するジェネレーターなどを簡単に作成することができます。

対話例

ここまでダラダラと説明してきましたが、言葉だけでは伝わらない部分も多いと思いますので、コードジェネレーターがどのように動作するのか?見ていきましょう。

ここでは自分が作成したジェネレーターをもとにお話しします。
(一応最後に今回使用したジェネレーターのサンプルコードを置いておきますので、細かい挙動など確認したい方はそちらを参照してください)

まず scripts に plop コマンドを登録した状態で以下のコマンドを叩きます。

$ yarn plop

すると

1. 「ジェネレーターの種類を選んでください」

? [PLOP] Please choose a generator. (Use arrow keys)
❯ feature - Create a new feature
  component - Create a new component

ここではcomponentを選んだとします。

2. 「features/〇〇/components/...の ○○ の path を教えてください」

以下のように features の中にあるディレクトリ一覧が出てきますので、どこのディレクトリにコンポーネントを作成したいか選びます。

? src/features/{path please}/components/... (Use arrow keys)
❯ accounts
  buildings
  calendar
  todo
(Move up and down to reveal more choices)

ここでは todo を選択したとします。

3. 「コンポーネントの名前を入力してください」

? component name please

最後に、作成したいコンポーネントの名前を聞かれます。

ここでは TodoCreateModal を入力したとします。

すると、以下のようなコンポーネントファイル群が生成されます。

? component name please TodoCreateModal
✔  ++ /src/features/todo/components/TodoCreateModal/index.ts
✔  ++ /src/features/todo/components/TodoCreateModal/TodoCreateModal.tsx
✔  ++ /src/features/todo/components/TodoCreateModal/TodoCreateModal.test.tsx
✔  _+ /src/features/todo/components/index.ts

振り返ると、たしかに対話内容に応じてファイルが自動で生成されていることがわかります。

? [PLOP] Please choose a generator. component - Create a new component
? src/features/{path please}/components/... todo
? component name please TodoCreateModal
✔  ++ /src/features/todo/components/TodoCreateModal/index.ts
✔  ++ /src/features/todo/components/TodoCreateModal/TodoCreateModal.tsx
✔  ++ /src/features/todo/components/TodoCreateModal/TodoCreateModal.test.tsx
✔  _+ /src/features/todo/components/index.ts

これにより新しいコンポーネントを手作業でセットアップするという面倒で退屈な作業から解放され、時間と労力を節約することができます。

さらにこれらのジェネレーターは他の開発者とも共有が可能なため、プロジェクト内におけるコード規約や設計を普及させることにもつながります。

まさに開発者にとっては魔法のようなツールですね ♪

セットアップ

次に Plop の導入手順について説明していきます。

1. Plop をプロジェクト内にインストール

$ npm i -D plop
$ yarn add -D plop
$ pnpm add -D plop

2. package.json にスクリプトを追加

各自お好きなようにカスタマイズしてください。

generategeneratorとする例が多そうです。

// package.json

"scripts": {
  "generate": "plop",
},

3. ルート直下に plopfile を作成する

// src/plopfile.mjs

export default function (
  /** @type {import('plop').NodePlopAPI} */
  plop
) {
  // ここではfeatureというジェネレーターを作成する
  plop.setGenerator("component", {
    description: ..., // 説明文
    prompts: [
      // プロンプト
    ],
    actions: [
      // 実行するアクション
    ],
  });
}

このファイルにはジェネレーターの設定を記述していきます。

以下ドキュメントにも書かれていますが、ジェネレーターの config オブジェクトには次の 3 つのプロパティが存在しています。

  • description:このジェネレータがどんなことを行うかの説明
  • prompts:ユーザーへの質問
  • actions: 入力に応じて実行されるアクション

https://plopjs.com/documentation/#setgenerator

これらを自身のお好みに合わせて設定していきます。(細かいメソッドや API の説明は後述します。)

4. テンプレートとなる雛形ファイルを作成

// src/plop-templates/component/Component.tsx.hbs

import { FC } from 'react';

export const {{pascalCase name}}: FC = () => {
  return (
    <div>{{pascalCase name}}</div>
  );
};

最後にテンプレートファイルを作成していきます。

Plop では JavaScript テンプレートエンジンである Handlebars を利用して雛形ファイルを作成します。(filename.tsx.hbs のような拡張子を用いる)

Handlebars のテンプレートはコンポーネントの名前が未入力の状態かつ、基本的な構造やプレースホルダーが設けられたファイルとして機能します。

詳細については以下のドキュメントなどを参考にしてください。

https://handlebarsjs.com/

propfile やテンプレートの細かい設定例

以上が Plop の導入手順です。

最後に plopfile やテンプレートファイルの具体的な設定方法を少し説明しておきます。

Propfile API

とりあえず setGenerator を使っておけばよいです。
一応以下のように handlebars で用いるヘルパーメソッドに対応するものをカスタマイズできたりしますが、あまり使うことはなさそう。

メソッド 説明
setGenerator generator をセットアップする
setHelper handlebars のヘルパーメソッドをセットアップする
setPartial handlebars のパーシャルをセットアップする
setActionType カスタムのアクションタイプを登録する
setPrompt カスタムのプロンプトを登録する
load 他の propfile や npm module からジェネレーターやヘルパー、パーシャルを読み込む
// src/plopfile.mjs

export default function (
...
) {
  plop.setGenerator("component", {})
}

description

description は単純にジェネレーターコマンドの説明を記述するだけですので、お好みに合わせて設定してください。

一般的には以下のような感じで記述することが多そう。

// src/plopfile.mjs

export default function (
...
) {
  plop.setGenerator("component", {
    description: "Generate a new React component",
    ...
  })

}

prompt

次にプロンプトを設定していきます。
プロンプト配列の各オブジェクトのプロパティとしては

  • type:プロンプトの種類
  • name:入力させた内容を actions 内や template で参照するための変数名
  • message:実際に表示するメッセージ

が主に利用されます。各 type の詳細については、以下の inquire.js 内のリファレンスを参照してください。

https://github.com/SBoudrias/Inquirer.js/tree/master#prompts


ひとつ面白い使い方として、typelist を指定して choices にその選択肢のリストを提供すると、以下のような選択式の prompt を作成できます。

? src/features/{path please}/components/... (Use arrow keys)
❯ accounts
  buildings
  calendar
  todo
(Move up and down to reveal more choices)
// src/plopfile.mjs

const fs = require('fs');

// ここで選択肢のリストを取得する
const features = fs
  .readdirSync('src/features')
  .map((it) => ({ name: it, value: it }));

module.exports = function (
  ...
) {
  plop.setGenerator('component', {
    description: "Generate a new React component",
    prompts: [
      {
        type: 'list',
        name: 'name',
        message: 'feature name please',
        choices: features,
      },
    ],
    ...

ここでは features 関数によって、features ディレクトリ配下の feature 名のリストを取得しています。

actions

各 prompt の入力内容に応じてどのようなアクションを実行するのか?記述していきます。

アクションは実行するコマンドの配列やその配列を返す関数を記述します。特にこだわりがないならば、prompts と同じようにコマンドの配列でよいでしょう。

各アクションオブジェクトのプロパティとしては主に以下のようなものが存在しています。

  • type:このアクションが実行する操作の種類。新しいファイルを追加するaddや既存のファイルに変更を加えるappendなどがあります。
  • path:作成または変更を加えるファイルの場所
  • templateFile:ファイルの作成・変更に利用される handlebars テンプレートへのパス。別々のファイルとして分離する必要ないような短いテンプレートの場合はtemplateを使用して直接記述したりします。

こちらも詳細はドキュメントに書いてありますので、適宜参照してください。

https://plopjs.com/documentation/#built-in-actions

以下設定例です。

// src/plopfile.mjs

export default function (
  /** @type {import('plop').NodePlopAPI} */
  plop
) {
  // create your generators here
  plop.setGenerator("feature", {
    description: "Generate a new feature",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "feature name please",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/features/{{name}}/index.ts",
        templateFile: "plop-templates/feature/index.ts.hbs",
      },
      {
        type: "add",
        path: "src/features/{{name}}/components/index.ts",
        templateFile: "plop-templates/feature/component/index.ts.hbs",
      },
    ],
  });
}

上記は、feature 配下の機能ディレクトリを新規作成する際のジェネレータです。

作成したい機能名を入力すると、機能ディレクトリとそのコンポーネントディレクトリに index.ts というエントリーポイントを作成します。
(本当は hooks や api ディレクトリなども作成できるようにするべきですが、手を抜いています 😇)

? [PLOP] Please choose a generator. feature - Create a new feature
? feature name please posts
✔  ++ /src/features/posts/index.ts
✔  ++ /src/features/posts/components/index.ts
/src/features/posts
+├─ components/
+│  ├─ ...
+│  ├─ ...
+│  └─ index.ts
+└─ index.ts 

押さえておくべきポイントしては、ファイルへのパスを定義する際には prompts で指定した name プロパティにアクセスすることが可能ということです。

こうすることで、ユーザーが プロンプトに入力した内容を読み取ることが可能になります。
さらに Handlebars の構文を用いることも可能で、以下のように入力内容をパスカルケースに変換することもできます。

actions: [
  {
    type: 'add',
    path: 'src/features/{{parentPath}}/components/{{pascalCase name}}/{{pascalCase name}}.tsx',
    templateFile: 'plop-templates/component/Component.tsx.hbs',
  },
  ...
],

Handlebars を用いたテンプレートファイルの作成

最後にファイル生成と変更に用いられるテンプレートファイルについてです。

このテンプレートファイル内でも、actions と同様に prompts で指定した name プロパティにアクセスすることが可能です。

また拡張子としては、.hbsを使用します。

具体的なコードの書き方などについてそこまで説明することもありませんので、割愛させていただきます。

// src/plop-templates/component/Component.tsx.hbs

import { FC } from 'react';

export const {{pascalCase name}}: FC = () => {
  return (
    <div>{{pascalCase name}}</div>
  );
};

最後に

今回はじめてこのようなジェネレーター系のツールを使ってみましたが、便利すぎて感動しました。

同じ構成のコンポーネントファイル群を手動で作成することに飽き飽きしている人は、ぜひ Plop を使って自動化してみてください!

参考文献

https://zenn.dev/shwld/articles/c26345ed8e781b

https://claritydev.net/blog/react-plop-js-code-generators

https://blog.wadackel.me/2022/scaffdog-v2/

https://zenn.dev/www_y/articles/58d6bb1da8a723#plop

おまけ

以下に自分がプロジェクトで実際に使用している plopfile と テンプレートを記載しておきます。

興味ある方は参考にしてください。

/src/plopfile.mjs
import { readdirSync } from "fs";

const features = readdirSync("src/features").map((it) => ({
  name: it,
  value: it,
}));

export default function (
  /** @type {import('plop').NodePlopAPI} */
  plop
) {
  // create your generators here
  plop.setGenerator("feature", {
    description: "Create a new feature",
    prompts: [
      // 入力させたい値につけたnameをactionsやtemplate内で参照できます
      {
        type: "input",
        name: "name",
        message: "feature name please",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/features/{{name}}/index.ts",
        templateFile: "plop-templates/feature/index.ts.hbs",
      },
      {
        type: "add",
        path: "src/features/{{name}}/components/index.ts",
        templateFile: "plop-templates/feature/component/index.ts.hbs",
      },
    ],
  });
  plop.setGenerator("component", {
    description: "Create a new component",
    prompts: [
      // 入力させたい値につけたnameをactionsやtemplate内で参照できます
      {
        type: "list",
        name: "parentPath",
        message: "src/features/{path please}/components/...",
        choices: features,
      },
      {
        type: "input",
        name: "name",
        message: "component name please",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/features/{{parentPath}}/components/{{pascalCase name}}/index.ts",
        templateFile: "plop-templates/component/index.ts.hbs",
      },
      {
        type: "add",
        path: "src/features/{{parentPath}}/components/{{pascalCase name}}/{{pascalCase name}}.tsx",
        templateFile: "plop-templates/component/Component.tsx.hbs",
      },
      {
        type: "add",
        path: "src/features/{{parentPath}}/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx",
        templateFile: "plop-templates/component/Component.test.tsx.hbs",
      },
      {
        type: "add",
        path: "src/features/{{parentPath}}/components/{{pascalCase name}}/{{pascalCase name}}.stories.tsx",
        templateFile: "plop-templates/component/Component.stories.tsx.hbs",
      },
      {
        type: "append",
        path: "src/features/{{parentPath}}/components/index.ts",
        template: `export { {{pascalCase name}} } from './{{pascalCase name}}';`,
      },
    ],
  });
}
/src/plop-templates/component
index.tsx.hbs
export * from './{{name}}';
/component/Component.tsx.hbs
import { FC } from 'react';

export const {{pascalCase name}}: FC = () => {
  return (
    <div>{{pascalCase name}}</div>
  );
};
Component.test.tsx.hbs
import { render, screen } from "@testing-library/react"
import { {{pascalCase name}} } from "."

test('renders {{pascalCase name}} component', () => {
  render(<{{pascalCase name}} />)


})
Component.stories.tsx.hbs
import type { Meta, StoryObj } from "@storybook/react"
import { {{pascalCase name}} } from '.'

type T = typeof {{pascalCase name}}

export default {
  component: {{pascalCase name}},
} satisfies Meta<T>

type Story = StoryObj<T>

export const Default: Story = {
  args: {},
}
/src/plop-templates/feature
component/index.tsx.hbs
// エントリーポイントのため何も記述しない
index.tsx.hbs
export * from './components';
脚注
  1. React アプリケーションのアーキテクチャの一例として公開されている GitHub リポジトリ。 ↩︎

COUNTERWORKS テックブログ

Discussion