diff --git a/index.js b/index.js index e638668..6a6139c 100755 --- a/index.js +++ b/index.js @@ -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; var EVENT_DESCRIPTIONS = { add: 'File added', @@ -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, @@ -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 + @@ -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 ' + @@ -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); @@ -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); + } } }); @@ -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(); diff --git a/package.json b/package.json index 7c4ebae..187d1ba 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/test/test-all.js b/test/test-all.js index d349e25..6f19f15 100644 --- a/test/test-all.js +++ b/test/test-all.js @@ -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}) @@ -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) { @@ -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(); }); }); });