Plopを使ってReactコンポーネントの雛形を自動作成する
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 コンポーネントやテストファイルを自動生成する方法を紹介したいと思います。
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 からなるテンプレートの間にある接着剤のようなコードである。
このツールを使うことで、例えば 対話形式で 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 にスクリプトを追加
各自お好きなようにカスタマイズしてください。
generate
やgenerator
とする例が多そうです。
// 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: 入力に応じて実行されるアクション
これらを自身のお好みに合わせて設定していきます。(細かいメソッドや 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 のテンプレートはコンポーネントの名前が未入力の状態かつ、基本的な構造やプレースホルダーが設けられたファイルとして機能します。
詳細については以下のドキュメントなどを参考にしてください。
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 内のリファレンスを参照してください。
ひとつ面白い使い方として、type
に list
を指定して 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
を使用して直接記述したりします。
こちらも詳細はドキュメントに書いてありますので、適宜参照してください。
以下設定例です。
// 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 を使って自動化してみてください!
参考文献
おまけ
以下に自分がプロジェクトで実際に使用している 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
export * from './{{name}}';
import { FC } from 'react';
export const {{pascalCase name}}: FC = () => {
return (
<div>{{pascalCase name}}</div>
);
};
import { render, screen } from "@testing-library/react"
import { {{pascalCase name}} } from "."
test('renders {{pascalCase name}} component', () => {
render(<{{pascalCase name}} />)
})
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
// エントリーポイントのため何も記述しない
export * from './components';
-
React アプリケーションのアーキテクチャの一例として公開されている GitHub リポジトリ。 ↩︎
ポップアップストアや催事イベント向けの商業スペースを簡単に予約できる「SHOPCOUNTER」と商業施設向けリーシングDXシステム「SHOPCOUNTER Enterprise」を運営しています。エンジニア採用強化中ですので、興味ある方はお気軽にご連絡ください! counterworks.co.jp/
Discussion