diff --git a/lib/cli.js b/lib/cli.js index dc4d15d1..283a72ca 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,6 +1,14 @@ #!/usr/bin/env node -require('@gustavnikolaj/async-main-wrap')(require('./subfont'))( - require('./parseCommandLineOptions')(), - console -); +const { yargs, help, ...options } = require('./parseCommandLineOptions')(); + +require('@gustavnikolaj/async-main-wrap')(require('./subfont'), { + processError(err) { + yargs.showHelp(); + if (err.constructor === SyntaxError) { + // Avoid rendering a stack trace for the wrong usage errors + err.customOutput = err.message; + } + return err; + } +})(options, console); diff --git a/lib/parseCommandLineOptions.js b/lib/parseCommandLineOptions.js index d439cef9..429068d7 100644 --- a/lib/parseCommandLineOptions.js +++ b/lib/parseCommandLineOptions.js @@ -1,22 +1,9 @@ -const yargs = require('yargs'); - module.exports = function parseCommandLineOptions(argv) { - const { - root, - canonicalRoot, - output, - debug, - dryRun, - silent, - inlineFonts, - inlineCss, - fontDisplay, - inPlace, - recursive, - fallbacks, - dynamic, - _: inputFiles - } = yargs(argv) + let yargs = require('yargs'); + if (argv) { + yargs = yargs(argv); + } + yargs .usage( 'Create optimal font subsets from your actual font usage.\n$0 [options] ' ) @@ -104,22 +91,13 @@ module.exports = function parseCommandLineOptions(argv) { type: 'boolean', default: false }) - .wrap(72).argv; + .wrap(require('yargs').terminalWidth()); + + const { _: inputFiles, ...rest } = yargs.argv; return { - root, - canonicalRoot, - output, - debug, - dryRun, - silent, - inlineFonts, - inlineCss, - fontDisplay, - inPlace, + yargs, inputFiles, - recursive, - fallbacks, - dynamic + ...rest }; }; diff --git a/lib/subfont.js b/lib/subfont.js index 2cb03a44..89d1bbd6 100644 --- a/lib/subfont.js +++ b/lib/subfont.js @@ -36,7 +36,7 @@ module.exports = async function subfont( if (rootUrl) { if (rootUrl.startsWith('file:')) { - console.error(`Guessing --root from input files: ${rootUrl}`); + console.warn(`Guessing --root from input files: ${rootUrl}`); } else { rootUrl = urlTools.ensureTrailingSlash(rootUrl); } @@ -46,20 +46,20 @@ module.exports = async function subfont( inputUrls = [`${rootUrl}**/*.html`]; console.warn(`No input files specified, defaulting to ${inputUrls[0]}`); } else { - throw new Error( + throw new SyntaxError( "No input files and no --root specified (or it isn't file:), cannot proceed.\n" ); } if (!inputUrls[0].startsWith('file:') && !outRoot && !dryRun) { - throw new Error( + throw new SyntaxError( '--output has to be specified when using non-file input urls' ); } if (!inPlace && !outRoot && !dryRun) { - throw new Error( - 'Either --output, --in-place, or --dryrun has to be specified' + throw new SyntaxError( + 'Either --output, --in-place, or --dry-run has to be specified' ); } diff --git a/test/cli.js b/test/cli.js new file mode 100644 index 00000000..0e7015be --- /dev/null +++ b/test/cli.js @@ -0,0 +1,122 @@ +const pathModule = require('path'); +const expect = require('unexpected').clone(); + +const childProcess = require('child_process'); + +function consumeStream(stream) { + return new Promise((resolve, reject) => { + const buffers = []; + stream + .on('data', buffer => buffers.push(buffer)) + .on('end', () => resolve(Buffer.concat(buffers))) + .on('error', reject); + }); +} + +async function run(commandAndArgs, stdin) { + if (typeof commandAndArgs !== 'undefined' && !Array.isArray(commandAndArgs)) { + commandAndArgs = [commandAndArgs]; + } + + const proc = childProcess.spawn(commandAndArgs[0], commandAndArgs.slice(1)); + + const promises = { + exit: new Promise((resolve, reject) => { + proc.on('error', reject).on('exit', exitCode => { + if (exitCode === 0) { + resolve(); + } else { + const err = new Error(`Child process exited with ${exitCode}`); + err.exitCode = exitCode; + reject(err); + } + }); + }), + stdin: new Promise((resolve, reject) => { + proc.stdin.on('error', reject).on('close', resolve); + }), + stdout: consumeStream(proc.stdout), + stderr: consumeStream(proc.stderr) + }; + + if (typeof stdin === 'undefined') { + proc.stdin.end(); + } else { + proc.stdin.end(stdin); + } + + try { + await Promise.all(Object.values(promises)); + return [await promises.stdout, await promises.stderr]; + } catch (err) { + err.stdout = await promises.stdout; + err.stderr = await promises.stderr; + throw err; + } +} + +async function runSubfont(...args) { + const proc = childProcess.spawn( + pathModule.resolve(__dirname, '..', 'lib', 'cli.js'), + args + ); + + const promises = { + exit: new Promise((resolve, reject) => { + proc.on('error', reject).on('exit', exitCode => { + if (exitCode === 0) { + resolve(); + } else { + const err = new Error(`Child process exited with ${exitCode}`); + err.exitCode = exitCode; + reject(err); + } + }); + }), + stdin: new Promise((resolve, reject) => { + proc.stdin.on('error', reject).on('close', resolve); + }), + stdout: consumeStream(proc.stdout), + stderr: consumeStream(proc.stderr) + }; + + proc.stdin.end(); + + let err; + try { + await Promise.all(Object.values(promises)); + } catch (_err) { + err = _err; + } + return { + err, + stdout: (await promises.stdout).toString('utf-8'), + stderr: (await promises.stderr).toString('utf-8') + }; +} + +describe('cli', function() { + it('should display usage info if --help is passed', async function() { + const { err, stdout } = await runSubfont('--help'); + expect(err, 'to be falsy'); + expect(stdout, 'to contain', 'Options:'); + expect(stdout, 'not to contain', 'No input files'); + }); + + it('should display usage info if an error is encountered', async function() { + const { err, stderr } = await runSubfont('i-do-not-exist.html'); + expect(err, 'to have property', 'exitCode', 1); + expect(stderr, 'to contain', 'Options:'); + }); + + it('should a wrong usage error without a stack trace', async function() { + const { err, stderr } = await runSubfont('https://example.com'); + expect(err, 'to have property', 'exitCode', 1); + expect( + stderr, + 'to contain', + '--output has to be specified when using non-file input urls' + ); + expect(stderr, 'not to match', /^\s+at/m); + }); +}); diff --git a/test/parseCommandLineOptions.js b/test/parseCommandLineOptions.js index 983c0cee..f4331687 100644 --- a/test/parseCommandLineOptions.js +++ b/test/parseCommandLineOptions.js @@ -10,7 +10,7 @@ describe('parseCommandLineOptions', function() { '--no-fallbacks', '--recursive' ]), - 'to equal', + 'to satisfy', { root: undefined, canonicalRoot: undefined,