1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-06-30 23:40:05 +01:00

Encapsulate CLI interface to ease testing.

Strip index.js down to its bare essentials, move primary logic to
main.js, and expose the latter via module.exports. This allows tests to
execute the same code path(s) HMR runs in production.
This commit is contained in:
hacksalot 2016-01-08 19:22:44 -05:00
parent 3b38c4818f
commit d878270bc6
2 changed files with 244 additions and 226 deletions

View File

@ -10,236 +10,15 @@ Command-line interface (CLI) for HackMyResume.
var SPAWNW = require('./core/spawn-watch')
, HMR = require( './hackmyapi')
, PKG = require('../package.json')
, FS = require('fs')
, EXTEND = require('./utils/extend')
, chalk = require('chalk')
, PATH = require('path')
, HACKMYSTATUS = require('./core/status-codes')
, safeLoadJSON = require('./utils/safe-json-loader')
, _opts = { }
, title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***')
, StringUtils = require('./utils/string.js')
, _ = require('underscore')
, Command = require('commander').Command;
try {
main();
var main = require('./main');
main( process.argv );
}
catch( ex ) {
require('./core/error-handler').err( ex, true );
}
/**
Kick off the HackMyResume application.
*/
function main() {
var args = initialize();
// Create the top-level (application) command...
var program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false);
//.usage('COMMAND <sources> [TO <targets>]');
// Create the NEW command
program
.command('new')
.arguments('<sources...>')
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.')
.action(function( sources ) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.option('-a --assert', 'Treat validation warnings as errors', false)
.description('Validate a resume in FRESH or JSON RESUME format.')
.action(function(sources) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program
.command('convert')
//.arguments('<sources...>')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.action(function() {
var x = splitSrcDest.call( this );
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.description('Analyze one or more resumes.')
.action(function( sources ) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the BUILD command
program
.command('build')
.alias('generate')
//.arguments('<sources> TO [targets]')
//.usage('...')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding', 'embed')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-tips', 'Disable theme tips and warnings.', false)
.description('Generate resume to multiple formats')
.action(function( sources, targets, options ) {
var x = splitSrcDest.call( this );
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
});
// program.on('--help', function(){
// console.log(' Examples:');
// console.log('');
// console.log(' $ custom-help --help');
// console.log(' $ custom-help -h');
// console.log('');
// });
program.parse( args );
if (!program.args.length) { throw { fluenterror: 4 }; }
}
/**
Massage command-line args and setup Commander.js.
*/
function initialize() {
logMsg( title );
// Support case-insensitive sub-commands (build, generate, validate, etc.)..
var oVerb, verb = '', args = process.argv.slice(), cleanArgs = args.slice(2);
if( cleanArgs.length ) {
var verbIdx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
if( verbIdx !== -1 ) {
oVerb = cleanArgs[ verbIdx ];
verb = args[ verbIdx + 2 ] = oVerb.trim().toLowerCase();
}
}
// Handle invalid verbs here (a bit easier here than in commander.js)...
if( verb && !HMR.verbs[ verb ] && !HMR.alias[ verb ] ) {
throw { fluenterror: HACKMYSTATUS.invalidCommand, shouldExit: true,
attempted: oVerb };
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function(name) {
if( this.name() !== 'new' )
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
return chalk.green.bold(manPage);
};
return args;
}
/**
Invoke a HackMyResume verb.
*/
function execVerb( src, dst, opts, log ) {
loadOptions.call( this, opts );
require('./core/error-handler').init( _opts.debug );
HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
}
/**
Initialize HackMyResume options.
*/
function loadOptions( o ) {
o.opts = this.parent.opts;
// Load the specified options file (if any) and apply options
if( o.opts && String.is( o.opts )) {
var json = safeLoadJSON( PATH.relative( process.cwd(), o.opts ) );
json && ( o = EXTEND( true, o, json ) );
if( !json ) {
throw safeLoadJSON.error;
}
}
// Merge in command-line options
o = EXTEND( true, o, this.opts() );
o.silent = this.parent.silent;
o.debug = this.parent.debug;
_opts = o;
}
/**
Split multiple command-line filenames by the 'TO' keyword
*/
function splitSrcDest() {
var params = this.parent.args.filter(function(j) { return String.is(j); });
if( params.length === 0 )
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
// Find the TO keyword, if any
var splitAt = _.findIndex( params, function(p) {
return p.toLowerCase() === 'to';
});
// TO can't be the last keyword
if( splitAt === params.length - 1 && splitAt !== -1 ) {
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') );
return;
}
return {
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
};
}
/**
Simple logging placeholder.
*/
function logMsg( msg ) {
msg = msg || '';
_opts.silent || console.log( msg );
}

239
src/main.js Normal file
View File

@ -0,0 +1,239 @@
(function(){
/**
Main function for HackMyResume
@license MIT. See LICENSE.md for details.
@module main.js
*/
var SPAWNW = require('./core/spawn-watch')
, HMR = require( './hackmyapi')
, PKG = require('../package.json')
, FS = require('fs')
, EXTEND = require('./utils/extend')
, chalk = require('chalk')
, PATH = require('path')
, HACKMYSTATUS = require('./core/status-codes')
, safeLoadJSON = require('./utils/safe-json-loader')
, _opts = { }
, title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***')
, StringUtils = require('./utils/string.js')
, _ = require('underscore')
, Command = require('commander').Command;
/**
Kick off the HackMyResume application.
*/
var main = module.exports = function( args ) {
var args = initialize( args );
// Create the top-level (application) command...
var program = new Command('hackmyresume')
.version(PKG.version)
.description(chalk.yellow.bold('*** HackMyResume ***'))
.option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
.option('-s --silent', 'Run in silent mode')
.option('--no-color', 'Disable colors')
.option('--color', 'Enable colors')
.option('-d --debug', 'Enable diagnostics', false);
//.usage('COMMAND <sources> [TO <targets>]');
// Create the NEW command
program
.command('new')
.arguments('<sources...>')
.option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
.alias('create')
.description('Create resume(s) in FRESH or JSON RESUME format.')
.action(function( sources ) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the VALIDATE command
program
.command('validate')
.arguments('<sources...>')
.option('-a --assert', 'Treat validation warnings as errors', false)
.description('Validate a resume in FRESH or JSON RESUME format.')
.action(function(sources) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the CONVERT command
program
.command('convert')
//.arguments('<sources...>')
.description('Convert a resume to/from FRESH or JSON RESUME format.')
.action(function() {
var x = splitSrcDest.call( this );
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
});
// Create the ANALYZE command
program
.command('analyze')
.arguments('<sources...>')
.description('Analyze one or more resumes.')
.action(function( sources ) {
execVerb.call( this, sources, [], this.opts(), logMsg);
});
// Create the BUILD command
program
.command('build')
.alias('generate')
//.arguments('<sources> TO [targets]')
//.usage('...')
.option('-t --theme <theme>', 'Theme name or path')
.option('-n --no-prettify', 'Disable HTML prettification', true)
.option('-c --css <option>', 'CSS linking / embedding', 'embed')
.option('-p --pdf <engine>', 'PDF generation engine')
.option('--no-tips', 'Disable theme tips and warnings.', false)
.description('Generate resume to multiple formats')
.action(function( sources, targets, options ) {
var x = splitSrcDest.call( this );
execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
});
// program.on('--help', function(){
// console.log(' Examples:');
// console.log('');
// console.log(' $ custom-help --help');
// console.log(' $ custom-help -h');
// console.log('');
// });
program.parse( args );
if (!program.args.length) { throw { fluenterror: 4 }; }
};
/**
Massage command-line args and setup Commander.js.
*/
function initialize( args ) {
logMsg( title );
// Support case-insensitive sub-commands (build, generate, validate, etc.)..
var oVerb, verb = '', args = args.slice(), cleanArgs = args.slice(2);
if( cleanArgs.length ) {
var verbIdx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
if( verbIdx !== -1 ) {
oVerb = cleanArgs[ verbIdx ];
verb = args[ verbIdx + 2 ] = oVerb.trim().toLowerCase();
}
}
// Handle invalid verbs here (a bit easier here than in commander.js)...
if( verb && !HMR.verbs[ verb ] && !HMR.alias[ verb ] ) {
throw { fluenterror: HACKMYSTATUS.invalidCommand, shouldExit: true,
attempted: oVerb };
}
// Override the .missingArgument behavior
Command.prototype.missingArgument = function(name) {
if( this.name() !== 'new' )
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
};
// Override the .helpInformation behavior
Command.prototype.helpInformation = function() {
var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
return chalk.green.bold(manPage);
};
return args;
}
/**
Invoke a HackMyResume verb.
*/
function execVerb( src, dst, opts, log ) {
loadOptions.call( this, opts );
require('./core/error-handler').init( _opts.debug );
HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
}
/**
Initialize HackMyResume options.
*/
function loadOptions( o ) {
o.opts = this.parent.opts;
// Load the specified options file (if any) and apply options
if( o.opts && String.is( o.opts )) {
var json = safeLoadJSON( PATH.relative( process.cwd(), o.opts ) );
json && ( o = EXTEND( true, o, json ) );
if( !json ) {
throw safeLoadJSON.error;
}
}
// Merge in command-line options
o = EXTEND( true, o, this.opts() );
o.silent = this.parent.silent;
o.debug = this.parent.debug;
_opts = o;
}
/**
Split multiple command-line filenames by the 'TO' keyword
*/
function splitSrcDest() {
var params = this.parent.args.filter(function(j) { return String.is(j); });
if( params.length === 0 )
throw { fluenterror: HACKMYSTATUS.resumeNotFound };
// Find the TO keyword, if any
var splitAt = _.findIndex( params, function(p) {
return p.toLowerCase() === 'to';
});
// TO can't be the last keyword
if( splitAt === params.length - 1 && splitAt !== -1 ) {
logMsg(chalk.yellow('Please ') +
chalk.yellow.bold('specify an output file') +
chalk.yellow(' for this operation or ') +
chalk.yellow.bold('omit the TO keyword') +
chalk.yellow('.') );
return;
}
return {
src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
};
}
/**
Simple logging placeholder.
*/
function logMsg( msg ) {
msg = msg || '';
_opts.silent || console.log( msg );
}
}());