コードジェネレーター入門 コンポーネントの自動生成ツールplopを使用したハンズオン (Next.js app router Storybook TailwindCSS Jestの複数ファイルを同時に作る)

# 今回のリポジトリ

masakinihirota/plop

https://github.com/masakinihirota/plop


インストール

```
gh repo clone masakinihirota/plop
npm i
npm i -g plop
plop
npm run storybook

```



# 環境&tool

Windows10
VSCode
GitHub Copilot
GitHub Copilot chat

Next.js
Storybook
TailwindCSS
Jest
plop


# Plopのマニュアルについて

正直読みにくいです。あのマニュアルは開発者視点で書かれていて、リファレンスのような感じです。
ユーザーが利用するという視点で書かれていないので、ユーザーがどういう使い方をするのか、どのような機能を使えばいいのかがよくわかりませんでした。

# テンプレートをまとめて一箇所に出力するという考え方

Next.js を使用して Storybook を開発する場合、コンポーネントコロケーションパターンを採用して開発します。

ページをコンポーンというブロックに分け、Storybook を使いコンポーネント単位で管理します。

各ファイルはコンポーネント毎に１つのフォルダに入れて管理をします。
コンポーネントファイル
ストーリーファイル
テストファイル

※ツールで typescript をコンポーネント単位でプロテクトをするツール
※vitest の In-source testing で コンポーネントファイルとテストファイルを同じファイル内に書く。

plop は、コンポーネントを追加するたびにテンプレートファイルを使用して生成します。



# ハンズオンの事前準備インストール

リポジトリを使用せずに０からインストールする。

すでにアプリを作成している場合はそちらを利用してください。
plop はアプリ本体に直接影響を与えません、テンプレートファイルを追加していくだけです、たとえ既存のファイルと同じ文字列を入力しても上書きになる前に停止します。

Next.js
Storybook
TailwindCSS
Jest
が使えるようにインストールしておきます。

Next.js のインストール
npx create-next-app@latest

Storybook のインストール
npx storybook@latest init

Jest のインストール
npm install --save-dev jest
npm install --save-dev @types/jest
npm install --save-dev @testing-library/jest-dom

testing-library のインストール
npm install --save-dev @testing-library/react

これで事前準備は終わりました、これから plop に関することだけに集中します。



## plop のインストール

グローバルにインストールする方法
npm install -g plop

※今回はグローバルに plop をインストールしていきます。
それも
src/components の直下に全てを自動生成するようにテンプレートを書いていきます。

理由は、プロジェクト毎に設定をする必要がなくなること、
コマンドが短くなること。の 2 つです。

コマンドも plop で動かすことが出来ます。



## 設定ファイルの作成

touch plopfile.mjs

plopfile.mjs に template ファイルの設定をします。

templateFile: "templates/component/component.tsx.hbs"

package.json ファイル がある場所を root として、
ターミナルでその下のパスに移動してても、root を基準にしてコンポーネントを自動生成してくれます。



# plop の守るべきルール

root は package.json がある場所が基準になります。
package.json と同じ位置に plopfile.mjs を置きます。

plopfile.mjs にルールを書き、
templates フォルダにテンプレートファイルを置きます。



# plop ハンズオン

## 目的

plop ツールを使い、テンプレートを利用してコンポーネントファイルを自動生成します。

場所は
src/components/\*
に出力するようにします。

Storybook も
src/components/\*
以下のみ見るように設定します。



## 本編

### globals.css の移動

globals.css を TailwindCSS の設定だけにします。

```src\app\globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

```

↑ このファイル
/src/app/globals.css
を
/src/styles/globals.css
に移動させます。

これは自分がここにおいたほうが管理しやすいと思ったためです。



### stories フォルダを移動

stories フォルダを
src\components\stories
に移動します。

※Storybook 関連のファイルをすべて src\components\の下に置き、この一箇所で Stories ファイルを管理するためです、不要なら消しても大丈夫です。



### Storybook の設定

Storybook の設定を components のみに限定します。

このように設定します。

`stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.@(js|jsx|ts|tsx)"],`

```.storybook\main.ts
import type { StorybookConfig } from "@storybook/nextjs"
const config: StorybookConfig = {
  stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
}
export default config

```



Storybook でも TailwindCSS が使えるようにします。

`import "../src/styles/globals.css"`
を ↓ のファイルに追加します。

```.storybook\preview.ts
import type { Preview } from "@storybook/react"
import "../src/styles/globals.css"

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
}

export default preview

```



## plop の設定

plop は全体として 2種類のファイルが必要となります。
設定ファイル（＝ plopfile.mjs）とテンプレートファイルの 2種類です。



### 設定ファイルの作成

touch plopfile.mjs

```plopfile.mjs
/* eslint-disable import/no-anonymous-default-export */
export default function (plop) {
  // controller generator
  plop.setGenerator("controller", {
    description: "application controller logic",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "controller name please",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/{{name}}.js",
        templateFile: "plop-templates/controller.hbs",
      },
    ],
  });
}


```

# 2個以上のジェネレーターの書き方

↑のジェネレーターの setGenerator() をコピーしてその下側に貼り付け、↓名前だけ変えました。

```plopfile.mjs
/* eslint-disable import/no-anonymous-default-export */
export default function (plop) {
  // setGenerator 1個目
  // controller generator
  plop.setGenerator("controller", {
    description: "application controller logic",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "controller name please",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/{{name}}.js",
        templateFile: "plop-templates/controller.hbs",
      },
    ],
  });

  // setGenerator 2個目 名前を変えただけです。↓
  plop.setGenerator("controller2", {
    description: "application controller logic",
    prompts: [
      {
        type: "input",
        name: "name",
        message: "controller name please",
      },
    ],
    actions: [
      {
        type: "add",
        path: "src/{{name}}.js",
        templateFile: "plop-templates/controller.hbs",
      },
    ],
  });
}

```

↑名前部分に相当する controller を controller2 に変更しました。
これだけで2個目のジェネレーターが出来ます。

↓実際に実行してみると、このように2種類選択出来るようになりました。

```terminal
> plop
? [PLOP] Please choose a generator. (Use arrow keys)
❯ controller - application controller logic
  controller2 - application controller logic

```

※名前を変えないと、同じジェネレーターとして扱われます。選択肢は出ずに、１個の時のジェネレーターと同じように実行されます。



# plop テンプレートファイルの作成

テンプレートファイルの置く場所

mkdir templates\component

このフォルダの中にテンプレートファイルを置きます。



## コンポーネントのテンプレートファイルを作成

touch templates\component\component.tsx.hbs

```templates\component\component.tsx.hbs
import { FC } from 'react';

type Props = {};

export const {{pascalCase name}}: FC<Props> = (props) => {
  return (
    <div className="">
      <h1>{{pascalCase name}} Component</h1>
    </div>
  );
};

```



## テストファイルのテンプレートファイルを作成

touch templates\component\component.test.tsx.hbs

```templates\component\component.test.tsx.hbs
import { render, screen } from "@testing-library/react";
import { {{pascalCase name}} } from ".";

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

  const titleElement = screen.getByRole( "heading", { level: 1, name: /{{pascalCase name}} Component/i });

  expect(titleElement).toBeInTheDocument();
});

```



## Storybookのテンプレートファイルを作成

touch templates\component\component.stories.tsx.hbs

```templates\component\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>;

export const Default: StoryObj<T> = {};

```



## plop の動作確認

plop をグローバルインストールした場合

plop

このようにフォルダ名やコンポーネント名を省略してもインタラクティブに質問してくれます。
※質問に答えながらテンプレートを自動生成することが出来ます。

1つのコマンドでフォルダ名とファイル名をまとめて指定することも出来ます。

plop フォルダ名 コンポーネント名



### 実際の使用例

↓ 完成するとこのように出力されます。
例

```
06-19 23:10:51> plop aaa/bbb
? コンポーネントの名前を入力してください ccc
✔  ++ \src\components\aaa\bbb\Ccc\index.tsx
✔  ++ \src\components\aaa\bbb\Ccc\Ccc.test.tsx
✔  ++ \src\components\aaa\bbb\Ccc\Ccc.stories.tsx



```

```
06-19 23:11:02> plop eee/fff jjj
✔  ++ \src\components\eee\fff\Jjj\index.tsx
✔  ++ \src\components\eee\fff\Jjj\Jjj.test.tsx
✔  ++ \src\components\eee\fff\Jjj\Jjj.stories.tsx



```

※フォルダ名とファイル名を CLI から 1 行で入力することが出来ます。
aaa/bbb、eee/fff が指定フォルダ名
jjj が指定コンポーネント名

コンポーネント名を指名しないとインタラクティブに
「コンポーネントの名前を入力してください」と聞いてきます。

※自動生成されたフォルダやファイルを削除する場合
フォルダごとまとめて削除するだけで大丈夫です。

npm run storybook
Storybook を起動して動作を確認します。

あとは適当にコンポーネントをインポートして使ってみます。

↓ こんな感じです。

![plop001.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44761/da89fe4e-0b58-bc56-ce0d-4a76d0db7b33.png)

テンプレートだけなので Storybook の中身はゼロです。
右上に警告が出ています。

確認が終了したら以上でハンズオンは終了です。



# テンプレートの書き方

templates\component\[ファイル名].tsx.hbs

テンプレートファイルは
.hbs という拡張子を使います。

パス
{{path}}

パスカルケース
{{pascalCase name}}
CurrentUserItem のように変換されます。
要素語( current user item )の最初を大文字で書き始めます。

ケバブケース
{{ kebabCase name}}
current-user-item のように変換されます。
ハイフン で要素語( current user item )を連結します。



# What's plop？

plop は、コードジェネレーターの 1 つで、テンプレートファイルを使用してコンポーネントやファイルを自動生成することができます。

plop を使用することで、開発者は手動でコンポーネントやファイルを作成する手間を省くことができます。

これは典型的な、後で楽をするために導入に苦労するタイプのツールです。
AI 時代になって GitHub Copilot があれば不要かもって思っています。
※GitHub Copilot と GitHub Copilot chat は便利ですので布教したいですね。

hygen よりも plop が選ばれるのは
ESM（import/export）が使えるから？？？
この理由がよくわからない。
hygen を使ったことがない、比較はできないので評価はしない。



## VSCode の拡張機能

File Templates - Visual Studio Marketplace
https://marketplace.visualstudio.com/items?itemName=SamKirkland.plop-templates

この VSCode の拡張機能は、右クリックから plop のテンプレートを使ってコンポーネントを自動生成できるようになります。今のところマウスからでも自動生成できるようになるだけです。

この拡張機能はデフォルトで、グローバルにインストールされた plop を使用することを想定しています。

npm install -g plop

ローカルの plop を使用する場合は、package.json の「scripts」レコードに「add-form-template」：「plop」を追加します。

```
"scripts": {
	"add-from-template": "plop"
}

```



# 拡張子.hrb ってなに？

.hbs は JavaScript テンプレートエンジン Handlebars で使われていた拡張子です。



## Handlebars とは？

Handlebars とはいわゆるテンプレートエンジンで、
JavaScript の値を参照して HTML を生成します。

公式
Handlebars
https://handlebarsjs.com/
Handlebars と plop は直接関係なさそうです、
過去の遺産（＝拡張子）をそのまま使っている感じですかね。



# その他

AI 開発とは
ChatGPT や GitHub Copilot chat を利用して開発する手法

コンポーネントコロケーションパターン - Qiita
https://qiita.com/masakinihirota/items/27f961dfa6871aad0550
コロケーションパターンとも呼ばれている。
アトミックデザインとは水と油。

コンポーネントコロケーションパターンは一つにまとめておく手法。
再利用しにくいが、一箇所にまとまってファイルを管理するのでメンテナンス性は高い、小規模開発向け。
Next.js 13 の Sever Action の発表によりコンポーネント単位で fetch（データ取得）が出来るようになり、コンポーネントの凝縮性は高まっていくと思われる。

アトミックデザインはパーツごとに切り分けて管理する手法。
再利用しやすい、どこのファイルがあるかすぐに分からずメンテナンス性は低い、大規模開発向け。



# 参考サイト

## 関連 URL

Visual Studio Code - Code Editing. Redefined
https://code.visualstudio.com/

Next.js by Vercel - The React Framework
https://nextjs.org/

Storybook: Frontend workshop for UI development
https://storybook.js.org/

Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.
https://tailwindcss.com/

Jest · 🃏 Delightful JavaScript Testing
https://jestjs.io/ja/

Consistency Made Simple : PLOP
https://plopjs.com/



## その他

mymactive/my-code-generator-templates: Templates of code generator
https://github.com/mymactive/my-code-generator-templates

【PLOP CLI】新しいファイルをテンプレートから生成する CLI
https://zenn.dev/anozon/articles/gatsby-plop-newpost

Features | Guide | Vitest
https://vitest.dev/guide/features.html#in-source-testing
In-source testing
コードとテストを同居させる機能。
AI 開発と相性が良いと思われる。

PLOP で Component ファイルを雛形から生成しよう
https://zenn.dev/shwld/articles/c26345ed8e781b



# 記事を書き終わって（駄文）

不要になったので最下部に移動しました。



# この記事を読む前に 2023 年 6 月 20 日現在

テンプレートからコードを自動生成するのははるか昔から星の数ほどありますが、React のコンポーネント、Storybook のストーリーファイル、Jest テストファイル、そして VSCode に対応したものだと、
hygen
plop
の 2 択だと思っていたのですが、
現在の選択肢は 3 つ目 ↓ があるようです、そのことをちょうどこの記事を書き終えた後知りました。



## scaffdog

scaffdog の VS Code 拡張機能をリリースした - wadackel.me

https://blog.wadackel.me/2023/scaffdog-vscode/

scaffdog - Visual Studio Marketplace

https://marketplace.visualstudio.com/items?itemName=scaffdog.scaffdog-vscode

この拡張機能を使うと、より便利だというのです。
さてどうなんでしょう？



## その後・・・ちょっと調べてみて

公式や下記の記事を読んでみて、似たようなことをやっていると、現在の自分の使い方はコンポーネントの場所で 3 つのファイルを作ることなのでツールを変更するほどではないかなと。scaffdog の VSCode の拡張機能の便利さはどこでも GUI で操作できることなのだろうか？
チームで運用したりしなければ必要ないのかなぁと。

scaffdog でテンプレファイルを自動生成する - Qiita

https://qiita.com/y-natani/items/b8034b2d7c7fbafe63bf




<details><summary>メモ plop</summary>

npm install -g plop

```plopfile.js
export default function (plop) {
    // create your generators here
    plop.setGenerator('basics', {
        description: 'this is a skeleton plopfile',
        prompts: [], // array of inquirer prompts
        actions: []  // array of actions
    });
};

```



----------------------------------------

Prompt types

[]はオプション
その他は必須

List - {type: 'list'}
Raw List - {type: 'rawlist'}
Expand - {type: 'expand'}
Checkbox - {type: 'checkbox'}
Confirm - {type: 'confirm'}
Input - {type: 'input'}
Input - {type: 'number'}
Password - {type: 'password'}
Editor - {type: 'editor'}


List - {type: 'list'}
選択肢の中から選ぶ
type
name
message
choices
[default, filter, loop]



Raw List - {type: 'rawlist'}
選択肢の中から選ぶ
type
name
message
choices
[default, filter, loop]


Expand - {type: 'expand'}
type
name
message
choices
[default]



Checkbox - {type: 'checkbox'}
選択する
type
name
message
choices
[filter, validate, default, loop]

{checked:true}とマークされた選択肢はデフォルトでチェックされます。






Confirm - {type: 'confirm'}
確認する
type
name
message
[default, transformer]



Input - {type: 'input'}
入力する
type
name
message
[default, filter, validate, transformer]



Input - {type: 'number'}
type
name
message
[default, filter, validate, transformer]


Password - {type: 'password'}
隠して入力
type
name
message
mask
[default, filter, validate]


Editor - {type: 'editor'}
type
name
message
[default, filter, validate, postfix, waitUserInput]







----------------------------------------

パラメーターを指定しない
plop
複数ある場合、選択できるジェネレータリストが表示されます。


パラメーターを指定する
plop [パラメーター]
ジェネレータを直接実行できます。


プロンプトの回避
plop component "some component name"
	componentというジェネレータを起動して、
	1つ目にコンポーネント名を入力を促されるジェネレータの場合
	このように1行に書いておくことで
	さらに、コンポーネント名を指定していることになります。
	もし2個目の入力が必要だった場合、
	プロンプトで入力を促されます。

プロンプトの回避とは促される前に入力してしまおうということ。


最初のプロンプトはスキップして、
2番目のプロンプトは入力したい場合は
↓アンダースコアを利用します。

plop component _ "input for second prompt"

↑これで1番目の入力はスキップされます。






----------------------------------------

plopはできる限り入力を解析しようとする

例えば
Confirm - {type: 'confirm'}
を使用する時

y
yes
t
true
を入力するとbool値をtureとして返されます。



value
index
key
name
値、インデックス、キー、または名前
を使用してリストから項目を選択できます。



チェックボックスプロンプトでは、
複数を選択するためにカンマで区切られた値のリストを
使用できます。



----------------------------------------

プロンプトをバイパスする
	スキップする？？？

バイパスの例
## Bypassing both prompt 1 and 2
$ plop component "my component" react
$ plop component -- --name "my component" --type react

## Bypassing only prompt 2 (will be prompted for name)
$ plop component _ react
$ plop component -- --type react




----------------------------------------

ジェネレータを強制実行する

通常は上書き禁止ですが
--force
をつけることでジェネレータ実行時に上書きされます。



----------------------------------------

plopfile API

plopfile APIはplop オブジェクトによって公開されるメソッドのコレクションです

TypeScript を使えます。

```plopfile.ts
import {NodePlopAPI} from 'plop';

export default function (plop: NodePlopAPI) {
  // plop generator code
};

```



```plopfile.js
export default function (
    /** @type {import('plop').NodePlopAPI} */
    plop
) {
    // plop generator code
};

```





----------------------------------------

setGenerator
	ジェネレーターを作ります。



Interface GeneratorConfig

必須
prompts
actions

オプション
description

※動的にすることも可能です。



Interface PlopGenerator

runPrompts
ジェネレーター内でプロンプトを実行する関数

runActions
ジェネレーター内でアクションを実行する関数

※このインターフェイスには、GeneratorConfig の
すべてのプロパティも含まれています。



----------------------------------------

Interface ActionConfig

type	実行する
force	強制（デフォルト false）
data			このアクションを実行するときにユーザーのプロンプト回答と
				混合する必要があるデータを指定します
abortOnFail		アクションが失敗した場合
skip				アクションを実行するかどうかを指定するオプションの関数



type
	add
	modify
	addMany
	etc



----------------------------------------

Other Methods

getHelper
getHelperList
getPartial
getPartialList
getActionType
getActionTypeList
setWelcomeMessage
getGenerator
getGeneratorList
setPlopfilePath
getPlopfilePath
getDestBasePath
setDefaultInclude
getDefaultInclude
renderString





----------------------------------------

Built-In Actions



----------------------------------------





</details>