index.js 4.7 KB
'use strict';
var util = require('gulp-util');
var assign = require('object-assign');
var path = require('path');
var PluginError = require('gulp-util').PluginError;
var chokidar = require('chokidar');
var Duplex = require('readable-stream').Duplex;
var vinyl = require('vinyl-file');
var File = require('vinyl');
var anymatch = require('anymatch');
var pathIsAbsolute = require('path-is-absolute');
var globParent = require('glob-parent');
var slash = require('slash');

function normalizeGlobs(globs) {
	if (!globs) {
		throw new PluginError('gulp-watch', 'glob argument required');
	}

	if (typeof globs === 'string') {
		globs = [globs];
	}

	if (!Array.isArray(globs)) {
		throw new PluginError('gulp-watch', 'glob should be String or Array, not ' + (typeof globs));
	}

	return globs;
}

function watch(globs, opts, cb) {
	var originalGlobs = globs;
	globs = normalizeGlobs(globs);

	if (typeof opts === 'function') {
		cb = opts;
		opts = {};
	}

	opts = assign({}, watch._defaultOptions, opts);
	cb = cb || function () {};

	function resolveFilepath(filepath) {
		if (pathIsAbsolute(filepath)) {
			return path.normalize(filepath);
		}
		return path.resolve(opts.cwd || process.cwd(), filepath);
	}

	function resolveGlob(glob) {
		var mod = '';

		if (glob[0] === '!') {
			mod = glob[0];
			glob = glob.slice(1);
		}

		return mod + slash(resolveFilepath(glob));
	}
	globs = globs.map(resolveGlob);

	var baseForced = Boolean(opts.base);
	var outputStream = new Duplex({objectMode: true, allowHalfOpen: true});

	outputStream._write = function _write(file, enc, done) {
		cb(file);
		this.push(file);
		done();
	};

	outputStream._read = function _read() { };

	var watcher = chokidar.watch(globs, opts);

	opts.events.forEach(function (ev) {
		watcher.on(ev, processEvent.bind(undefined, ev));
	});

	['add', 'change', 'unlink', 'addDir', 'unlinkDir', 'error', 'ready', 'raw']
		.forEach(function (ev) {
			watcher.on(ev, outputStream.emit.bind(outputStream, ev));
		});

	outputStream.add = function add(newGlobs) {
		newGlobs = normalizeGlobs(newGlobs)
			.map(resolveGlob);
		watcher.add(newGlobs);
		globs.push.apply(globs, newGlobs);
	};
	outputStream.unwatch = watcher.unwatch.bind(watcher);
	outputStream.close = function () {
		watcher.close();
		this.emit('end');
	};

	function processEvent(event, filepath) {
		filepath = resolveFilepath(filepath);
		var fileOpts = assign({}, opts);

		var glob;
		var currentFilepath = filepath;
		while (!(glob = globs[anymatch(globs, currentFilepath, true)]) && currentFilepath !== (currentFilepath = path.dirname(currentFilepath))) {} // eslint-disable-line no-empty-blocks/no-empty-blocks

		if (!glob) {
			util.log(
				util.colors.cyan('[gulp-watch]'),
				util.colors.yellow('Watched unexpected path. This is likely a bug. Please open this link to report the issue:\n') +
				'https://github.com/floatdrop/gulp-watch/issues/new?title=' +
				encodeURIComponent('Watched unexpected filepath') + '&body=' +
				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```')
			);
			return;
		}

		if (!baseForced) {
			fileOpts.base = path.normalize(globParent(glob));
		}

		// Do not stat deleted files
		if (event === 'unlink' || event === 'unlinkDir') {
			fileOpts.path = filepath;

			write(event, null, new File(fileOpts));
			return;
		}

		// Workaround for early read
		setTimeout(function () {
			vinyl.read(filepath, fileOpts).then(function (file) {
				write(event, null, file);
			});
		}, opts.readDelay);
	}

	function write(event, err, file) {
		if (err) {
			outputStream.emit('error', err);
			return;
		}

		if (opts.verbose) {
			log(event, file);
		}

		file.event = event;
		outputStream.push(file);
		cb(file);
	}

	function log(event, file) {
		event = event[event.length - 1] === 'e' ? event + 'd' : event + 'ed';

		var msg = [util.colors.magenta(file.relative), 'was', event];

		if (opts.name) {
			msg.unshift(util.colors.cyan(opts.name) + ' saw');
		}

		util.log.apply(util, msg);
	}

	return outputStream;
}

// This is not part of the public API as that would lead to global state (singleton) pollution,
// and allow unexpected interference between unrelated modules that make use of gulp-watch.
// This can be useful for unit tests and root application configuration, though.
// Avoid modifying gulp-watch's default options inside a library/reusable package, please.
watch._defaultOptions = {
	events: ['add', 'change', 'unlink'],
	ignoreInitial: true,
	readDelay: 10
};

module.exports = watch;