[React]コンポーネントを作るときに他からコピーしてくる作業を自動化するCLIツールを作る

# TL;DR

Reactでコンポーネントを新しく作るとき、Storybookやテストも作成するのであれば、ディレクトリを作成してから、
`index.ts`, `Component.tsx` `Component.stories.tsx` `Component.spec.tsx`
と4ファイルくらい作成することになると思いますが、
毎回これらを手動で作成して、他のコンポーネントからimport文とかをコピーしてくるのは面倒なので、CLIツールを作成して、コマンド一発で出来るようにします。

コードは[こちら](https://github.com/tachibanayu24/react-ts-atomicdesign-boilerplate/blob/main/tools/create-component.js)

# 作ったもの

## ディレクトリ構成

AtomicDesign想定です。

```
.
├── package.json
├── src
│   └── components
│       └── atoms
│           └── Button
│               ├── Button.spec.tsx
│               ├── Button.stories.tsx
│               ├── Button.tsx
│               └── index.ts
└── tools
    ├── componentTemplates
    │   ├── Component.tsx
    │   ├── Spec.tsx
    │   ├── Story.tsx
    │   └── index.ts
    └── createComponent.js
```

## コード

### テンプレート作成

まず、環境に合わせて `./tools/componentTemplates/` 以下に、作成したいファイルのテンプレートを配置します。
このテンプレートをリネームして、必要な場所にコピーする感じです。

テンプレートは[こちら](https://github.com/tachibanayu24/react-ts-atomicdesign-boilerplate/tree/main/tools/componentTemplates)を参照してください。

### script作成

コマンドライン引数 オプション `-C` で、コンポーネントの粒度とコンポーネント名を受け取ります。

以下はコマンドのhelpです。

```
Usage: create-component [options]

Options:
  -C, --component [dir/Component]  The name of the component to be created (ex) atoms/Button
  -h, --help                       display help for command
```

* 引数が不正である場合はエラー(atoms, molecules, organismsしか許容しない)
* すでにコンポーネントが存在しているときはエラー(上書きしない)
* ディレクトリが存在しないとき(`/molecules/` など未作成の場合)は新規作成

します。

コードは以下です。

```js
const { program } = require("commander");
const fs = require("fs");

program.option(
  "-C, --component [component name]",
  "The name of the component to be created (ex) atoms/Button"
);

program.parse(process.argv);

if (!program.component) {
  program.help();
}

const argument = program.component.split("/");
const dir = argument[0];
const component = argument[1];

if (!["atoms", "molecules", "organisms"].includes(dir)) {
  console.error(
    "ERROR: Only 'atoms', 'molecules', and 'organisms' can be specified."
  );
  return;
}

// If the directory is not yet created, create it.
if (!fs.existsSync(`./src/components/${dir}`)) {
  fs.mkdirSync(`./src/components/${dir}`);
}

const templates = [
  "./tools/componentTemplates/index.ts",
  "./tools/componentTemplates/Component.tsx",
  "./tools/componentTemplates/Story.tsx",
  "./tools/componentTemplates/Spec.tsx",
];

const dests = [
  `./src/components/${dir}/${component}/index.ts`,
  `./src/components/${dir}/${component}/${component}.tsx`,
  `./src/components/${dir}/${component}/${component}.stories.tsx`,
  `./src/components/${dir}/${component}/${component}.spec.tsx`,
];

fs.mkdirSync(`./src/components/${dir}/${component}`);

// Error when a component already exists
templates.forEach((template, index) => {
  fs.copyFileSync(
    template,
    dests[index],
    fs.constants.COPYFILE_EXCL,
    (error) => {
      if (error) {
        throw error;
      }
    }
  );
  console.log(`✨ Create component template ${dests[index]}`);
});
```

### package.jsonに追記

`package.json`のscriptsに、

```json
    "create-component": "node ./tools/createComponent.js"
```

を追記して完了です。

## 使用例

```sh
$ yarn create-component -C atoms/Icon

✨ Create component ./src/components/atoms/Icon/index.ts
✨ Create component ./src/components/atoms/Icon/Icon.tsx
✨ Create component ./src/components/atoms/Icon/Icon.stories.tsx
✨ Create component ./src/components/atoms/Icon/Icon.spec.tsx
```

これでめんどくさい作業から開放されました！