scaffdogでテンプレファイルを自動生成する

## はじめに
こんにちは！
Webフロントエンドエンジニアの那谷（なたに）です。

みなさんはコンポーネントを作るときって最初のファイルはどうしていますか。
1から書いていますか？
それとも別コンポーネントをコピペして中身を微修正していますか？
私は後者のパターンでした。

ただ、細かいところまで修正しきれておらず、PRで「ボタン関連コンポーネントなのに〇〇Checkbox(コピー元コンポーネント)になっている！」と指定を受けたりしました。

人間は気をつけていてもミスをするもの。。。。
こういった仕事はツールで自動化したいですね！

## 解決したいこと
- 新規コンポーネントのファイル作成がめんどう
- 開発者ごとに書き方にゆらぎがある

## scaffdogとは

Node.js製のコマンド1つでテンプレを生成するツールです。
設定ファイルもmarkdownから設定ができます。

## scaffdogの導入方法
1. scaffdogをインストールします。
   ```sh
   npm i -D scaffdog
   ```
1. package.jsonのscriptsに追加
   ```json: package.json
   {
     "scripts": {
       "scaffdog": "scaffdog"
     },
   }
   ```
1. scaffdogのディレクトリ、初期テンプレートを生成
   ```sh
   npm run scaffdog init
   ```
   初期テンプレートのファイル名を聞かれます。
   本記事では、初期テンプレート名を`MyFirstTemplate`としています。

1. 最後に動作確認をしましょう
   ```sh
   npm run scaffdog generate
   ```
   `[入力した文字].md`が指定したディレクトリに生成されていれば導入は完了です！

## /.scaffdogディレクトリについて
`scaffdog init`で生成されたファイルについて説明します。

```
.scaffdog
  ├── MyFirstTemplate.md
  └── config.js
```
#### config.js
テンプレートとして使用する`*.md`ファイルのパスを定義しています。
他にもグローバル変数の定義、mustache記法を任意の記法に変更できます。

参考：[scaffdog-config](https://scaff.dog/docs/config)

#### MyFirstTemplate.md

~~~md:MyFirstTemplate.md
---
name: "MyFirstTemplate"   ← テンプレート名
root: "."                 ← 生成するテンプレートのrootディレクトリ
output: "**/*"            ← rootを基準に候補として表示するディレクトリ
ignore: []                ← 候補から無視するディレクトリ
questions:                ← CLIの質問。回答結果を変数{{inputs.[key名]}}の形で利用する
  value: "Please enter any text."
---

# `{{ inputs.value }}.md`  ← 生成するファイル名(変数でなくてもOK)

```markdown                ← 生成するファイルの中身
Let's make a document!
See scaffdog documentation for details.
https://scaff.dog/docs/templates
```
~~~

参考：[scaffdog-Attributes](https://scaff.dog/docs/templates/attributes)

## scaffdogでオリジナルテンプレートを作ってみる
実際に使いそうなファイルを用いてテンプレートを作ってみましょう！

### サンプルコード
サンプルとして以下のようなディレクトリを作ってみます。
よくあるコンポーネントのあるある三銃士ですね！
```
└── [コンポーネント名]
    ├── index.tsx             // コンポーネントファイル
    ├── index.stories.tsx     // Storybookファイル
    └── index.test.tsx        // テストファイル
```

テンプレート化する前の中身は以下のようになっています。
手動でコピペすると、どこか変更し忘れそうですね。

<details>
<summary>index.tsx</summary>

```tsx:index.tsx
import { FC } from "react";

export const SomeText: FC = () => {
  return <div data-testid="text">Show some text...</div>;
};

```
</details>

<details>
<summary>index.stories.tsx</summary>

```tsx:index.stories.tsx
import { ComponentStoryObj, ComponentMeta } from "@storybook/react";
import { SomeText } from ".";
type StoryObj = ComponentStoryObj<typeof SomeText>;
type ComponentProps = Required<typeof SomeText.defaultProps>;

export default {
  title: "SomeText",
  component: SomeText,
} as ComponentMeta<typeof SomeText>;

const args: ComponentProps = {};

export const Basic: StoryObj = {
  args: { ...args },
};

```
</details>

<details>
<summary>index.test.tsx</summary>

```tsx:index.test.tsx
import { composeStories } from "@storybook/testing-react";
import "@testing-library/jest-dm";
import { render, screen } from "@testing-library/react";

import * as stories from "./index.stories";

const { Basic } = composeStories(stories);

describe("コンポーネント", () => {
  it("div内に適切な文字があること", () => {
    render(<Basic />);
    expect(screen.getByTestId("text")).toHaveTextContent("Show some text...");
  });
});

```
</details>

### 1. テンプレートのmdファイル作成
`.scaffdog`直下に任意の名前のmdファイルを作る。

デフォルトと異なり、下記を変更しています。
- テンプレート名
- 質問文、キー名の変更
- コードを別ファイルから読み込む(`{{ 'templates/ReactTemplate/*.tsx' | read }}`の部分)

~~~md:.scaffdog/ReactTemplate.md
---
name: "ReactTemplate"
root: "."
output: "**/*"
ignore: []
questions:
  componentName: "What is component name?"
---

# `{{inputs.componentName}}/index.tsx`

```
{{ 'templates/ReactTemplate/index.tsx' | read }}
```

# `{{inputs.componentName}}/index.stories.tsx`

```
{{ 'templates/ReactTemplate/index.stories.tsx' | read }}
```

# `{{inputs.componentName}}/index.test.tsx`

```
{{ 'templates/ReactTemplate/index.test.tsx' | read }}
```
~~~

### 2. 変数の埋め込み
質問に応じて可変にする項目に気をつけながら、`.scaffdog/templates/ReactTemplates`直下にファイルを作成します。

```tsx:.scaffdog/templates/ReactTemplate/index.tsx
import { FC } from "react";

export const {{ inputs.componentName }}: FC = () => {
  return <div data-testid="text">Show some text...</div>;
};
```

```tsx:.scaffdog/templates/ReactTemplate/index.stories.tsx
import { ComponentStoryObj, ComponentMeta } from "@storybook/react";
import { {{ inputs.componentName }} } from ".";
type StoryObj = ComponentStoryObj<typeof {{ inputs.componentName }}>;
type ComponentProps = Required<typeof {{ inputs.componentName }}.defaultProps>;

export default {
  title: "{{ inputs.componentName }}",
  component: {{ inputs.componentName }},
} as ComponentMeta<typeof {{ inputs.componentName }}>;

const args: ComponentProps = {};

export const Basic: StoryObj = {
  args: { ...args },
};
```

```tsx:.scaffdog/templates/ReactTemplate/index.test.tsx
import { composeStories } from "@storybook/testing-react";
import "@testing-library/jest-dm";
import { render, screen } from "@testing-library/react";

import * as stories from "./index.stories";

const { Basic } = composeStories(stories);

describe("コンポーネント", () => {
  it("div内に適切な文字があること", () => {
    render(<Basic />);
    expect(screen.getByTestId("text")).toHaveTextContent("Show some text...");
  });
});
```

### 3. 実行
できあがったら再度`npm run scaffdog generate`で生成してみましょう！
任意のディレクトリにファイルが3つ生成されていれば完成です！

## まとめ
コマンド1つで複数ファイルが生成でき、かなり効率良く作業を進められるようになりました。
プロジェクトの細かいルールを取り入れながらカスタマイズができそうです。

かなり読みやすい公式サイトもあり、困ったことがあればここだけで解決できそうですね！
参考：[scaffdog - Markdown driven scaffolding tool.](https://scaff.dog/)