はじめに
こんにちは!
Webフロントエンドエンジニアの那谷(なたに)です。
みなさんはコンポーネントを作るときって最初のファイルはどうしていますか。
1から書いていますか?
それとも別コンポーネントをコピペして中身を微修正していますか?
私は後者のパターンでした。
ただ、細かいところまで修正しきれておらず、PRで「ボタン関連コンポーネントなのに〇〇Checkbox(コピー元コンポーネント)になっている!」と指定を受けたりしました。
人間は気をつけていてもミスをするもの。。。。
こういった仕事はツールで自動化したいですね!
解決したいこと
- 新規コンポーネントのファイル作成がめんどう
- 開発者ごとに書き方にゆらぎがある
scaffdogとは
Node.js製のコマンド1つでテンプレを生成するツールです。
設定ファイルもmarkdownから設定ができます。
scaffdogの導入方法
-
scaffdogをインストールします。
npm i -D scaffdog
-
package.jsonのscriptsに追加
package.json{ "scripts": { "scaffdog": "scaffdog" }, }
-
scaffdogのディレクトリ、初期テンプレートを生成
npm run scaffdog init
初期テンプレートのファイル名を聞かれます。
本記事では、初期テンプレート名をMyFirstTemplate
としています。 -
最後に動作確認をしましょう
npm run scaffdog generate
[入力した文字].md
が指定したディレクトリに生成されていれば導入は完了です!
/.scaffdogディレクトリについて
scaffdog init
で生成されたファイルについて説明します。
.scaffdog
├── MyFirstTemplate.md
└── config.js
config.js
テンプレートとして使用する*.md
ファイルのパスを定義しています。
他にもグローバル変数の定義、mustache記法を任意の記法に変更できます。
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でオリジナルテンプレートを作ってみる
実際に使いそうなファイルを用いてテンプレートを作ってみましょう!
サンプルコード
サンプルとして以下のようなディレクトリを作ってみます。
よくあるコンポーネントのあるある三銃士ですね!
└── [コンポーネント名]
├── index.tsx // コンポーネントファイル
├── index.stories.tsx // Storybookファイル
└── index.test.tsx // テストファイル
テンプレート化する前の中身は以下のようになっています。
手動でコピペすると、どこか変更し忘れそうですね。
index.tsx
import { FC } from "react";
export const SomeText: FC = () => {
return <div data-testid="text">Show some text...</div>;
};
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 },
};
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...");
});
});
1. テンプレートのmdファイル作成
.scaffdog
直下に任意の名前のmdファイルを作る。
デフォルトと異なり、下記を変更しています。
- テンプレート名
- 質問文、キー名の変更
- コードを別ファイルから読み込む(
{{ 'templates/ReactTemplate/*.tsx' | read }}
の部分)
---
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
直下にファイルを作成します。
import { FC } from "react";
export const {{ inputs.componentName }}: FC = () => {
return <div data-testid="text">Show some text...</div>;
};
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 },
};
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.