mirror of
https://github.com/JuanCanham/HackMyResume.git
synced 2024-11-22 08:20:11 +00: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:
parent
3b38c4818f
commit
d878270bc6
231
src/index.js
231
src/index.js
@ -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
239
src/main.js
Normal 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 );
|
||||
}
|
||||
|
||||
}());
|
Loading…
Reference in New Issue
Block a user