UNPKG

neon-cli

Version:

Build and load native Rust/Neon modules.

398 lines (364 loc) 9.9 kB
import path from "path"; import neon_new from "./ops/neon_new"; import neon_build from "./ops/neon_build"; import neon_clean from "./ops/neon_clean"; import * as style from "./style"; import cliCommands = require("command-line-commands"); import cliArgs = require("command-line-args"); import cliUsage = require("command-line-usage"); import log from "./log"; import { setup as setupLogging } from "./log"; import * as JSON from "ts-typed-json"; import { Toolchain } from "./rust"; let metadata = JSON.loadSync(path.resolve(__dirname, "..", "package.json")); function commandUsage(command: string) { if (!spec[command]) { let e = new Error(); (e as any).command = command; e.name = "INVALID_COMMAND"; throw e; } console.error(cliUsage(spec[command].usage)); } function logIf(multiple: boolean, action: string, cwd: string, module: string) { if (multiple) { log(action + " Neon package at " + (path.relative(cwd, module) || ".")); } } function parseArgv(argv: string[]): { cli: string[]; extra: string[] } { let splitAt = argv.indexOf("--"); // No additional arguments provided if (splitAt < 0) { return { cli: argv, extra: [], }; } return { cli: argv.slice(0, splitAt), extra: argv.slice(splitAt + 1), }; } function parseModules(cwd: string, names: string[], paths: boolean) { let modules = names.length ? names.map((m) => paths ? path.resolve(cwd, m) : path.resolve(cwd, "node_modules", m) ) : [cwd]; return { modules, multiple: modules.length > 1, }; } type Action = ( this: CLI, options: Record<string, unknown>, usage: string ) => void; type Command = { args: cliArgs.OptionDefinition[]; usage: cliUsage.Sections; action: Action; }; type Spec = Record<string, Command>; const spec: 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] <command> -- [cargo options]", }, { header: "Command List", content: [ { name: "new", summary: "Create a new Neon project." }, { name: "build", summary: "(Re)build a Neon project." }, { name: "clean", summary: "Remove build artifacts from 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 as string); } 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 as string); } else if (options && options.help) { commandUsage("help"); } else { console.error(cliUsage(spec.null.usage)); } }, }, new: { args: [ { name: "name", type: String, defaultOption: true }, { name: "neon", alias: "n", type: String }, { name: "features", alias: "f", type: String }, { name: "no-default-features", type: Boolean }, { name: "help", alias: "h", type: Boolean }, ], usage: [ { header: "neon new", content: "Create a new Neon project.", }, { header: "Synopsis", content: "$ neon new [options] [@<scope>/]<name>", }, { header: "Options", optionList: [ { name: "neon", alias: "n", type: String, description: "Specify a semver version of Neon or path to a local Neon repository.", }, { name: "features", alias: "f", type: String, description: "Space-separated list of experimental Neon features to enable.", }, { name: "no-default-features", type: Boolean, description: "Do not activate the `default` Neon feature.", }, ], }, ], action: function (options) { if (options.help) { commandUsage("new"); } else if (!options.name) { console.error(cliUsage(spec.new.usage)); } else { return neon_new( this.cwd, options.name as string, (options.neon || null) as string | null, (options.features || null) as string | null, !!options["no-default-features"] ); } return; }, }, build: { args: [ { name: "release", alias: "r", type: Boolean }, { name: "path", alias: "p", type: Boolean }, { name: "modules", type: String, multiple: true, defaultOption: true }, { 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: "release", alias: "r", type: Boolean, description: "Release 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 extra = options.extra as string[]; let { modules, multiple } = parseModules( this.cwd, (options.modules || []) as string[], !!options.path ); for (let module of modules) { logIf(multiple, "building", this.cwd, module); await neon_build(module, this.toolchain, !!options.release, extra); } }, }, clean: { args: [ { name: "path", alias: "p", type: Boolean }, { name: "modules", type: String, multiple: true, defaultOption: true }, { name: "help", alias: "h", type: Boolean }, ], usage: [ { header: "neon clean", content: "Remove build artifacts from a Neon project.", }, { header: "Synopsis", content: [ "$ neon clean [options]", "$ neon clean [options] {underline module} ...", ], }, { header: "Options", optionList: [ { name: "path", alias: "p", type: Boolean, description: "Specify modules by path instead of name.", }, ], }, ], action: async function (options) { if (options.help) { commandUsage("clean"); return; } let { modules, multiple } = parseModules( this.cwd, (options.modules || []) as string[], !!options.path ); for (let module of modules) { logIf(multiple, "cleaning", this.cwd, module); await neon_clean(module); } }, }, 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(JSON.asObject(metadata).version); }, }, }; export default class CLI { readonly toolchain: Toolchain; readonly argv: string[]; readonly cwd: string; constructor(argv: string[], cwd: string) { // Check for a toolchain argument in the style of Rust tools (e.g., `neon +nightly build`). if (argv.length > 2 && argv[2].trim().startsWith("+")) { this.toolchain = argv[2].substring(1).trim() as Toolchain; this.argv = argv.slice(3); } else { this.toolchain = "default"; this.argv = argv.slice(2); } this.cwd = cwd; } async exec() { setupLogging((msg) => { console.log(style.info(msg)); }); let parsed; try { parsed = cliCommands( [null, "help", "new", "build", "clean", "version"], this.argv ); } catch (e) { 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; } process.exit(1); } try { let { command, argv } = parsed; let { cli, extra } = parseArgv(argv); let options = { extra, ...cliArgs(spec[command].args, { argv: cli }) }; await spec[command].action.call( this, options, cliUsage(spec[command].usage) ); } catch (e) { console.error(style.error(e.message)); console.error(); console.error(e.stack); throw e; } } }