⚙️

hygen で生成 - 対話形式の Component 雛形 -

2020/10/18に公開

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 との対話例

自分が作った対話例を紹介します。
https://github.com/takefumi-yoshii/hygen-sanbox

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