s-h-grunt
Version:
The Grunt hook from Sails core in v1.0 and up.
276 lines (233 loc) • 11.8 kB
JavaScript
/**
* Module dependencies
*/
var path = require('path');
var ChildProcess = require('child_process');
var _ = require('@sailshq/lodash');
var sanitizeGruntOutputChunk = require('./sanitize-grunt-output-chunk');
var logGruntAbortedError = require('./log-grunt-aborted-error');
/**
* `helpRunTask()`
*
* Fork a Grunt child process that runs the specified task.
* > Private utility function that powers `sails.hooks.grunt.runTask()`.
*
*
* @param {SailsApp} sails
* The Sails app instance.
*
* @param {String} taskName
* The name of the Grunt task to run.
*
* @param {Function} cueAfterTaskStarted
* Optional. Fires when the Grunt task has been started (non-production) or exits successfully (in production).
* Note that the `grunt:hook:error` and `grunt:hook:done` events are fired regardless of whether
* a callback is provided or not. Also note that this callback is not precisely equivalent to the semantic
* of these two events (for backwards compatibility).
* (Note: This is a "consequence procedure" --
* see https://gist.github.com/mikermcneil/1efd95bdcedecbd064f9d0d0ba725b13)
*
* @api private
* (however note that this is called ALMOST directly by `sails www` in the CLI)
*/
module.exports = function helpRunTask(sails, taskName, cueAfterTaskStarted) {
// Determine the path to the root directory of the current running instance
// of Sails core.
var pathToSails = path.resolve(__dirname, '../../../');
// If provided task is not a string, fail silently.
// FUTURE: handle this more elegantly-- leaving this in as-is to ensure backwards compatiblity,
// but it should be replaced in a subsequent release with an error instead (b/c )
if (!taskName) {
taskName = '';
}
// Fork Grunt child process
var child = ChildProcess.fork(
// Set our Grunt wrapper file in Sails core as the working directory for
// the Grunt child process.
path.join(__dirname, 'grunt-wrapper.js'),
// Command-line arguments (e.g. `foo bar`) and command-line options (e.g. `--foo="bar"`)
// to pass to the child process.
[
taskName,
'--pathToSails=' + pathToSails,
// Backwards compatibility for v0.9.x
'--gdsrc=' + pathToSails + '/node_modules'
],
// Command-line options (e.g. `--foo="bar"`) to pass to the child process.
{
silent: true,
stdio: 'pipe',
// Pass all current node process arguments to the child process,
// except the debug-related arguments, see issue #2670
execArgv: process.execArgv.slice(0).filter(function(param) {
return !(new RegExp('--(debug|inspect)(-brk=[0-9]+)?').test(param));
})
}
);
// Initialize local variables which will be used to buffer the
// incoming output from our Grunt child process.
//
// `errorMsg` will end up holding the human-readable error message,
// while `stackTrace` will end up with our best guess at a reasonable
// stack trace parsed from the incoming child proc output.
var errorMsg = '';
var stackTrace = '';
// Receive output as it comes in from the child proc's stdout
// (e.g. when Grunt does `console.log()`)
child.stdout.on('data', function(consoleMsg) {
// store all the output
consoleMsg = consoleMsg.toString();
errorMsg += consoleMsg;
// Clean out all the whitespace
var trimmedStackTrace = (typeof stackTrace === 'string') ? stackTrace : '';
trimmedStackTrace = trimmedStackTrace.replace(/[\n\s]*$/, '');
trimmedStackTrace = trimmedStackTrace.replace(/^[\n\s]*/, '');
var trimmedConsoleMsg = (typeof consoleMsg === 'string') ? consoleMsg : '';
trimmedConsoleMsg = trimmedConsoleMsg.replace(/[\n\s]*$/, '');
trimmedConsoleMsg = trimmedConsoleMsg.replace(/^[\n\s]*/, '');
// Remove '--force to continue' message since it makes no sense
// in this context:
trimmedConsoleMsg = trimmedConsoleMsg.replace(/Use --force to continue\./i, '');
trimmedStackTrace = trimmedStackTrace.replace(/Use --force to continue\./i, '');
// Find the stack trace related to this warning
stackTrace = errorMsg.substring(errorMsg.lastIndexOf('Running "'));
// Handle fatal errors, like missing grunt dependency, etc.
if (consoleMsg.match(/Fatal error/g)) {
// If no Gruntfile exists, don't crash- just display a warning.
if (consoleMsg.match(/Unable to find Gruntfile/i)) {
sails.log.info('Gruntfile could not be found.');
sails.log.info('(no grunt tasks will be run.)');
// In production, we consider this the end and trigger the callback if provided.
// (otherwise, we've already triggered the callback at this point, when the task started)
if (sails.config.environment === 'production') {
if (_.isFunction(cueAfterTaskStarted)) { return cueAfterTaskStarted(); }
else { sails.log.silly('`runTask()` could not find a Gruntfile.'); }
}
return;
} // </console message contains "Unable to find Gruntfile">
// Otherwise this is some other kind of fatal error, so log it as a "Grunt aborted" error.
else {
logGruntAbortedError(trimmedConsoleMsg, trimmedStackTrace, sails);
return;
}
} // </console message contains "Fatal error">
// Handle fatal Grunt errors by killing Sails process as well:
////////////////////////////////////////////////////////////////////
//
// "Aborted due to warnings"
else if (consoleMsg.match(/Aborted due to warnings/)) {
sails.log.error('** Grunt :: An error occurred. **');
// sails.log.warn(trimmedStackTrace);
// sails.emit('hook:grunt:error', trimmedStackTrace);
logGruntAbortedError(trimmedConsoleMsg, trimmedStackTrace, sails);
return;
} // </console message contains "Aborted due to warnings">
// "Warning: EMFILE"
else if (consoleMsg.match(/EMFILE/ig)){
sails.log.error('** Grunt :: An EMFILE error occurred. **');
sails.log.error(
'Usually this means there are too many files open as per your system settings.\n'+
'If you are developing on one of the many unix-based operating systems that has\n'+
'the `ulimit` command, then you might try running: `ulimit -n 1024`. For more tips,\n'+
'see https://github.com/balderdashy/sails/issues/3523#issuecomment-175922746.\n'+
'(command[⌘]+double-click to open links in the terminal)'
);
logGruntAbortedError(trimmedConsoleMsg, trimmedStackTrace, sails);
return;
} // </console message contains "Warning: EMFILE">
// "Warning: watch ……… ENOSPC"
// > See https://stackoverflow.com/questions/16748737/grunt-watch-error-waiting-fatal-error-watch-enospc)
else if (consoleMsg.match(/Warning\:\ watch\ .+\ ENOSPC/ig)){
sails.log.error('** Grunt :: An ENOSPC error occurred. **');
sails.log.error(
'Usually this means there are too many files open as per your system settings.\n'+
'If you are developing on an operating system that has the `inotify` command,\n'+
'then you might try using it. For more informaiton,\n'+
'see https://github.com/balderdashy/sails/issues/3523#issuecomment-175922746.\n'
);
logGruntAbortedError(trimmedConsoleMsg, trimmedStackTrace, sails);
return;
} // </console message contains "Warning: ENOSPC">
// "ParseError"
else if (consoleMsg.match(/ParseError/)) {
sails.log.warn('** Grunt :: Parse Warning **');
sails.log.warn(trimmedStackTrace);
} // </console message contains "ParseError">
// Only display console message if it has content besides whitespace
//////////////////////////////////////////////////////////////////
else if (!consoleMsg.match(/^\s*$/)) {
sails.log.silly('Grunt :: ' + trimmedConsoleMsg);
} // </console message has content which is not whitespace>
}); // </ stdout.on('data') >
// Handle errors on the stdout stream
// (rare- this is mainly to prevent throwing and crashing the process)
child.stdout.on('error', function(gruntOutput) {
sails.log.error('Grunt ::', sanitizeGruntOutputChunk(gruntOutput));
}); //</on stdout error>
// Receive output from the proc's stderr stream.
// (e.g. when Grunt does `console.error()`)
child.stderr.on('data', function(gruntOutput) {
gruntOutput = sanitizeGruntOutputChunk(gruntOutput);
// Ignore the "debugger listening" message from node --debug
if (gruntOutput.match(/debugger listening on port/)) {
return;
}
// Any other stderr output gets logged using `sails.log.error`.
else {
sails.log.error('Grunt ::', gruntOutput);
}
}); //</on stderr data>
// Handle errors on the stderr stream.
// (rare- this is mainly to prevent throwing and crashing the process)
child.stderr.on('error', function(gruntOutput) {
sails.log.error('Grunt ::', sanitizeGruntOutputChunk(gruntOutput));
}); //</on stderr error>
// When Grunt child process exits, fire event on `sails` app object.
child.on('exit', function(code) {
// If this is a non-zero status code, emit the 'hook:grunt:error' event.
if (code !== 0) {
// (in production, the callback is called in the appropriate spots above, so
// it does not need to be triggered again here)
// --FUTURE: make this implementation simpler! It'll be a breaking change.--
return sails.emit('hook:grunt:error');
}
// Otherwise emit 'hook:grunt:done'
sails.emit('hook:grunt:done');
// Note that, if we're in a production environment, we wait until the Grunt
// child process actually exits (Grunt task finishes running) before firing
// the callback passed in to `runTask`.
// (if this is not production, then we already fired the callback when the
// task was first run)
if (sails.config.environment === 'production') {
if (_.isFunction(cueAfterTaskStarted)) { return cueAfterTaskStarted(); }
else { sails.log.silly('`runTask()` has finished the Grunt task.'); }
}
}); //</when child process exits>
// Since there could be long-running tasks like `grunt-contrib-watch` involved,
// we'll want the ability to flush our child process later.
// So we save a reference to it on `sails.childProcesses`.
sails.log.silly('Tracking new grunt child process...');
if (!_.isArray(sails.childProcesses)) {
var consistencyViolationErr = new Error(
'Consistency violation: `sails.childProcesses` should exist and be an array. ' +
'Instead it\'s type: `' + typeof sails.childProcesses + '` '+
'This means that either a custom hook or app code has damaged the `sails.childProcesses` array, '+
'or there is a bug in Sails core. If you expect the latter, please file an issue in the GitHub repo.'
);
if (_.isFunction(cueAfterTaskStarted)) { return cueAfterTaskStarted(consistencyViolationErr); }
else { sails.log.error(consistencyViolationErr); }
}
sails.childProcesses.push(child);
// Now that the child process is chugging along, if we are NOT in a production
// environment, we'll go ahead and fire our callback (since Grunt might just be sitting
// here backgrounded, assuming the conventional default pipeline is being used with
// grunt-contrib-watch.)
//
// Note that, if we were in a production environment, we'd wait until the Grunt
// child proc actually finished running before firing our callback. But we go ahead
// and fire it here.
if (sails.config.environment !== 'production') {
if (_.isFunction(cueAfterTaskStarted)) { return cueAfterTaskStarted(); }
else { sails.log.silly('`runTask()` has started the Grunt task.'); }
}
};//</function definition :: sails.hooks.grunt.runTask()>