JavaScriptの開発者にとって、新しいプロジェクトを始めるときに繰り返しで退屈なプロセスが多くあります。プロジェクトごとにpackage.jsonファイルを追加し、標準的な依存オブジェクトを持ってきて構成し、適切なディレクトリ構造を作り、そのほかのファイルを追加し……と、やるべきことは続きます。
ラクしたいですよね? 幸いなことに、これらの作業を自動化できます。特別なツールや言語は必要ありません。JavaScriptの知識があれば簡単です。
このチュートリアルでは、Node.jsを使ってクロスプラットフォーム・コマンドラインインターフェイス(CLI)を作ります。あらかじめ定義したテンプレートのセットを使って、新しいプロジェクトを素早くスキャホールド(足場を組むこと)ができます。拡張性があるので、ニーズに合わせるのも簡単です。ワークフローの面倒な部分を自動化できるのです。
ニーズに合わせた「自作ツール」のススメ
Yeomanをはじめ、タスクを自動化するツールはたくさんありますが、自作することで知識と経験が得られます。さらにカスタマイズが可能なので、特化した問題を解決したいなら、既存のツールより自作のツールを考えるべきです。ソフトウェアの再利用を考える慣習には反しますが、自分で実装すると大きな見返りが得られます。知識を得るだけではなく、ニーズに特化した効率的なツールが得られます。
とはいえ、「車輪を再発明しよう」というわけではありません。CLIそのものはライブラリーCaporal.jsを使います。内部ではユーザーデータの入力を促すpromptを使いますし、Node.jsの環境ではUnixツールを提供するshellJSを使います。これらのライブラリーを選んだ理由は使い勝手が良いからですが、チュートリアルを終えたあとは、自分のニーズに一番良く合う別のライブラリーを使っても構いません。
Github: https://github.com/sitepoint-editors/node-scaffolding-toolにプロジェクトを上げています。
では、始めます。
Caporal.jsをインストールして実行する
コンピューターのどこかに新しいディレクトリを作成します。最終的なコマンドが毎回そこから呼ばれるので、長期間変更しない専用のディレクトリを作ります。
ディレクトリの中に、以下のpackage.jsonファイルを作ります。
{
"name": "scaffold",
"version": "1.0.0",
"main": "index.js",
"bin": {
"scaffold": "index.js"
},
"dependencies": {
"caporal": "^0.3.0",
"colors": "^1.1.2",
"prompt": "^1.0.0",
"shelljs": "^0.7.7"
}
}
この中に必要なものがすべて含まれます。次に、パッケージをインストールするためにnpm installを実行すると、マークした依存オブジェクトをプロジェクトで使えるようになります。パッケージは執筆の時点で最新のものです。さらに新しいバージョンが出たらAPIの変更に気をつけつつ、 アップデートしても良いかもしれません。
binのscaffoldの値に注意してください。 コマンドの名前とターミナル(index.js)にコマンドを入力するたびに呼ばれるファイル名を示します。値は必要に応じて変更してください。
エントリーポイントを作る
CLIの最初のコンポーネントはindex.jsファイルで、コマンド、オプション、および関連する関数のリストが含まれます。このファイルを書く前に、CLIがなにをするのか少し詳細に定義します。
- メインで唯一のコマンドはcreateで、選んだプロジェクトボイラープレートを作成します
- createコマンドにはtemplate引数が必要で、使いたいテンプレートを示します
- --variantオプションも取ります。どのバリエーションのテンプレートを選ぶか選択します
- 特にバリエーションの指定がなければ、後で定義するデフォルトを使います
Caporal.jsは上の内容をコンパクトな形で定義します。次の内容をindex.jsファイルに加えます。
#!/usr/bin/env node
const prog = require('caporal');
prog
.version('1.0.0')
.command('create', 'Create a new application')
.argument('<template>', 'Template to use')
.option('--variant <variant>', 'Which <variant> of the template is going to be created')
.action((args, options, logger) => {
console.log({
args: args,
options: options
});
});
prog.parse(process.argv);
最初の行は、Node.jsの実行ファイルであることを示すシェバンです。
次に、progとしてCaporal.jsパッケージをインクルードし、プログラムの定義を開始します。command functionを使って、createコマンドを最初のパラメーターとして定義し、簡単な記述を2番目のパラメーターとして定義します。--helpを使って自動的に生成されるCLIのヘルプのオプションを示します。
argument functionのtemplate引数をチェーンします。必要な引数なので、やまかっこ(<と>)で囲みます。
バリアントのオプションをoption functionで--variant <variant>と書いて定義します。コマンドのオプションが--variantと呼ばれて、値はvariant変数に格納されます。
最後に、action commandで、現在のコマンドを扱う関数をもう1つ渡します。このコールバックは3つの引数とともに呼ばれます。
- 渡される引数(args)
- 渡されるオプション(options)
- スクリーンに表示するためのユーティリティオブジェクト(logger)
この時点で、渡された引数とオプションの値のログをとります。CLIからアクションを実行するのに必要な情報の入手方法が分かります。
最後の行は、もっとも手間のかかる処理をするCaporal.jsパーサーにscaffoldコマンドから情報を渡します。
CLIをグローバルに使えるようにする
計画どおりかアプリケーションのテストをします。npmのlinkコマンドを使って、システムをグローバルで使えるようにします。プロジェクトルートから以下を実行します。
npmlink
プロセスが完了したら、index.jsファイルを参照しなくても、どのディレクトリのターミナルじゃらでもscaffoldを実行できるようになります。
scaffold create node --variant mvc
レスポンスは以下の通りです。
{ args: { template: 'node' }, options: { variant: 'mvc' } }
テンプレートからプロジェクトを作るために、次に使う情報のサンプルになります。
テンプレートを作る
テンプレートは、プロジェクトをインストールして実行するためのファイルとディレクトリの構造からなります。すべてのテンプレートには、プレースホルダーの値を持ったpackage.jsonがあり、実際のデータで埋められます。
まず、プロジェクトのtemplatesディレクトリを作り、その中にnodeディレクトリを作ります。nodeディレクトリの中にvariantオプションを指定しなかったときに使われるdefaultディレクトリと、2番目のmvcと呼ばれるMVC architectureを使ってNode.jsプロジェクトを作るためのディレクトリを作ります。
最終的な構造は以下の通りです。
.
└── templates
└── node
├── default
└── mvc
defaultとmvcフォルダーにファイルを入れます。自分で作るか、sample appにあるファイルを使います。
次に、動的な値を使いたい場所に変数識別子を置きます。各テンプレートのフォルダーにはpackage.jsonが必須です。これらを開いて、変数を大文字(スペースは不可)と角かっこでインクルードします。
以下がデフォルトテンプレートのpackage.jsonファイルです。
{
"name": "[NAME]",
"version": "[VERSION]",
"description": "[DESCRIPTION]",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"start:dev": "nodemon server.js"
},
"author": "[AUTHOR]",
"license": "[LICENSE]",
"dependencies": {
"dotenv": "^2.0.0",
"hapi": "^16.1.0",
"hoek": "^4.1.0"
},
"devDependencies": {
"nodemon": "^1.11.0"
}
}
変数を作成したあと、同様にテンプレートのディレクトリの_variables.jsファイルに変数を入れます。
/*
* Variables to replace
* --------------------
* They are asked to the user as they appear here.
* User input will replace the placeholder values
* in the template files
*/
module.exports = [
'name',
'version',
'description',
'author',
'license'
];
エクスポートされる配列内の名前は、ファイル中の名前と一緒ですが、小文字で角かっこは使いません。このファイルは、CLIが各変数の値を問い合せるために使います。
これから、すべての処理をするcreateコマンドの関数を作成します。
「Create」関数を作る
index.jsファイルではCLIが受け取った値を記録するaction()に簡単な関数を渡しました。その関数をscaffoldコマンドが実行されるディレクトリに、テンプレートファイルをコピーした新しい関数を置き換えます。また、プレースホルダー変数をユーザーが入力した値と入れ替えます。
物事を整理するためにlibディレクトリに、 create.jsファイルを加え、次の内容を入れます。
module.exports = (args, options, logger) => {
};
アプリケーションのロジックをこの関数の中に置きます。それにともないindex.jsファイルを変更します。
#!/usr/bin/env node
const prog = require('caporal');
const createCmd = require('./lib/create');
prog
.version('1.0.0')
.command('create', 'Create a new application')
.argument('<template>', 'Template to use')
.option('--variant <variant>', 'Which <variant> of the template is going to be created')
.action(createCmd);
prog.parse(process.argv);
依存オブジェクトをインポートし、変数を設定する
create.jsファイルに戻り、ファイルの先頭に以下を加えて必要なパッケージを使えるようにします。
const prompt = require('prompt');
const shell = require('shelljs');
const fs = require('fs');
const colors = require("colors/safe");
// Set prompt as green and use the "Replace" text
prompt.message = colors.green("Replace");
注意:プロンプトメッセージのcustomizationの設定はオプションです。
エクスポートした関数に変数を加えます。
const variant = options.variant || 'default';
const templatePath = `${__dirname}/../templates/${args.template}/${variant}`;
const localPath = process.cwd();
scaffoldコマンドに渡されたvariantオプションの値を見て、オプションが省略されていれば「default」をセットします。templatePath変数は指定されたテンプレートの完全なパスを含み、localPathはコマンドが実行されるディレクトリの参照を含みます。
テンプレートのファイルをコピーする
ファイルをコピーするプロセスはshellJSのcp関数を使います。先ほどインクルードした変数のあとに以下のコードを追加します。
if (fs.existsSync(templatePath)) {
logger.info('Copying files…');
shell.cp('-R', `${templatePath}/*`, localPath);
logger.info(' The files have been copied!');
} else {
logger.error(`The requested template for ${args.template} wasn't found.`)
process.exit(1);
}
テンプレートが存在するか確かめます。存在していなければ、Caporal.jsのlogger.error()関数でエラーメッセージを表示し、プロセスを終了します。テンプレートがあれば、logger.info()で通知メッセージを表示し、shell.cp()でファイルをコピーします。-Rオプションは、テンプレートのパスからコマンドが実行されているパスに再帰的にコピーすることを示します。ファイルがコピーされたら、確認のメッセージを表示します。shellJS関数は同期なのでコールバックやプロミスなどは使わずに、手続き型でコード書きます。
変数を置換する
ファイルの変数を置換するのは複雑なように聞こえますが、ツールを正しく使えば簡単です。そんなツールの1つがUnixの古典的なsedエディターで、動的にテキストを変換できます。 ShellJSでは、WindowsだけでなくLinuxとOS XのUnixシステムで動くユーティリティが使えます。
すべて置換するために、いままで作ったコードのあとに以下のコードを加えます。
const variables = require(`${templatePath}/_variables`);
if (fs.existsSync(`${localPath}/_variables.js`)) {
shell.rm(`${localPath}/_variables.js`);
}
logger.info('Please fill the following values…');
// Ask for variable values
prompt.start().get(variables, (err, result) => {
// Remove MIT License file if another is selected
// Omit this code if you have used your own template
if (result.license !== 'MIT') {
shell.rm(`${localPath}/LICENSE`);
}
// Replace variable values in all files
shell.ls('-Rl', '.').forEach(entry => {
if (entry.isFile()) {
// Replace '[VARIABLE]` with the corresponding variable value from the prompt
variables.forEach(variable => {
shell.sed('-i', `\\[${variable.toUpperCase()}\\]`, result[variable], entry.name);
});
// Insert current year in files
shell.sed('-i', '\\[YEAR\\]', new Date().getFullYear(), entry.name);
}
});
logger.info('✔ Success!');
});
「,」を読むことで開始し、以前に作成したテンプレートの_variables.jsファイルの内容にvariablesを設定します。
これで、 テンプレートからすべてのファイルをコピーしました。最初のif文はCLIの中でしか必要とされないので、_variables.jsファイルをローカルディレクトリから削除します。
プロンプトツールで各変数の値を得て、変数の配列がget()関数に渡されます。こうして、CLIは配列中の各項目の値を尋ねて、結果をオブジェクトresultに格納し、コールバック関数に渡します。このオブジェクトはキーとして各変数を保持し、代入されたテキストを値として保持します。
次のif文は、LICENSEファイルもインクルードしたので、レポジトリでインクルードされたテンプレートを使っているときだけ必要になります。それでも、各変数の値を持ってくる方法を知ることができます。この場合result.licenseでlicenseプロパティから値を持ってきます。 MIT以外のライセンスを入力すると、ShellJsのrm()関数を使ってディレクトリからLICENSEファイルを消去します。
ここからがおもしろいところです。 ShellJSのls関数で、変数を置換する現在のディレクトリ(.)のファイルリストをすべて得られます。-Rlオプションを渡すと再帰となり、ファイル名の代わりにfileオブジェクトを返します。
forEach()でファイルオブジェクトのリストをループし、各オブジェクトに対してisFile()でファイルが返されるかをチェックします。ディレクトリを受け取った場合はなにもしません。
受け取ったファイルごと、すべての変数をループしてsed関数を実行します。
shell.sed('-i', `\\[${variable.toUpperCase()}\\]`, result[variable], entry.name);
-iオプションを渡してテキストを置換し、大文字で角かっこ([と])で囲まれたvariable識別子にマッチするregex文字列を渡します。regexが一致すると、対応する変数(result[variable])で置換され、最後にforEach()関数(entry.name)から置換するファイル名を渡します。
2番目のsedは、[YEAR]が使われている場合に現在の年で置換するためのオプションです。LICENSEあるいはREADME.mdファイルに使うと便利です。
以上で終了です。空のディレクトリでもう一度コマンドを実行すると、プロジェクト構造が作られる様子や変数がすべて新しい変数で置換されます。
// To generate a Node.js MVC project
scaffold create node --variant mvc
// To generate a default Node.js project
scaffold create node
コマンドを実行すると、変数の値を聞いてきます。プロセスが終了すると、「成功した」というメッセージが表示されます。すべてが期待どおり進んだのをチェックするために、変数が格納されたファイルを開いて、大文字の識別子に代わってCLIプロセスで入力したテキストが格納されているか確認してください。
リポジトリのテンプレートを利用した場合、実行中のNodeプロジェクトが作成されるはずです。npm installの次にnpm startを実行できます。
次にすること
テンプレートから新しいNode.jsを作るCLIツールを作りました。ゼロから自作ツールを作ったので、なにをするのも自由です。次に挙げるアイデアをヒントにしてください。
- 簡単な単語の代わりにコードブロックを置換するよう変数を拡張する。sed関数でより複雑な正規表現を使いグループをキャプチャし実行する
- MVCテンプレートの新しいモデルのように、プロジェクトの種類に応じて特別なファイルを作るコマンドを追加する
- サーバーにプロジェクトを展開するコマンドを追加する。SSH経由のrsyncやリモートコマンドのライブラリーで実現できる
- 複雑な設定なら、静的なアセットやソースファイルを作るコマンドを加える。静的なサイトの場合とても便利
- 変数の名前からファイル名を変更するmv関数を使う
最後に
慣れ親しんだ環境で手早く新しいプロジェクトを始めるためのCLIを作りました。今回のプロジェクトに限定されるツールではありません。必要なら拡張もできます。自動化ツールを自作できるのは開発者の特権です。繰り返しのタスクがあれば、自動化できないか考えてみてください。多くの場合は可能で、長期的な恩恵はとても大きいでしょう。
本記事はJoan Yin、Camilo Reyes、Tim Severienが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Create Your Own Yeoman-Style Scaffolding Tool with Caporal.js)
[翻訳:関 宏也/編集:Livit]