Vueを用いた開発プロジェクト用にカスタムジェネレーターを作ってみる

# はじめに

こんにちは、[モチベーションクラウド](https://www.motivation-cloud.com)の開発にフリーのエンジニアとして参画している@HayatoKamonoです。

この記事は、「[モチベーションクラウド Advent Calendar 2018](https://qiita.com/advent-calendar/2018/mcs)」2日目の記事となります。

# 先に成果物のイメージ
```
$ yo mcs

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │    Welcome to MCS App    │
   `---------´   │        Generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What do you want to generate? (Use arrow keys)
❯ Vue Component
  Vuex Store Module
? What's the component name? SampleComponent
   create SampleComponent/SampleComponent.index.js
   create SampleComponent/SampleComponent.vue
   create SampleComponent/SampleComponent.stories.js
   create SampleComponent/SampleComponent.specs.js
```
↑ この記事では、このようなものを作っていきます。

# 概要

モチベーションクラウドの開発チームでは2018年10月から改善期間と称して、開発に関するガイドラインやルール作りをはじめとする、様々な改善活動に取り組んでいます。

私が所属するフロントエンド開発チームでも、改善活動の一環として複数のガイドラインを作成しているのですが、その作成したガイドラインの１つに、コンポーネントを配置するディレクトリ構成・ファイル構成に関するガイドラインがあります。

```
.
├── atoms
├── molecules
├── organisms
│   └── SampleComponent
│       ├── index.js
│       ├── SampleComponent.specs.js
│       ├── SampleComponent.stories.js
│       └── SampleComponent.vue
├── templates
├── pages
└── decorators
```

簡単に説明をすると、`components`ディレクトリ配下のディレクトリを、[Atomic Design](http://bradfrost.com/blog/post/atomic-web-design/)の`atoms`、`molecules`、`organisms`、`templates`、`pages`で切り、そして、開発するコンポーネント毎にそのコンポーネント名でディレクトリを切るというディレクトリ構成で行くという方針で固まりました。

また、コンポーネント単位のディレクトリ配下には、「index.js」、単一ファイルコンポーネントの「ComponentName.vue」、Storybook用の「ComponentName.stories.js」、テスト用の「ComponentName.specs.js」を作成するというファイル構成で行くことに決まりました。

しかしながら、コンポーネント単位のディレクトリ以下に、毎回、４つのファイルを手動で決まった命名規則に合わせて作成するのは何気に面倒ですし、また、コード内に登場するコンポーネント名や、`import`するファイル名などを毎回、手動で書いたりするのも面倒です。

そこで今回はこれらのファイル一式を雛形ファイルをもとに自動生成するジェネレーターを作ってみたいと思います。

# Yeoman事始め

![yeoman-logo.png](https://qiita-image-store.s3.amazonaws.com/0/269173/7affebd7-fd53-2486-3a0b-68a889f3e0ef.png)

今回、ジェネレーターを作成するに当たっては、[Yeoman](http://yeoman.io/)を利用します。

> **What's Yeoman?**
> Yeoman helps you to kickstart new projects, prescribing best practices and tools to help you stay productive.
> To do so, we provide a generator ecosystem. A generator is basically a plugin that can be run with the `yo` command to scaffold complete projects or useful parts.


```
$ npm install -g yo
```
まずは、Yeomanをインストール。

```
$ mkdir generator-mcs
```
早速、ジェネレーター用のフォルダをYeomanの命名規則に従って作成します。
ジェネレーター用のフォルダ名は「generator-name」のように、「generator」をフォルダ名のprefixとし、ハイフンで次に続くジェネレーター名を区切る必要があります。

```
$ cd generator-mcs
$ npm init -y
$ npm install --save yeoman-generator　yosay
```
次に、`package.json`を作成し、今回の記事で作成するジェネレーターで使う依存モジュールを先にインストールしておきます。

※ 今回は、`npm init -y`で`package.json`の細かな設定はスキップしましたが、作成したジェネレーターを[YeomanのGeneratorsページ](http://yeoman.io/generators/)にインデックスさせたい場合は、`package.json`の`keyword`に` ["yeoman-generator"]`を指定、また、`description`に任意の説明文を入れる必要があります。

```
$ mkdir -p generators/app generators/component
$ touch generators/app/index.js generators/component/index.js
```

今度は作業ディレクトリに`generators`フォルダーを作成し、その中に`app`フォルダーと`component`フォルダーを作成します。

```
.
├── generators
│   ├── app
│   │   └── index.js
│   └── component
│       └── index.js
└── package.json

```
Yeomanには`app generator`というメインのジェレネーターと、`sub generator`というサブのジェネレーターの２種類のジェネレーターが作成可能です。

両者の使い分けは、`app generator`は主にアプリケーション全体の雛形を作る為に利用し、`sub generator`はより限定的な用途に利用するといったイメージです。

デフォルトの設定では、`app`ディレクトリ以下の`index.js`が`app generator`に対応します。

そして、その他の任意の名前をつけるディレクトリ以下の`index.js`がそれぞれ`sub generator`に対応し、今回の例でいうと、`component`ディレクトリ下の`index.js`が`sub generator`に該当します。


```
$ yo mcs
```

例えば、この記事で作成しているジェネレーターの名前は`genertor-mcs`なので、`app`ディレクトリ以下の`app generator`は上記のコマンドで後ほど実行出来ることになります。

```
$ yo mcs:component
```

また、先ほど`generator`ディレクトリ以下に作成した`component`ディレクトリの名前は、そのまま、サブジェネレーターの名前となるので、上記のコマンドでこちらのサブジェネレーターを後ほど実行出来るようになります。

```app/index.js
const Generator = require('yeoman-generator');
const yosay = require('yosay');

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
    this.log(yosay('Welcome to MCS App Generator!'));
  }
};

```
とりあえず、`app generator`に対応する`app/index.js`を上記のように編集して、Hello World的なものを作り動作確認をしてみます。


```
$ npm link
```

動作確認をする為にも、上記のコマンドを実行して、現在作成しているジェネレーターをグローバールインストールしておきます。


```
$ yo mcs

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │    Welcome to MCS App    │
   `---------´   │        Generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `
```

`yo generator-name`を実行すると、無事に`app generator`の実行が確認出来るはずです。

`This Generator is empty. Add at least one method for it to run.`というエラーが出るかもしれませんが、後で他にメソッドを追加すれば解消されるエラーなので、今は特に気にせず、次に進みます。

ちなみに、最初にインストールした`yosay`ライブラリは、上記の実行結果を見て分かる通り、Yeomanのイメージキャラクターのアスキーアートを出力する為のライブラリです。(見た目大事、楽しさ大事！)

今回は特にアプリケーション全体の雛形ファイルを作ることはしないので、一旦、`app generator`はここまでとします。

# 雛形ファイルをもとにファイルを自動生成する

```
.
├── atoms
├── molecules
├── organisms
│   └── SampleComponent
│       ├── index.js
│       ├── SampleComponent.specs.js
│       ├── SampleComponent.stories.js
│       └── SampleComponent.vue
├── templates
├── pages
└── decorators
```

ここからは、この記事の概要で説明した通り、上記のようにcomponent単位のディレクトリ以下に`index.js`、`componentName.vue`, `componentName.stories.js`、`componentName.specs.js`の４ファイルを雛形ファイルをもとに生成する`sub generator`を作って行きます。

```
.
├── generators
│   ├── app
│   │   └── index.js
│   └── component
│       └── index.js
└── package.json
```

それでは、既に作成しておいた`component`ディレクトリ以下の`index.js`を編集して、`yo mcs:component`で実行出来るジェネレーターを作成して行きます。

```
$ mkdir generators/component/templates
```

先に、`component`ディレクトリ以下に`templates`という名前で雛形ファイルを配置する為のディレクトリを作成しておきます。

Yeomanのデフォルトの設定では、`templates`と名前の付いたディレクトリのパスが、後述する`this.templatePath()`で取得可能なパスになります。

```
$ cd generators/component/templates/
$ touch _index.js _ComponentName.vue _ComponentName.specs.js _ComponentName.stories.js
$ tree .
.
├── _ComponentName.specs.js
├── _ComponentName.stories.js
├── _ComponentName.vue
└── _index.js
```

続けて、`templates`ディレクトリ以下に雛形となる４つのファイルを任意の名前で作成します。

現時点では単に空ファイルを作成しているだけですが、後ほど、これらの雛形ファイルにそれらしいベースとなるコードを追記したいと思います。

※　ファイル名は任意の名前で大丈夫です。`_`をファイル名の先頭に付けることは必須ではありません。


```component/index.js
const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
  }

  writing () {
    this.fs.copyTpl(
      this.templatePath('_index.js'),
      this.destinationPath('ComponentName/ComponentName.index.js')
    );

    this.fs.copyTpl(
      this.templatePath('_ComponentName.vue'),
      this.destinationPath('ComponentName/ComponentName.vue')
    );

    this.fs.copyTpl(
      this.templatePath('_ComponentName.stories.js'),
      this.destinationPath('ComponentName/ComponentName.stories.js')
    );

    this.fs.copyTpl(
      this.templatePath('_ComponentName.specs.js'),
      this.destinationPath('ComponentName/ComponentName.specs.js')
    );
  }
};
```

早速、`component/index.js`を上記のように編集し、雛形ファイルをもとに新規ファイルを生成してみます。

ファイルの書き込み処理は、`Generator`クラスに定義されている`writing`メソッドの中に記述するようにします。

現段階では、コンポーネント名を直書きしていますが、後ほど、ユーザーからコンポーネント名の入力を受け付けるようにして、動的にコンポーネント名をファイル名に適用して行きます。

```component/index.js
this.fs.copyTpl(
  this.templatePath('_index.js'),
  this.destinationPath('ComponentName/ComponentName.index.js')
);
```
ここでやっていることは、`templates`ディレクトリ以下の雛形ファイルをコピー元として指定し、`yo`コマンドが実際に実行されるカレントディレクトリ以下の`ComponentName/ComponentName.index.js`にファイルをコピーするという処理です。

```
$ mkdir ~/demo-project
$ cd ~/demo-project
$ yo mcs:component
```
ジェネレーター開発用の作業ディレクトリで、`yo`コマンドでジェネレーターを実行してしまうと、作業ディレクト内にファイルが生成されてしまうので、ここからは、`yo`コマンドを実行する時には、別に作成したプロジェクトディレクトリ内で実行して行きます。

```
.
└── ComponentName
    ├── ComponentName.index.js
    ├── ComponentName.specs.js
    ├── ComponentName.stories.js
    └── ComponentName.vue
```

`yo`コマンドを実行すると、コマンドを実行したカレントディレクトリ上に、`ComponentName`というディクレトリが作られ、その中に４つのファイルが生成されていることが確認出来ました。

今度は動的にコンポーネント名を生成されるファイルに反映出来るようにして行きます。

```component/index.js
const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
  }

  async prompting() {
    // ユーザーの入力に関する情報をインスタンス変数に入れておく
    this.answers = await this.prompt([
      {
        type: 'input',
        name: 'componentName',
        message: `What's the component name?`,
        validate (input) {
          if (input.length > 0) {
            return true;
          } else {
            // 文字列を返すと検証エラー時にそのメッセージが出力表示される
            return "You need to provide the component name.";
          }
        }
      }
    ]);
  }

  writing () {
    // ユーザーが入力したコンポーネント名をインスタンス変数を参照して渡す
    this.fs.copyTpl(
      this.templatePath('_index.js'),
      this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.index.js`)
    );

    this.fs.copyTpl(
      this.templatePath('_ComponentName.vue'),
      this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.vue`)
    );

    this.fs.copyTpl(
      this.templatePath('_ComponentName.stories.js'),
      this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.stories.js`)
    );

    this.fs.copyTpl(
      this.templatePath('_ComponentName.specs.js'),
      this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.specs.js`)
    );
  }

};
```
ユーザーからの入力を受け付ける処理は、`Generator`クラスに定義されている`prompting()`の中に記述するようにします。

`prompting`メソッドの中で実行している`prompt`メソッドは非同期実行され、`promise`を返します。

そのため、`async`, `await`を`prompting`メソッドのところでは使っています。


```
$ yo mcs:component
? What's the component name? SampleComponent
   create SampleComponent/SampleComponent.index.js
   create SampleComponent/SampleComponent.vue
   create SampleComponent/SampleComponent.stories.js
   create SampleComponent/SampleComponent.specs.js
```

`yo`コマンドを実行すると、コンポーネント名を入力するプロンプトが表示され、入力したコンポーネント名が反映されたディレクトリとファイルが生成されることが確認出来ました。

```/templates/_ComponentName.vue
<template>

</template>

<script>
  export default {
    name: "<%= componentName %>"
  }
</script>

<style lang="scss" scoped>

</style>
```

ここまでは、空の雛形ファイルをもとに動作確認を進めていたので、`templates/_ComponentName.vue`に簡易的なコードを加えて保存しておきます。

既に雛形ファイルをコピーする際に使用してきた`fs.copyTpl`メソッドは`ejs template syntax`を使用しているので、雛形ファイルの方では、`<%= componentName %>`のような`ejs`の記法を使用することが可能です。

```component/index.js
this.fs.copyTpl(
  this.templatePath('_ComponentName.vue'),
  this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.vue`),
  { componentName: this.answers.componentName }
);
```

`_ComponentName.vue`内の`<%= componentName %>`の部分に、ユーザーから入力されたコンポーネント名を反映出来るようにしてみます。

その為には、上記のコードのように、`fs.copyTpl`メソッドの第３引数に対象の雛形ファイルに渡したい変数を`key`, `value`ペアのオブジェクトで渡してあげます。


```
$ yo mcs:component
? What's the component name? SampleComponent
identical SampleComponent/SampleComponent.index.js
 conflict SampleComponent/SampleComponent.vue
? Overwrite SampleComponent/SampleComponent.vue? (ynaxdH)
  y) overwrite
  n) do not overwrite
  a) overwrite this and all others
  x) abort
  d) show the differences between the old and the new
  h) Help, list all options
```

これで、再度、`yo`コマンドを実行し、コンポーネント名を入力してあげれば、入力したコンポーネント名が生成される`.vue`ファイルの該当箇所に反映されます。

尚、ファイル生成時に既に同じファイルが存在していて、既存のファイルと新規に生成されるファイルの内容が異なる場合は、上記のように`conflict`が発生します。

その際は、`H`を押せば選択可能なオプションが表示されるので、その中から任意のオプションを選択すると良いです。

ちなみに、`y`を押せば上書き保存してくれます。


# app generatorからsub generatorに処理を委譲する

最後に、ジェネレーターから別のジェネレーターに処理を委譲する方法を紹介して、今回の記事を終わりにしたいと思います。

ここでは例として、`app generator`から`sub generator`に処理を委譲してみたいと思います。

```app/index.js
const Generator = require('yeoman-generator');
const yosay = require('yosay');

module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts);
    this.log(yosay('Welcome to MCS App Generator!'));
  }

  // ユーザーにサブジェネレーターを選択してもらう
  async prompting() {
    const answers = await this.prompt([
      {
        type: 'list',
        name: 'generatorName',
        message: 'What do you want to generate?',
        choices: [
          {
            name: 'Vue Component',
            value: 'component'
          },
          {
            name: 'Vuex Store Module',
            value: 'module'
          }
        ]
      }
    ]);

    // ユーザーが選択したサブジェネレーターに処理を委譲する
    this.composeWith(
      require.resolve(`../${answers.generatorName}`)
    );
  }
};
```
`app generator`に対応する`app/index.js`を上記のように編集します。

これで、`yo mcs`とコマンド実行した時に、先ほど作成した`component`サブジェネレーターを実行するのか、`module`サブジェネレーターを実行するのかを、ユーザーに選択してもらえるようになり、選択されたサブジェネレーターに処理が委譲され実行されることになります。

※ 尚、２つ目の選択肢に対応するサブジェネレーターは未作成の為、選択するとファイルが見つからずエラーになります。上記の例は`module`サブジェネレーターが仮に既に作成済みであったという想定の例になります。

```
$ yo mcs

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │    Welcome to MCS App    │
   `---------´   │        Generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What do you want to generate? (Use arrow keys)
❯ Vue Component
  Vuex Store Module
? What's the component name? SampleComponent
   create SampleComponent/SampleComponent.index.js
   create SampleComponent/SampleComponent.vue
   create SampleComponent/SampleComponent.stories.js
   create SampleComponent/SampleComponent.specs.js
```

# 今後やりたいこと

1. yosayライブラリで出力されるYeomanのイメージキャラクターの代わりに、モチベーションクラウドの広告に起用されている役所広司さんのアスキーアートをウェルカムメッセージに表示出来るようにしたい
2. 実際のプロジェクトで使われているコードを雛形ファイルにし、今回作成したジェネレーターを実用的なものにしたい
3. 単一ファイルコンポーネントの`<script>`部分をASTにparseして`props`情報を抽出し、その`props`情報をもとにStorybook addonの`knobs`に対応したStorybookファイルを自動生成出来るようにしたい

# 参考
- [WRITING YOUR OWN YEOMAN GENERATOR](http://yeoman.io/authoring/)
- [SBoudrias/Inquirer.js](https://github.com/SBoudrias/Inquirer.js)
- [Sboudrias/mem-fs-editor](https://github.com/sboudrias/mem-fs-editor)
- [EJS#docs](https://ejs.co/#docs)

# 関連記事
こちらの記事は[モチベーションクラウド Advent Calendar 2018](https://qiita.com/advent-calendar/2018/mcs)に投稿した記事です。

他にも、以下の記事を[モチベーションクラウド Advent Calendar 2018](https://qiita.com/advent-calendar/2018/mcs)に投稿しています。
- [Vueを用いた開発プロジェクト用にカスタムジェネレーターを作ってみる](https://qiita.com/HayatoKamono/items/716c062aba8c64777a05)
- [Vueを用いた開発プロジェクト用に「コンポーネント設計・実装ガイドライン」を作った話](https://qiita.com/HayatoKamono/items/72b02d439a83d68fc15a)
- [Vuexを用いた開発プロジェクト用にガイドラインを作成した話](https://qiita.com/HayatoKamono/items/eb6d253a1c73ab001659)