index.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use strict';
  2. var util = require('gulp-util');
  3. var assign = require('object-assign');
  4. var path = require('path');
  5. var PluginError = require('gulp-util').PluginError;
  6. var chokidar = require('chokidar');
  7. var Duplex = require('readable-stream').Duplex;
  8. var vinyl = require('vinyl-file');
  9. var File = require('vinyl');
  10. var anymatch = require('anymatch');
  11. var pathIsAbsolute = require('path-is-absolute');
  12. var globParent = require('glob-parent');
  13. var slash = require('slash');
  14. function normalizeGlobs(globs) {
  15. if (!globs) {
  16. throw new PluginError('gulp-watch', 'glob argument required');
  17. }
  18. if (typeof globs === 'string') {
  19. globs = [globs];
  20. }
  21. if (!Array.isArray(globs)) {
  22. throw new PluginError('gulp-watch', 'glob should be String or Array, not ' + (typeof globs));
  23. }
  24. return globs;
  25. }
  26. function watch(globs, opts, cb) {
  27. var originalGlobs = globs;
  28. globs = normalizeGlobs(globs);
  29. if (typeof opts === 'function') {
  30. cb = opts;
  31. opts = {};
  32. }
  33. opts = assign({}, watch._defaultOptions, opts);
  34. cb = cb || function () {};
  35. function resolveFilepath(filepath) {
  36. if (pathIsAbsolute(filepath)) {
  37. return path.normalize(filepath);
  38. }
  39. return path.resolve(opts.cwd || process.cwd(), filepath);
  40. }
  41. function resolveGlob(glob) {
  42. var mod = '';
  43. if (glob[0] === '!') {
  44. mod = glob[0];
  45. glob = glob.slice(1);
  46. }
  47. return mod + slash(resolveFilepath(glob));
  48. }
  49. globs = globs.map(resolveGlob);
  50. var baseForced = Boolean(opts.base);
  51. var outputStream = new Duplex({objectMode: true, allowHalfOpen: true});
  52. outputStream._write = function _write(file, enc, done) {
  53. cb(file);
  54. this.push(file);
  55. done();
  56. };
  57. outputStream._read = function _read() { };
  58. var watcher = chokidar.watch(globs, opts);
  59. opts.events.forEach(function (ev) {
  60. watcher.on(ev, processEvent.bind(undefined, ev));
  61. });
  62. ['add', 'change', 'unlink', 'addDir', 'unlinkDir', 'error', 'ready', 'raw']
  63. .forEach(function (ev) {
  64. watcher.on(ev, outputStream.emit.bind(outputStream, ev));
  65. });
  66. outputStream.add = function add(newGlobs) {
  67. newGlobs = normalizeGlobs(newGlobs)
  68. .map(resolveGlob);
  69. watcher.add(newGlobs);
  70. globs.push.apply(globs, newGlobs);
  71. };
  72. outputStream.unwatch = watcher.unwatch.bind(watcher);
  73. outputStream.close = function () {
  74. watcher.close();
  75. this.emit('end');
  76. };
  77. function processEvent(event, filepath) {
  78. filepath = resolveFilepath(filepath);
  79. var fileOpts = assign({}, opts);
  80. var glob;
  81. var currentFilepath = filepath;
  82. while (!(glob = globs[anymatch(globs, currentFilepath, true)]) && currentFilepath !== (currentFilepath = path.dirname(currentFilepath))) {} // eslint-disable-line no-empty-blocks/no-empty-blocks
  83. if (!glob) {
  84. util.log(
  85. util.colors.cyan('[gulp-watch]'),
  86. util.colors.yellow('Watched unexpected path. This is likely a bug. Please open this link to report the issue:\n') +
  87. 'https://github.com/floatdrop/gulp-watch/issues/new?title=' +
  88. encodeURIComponent('Watched unexpected filepath') + '&body=' +
  89. encodeURIComponent('Node.js version: `' + process.version + ' ' + process.platform + ' ' + process.arch + '`\ngulp-watch version: `' + require('./package.json').version + '`\nGlobs: `' + JSON.stringify(originalGlobs) + '`\nFilepath: `' + filepath + '`\nEvent: `' + event + '`\nProcess CWD: `' + process.cwd() + '`\nOptions:\n```js\n' + JSON.stringify(opts, null, 2) + '\n```')
  90. );
  91. return;
  92. }
  93. if (!baseForced) {
  94. fileOpts.base = path.normalize(globParent(glob));
  95. }
  96. // Do not stat deleted files
  97. if (event === 'unlink' || event === 'unlinkDir') {
  98. fileOpts.path = filepath;
  99. write(event, null, new File(fileOpts));
  100. return;
  101. }
  102. // Workaround for early read
  103. setTimeout(function () {
  104. vinyl.read(filepath, fileOpts).then(function (file) {
  105. write(event, null, file);
  106. });
  107. }, opts.readDelay);
  108. }
  109. function write(event, err, file) {
  110. if (err) {
  111. outputStream.emit('error', err);
  112. return;
  113. }
  114. if (opts.verbose) {
  115. log(event, file);
  116. }
  117. file.event = event;
  118. outputStream.push(file);
  119. cb(file);
  120. }
  121. function log(event, file) {
  122. event = event[event.length - 1] === 'e' ? event + 'd' : event + 'ed';
  123. var msg = [util.colors.magenta(file.relative), 'was', event];
  124. if (opts.name) {
  125. msg.unshift(util.colors.cyan(opts.name) + ' saw');
  126. }
  127. util.log.apply(util, msg);
  128. }
  129. return outputStream;
  130. }
  131. // This is not part of the public API as that would lead to global state (singleton) pollution,
  132. // and allow unexpected interference between unrelated modules that make use of gulp-watch.
  133. // This can be useful for unit tests and root application configuration, though.
  134. // Avoid modifying gulp-watch's default options inside a library/reusable package, please.
  135. watch._defaultOptions = {
  136. events: ['add', 'change', 'unlink'],
  137. ignoreInitial: true,
  138. readDelay: 10
  139. };
  140. module.exports = watch;