UNPKG

vlt

Version:
450 lines (445 loc) 13.2 kB
var global = globalThis; import {Buffer} from "node:buffer"; import {setTimeout,clearTimeout,setImmediate,clearImmediate,setInterval,clearInterval} from "node:timers"; import {createRequire as _vlt_createRequire} from "node:module"; var require = _vlt_createRequire(import.meta.filename); import { PackageInfoClient } from "./chunk-FZMPFIDM.js"; import { Monorepo } from "./chunk-D36DAG56.js"; import { PackageJson } from "./chunk-YWPMIBJS.js"; import { find, load, reload, save } from "./chunk-5UBJ3ZBM.js"; import { PathScurry } from "./chunk-GY4L7O2Y.js"; import { assertRecordStringString, assertRecordStringT, isRecordStringString } from "./chunk-X4RDKJKD.js"; import { commands, definition, getCommand, isRecordField, recordFields } from "./chunk-BNCOU5ZT.js"; import { error } from "./chunk-RV3EHS4P.js"; // ../../src/cli-sdk/src/config/index.ts import { readFile, rm, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; // ../../src/cli-sdk/src/config/merge.ts var merge = (base, add) => Object.fromEntries( Object.entries(base).map(([k, v]) => [ k, add[k] === void 0 ? v : Array.isArray(v) && Array.isArray(add[k]) ? [ .../* @__PURE__ */ new Set([ ...v, ...add[k] ]) ] : Array.isArray(v) || Array.isArray(add[k]) ? add[k] : !!v && typeof v === "object" && !!add[k] && typeof add[k] === "object" ? merge(v, add[k]) : add[k] ]).concat( // already merged together if existing, so just get new additions Object.entries(add).filter(([k]) => base[k] === void 0) ) ); // ../../src/cli-sdk/src/config/index.ts var kCustomInspect = Symbol.for("nodejs.util.inspect.custom"); var reducePairs = (pairs) => { const record = {}; for (const kv of pairs) { const eq = kv.indexOf("="); if (eq === -1) record[kv] = ""; else { const key = kv.substring(0, eq); const val = kv.substring(eq + 1); record[key] = val; } } return record; }; var isRecordFieldValue = (k, v) => Array.isArray(v) && recordFields.includes(k); var pairsToRecords = (obj) => { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [ k, k === "command" && v && typeof v === "object" ? Object.fromEntries( Object.entries(v).map(([k2, v2]) => [ k2, pairsToRecords(v2) ]) ) : isRecordFieldValue(k, v) ? reducePairs(v) : v ]) // hard cast because TS can't see through the entries/fromEntries ); }; var recordsToPairs = (obj) => { return Object.fromEntries( Object.entries(obj).filter( ([k]) => !(k === "scurry" || k === "packageJson" || k === "monorepo" || k === "projectRoot" || k === "packageInfo") ).map(([k, v]) => [ k, k === "command" && v && typeof v === "object" ? recordsToPairs(v) : !v || typeof v !== "object" || Array.isArray(v) || !isRecordField(k) ? v : Object.entries(v).map(([k2, v2]) => `${k2}=${v2}`) ]) ); }; var kRecord = Symbol("parsed key=value record"); var Config = class _Config { /** * The {@link https://npmjs.com/jackspeak | JackSpeak} object * representing vlt's configuration */ jack; /** * Parsed values in effect */ values; /** * Command-specific config values */ commandValues = {}; /** * A flattened object of the parsed configuration */ get options() { if (this.#options) return this.#options; const scurry = new PathScurry(this.projectRoot); const packageJson = new PackageJson(); const asRecords = pairsToRecords(this.parse().values); const extras = { projectRoot: this.projectRoot, scurry, packageJson, monorepo: Monorepo.maybeLoad(this.projectRoot, { scurry, packageJson, load: { paths: asRecords.workspace, groups: asRecords["workspace-group"] } }), catalog: load( "catalog", assertRecordStringString ), catalogs: load( "catalogs", (o) => assertRecordStringT( o, isRecordStringString, "Record<string, Record<string, string>>" ) ) }; const options = Object.assign( asRecords, extras ); this.#options = Object.assign(options, { packageInfo: new PackageInfoClient(options), [kCustomInspect]() { return Object.fromEntries( Object.entries(options).filter( ([k]) => k !== "monorepo" && k !== "scurry" && k !== "packageJson" && k !== "packageInfo" ) ); } }); return this.#options; } /** * Reset the options value, optionally setting a new project root * to recalculate the options. */ resetOptions(projectRoot = process.cwd()) { this.projectRoot = projectRoot; this.#options = void 0; } // memoized options() getter value #options; /** * positional arguments to the vlt process */ positionals; /** * Original arguments used for parsing (stored for reload purposes) * @internal */ #originalArgs; /** * The root of the project where a vlt.json, vlt.json, * package.json, or .git was found. Not necessarily the `process.cwd()`, * though that is the default location. * * Never walks up as far as `$HOME`. So for example, if a project is in * `~/projects/xyz`, then the highest dir it will check is `~/projects` */ projectRoot; /** * `Record<alias, canonical name>` to dereference command aliases. */ commands; /** * Which command name to use for overriding with command-specific values, * determined from the argv when parse() is called. */ command; constructor(jack = definition, projectRoot = process.cwd()) { this.projectRoot = projectRoot; this.commands = commands; this.jack = jack; } /** * Parse the arguments and set configuration and positionals accordingly. */ parse(args = process.argv) { if (isParsed(this)) return this; this.#originalArgs = [...args]; this.jack.loadEnvDefaults(); const p = this.jack.parseRaw(args); const fallback = getCommand(p.values["fallback-command"]); this.command = getCommand(p.positionals[0]); const cmdOrFallback = this.command ?? fallback; const cmdSpecific = cmdOrFallback && this.commandValues[cmdOrFallback]; if (cmdSpecific) { this.jack.setConfigValues(recordsToPairs(cmdSpecific)); } this.jack.applyDefaults(p); this.jack.writeEnv(p); if (this.command) p.positionals.shift(); else this.command = getCommand(p.values["fallback-command"]); Object.assign(this, p); if (!isParsed(this)) throw error("failed to parse config"); return this; } /** * Get a `key=value` list option value as an object. * * For example, a list option with a vlaue of `['key=value', 'xyz=as=df' ]` * would be returned as `{key: 'value', xyz: 'as=df'}` * * Results are memoized, so subsequent calls for the same key will return the * same object. If new strings are added to the list, then the memoized value * is *not* updated, so only use once configurations have been fully loaded. * * If the config value is not set at all, an empty object is returned. */ getRecord(k) { const pairs = this.get(k); if (!pairs) return {}; if (pairs[kRecord]) return pairs[kRecord]; const kv = pairs.reduce((kv2, pair) => { const eq = pair.indexOf("="); if (eq === -1) return kv2; const key = pair.substring(0, eq); const val = pair.substring(eq + 1); kv2[key] = val; return kv2; }, {}); Object.assign(pairs, { [kRecord]: kv }); return kv; } /** * Get a configuration value. * * Note: `key=value` pair configs are returned as a string array. To get them * as an object, use {@link Config#getRecord}. */ get(k) { return (this.values ?? this.parse().values)[k]; } /** * Write the config values to the user or project config file. */ async writeConfigFile(which, values) { save("config", pairsToRecords(values), which); await this.#reloadConfig(); } /** * Fold in the provided fields with the existing properties * in the config file. */ async addConfigToFile(which, values) { return this.writeConfigFile( which, merge(await this.#maybeLoadConfigFile(which) ?? {}, values) ); } // called in this weird bound way so that it can be used by the // vlt-json config loading module. #validator = function(c, file) { this.#validateConfig(c, file); }.bind(this); #validateConfig(c, file) { if (!c || typeof c !== "object" || Array.isArray(c)) { throw error("invalid config, expected object", { path: file, found: c, wanted: "ConfigFileData" }); } const { command, ...values } = recordsToPairs(c); if (command) { for (const [c2, opts] of Object.entries(command)) { const cmd = getCommand(c2); if (cmd) { this.commandValues[cmd] = merge( this.commandValues[cmd] ?? {}, opts ); } } } this.jack.setConfigValues(values, file); } /** * if the file exists, parse and load it. returns object if data was * loaded, or undefined if not. */ async #maybeLoadConfigFile(whichConfig) { return load("config", this.#validator, whichConfig); } /** * Deletes the specified config fields from the named file * Returns `true` if anything was changed. */ async deleteConfigKeys(which, fields) { const data = await this.#maybeLoadConfigFile(which); if (!data) return false; let didSomething = false; for (const f of fields) { const [key, ...sk] = f.split("."); const subs = sk.join("."); const k = key; const v = data[k]; if (v === void 0) continue; if (subs && v && typeof v === "object") { if (Array.isArray(v)) { const i = v.findIndex( (subvalue) => subvalue.startsWith(`${subs}=`) ); if (i !== -1) { if (v.length === 1) delete data[k]; else v.splice(i, 1); didSomething = true; } } else { if (v[subs] !== void 0) { delete v[subs]; if (Object.keys(v).length === 0) delete data[k]; didSomething = true; } } } else { didSomething = true; delete data[k]; } } if (didSomething) await this.writeConfigFile(which, data); return didSomething; } /** * Edit the user or project configuration file. * * If the file isn't present, then it starts with `{}` so the user has * something to work with. * * If the result is not valid, or no config settings are contained in the * file after editing, then it's restored to what it was before, which might * mean deleting the file. */ async editConfigFile(which, edit) { const file = find(which); const backup = await readFile(file, "utf8").catch(() => void 0); if (!backup) { await writeFile( file, JSON.stringify({ config: {} }, null, 2) + "\n" ); } let valid = false; try { await edit(file); const result = reload("config", which); save("config", result ?? {}, which); valid = true; } finally { if (!valid) { if (backup) { await writeFile(file, backup); reload(which); } else { await rm(file, { force: true }); } } } } /** * Find the local config file and load both it and the user-level config in * the XDG config home. */ async loadConfigFile() { await this.#maybeLoadConfigFile("user"); this.projectRoot = dirname(find("project", this.projectRoot)); await this.#maybeLoadConfigFile("project"); return this; } /** * Clear cached config values to force re-reading from updated files. * @internal */ async #reloadConfig() { this.#options = void 0; } /** * Force a complete reload of config files from disk. * This clears all caches and re-reads config files. * Useful for long-running processes that need to pick up config changes. */ async reloadFromDisk() { this.#options = void 0; this.values = void 0; this.positionals = void 0; this.command = void 0; const { unload } = await import("./src-YAH6SXNK.js"); unload("user"); unload("project"); await this.#maybeLoadConfigFile("user"); await this.#maybeLoadConfigFile("project"); this.parse(this.#originalArgs); } /** * cache of the loaded config */ static #loaded; /** * Load the configuration and return a Promise to a * {@link Config} object */ static async load(projectRoot = process.cwd(), argv = process.argv, reload2 = false) { if (this.#loaded && !reload2) return this.#loaded; const a = new _Config(definition, projectRoot); const b = await a.loadConfigFile(); this.#loaded = b.parse(argv); return this.#loaded; } }; var isParsed = (c) => !!(c.values && c.positionals && c.command); export { kCustomInspect, pairsToRecords, recordsToPairs, Config }; //# sourceMappingURL=chunk-FRVD5QAW.js.map