diff --git a/package.json b/package.json index 21f6ca0..2a16ff2 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ "license": "MIT", "dependencies": { "chalk": "^1.1.1", + "command-line-args": "^4.0.2", + "command-line-commands": "^2.0.0", + "command-line-usage": "^4.0.0", "fs-extra": "^0.26.3", "git-config": "0.0.7", "handlebars": "^4.0.3", "in-publish": "^2.0.0", "inquirer": "^0.11.0", - "minimist": "^1.2.0", "rsvp": "^3.1.0", "semver": "^5.1.0", "shallow-copy": "0.0.1", diff --git a/src/cli.js b/src/cli.js index c8cb0f8..daec2b0 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,84 +1,210 @@ import path from 'path'; import metadata from '../package.json'; -import minimist from 'minimist'; import neon_new from './ops/neon_new'; import neon_build from './ops/neon_build'; import * as style from './ops/style'; +import parseCommands from 'command-line-commands'; +import parseArgs from 'command-line-args'; +import parseUsage from 'command-line-usage'; -function printUsage() { - console.log(); - console.log("Usage:"); - console.log(); - console.log(" neon new [@/] [--rust|-r nightly|stable|default]"); - console.log(" create a new Neon project"); - console.log(); - console.log(" neon build [--rust|-r nightly|stable|default] [--debug|-d]"); - console.log(" rebuild the project"); - console.log(); - console.log(" neon version"); - console.log(" print neon-cli version"); - console.log(); - console.log(" neon help"); - console.log(" print this usage information"); - console.log(); - console.log("neon-cli@" + metadata.version + " " + path.dirname(__dirname)); +function channel(value) { + if (!['default', 'nightly', 'beta', 'stable'].indexOf(value) > -1) { + throw new Error("Expected one of 'default', 'nightly', 'beta', or 'stable', got '" + value + "'"); + } + return value; +} + +function commandUsage(command) { + if (!spec[command]) { + let e = new Error(); + e.command = command; + e.name = 'INVALID_COMMAND'; + throw e; + } + console.error(parseUsage(spec[command].usage)); } -const SUBCOMMANDS = { - 'version': function() { - console.log(metadata.version); +const spec = { + + null: { + args: [{ name: "version", alias: "v", type: Boolean }, + { name: "help", alias: "h", type: String, defaultValue: null }], + usage: [{ + header: "Neon", + content: "Neon is a tool for building native Node.js modules with Rust." + }, { + header: "Synopsis", + content: "$ neon [options] " + }, { + header: "Command List", + content: [{ name: "new", summary: "Create a new Neon project." }, + { name: "build", summary: "(Re)build a Neon project." }, + { name: "version", summary: "Display the Neon version." }, + { name: "help", summary: "Display help information about Neon." }] + }], + action: function(options, usage) { + if (options.version && options.help === undefined) { + spec.version.action.call(this, options); + } else if (options.help !== undefined) { + commandUsage(options.help); + } else { + console.error(usage); + } + } + }, + + help: { + args: [{ name: "command", type: String, defaultOption: true }, + { name: "help", alias: "h", type: Boolean }], + usage: [{ + header: "neon help", + content: "Get help about a Neon command" + }, { + header: "Synopsis", + content: "$ neon help [command]" + }], + action: function(options) { + if (options && options.command) { + commandUsage(options.command); + } else if (options && options.help) { + commandUsage('help'); + } else { + console.error(parseUsage(spec.null.usage)); + } + } }, - 'help': function() { - printUsage(); + new: { + args: [{ name: "rust", alias: "r", type: channel, defaultValue: "default" }, + { name: "name", type: String, defaultOption: true }, + { name: "help", alias: "h", type: Boolean }], + usage: [{ + header: "neon new", + content: "Create a new Neon project." + }, { + header: "Synopsis", + content: "$ neon new [options] [@/]" + }, { + header: "Options", + optionList: [{ + name: "rust", + alias: "r", + type: channel, + description: "Rust channel (default, nightly, beta, or stable). [default: default]" + }] + }], + action: function(options) { + if (options.help) { + commandUsage('new'); + return; + } + + return neon_new(this.cwd, options.name, options.rust); + } }, - 'new': function() { - if (this.args._.length !== 1) { - printUsage(); - console.log(); - throw new Error(this.args._.length === 0 ? "You must specify a project name." : "Too many arguments."); + build: { + args: [{ name: "debug", alias: "d", type: Boolean }, + { name: "path", alias: "p", type: Boolean }, + { name: "rust", alias: "r", type: channel, defaultValue: "default" }, + { name: "modules", type: String, multiple: true, defaultOption: true }, + { name: "node_module_version", type: Number }, + { name: "help", alias: "h", type: Boolean }], + usage: [{ + header: "neon build", + content: "(Re)build a Neon project." + }, { + header: "Synopsis", + content: ["$ neon build [options]", + "$ neon build [options] [underline]{module} ..."] + }, { + header: "Options", + optionList: [{ + name: "rust", + alias: "r", + type: channel, + description: "Rust channel (default, nightly, beta, or stable). [default: default]" + }, { + name: "debug", + alias: "d", + type: Boolean, + description: "Debug build." + }, { + name: "path", + alias: "p", + type: Boolean, + description: "Specify modules by path instead of name." + }] + }], + action: async function(options) { + if (options.help) { + commandUsage('build'); + return; + } + + let modules = options.modules + ? options.modules.map(m => options.path ? path.resolve(this.cwd, m) + : path.resolve(this.cwd, 'node_modules', m)) + : [this.cwd]; + + let info = modules.length > 1; + + for (let mod of modules) { + if (info) { + console.log(style.info("building Neon package at " + (path.relative(this.cwd, mod) || "."))); + } + + await neon_build(mod, options.rust, options.debug ? 'debug' : 'release', options.node_module_version); + } } - return neon_new(this.cwd, this.args._[0], this.args.rust || this.args.r || 'default'); }, - 'build': function() { - if (this.args._.length > 0) { - printUsage(); - console.log(); - throw new Error("Too many arguments."); + version: { + args: [{ name: "help", alias: "h", type: Boolean }], + usage: [{ + header: "neon version", + content: "Display the Neon version." + }, { + header: "Synopsis", + content: "$ neon version" + }], + action: function(options) { + if (options.help) { + commandUsage('version'); + return; + } + + console.log(metadata.version); } - return neon_build(this.cwd, - this.args.rust || this.args.r || 'default', - this.args.debug || this.args.d ? 'debug' : 'release', - this.args.node_module_version); } + }; export default class CLI { constructor(argv, cwd) { - this.command = argv[2]; - this.args = minimist(argv.slice(3)); + this.argv = argv.slice(2); this.cwd = cwd; } async exec() { try { - if (!this.command) { - printUsage(); - throw null; - } - if (!SUBCOMMANDS.hasOwnProperty(this.command)) { - printUsage(); - console.log(); - throw new Error("'" + this.command + "' is not a neon command."); - } - await SUBCOMMANDS[this.command].call(this); + let { command, argv } = parseCommands([ null, 'help', 'new', 'build', 'version' ], this.argv); + + await spec[command].action.call(this, + parseArgs(spec[command].args, { argv }), + parseUsage(spec[command].usage)); } catch (e) { - if (e) { - console.log(style.error(e.message)); + spec.help.action.call(this); + + switch (e.name) { + case 'INVALID_COMMAND': + console.error(style.error("No manual entry for `neon " + e.command + "`")); + break; + + default: + console.error(style.error(e.message)); + break; } - throw e; } } } diff --git a/src/ops/neon_build.js b/src/ops/neon_build.js index e30fb22..5f02287 100644 --- a/src/ops/neon_build.js +++ b/src/ops/neon_build.js @@ -36,7 +36,7 @@ function explicit_cargo_target() { } } -function cargo(toolchain, configuration, nodeModuleVersion, target) { +function cargo(root, toolchain, configuration, nodeModuleVersion, target) { let macos = process.platform === 'darwin'; let [command, prefix] = toolchain === 'default' @@ -57,16 +57,16 @@ function cargo(toolchain, configuration, nodeModuleVersion, target) { console.log(style.info([command].concat(args).join(" "))); - return spawn(command, args, { cwd: 'native', stdio: 'inherit', env: env }); + return spawn(command, args, { cwd: path.resolve(root, 'native'), stdio: 'inherit', env: env }); } -async function main(name, configuration, target) { +async function main(root, name, configuration, target) { let pp = process.platform; let output_directory = target ? - path.resolve('native', 'target', target, configuration) : - path.resolve('native', 'target', configuration); + path.resolve(root, 'native', 'target', target, configuration) : + path.resolve(root, 'native', 'target', configuration); let dylib = path.resolve(output_directory, LIB_PREFIX[pp] + name + LIB_SUFFIX[pp]); - let index = path.resolve('native', 'index.node'); + let index = path.resolve(root, 'native', 'index.node'); console.log(style.info("generating native" + path.sep + "index.node")); @@ -74,9 +74,9 @@ async function main(name, configuration, target) { await copy(dylib, index); } -export default async function neon_build(pwd, toolchain, configuration, nodeModuleVersion) { +export default async function neon_build(root, toolchain, configuration, nodeModuleVersion) { // 1. Read the Cargo metadata. - let metadata = TOML.parse(await readFile(path.resolve('native', 'Cargo.toml'), 'utf8')); + let metadata = TOML.parse(await readFile(path.resolve(root, 'native', 'Cargo.toml'), 'utf8')); if (!metadata.lib.name) { throw new Error("Cargo.toml does not contain a [lib] section with a 'name' field"); @@ -87,10 +87,10 @@ export default async function neon_build(pwd, toolchain, configuration, nodeModu console.log(style.info("running cargo")); // 2. Build the binary. - if ((await cargo(toolchain, configuration, nodeModuleVersion, target)) !== 0) { + if ((await cargo(root, toolchain, configuration, nodeModuleVersion, target)) !== 0) { throw new Error("cargo build failed"); } // 3. Copy the dylib into the main index.node file. - await main(metadata.lib.name, configuration, target); + await main(root, metadata.lib.name, configuration, target); } diff --git a/test/acceptance/help.js b/test/acceptance/help.js index 6b13687..417f1bd 100644 --- a/test/acceptance/help.js +++ b/test/acceptance/help.js @@ -1,17 +1,111 @@ import { setup } from '../support/acceptance'; -describe('neon help', function() { - setup(); - - it('should print neon usage', function(done) { - this.spawn(['help']) - .wait('Usage:') - .wait('neon new') - .wait('neon version') - .wait('neon help') - .run(err => { - if (err) throw err; - done(); - }); +function describeHelp(cmd, should, test, args) { + describe(cmd, function() { + setup('stderr'); + + it(should, function(done) { + test(this.spawn(args), done); + }); }); -}); +} + +function testHelp(proc, done) { + return proc + .wait("Neon") + .wait("native Node.js modules with Rust") + .wait("Synopsis") + .wait("$ neon [options] ") + .wait("Command List") + .wait("new") + .wait("build") + .wait("version") + .wait("help") + .run(err => { + if (err) throw err; + done(); + }); +} + +describeHelp("neon help", "should print neon usage", testHelp, ['help']); +describeHelp("neon --help", "should print neon usage", testHelp, ['--help']); +describeHelp("neon -h", "should print neon usage", testHelp, ['-h']); + +function testHelpVersion(proc, done) { + return proc + .wait("neon version") + .wait("Display the Neon version.") + .wait("Synopsis") + .wait("$ neon version") + .run(err => { + if (err) throw err; + done(); + }); +} + +describeHelp("neon help version", "should print `neon version` usage", testHelpVersion, ['help', 'version']); +describeHelp("neon version --help", "should print `neon version` usage", testHelpVersion, ['version', '--help']); +describeHelp("neon version -h", "should print `neon version` usage", testHelpVersion, ['version', '-h']); +describeHelp("neon --help version", "should print `neon version` usage", testHelpVersion, ['--help', 'version']); +describeHelp("neon -h version", "should print `neon version` usage", testHelpVersion, ['-h', 'version']); + +function testHelpNew(proc, done) { + return proc + .wait("neon new") + .wait("Create a new Neon project") + .wait("Synopsis") + .wait("$ neon new [options] [@/]") + .wait("Options") + .wait("-r, --rust") + .run(err => { + if (err) throw err; + done(); + }); +} + +describeHelp("neon help new", "should print `neon new` usage", testHelpNew, ['help', 'new']); +describeHelp("neon new --help", "should print `neon new` usage", testHelpNew, ['new', '--help']); +describeHelp("neon new -h", "should print `neon new` usage", testHelpNew, ['new', '-h']); +describeHelp("neon --help new", "should print `neon new` usage", testHelpNew, ['--help', 'new']); +describeHelp("neon -h new", "should print `neon new` usage", testHelpNew, ['-h', 'new']); + +function testHelpBuild(proc, done) { + return proc + .wait("neon build") + .wait("(Re)build a Neon project") + .wait("Synopsis") + .wait("$ neon build [options]") + .wait("$ neon build [options] module ...") + .wait("Options") + .wait("-r, --rust") + .wait("-d, --debug") + .wait("-p, --path") + .run(err => { + if (err) throw err; + done(); + }); +} + +describeHelp("neon help build", "should print `neon build` usage", testHelpBuild, ['help', 'build']); +describeHelp("neon build --help", "should print `neon build` usage", testHelpBuild, ['build', '--help']); +describeHelp("neon build -h", "should print `neon build` usage", testHelpBuild, ['build', '-h']); +describeHelp("neon --help build", "should print `neon build` usage", testHelpBuild, ['--help', 'build']); +describeHelp("neon -h build", "should print `neon build` usage", testHelpBuild, ['-h', 'build']); + +function testHelpHelp(proc, done) { + return proc + .wait("neon help") + .wait("Get help about a Neon command") + .wait("Synopsis") + .wait("$ neon help [command]") + .run(err => { + if (err) throw err; + done(); + }); +} + +describeHelp("neon help help", "should print `neon help` usage", testHelpHelp, ['help', 'help']); +describeHelp("neon help --help", "should print `neon help` usage", testHelpHelp, ['help', '--help']); +describeHelp("neon help -h", "should print `neon help` usage", testHelpHelp, ['help', '-h']); +describeHelp("neon --help help", "should print `neon help` usage", testHelpHelp, ['--help', 'help']); +describeHelp("neon -h help", "should print `neon help` usage", testHelpHelp, ['-h', 'help']); diff --git a/test/support/acceptance.js b/test/support/acceptance.js index ccc00c7..5f19f44 100644 --- a/test/support/acceptance.js +++ b/test/support/acceptance.js @@ -5,14 +5,14 @@ import { spawn } from 'nexpect'; const NODE = process.execPath; const NEON = path.resolve('bin/cli.js'); -export function setup() { +export function setup(stream = 'all') { let tmpobj; beforeEach(function() { tmpobj = tmp.dirSync({ unsafeCleanup: true }); this.cwd = tmpobj.name; - this.spawn = (args) => spawn(NODE, [NEON].concat(args), { cwd: this.cwd }); + this.spawn = (args) => spawn(NODE, [NEON].concat(args), { cwd: this.cwd, stream, stripColors: true }); }); afterEach(function() {