import path from "path";
import fs from "fs";
import { Command } from "commander";
import { CliModule } from "./types";
/**
* @description Utility class for CLI operations
* @summary A static utility class that provides methods for loading modules, retrieving package information, and initializing CLI commands
*
* @example
* // Initialize a Command object with package information
* const command = new Command();
* CLIUtils.initialize(command, './path/to/package');
*
* // Load a CLI module from a file
* const module = await CLIUtils.loadFromFile('./path/to/cli-module.js');
*
* @class CLIUtils
*/
export class CLIUtils {
/**
* @description Dynamically imports a module file
* @summary Loads a JavaScript file and returns it as a CliModule, handling both ESM and CommonJS formats
*
* @param {string} path The file path to the module to load
* @return {Promise<CliModule>} A promise that resolves to the loaded CliModule
*/
static async loadFromFile(path: string): Promise<CliModule> {
try {
return CLIUtils.normalizeImport(import(path));
} catch (e: unknown) {
throw new Error(
`Failed to load from ${path}: ${e instanceof Error ? e.message : e}`
);
}
}
/**
* @description Normalizes module imports to handle both ESM and CommonJS formats
* @summary Properly imports JavaScript files regardless of their module format by handling the ESM wrapper for CommonJS modules
*
* @template T The type of the imported module
* @param {Promise<T>} importPromise The promise returned by the dynamic import
* @return {Promise<T>} A promise that resolves to the normalized module
* @private
*/
static async normalizeImport<T>(importPromise: Promise<T>): Promise<T> {
// CommonJS's `module.exports` is wrapped as `default` in ESModule.
return importPromise.then(
(m: unknown) => ((m as { default: T }).default || m) as T
);
}
/**
* @description Initializes a Command object with package information
* @summary Sets up a Commander Command object with the package name, description, and version from the package.json file
*
* @param {Command} command The Command object to initialize
* @param {string} [basePath] The base path where the package.json file is located, defaults to the current working directory
* @return {void}
*/
static initialize(command: Command, basePath: string) {
const name = CLIUtils.packageName(basePath);
command
.name(name)
.description(`Runs ${name} related commands`)
.version(CLIUtils.packageVersion(basePath));
}
/**
* @description Retrieves and parses the package.json file
* @summary Reads the package.json file from the specified path and parses it into a JavaScript object
*
* @param {string} basePath The base path where the package.json file is located
* @return {Record<string, unknown>} The parsed package.json content as an object
* @private
*/
private static getPackage(basePath: string): Record<string, unknown> {
try {
return JSON.parse(
fs.readFileSync(path.join(basePath, "package.json"), "utf8")
);
} catch (e: unknown) {
throw new Error(`Unable to read version from ${basePath}: ${e}`);
}
}
/**
* @description Returns the version from package.json
* @summary Retrieves the version field from the package.json file at the specified path
*
* @param {string} basePath The base path where the package.json file is located
* @return {string} The package version string
*/
static packageVersion(basePath: string): string {
return CLIUtils.getPackage(basePath)["version"] as string;
}
/**
* @description Returns the name from package.json
* @summary Retrieves the name field from the package.json file at the specified path and extracts the package name without the scope
*
* @param {string} basePath The base path where the package.json file is located
* @return {string} The package name without the scope (e.g., "cli" from "@decaf-ts/cli")
*/
static packageName(basePath: string): string {
const name = (CLIUtils.getPackage(basePath)["name"] as string).split("/");
return name[name.length - 1];
}
}
Source