Node.js
gulp
Node.jsDay 24

gulp4.0 migration guide

More than 2 years have passed since last update.

背景

ひと昔前までフロント側build toolと言えば grunt が主流でしたが、今は gulp が主流になりました。

gulpは現在のところ3.9が最新ですが、水面下では4.0が準備中です。
4.0は久しぶりの major version release だけあって、結構な breaking changes が含まれる予定です。

4.0がいつリリースされるかは分かりませんが、 4.0 branch は用意されていて、そのほとんどの機能はもう動かすことができます。

本エントリではchangelogを見ながら、その機能を実際に試していって、どういったものか説明していこうと思います。

今回書いたコードはgithubにあげたので、もし試したい方がいればどうぞ。

資料

バージョン

$ ./node_modules/gulp/node_modules/gulp-cli/bin/gulp.js -v
[05:42:31] Requiring external module babel-core/register
[05:42:31] CLI version 0.4.0
[05:42:31] Local version 4.0.0-alpha.2

最初の準備

適当なディレクトリを作って、下記コマンドでgulp#4.0をinstallします。
今回はes6でgulpfileを書くので、babel-coreも一緒にinstallしておきます。

npm init -y
npm i git://github.com/gulpjs/gulp.git#4.0 --save-dev
npm i babel-core --save-dev

最初のタスクを作ってみる

いつも通りこんなタスクを書いて実行してみます。
(今回はローカルに入れているので、node-modules以下にinstallしたgulp.jsを直接叩くことにします。)

gulpfile.babel.js
const gulp = require('gulp');

gulp.task('task:first', () =>
  console.log('task:first done!')
);

実行するとbuildタスクが終わってないよ!と言われました。

$ ./node_modules/gulp/bin/gulp.js task:first
[21:04:07] Requiring external module babel-core/register
[21:04:07] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[21:04:07] Starting 'task:first'...
task:first done!
[21:04:07] The following tasks did not complete: task:first
[21:04:07] Did you forget to signal async completion?

今まで待ち合わせる必要のない非同期タスクは雑に書いていましたが、そういうタスクの実行時には警告が表示されるようになったようです。
streamを返却するか、↓のように、渡されてくるcallbackを実行すると解消します。
(streamを返却しない場合にはcallbackを実行することで、gulpにtaskの終了を伝えられます。)

gulpfile.babel.js
gulp.task('task:first', (cb) => {
  console.log('task:first done!');
  cb();
});

ここからchangelogの内容に沿っていってみます。

Task system changes

gulp.task: removed 3 argument syntax

gulp.taskはこれまで、第2引数にタスク名のstring(or string配列)を渡すことで、依存関係を定義することができました。

gulp.task('build', 'clean', function() {
  console.log('この場合はbuildタスクの前にcleanタスクが実行される');
});

しかし、4.0から引数を3つ渡すAPIは廃止になりました。
代わりに依存関係の定義は、後述のgulp.seriesgulp.parallelを使って行います。

gulp.task: added single argument syntax

gulp.taskに引数が1つだけのsyntaxが追加されました。
この引数は名前付きfunctionである必要があります。
どういうことかいまいちよく分からなかったので、ソースコードを読んでみました。

https://github.com/gulpjs/undertaker/blob/v0.13.0/lib/task.js#L6

どうやら名前付きfunctionを渡すと、その名前がそのままtaskとして登録されるようです。

定義してみます。

gulpfile.babel.js
function functionalTask(cb) {
  console.log('functionalTask done!');
  cb();
}

gulp.task(functionalTask);

実行してみます。

$ ./node_modules/gulp/bin/gulp.js functionalTask
[21:43:50] Requiring external module babel-core/register
[21:43:50] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[21:43:50] Starting 'functionalTask'...
functionalTask done!
[21:43:50] Finished 'functionalTask' after 1.47 ms

確かに実行できました。

だけど、個人的には名前付きfunctionをgulpfileの中で定義することがないので嬉しさがよく分からない。。

added gulp.series and gulp.parallel methods for composing tasks

changelogでは、「Everything must use these now.」と続きます。
つまり全員必ず使え

gulpはこれまで、タスク処理機構に orchestrator というnpmを使っていました。
しかし、4.0から undertaker というnpmに変更されます。
それにより、これまでrun-sequense等で賄っていたstreamの平行処理、直列化がgulp本体のAPIで行えるようになります。

gulp.parallel

タスクを並列化します。
(わかりやすいように'task:second'は setTimeout で処理を遅らせています。)

gulpfile.babel.js
gulp.task('task:first', (cb) => {
  console.log('task:first done!');
  cb();
});

gulp.task('task:second', (cb) => {
  setTimeout(() => {
    console.log('task:second done!');
    cb();
  }, 1000);
});

gulp.task('task:third', (cb) => {
  console.log('task:third done!');
  cb();
});

gulp.task('task:composing',
  gulp.parallel('task:first', 'task:second', 'task:third')
);

実行してみます。

$ ./node_modules/gulp/bin/gulp.js task:composing
[22:13:10] Requiring external module babel-core/register
[22:13:10] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[22:13:10] Starting 'task:composing'...
[22:13:10] Starting 'task:first'...
[22:13:10] Starting 'task:second'...
[22:13:10] Starting 'task:third'...
task:first done!
[22:13:10] Finished 'task:first' after 1.23 ms
task:third done!
[22:13:10] Finished 'task:third' after 1.92 ms
task:second done!
[22:13:11] Finished 'task:second' after 1 s
[22:13:11] Finished 'task:composing' after 1.01 s

平行にtaskが処理されて、secondが一番最後に終了してますね。

gulp.series

タスクを直列化します。
先ほどの'task:composing'を、gulp.seriesに変えます。

gulpfile.babel.js
gulp.task('task:composing',
  gulp.series('task:first', 'task:second', 'task:third')
);

実行してみます。

$ ./node_modules/gulp/bin/gulp.js task:composing
[22:21:42] Requiring external module babel-core/register
[22:21:42] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[22:21:42] Starting 'task:composing'...
[22:21:42] Starting 'task:first'...
task:first done!
[22:21:42] Finished 'task:first' after 879 μs
[22:21:42] Starting 'task:second'...
task:second done!
[22:21:43] Finished 'task:second' after 1 s
[22:21:43] Starting 'task:third'...
task:third done!
[22:21:43] Finished 'task:third' after 369 μs
[22:21:43] Finished 'task:composing' after 1.01 s

直列にtaskが処理されるので、前タスクの終了を待って次タスクが実行されていますね。

合わせて使う

first、second、thirdタスクは平行に処理し、終了を待ち合わせてログ出力するようにします。

gulpfile.babel.js
gulp.task('task:composing',
  gulp.series(
    gulp.parallel('task:first', 'task:second', 'task:third'),
    (cb) => {
      console.log('task:composing done!');
      cb();
    }
  )
);

実行してみます。

$ ./node_modules/gulp/bin/gulp.js task:composing
[22:30:04] Requiring external module babel-core/register
[22:30:04] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[22:30:04] Starting 'task:composing'...
[22:30:04] Starting 'parallel'...
[22:30:04] Starting 'task:first'...
[22:30:04] Starting 'task:second'...
[22:30:04] Starting 'task:third'...
task:first done!
[22:30:04] Finished 'task:first' after 842 μs
task:third done!
[22:30:04] Finished 'task:third' after 1.63 ms
task:second done!
[22:30:05] Finished 'task:second' after 1 s
[22:30:05] Finished 'parallel' after 1 s
[22:30:05] Starting '<anonymous>'...
task:composing done!
[22:30:05] Finished '<anonymous>' after 352 μs
[22:30:05] Finished 'task:composing' after 1.01 s

added gulp.tree method

gulp.tree メソッドで、タスクの依存関係が取得できるようになりました。

gulpfile.babel.js
gulp.task('tree', (cb) => {
  console.log(gulp.tree({ deep: true }));
  cb();
});
$ ./node_modules/gulp/bin/gulp.js tree
[22:39:27] Requiring external module babel-core/register
[22:39:28] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[22:39:28] Starting 'tree'...
{ label: 'Tasks',
  nodes:
   [ { label: 'functionalTask', type: 'task', nodes: [] },
     { label: 'task:first', type: 'task', nodes: [] },
     { label: 'task:second', type: 'task', nodes: [] },
     { label: 'task:third', type: 'task', nodes: [] },
     { label: 'task:composing', type: 'task', nodes: [Object] },
     { label: 'tree', type: 'task', nodes: [] } ] }
[22:39:28] Finished 'tree' after 3.65 ms

added gulp.registry

カスタムタスクを登録します。これを使うと、外部ファイルに定義したタスクを読み込んで、登録することができるようになります。

例を見るのがわかりやすいです。

https://github.com/gulpjs/gulp/blob/4.0/docs/API.md#gulpregistryregistry

今回はあえてes6-classesを使って書いてみました。

custom_registory.babel.js
'use strict';

const gulp            = require('gulp');
const DefaultRegistry = require('undertaker-registry');

class MyCustomTask extends DefaultRegistry {
  init() {
    gulp.task('task:custom', (cb) => {
      console.log('task:custom done!');
      cb();
    });
  }
};

module.exports = new MyCustomTask();
gulpfile.babel.js
const customTask = require('./custom_registory.babel.js');

gulp.registry(customTask);

実行してみます。

$ ./node_modules/gulp/bin/gulp.js task:custom
[23:13:34] Requiring external module babel-core/register
[23:13:34] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[23:13:34] Starting 'task:custom'...
task:custom done!
[23:13:34] Finished 'task:custom' after 1.5 ms

CLI changes

--tasks-json

定義しているtaskをjson形式で吐き出します。

$ ./node_modules/gulp/bin/gulp.js --tasks-json
[21:22:19] Requiring external module babel-core/register
{"label":"Tasks for ~/src/try-gulp4/gulpfile.babel.js","nodes":[{"label":"task:first","type":"task","nodes":[]}]}

--verify

gulpには使用を推奨されないpluginをblacklist化して登録しておく場所があります。
このオプションをつけて実行するとpackage.jsonを検査してblacklistに乗っているpackageが使われていないか検査してくれます。

$ ./node_modules/gulp/bin/gulp.js --verify
[21:24:02] Requiring external module babel-core/register
[21:24:02] Verifying plugins in /Users/joe-re/src/try-gulp4/package.json
[21:24:03] There are no blacklisted plugins in this project

vinyl/vinyl-fs changes

added gulp.symlink

gulp.symlinkというAPIが追加されました。
これは gulp.dest と同じ感覚で使えますが、ファイルを出力するのではなくsymlinkを生成します。

まず適当にファイルを作ります。

$ mkdir src
$ touch src/test.js
$ echo "console.log('test');" > src/test.js

taskを定義します。

gulpfile.babel.js
gulp.task('make:symlink', () =>
  gulp.src('./src/test.js').
    pipe(gulp.symlink('dist'))
);

(es6で書くと、ブロック付けない場合は評価値が返り値になるので、大抵は1つのstreamしか書かないgulpのタスクにおいてはreturn書かずに済んで相性がいいですね。)

実行してみます。

$ ./node_modules/gulp/bin/gulp.js make:symlink
[02:02:40] Requiring external module babel-core/register
[02:02:40] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[02:02:40] Starting 'make:symlink'...
[02:02:40] Finished 'make:symlink' after 19 ms

結果を見ます。

$ ls -l dist
total 8
lrwxr-xr-x  1 joe-re  staff  46 Nov 30 03:07 test.js -> /Users/joe-re/src/try-gulp4/src/test.js

added dirMode param to gulp.dest and gulp.symlink

gulp.destとgulp.symlinkのオプションでdirModeというのが追加されました。
これは作成するディレクトリのパーミッションを選択できます。

先ほどの'make:symlink'タスクで試してみます。

gulpfile.babel.js
gulp.task('make:symlink', () =>
  gulp.src('./src/test.js').
    pipe(gulp.symlink('dist', { dirMode: 0o711 }))
);

実行して、結果を見ます。

$ ./node_modules/gulp/node_modules/gulp-cli/bin/gulp.js make:symlink
[03:38:47] Requiring external module babel-core/register
[03:38:47] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[03:38:47] Starting 'make:symlink'...
[03:38:47] Finished 'make:symlink' after 17 ms
$ ls -l
total 32
#.. 省略
drwx--x--x   3 joe-re  staff   102B Nov 30 03:38 dist

確かに0711でパーミッション設定されてますね。

globs passed to gulp.src will be evaluated in order

gulp.srcにglobで指定した順番がちゃんと評価されるようになりました。

bから始まるファイルを除き、.jsの拡張子のファイルを全て対象にしてstreamに呼び込む。ただし、bad.jsは対象にする。
という指定をしたい場合は以下になります。

gulpfile.babel.js
gulp.task('task:order', () =>
  gulp.src([ './src/*.js', '!./src/b*.js', './src/bad.js' ]).
    pipe(gulp.dest('dist'))
);

src以下にbad.js、bar.js、test.jsを作ります。

$ ls -l src
-rw-r--r--  1 joe-re  staff  20 Dec 21 03:17 bad.js
-rw-r--r--  1 joe-re  staff  20 Dec 21 03:17 bar.js
-rw-r--r--  1 joe-re  staff  21 Nov 30 03:07 test.js

実行して、結果をみます。

$ ./node_modules/gulp/node_modules/gulp-cli/bin/gulp.js task:order
[03:44:00] Requiring external module babel-core/register
[03:44:00] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[03:44:00] Starting 'task:order'...
[03:44:00] Finished 'task:order' after 35 ms
$ ls -l dist
total 16
-rw-r--r--  1 joe-re  staff  20 Dec 21 03:17 bad.js
-rw-r--r--  1 joe-re  staff  21 Nov 30 03:07 test.js

確かにbad.js意外のb始まりのファイル(bar.js)は対象から外れていますね。

performance for gulp.src has improved massively

gulp.srcの消費するCPUリソースが激減したようです。
これはAPIへの変更ではないので、今回は割愛します。
そのうち、検証してみようかなーと思います。

added since option to gulp.src

gulp.srcにsinceオプションが追加されました。
これはインクリメンタルビルドをサポートするための、オプションで、sinceにunix timeを渡すと、その時刻以降に変更されているファイルのみを対象とするようになります。
加えて、gulp.lastRunというAPIも追加されていて、これは指定したタスクの最後の実行時刻を取得できます。

これらを組み合わせることで、これまでgulp-cacheなどで実現していたインクリメンタルビルドを、gulp本体のみで行うことができるようになります。
(詳しくは4.0のREADMEに書いてあります。)

実際にタスクを定義して実行してみます。
incremental buildがちゃんとされているか見るため、streamの中身をgulp-debugを利用して出力するようにしました。

gulp.task('task:incrementalbuild', () =>
  gulp.src('./src/*.js', { since: gulp.lastRun('task:incrementalbuild') }).
    pipe(debug()).
    pipe(gulp.dest('dist'))
);

gulp.task('watch', () =>
  gulp.watch('./src/*.js', gulp.series('task:incrementalbuild'))
);

gulp.watchを実行後、何回かsrc以下のファイルを修正してみます。

$ ./node_modules/gulp/node_modules/gulp-cli/bin/gulp.js watch
[04:36:57] Requiring external module babel-core/register
[04:36:57] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[04:36:57] Starting 'watch'...
[04:37:02] Starting 'series'...
[04:37:02] Starting 'task:incrementalbuild'...
[04:37:02] gulp-debug: src/bad.js
[04:37:02] gulp-debug: src/bar.js
[04:37:02] gulp-debug: src/test.js
[04:37:02] gulp-debug: 3 items
[04:37:02] Finished 'task:incrementalbuild' after 41 ms
[04:37:02] Finished 'series' after 43 ms
[04:37:06] Starting 'series'...
[04:37:06] Starting 'task:incrementalbuild'...
[04:37:06] gulp-debug: src/bar.js
[04:37:06] gulp-debug: 1 item
[04:37:06] Finished 'task:incrementalbuild' after 13 ms
[04:37:06] Finished 'series' after 14 ms
[04:37:11] Starting 'series'...
[04:37:11] Starting 'task:incrementalbuild'...
[04:37:11] gulp-debug: src/bad.js
[04:37:11] gulp-debug: 1 item
[04:37:11] Finished 'task:incrementalbuild' after 11 ms
[04:37:11] Finished 'series' after 12 ms

初回は全ファイル(3items)対象になっていますが、2回目以降はファイルが変更のあったファイルのみ(1item)になっています。

fixed gulp.src not following symlinks

gulp.srcの対象から、symlinksは対象外になるようです。
やってみます。

まずは適当にsymlinkを作って、

$ touch symlink.js
$ ln -s symlink.js src/
$ ls -la src
total 32
drwxr-xr-x   6 joe-re  staff  204 Dec 21 04:52 .
drwxr-xr-x  11 joe-re  staff  374 Dec 21 04:51 ..
-rw-r--r--   1 joe-re  staff   20 Dec 21 04:37 bad.js
-rw-r--r--   1 joe-re  staff   20 Dec 21 04:37 bar.js
lrwxr-xr-x   1 joe-re  staff   10 Dec 21 04:52 symlink.js -> symlink.js
-rw-r--r--   1 joe-re  staff   21 Dec 21 04:37 test.js

先ほど作ったincremental build用のタスクにかけてみます。

$ ./node_modules/gulp/node_modules/gulp-cli/bin/gulp.js task:incrementalbuild
[04:43:19] Requiring external module babel-core/register
[04:43:19] Using gulpfile ~/src/try-gulp4/gulpfile.babel.js
[04:43:19] Starting 'task:incrementalbuild'...
[04:43:19] gulp-debug: src/bad.js
[04:43:19] gulp-debug: src/bar.js
[04:43:19] 'task:incrementalbuild' errored after 33 ms
[04:43:19] Error: ELOOP: too many symbolic links encountered, stat '/Users/joe-re/src/try-gulp4/src/gulpfile.babel.js'
    at Error (native)

あれれ、エラーになってしまいました。

vinyl-fsのREADMEを参考にして、gulp.srcのオプションにfollowSymlinks: false を追加すると、対象外になってくれました。

gulp.task('task:incrementalbuild', () =>
  gulp.src('./src/*.js', { since: gulp.lastRun('task:incrementalbuild'), followSymlinks: false }).
    pipe(debug()).
    pipe(gulp.dest('dist'))
);

どうもデフォルトで対象外になる、ということではないっぽいです。

vinyl-fsにfollowSymlinkオプションが追加されたのがこのコミットで、2015年8月14日。
READMEにこの部分が書かれたのがこのコミットで、2015年2月24日。
ということでREADMEの方が古いので、状況が変わったのかな?

とりあえずfollowSymlinkオプションで対象外にできることは分かったのでおっけー。

added overwrite option to gulp.dest

上書きを禁止するオプションがgulp.destに追加されました。

こんな感じでタスクを定義します。

gulpfile.babel.js
gulp.task('task:cantoverwrite', () =>
  gulp.src('./src/*.js', { followSymlinks: false }).
    pipe(debug()).
    pipe(gulp.dest('dist', { overwrite: false }))
);

こうしておくと、もうすでに出力ファイルが存在するときは上書きしません。(特にエラーになるわけではないです。)

おわり

冒頭でbreaking多いと言っておいてなんですが、実際にやってみるとそこまでは多くなかったですね。
gulp本体でstreamの直列・並列処理の実行や、incremental buildがサポートされるようになったのが大きいところでしょうか。
4.0 branchはもうほとんどちゃんと動くので、そろそろリリースされるかなー、と思います。

来るべき日のお役に立てば幸いです。