node-watch-changes
Version:
Run user defined commands on file changes.
403 lines (338 loc) • 9.71 kB
JavaScript
var spawn = require('child_process').spawn;
var path = require('path');
var chokidarLib = require('chokidar');
var colors = require('colors/safe');
var configPath = '';
var chokidar = null;
var config = {};
RegExp.prototype.toJSON = RegExp.prototype.toString;
Function.prototype.toJSON = function () {
return this.toString().replace(/\n/g, '');
};
var watcher = function (initialConfig) {
var noConfigFound = false;
initialConfig = initialConfig || path.resolve(process.cwd(), '.watcher-config.js');
if (initialConfig.constructor === String) {
var resolvedPath = path.resolve(initialConfig);
configPath = resolvedPath;
try {
config = require(resolvedPath);
} catch (error) {
console.error(error);
config = {};
configPath = '';
noConfigFound = true;
}
} else {
config = initialConfig;
}
ensureConfigValues();
if (noConfigFound) {
console.log(colors.yellow("Couldn't find the config file »" + resolvedPath + '«. Will use default values.'));
console.log(colors.yellow('Default config values are:'));
console.log(JSON.stringify(config, null, 2));
console.log();
} else {
if (config.verbosity >= verbosity.normal) {
console.log(colors.green('Using following config (' + configPath + '):'));
console.log(JSON.stringify(config, null, 2));
console.log();
}
}
if (config.onStart) {
try {
config.onStart.call(null, spawnPromisified);
} catch (error) {
console.log(colors.red('The onStart callback threw an error.', error));
}
}
setupCommandsOnChange();
process.on('SIGTERM', function () {
if (chokidar) {
chokidar.close();
}
if (config.onEnd) {
try {
config.onEnd.call(null, spawnPromisified);
} catch (error) {
console.log(colors.red('The onEnd callback threw an error.', error));
}
}
});
};
var ensureConfigValues = function () {
config.directory = config.directory || process.cwd();
config.delay = config.delay || 1000;
config.ignore = config.ignore || [];
config.verbosity = config.verbosity || 'normal';
config.verbosity = verbosity[config.verbosity];
config.onStart = config.onStart || function () {};
config.onChange = config.onChange || function () {};
config.onEnd = config.onEnd || function () {};
};
var setupCommandsOnChange = function () {
var timeoutID = 0;
var allEvents = {};
if (config.onChange) {
var directoryToWatch = null;
if (config.directory.constructor === String) {
directoryToWatch = config.directory.indexOf('/') !== 0 ? path.resolve(process.cwd(), config.directory) : config.directory;
} else if (config.directory.constructor === Array) {
directoryToWatch = [];
for (var i = 0; i < config.directory.length; i++) {
var directory = config.directory[i];
directoryToWatch.push(directory.indexOf('/') !== 0 ? path.resolve(process.cwd(), directory) : directory);
}
}
chokidar = chokidarLib.watch(directoryToWatch, {
ignoreInitial: true,
ignored: config.ignore,
});
chokidar.on('all', function (event, path) {
clearTimeout(timeoutID);
if (!allEvents[event]) {
allEvents[event] = [];
}
allEvents[event].push(path);
if (configPath && event === 'change' && configPath === path) {
// The config file changed
delete require.cache[configPath];
config = require(configPath);
ensureConfigValues();
if (config.verbosity >= verbosity.normal) {
console.log(colors.green('Config file changed, reloaded it. The new values are:'));
console.log(JSON.stringify(config, null, 2));
console.log();
}
allEvents = {};
return;
}
timeoutID = setTimeout(function () {
if (config.verbosity >= verbosity.verbose) {
console.log(colors.green('Executing callbacks for the following events:'));
console.log(JSON.stringify(allEvents, null, 2));
console.log();
}
try {
config.onChange.call(null, allEvents, spawnPromisified);
} catch (error) {
console.log(colors.red('The onChange callback threw an error.', error));
}
allEvents = {};
}, config.delay);
});
}
};
const spawnPromisified = function (command, args) {
return new Promise(function (resolve, reject) {
if (config.verbosity >= verbosity.normal) {
console.log(
colors.green('Spawning the command') + ' "' + command + '" ' + colors.green('with the arguments ') + JSON.stringify(args)
);
console.log();
}
if (!args) {
// No args are being passed in, so we parse the command string
var parsedCommand = parseCommand(command);
command = parsedCommand.command;
args = parsedCommand.arguments;
}
child = spawn(command, args, {
stdio: 'inherit',
});
var onExit = function (code) {
if (code !== 0) {
reject(code);
} else {
if (config.verbosity >= verbosity.normal) {
console.log(
colors.green('The command') +
' "' +
command +
'" ' +
colors.green('with arguments ') +
JSON.stringify(args) +
colors.green(' exited')
);
console.log();
}
resolve();
}
};
const onError = function (error) {
if (config.verbosity >= verbosity.normal) {
console.log(
colors.red('The command') +
' "' +
command +
'" ' +
colors.red('with arguments ') +
JSON.stringify(args) +
colors.red(' errored')
);
console.log();
}
reject(error);
};
child.on('error', onError);
child.on('exit', onExit);
child.on('SIGTERM', onExit);
child.on('SIGINT', onExit);
});
};
var parseCommand = function (command) {
var quoted = false;
var parsedCommand = {
command: '',
arguments: [],
remainingString: '',
};
if (!command) {
return parsedCommand;
}
var checkForArguments = function () {
var remainingString = command.replace(parsedCommand.command, '').trim();
if (remainingString.indexOf(';') === 0) {
parsedCommand.remainingString = remainingString.substring(1).trim();
return parsedCommand;
}
var parsedArguments = parseArguments(remainingString);
parsedCommand.arguments = parsedArguments.arguments;
parsedCommand.remainingString = parsedArguments.remainingString;
return parsedCommand;
};
for (var i = 0; i < command.length; i++) {
var char = command.charAt(i);
var previousChar = i > 0 ? command.charAt(i - 1) : '';
if (char === ' ') {
if (quoted || previousChar === '\\') {
parsedCommand.command += char;
} else {
return checkForArguments();
}
} else if (previousChar !== '\\' && (char === '"' || char === "'")) {
if (quoted) {
return checkForArguments();
}
quoted = true;
} else if (char === ';' && !quoted) {
return checkForArguments();
} else if (char === '&' && previousChar === '&' && !quoted) {
return checkForArguments();
} else {
parsedCommand.command += char;
}
}
return checkForArguments();
};
var parseArguments = function (arguments) {
var quoted = false;
var parentheses = 0;
var brackets = 0;
var braces = 0;
var parsedArguments = {
arguments: [],
remainingString: '',
};
if (!arguments) {
return parsedArguments;
}
var currentArgument = '';
var addArgument = function () {
currentArgument = currentArgument.trim();
if (currentArgument.indexOf('"') === 0 && currentArgument.match(/"$/)) {
currentArgument = currentArgument.substring(1, currentArgument.length - 1);
}
if (currentArgument !== '' && currentArgument !== '&') {
parsedArguments.arguments.push(currentArgument);
}
currentArgument = '';
quoted = false;
var parentheses = 0;
var brackets = 0;
var braces = 0;
};
for (var i = 0; i < arguments.length; i++) {
var char = arguments[i];
var previousChar = i > 0 ? arguments.charAt(i - 1) : '';
if (char === ' ') {
if (quoted || previousChar === '\\') {
currentArgument += char;
} else {
addArgument();
}
} else if (previousChar !== '\\' && (char === '"' || char === "'")) {
currentArgument += char;
if (quoted && !parentheses && !brackets && !braces) {
addArgument();
} else if (quoted) {
quoted = false;
} else {
quoted = true;
}
} else if (char === '(') {
currentArgument += char;
parentheses++;
} else if (char === ')') {
currentArgument += char;
parentheses--;
if (!parentheses && !brackets && !braces && !quoted) {
addArgument();
}
} else if (char === '[') {
currentArgument += char;
brackets++;
} else if (char === ']') {
currentArgument += char;
brackets--;
if (!parentheses && !brackets && !braces && !quoted) {
addArgument();
}
} else if (char === '{') {
currentArgument += char;
braces++;
} else if (char === '}') {
currentArgument += char;
braces--;
if (!parentheses && !brackets && !braces && !quoted) {
addArgument();
}
} else if (char === ';') {
if (!quoted && !parentheses && !brackets && !braces) {
addArgument();
parsedArguments.remainingString = arguments.substring(i + 1).trim();
return parsedArguments;
} else {
currentArgument += char;
}
} else if (char === '&' && previousChar === '&') {
if (!quoted && !parentheses && !brackets && !braces) {
addArgument();
parsedArguments.remainingString = arguments.substring(i + 1).trim();
return parsedArguments;
} else {
currentArgument += char;
}
} else {
currentArgument += char;
}
}
if (currentArgument) {
addArgument();
}
return parsedArguments;
};
var verbosity = {
minimal: 0,
normal: 1,
verbose: 2,
};
module.exports = watcher;
if (process.env.testing) {
module.exports.test = {
parseCommand: parseCommand,
parseArguments: parseArguments,
setupCommandsOnChange: setupCommandsOnChange,
ensureConfigValues: ensureConfigValues,
};
}