clime
Version:
The command-line interface framework for TypeScript.
501 lines • 21.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const Path = require("path");
const v = require("villa");
const command_1 = require("./command");
const object_1 = require("./object");
const error_1 = require("./error");
const internal_util_1 = require("../internal-util");
const COMMAND_NAME_REGEX = /^[\w\d]+(?:-[\w\d]+)*$/;
const HELP_OPTION_REGEX = /^(?:-[h?]|--help)$/;
/**
* Clime command line interface.
*/
class CLI {
constructor(
/** Command entry name. */
name,
/** Root directory of command modules. */
roots) {
this.name = name;
roots = Array.isArray(roots) ? roots : [roots];
this.roots = roots.map((root) => {
let label;
let path;
if (typeof root === 'string') {
path = root;
}
else {
label = root.label;
path = root.path;
}
return {
label: label || 'Subcommands',
path: Path.resolve(path),
};
});
}
execute(argv, contextExtension, cwd) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof contextExtension === 'string') {
cwd = contextExtension;
contextExtension = undefined;
}
if (!cwd) {
cwd = process.cwd();
}
let { sequence, args, path, module, searchContexts, possibleUnknownCommandName, } = yield this.preProcessArguments(argv);
let description;
if (module) {
let TargetCommand = module.default;
if (TargetCommand && TargetCommand.prototype instanceof command_1.Command) {
// This is a command module with an actual command.
if (!TargetCommand.decorated) {
throw new TypeError(`Command defined in module "${path}" does not seem to be initialized, \
make sure to decorate it with \`\``);
}
TargetCommand.path = path;
TargetCommand.helpBuildingContexts = searchContexts.map((context) => {
return {
label: context.label,
dir: context.searchBase,
};
});
TargetCommand.sequence = sequence;
let argsParser = new ArgsParser(TargetCommand);
let parsedArgs = yield argsParser.parse(sequence, args, cwd, contextExtension);
if (!parsedArgs) {
return command_1.HelpInfo.build(TargetCommand);
}
let command = new TargetCommand();
let { args: commandArgs, extraArgs: commandExtraArgs, options: commandOptions, context, } = parsedArgs;
return this.executeCommand(command, commandArgs, commandExtraArgs, commandOptions, context);
}
else {
// This is a command module with only description and subcommand definitions.
description = module.description;
}
}
let helpInfo = yield command_1.HelpInfo.build({
sequence,
contexts: searchContexts.map((context) => {
return {
label: context.label,
dir: context.searchBase,
};
}),
description,
});
if (possibleUnknownCommandName) {
throw new UsageError(`Unknown subcommand "${possibleUnknownCommandName}"`, {
getHelp() {
return helpInfo;
},
});
}
if (args.length && HELP_OPTION_REGEX.test(args[0])) {
return helpInfo;
}
else {
throw helpInfo;
}
});
}
preProcessSearchBase(searchBase, possibleCommandName, aliasMap) {
return __awaiter(this, void 0, void 0, function* () {
let definitions = yield CLI.getSubcommandDefinitions(searchBase);
let definitionMap = new Map();
for (let definition of definitions) {
definitionMap.set(definition.name, definition);
let aliases = definition.aliases || (definition.alias && [definition.alias]);
if (!aliases) {
continue;
}
for (let alias of aliases) {
if (aliasMap.has(alias)) {
let targetName = aliasMap.get(alias);
if (targetName !== definition.name) {
throw new Error(`Alias "${alias}" already exists and points to "${targetName}" \
instead of "${definition.name}"`);
}
continue;
}
aliasMap.set(alias, definition.name);
}
}
possibleCommandName = definitionMap.has(possibleCommandName)
? possibleCommandName
: aliasMap.get(possibleCommandName) || possibleCommandName;
searchBase = Path.join(searchBase, possibleCommandName);
let entry = yield CLI.findEntryBySearchBase(searchBase);
return {
name: possibleCommandName,
searchBase: internal_util_1.existsDir(searchBase) ? searchBase : undefined,
path: entry && entry.path,
module: entry && entry.module,
};
});
}
/**
* Mapping the command line arguments to a specific command file.
*/
preProcessArguments(argv) {
return __awaiter(this, void 0, void 0, function* () {
let sequence = [this.name];
let possibleUnknownCommandName;
let argsIndex = 0;
let targetPath;
let targetModule;
let contexts = yield v.map(this.roots, (root) => __awaiter(this, void 0, void 0, function* () {
let path = Path.join(root.path, CLI.commandModuleDefaultFileName);
path = (yield internal_util_1.existsFile(path)) ? path : undefined;
let module;
if (path) {
module = (yield internal_util_1.dynamicImport(path));
if (module.default || !targetPath) {
targetPath = path;
targetModule = module;
}
}
return {
label: root.label,
name: this.name,
searchBase: root.path,
path,
module,
};
}));
for (let i = argsIndex; i < argv.length && contexts.length; i++) {
let possibleCommandName = argv[i];
if (!COMMAND_NAME_REGEX.test(possibleCommandName)) {
break;
}
let aliasMap = new Map();
let nextContexts = yield v.map(contexts, (context) => __awaiter(this, void 0, void 0, function* () {
let searchBaseContext = yield this.preProcessSearchBase(context.searchBase, possibleCommandName, aliasMap);
return Object.assign({ label: context.label }, searchBaseContext);
}));
let targetContexts = nextContexts.filter((context) => !!context.path);
if (!targetContexts.length) {
possibleUnknownCommandName = possibleCommandName;
break;
}
let targetContext = targetContexts[0];
for (let context of targetContexts.slice(1)) {
let module = context.module;
if (module && module.default) {
targetContext = context;
}
}
targetPath = targetContext.path;
targetModule = targetContext.module;
possibleCommandName = targetContext.name;
argsIndex = i + 1;
sequence.push(possibleCommandName);
contexts = nextContexts.filter((context) => !!context.searchBase);
}
return {
sequence,
args: argv.slice(argsIndex),
path: targetPath,
module: targetModule,
searchContexts: contexts,
possibleUnknownCommandName,
};
});
}
executeCommand(command, commandArgs, commandExtraArgs, commandOptions, context) {
let executeMethodArgs = commandArgs.concat();
if (commandExtraArgs) {
executeMethodArgs.push(commandExtraArgs);
}
if (commandOptions) {
executeMethodArgs.push(commandOptions);
}
if (context) {
executeMethodArgs.push(context);
}
return command.execute(...executeMethodArgs);
}
static get commandModuleDefaultFileName() {
return `${this.commandModuleDefaultName}${this.commandModuleExtension}`;
}
/**
* @internal
* Get subcommands definition written as `export subcommands = [...]`.
*/
static getSubcommandDefinitions(searchBase) {
return __awaiter(this, void 0, void 0, function* () {
let entry = yield this.findEntryBySearchBase(searchBase);
if (!entry || !entry.module) {
return [];
}
return entry.module.subcommands || [];
});
}
static findEntryBySearchBase(searchBase) {
return __awaiter(this, void 0, void 0, function* () {
let possiblePaths = [
`${searchBase}${this.commandModuleExtension}`,
Path.join(searchBase, this.commandModuleDefaultFileName),
];
for (let possiblePath of possiblePaths) {
if (yield internal_util_1.existsFile(possiblePath)) {
return {
path: possiblePath,
module: (yield internal_util_1.dynamicImport(possiblePath)),
};
}
}
if (yield internal_util_1.existsDir(searchBase)) {
return {
path: searchBase,
module: undefined,
};
}
return undefined;
});
}
}
/**
* The name of the default sub-command module.
*
* E.g.:
*
* ```ts
* import {CLI} from 'clime';
*
* CLI.commandModuleDefaultName = 'mySpecialDefaultName';
* ```
*/
CLI.commandModuleDefaultName = 'default';
/**
* The extension to use when looking up sub-command modules.
*
* E.g.:
*
* ```ts
* import {CLI} from 'clime';
*
* CLI.commandModuleExtension = '.cjs'
* ```
*/
CLI.commandModuleExtension = '.js';
exports.CLI = CLI;
class ArgsParser {
constructor(command) {
this.helpProvider = command;
this.paramDefinitions = command.paramDefinitions;
this.requiredParamsNumber = command.requiredParamsNumber;
this.paramsDefinition = command.paramsDefinition;
this.optionsConstructor = command.optionsConstructor;
this.optionDefinitions = command.optionDefinitions;
this.skippedArgsEnabled = command.skippedArgsEnabled;
this.contextConstructor = command.contextConstructor;
if (this.optionDefinitions) {
this.optionFlagMapping = new Map();
this.optionDefinitionMap = new Map();
for (let definition of this.optionDefinitions) {
let { name, flag } = definition;
this.optionDefinitionMap.set(name, definition);
if (flag) {
this.optionFlagMapping.set(flag, name);
}
}
}
}
parse(sequence, args, cwd, contextExtension) {
return __awaiter(this, void 0, void 0, function* () {
let that = this;
let ContextConstructor = this.contextConstructor || command_1.Context;
let context = new ContextConstructor({
cwd,
commands: sequence,
}, contextExtension);
args = args.concat();
let OptionConstructor = this.optionsConstructor;
let optionDefinitions = this.optionDefinitions;
let optionDefinitionMap = this.optionDefinitionMap || new Map();
let optionFlagMapping = this.optionFlagMapping || new Map();
let requiredOptionSet;
let paramDefinitions = this.paramDefinitions || [];
let pendingParamDefinitions = paramDefinitions.concat();
let paramsDefinition = this.paramsDefinition;
let argsNumber = args.length;
let commandArgs = [];
let commandExtraArgs = paramsDefinition && [];
let commandOptions;
if (OptionConstructor) {
commandOptions = new OptionConstructor();
requiredOptionSet = new Set();
for (let definition of optionDefinitions) {
let { name, key, type, required, validators, toggle, default: defaultValue, } = definition;
if (required) {
requiredOptionSet.add(name);
}
if (toggle) {
commandOptions[key] = false;
}
else {
commandOptions[key] =
typeof defaultValue === 'string'
? yield castArgument(defaultValue, name, type, validators, true)
: defaultValue;
}
}
}
while (args.length) {
let arg = args.shift();
if (arg === '-?' ||
(arg === '-h' && !optionFlagMapping.has('h')) ||
(arg === '--help' && !optionDefinitionMap.has('help'))) {
return undefined;
}
if (this.skippedArgsEnabled && arg === '--') {
break;
}
if (arg[0] === '-' && isNaN(Number(arg))) {
if (arg[1] === '-') {
yield consumeToggleOrOption(arg.substr(2));
}
else {
yield consumeFlags(arg.substr(1));
}
}
else if (pendingParamDefinitions.length) {
let definition = pendingParamDefinitions.shift();
let casted = yield castArgument(arg, definition.name, definition.type, definition.validators, false);
commandArgs.push(casted);
}
else if (paramsDefinition) {
let casted = yield castArgument(arg, paramsDefinition.name, paramsDefinition.type, paramsDefinition.validators, false);
commandExtraArgs.push(casted);
}
else {
throw new UsageError(`Expecting ${paramDefinitions.length} parameter(s) at most but got ${argsNumber} instead`, this.helpProvider);
}
}
context.skippedArgs = args;
{
let expecting = this.requiredParamsNumber;
let got = commandArgs.length;
if (got < expecting) {
let missingArgNames = pendingParamDefinitions
.slice(0, expecting - got)
.map((definition) => `\`${definition.name}\``);
throw new UsageError(`Expecting parameter(s) ${missingArgNames.join(', ')}`, this.helpProvider);
}
}
let missingOptionNames = requiredOptionSet && Array.from(requiredOptionSet);
if (missingOptionNames && missingOptionNames.length) {
throw new UsageError(`Missing required option(s) \`${missingOptionNames.join('`, `')}\``, this.helpProvider);
}
for (let definition of pendingParamDefinitions) {
let defaultValue = definition.default;
let value = typeof defaultValue === 'string'
? yield castArgument(defaultValue, definition.name, definition.type, definition.validators, true)
: defaultValue;
commandArgs.push(value);
}
if (paramsDefinition &&
paramsDefinition.required &&
!commandExtraArgs.length) {
throw new UsageError(`Expecting at least one element for variadic parameters \`${paramsDefinition.name}\``, this.helpProvider);
}
return {
args: commandArgs,
extraArgs: paramsDefinition && commandExtraArgs,
options: commandOptions,
context: this.contextConstructor ? context : undefined,
};
function consumeFlags(flags) {
return __awaiter(this, void 0, void 0, function* () {
for (let i = 0; i < flags.length; i++) {
let flag = flags[i];
if (!optionFlagMapping.has(flag)) {
throw new UsageError(`Unknown option flag "${flag}"`, that.helpProvider);
}
let name = optionFlagMapping.get(flag);
let definition = optionDefinitionMap.get(name);
if (definition.required) {
requiredOptionSet.delete(name);
}
if (definition.toggle) {
commandOptions[definition.key] = true;
}
else {
if (i !== flags.length - 1) {
throw new UsageError('Only the last flag in a sequence can refer to an option instead of a toggle', that.helpProvider);
}
yield consumeOption(definition);
}
}
});
}
function consumeToggleOrOption(name) {
return __awaiter(this, void 0, void 0, function* () {
if (!optionDefinitionMap.has(name)) {
throw new UsageError(`Unknown option \`${name}\``, that.helpProvider);
}
let definition = optionDefinitionMap.get(name);
if (definition.required) {
requiredOptionSet.delete(name);
}
if (definition.toggle) {
commandOptions[definition.key] = true;
}
else {
yield consumeOption(definition);
}
});
}
function consumeOption(definition) {
return __awaiter(this, void 0, void 0, function* () {
let { name, key, type, validators } = definition;
let arg = args.shift();
if (arg === undefined) {
throw new UsageError(`Expecting value for option \`${name}\``, that.helpProvider);
}
if (arg[0] === '-' && isNaN(Number(arg))) {
throw new UsageError(`Expecting a value instead of an option or toggle "${arg}" for option \`${name}\``, that.helpProvider);
}
commandOptions[key] = yield castArgument(arg, name, type, validators, false);
});
}
function castArgument(arg, name, type, validators, usingDefault) {
return __awaiter(this, void 0, void 0, function* () {
let castingContext = object_1.buildCastingContext(context, {
name,
default: usingDefault,
validators,
});
return object_1.cast(arg, type, castingContext);
});
}
});
}
}
class UsageError extends error_1.ExpectedError {
constructor(message, helpProvider) {
super(message);
this.helpProvider = helpProvider;
}
print(stdout, stderr) {
const _super = name => super[name];
return __awaiter(this, void 0, void 0, function* () {
_super("print").call(this, stdout, stderr);
let help = yield this.helpProvider.getHelp();
help.print(stdout, stderr);
});
}
}
exports.UsageError = UsageError;
//# sourceMappingURL=cli.js.map