【フロントエンド自動化】CLIでテンプレートを生成しよう

media thumbnail

こんにちは、株式会社Gizumoでエンジニアをしている田中です。
弊社の短期研修プログラムを経てエンジニアとなり、客先常駐を経験し現在は社内で自社サービスの立ち上げに関わっています。

今回はHygenというライブラリを使用してテンプレート化された構成(決まったディレクトリ名やファイル名での生成)をCLIで自動化する方法を紹介します。

弊社で開発中の自社サービスでは未導入のHygenですが、実際に使ってみて、導入に持っていけたらと画策中です。

前提・対象とする読者

この記事は以下を前提として読み進めてください。

開発環境

npmが実行可能であることと下記環境を想定しています。

  • Next.js 13.1.x
  • React.js 18.2.x
  • TypeScript 4.9.x
  • hygen 6.2.x
  • @emotion/react 11.1x.x

また、対象とする読者は以下のような方を想定しています。

  • 決まりきった構成(テンプレート)をCLIで自動化したい方
  • 自動化のためのライブラリHygenについて知りたい方

今回のゴール

今回の記事のゴールはCLIの命令によって、決まりきった「ディレクトリ構成、ファイル構成、ファイルの内容」を自動で生成することです。

具体的には以下のことが実現可能です。

  1. コマンド
npm run new:fc
  1. 対話に答えてディレクトリとファイルを自動生成
? コンポーネントをどのディレクトリに配置しますか? …
❯ atoms
  molecules
  organisms
  templates
  pages
? コンポーネント名を入力してください。 ex. MyButton · MyExample
? そのコンポーネントはスタイルを所持しますか? (y/N) · true

Loaded templates: .hygen
       added: components/atoms/MyExample/index.tsx
       added: components/atoms/MyExample/style.ts

自動化のメリット

自動化のメリットとしては以下を想定しています。

開発プロジェクトではファイル構成やファイルの内容がテンプレート化されていることが多いです。それらを手で毎回追加していくのを煩わしいと思った経験はないでしょうか?その煩わしさは自動化によって解決できます。

  • ディレクトリ・ファイル作成、テンプレート記述の工数削減
  • テンプレート構造に対する人的ミスの混在防止

上記は自動化により明確に解決できる部分なので解説は省略します。

テンプレートを作成する際の心理的ハードルの低減

Reactを例にとると、コンポーネント作成はまずディレクトリとファイルを作成し、テンプレート化されたファイルの内容を記述するところから始まります。

この工程は明確ですがやることが多く、他のファイルと表記揺れがないか気をつけながら進める必要があるため非常にめんどくさく気を遣う作業です。正直、コンポーネント作成の出だしは気が重く、モチベーションが低下します。

この作業を削減することでコンポーネント作成時の最初のハードルを無くし、実装者のモチベーションを高く保つことができます。

例:テンプレート化されたファイル構成

弊社自社プロダクトのフロントエンド開発で用いるAtomic Designの各ディレクトリには明確に責務を持たせて管理しています。そのため配下のファイル群は同じ構造になるように設計されています。

たとえば、Atomsコンポーネントは決まって以下の構成です。

src/components/atoms/MyButton
├── index.tsx
└── style.ts

Organismsコンポーネントだと以下のようになります。

src/components/organisms/MyExample
├── index.tsx
├── index.test.tsx
├── style.ts
├── hooks.ts
└── type.ts

例:テンプレート化されたファイル内容

また、ファイルの初期内容もテンプレート化されています。

たとえば、Atomsコンポーネントは大方以下の内容からスタートします。

// MyButton/index.tsx

import { memo } from 'react';

import { MyButtonStyle } from './style';

import type { MyButtonStylesType } from './style';
import type { FC } from 'react';

type MyButtonProps = {
  styles?: MyButtonStylesType;
};

export const MyButton: FC<MyButtonProps> = memo(
  ({ styles = {}, ...props }) => (
    <button
      {...props}
      css={(theme) => MyButtonStyle(theme, styles)}
    >
      {children}
    </button>
  )
);

MyButton.displayName = 'MyButton';
MyButton.defaultProps = {
  styles: {},
};
// MyButton/style.ts

import { css } from '@emotion/react';

import type { CSSInterpolation } from '@emotion/serialize/types';
import type { SerializedStyles, Theme } from '@emotion/react';

export type MyButtonStylesType = CSSInterpolation;

export const MyButtonStyle = (
  theme: Theme,
  styles: MyButtonStylesType
): SerializedStyles => css([
    {},
    styles,
  ]);

Hygenとは

hygenとはOSSで開発されているコードジェネレーターです。
設定ファイルを記述し、CLIの命令によってコードとディレクトリ・ファイルの自動生成を行なってくれます。

Hygenのインストール

brewnpmでインストールが可能です。
プロジェクト配下で使用する際は下記コマンドでインストールします。今回はnpmを使用します。

npm i -D hygen

自動化の実践

Hygenを使用して自動化を実践します。

今回はAtomic DesignOrganismsのテンプレートファイルとファイルの初期内容を作成する過程を見ていきましょう。

Hygenの初期設定

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

mkdir .hygen

※補足

npx hygen init selfでも初期ディレクトリとファイルが作成できますが、目的以外のファイルもたくさん生成されるのでコマンドは使わずスクラッチで作っていきます。

実行ファイル管理用のディレクトリ作成

Hygen実行は下記コマンドを想定しています。
Hygen実行の引数にディレクトリ名を渡すことでそのディレクトリ配下のスクリプトを実行します。
プロジェクトルートへ移動していない方はプロジェクトルートへ移動してから下記コマンドを実行してください。

npx hygen new fc

今回の場合、下記ディレクトリ構造で実践します。fcReactFunctionalComponentの略です。fcのディレクトリ名は目的に合わせて変更可能です。

.hygen
└── new
    └── fc
        ├── index.js
        ├── index.tsx.ejs.t
        └── style.ts.ejs.t

たとえば、npx hygen foo fcを実行したい場合下記ディレクトリを作成します。

.hygen
└── foo
    └── fc
        ├── index.js
        ├── index.tsx.ejs.t
        └── style.ts.ejs.t

ではプロジェクトルート.hygen/配下で実行ファイル管理用のディレクトリを作成しましょう。

mkdir new/fc

実行ファイルの作成

実行ファイルはindex.jsファイルを起点にして、自動生成したいファイルと1対1になるように.ejsファイルを作成します。

実行ファイル管理用のディレクトリ作成で作成した.hygen/new/fc/配下にファイルと記述をそれぞれ追加します。
.hygen/new/fc/配下に移動していない方は移動してから下記コマンドを実行してください。

touch index.js index.tsx.ejs.t
// .hygen/new/fc/index.js

module.exports = {
  prompt: ({ inquirer }) => {
    const questions = [
      {
        type: 'input',
        name: 'component_name',
        message: 'コンポーネント名を入力してください。 ex. MyButton',
      },
    ];

    return inquirer.prompt(questions).then((answers) => {
      const path = `components/${answers.component_name}`;
      return { ...answers, path };
    });
  },
};

.hygen/new/fc/index.jsファイルは、

  • questionsHygenコマンドを実行した際の質問を記述します。
  • 最後にreturnした変数を後述するindex.tsx.ejs.tファイルで使用します。
// index.tsx.ejs.t

---
to: <%= path %>/index.tsx
---

import type { FC } from 'react';

export const <%= component_name %>: FC = () => {
  return (
    <div><%= component_name %></div>
  )
};

index.tsx.ejs.tファイルは、

  • index.jsファイルから変数を受け取り使用することが可能です。
  • テンプレートエンジンのEJS記法を使用します。

ルートファイルの作成

どのディレクトリからでもHygenコマンドを実行できるようにプロジェクトルートにHygen用のルートファイルを作成します。
プロジェクトルートに移動していない方はプロジェクトルートに移動してから下記コマンドを実行してください。

touch .hygen.js
// .hygen.js

module.exports = {
  templates: `${__dirname}/.hygen`,
};

Hygenコマンド実行

自動生成の準備が整ったのでcomponentsが配置されている同階層で下記コマンドを実行します。

componentsが配置されている同階層

/src/componentsのようにsrc/配下にcomponentsディレクトリが配置されていればsrc/配下まで移動してください。

npx hygen new fc

コンポーネント名を聞かれるので任意の文字列を入力します。

> npx hygen new fc
? コンポーネント名を入力してください。 ex. MyButton · MyTest

Loaded templates: .hygen
       added: components/MyTest/index.tsx

components/MyTest/index.tsxが作成され、index.tsx.ejs.tファイルに記述した内容の反映を確認できれば成功です。

npm scriptsにコマンド追加

最後にpackage.jsonnpm scriptsにコマンドを追加しておきましょう。

npm scriptsにプロジェクトのコマンドとして実行内容を登録し、コマンドの詳細を隠蔽できます。コマンドの詳細を隠蔽することでプロジェクトメンバーがHygenを意識する必要がなく、コマンドの詳細が変更されても同じコマンドのまま実行が可能です。

// package.json

// ...省略

"scripts": {
  ...
  "new:fc": "hygen new fc", // 追加
  ...
}

// ...省略

Organismsのテンプレート作成

Organismsのテンプレートを作成します。構成は自動化が必要な背景で解説したものと同じになります。

Organismsはアプリケーション独自の知識を持つためビジネスロジックの配置や結合テストの対象となります。そのため配置するファイルが非常に多くなります。

src/components/organisms/MyExample
├── index.tsx
├── index.test.tsx
├── style.ts
├── hooks.ts
└── type.ts

実行ファイルの編集・作成

実行ファイルの編集

.hygen/new/fc/index.jsファイルを編集します。

// .hygen/new/fc/index.js

module.exports = {
  prompt: ({ inquirer }) => {
    const questions = [
      // 追加
      {
        type: 'select',
        name: 'category',
        message: 'コンポーネントをどのディレクトリに配置しますか?',
        choices: ['atoms', 'molecules', 'organisms', 'templates', 'pages'],
      },
      {
        type: 'input',
        name: 'component_name',
        message: 'コンポーネント名を入力してください。 ex. MyButton',
      },
      // 追加
      {
        type: 'confirm',
        name: 'have_style',
        message: 'そのコンポーネントはスタイルを所持しますか?',
      },
      // 追加
      {
        type: 'confirm',
        name: 'have_hooks',
        message: 'そのコンポーネントはhooksを所持しますか?',
      },
      // 追加
      {
        type: 'confirm',
        name: 'have_type',
        message: 'そのコンポーネントはtypeを所持しますか?',
      },
    ];

    return inquirer.prompt(questions).then((answers) => {
      const { category, component_name } = answers;
      // コンポーネントのパスを作成
      const path = `${category}/${component_name}`;
      const abs_path = `components/${path}`;
      // タグの生成
      const tag = args.tag ? args.tag : 'div';
      return { ...answers, path, abs_path, tag };
    });
  },
};

.hygen/new/fc/index.jsファイルは、

  • questionsに4点追加しています。
  • style, hooks, typeについては必要ない場合を考慮して選択型にしておきます。
実行ファイルの作成

.hygen/new/fc配下に新しくファイルを作成します。
.hygen/new/fc配下に移動していない方は.hygen/new/fc配下に移動してから下記コマンドを実行してください。

touch index.test.tsx.ejs.t style.ts.ejs.t hooks.ts.ejs.t type.ts.ejs.t

現時点で.hygenディレクトリは下記構成になっています。

.hygen
└── new
     └── fc
         ├── index.js
         ├── index.tsx.ejs.t
         ├── index.test.tsx.ejs.t
         ├── style.ts.ejs.t
         └── hooks.ts.ejs.t

作成したファイルに記述を追加します。

// .hygen/new/fc/style.ts.ejs.t

---
to: "<%= have_style ? `${abs_path}/style.ts` : null %>"
---
import { css } from '@emotion/react';

import type { SerializedStyles, Theme } from '@emotion/react';
import type { CSSInterpolation } from '@emotion/serialize/types';

export type <%= component_name %>StylesType = CSSInterpolation;

export const <%= component_name %>Style = (
  theme: Theme,
  styles: <%= component_name %>StylesType = {}
): SerializedStyles =>
  css([
    {},
    styles,
  ]);

.hygen/new/fc/style.ts.ejs.tファイルは、

  • have_styleの値に応じてファイルの生成を制御しています。
// .hygen/new/fc/hooks.ts.ejs.t

---
to: "<%= category === 'organisms' && have_hooks ? `${abs_path}/hooks.ts` : null %>"
---
import { useState } from 'react';

export const use<%= component_name %> = () => {
  const [myState, setMyState] = useState(undefined);
  return { myState };
};

.hygen/new/fc/hooks.ts.ejs.tファイルは、

  • organismsかつhave_hooksの値に応じてファイルの生成を制御しています。
  • ファイル内でエラーが生じないように適当なstateを定義しています。
// .hygen/new/fc/type.ts.ejs.t

---
to: "<%= category === 'organisms' && have_type ? `${abs_path}/type.ts` : null %>"
---
export type <%= component_name %> = {
  id: string;
};

.hygen/new/fc/type.ts.ejs.tファイルは、

  • organisms内で使用する型を記述します。
  • 型の内容は用途に応じて異なるためidのみ仮で定義しています。
// .hygen/new/fc/index.test.tsx.ejs.t

---
to: "<%= category === 'organisms' ? `${abs_path}/index.test.tsx` : null %>"
---
describe('<%= component_name %>', () => {
  test.todo('');
});

.hygen/new/fc/index.test.tsx.ejs.tファイルは、

  • organismsであればテストファイルを作成します。
  • describe単体ではlintエラーが出るためtest.todoを追加しておきます。
// .hygen/new/fc/index.tsx.ejs.t

---
to: <%= abs_path %>/index.tsx
---
import { memo } from 'react';

// hooksが必要な場合import
<% if (category === 'organisms' && have_hooks) { -%>
import { use<%= component_name %> } from './hooks';
<% } -%>
// styleが必要な場合import
<% if (have_style) { -%>
import { <%= component_name %>Style } from './style';

import type { <%= component_name %>StylesType } from './style';
<% } -%>
import type { FC } from 'react';

// styleが必要な場合型をPropsとして受け取る
<% if (have_style) { -%>
type <%= component_name %>Props = {
  styles?: <%= component_name %>StylesType;
};
<% } else { -%>
type <%= component_name %>Props = {};
<% } -%>

export const <%= component_name %>: FC<<%= component_name %>Props> = memo(
  ({ <% if (have_style) { -%>styles = {}, <% } -%>...props }) => {
  // hooksが必要な場合
  <% if(category === 'organisms' && have_hooks) { -%>
  const { myState } = use<%= component_name %>()
  <% } -%>
  return (
  <% if (have_style) { -%>
    <<%= tag %> css={(theme) => <%= component_name %>Style(theme, styles)} {...props}>
        <%= component_name %>
      </<%= tag %>>
  <% } else { -%>
    <<%= tag %> {...props}>
      <%= component_name %>
    </<%= tag %>>
  <% } -%>
  );
  }
);

<%= component_name %>.displayName = '<%= component_name %>';
<%= component_name %>.defaultProps = {
  styles: {},
};

.hygen/new/fc/index.tsx.ejs.tファイルは、

  • styleの有無、hooksの有無に応じてファイルの記述を変更します。

Hygenコマンドの実行

Organismsのテンプレートを自動生成するための準備が整ったため下記コマンドを実行します。
componentsと同階層に移動してから下記コマンドを実行してください。

npm run new:fc

下記問いにそれぞれ答えることでファイルとその内容が追加されます。

> npm run new:fc

> src@0.1.0 new:fc
> hygen new fc

✔ コンポーネントをどのディレクトリに配置しますか? · organisms
✔ コンポーネント名を入力してください。 ex. MyButton · MyExample
✔ そのコンポーネントはスタイルを所持しますか? (y/N) · true
✔ そのコンポーネントはhooksを所持しますか? (y/N) · true
✔ そのコンポーネントはtypeを所持しますか? (y/N) · true

Loaded templates: .hygen
       added: components/organisms/MyExample/hooks.ts
       added: components/organisms/MyExample/index.test.tsx
       added: components/organisms/MyExample/index.tsx
       added: components/organisms/MyExample/style.ts
       added: components/organisms/MyExample/type.ts

最後に

Hygenはプロジェクトには導入できておらず、まだ使い方も甘いですが、多くの課題を解決する素晴らしいツールだと実際に使ってみて感じることができました。

弊社Gizumoは研修生の方が社内プロジェクトに一時期参画するケースも少なからずあるため、そのような場合にも多くの価値を提供してくれそうだなと感じています。

この記事を読んでもしCLIによる自動化を良いなと感じてくれたら幸いです。
ここまで読んでいただきありがとうございました。

参考

少しでも開発にお困りの方は
相談しやすいスペシャリストにお問い合わせください

お問い合わせ
  1. breadcrumb-logo
  2. メディア
  3. 【フロントエンド自動化】CLIでテンプレ...