FuseBox × Babel ~ Sparky で Gulp のようにタスクを定義する

wakamsha
31

FuseBox は単なるモジュールバンドラーの枠に留まらず、 Sparky というタスクランナーを搭載しています1)名称が似てますが、 node-sparky とは別モノです。。これにより、複雑なバンドルタスクでも可読性を損なうことなく、まるで Gulp のように記述することが出来ます。

今回は Sparky の基本的な使い方と、これを活用して FuseBox と Babel を組合せたモジュールバンドル & ECMAScript5 トランスパイル環境を構築する方法をご紹介します。

Sparky の特徴

  • はじめから FuseBox に搭載されているので、別途インストールする必要なし
  • シンプルで直感的な API
  • Gulp と非常によく似た記法
    • Gulp を触ったことがあれば学習コストはほぼゼロ
  • 並列実行 ( Parallel モード ) だけでなく直列・順次実行 ( waterfall モード ) も可能
  • Promise はもちろん async / await 構文にも対応

前回のエントリ で FuseBox は 多機能であると説明しました。まさかタスクランナー機能まで搭載しているとは驚きです。ファイル・ディレクトリ操作といった基本的なタスクはもちろん、プラグインを組み合わせることで非常に複雑かつ高度なタスクも定義出来ます。Sparky の API はとても直感的であるだけでなく、タスクの記述式が Gulp のそれと非常によく似ているので、多少なりとも Gulp を触ったことのある方であれば容易に習得できます。

加えて Sparky はタスクを並列だけでなく直列・順次実行する機能をはじめから備えています。Gulp でこれを実現するには Ver.4 ( beta 版 ) を使うか run-sequence プラグインを別途インストールしなくてはなりませんが、Sparky はそれすら必要ないのです。

更に Promise や async / await も普通に使うことが出来るため、例えば以下のようなタスクの定義も可能です。

const {Sparky} = require('fuse-box');

// Promise を使ったパターン
Sparky.task('foo', () => {
	return new Promise((resolve, reject) => {
		setTimeout(() => resolve(), 1000);
	});
});

// async / await を使ったパターン
Sparky.task('foo', async() => await someAsynchronousFunction());

基本的なファイル・ディレクトリ操作

まずは簡単なタスクからということで、任意のディレクトリを空にするタスクとファイルをコピーするタスクを定義してみましょう。

はじめに Sparky を fuse-box から require します。

const {Sparky} = require('fuse-box');

次にタスクを定義します。

// ディレクトリを空にする
Sparky.task('clean', () => Sparky.src('public').clean('public'));

// ファイルをコピーする
Sparky.task('copy', () => Sparky.src('src/images/**/*.(jpg|png|gif|svg)').dest('public/assets'));

いかがでしょう?見事なまでにGulp と殆ど同じかそれ以上にシンプルな書き方です。task() メソッドの第一引数にタスク名を指定し、第二引数にその実行内容である関数を指定します。src() メソッドは実行対象となるソースを指定しますが、Gulp と同じように正規表現で記述可能です。最後に dest()メソッドでアウトプット先を指定します。一連の流れをチェーンメソッドで記述出来るのも Gulp 風というか Node.js らしくて良いですね。

ちなみに src() の代わりに watch() メソッドを使うとSparkyは指定したソースを監視するようになり、そこに変更が入るたびに処理が行われます。

タスクを実行

FuseBox は他のバンドラーのような cli コマンドを持たないため、Node.js のコマンドから実行するわけですが、この時に Sparky のタスク名を引数に渡すとそのタスクが実行されます。

# clean タスク実行

$ node fuse.js clean
 
 Launch "clean"
# copy タスク実行

$ node fuse.js copy

 Launch "copy"
→Watch src/images/**/*.(jpg|png|gif|svg)
→Copy to public/assets/
→Resolved 2 files

default という特別な名前でタスクを定義すると、node コマンドにタスク名を引数に渡さずとも実行できます。このあたりも Gulp や Grunt と全く同じですね。

base オプション

先ほどの copy タスクを実行すると src/ ごと public/assets/ フォルダ内にコピーされてしまいます。

public/
└── assets/
    └── src/
        ├── icon.png
        └── icon.png

通常であれば images/フォルダから下を assets 以下にコピーしたいと思うでしょう。そんな時は base オプションを指定します。

Sparky.src('images/**/*.(jpg|png|gif|svg)', {
  base: 'src'
}).dest('public/assets')

こうすることでベースとなるパスより下のディレクトリを解決対象にできます。

public/
└── assets/
    ├── icon.png
    └── logo.jpg

Parallel モードと Waterfall モード

Sparky は責務ごとにタスクを細かく定義し、それらをまとめて呼び出し実行するという書き方が推奨されています。Sparky には複数のタスクを並列で実行する Parallel モードと直列に順次実行する Waterfall モードの2種類が用意されています。

Parallel モードはその名の通り複数のタスクを並列で非同期に実行します。各タスクはお互いの処理完了に依存しないため、『画像ファイルのコピー』と『TypeScript のトランスパイル』といった依存関係のないタスクを同時に捌きたい時などに有用です。

Waterfall モードはタスクを定義した順序に基づいて実行します。『出力先フォルダを初期化』してから『画像ファイルのコピー』をしたいといった実行順序が重要な時に有用です。

それぞれのタスクの定義の仕方は以下の通り。

// public フォルダを初期化してから画像ファイルをコピーする
Sparky.task('waterfall', ['reset', 'copy'], () => {
	// 上記タスク完了後に実行したいタスクをここに定義
});

// 画像ファイルのコピーと TypeScript のトランスパイルを同時に行う
Sparky.task('parallel', ['&copy', '&script'], () => {
	// 上記タスクと並列に実行したいタスクをここに定義
});

task() メソッドの第二引数に呼び出したいタスク名を配列で渡します。タスク名をそのまま渡すと Waterfall モードで実行され、接頭辞に&を付けると Parallel モードで実行されます。

development 用 / production 用のタスクを定義する

前回のエントリでは yargs を使って fuse.js にオプション値を渡すことで development 用 / production 用 の処理を分ける方法をご紹介しましたが、Sparky でそれぞれのタスクを定義することで同様のことが実現出来ます。

今回それぞれの処理で行いたい内容は以下の通り。

development 用 production 用
Source Map
Cache
Watch
Minify
ECMAScript5 変換

それでは順を追って説明します。

uglify-es をインストール

production 用ではファイル圧縮 ( minify ) をするため uglify-es を使います。yarn addコマンドでインストールしておきましょう。

$ yarn add -D uglify-es

ファイル圧縮のための npm-scripts を別途定義する必要はありません。こいつは FuseBox から直接呼び出すことが出来るのです。

Babel をインストールして ECMAScript 5 に対応させる

TypeScript でコーディングする方の多くは ECMAScript 2015+ の仕様で記述されることでしょう。使用する JS フレームワークによっては ECMAScript 2015+ の仕様のまま JavaScript にトランスパイルすることもありますが、これでは Internet Explorer 11 など古いブラウザでは動作しません。そのため Babel を使って ECMAScript5 へ更にトランスパイルすることが求められます。

uglify-es 同様、yarn addコマンドで Babel 本体とその関連モジュールをインストールしします。

$ yarn add -D babel-core babel-preset-es2015

これらのモジュールも FuseBox から直接呼び出すことが出来ます。

バンドルタスクを定義する

まずは基本となる部分を記述します。

const {FuseBox, Sparky} = require('fuse-box');

let fuse;

Sparky.task('config', () => {
  fuse = FuseBox.init({
    homeDir: 'src/scripts',
    output: 'public/assets/$name.js',
    tsConfig: 'tsconfig.json'
  });
  app = fuse.bundle('app').instructions('> main.ts');
});

Sparky.task('default', ['config'], () => {
  app.watch();
  return fuse.run();
});

Sparky.task('prod', ['config'], () => {
  return fuse.run();
});

development 用は default タスク、production 用は prod タスクとして定義しました。FuseBox の初期設定は config タスクとして切り出し、default、production タスクから Waterfall モードで呼び出します。watch 機能は development 時のみ有効化すれば良いので、default タスク内で実行しています。

production 用フラグを追加して処理を分岐

isProduction という Boolean 型の変数を用意し、これを true にするタスクを prod タスクから呼び出します。

let isProduction = false;

Sparky.task('set-production', () => {
  isProduction = true;
});
⋮
Sparky.task('prod', ['set-production', 'config'], () => {
  return fuse.run();
});

config タスクより先に set-production タスクを実行することにより、このフラグの値に応じて config タスクの設定を動的に変更出来るようになります。

Sparky.task('config', () => {
  fuse = FuseBox.init({
    homeDir: 'src/scripts',
    output: 'public/assets/$name.js',
    tsConfig: 'tsconfig.json',
    sourceMaps: !isProduction,
    cache: !isProduction
  });
  app = fuse.bundle('app').instructions('> main.ts');
});

FuseBox から uglify-es を実行出来るようにする

FuseBox には uglify-es を実行するためのプラグインが予め搭載されており、それを使って production 用にファイル圧縮を行います。

const {FuseBox, Sparky, UglifyESPlugin} = require('fuse-box');
⋮
Sparky.task('config', () => {
  fuse = FuseBox.init({
    homeDir: 'src/scripts',
    output: 'public/assets/$name.js',
    tsConfig: 'tsconfig.json',
    sourceMaps: !isProduction,
    cache: !isProduction,
      plugins: [
        isProduction && UglifyESPlugin()
      ]
  });
  app = fuse.bundle('app').instructions('> main.ts');
});

プラグインは FuseBox.init() に渡す引数の plugins パラメータに配列として渡すことで適用されます。配列に渡す値はプラグインの実行結果です。UglifyESPlugin を使うことで FuseBox から uglify-es を実行出来るようになります。

類似するものに Uglify JS プラグイン がありますが、こちらは ES5 までの JavaScript コードにしか対応しておらず、ES2015 + のコードを扱うには UglifyES プラグインを使う必要があります。

FuseBox から Babel を実行出来るようにする

uglify-es 同様、Babel を直接実行するためのプラグインも搭載されています。fuse.js に更に追記します。

const {FuseBox, Sparky, UglifyESPlugin, BabelPlugin} = require('fuse-box');
⋮
Sparky.task('config', () => {
  fuse = FuseBox.init({
    homeDir: 'src/scripts',
    output: 'public/assets/$name.js',
    tsConfig: 'tsconfig.json',
    sourceMaps: !isProduction,
    cache: !isProduction,
    plugins: isProduction
      ? [ UglifyESPlugin(), BabelPlugin({presets: ['es2015']}) ]
      : []
  });
  app = fuse.bundle('app').instructions('> main.ts');
});

BabelPlugin を使うことで FuseBox から Babel を直接実行することが出来ます。今回はシンプルに ECMAScript5 へのトランスパイルだけですが、他にも様々な機能を呼び出すことが可能です。このあたりの仕組みは Gulp とほぼ同じですね。

タスクを実行

Node.js コマンドからそれぞれのタスクを実行します。development 用は default タスクとして定義しているので、オプションの指定は不要です。

# development 用
$ node fuse.js

# production 用
$ node fuse.js prod

ちなみに前回のエントリでご紹介した Angular の公式チュートリアル ( Angular – Tutorial: Tour of Heroes ) のファイルを圧縮してみたところ、このような結果となりました。

development 用 production 用
ファイルサイズ 3,477 KB 1,151 KB
所要時間 3.28 s 19.12 s

Angular 自体のファイルサイズが非常に大きいため圧縮・トランスパイルに相応の時間がかかりましたが、その分効果は抜群のようです。サイズを 3分の1以下にまで圧縮出来ました。これなら実用にも充分耐えられそうですね。

締め

Sparky を活用することで処理内容に応じたタスクの定義を簡単に記述できるようになりました。多機能であるがゆえについついたくさんのタスクを定義して FuseBox だけで完結したくなりがちですが、いくら Gulp のように簡単に書けるとはいえ何でもかんでも詰め込んでしまうと後々保守が困難になります。使いどころを見極め、用法用量を守って割り切った使い方を心がけることが大切です。

脚注   [ + ]

1. 名称が似てますが、 node-sparky とは別モノです。