経緯
ひょんなことから 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
のドキュメントを見ます。
ignore
Add a pattern or an array of glob patterns to exclude matches. Note:ignore
patterns are always indot:true
mode, regardless of any other settings.
先ほどの「他の設定に関わらず dot files との照合が常に行われるようになる」の正体が書いてあります。 dot
オプションが常に true
として動作するようになる、とのことです。
それでは、この dot
オプションの説明はと言うと……。
dot
Include.dot
files in normal matches andglobstar
matches. 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