neon-cli
Version:
Build and load native Rust/Neon modules.
398 lines (364 loc) • 9.9 kB
text/typescript
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;
}
}
}