Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run one process for command by default, add --concurrent option #29

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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