@stencila/jesta
Version:
Stencila plugin for executable documents using JavaScript
321 lines (297 loc) • 12.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = exports.cli = void 0;
const schema_1 = require("@stencila/schema");
const minimist_1 = __importDefault(require("minimist"));
const path_1 = __importDefault(require("path"));
const readline_1 = __importDefault(require("readline"));
const types_1 = require("./types");
const readline_2 = require("./util/readline");
/**
* Expose a command line interface for the plugin.
*
* The command line interface is intended to be simple and mainly
* for use by plugin developers and users that want to use
* plugins standalone.
*
* Note: This function is a simple wrapper around the `run` function that
* passes it the args vector and exits on error.
*/
// istanbul ignore next
function cli() {
run
.bind(this)(process.argv.slice(2))
.then(() => process.exit(0))
.catch((error) => {
console.error(error instanceof Error && !process.argv.includes('--debug')
? error.message
: error);
process.exit(1);
});
}
exports.cli = cli;
/**
* Run a command.
*
* Note: This function is separate from the `cli` function to facilitate testing.
*
* @param plugin The plugin to run the command on (Jesta or a derived plugin)
* @param argv The vector of string arguments
*/
async function run(argv) {
var _a;
const { name: pluginName, softwareVersion, description } = this.manifest();
let { _: [method, ...args], ...options } = minimist_1.default(argv, {
boolean: ['force', 'debug', 'interact', 'update'],
});
let calls = [];
if (method === null || method === void 0 ? void 0 : method.includes('+')) {
calls = method.split('+');
method = types_1.Method.pipe;
}
switch (method) {
// Methods are arranged in groups below according to the
// arguments they require
// Note: ...options should be sent to all `dispatcher` calls
// Some of the assignments below from options are type unsafe
// but we allow these as the dispatch function handles type checking
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
case 'manifest':
return console.log(JSON.stringify(this.manifest(), null, ' '));
case 'serve':
return this.serve();
case types_1.Method.convert:
case types_1.Method.pull: {
const input = url(args[0]);
const output = url(args[1]);
await this.dispatch(method, {
input,
output,
...options,
});
return;
}
case types_1.Method.select: {
const input = url(args[0]);
const query = args[1];
const output = url(args[2]);
const node = await this.dispatch(types_1.Method.import, {
input,
...options,
format: options.from,
});
const select = async (query, output) => {
const selected = await this.dispatch(types_1.Method.select, {
node,
query,
...options,
});
if (selected !== undefined && output !== undefined)
await this.dispatch(types_1.Method.export, {
node: selected,
output,
...options,
format: options.to,
});
else
console.log(selected);
};
if (options.interact === true) {
const rl = readline_1.default.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '>> ',
});
readline_2.persist(rl, pluginName, 'select');
rl.prompt();
for await (const line of rl) {
select(line, output)
.then(() => rl.prompt())
.catch(console.error);
}
}
else {
await select(query, output);
}
return;
}
case types_1.Method.build:
case types_1.Method.clean:
case types_1.Method.compile:
case types_1.Method.enrich:
case types_1.Method.pipe:
case types_1.Method.upcast:
case types_1.Method.downcast:
case types_1.Method.validate: {
const input = url(args[0]);
const output = url((_a = args[1]) !== null && _a !== void 0 ? _a : input);
cd(input);
const node = await this.dispatch(types_1.Method.import, {
input,
...options,
format: options.from,
});
const result = await this.dispatch(method, { node, ...options, calls });
await this.dispatch(types_1.Method.export, {
node: result,
output,
...options,
format: options.to,
});
return;
}
case 'run':
case types_1.Method.execute:
case types_1.Method.vars:
case types_1.Method.get:
case types_1.Method.set:
case types_1.Method.delete:
case types_1.Method.funcs:
case types_1.Method.call: {
const input = url(args[0]);
const name = args[1];
const value = args[2]; // for set
if (method === types_1.Method.execute && options.interact === true) {
const rl = readline_1.default.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'js> ',
});
readline_2.persist(rl, pluginName, 'execute');
rl.prompt();
for await (const line of rl) {
const node = schema_1.codeChunk({
programmingLanguage: 'js',
text: line,
});
this.dispatch(types_1.Method.execute, {
node,
...options,
})
.then((node) => {
const { outputs, errors } = node;
if (outputs) {
for (const output of outputs)
console.log(output);
}
if (errors) {
for (const error of errors) {
const { errorType, errorMessage } = error;
console.error(`${errorType !== null && errorType !== void 0 ? errorType : 'Error'}: ${errorMessage}`);
}
}
rl.prompt();
})
.catch(console.error);
}
return;
}
const node = await this.dispatch(types_1.Method.import, {
input,
...options,
});
await this.dispatch(types_1.Method.execute, { node, ...options });
if (method === 'run')
await this.serve();
else {
let args_ = {};
if (method === types_1.Method.call) {
args_ = args.slice(2).reduce((prev, curr, index) => {
const match = /\w+=.*/.exec(curr);
const [name, json] = match ? match.slice(1) : [`${index}`, curr];
const value = JSON.parse(json);
return { ...prev, [name]: value };
}, {});
}
const result = await this.dispatch(method, {
name,
value,
args: args_,
...options,
});
console.log(result);
}
return;
}
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
case 'help':
case undefined: {
const methodName = args[0];
// Method-specific help
if (methodName !== undefined) {
// Just print out the schema for the method
const schema = this[methodName].schema;
if (schema === undefined) {
return console.log(`No schema yet defined for method "${methodName}", sorry.`);
}
return console.log(JSON.stringify(schema, null, ' '));
}
// Generic help
return console.log(`
${pluginName} ${softwareVersion}: ${description}
Usage:
${pluginName} <command>
Primary commands (required for plugin integration)
manifest Get ${pluginName}'s manifest
serve Serve ${pluginName} over stdio
Secondary commands (mainly for plugin testing and development)
help Print this message
help <command> Print the JSON Schema for a command
update [codemeta.json] Update and get ${pluginName}'s manifest
pull <in> <out> Pull content from <in> to <out>
convert <in> <out> Convert document <in> to <out>
validate <in> [out] Validate document <in> (save as [out])
reshape <in> [out] Reshape document <in> (save as [out])
enrich <in> [out] Enrich document <in> (save as [out])
select <in> <query> [out] Select nodes from document <in> (save as [out])
compile <in> [out] Compile document <in> (save as [out])
build <in> [out] Build document <in> (save as [out])
execute <in> [out] Execute document <in> (save as [out])
clean <in> [out] Clean document <in> (save as [out])
vars <in> List variables in document <in>
get <in> <name> Get a variable from document <in>
set <in> <name> <value> Set a variable in document <in>
delete <in> <name> Delete a variable from document <in>
funcs <in> List functions in document <in>
call <in> [func] [name=val] Call document <in> (or a function within it)
run <in> Run document <in> (execute and serve)
Notes:
- use the --debug option for debug level logging and error stack traces
- \`in\` and \`out\` are file paths or URLs (e.g. http://..., file://...);
but only some URL protocols may be supported (see manifest)
- most commands with an \`in\` or \`out\` argument support the \`--format\` option
- commands with both \`in\` and \`out\` arguments support \`--from\` and
\`--to\` options for the respective formats
- \`select\` supports the \`--lang\` option for the query language
- \`validate\`, \`compile\`, \`build\` and \`execute\` support the \`--force\` option
- \`call\` accepts \`name=val\` pairs for string parameters and \`name:=json\` for
other parameters
- some methods can be piped together, e.g. \`clean+compile+build+execute\`
For a more advanced command line interface install ${pluginName} as a Stencila plugin
and use it from there: \`stencila plugins install ${pluginName}\`.
`.trim());
}
default:
console.error(`Unknown command "${method}"`);
}
}
exports.run = run;
function url(value) {
if (value === undefined)
return undefined;
if (value === '-')
return 'stdio://';
return /^([a-z]{2,6}):\/\//.test(value)
? value
: `file://${path_1.default.resolve(value)}`;
}
/**
* Change the working directory to directory of a document.
*/
function cd(url) {
if (url === null || url === void 0 ? void 0 : url.startsWith('file://'))
process.chdir(path_1.default.dirname(url.slice(7)));
}