actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
226 lines (193 loc) • 6.42 kB
text/typescript
import * as path from "path";
import * as fs from "fs";
import { program, InvalidArgumentError } from "commander";
import { typescript } from "../classes/process/typescript";
import { projectRoot } from "../classes/process/projectRoot";
import { ensureNoTsHeaderOrSpecFiles } from "../modules/utils/ensureNoTsHeaderOrSpecFiles";
import { CLI } from "../classes/cli";
import { PackageJson } from "type-fest";
// load explicitly to find the type changes for the config module
import "../config/api";
import "../config/plugins";
import "../config/logger";
import "../config/routes";
import { safeGlobSync } from "../modules/utils/safeGlob";
export namespace ActionheroCLIRunner {
export async function run() {
program.storeOptionsAsProperties(false);
program.version(getVersion());
let pathsLoaded: string[] = [];
try {
const { config } = await import("../index");
// this project
for (const p of config.general.paths.cli) {
await loadDirectory(path.join(p), pathsLoaded);
}
// plugins
for (const plugin of Object.values(config.plugins)) {
if (plugin.cli !== false) {
// old plugins
await loadDirectory(path.join(plugin.path, "bin"), pathsLoaded);
// new plugins
await loadDirectory(
path.join(plugin.path, "dist", "bin"),
pathsLoaded,
);
}
}
// core
if (config.general.cliIncludeInternal !== false) {
await loadDirectory(__dirname, pathsLoaded);
}
} catch (e) {
// we are trying to build a new project, only load the generate command
await loadDirectory(path.join(__dirname), pathsLoaded, "generate");
}
program.parse(process.argv);
}
// --- Utils --- //
export async function loadDirectory(
dir: string,
pathsLoaded: string[],
match = "*",
) {
if (!fs.existsSync(dir)) return;
const realpath = fs.realpathSync(dir);
if (pathsLoaded.includes(realpath)) return;
pathsLoaded.push(realpath);
const matcher = `${realpath}/**/+(${
typescript ? `${match}.js|*.ts` : `${match}.js`
})`;
const files = ensureNoTsHeaderOrSpecFiles(safeGlobSync(matcher));
for (const i in files) {
const collection = await import(files[i]);
for (const j in collection) {
const command = collection[j];
convertCLIToCommanderAction(command);
}
}
}
export async function convertCLIToCommanderAction(
cliConstructor: new () => CLI,
) {
if (
Object.getPrototypeOf(cliConstructor?.prototype?.constructor || {})
.name !== "CLI"
) {
return;
}
const instance: CLI = new cliConstructor();
const command = program
.command(instance.name)
.description(instance.description)
.action(async (_arg1, _arg2, _arg3, _arg4, _arg5) => {
await runCommand(instance, _arg1, _arg2, _arg3, _arg4, _arg5);
})
.on("--help", () => {
if (instance.example) {
console.log("");
console.log("Example: \r\n" + " " + instance.example);
}
if (typeof instance.help === "function") instance.help();
});
for (const key in instance.inputs) {
const input = instance.inputs[key];
if (input.flag && !input.letter) {
throw new Error(
`flag inputs require a short letter (${JSON.stringify(input)})`,
);
}
const separators =
input.required || input.requiredValue ? ["<", ">"] : ["[", "]"];
const methodName = input.required ? "requiredOption" : "option";
const argString = `${input.letter ? `-${input.letter}, ` : ""}--${key} ${
input.flag
? ""
: `${separators[0]}${input.placeholder || key}${
input.variadic ? "..." : ""
}${separators[1]}`
}`;
const argProcessor = (
value: string,
accumulator?: unknown[],
): unknown => {
try {
if (typeof input.formatter === "function") {
value = input.formatter(value);
}
if (typeof input.validator === "function") {
input.validator(value);
}
if (input.variadic) {
if (!Array.isArray(accumulator)) accumulator = [];
accumulator.push(value);
return accumulator;
}
return value;
} catch (error) {
throw new InvalidArgumentError(error?.message ?? error);
}
};
command[methodName](
argString,
input.description,
argProcessor,
input.default,
);
}
}
export async function runCommand(
instance: CLI,
_arg1: any,
_arg2: any,
_arg3: any,
_arg4: any,
_arg5: any,
) {
let toStop = false;
let _arguments: string[] = [];
let params: Record<string, string[]> = {};
[_arg1, _arg2, _arg3, _arg4, _arg5].forEach((arg) => {
if (typeof arg?.opts === "function") {
params = arg.opts();
} else if (arg !== null && arg !== undefined && typeof arg !== "object") {
_arguments.push(arg);
}
});
params["_arguments"] = _arguments;
if (instance.initialize === false && instance.start === false) {
toStop = await instance.run({ params });
} else {
try {
const { Process } = await import("../index");
const actionHeroProcess = new Process();
if (instance.initialize) await actionHeroProcess.initialize();
if (instance.start) await actionHeroProcess.start();
toStop = await instance.run({ params });
} catch (error) {
console.error(error.toString());
process.exit(1);
}
}
if (toStop || toStop === null || toStop === undefined) {
setTimeout(process.exit, 500, 0);
}
}
export function readPackageJSON(file: string) {
return JSON.parse(fs.readFileSync(file).toString());
}
export function getVersion(): string {
const parentPackageJSON = path.join(projectRoot, "package.json");
if (fs.existsSync(parentPackageJSON)) {
const pkg: PackageJson = readPackageJSON(parentPackageJSON);
return pkg.version;
} else {
const pkg: PackageJson = readPackageJSON(
path.join(__dirname, "..", "..", "package.json"),
);
return pkg.version;
}
}
}
ActionheroCLIRunner.run();