@h3ravel/shared
Version:
Shared Utilities.
500 lines (491 loc) • 14.8 kB
JavaScript
import { access } from "fs/promises";
import escalade from "escalade/sync";
import path from "path";
import chalk from "chalk";
import autocomplete from "inquirer-autocomplete-standalone";
import { confirm, input, password, select } from "@inquirer/prompts";
import crypto from "crypto";
import preferredPM from "preferred-pm";
//#region src/Utils/EnvParser.ts
var EnvParser = class {
static parse(initial) {
const parsed = { ...initial };
for (const key in parsed) {
const value = parsed[key];
parsed[key] = this.parseValue(value);
}
return parsed;
}
static parseValue(value) {
/**
* Null/undefined stay untouched
*/
if (value === null || value === void 0) return value;
/**
* Convert string "true"/"false" to boolean
*/
if (value === "true") return true;
if (value === "false") return false;
/**
* Convert string numbers to number
*/
if (!isNaN(value) && value.trim() !== "") return Number(value);
/**
* Convert string "null" and "undefined"
*/
if (value === "null") return null;
if (value === "undefined") return void 0;
/**
* Otherwise return as-is (string)
*/
return value;
}
};
//#endregion
//#region src/Utils/FileSystem.ts
var FileSystem = class {
static findModulePkg(moduleId, cwd) {
const parts = moduleId.replace(/\\/g, "/").split("/");
let packageName = "";
if (parts.length > 0 && parts[0][0] === "@") packageName += parts.shift() + "/";
packageName += parts.shift();
const packageJson = path.join(cwd ?? process.cwd(), "node_modules", packageName);
const resolved = this.resolveFileUp("package", ["json"], packageJson);
if (!resolved) return;
return path.join(path.dirname(resolved), parts.join("/"));
}
/**
* Check if file exists
*
* @param path
* @returns
*/
static async fileExists(path$1) {
try {
await access(path$1);
return true;
} catch {
return false;
}
}
/**
* Recursively find files starting from given cwd
*
* @param name
* @param extensions
* @param cwd
*
* @returns
*/
static resolveFileUp(name, extensions, cwd) {
cwd ??= process.cwd();
return escalade(cwd, (dir, filesNames) => {
if (typeof extensions === "function") return extensions(dir, filesNames);
const candidates = new Set(extensions.map((ext) => `${name}.${ext}`));
for (const filename of filesNames) if (candidates.has(filename)) return filename;
return false;
}) ?? void 0;
}
};
//#endregion
//#region src/Utils/Logger.ts
var Logger = class Logger {
/**
* Global verbosity configuration
*/
static verbosity = 0;
static isQuiet = false;
static isSilent = false;
/**
* Configure global verbosity levels
*/
static configure(options = {}) {
this.verbosity = options.verbosity ?? 0;
this.isQuiet = options.quiet ?? false;
this.isSilent = options.silent ?? false;
}
/**
* Check if output should be suppressed
*/
static shouldSuppressOutput(level) {
if (this.isSilent) return true;
if (this.isQuiet && (level === "info" || level === "success")) return true;
if (level === "debug" && this.verbosity < 3) return true;
return false;
}
static twoColumnDetail(name, value, log = true, spacer = ".") {
const regex = /\x1b\[\d+m/g;
const width = Math.max(process.stdout.columns, 100);
const dots = Math.max(width - name.replace(regex, "").length - value.replace(regex, "").length - 10, 0);
if (log) return console.log(name, chalk.gray(spacer.repeat(dots)), value);
else return [
name,
chalk.gray(spacer.repeat(dots)),
value
];
}
static describe(name, desc, width = 50, log = true) {
width = Math.max(width, 30);
const dots = Math.max(width - name.replace(/\x1b\[\d+m/g, "").length, 0);
if (log) return console.log(name, " ".repeat(dots), desc);
else return [
name,
" ".repeat(dots),
desc
];
}
/**
* Logs the message in two columns but allways passing status
*
* @param name
* @param value
* @param status
* @param exit
* @param preserveCol
*/
static split(name, value, status, exit = false, preserveCol = false) {
status ??= "info";
const color = {
success: chalk.bgGreen,
info: chalk.bgBlue,
error: chalk.bgRed
};
const [_name, dots, val] = this.twoColumnDetail(name, value, false);
console.log(this.textFormat(_name, color[status], preserveCol), dots, val);
if (exit) process.exit(0);
}
/**
* Wraps text with chalk
*
* @param txt
* @param color
* @param preserveCol
* @returns
*/
static textFormat(txt, color, preserveCol = false) {
const str = String(txt);
if (preserveCol) return str;
const [first, ...rest] = str.split(":");
if (rest.length === 0) return str;
return color(` ${first} `) + rest.join(":");
}
/**
* Logs a success message
*
* @param msg
* @param exit
* @param preserveCol
*/
static success(msg, exit = false, preserveCol = false) {
if (!this.shouldSuppressOutput("success")) console.log(chalk.green("✓"), this.textFormat(msg, chalk.bgGreen, preserveCol));
if (exit) process.exit(0);
}
/**
* Logs an informational message
*
* @param msg
* @param exit
* @param preserveCol
*/
static info(msg, exit = false, preserveCol = false) {
if (!this.shouldSuppressOutput("info")) console.log(chalk.blue("ℹ"), this.textFormat(msg, chalk.bgBlue, preserveCol));
if (exit) process.exit(0);
}
/**
* Logs an error message
*
* @param msg
* @param exit
* @param preserveCol
*/
static error(msg, exit = true, preserveCol = false) {
if (!this.shouldSuppressOutput("error")) if (msg instanceof Error) {
if (msg.message) console.error(chalk.red("✖"), this.textFormat("ERROR:" + msg.message, chalk.bgRed, preserveCol));
console.error(chalk.red(`${msg.detail ? `${msg.detail}\n` : ""}${msg.stack}`));
} else console.error(chalk.red("✖"), this.textFormat(msg, chalk.bgRed, preserveCol));
if (exit) process.exit(1);
}
/**
* Logs a warning message
*
* @param msg
* @param exit
* @param preserveCol
*/
static warn(msg, exit = false, preserveCol = false) {
if (!this.shouldSuppressOutput("warn")) console.warn(chalk.yellow("⚠"), this.textFormat(msg, chalk.bgYellow, preserveCol));
if (exit) process.exit(0);
}
/**
* Logs a debug message (only shown with verbosity >= 3)
*
* @param msg
* @param exit
* @param preserveCol
*/
static debug(msg, exit = false, preserveCol = false) {
if (!this.shouldSuppressOutput("debug")) if (Array.isArray(msg)) for (let i = 0; i < msg.length; i++) console.log(chalk.gray("🐛"), chalk.bgGray(i + 1), this.textFormat(msg[i], chalk.bgGray, preserveCol));
else console.log(chalk.gray("🐛"), this.textFormat(msg, chalk.bgGray, preserveCol));
if (exit) process.exit(0);
}
/**
* Terminates the process
*/
static quiet() {
process.exit(0);
}
static chalker(styles) {
return (input$1) => styles.reduce((acc, style) => {
if (style in chalk) return (typeof style === "function" ? style : chalk[style])(acc);
return acc;
}, input$1);
}
static parse(config, joiner = " ", log = true, sc) {
const string = config.map(([str, opt]) => {
if (Array.isArray(opt)) opt = Logger.chalker(opt);
const output = typeof opt === "string" && typeof chalk[opt] === "function" ? chalk[opt](str) : typeof opt === "function" ? opt(str) : str;
if (!sc) return output;
return this.textFormat(output, Logger.chalker(Array.isArray(sc) ? sc : [sc]));
}).join(joiner);
if (log && !this.shouldSuppressOutput("line")) console.log(string);
else return string;
}
/**
* Ouput formater object or format the output
*
* @returns
*/
static log = ((config, joiner, log = true, sc) => {
if (typeof config === "string") {
const conf = [[config, joiner]];
return this.parse(conf, "", log, sc);
} else if (config) return this.parse(config, String(joiner), log, sc);
return this;
});
};
//#endregion
//#region src/Utils/PathLoader.ts
var PathLoader = class {
paths = {
base: "",
views: "/src/resources/views",
assets: "/public/assets",
routes: "/src/routes",
config: "/src/config",
public: "/public",
storage: "/storage",
database: "/src/database"
};
/**
* Dynamically retrieves a path property from the class.
* Any property ending with "Path" is accessible automatically.
*
* @param name - The base name of the path property
* @param prefix - The base path to prefix to the path
* @returns
*/
getPath(name, prefix) {
let path$1;
if (prefix && name !== "base") path$1 = path.join(prefix, this.paths[name]);
else path$1 = this.paths[name];
if (name === "public") path$1 = path$1.replace("/public", path.join("/", process.env.DIST_DIR ?? ".h3ravel/serve"));
else path$1 = path$1.replace("/src/", `/${process.env.DIST_DIR ?? ".h3ravel/serve"}/`.replace(/([^:]\/)\/+/g, "$1"));
return path.normalize(path$1);
}
/**
* Programatically set the paths.
*
* @param name - The base name of the path property
* @param path - The new path
* @param base - The base path to include to the path
*/
setPath(name, path$1, base) {
if (base && name !== "base") this.paths[name] = path.join(base, path$1);
this.paths[name] = path$1;
}
};
//#endregion
//#region src/Utils/Prompts.ts
var Prompts = class extends Logger {
/**
* Allows users to pick from a predefined set of choices when asked a question.
*/
static async choice(message, choices, defaultIndex) {
return select({
message,
choices,
default: defaultIndex ? choices.at(defaultIndex) : void 0
});
}
/**
* Ask the user for a simple "yes or no" confirmation.
* By default, this method returns `false`. However, if the user enters y or yes
* in response to the prompt, the method would return `true`.
*/
static async confirm(message, def) {
return confirm({
message,
default: def
});
}
/**
* Prompt the user with the given question, accept their input,
* and then return the user's input back to your command.
*/
static async ask(message, def) {
return input({
message,
default: def
});
}
/**
* Prompt the user with the given question, accept their input which
* will not be visible to them as they type in the console,
* and then return the user's input back to your command.
*/
static async secret(message, mask) {
return password({
message,
mask
});
}
/**
* Provide auto-completion for possible choices.
* The user can still provide any answer, regardless of the auto-completion hints.
*/
static async anticipate(message, source, def) {
return autocomplete({
message,
source: Array.isArray(source) ? async (term) => {
return (term ? source.filter((e) => e.includes(term)) : source).map((e) => ({ value: e }));
} : source,
suggestOnly: true,
default: def
});
}
};
//#endregion
//#region src/Utils/Resolver.ts
var Resolver = class {
static async getPakageInstallCommand(pkg) {
const pm = (await preferredPM(process.cwd()))?.name ?? "pnpm";
let cmd = "install ";
if (!pkg) if (pm === "npm" || pm === "pnpm" || pm === "bun") cmd = "install";
else cmd = "";
else if (pm === "yarn" || pm === "pnpm" || pm === "bun") cmd = "add ";
return `${pm} ${cmd}${pkg ?? ""}`;
}
static async getInstallCommand(pkg) {
const pm = (await preferredPM(process.cwd()))?.name ?? "pnpm";
let cmd = "install";
if (pm === "yarn" || pm === "pnpm") cmd = "add";
else if (pm === "bun") cmd = "create";
return `${pm} ${cmd} ${pkg}`;
}
/**
* Create a hash for a function or an object
*
* @param provider
* @returns
*/
static hashObjectOrFunction(provider) {
return crypto.createHash("sha1").update(provider.toString()).digest("hex");
}
/**
* Checks if a function is asyncronous
*
* @param func
* @returns
*/
static isAsyncFunction(func) {
if (typeof func !== "function") return false;
return Object.prototype.toString.call(func) === "[object AsyncFunction]";
}
};
//#endregion
//#region src/Utils/scripts.ts
const mainTsconfig = {
extends: "@h3ravel/shared/tsconfig.json",
compilerOptions: {
baseUrl: ".",
outDir: "dist",
paths: {
"src/*": ["./../src/*"],
"App/*": ["./../src/app/*"],
"root/*": ["./../*"],
"routes/*": ["./../src/routes/*"],
"config/*": ["./../src/config/*"],
"resources/*": ["./../src/resources/*"]
},
target: "es2022",
module: "es2022",
moduleResolution: "Node",
esModuleInterop: true,
strict: true,
allowJs: true,
skipLibCheck: true,
resolveJsonModule: true,
noEmit: true,
experimentalDecorators: true,
emitDecoratorMetadata: true
},
include: ["./**/*.d.ts", "./../**/*"],
exclude: [
".",
"./../**/console/bin",
"./../dist",
"./../**/dist",
"./../**/node_modules",
"./../.node_modules",
"./../**/node_modules/*",
"./../**/public",
"./../public",
"./../**/storage",
"./../storage",
"./../**coverage**",
"./../eslint.config.js",
"./../jest.config.ts",
"./../arquebus.config.js"
]
};
const baseTsconfig = { extends: "./.h3ravel/tsconfig.json" };
const packageJsonScript = {
build: "NODE_ENV=production tsdown --config-loader unconfig -c tsdown.default.config.ts",
dev: "NODE_ENV=development pnpm tsdown --config-loader unconfig -c tsdown.default.config.ts",
start: "DIST_DIR=dist node -r source-map-support/register dist/server.js",
lint: "eslint . --ext .ts",
test: "NODE_NO_WARNINGS=1 NODE_ENV=testing jest --passWithNoTests",
postinstall: "pnpm prepare"
};
//#endregion
//#region src/Utils/TaskManager.ts
var TaskManager = class {
static async taskRunner(description, task) {
const startTime = process.hrtime();
let result = false;
try {
result = await Promise.all([(task || (() => true))()].flat());
} finally {
const endTime = process.hrtime(startTime);
const duration = (endTime[0] * 1e9 + endTime[1]) / 1e6;
Logger.twoColumnDetail(Logger.parse([[description, "green"]], "", false), [Logger.parse([[`${Math.floor(duration)}ms`, "gray"]], "", false), Logger.parse([[result !== false ? "✔" : "✘", result !== false ? "green" : "red"]], "", false)].join(" "));
}
}
static async advancedTaskRunner(info, task) {
const startTime = process.hrtime();
const [startInfo, stopInfo] = info;
if (stopInfo) Logger.twoColumnDetail(startInfo[0], Logger.log(startInfo[1], ["yellow", "bold"], false));
try {
return await Promise.race([task()]);
} catch (e) {
Logger.error("ERROR: " + e.message);
} finally {
const endTime = process.hrtime(startTime);
const duration = (endTime[0] * 1e9 + endTime[1]) / 1e6;
Logger.twoColumnDetail(stopInfo?.[0] ?? startInfo[0], [Logger.parse([[`${Math.floor(duration)}ms`, "gray"]], "", false), Logger.parse([[`✔ ${stopInfo?.[1] ?? startInfo[1]}`, ["green", "bold"]]], "", false)].join(" "));
}
}
};
//#endregion
export { EnvParser, FileSystem, Logger, PathLoader, Prompts, Resolver, TaskManager, baseTsconfig, mainTsconfig, packageJsonScript };
//# sourceMappingURL=index.js.map