【フロントエンド自動化】CLIでテンプレートを生成しよう
こんにちは、株式会社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
の命令によって、決まりきった「ディレクトリ構成、ファイル構成、ファイルの内容」を自動で生成することです。
具体的には以下のことが実現可能です。
- コマンド
npm run new:fc
- 対話に答えてディレクトリとファイルを自動生成
? コンポーネントをどのディレクトリに配置しますか? …
❯ 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
のインストール
brew
やnpm
でインストールが可能です。
プロジェクト配下で使用する際は下記コマンドでインストールします。今回はnpm
を使用します。
npm i -D hygen
自動化の実践
Hygen
を使用して自動化を実践します。
今回はAtomic Design
のOrganisms
のテンプレートファイルとファイルの初期内容を作成する過程を見ていきましょう。
Hygen
の初期設定
プロジェクトルートに.hygen
ディレクトリを作成します。
mkdir .hygen
※補足
npx hygen init self
でも初期ディレクトリとファイルが作成できますが、目的以外のファイルもたくさん生成されるのでコマンドは使わずスクラッチで作っていきます。
実行ファイル管理用のディレクトリ作成
Hygen
実行は下記コマンドを想定しています。Hygen
実行の引数にディレクトリ名を渡すことでそのディレクトリ配下のスクリプトを実行します。
プロジェクトルートへ移動していない方はプロジェクトルートへ移動してから下記コマンドを実行してください。
npx hygen new fc
今回の場合、下記ディレクトリ構造で実践します。fc
はReact
のFunctionalComponent
の略です。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
ファイルは、
questions
にHygen
コマンドを実行した際の質問を記述します。- 最後に
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.json
のnpm 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
による自動化を良いなと感じてくれたら幸いです。
ここまで読んでいただきありがとうございました。