コマンドラインツールにテンプレート機能を取り入れるための調査と実験

はじめに

Yeoman の generator みたいなテンプレート機能を自前のコマンドラインツール*1に作り込む必要が生じたので調査しました.

結論としては yeoman-environment と mem-fs-editor,inquirer あたりを組み合わせることで容易に実現できました.

Yeomon の調査

今回は Interacting with the file system に記載されている機能の一部 (templates からのファイルコピーと ejs テンプレート処理) が欲しいのでそこらへんを中心に調べます.

ejs によるテンプレート処理については this.fs.copyTpl を使えば良いことがマニュアルから読み取れます.パッケージは mem-fs-editor のようです.

それからコピー元のパスである templatePath を解決するための情報については,コードを調べてみると yeoman-environment がこの辺りの機能を担っていることがわかります.

https://github.com/yeoman/environment/blob/v2.8.0/lib/resolver.js#L42-L76

また,コピー先を決定する destinationPath.yo-rc.json や引数による指定がなければ process.cwd() から取得しているようです.

https://github.com/yeoman/generator/blob/v4.5.0/lib/index.js#L162-L176

https://github.com/yeoman/generator/blob/v4.5.0/lib/index.js#L831

https://github.com/yeoman/environment/blob/master/lib/environment.js#L209

実験

まずは yeoman-environment の方から.npm install --save yeoman-environment して以下のようなコードを書いて実行すると,

// file: /path/to/use-yeoman-environment/index.js
const y = require('yeoman-environment');
const env = y.createEnv();
const res = env.lookup({
  packagePatterns: 'yeoman-*',
  filePatterns: '*\/environment.js'
});
console.log(res);
$ node index.js 
[ { generatorPath:
     '/path/to/use-yeoman-environment/node_modules/yeoman-environment/lib/environment.js',
    packagePath:
     '/path/to/use-yeoman-environment/node_modules/yeoman-environment',
    namespace:
     'path:to:use-yeoman-environment:node_modules:yeoman-environment:lib:environment',
    registered: true } ]

こういう結果が返ってきます.なのでテンプレートデータのコピー元の絶対パス解決には packagePath を使えば良さそうです.

mem-fs-editor の方は以下のように利用します.

const memfs  = require('mem-fs');
const editor = require('mem-fs-editor');

const fs = editor.create(memfs.create());

const context = { name: 'hoge' };

fs.copyTpl('template/test.json', 'dest/test.json', context);
fs.commit(() => console.log('保存した'));

commit を呼び出すとファイルとして書き出されます.

試作

実際にテンプレートから雛形を生成する試作コマンド proto-cli を作成します.

proto-cli/
  template/
    index.js
    package.json
  main.js
  package.json

template/ 以下の内容を雛形としてコピーします.template/package.json の中身は以下の通り.

{
  "name": "<%= name %>",
  "version": "<%= version %>",
  "description": "<%= description %>",
  "private": true,
  "main": "index.js"
}

そして main.js の中身は以下の通り.ユーザー入力は inquirer でハンドリングします.

#!/usr/bin/env node

const yeoman   = require('yeoman-environment');
const memfs    = require('mem-fs');
const editor   = require('mem-fs-editor');
const inquirer = require('inquirer');
const path     = require('path');

const fs  = editor.create(memfs.create());
const env = yeoman.createEnv();

const res = env.lookup({
  packagePatterns: 'proto-cli',
  filePatterns: 'main.js'
});

const templateRoot    = path.join(res[0].packagePath, 'template');
const destinationRoot = process.cwd();
const templatePath    = file => path.join(templateRoot, file);
const destinationPath = file => path.join(destinationRoot, file);

const copyTemplate = context => {
  fs.copyTpl(
    templatePath('package.json'),
    destinationPath('package.json'),
    context
  );
  fs.copy(
    templatePath('index.js'),
    destinationPath('index.js')
  );
  fs.commit(() => console.log('initialized'));
};

inquirer.prompt([
  {
    name: 'name',
    message: 'package name',
    default: path.basename(__dirname)
  },
  {
    name: 'version',
    message: 'version',
    default: '0.1.0'
  },
  {
    name: 'description',
    message: 'description'
  },
  {
    name: 'author',
    message: 'author'
  }
]).then(answers => {
  copyTemplate(answers);
});

実際に動くコードはこちら. github.com

npm link した後,適当なディレクトリの中で proto-cli を実行すると動作を確認できます.

おわりに

yeoman-environmentmem-fs-editor (+ inquirer) を利用することで期待するテンプレート機能を実装することができました.当初は自前で実装しようと思っていましたが便利な形でパッケージ化されていたため助かりました.

さらにここから commander を組み合わせることで,よく見かける CLI ツールが作成できます.

*1:環境は Node.js