UNPKG

@villedemontreal/scripting

Version:
299 lines 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScriptBase = exports.TESTING_SCRIPT_NAME_PREFIX = void 0; const caporal_1 = require("@villedemontreal/caporal"); const general_utils_1 = require("@villedemontreal/general-utils"); const _ = require("lodash"); const path = require("path"); const configs_1 = require("./config/configs"); /* eslint-disable @typescript-eslint/no-empty-object-type */ /** * A script with a name starting with this prefix * will only be registered on Caporal when tests * are running (ie: when the NODE_APP_INSTANCE env * var is "tests"). */ exports.TESTING_SCRIPT_NAME_PREFIX = 'testing:'; let projectDirectDependencies; /** * Base class for a Script. * * You can parametrize it so `this.options` and `this.args` * are typed. */ class ScriptBase { constructor(actionParams) { this._actionParams = actionParams; } get actionParams() { if (!this._actionParams) { throw new Error(`No actions parameters specified!`); } return this._actionParams; } /** * Dependencies required for the script to run properly. */ get requiredDependencies() { return []; } /** * The script's arguments. */ get args() { return (this.actionParams.args || {}); } /** * The script's options. */ get options() { return (this.actionParams.options || {}); } /** * The script's logger. Will respect any specified log * level (for example if the script was called with `--silent`). */ get logger() { return this.actionParams.logger; } /** * Register the script on Caporal */ async registerScript(caporal) { const command = caporal.command(this.name, `${this.description}\n`, this.commandConfig); await this.addAction(command); await this.addHelpBody(command); await this.configure(command); } async addAction(command) { command.action(async (params) => { const script = new this.constructor(params); await script.run(); }); } async addHelpBody(command) { command.help(this.description); // only the description by default } /** * WARNING: The code in this method only makes sense *when launching * a new process*! Using this to run code in the current process * will not result in the proper configs to be used since configs * are already loaded. * * Instead of using this method, you should probably use `invokeShellCommand()` * with the `useTestsNodeAppInstance` param set to `true`. */ async withTestsNodeAppInstance(runner) { const nodeAppInstanceOriginal = process.env[general_utils_1.globalConstants.envVariables.NODE_APP_INSTANCE]; process.env[general_utils_1.globalConstants.envVariables.NODE_APP_INSTANCE] = general_utils_1.globalConstants.appInstances.TESTS; try { return await runner(); } finally { if (nodeAppInstanceOriginal) { process.env[general_utils_1.globalConstants.envVariables.NODE_APP_INSTANCE] = nodeAppInstanceOriginal; } else { delete process.env[general_utils_1.globalConstants.envVariables.NODE_APP_INSTANCE]; } } } /** * Invokes the specified script. * * @param scriptType the class of a Script to invoke * @param options specify the target options for the script to invoke. Those options * will be *merged* to the current global options, if any. * @param args specify the target args for the script to invoke */ async invokeScript(scriptType, options, args) { const allOptions = this.addGlobalOptions(options); const actionParams = { ...this.actionParams, options: allOptions, args, }; const script = new scriptType(actionParams); return await script.run(); } /** * Execute a shell command. * * This function is a promisified version of Node's `spawn()` * with extra options added * ( https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options ). * * Will fail if the process returns a code different than * `options.successExitCode` ("0" by default). The exit * code would then be available in the generated Error: * `err.exitCode`. * * @param bin The executable program to call. * * @param args The arguments for the program. * * @param options.successExitCodes The acceptable codes the * process must exit with to be considered as a success. * Defaults to [0]. * * @param options.outputHandler A function that will receive * the output of the process (stdOut and stdErr). * This allows you to capture this output and manipulate it. * No handler by default. * * @param options.disableConsoleOutputs Set to `true` in order * to disable outputs in the current parent process * (you can still capture them using a `options.dataHandler`). * Defaults to `false`. * * @param options.stdio See https://nodejs.org/api/child_process.html#child_process_options_stdio * Defaults to `['inherit', 'pipe', 'pipe']`. * * @param options.useShellOption See the "shell" option: * https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options * Defaults to `true`. * * @param options.escapeArgs will automatically escape the submitted args. * Defaults to `false`. * * @param options.useTestsNodeAppInstance Execute the specified command with the * "NODE_APP_INSTANCE" env var set to "tests". This makes the testing configurations * be used in the launched process. * * @returns The exit code *when the execution is a success*. This may be useful when more * than one exit codes can be considered as a success (those specified using * `options.successExitCodes`). Note that if an error occures, an `ExecError` is thrown * so nothing is returned (see below). * * @throws Will fail with a `ExecError` error if the process returns a code different than * `options.successExitCodes` ("0" by default). The exit code would then be available in the * generated Error: `err.exitCode.` */ async invokeShellCommand(bin, args, options) { const useTestsNodeAppInstance = options?.useTestsNodeAppInstance ?? false; const execOptions = { ...options }; delete execOptions?.useTestsNodeAppInstance; this.logger.info(`Executing: ${bin} ${args}`); if (useTestsNodeAppInstance) { return await this.withTestsNodeAppInstance(async () => { return await general_utils_1.utils.exec(bin, args, execOptions); }); } return await general_utils_1.utils.exec(bin, args, execOptions); } getCommand() { const command = configs_1.configs.caporal.getCommands().find((c) => c.name === this.name); return command; } getCommandOptionsNames() { const optionsNames = new Set(); const command = this.getCommand(); if (command) { for (const option of command.options) { option.allNames.forEach((name) => optionsNames.add(name)); } } return optionsNames; } addGlobalOptions(options) { const currentGlobalOptions = {}; const commandOptionsnames = this.getCommandOptionsNames(); for (const [key, val] of Object.entries(this.options)) { if (!commandOptionsnames.has(key)) { currentGlobalOptions[key] = val; } } return { ...currentGlobalOptions, ...options, }; } /** * Will be used to identify the script * when outputing console messages. */ get outputName() { return this.name; } get commandConfig() { return {}; // nothing by default } /** * Override this method in order to add * options or to configure a script that * requires more information than a name and * description. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars async configure(command) { // nothing by default } /** * Get the project direct dependencies (those * explicitly listed in its `package.json`). */ async getProjectDirectDependencies() { if (!projectDirectDependencies) { const packageJsonObj = await Promise.resolve(`${path.join(configs_1.configs.projectRoot, 'package.json')}`).then(s => require(s)); projectDirectDependencies = [ ...Object.keys(packageJsonObj.dependencies || {}), ...Object.keys(packageJsonObj.devDependencies || {}), ]; } return projectDirectDependencies; } /** * Returns `true` if the specified dependency is a * direct dependency in the project. */ async isProjectDirectDependency(dependencyName) { return (await this.getProjectDirectDependencies()).includes(dependencyName); } /** * Validate the required dependencies. */ async validateRequiredDependencies() { const requiredDeps = this.requiredDependencies; const projectDeps = await this.getProjectDirectDependencies(); const missingDirectDeps = _.difference(requiredDeps, projectDeps); if (missingDirectDeps && missingDirectDeps.length > 0) { this.logger.warn(`This script requires some dependencies that are not direct dependencies in your project:`); for (const missingDep of missingDirectDeps) { this.logger.warn(`- ${missingDep}`); } this.logger.warn(`The script may still work if those dependencies are available ${caporal_1.chalk.italic('transitively')}, but it may be a good idea to add them directly to your "${caporal_1.chalk.cyanBright('package.json')}" file.`); } } /** * Runs the script. */ async run() { const start = new Date(); this.logger.info(`Script "${caporal_1.chalk.cyanBright(this.outputName)}" starting...`); await this.validateRequiredDependencies(); try { await this.main(); } catch (originalError) { const err = typeof originalError === 'string' ? new Error(originalError) : originalError; if (err.__reported) { this.logger.warn(`Script "${caporal_1.chalk.cyanBright(this.outputName)}" was aborted after ${caporal_1.chalk.magenta(calcElapsedTime(start, new Date()))}`); } else { this.logger.error(`Script "${caporal_1.chalk.cyanBright(this.outputName)}" failed after ${caporal_1.chalk.magenta(calcElapsedTime(start, new Date()))} with: ${caporal_1.chalk.red(err.message)}`); err.__reported = true; } throw err; } this.logger.info(`Script "${caporal_1.chalk.cyanBright(this.outputName)}" successful after ${caporal_1.chalk.magenta(calcElapsedTime(start, new Date()))}`); } } exports.ScriptBase = ScriptBase; function calcElapsedTime(from, to) { const deltaSecs = Math.round(10 * ((to.getTime() - from.getTime()) / 1000.0)) / 10; const deltaMinutes = Math.round(10 * (deltaSecs / 60.0)) / 10; const deltaText = deltaSecs > 120 ? `${deltaMinutes} m` : `${deltaSecs} s`; return deltaText; } //# sourceMappingURL=scriptBase.js.map