Skip to content
Open
55 changes: 35 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/usr/bin/env node

var childProcess = require('child_process');
var Promise = require('bluebird');
var _ = require('lodash');
var chokidar = require('chokidar');
var utils = require('./utils');
var spawn = require('npm-run-all/lib/spawn').default;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.default doesn't work anymore. Just remove it and it works.


var EVENT_DESCRIPTIONS = {
add: 'File added',
Expand All @@ -14,6 +13,12 @@ var EVENT_DESCRIPTIONS = {
change: 'File changed'
};

// Try to resolve path to shell.
// We assume that Windows provides COMSPEC env variable
// and other platforms provide SHELL env variable
var SHELL_PATH = process.env.SHELL || process.env.COMSPEC;
var EXECUTE_OPTION = process.env.COMSPEC !== undefined && process.env.SHELL === undefined ? '/c' : '-c';

var defaultOpts = {
debounce: 400,
throttle: 0,
Expand All @@ -25,7 +30,8 @@ var defaultOpts = {
verbose: false,
silent: false,
initial: false,
command: null
command: null,
concurrent: false
};

var VERSION = 'chokidar-cli: ' + require('./package.json').version +
Expand Down Expand Up @@ -83,6 +89,11 @@ var argv = require('yargs')
default: defaultOpts.initial,
type: 'boolean'
})
.option('concurrent', {
describe: 'When set, command is not killed before invoking again',
default: defaultOpts.concurrent,
type: 'boolean'
})
.option('p', {
alias: 'polling',
describe: 'Whether to use fs.watchFile(backed by polling) instead of ' +
Expand Down Expand Up @@ -134,15 +145,25 @@ function getUserOpts(argv) {
return argv;
}

// Estimates spent working hours based on commit dates
function startWatching(opts) {
var child;
var chokidarOpts = createChokidarOpts(opts);
var watcher = chokidar.watch(opts.patterns, chokidarOpts);
var execFn = _.debounce(_.throttle(function(event, path) {
if (child) child.removeAllListeners();
child = spawn(SHELL_PATH, [
EXECUTE_OPTION,
opts.command.replace(/\{path\}/ig, path).replace(/\{event\}/ig, event)
], {
stdio: 'inherit'
});
child.once('error', function(error) { throw error; });
child.once('exit', function() { child = undefined; });
}, opts.throttle), opts.debounce);

var throttledRun = _.throttle(run, opts.throttle);
var debouncedRun = _.debounce(throttledRun, opts.debounce);
watcher.on('all', function(event, path) {
var description = EVENT_DESCRIPTIONS[event] + ':';
var executeCommand = _.partial(execFn, event, path);

if (opts.verbose) {
console.error(description, path);
Expand All @@ -152,13 +173,15 @@ function startWatching(opts) {
}
}

// XXX: commands might be still run concurrently
if (opts.command) {
debouncedRun(
opts.command
.replace(/\{path\}/ig, path)
.replace(/\{event\}/ig, event)
);
// If a previous run of command created a child, and the concurrent option is not set,
// then we should kill that child process before running it again
if (child && !opts.concurrent) {
child.once('exit', executeCommand);
child.kill();
} else {
setImmediate(executeCommand);
}
}
});

Expand Down Expand Up @@ -211,12 +234,4 @@ function _resolveIgnoreOpt(ignoreOpt) {
});
}

function run(cmd) {
return utils.run(cmd)
.catch(function(err) {
console.error('Error when executing', cmd);
console.error(err.stack);
});
}

main();
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"bluebird": "^2.9.24",
"chokidar": "^1.0.1",
"lodash": "^3.7.0",
"npm-run-all": "1.6.0",
"shell-quote": "^1.4.3",
"yargs": "^3.7.2"
},
Expand Down
76 changes: 72 additions & 4 deletions test/test-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('chokidar-cli', function() {
.then(function() {
done();
});
})
});

it('help should be succesful', function(done) {
run('node index.js --help', {pipe: DEBUG_TESTS})
Expand Down Expand Up @@ -88,9 +88,77 @@ describe('chokidar-cli', function() {
fs.writeFileSync(resolve('dir/subdir/c.less'), 'content');

setTimeout(function() {
assert(changeFileExists(), 'change file should exist')
}, TIMEOUT_CHANGE_DETECTED)
assert(changeFileExists(), 'change file should exist');
}, TIMEOUT_CHANGE_DETECTED);
}, TIMEOUT_WATCH_READY);
});

it('should throttle invocations of command', function(done) {
var touch = 'touch ' + CHANGE_FILE;
var changedDetectedTime = 100;
var throttleTime = (2 * changedDetectedTime) + 100;

run('node ../index.js "dir/**/*.less" --debounce 0 --throttle ' + throttleTime + ' -c "' + touch + '"', {
pipe: DEBUG_TESTS,
cwd: './test',
callback: function(child) {
setTimeout(function killChild() {
// Kill child after test case
child.kill();
}, TIMEOUT_KILL);
}
})
.then(function childProcessExited(exitCode) {
done();
});

setTimeout(function afterWatchIsReady() {
fs.writeFileSync(resolve('dir/subdir/c.less'), 'content');
setTimeout(function() {
assert(changeFileExists(), 'change file should exist after first change');
fs.unlinkSync(resolve(CHANGE_FILE));
fs.writeFileSync(resolve('dir/subdir/c.less'), 'more content');
setTimeout(function() {
assert.equal(changeFileExists(), false, 'change file should not exist after second change');
}, changedDetectedTime);
}, changedDetectedTime);
}, TIMEOUT_WATCH_READY);
});

it('should debounce invocations of command', function(done) {
var touch = 'touch ' + CHANGE_FILE;
var changedDetectedTime = 100;
var debounceTime = (2 * changedDetectedTime) + 100;
var killTime = TIMEOUT_WATCH_READY + (2 * changedDetectedTime) + debounceTime + 1000;

run('node ../index.js "dir/**/*.less" --debounce ' + debounceTime + ' -c "' + touch + '"', {
pipe: DEBUG_TESTS,
cwd: './test',
callback: function(child) {
setTimeout(function killChild() {
// Kill child after test case
child.kill();
}, killTime);
}
})
.then(function childProcessExited(exitCode) {
done();
});

setTimeout(function afterWatchIsReady() {
fs.writeFileSync(resolve('dir/subdir/c.less'), 'content');
setTimeout(function() {
assert.equal(changeFileExists(), false, 'change file should not exist earlier than debounce time (first)');
fs.writeFileSync(resolve('dir/subdir/c.less'), 'more content');
setTimeout(function() {
assert.equal(changeFileExists(), false, 'change file should not exist earlier than debounce time (second)');
}, changedDetectedTime);
setTimeout(function() {
assert(changeFileExists(), 'change file should exist after debounce time');
}, debounceTime + changedDetectedTime);
}, changedDetectedTime);
}, TIMEOUT_WATCH_READY);

});

it('should replace {path} and {event} in command', function(done) {
Expand All @@ -110,7 +178,7 @@ describe('chokidar-cli', function() {
.then(function() {
var res = fs.readFileSync(resolve(CHANGE_FILE)).toString().trim();
assert.equal(res, 'change:dir/a.js', 'need event/path detail');
done()
done();
});
});
});
Expand Down