Yeomanで自動化!自作generatorの作り方

Yeomanで自動化!自作generatorの作り方

ほりでー

ほりでー

こんにちは! フロントエンドエンジニアのほりでーです。

Web制作に関わる方であれば、「Yeoman」というツールをご存知の方も多いのではないでしょうか。Yeomanは、Googleが中心に開発しているnode.jsのツールで、よく「WebサイトやWebアプリケーションのベースを、まるっと簡単に構築してくれるツール」として紹介されていることが多いです。

僕も最近までYeomanのことを開発環境を構築するためのツールだと思っていました。しかし、Yeomanのテンプレート(通称:generator)は、簡単に自作することができるので、汎用的なひな型ツールとしての側面も持っています。

今回は、Yeomanのgenerator-generatorというテンプレートを使い、ちょっとした制作効率アップのためのテンプレートを作成してみたいと思います。

Yeomanで自動化!自作generatorの作り方

今回作りたいテンプレート

僕はWebサイトのコーディングをおこなう際、パーツ単位でHTML(EJS)ファイルとSCSSファイルを作成し、同じフォルダの中に入れて整理する、ということをよくやっています。

components
└── my-component
├── _my-component.ejs
└── _my-component.scss

具体的には、上のmy-componentフォルダのような感じです。
ファイルの内容は、以下の部分が常に共通しています。

_my-component.ejs

<% var name = 'my-component' %>
<% if (typeof modifier === 'undefined') { var modifier = ''; } %>
<!— <%= name %> —>
<div class="<%= name %> <%= modifier >"> 
<%# ここにHTMLを実装 %>
</div>
<!— <%= name %> —>

_my-component.scss

.my-component {
  $this: &;
  // ここにCSSを実装する
}

作成したEJSとSCSSファイルは、他のファイルからincludeや@importして使用する想定です。上図では「my-component」という名前を取っていますが、もちろんコンポーネントごとに実際の名前は変えなければいけません。

内容がほとんど一緒で、名前だけ違うものをYeomanで量産する

サイトのコンポーネントが増えるたびに、componentsフォルダへこの構成のフォルダを増やしていくのですが、毎回同じ構成で名前だけが違うものに時間を割くというのは非生産的です。加えて、EJSやSCSSファイルの中身も合わせて名前を書きかえる必要があり、単純なファイルの複製では済みません。

そこで今回の例では、先程の構成を名前部分だけ自動で置換して量産できるgeneratorを作成しています。

Yeomanとgenerator-generatorのインストール

事前にnode.js環境が必要です。まだインストールしたことがない方は、下記の記事をご覧ください。

下記のコマンドで、Yeomanとgenerator-generatorをインストールできます。

$npm install -g yo
$npm install -g generator-generator

generator-generatorによるgeneratorの作成

generator-generatorは、generatorを自作するためのテンプレートとなるgeneratorです。

Yeoman generator generating a Yeoman generator
GitHub – yeoman/generator-generator: Generate a Yeoman generator

早速generator-generatorで自作のgeneratorを作成してみましょう!(言葉にすると意味わかんないですね)
今回は、generator-lig-sampleという名前で作成してみます。yoコマンドを実行すると選択肢が表れるので、上下キーでRun a generatorの下にあるGeneratorを選択し、Enterを押します。

$mkdir generator-lig-sample && cd $_
$yo
? ‘Allo Yuma! What would you like to do? (Use arrow keys)
Run a generator
❯ Generator
──────────────
Update your generators
Install a generator
Find some help
Clear global config
Get me out of here!
──────────────

いろいろ聞かれますが、とりあえず全部Enterで良いでしょう。(これらの設問は、作成したgeneratorをnpmに公開したい場合に必要となります)

? Your generator name generator-lig-sample
? Description
? Project homepage url
? Author’s Name
? Author’s Email
? Author’s Homepage
? Package keywords (comma to split)
? Send coverage reports to coveralls Yes
? GitHub username or organization
? Your website (optional):
? Which license do you want to use? Apache 2.0

最後にnpmパッケージのインストールが始まるので少し待ちます。
yeoman

この画面が出たら、generatorの生成は完了です。

作った素のgeneratorを実行してみる

まだ何もいじっていませんが、作成したgeneratorを実行してみましょう。

$npm link

そのためには、generator-lig-sampleフォルダ内でのnpm linkをすることが必要です。

 

$npm unlink -g generator-lig-sample

なお、generator-lig-sampleが不要になった場合は、上記のコマンドでnpm linkを取り消すことができます。適当な他の階層に移動してから、もう一度yoコマンドを実行してみます。

 

$cd ~/Desktop
$yo
? ‘Allo Yuma! What would you like to do?
──────────────
Run a generator
Generator
❯ Lig Sample
──────────────
Update your generators
Install a generator

Run a generatorの所に「Lig Sample」という項目が増えているはずです。今度はこれを実行してみましょう。
yeoman2

すると、AAの後に下記の質問が来ました。

? Would you like to enable this option? (Y/n)

これはダミーの質問なのでどう回答しても結果は変わりません。コマンドが終了すると、デスクトップに「dummyfile.txt」というファイルと、空っぽの「node_modules」というファイルが作成されています。

一体何が起こったのか

先程作成したgenerator-lig-sampleディレクトリの「generators/app/index.js」を開いてみましょう。

// Have Yeoman greet the user.
this.log(yosay(
  'Welcome to the super-duper ' + chalk.red('generator-lig-sample') + ' generator!'
));

どうやらAAの吹き出しの中身は、ここに書いたものが表示されているようです。

 

var prompts = [{
  type: 'confirm',
  name: 'someOption',
  message: 'Would you like to enable this option?',
  default: true
}];

実行時の質問も、このpromp配列の中に定義されているっぽいです。

 

writing: function () {
  this.fs.copy(
    this.templatePath('dummyfile.txt'),
    this.destinationPath('dummyfile.txt')
  );
},

dummyfile.txtはこの命令によって作られています。dummyfile.txtは「generators/app/templates」階層にあるため、ここにコピー元のファイルを保存しておくルールのようです。

 

install: function () {
  this.installDependencies();
}

空のnode_modulesフォルダは、このinstallDependenciesメソッドによって作成されたのでしょう。このように、generatorの動作はこのindex.jsの中身によって決められていることが分かります。

index.jsをカスタマイズ

次は、このindex.jsとtemplatesフォルダ内のファイルを編集し、generatorをカスタマイズしていきましょう。

書き換え前:

var prompts = [{
      type: 'confirm',
      name: 'someOption',
      message: 'Would you like to enable this option?',
      default: true
    }];

    this.prompt(prompts, function (props) {
      this.props = props;
      // To access props later use this.props.someOption;

      done();
    }.bind(this));

書き換え後:

var prompts = [{
      name: 'moduleName',
      message: 'type new module name, please',
      default: 'newName'
    }];

    this.prompt(prompts, function (props) {
      this.props = props;
      // To access props later use this.props.someOption;
      this.moduleName = this.props.moduleName;
      done();
    }.bind(this));

まずは、ユーザの入力から新しいコンポーネントの名前を受け取れるようにします。書き換え後のコードでは、ユーザの入力した文字を「moduleName」というプロパティに格納しています。
書き換え前:

writing: function () {
    this.fs.copy(
      this.templatePath('dummyfile.txt'),
      this.destinationPath('dummyfile.txt')
    );
  },

書き換え後:

writing: function () {
    var moduleName = this.moduleName;
    var ejsName = '_' + moduleName + '.ejs';
    var scssName = '_' + moduleName + '.scss';

    this.fs.copyTpl(
      this.templatePath('_template.ejs'),
      this.destinationPath(moduleName + '/' + ejsName),
      { moduleName:moduleName, end:'%>' }
    );
    this.fs.copyTpl(
      this.templatePath('_template.scss'),
      this.destinationPath(moduleName + '/' + scssName),
      { moduleName:moduleName }
    );
  },

そして、このmoduleNameを元にファイル名、ディレクトリ名が作成されるようにします。ファイル名や保存先を、先程作ったプロパティを元に作成しています。
書き換え後:

install: function () {
    // this.installDependencies();
  }

今回作成するgeneratorでは特にnpmやbowerのインストールは不要なので、this.installDependencies()はコメントアウトしていまいます。

これでindex.jsの変更は完了です。ポイントは、this.fs.copyTpl()の第3引数にある{ moduleName:moduleName }の部分です。このオブジェクト内のキー名と値が、ひな型ファイル内から参照できるパラメータの名前と値に対応しています。

templates内にひな型ファイルを配置

次に、「generators/app/templates」フォルダ内にコピー元となるひな型のファイルを配置します。

generator
└── app
└── templates
├── _template.ejs
└── _template.scss

_template.ejs

<%% var name = '<%= moduleName %>'; <%- end %> %>
<%% if (typeof modifier === 'undefined') { var modifier = ''; } %>
<!-- <%%= name %> -->
<div class="<%%= name %> <%%= modifier %>">
</div>
<!-- /<%%= name %> -->

_template.scss

<%= moduleName %> {
  $this: &;
}

<%= moduleName %>の箇所へ、先程のindex.jsで定義したユーザーの入力値が反映されます。

<%%によるエスケープ処理

今回はEJSファイルをテンプレート化していますが、Yeoman自体もEJSを使っているため、generator実行時に処理したくない部分は%を二重にしてエスケープする必要があります。

また、エスケープされたEJSタグ内にEJSタグを挿入すると、Yeomanの処理後にEJSの閉じタグが上手く処理できないバグがありました。これを回避するために、index.js内でendというパラメータを用意し、無理やりEJSの閉じタグを挿入しています。

実行してみよう!

これでgeneratorは完成です。早速実行してみましょう。

適当なディレクトリに移動してgenerator-lig-sampleを実行してみます。

$ cd ~/Desktop
$ yo lig-sample

yoコマンドの後にgenerator名を入力すると、直接generatorを実行することができます。

 

? type new module name, please (newName)

AAの後に出てくる質問が、書き換えたオリジナルの質問になっているはずです。ここにholidayと入力してみます。

? type new module name, please holiday
create holiday/_holiday.ejs
create holiday/_holiday.scss

holidayコンポーネントが生成されました!

~/Desktop
└── holiday
├── _holiday.ejs
└── _holiday.scss

_holiday.ejs

<% var name = 'holiday'; %>
<% if (typeof modifier === 'undefined') { var modifier = ''; } %>
<!-- <%= name %> -->
<div class="<%= name %> <%= modifier %>">
</div>
<!-- /<%= name %> -->

_holiday.scss

.holiday {
  $this: &;

}

<%= moduleName %>の部分がholidayに置き換わっているので、成功です。

まとめ

  • yeomanのgeneratorを自作すると、複数のファイルからできている構造をまるごとテンプレート化できる
  • generator-generatorでgeneratorのひな型が作成できる
  • generators/app/index.jsの中に対話やファイルの処理内容を書ける
  • this.fs.copyTpl()の第3引数で、テンプレートにパラメータを渡すことができる
  • テンプレートファイルはgenerators/app/templates以下に格納する
  • テンプレート内では、EJSのタグを使って受け取ったパラメータを展開できる

この作例とほぼ同じ内容のgeneratorを実際に使っていますが、手作業で作っていたものがサクサク量産できるようになり、非常に快適に作業ができるようになりました。アイデア次第でいろいろなものをテンプレート化できるので、興味のある方はぜひ自作generatorに挑戦してみてください!

ところで

この記事の「ところで」よりも前の記事中に、コードの部分や大文字小文字違い、複数形も含め、一体何回「generator」という言葉が登場したでしょうか……!?(genrating、generateは含みません)

正解は68回でした!

LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。

Webサイト制作の実績・料金を見る

この記事のシェア数

ほりでー
ほりでー フロントエンドエンジニア / 堀 祐磨

デザイナーから転職してきました。毎日がホリデーです。

このメンバーの記事をもっと読む