🥑

plopを使ってコンポーネントを簡単に作れるようにした

2023/12/27に公開

ポート株式会社 サービス開発部 Advent Calendar 2023 10日目の記事です。

ポート株式会社でフロントエンドエンジニアをしているmiuiimiです。
最近plopというnpmパッケージを導入したので紹介します。
https://www.npmjs.com/package/plop

何で入れたか

弊社サービスのフロントエンドではReactを用いています。そのため、新しくコンポーネントを作成する機会が度々あります。
ですが、フォルダの階層が複数あるためどこに作成すべきなのか、コンポーネント毎にフォルダ内のファイルが異なるため最低限必要なファイルが何なのかが分かりづらいことが問題でした。
今回plopを導入したことでこれらの問題が解決し、簡単にコンポーネントを作成できるようになりました。

完成したもの

plop導入後のコンポーネント作成は以下の手順です。

① コマンドを叩く

npm run create-component

② 質問に答える

? どのディレクトリに作成しますか? (ex: src/components/) src/components/TodoList
? コンポーネント名は何にしますか? (ex: Button) TodoList

③ 指定した場所にコンポーネントに必要なファイルが作成される🎉
コンポーネント本体のファイルに加え、テスト、storybook、スタイルシートのファイルも作成されるようにしています。

✔  ++ /src/components/TodoList/TodoList/index.tsx
✔  ++ /src/components/TodoList/TodoList/index.test.tsx
✔  ++ /src/components/TodoList/TodoList/index.stories.mdx
✔  ++ /src/components/TodoList/TodoList/index.module.scss

導入方法

実際にplopを導入する手順は以下の通りです。

① npm installする。
② package.jsonにスクリプト追記する
③ plopfile.jsを作成する
④ コマンド実行時に作成するファイルのテンプレートを準備する

npm installする

plopを使用したいルートにてinstallを行います。

npm install plop

package.jsonにスクリプトを追記する

package.jsonのscriptsにplopの実行コマンドを追記します。
名前は自由ですが、今回はcreate-componentとしました。

// ~~~
"scripts": {
    "create-component": "plop component",
// ~~~

plopfile.jsを作成する

ルート直下にplopfile.jsを作成し、以下をコピペします。

module.exports = function (
  /** @type {import('plop').NodePlopAPI} */
  plop,
) {
  plop.setGenerator('component', {
    description: 'Create a new component',
    prompts: [],
    actions: [],
  });
}

promptsには実行時に表示する質問、actionsには実行するアクションを設定します。
prompts、actionsそれぞれ追記していきます。

prompts

typeは種類、nameは変数名、messageは質問文を指定します。

下記の例では、typeにinput、nameにpathname、messageに質問文を指定しました。
コマンド実行時に入力した値がそれぞれ挿入され、各テンプレート内で変数として使用されるようになります。

prompts: [
      {
        type: 'input',
        name: 'path',
        message: 'どのディレクトリに作成しますか? (ex: src/components/)',
      },
      {
        type: 'input',
        name: 'name',
        message: 'コンポーネント名は何にしますか? (ex: Button)',
      },
    ],

actions

typeは種類、pathは作成したいファイルのパス、templateFileは参照するテンプレートファイルのパスを指定します。

下記の例では、typeにadd、pathにはpropmtsで用いた変数を使ってパスを指定、templateFileはこの後作成するテンプレートファイルのパスを指定しました。
前述の通り、今回はコンポーネントのファイルに加え、テスト、storybook、スタイルシートのファイルも作成されるようにしています。

actions: [
      {
        type: 'add',
        path: '{{path}}/{{pascalCase name}}/index.tsx',
        templateFile: 'plop-templates/component/component.tsx.hbs',
      },
      {
        type: 'add',
        path: '{{path}}/{{pascalCase name}}/index.test.tsx',
        templateFile: 'plop-templates/component/component.test.tsx.hbs',
      },
      {
        type: 'add',
        path: '{{path}}/{{pascalCase name}}/index.stories.mdx',
        templateFile: 'plop-templates/component/component.stories.mdx.hbs',
      },
      {
        type: 'add',
        path: '{{path}}/{{pascalCase name}}/index.module.scss',
        templateFile: 'plop-templates/component/component.module.scss.hbs',
      },
    ],

作成するファイルのテンプレートを準備する

actionsのtemplateFileで指定したパスに、テンプレートファイルを作成します。

actions同様、promptsで指定した変数名を使用することができます。
今回の場合、{{path}}{{name}}を使用可能です。

plop-templates/component/component.tsx.hbs

import React, { FC } from 'react';

import styles from './index.module.scss';

type Props = {
  title?: string;
};

export const {{pascalCase name}}: FC<Props> = ({ title = '{{pascalCase name}}' }) => {
  return (
    <div className={styles.modules}>
      <h1>Welcome!, {title} Component</h1>
    </div>
  );
};

plop-templates/component/component.test.tsx.hbs

import { render, screen } from "@testing-library/react";
import React from 'react';

import { {{pascalCase name}} } from ".";

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

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

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

component/component.module.scss.hbs

.modules {
  // Add your styles here
}

plop-templates/component/component.stories.mdx.hbs

import { Meta, Story, ArgsTable, Canvas } from '@storybook/addon-docs/blocks';
import { PCPreview, SPPreview, PCPreviewStory, PREVIEW_PC_CLASSNAME } from 'stories/utils/Preview';
import { {{pascalCase name}} } from '.';

<Meta
  id="components/{{pascalCase name}}"
  title="components/{{pascalCase name}}"
  component={ {{pascalCase name}} }/>

# {{pascalCase name}}

## 🕹 Playground

<Canvas className={PREVIEW_PC_CLASSNAME}>
  <Story name="playground">
    {({ ...props }) => (
      <PCPreviewStory>
        <{{pascalCase name}} {...props} />
      </PCPreviewStory>
    )}
  </Story>
</Canvas>

## 🦾 Props

<ArgsTable story="playground" />

## PC

<PCPreview>
  <{{pascalCase name}} />
</PCPreview>

## SP

<SPPreview>
  <{{pascalCase name}} />
</SPPreview>

🎉

おわり

今回、plopを導入したことでコンポーネント作成時のハードルを減らすことができました。
また、複数コンポーネントを作成するような時もファイル漏れが発生することが減らすことができ、また新しく参入したメンバーや、普段フロントを触らないメンバーもコンポーネントを簡単に作成することが可能になったかと思います。
今後も便利なパッケージがあったら積極的に取り入れていきたいと思います。

Discussion