hygen で生成 - 対話形式の Component 雛形 -
Component 作成にあたり、storybook や test も一度にコミットする場面が増えてきました。そして CSS Modules や、特定 Component 専用の custom hooks など、一つの Component を構成するファイル群はそれなりの量になってきます。例えば、以下の様な module 構成の Component です。これを手作業で作成するとなると、少し億劫になりますよね。
└── atoms
└── Button
├── Button.stories.tsx
├── Button.test.tsx
├── Button.tsx
├── dependencies.ts
├── index.tsx
└── style.module.css
作成時にButton
という名称だけ決めてしまい、CLI から雛形出力できれば、作業効率向上が見込めます。(storybook/test も書くぞ…!という習慣も生まれそうです)最近、これにまつわる呟きをしたところ、フォロワーさんから Node.js製の template generator ツール hygen を教えていただきました。(公式:https://www.hygen.io)
hygen との対話例
自分が作った対話例を紹介します。
npm scripts に new:fc
という script を登録しています。実行すると…
$ npm run new:fc
1.「Atomic Design のカテゴリはどれですか?」
? Which Atomic Design category? …
❯ atoms
molecules
organisms
templates
2.「Component の名前は何ですか?」
? What is the name of component? · Button
3.「どこのディレクトリですか?」
? Where is the directory? (No problem in blank) › buttons
4.「style はありますか?」
? Is it have style? (y/N) · true
5.「props はありますか?」
? Is it have props? (y/N) · true
6.「hooks はありますか?」
? Is it have hooks? (y/N) › true
この対話を経ると、対話内容に応じてファイルが自動生成されます。
✔ Which Atomic Design category? · atoms
✔ What is the name of component? · Button
✔ Where is the directory? (No problem in blank) · buttons
✔ Is it have style? (y/N) · true
✔ Is it have props? (y/N) · true
✔ Is it have hooks? (y/N) · true
Loaded templates: .hygen
added: src/components/atoms/buttons/Button/Button.stories.tsx
added: src/components/atoms/buttons/Button/Button.test.tsx
added: src/components/atoms/buttons/Button/Button.tsx
added: src/components/atoms/buttons/Button/dependencies.ts
added: src/components/atoms/buttons/Button/index.tsx
added: src/components/atoms/buttons/Button/style.module.css
✨ Done in 7.58s.
ファイル名称だけでなく、出力内容も、対話から得た変数が適用されていることが確認できます。手作業で面倒だった部分が自動化されるので、嬉しいですね。
【src/components/atoms/buttons/Button/Button.stories.tsx】
import React from "react";
import { storiesOf } from "@storybook/react";
import { Button } from "./";
// ______________________________________________________
//
storiesOf("atoms/buttons/Button", module)
.add("default", () => <Button />);
自作できるところ
フルスタックFWの「あの部分だけ」を良い感じに切り出している hygen。用意する ejs の template は完全に自作ができます。上記のButton.stories.tsx
は以下の様な template から生成されました。
---
to: <%= abs_path %>/<%= component_name %>.stories.tsx
---
import React from "react";
import { storiesOf } from "@storybook/react";
import { <%= component_name %> } from "./";
// ______________________________________________________
//
storiesOf("<%= path %>", module)
.add("default", () => <<%= component_name %> />);
対話を設ける例は少し進んだ利用方法になりますが、設問内容を設ける場合も、この様な question object を自由に組み替え、prompt.js
から配列で export するだけです。
{
type: 'select', // 入力の種類 [input/confirm/select]
name: 'category', // process 上で扱われる変数名
message: 'Which Atomic Design category?', // 対話メッセージ
choices: ['atoms', 'molecules', 'organisms', 'templates'] // 値
}
出力先も、template 単位で振り分けが出来るため、現状のワークフローに合わせたかたちで、作業自動化を導入することができます。上記の内容であれば、同じ内容を__stories__
と__tests__
に出しわけることが可能ということです。
added: __stories__/components/atoms/buttons/Button/Button.stories.tsx
added: __tests__/components/atoms/buttons/Button/Button.test.tsx
added: src/components/atoms/buttons/Button/Button.tsx
added: src/components/atoms/buttons/Button/index.tsx
added: src/components/atoms/buttons/Button/style.module.css
対話で得られた変数をもとに出力判断する、Conditional Rendering もあり「style sheet は要らないよ」と答えた場合はファイル出力と import句が削除されます。
added: __tests__/components/atoms/buttons/Button/Button.test.tsx
added: src/components/atoms/buttons/Button/Button.tsx
added: src/components/atoms/buttons/Button/index.tsx
今回は Component 雛形を生成していますが、Redux / Express / RN の例が公式で紹介されている様に、定型化作業であれば対象は何でも構いません。「既存ファイルの一部分に、テンプレートを追記する」といった利用法もあるため、定型化作業にとても向いたツールといえます。
サンプルで工夫したところ
hygen は出来ることが多い割に、公式ドキュメントがあまり充実していません。見つけた issue を元に prompt の設定を行いました。
参考:https://github.com/jondot/hygen/issues/35
作り込んでくると、入力内容や CLI 引数に応じて、高度な変数算出を行いたくなってきます。しかし generator ディレクトリに配置する prompt.js
ファイルでは、出来ることが限られます。以下の様に、該当ディレクトリindex.js
に定義を書くことで、今回紹介した様な対話内容から変数算出を行うことができました。
prompt
関数引数のinquirer.prompt
に質問配列を食わせれば、answers
引数を得ることができ、戻り値が ejs templates に変数として与えられます。
【.hygen/new/fc/index.js】
module.exports = {
prompt: ({ inquirer, args }) => {
const questions = [
{
type: 'select',
name: 'category',
message: 'Which Atomic Design category?',
choices: ['atoms', 'molecules', 'organisms', 'templates']
},
{
type: 'input',
name: 'component_name',
message: 'What is the name of component?'
},
{
type: 'input',
name: 'dir',
message: 'Where is the directory? (No problem in blank)',
},
{
type: 'confirm',
name: 'have_style',
message: 'Is it have style?',
},
{
type: 'confirm',
name: 'have_props',
message: 'Is it have props?',
},
{
type: 'confirm',
name: 'have_hooks',
message: 'Is it have hooks?',
},
]
return inquirer
.prompt(questions)
.then(answers => {
const { category, component_name, dir, have_props } = answers
const path = `${category}/${ dir ? `${dir}/` : `` }${component_name}`
const abs_path = `src/components/${path}`
const type_annotate = have_props ? "React.FC<Props>" : 'React.FC'
const props = have_props ? '(props)' : '()'
const tag = args.tag ? args.tag : 'div'
return { ...answers, path, abs_path, type_annotate, props, tag }
})
}
}
Component 雛形だけでなく、いろいろな場面で活用できそうです。
Discussion