経緯
ひょんなことから gulp.src に第二引数で options としてオブジェクトを渡すことができることを知りまして。
その中の ignore キーを使って、 ! パターンよりも可読性の高そうな書き方に置換できないか、と試してみました。
しかし、 JavaScript の処理が上手く動かなくなってしまいました。
現象
現象としては、以下の状況に陥りました。
gulpでタスクを走らせる初回は正常にトランスパイルとブラウザ起動( Browsersync )が行われる.jsファイルに何らかの変更を加えて上書き保存をすると、.js関連のタスクと Browsersync のリロードが無限に繰り返される
.js ファイルへの処理
この .js ファイルへの処理は以下の処理を gulp.series でまとめたものになります。
- jQuery等のライブラリの
.jsファイルをgulp-concatで連結し、./src/js/concat/の中にlib.jsとして中間生成 ./src/js/concat/の中(サブディレクトリ下も含む)の.jsファイル(1.で生成されたlib.jsを想定)をgulp-uglify-esで uglify して、結果を./dist/js/の下にXXX.min.jsとして出力./src/js/の中(サブディレクトリ下も含む)の.jsファイルをgulp-uglify-esで uglify して、結果を./dist/js/の下にYYY.min.jsとして出力- ただし
./src/js/concat/は対象から除外
- ただし
browsersync.js
Browsersync から .js ファイルへ処理をキックするトリガー部分について、以前は以下のように記述されていました。
const gulp = require('gulp');
const watch = require('gulp-watch');
const browserSync = require('browser-sync');
const jsBuild = require('./js');
const browsersync = () => {
watch(
[
'./src/js/**/*.js',
'!./src/js/concat/**/*.js'
],
gulp.series(jsBuild, browserSync.reload)
);
};
module.exports = browsersync;
これを、以下のように書き換えたところ、上述の現象が発生するようになりました。
const gulp = require('gulp');
const watch = require('gulp-watch');
const browserSync = require('browser-sync');
const jsBuild = require('./js');
const browsersync = () => {
watch(
'./src/js/**/*.js',
{
ignore: './src/js/concat/**/*.js'
},
gulp.series(jsBuild, browserSync.reload)
);
};
module.exports = browsersync;
調査(不完全)
まずgulp-watch – npm(5.0.1) を見ると、 options の項目に以下のような記述があります。
This object is passed to the chokidar options directly. Options for gulp.src are also available.
gulp.src の options が使用可能、ということなので gulp.src のドキュメント(src() | gulp.js)を見ます。
| ignore | string array | Globs to exclude from matches. This option is combined with negated globs. Note: These globs are always matched against dot files, regardless of any other settings. This option is passed directly to node-glob. |
|---|
気になるのは2行目の記述。他の設定に関わらず dot files との照合が常に行われるようになる、とのこと。
また、 This option is passed directly to node-glob. とも書いてあります。ということで、リンク先の node-glob のドキュメントを見ます。
ignoreAdd a pattern or an array of glob patterns to exclude matches. Note:ignorepatterns are always indot:truemode, regardless of any other settings.
先ほどの「他の設定に関わらず dot files との照合が常に行われるようになる」の正体が書いてあります。 dot オプションが常に true として動作するようになる、とのことです。
それでは、この dot オプションの説明はと言うと……。
dotInclude.dotfiles in normal matches andglobstarmatches. Note that an explicit dot in a portion of the pattern will always match dot files.
これを読む限り、.gitignore のような dot files を通常のマッチと globstar (/**/)でマッチするように動作する、ということで、普通のファイル(app.js のような普通のファイル名のファイル)は影響を受けなさそうな感じがします。
しかし、 gulp.src の dot キーについて見てみると
| dot | boolean | false | If true, compare globs against dot files, like .gitignore. This option is passed directly to node-glob. |
|---|
とあるので、 node-glob の options の説明と照合すると gulp.src の ignore キーに指定を行うと、 dot キーがデフォルトの false の挙動から、 node-glob 側では true の挙動に置き換わるのではないか、と思われます。
他に挙動が変化する、という説明はざっとドキュメントを眺めた感じでは見付けられなかったので、現状ではここの挙動の変化が「 ./src/js/ の中(サブディレクトリ下も含む)の .js ファイルを gulp-uglify-es で uglify して、結果を ./dist/js/ の下に YYY.min.js として出力 (ただし ./src/js/concat/ は対象から除外)」の挙動に影響を及ぼしているのではないか……と考えています。
しかし、一見すると影響はなさそうなオプションなので断定できず、かといって今後また同じところで嵌まるかもしれないので「不完全メモ」とさせていただきます。
備考1
gulp-watch は上述の通りダメでしたが、 gulp.src の options として ignore があるならば、 gulp デフォルトで持っている gulp.watch ならばどうでしょうか。
| ignored | array string RegExp function array | Defines globs to be ignored. If a function is provided, it will be called twice per path – once with just the path, then with the path and the fs.Statsobject of that file. This option is passed directly to chokidar.. |
|---|
……こちらは ignore ではなく ignored というキーのようです。紛らわしい。
これを使って以下のように書き換えます。
const gulp = require('gulp');
const browserSync = require('browser-sync');
const jsBuild = require('./js');
const browsersync = () => {
gulp.watch(
'.src/js/**/*.js',
{
ignored: './src/js/concat/**/*.js'
},
gulp.series(jsBuild, browserSync.reload)
);
};
module.exports = browsersync;
これで直った……かと思ったら、初回実行、1度目の上書きまでは動作しますが、2回目以降の上書きでは動作しませんでした。……惜しい!
ただし、無限ループは止まったので ignored キーはしっかりと効いているようです(最終結果は末尾に付記します)。
最初に立ち戻って gulp-watch の options を見ると
This object is passed to the chokidar options directly. Options for gulp.src are also available.
とありました。 gulp.watch も This option is passed directly to chokidar. とあるように、使用しているのは同じ chokidar のはずです。
バージョンの差異なのか、あるいは呼び出し元のプログラムの挙動の違いなのか……この挙動の違いを見るために、2つのコードを見比べみます。
gulp-watch
まずは gulp-watch 。
// 略
function watch(globs, opts, cb) {
var originalGlobs = globs;
globs = normalizeGlobs(globs);
// 略
var baseForced = Boolean(opts.base);
// 略
var watcher = chokidar.watch(globs, opts);
// 略
}
chokidar に対して引数で渡された globs と opts を渡しています。
双方ともに多少の処理が入っていますが、 opts の方を見ると cwd, base, events 等が対象になっています。が、 ignored については触れていません。
気になるのは先ほどのキー名で、 gulp-watch は gulp.src の options も使用可能ということで ignore キーとして指定(これは引き渡し先が node-globs の options のため)していましたが、 chokidar は ignored なのですよね。
試しに
const gulp = require('gulp');
const watch = require('gulp-watch');
const browserSync = require('browser-sync');
const jsBuild = require('./js');
const browsersync = () => {
watch(
'.src/js/**/*.js',
{
ignored: './src/js/concat/**/*.js'
},
gulp.series(jsBuild, browserSync.reload)
);
};
module.exports = browsersync;
と書き換えても現象は変化しませんでした。やはり上手く効いていない模様。
gulp.watch
続いて gulp.watch 。
// 略
var defaultOpts = {
delay: 200,
events: ['add', 'change', 'unlink'],
ignored: [],
ignoreInitial: true,
queue: true,
};
// 略
function watch(glob, options, cb) {
if (typeof options === 'function') {
cb = options;
options = {};
}
var opt = defaults(options, defaultOpts);
// 略
// We only do add our custom `ignored` if there are some negative globs
// TODO: I'm not sure how to test this
if (negatives.some(exists)) {
var normalizedPositives = positives.map(joinCwd);
var normalizedNegatives = negatives.map(joinCwd);
var shouldBeIgnored = function(path) {
var positiveMatch = anymatch(normalizedPositives, path, true);
var negativeMatch = anymatch(normalizedNegatives, path, true);
// If negativeMatch is -1, that means it was never negated
if (negativeMatch === -1) {
return false;
}
// If the negative is "less than" the positive, that means
// it came later in the glob array before we reversed them
return negativeMatch < positiveMatch;
};
opt.ignored = [].concat(opt.ignored, shouldBeIgnored);
}
var watcher = chokidar.watch(toWatch, opt);
// 略
}
chokidar に toWatch で globs, opt で options を渡しています。
特に ignored についての処理がありますが、これは negative globs (! で始まる globs )がある場合にそれを ignored キーで指定したオブジェクトと連結しているようです。
そのため、 negative globs を使用していない今回では影響はなさそう。
……そう考えると、決定的な違いが分からなくなってきました。
コードの中身が全く別物なので挙動として異なる挙動をしてもおかしくはなさそう、とは言えそうですが。
備考2
備考1であと一歩のところまで来た感じがあります。
ただ、2回目以降のイベントが上手く拾えない……というところで chokidar のドキュメント等を見た結果、 on というイベントハンドラーがあることに気付きました。
これを利用した結果が以下。
const gulp = require('gulp');
const watch = require('gulp-watch');
const browserSync = require('browser-sync');
const jsBuild = require('./js');
const browsersync = () => {
gulp.watch(
'.src/js/**/*.js',
{
ignored: './src/js/concat/**/*.js'
}
)
.on('add' , gulp.series(jsBuild, browserSync.reload))
.on('change', gulp.series(jsBuild, browserSync.reload))
.on('unlink', gulp.series(jsBuild, browserSync.reload));
};
module.exports = browsersync;
これで試した結果、ようやく意図した挙動になりました。……長かった。
余談1
gulp-watch でも gulp.watch でも登場した chokidar について見てみると、これは fs のファイル監視をラップしたパッケージとのことです。
名前がよく分からない感じでしたが、 chokidar のドキュメントの最後に
Why was chokidar named this way? What’s the meaning behind it?
Chowkidar is a transliteration of a Hindi word meaning ‘watchman, gatekeeper’, चौकीदार. This ultimately comes from Sanskrit _ चतुष्क_ (crossway, quadrangle, consisting-of-four).
とありました。サンスクリットで「警備員・夜警・番人・門番(英語の watchman, gatekeeper )」の意味だそうです。
確かにファイル監視ですからね。個人的にはこういうネタは大好きです。
余談2
Ususama がいつから gulp-watch を使用していたかも気になったので洗い出してみました。
詳細な記録は残っていなかったのですが、
- kiribi_ususama/gulpfile.js at v_3.6.1-4.1.3 · arm-band/kiribi_ususama
- kiribi_ususama/gulpfile.js at v_3.7.0-4.1.3 · arm-band/kiribi_ususama
ver.3.6.1-4.1.3 ( Gulp のバージョンは3.9.1)では今回の最終結果と同様 gulp.watch の書き方をしていました。
※ただし、現在の gulp.watch は chokidar ですが、Gulp 3.9.1 は vinyl-fs の中の watch を使用していました
しかし、以下の理由のいずれか、または複数に該当したために gulp-watch に移行した……のかもしれません(うろ覚え)。
- Gulp 4.0.0 以降の
gulp.seriesやgulp.parallelを使用した書き方や Browsersync との兼ね合い - リリースしたばかりで変更点に追いつけず、ドキュメントの読み込みなどもできなかった
- 上述の
gulp.watchのコールバック指定だと1回しか watch されない現象に遭遇した?(記憶がない)
まとめ
gulp-watchで第一引数のglobsに!(negative globs) で除外するのではなく、第二引数optionsのignoreキー(gulp.srcと同じものが使える、とあったので)で指定しようとしたら、一部タスクが正常に動作しなくなった- Gulp デフォルトの
gulp.watchに切り替えたところ、上述の症状は発生しなかったが、別の現象が発生したgulp-watchとgulp.watchのコードを見比べたが、挙動の違いの原因は特定できなかった
gulp.watchのイベントハンドラを使用することで、意図した挙動にすることができた
長くなりましたが、現状を記録として残すためにメモとさせていただきます。
参考
- gulp-watch – npm
- src() | gulp.js
- isaacs/node-glob: glob functionality for node.js
- watch() | gulp.js
- paulmillr/chokidar: An efficient wrapper around node.js fs.watch / fs.watchFile / FSEvents
- node.jsでファイル監視を手軽に行えるモジュール「chokidar」 ? GUNMA GIS GEEK
- Node.js – chokidar と forever でファイルとディレクトリの変更を検知してログする – Qiita
- glob-watcher/index.js at master · gulpjs/glob-watcher
- Explaining Globs | gulp.js
- negative globs:
!で始まる globs のこと
- negative globs:
- gulp-watch/index.js at master · floatdrop/gulp-watch
- gulp/index.js at 9c14e3a13a73a32e424f144d62566671b2fcdbed · gulpjs/gulp
- Gulp 3 から 4 に変えたら Browser-Sync が動かなくなったので全面的に修正した・変更点をおさらい – Corredor