npm-scriptsでビルド環境を作る
By kkoudev
タスクランナーといえば近年 gulp が非常に人気が高く、利用者も未だに多いことかと思います。 しかし、バージョン4が何年経ってもリリースされない現状を垣間見ると、これ以上 gulp に依存するのは今後新しいアーキテクチャが出てきた際に追従できない可能性が出てきます。そこで、完全に gulp をやめてしまおうというのが今回の趣旨です。 しかし、gulpを無くしてしまった場合、今までのビルドしていたものはどうやってビルドするの?となるかと思います。 これは非常に単純な話で、例えば pug や babel にはビルドするための専用のCLIツールが配布されています。gulpのプラグインも実はこれを呼び出しているに過ぎないので、自作することは簡単なのです。
gulpに代わるものを作るためには
具体的にどのように実現するのかについては、npm-scriptsの機能を使って実現します。その上で、最低限必要だと思われる以下の必要なライブラリ(標準含む)を紹介します。
・npm-run-all
複数のタスクを並列実行することが出来ます。例えば「watch:pug」と「watch:postcss」というnpm-scriptsが定義されていた場合、「npm-run-all -p watch:*」と記述すると該当するタスクを一度に並列実行できます。(個々に指定してもOK)
・chokidar
ファイル監視(watch)を実現するために使用します。
ファイルの追加or変更時にビルド処理を呼び出すことができます。
・glob
言わずと知れた glob パターンでファイルの複数指定を行う場合に使用します。(例: *.css といった記述が可能)
大抵の場合、CLIツールもglobパターンの指定をサポートしているので、直接利用する必要がある場合とそうでない場合があります。
・chalk
コンソール文字列に簡単に色をつけることができます。
・node-notifier
エラーが出た際に通知を表示するのに利用します。
・nodemon
タスクを記述したJavaScriptが更新された際にプロセスを立ち上げ直してくれます。ファイル監視タスクにおいて、nodeコマンドの代わりに利用します。
・path
ファイルパスの操作を簡単に行えます。標準ライブラリです。
・fs-extra
fsの拡張版。例えば ファイル書き込み時に存在しないディレクトリを自動で作成してくれたりなど、地味に便利になった fs です。
タスクを実行&ファイル監視するための便利関数を作る
説明ばかりではイメージがつきにくいかと思うので、 普段私が利用しているタスクを実行したりファイル監視をするための便利関数群を紹介します。
/**
* @file タスク設定で利用する共通関数定義
*
* @author kkoudev
*/
const path = require('path');
const chokidar = require('chokidar');
const childProcess = require('child_process');
const chalk = require('chalk');
const notifier = require('node-notifier');
const moment = require('moment');
const log = console.log; // eslint-disable-line no-console
// コマンド実行で利用する環境変数を定義する
const usingEnv = Object.assign({}, process.env, {
FORCE_COLOR: true
});
// デフォルトタスク名
const defaultTaskName = path.basename(process.argv[1], '.js');
// コンソール時間フォーマット
const formatConsoleTime = 'HH:mm:ss';
// エラー通知ベース設定
const errorNotifyOptions = {
title: 'Error Occurred',
message: 'エラーが発生しました。コンソールの内容を確認してください',
sound: 'Basso',
};
/**
* 指定されたコマンドを実行する。
*
* @param {string} command コマンド文字列
* @param {object} [options] 実行オプション
* @param {function} [callback] コールバック関数
*/
exports.exec = (command, options, callback) => {
// 指定されたコマンドを実行する
childProcess.exec(
`${command}`,
{
env: usingEnv
},
(error, stdout, stderr) => {
// エラーがある場合
if (error) {
// エラー内容を通知する
notifier.notify(errorNotifyOptions);
}
(!options || !options.noError && error) && log(error);
(!options || !options.noStdout && stdout) && log(stdout);
(!options || !options.noStderr && stderr) && log(stderr);
callback && callback();
});
};
/**
* 指定されたターゲットを変更監視対象とする。
*
* @param {string} target 監視対象ファイルまたはディレクトリ
* @param {function} [callback] 監視追加変更時のコールバック関数
*/
exports.watch = (target, callback) => {
const watcher = chokidar.watch(
target,
{
persistent: true
})
.on('ready', () => {
// 追加と更新のイベント登録
watcher.on('add', (file) => {
callback && callback(file);
}).on('change', (file) => {
callback && callback(file);
});
});
};
/**
* 処理開始のコンソール文字列を出力する
*
* @param {string} [taskName] 実行タスク名
* @returns {object} 実行開始コンテキスト
*/
exports.consoleBegin = (taskName) => {
const beginTime = new Date(); // 開始日時
const usingTaskName = taskName || defaultTaskName; // 実行タスク名
// 実行開始情報をコンソールへ出力する
log(
`[${chalk.gray(moment(beginTime).format(formatConsoleTime))}]`,
`Beginning '${chalk.cyan(usingTaskName)}'....`
);
// 実行開始情報を返す
return {
taskName: usingTaskName,
beginTime,
};
};
/**
* 処理終了のコンソール文字列を出力する。
*
* @param {object} context 実行開始コンテキスト
*/
exports.consoleFinish = (context) => {
const finishedTime = new Date(); // 処理終了時間
const processMillis = finishedTime - context.beginTime; // 処理時間(ms)
// 実行終了情報をコンソールへ出力する
log(
`[${chalk.gray(moment(finishedTime).format(formatConsoleTime))}]`,
`Finished '${chalk.cyan(context.taskName)}' after ${chalk.magenta(processMillis)} ms`
);
};
/**
* ビルド処理と監視処理を行う。
*
* 環境変数の NODE_WATCH が true の場合にはビルドと監視処理を行い、
* それ以外の場合はビルドのみを行う。
*
* @param {string} watchTarget 監視対象ファイルまたはディレクトリ
* @param {(string|function)} buildCommand ビルドコマンドまたは処理関数。処理関数の場合は Promise を返却すること
* @param {object} [options] 実行オプション
*/
exports.watchBuilding = (watchTarget, buildCommand, options) => {
if (typeof buildCommand === 'function') {
const executeWrapper = () => {
const beginContext = exports.consoleBegin();
// ビルド処理を実行する
buildCommand(options).then(() => {
exports.consoleFinish(beginContext);
});
};
// ビルド処理を実行する
executeWrapper();
// 監視処理が有効な場合は監視処理を実行する
process.env.NODE_WATCH && exports.watch(watchTarget, executeWrapper);
} else {
const executeWrapper = () => {
const beginContext = exports.consoleBegin();
// ビルド処理を実行する
exports.exec(buildCommand, options, () => {
exports.consoleFinish(beginContext);
});
};
// ビルド処理を実行する
executeWrapper();
// 監視処理が有効な場合は監視処理を実行する
process.env.NODE_WATCH && exports.watch(watchTarget, executeWrapper);
}
};
各関数について簡単に説明すると
・exec
コマンドを実行するための関数
・watch
指定されたディレクトリまたはglobパターンで指定されたファイルを監視する関数
・watchBuilding
監視対象と実行コマンドを同時に指定することで、ファイル監視イベントが発生するごとに指定されたビルドコマンドを実行する関数 (基本これだけで完結できます)
タスクを作ってみる
それでは、試しに pug をビルドするタスクを作ってみます。
プロジェクト内に node_scripts/tasks/pug.js というファイルを用意し、上記の便利関数を定義したファイル (node_scripts/utils/functions.js とする) を合わせて利用して作成します。pugのcliを利用するために、「pug」と「pug-cli」を npm install もしくは yarn add しておきましょう。
const funcs = require('../utils/functions');
funcs.watchBuilding(
`./app/views`,
'pug ./app/views -o ./build -P',
{
noError: true
}
);
たったこれだけのコードでタスクの作成は完了です。
あとは npm-scripts として package.json に以下のように記述すればビルドタスクと監視タスクの両方を1つのスクリプトで実現できます。
"scripts": {
"build:pug": "node ./node_scripts/tasks/pug.js",
"watch:pug": "cross-env NODE_WATCH=true nodemon ./node_scripts/tasks/pug.js"
}
まとめ
以上で、gulpをやめてnpm-scriptsへ移行する方法を紹介しました。
便利関数さえ作ってしまえば gulp よりも完結に記述でき、かつオリジナルのCLIツールを呼び出してビルドが可能なので、gulpやそのプラグインの不具合に引っ張られることはありません。
PostCSSやwebpackについても同等の方法で簡単にタスクを作成できますので、興味がある方は是非試してみてください。