import * as fs from "fs/promises";
import * as path from "path";
import inquirer from "inquirer";
import { parse } from "@babel/parser";
import pkg from "@babel/generator";
import babel from "@babel/core";
import { createContext, Script } from "vm";
import chalk from "chalk";
import { task, command, runs, commandLive } from "./cfgi-runner.js";
const generate = pkg.default;
/**
* @fileOverview Manages the running of a configuration file.
* @author Gerard Hernandez
*
* @module cfgi/cli
*
* @requires {@link https://nodejs.org/api/fs.html | fs}
* @requires {@link https://www.npmjs.com/package/inquirer | inquirer}
* @requires {@link https://www.npmjs.com/package/@babel/parser | @babel/parser}
* @requires {@link https://www.npmjs.com/package/@babel/generator | @babel/generator}
* @requires {@link https://www.npmjs.com/package/@babel/core | @babel/core}
* @requires {@link https://www.npmjs.com/package/vm | vm}
* @requires {@link https://www.npmjs.com/package/chalk | chalk}
*
* @requires {@link module:cfgi-runner~task | task}
* @requires {@link module:cfgi-runner~command | command}
* @requires {@link module:cfgi-runner~runs | runs}
* @requires {@link module:cfgi-runner~commandLive | commandLive}
* @requires {@link module:cfgi-runner~TaskConfig | TaskConfig}
*
*/
const currentDirectory = process.cwd();
export async function findConfigFilesInDir(dir) {
const configPath = dir || process.cwd();
const files = await fs.readdir(configPath);
let cfgiDir = files.find((file) => file === "cfgi");
let configFiles = [];
if (cfgiDir) {
const cfgiFiles = await fs.readdir(path.join(configPath, cfgiDir));
configFiles = cfgiFiles
.filter((file) => file.endsWith(".cfgi.js") ||
file.endsWith(".cfgi.ts") ||
file.endsWith(".mjs"))
.map((file) => path.join(cfgiDir, file)); // prepend the directory name
}
else {
configFiles = files.filter((file) => file.endsWith(".cfgi.js") ||
file.endsWith(".cfgi.ts") ||
file.endsWith(".mjs"));
}
const availableFiles = configFiles.sort((a, b) => a.localeCompare(b));
return availableFiles;
}
/**
* Selects a configuration file from a directory.
* @function
* @async
* @param {string[]} files - The configuration files in the directory.
* @returns {Promise<string>} - The name of the selected configuration file.
*/
export async function selectConfigNameFromDir(files) {
const response = await inquirer.prompt({
type: "list",
name: "config",
message: "Which config file would you like to run?",
choices: files,
});
return response.config;
}
/**
* Validates the provided configuration name.
* @function
* @async
* @param {string} name - The name of the configuration file.
* @returns {Promise<string | undefined>} - The matched configuration file name.
*/
export async function validateProvidedConfigName(name) {
if (!name)
return;
const files = await fs.readdir(currentDirectory);
const configFiles = files.filter((file) => file.endsWith(".cfgi.js") ||
file.endsWith(".cfgi.ts") ||
file.endsWith(".mjs"));
const matchedFile = configFiles.find((file) => file.includes(name));
return matchedFile;
}
/**
* Parses the configuration file.
* @function
* @async
* @param {string} configFName - The name of the configuration file.
* @returns {Promise<options:TaskConfig,imports:Array<string>,tasks:Array<{name:string,node:Node }>>} - The parsed configuration file.
*/
export async function parseConfig(configFName) {
// read the contents of the config file as text
const code = await fs.readFile(configFName, "utf-8");
try {
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript"],
});
let options = {};
let imports = [];
let tasks = [];
ast.program.body.forEach((node) => {
if (node.type === "ImportDeclaration") {
const importString = generate(node).code;
imports.push(importString);
}
if (node.type === "VariableDeclaration") {
// @ts-ignore
const declaredVariableName = node.declarations[0]?.id.name;
if (declaredVariableName === "options")
if (node.declarations[0]?.init) {
const optionsObjString = generate(node.declarations[0]?.init).code;
options = eval(`(${optionsObjString})`);
}
}
if ((node.type = "ExpressionStatement")) {
// @ts-expect-error
if (node.expression && node.expression.callee.name) {
// @ts-expect-error
const taskName = node.expression.arguments[0].value;
tasks.push({ name: taskName, node: node });
}
}
});
return { options, imports, tasks };
}
catch (e) {
return { options: {}, imports: [], tasks: [] };
}
}
/**
* Selects a task from the configuration file.
* @function
* @async
* @param {Array<{name: string, node: Node}>} tasks - The tasks in the configuration file.
* @returns {Promise<Array<{name: string, node: Node}>>} - The selected tasks.
*/
export async function selectTaskFromConfig(tasks) {
const taskNames = tasks.map((t) => t.name);
const response = await inquirer.prompt({
type: "list",
name: "task",
message: "Which task would you like to execute?",
choices: taskNames.concat("all"),
});
if (response.task === "all")
return tasks;
return [tasks.find((t) => t.name === response.task)];
}
/**
* Generates an individual task file.
* @function
* @param {TaskConfig} options - The task configuration options.
* @param {string[]} imports - The imports in the configuration file.
* @param {Array<{name: string, node: Node}>} tasks - The tasks in the configuration file.
* @returns {string} - The generated task file.
*/
export function generateIndividualTaskFile(options, imports, tasks) {
const task = tasks[0];
const generatedCode = `
const options = ${JSON.stringify(options, null, 2)};
${generate(task.node).code};`;
const transpiledCode = babel.transformSync(generatedCode, {
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead", // Adjust this according to your needs
},
],
],
}).code;
const ast = parse(transpiledCode, {
sourceType: "module",
plugins: ["typescript"],
});
const generated = generate(ast, { retainLines: true });
return generated.code;
}
/**
* Generates a multi-task file.
* @function
* @param {TaskConfig} options - The task configuration options.
* @param {string[]} imports - The imports in the configuration file.
* @param {Array<{name: string, node: Node}>} tasks - The tasks in the configuration file.
* @returns {string} - The generated multi-task file.
*/
export function generateMultiTaskFile(options, imports, tasks) {
const generatedCode = `
const options = ${JSON.stringify(options, null, 2)};
${tasks.map((task) => generate(task.node).code).join("\n")};`;
const transpiledCode = babel.transformSync(generatedCode, {
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead", // Adjust this according to your needs
},
],
],
}).code;
const ast = parse(transpiledCode, {
sourceType: "module",
plugins: ["typescript"],
});
const generated = generate(ast, { retainLines: true });
return generated.code;
}
/**
* Runs the provided code in a virtual machine.
* @function
* @param {string} code - The code to run.
* @returns {void}
*/
export function runInVM(code) {
console.log(chalk.yellow(`\nℹ Running task ${chalk.blue(task.name)}:\n`));
const script = new Script(code);
const context = createContext({
task,
command,
runs,
commandLive,
});
try {
script.runInContext(context);
}
catch (e) {
console.log(chalk.red("✖ Something went wrong!"));
console.log(e);
}
}