From 6028a36417c4078db3fb6ec96c296a931a6d6a95 Mon Sep 17 00:00:00 2001 From: Juanra GM Date: Tue, 21 Nov 2023 15:00:55 +0100 Subject: [PATCH] feat(cli): add global config file --- .changeset/weak-weeks-poke.md | 5 + .gitignore | 2 + packages/cli/src/actions/backup.ts | 11 +- packages/cli/src/actions/diff.ts | 122 +++++++++++++--------- packages/cli/src/actions/install.ts | 35 ++++--- packages/cli/src/actions/options.ts | 6 +- packages/cli/src/actions/render.ts | 9 +- packages/cli/src/actions/repair-lock.ts | 8 +- packages/cli/src/actions/restore.ts | 8 +- packages/cli/src/cli.ts | 17 +-- packages/cli/src/index.ts | 1 + packages/cli/src/resources/AbstractRes.ts | 2 +- packages/cli/src/resources/SecretRes.ts | 10 +- packages/cli/src/utils/fs.ts | 17 +++ packages/cli/src/utils/path.ts | 12 +++ packages/cli/src/utils/self/config.ts | 94 +++++++++++++++++ packages/cli/src/utils/self/lock.ts | 10 +- packages/cli/src/utils/self/options.ts | 10 +- packages/cli/src/utils/self/resolve.ts | 91 +++++----------- packages/cli/src/utils/self/secrets.ts | 5 - 20 files changed, 292 insertions(+), 183 deletions(-) create mode 100644 .changeset/weak-weeks-poke.md create mode 100644 packages/cli/src/utils/self/config.ts diff --git a/.changeset/weak-weeks-poke.md b/.changeset/weak-weeks-poke.md new file mode 100644 index 0000000..8ec8026 --- /dev/null +++ b/.changeset/weak-weeks-poke.md @@ -0,0 +1,5 @@ +--- +"@rtpl/cli": minor +--- + +Add global config file diff --git a/.gitignore b/.gitignore index 4192b50..cdd3ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,7 @@ lib node_modules /resources rtpl-lock.json +rtpl-secrets.json +rtpl-config.* /rtpl.* tsconfig.tsbuildinfo \ No newline at end of file diff --git a/packages/cli/src/actions/backup.ts b/packages/cli/src/actions/backup.ts index 632e0b7..15125ab 100644 --- a/packages/cli/src/actions/backup.ts +++ b/packages/cli/src/actions/backup.ts @@ -1,4 +1,5 @@ import { GlobalOptions, pkg } from "../cli.js"; +import { parseConfigFile } from "../utils/self/config.js"; import * as lock from "../utils/self/lock.js"; import chalk from "chalk"; import { createHash } from "crypto"; @@ -14,10 +15,12 @@ export type BackupOptions = GlobalOptions & { }; export default async function backup(options: BackupOptions) { + const config = await parseConfigFile(options.config); + const log = options.log ?? true; - const lockData = (lock.parseFile(options.lockPath, true) ?? { - templates: {}, - }) as any as lock.LockData<{ contents: string }>; + const lockData = lock.parseFile(config.lock.path) as any as lock.LockData<{ + contents: string; + }>; let files = 0; @@ -39,7 +42,7 @@ export default async function backup(options: BackupOptions) { ]; const file = [...header, yaml].join("\n"); - const path = join(options.backupPath, `${Date.now()}.yaml`); + const path = join(config.backup.path, `${Date.now()}.yaml`); await mkdir(dirname(path), { recursive: true }); await writeFile(path, file); diff --git a/packages/cli/src/actions/diff.ts b/packages/cli/src/actions/diff.ts index bbbc81d..15542bc 100644 --- a/packages/cli/src/actions/diff.ts +++ b/packages/cli/src/actions/diff.ts @@ -1,6 +1,14 @@ import { GlobalOptions } from "../cli.js"; +import { AbstractRes } from "../index.js"; import { readIfExists } from "../utils/fs.js"; -import { readTplFile, resolveTpl } from "../utils/self/resolve.js"; +import { parseConfigFile } from "../utils/self/config.js"; +import { + ActionEnum, + getFileActions, + logAction, +} from "../utils/self/install.js"; +import { parseFile } from "../utils/self/lock.js"; +import { parseTplFile, resolveTpl } from "../utils/self/resolve.js"; import chalk from "chalk"; import { diffLines } from "diff"; @@ -10,69 +18,83 @@ export type DiffOptions = GlobalOptions & { lines: number; }; export default async function diff(options: DiffOptions) { - const tpl = await readTplFile(options.templatePath); + const config = await parseConfigFile(options.config); + const tpl = await parseTplFile(config.template.path); const { resources } = await resolveTpl(tpl, { + config, filter: options.filter, - lockPath: options.lockPath, - outPath: options.outPath, }); let changes = 0; - for (const path in resources) { - const res = resources[path]; - let resValue: string; - try { - resValue = res.toString(); - } catch (error) { - console.info("\x1b[36m%s\x1b[0m", path); - throw error; + const lockData = parseFile(config.lock.path); + const selfLockData = lockData.templates[tpl.config.name] || { + files: {}, + dirs: {}, + }; + const actions = await getFileActions(resources, selfLockData); + for (const path in actions) { + const action = actions[path]; + if (action.type === ActionEnum.NONE) continue; + logAction(action.type, path); + changes++; + if (action.type === ActionEnum.ADD || action.type === ActionEnum.UPDATE) { + await showDiff(resources[path], path, options); } + } - const old = (await readIfExists(path))?.toString(); - const lines = diffLines(old ?? "", resValue); - const haveChanges = - typeof old !== "string" || - lines.some((line) => line.added || line.removed); + if (!changes) process.stderr.write(`${chalk.grey("No changes")}\n`); + return { exitCode: changes ? 1 : 0, changes }; +} - if (haveChanges) changes++; - if (!options.showAll && !haveChanges) continue; +async function showDiff( + res: AbstractRes, + path: string, + options: DiffOptions, +) { + let resValue: string; + try { + resValue = res.toString(); + } catch (error) { console.info("\x1b[36m%s\x1b[0m", path); + throw error; + } + const old = (await readIfExists(path))?.toString(); + const lines = diffLines(old ?? "", resValue); + const haveChanges = + typeof old !== "string" || lines.some((line) => line.added || line.removed); - let lineIndex = 0; - let removed = 0; + if (!options.showAll && !haveChanges) return; - for (const line of lines) { - const sublines = line.value.split(/\r?\n/).slice(0, line.count); - for (const subline of sublines) { - ++lineIndex; - let result: string; + let lineIndex = 0; + let removed = 0; - if (line.added) { - lineIndex -= removed; - removed = 0; - result = chalk.green(subline); - } else if (line.removed) { - removed++; - result = chalk.red(subline); - } else { - result = subline; - } + for (const line of lines) { + const sublines = line.value.split(/\r?\n/).slice(0, line.count); + for (const subline of sublines) { + ++lineIndex; + let result: string; - const haveLineChanges = line.added || line.removed; - if (options.lines === 1 && !haveLineChanges) continue; + if (line.added) { + lineIndex -= removed; + removed = 0; + result = chalk.green(subline); + } else if (line.removed) { + removed++; + result = chalk.red(subline); + } else { + result = subline; + } + + const haveLineChanges = line.added || line.removed; + if (options.lines === 1 && !haveLineChanges) continue; - if (options.hideLines) { - console.info(result); - } else { - const lineIndexStr = lineIndex.toString().padStart(3, " "); - console.info( - `${chalk.grey(lineIndexStr)} ${chalk.white("|")} ${result}`, - ); - } + if (options.hideLines) { + console.info(result); + } else { + const lineIndexStr = lineIndex.toString().padStart(3, " "); + console.info( + `${chalk.grey(lineIndexStr)} ${chalk.white("|")} ${result}`, + ); } } - console.info(); } - - if (!changes) process.stderr.write(`${chalk.grey("No changes")}\n`); - return { exitCode: changes ? 1 : 0, changes }; } diff --git a/packages/cli/src/actions/install.ts b/packages/cli/src/actions/install.ts index f2fd8a5..c4e7434 100644 --- a/packages/cli/src/actions/install.ts +++ b/packages/cli/src/actions/install.ts @@ -1,5 +1,6 @@ import { GlobalOptions } from "../cli.js"; import { confirmPrompt } from "../utils/cli.js"; +import { parseConfigFile } from "../utils/self/config.js"; import { ActionEnum, execFileAction, @@ -8,12 +9,12 @@ import { } from "../utils/self/install.js"; import * as lock from "../utils/self/lock.js"; import { splitGlobalOptions } from "../utils/self/options.js"; -import { readTplFile, resolveTpl } from "../utils/self/resolve.js"; +import { parseTplFile, resolveTpl } from "../utils/self/resolve.js"; import backup from "./backup.js"; import diff from "./diff.js"; import chalk from "chalk"; import { rmdir, writeFile } from "fs/promises"; -import { dirname, join } from "path"; +import { join } from "path"; export type InstallActionOptions = GlobalOptions & { dryRun: boolean; @@ -39,7 +40,9 @@ export default async function install(options: InstallActionOptions) { console.info(); } - if (!options.noBackup) { + const config = await parseConfigFile(globalOptions.config); + + if (!options.noBackup && config.backup.enabled) { const backupResult = await backup({ ...globalOptions, log: false, @@ -48,24 +51,19 @@ export default async function install(options: InstallActionOptions) { return { exitCode: backupResult.exitCode, changes: 0 }; } - const tpl = await readTplFile(options.templatePath); + const tpl = await parseTplFile(config.template.path); const { resources, secrets } = await resolveTpl(tpl, { + config, filter: options.filter, - lockPath: options.lockPath, - outPath: options.outPath, }); - const secretsPath = join(dirname(options.lockPath), "rtpl.secrets.json"); - - await writeFile(secretsPath, JSON.stringify(secrets, null, 2)); + if (secrets) + await writeFile(config.secrets.path, JSON.stringify(secrets, null, 2)); //await tpl.onBeforeInstall?.(options); - const lockData = lock.parseFile(options.lockPath, true) ?? { - templates: {}, - }; + const lockData = lock.parseFile(config.lock.path); - const lockDir = dirname(options.lockPath); let changes = 0; let errors = 0; @@ -83,7 +81,12 @@ export default async function install(options: InstallActionOptions) { changes++; } try { - const dirs = await execFileAction(action, lockDir, path, options.dryRun); + const dirs = await execFileAction( + action, + config.root, + path, + options.dryRun, + ); if (action.lock) { selfLockData.files[path] = action.lock; if (dirs) { @@ -109,14 +112,14 @@ export default async function install(options: InstallActionOptions) { const dirs = Object.keys(selfLockData.dirs).sort().reverse(); for (const dir of dirs) { try { - await rmdir(join(lockDir, dir)); + await rmdir(join(config.root, dir)); delete selfLockData.dirs[dir]; } catch (error) {} } lockData.templates[tpl.config.name] = selfLockData; - if (!options.dryRun) await lock.writeFile(options.lockPath, lockData); + if (!options.dryRun) await lock.writeFile(config.lock.path, lockData); //await tpl.onInstall?.(options); diff --git a/packages/cli/src/actions/options.ts b/packages/cli/src/actions/options.ts index 46cfbce..9063074 100644 --- a/packages/cli/src/actions/options.ts +++ b/packages/cli/src/actions/options.ts @@ -1,5 +1,6 @@ import { GlobalOptions } from "../cli.js"; -import { readTplFile } from "../utils/self/resolve.js"; +import { parseConfigFile } from "../utils/self/config.js"; +import { parseTplFile } from "../utils/self/resolve.js"; import camelCase from "lodash.camelcase"; import { stringify } from "yaml"; @@ -8,7 +9,8 @@ export type OptionsOptions = GlobalOptions & { }; export default async function options(options: OptionsOptions) { - const tpl = await readTplFile(options.templatePath); + const config = await parseConfigFile(options.config); + const tpl = await parseTplFile(config.template.path); const tplOptions = await tpl.options(); if (options.format === "yaml") { diff --git a/packages/cli/src/actions/render.ts b/packages/cli/src/actions/render.ts index 8bc4cdb..6bccdff 100644 --- a/packages/cli/src/actions/render.ts +++ b/packages/cli/src/actions/render.ts @@ -1,12 +1,13 @@ import { GlobalOptions } from "../cli.js"; -import { readTplFile, resolveTpl } from "../utils/self/resolve.js"; +import { parseConfigFile } from "../utils/self/config.js"; +import { parseTplFile, resolveTpl } from "../utils/self/resolve.js"; export default async function render(options: GlobalOptions) { - const tpl = await readTplFile(options.templatePath); + const config = await parseConfigFile(options.config); + const tpl = await parseTplFile(config.template.path); const { resources } = await resolveTpl(tpl, { + config, filter: options.filter, - lockPath: options.lockPath, - outPath: options.outPath, }); for (const path in resources) { const res = resources[path]; diff --git a/packages/cli/src/actions/repair-lock.ts b/packages/cli/src/actions/repair-lock.ts index b797dcc..c68206d 100644 --- a/packages/cli/src/actions/repair-lock.ts +++ b/packages/cli/src/actions/repair-lock.ts @@ -1,5 +1,6 @@ import { GlobalOptions } from "../cli.js"; import { confirmPrompt } from "../utils/cli.js"; +import { parseConfigFile } from "../utils/self/config.js"; import * as lock from "../utils/self/lock.js"; import chalk from "chalk"; import { existsSync } from "fs"; @@ -7,9 +8,8 @@ import { existsSync } from "fs"; export type RepairLockOptions = GlobalOptions & {}; export default async function repairLock(options: RepairLockOptions) { - const lockData = (lock.parseFile(options.lockPath, true) ?? { - templates: {}, - }) as any as lock.LockData; + const config = await parseConfigFile(options.config); + const lockData = lock.parseFile(config.lock.path) as any as lock.LockData; const notfounds: string[] = []; @@ -38,7 +38,7 @@ export default async function repairLock(options: RepairLockOptions) { if (!confirm) return { exitCode: 1 }; - await lock.writeFile(options.lockPath, lockData); + await lock.writeFile(config.lock.path, lockData); console.info(chalk.green("Lock file repared.")); diff --git a/packages/cli/src/actions/restore.ts b/packages/cli/src/actions/restore.ts index 8146a6b..481c288 100644 --- a/packages/cli/src/actions/restore.ts +++ b/packages/cli/src/actions/restore.ts @@ -1,5 +1,6 @@ import { GlobalOptions } from "../cli.js"; import { confirmPrompt } from "../utils/cli.js"; +import { parseConfigFile } from "../utils/self/config.js"; import { LockData } from "../utils/self/lock.js"; import chalk from "chalk"; import { existsSync } from "fs"; @@ -43,8 +44,9 @@ const normalizePath = (path: string) => relative(process.cwd(), path).replace(/\\/g, "/"); export default async function restore(options: RestoreOptions) { + const config = await parseConfigFile(options.config); const path = normalizePath( - await findBackup(options.backupPath, options.input), + await findBackup(config.backup.path, options.input), ); const yaml = (await readFile(path)).toString(); const lockData = parse(yaml, { version: "1.1" }) as LockData<{ @@ -74,7 +76,7 @@ export default async function restore(options: RestoreOptions) { errorFiles.push(path); }; - if (existsSync(options.lockPath)) logError(options.lockPath); + if (existsSync(config.lock.path)) logError(config.lock.path); for (const name in lockData.templates) { const tpl = lockData.templates[name]; @@ -105,7 +107,7 @@ export default async function restore(options: RestoreOptions) { } } - await writeFile(options.lockPath, JSON.stringify(lockData, null, 2)); + await writeFile(config.lock.path, JSON.stringify(lockData, null, 2)); console.info(); console.info(chalk.green("Backup restored successfully.")); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 234a522..8f128f9 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -7,23 +7,17 @@ import options from "./actions/options.js"; import render from "./actions/render.js"; import repairLock from "./actions/repair-lock.js"; import restore from "./actions/restore.js"; -import { resolveUnixPath } from "./utils/fs.js"; import { GlobalOptions } from "./utils/self/options.js"; import { parseStringListValue } from "./utils/string.js"; import chalk from "chalk"; import { program } from "commander"; import { readFileSync } from "fs"; -import { resolve } from "path"; import { fileURLToPath } from "url"; export type { GlobalOptions }; function getGlobalOptions(): GlobalOptions { - const options = program.opts() as GlobalOptions; - options.templatePath = resolveUnixPath(options.templatePath); - options.outPath = resolve(options.outPath); - options.lockPath = resolve(options.lockPath); - return options; + return program.opts() as GlobalOptions; } function makeAction(cb: (options: any) => Promise) { @@ -50,14 +44,7 @@ export default (defaultOptions?: Partial) => { program.name("rtpl"); program.version(pkg.version); program - .option("-t,--template-path ", "template file path", ".") - .option("--backup-path ", "backup path", "./.rtpl-backup") - .option( - "-l,--lock-path ", - "lock file path", - defaultOptions?.lockPath ?? "rtpl-lock.json", - ) - .option("-o,--out-path ", "out path", ".") + .option("-c,--config ", "config file path", "./rtpl-config.*") .option( "-f,--filter ", "patterns filter", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e8be571..632864e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -17,3 +17,4 @@ export { SecretRes, SecretData } from "./resources/SecretRes.js"; export { YamlRes } from "./resources/YamlRes.js"; export { default as cli } from "./cli.js"; export { type ResourceSystem } from "./utils/self/rs.js"; +export { defineConfig, RtplConfig, InRtplConfg } from "./utils/self/config.js"; diff --git a/packages/cli/src/resources/AbstractRes.ts b/packages/cli/src/resources/AbstractRes.ts index 9f53f3e..d1d8832 100644 --- a/packages/cli/src/resources/AbstractRes.ts +++ b/packages/cli/src/resources/AbstractRes.ts @@ -89,7 +89,7 @@ export abstract class AbstractRes< ); } - async onReady(path: string, secrets: Secrets) { + async onReady(path: string, secrets: Secrets | undefined) { setDelayedValue(this.path, path); setDelayedValue(this.dirname, path); } diff --git a/packages/cli/src/resources/SecretRes.ts b/packages/cli/src/resources/SecretRes.ts index 8d47892..e2136bf 100644 --- a/packages/cli/src/resources/SecretRes.ts +++ b/packages/cli/src/resources/SecretRes.ts @@ -6,6 +6,7 @@ import { randomString, upperAlphaCharset, } from "../utils/crypto.js"; +import { readIfExists } from "../utils/fs.js"; import { Secrets } from "../utils/self/secrets.js"; import { AbstractRes, ResOptions, ResType } from "./AbstractRes.js"; import { DelayedValue, setDelayedValue } from "./DelayedValue.js"; @@ -60,9 +61,11 @@ export class SecretRes extends AbstractRes< override toString() { return this.value.toString(); } - override async onReady(path: string, secrets: Secrets) { + override async onReady(path: string, secrets: Secrets | undefined) { await super.onReady(path, secrets); - const value = secrets[path]; + const value = secrets + ? secrets[path] + : (await readIfExists(path))?.toString(); const hasNewValue = value ? false : true; const generate = this.data?.generate ?? true; let endValue = value; @@ -83,7 +86,8 @@ export class SecretRes extends AbstractRes< if (charset.length <= 10) { throw new Error(`Charset length is too small`); } - endValue = secrets[path] = randomString(length, charset); + endValue = randomString(length, charset); + if (secrets) secrets[path] = endValue; } const auxValue = await this.data?.onReady?.({ path, diff --git a/packages/cli/src/utils/fs.ts b/packages/cli/src/utils/fs.ts index 0e7c46c..b9f675c 100644 --- a/packages/cli/src/utils/fs.ts +++ b/packages/cli/src/utils/fs.ts @@ -14,6 +14,11 @@ export async function checkPath(path: string) { return !!(await statIfExists(path)); } +export async function findPath(paths: string[]): Promise { + for (const path of paths) { + if (await checkPath(path)) return path; + } +} export async function readIfExists(path: string) { try { return await readFile(path); @@ -46,3 +51,15 @@ export async function mkrdir(dir: string, baseDir?: string) { } return dirs; } + +export async function readAnyFile(path: string) { + if (/\.[cm]?[jt]s$/i.test(path)) { + const object = await import(`file://${path}`); + return object.default ?? object; + } else if (/\.json$/i.test(path)) { + const buffer = await readFile(path); + return JSON.parse(buffer.toString()); + } else { + throw new Error(`Invalid extension: ${path}`); + } +} diff --git a/packages/cli/src/utils/path.ts b/packages/cli/src/utils/path.ts index 29d037c..861e1ab 100644 --- a/packages/cli/src/utils/path.ts +++ b/packages/cli/src/utils/path.ts @@ -23,3 +23,15 @@ export function stripRootBackPaths(path: string) { }) .join("/"); } + +export function expandPaths( + path: string, + ext: { js?: boolean; ts?: boolean; json?: boolean }, +) { + const exts: string[] = [ + ...(ext.js ? ["js", "cjs", "mjs"] : []), + ...(ext.ts ? ["ts", "cts", "mts"] : []), + ...(ext.json ? ["json"] : []), + ]; + return path.includes("*") ? exts.map((e) => path.replace("*", e)) : [path]; +} diff --git a/packages/cli/src/utils/self/config.ts b/packages/cli/src/utils/self/config.ts new file mode 100644 index 0000000..99b6911 --- /dev/null +++ b/packages/cli/src/utils/self/config.ts @@ -0,0 +1,94 @@ +import { findPath, readAnyFile } from "../fs.js"; +import { merge } from "../object.js"; +import { expandPaths } from "../path.js"; +import { dirname, join, resolve } from "path"; + +export type RtplConfig = { + /** + * @default "." + */ + root: string; + template: { + /** + * @default "rtpl.*" + */ + path: string; + }; + lock: { + /** + * @default "rtpl-lock.json" + */ + path: string; + }; + resources: { + /** + * @default "resources" + */ + path: string; + }; + secrets: { + /** + * @default true + */ + enabled: boolean; + /** + * @default "rtpl-secrets.json" + */ + path: string; + }; + backup: { + /** + * @default true + */ + enabled: boolean; + /** + * @default ".rtpl-backup" + */ + path: string; + }; +}; + +export type InRtplConfg = Partial; + +const defaults: RtplConfig = { + root: ".", + template: { + path: "rtpl.*", + }, + lock: { + path: "rtpl-lock.json", + }, + resources: { + path: "resources", + }, + secrets: { + enabled: true, + path: "rtpl-secrets.json", + }, + backup: { + enabled: true, + path: ".rtpl-backup", + }, +}; + +export function defineConfig(config: InRtplConfg): InRtplConfg { + return config; +} + +export async function parseConfigFile(inPath: string): Promise { + const paths = expandPaths(inPath, { js: true, ts: true, json: true }); + const path = await findPath(paths); + const inConfig = path ? await readAnyFile(resolve(path)) : undefined; + const config: RtplConfig = merge( + structuredClone(defaults), + structuredClone(inConfig), + ); + if (!config.root || config.root === ".") config.root = dirname(inPath); + config.root = resolve(config.root); + config.template.path = join(config.root, config.template.path); + config.lock.path = join(config.root, config.lock.path); + config.resources.path = join(config.root, config.resources.path); + config.backup.path = join(config.root, config.backup.path); + config.secrets.path = join(config.root, config.secrets.path); + return config; +} diff --git a/packages/cli/src/utils/self/lock.ts b/packages/cli/src/utils/self/lock.ts index 866f100..2f2095c 100644 --- a/packages/cli/src/utils/self/lock.ts +++ b/packages/cli/src/utils/self/lock.ts @@ -90,15 +90,15 @@ export async function writeFile(path: string, data: LockData) { await _writeFile(path, JSON.stringify(data, null, 2)); } -export function parseFile( - path: string, - ifExists?: boolean, -): LockData | undefined { +export function parseFile(path: string): LockData { try { const json = readFileSync(path).toString(); return JSON.parse(json); } catch (error) { - if (ifExists && (error as NodeJS.ErrnoException).code === "ENOENT") return; + if ((error as NodeJS.ErrnoException).code === "ENOENT") + return { + templates: {}, + }; throw error; } } diff --git a/packages/cli/src/utils/self/options.ts b/packages/cli/src/utils/self/options.ts index 21af0d7..f2e18da 100644 --- a/packages/cli/src/utils/self/options.ts +++ b/packages/cli/src/utils/self/options.ts @@ -1,15 +1,11 @@ export type GlobalOptions = { - templatePath: string; - backupPath: string; - outPath: string; - lockPath: string; + config: string; filter: string[] | undefined; }; export function splitGlobalOptions( input: T, ): [GlobalOptions, Omit] { - const { backupPath, filter, lockPath, outPath, templatePath, ...others } = - input; - return [{ backupPath, filter, lockPath, outPath, templatePath }, others]; + const { config, filter, ...others } = input; + return [{ config, filter }, others]; } diff --git a/packages/cli/src/utils/self/resolve.ts b/packages/cli/src/utils/self/resolve.ts index 8b32c89..a3d487d 100644 --- a/packages/cli/src/utils/self/resolve.ts +++ b/packages/cli/src/utils/self/resolve.ts @@ -1,32 +1,22 @@ import { AbstractRes } from "../../resources/AbstractRes.js"; import { MinimalDirRes } from "../../resources/DirRes.js"; -import { checkPath, statIfExists } from "../fs.js"; +import { findPath, readAnyFile } from "../fs.js"; import { isPlainObject } from "../object.js"; -import { isDir, isPath, stripRootBackPaths } from "../path.js"; +import { expandPaths, isDir, isPath, stripRootBackPaths } from "../path.js"; import { makeFilter } from "../string.js"; +import { RtplConfig } from "./config.js"; import { MinimalTpl, ResourcesResultItem } from "./minimal-tpl.js"; import { createResourceSystem } from "./rs.js"; -import { getSecretsPath, parseSecretsFile } from "./secrets.js"; +import { parseSecretsFile } from "./secrets.js"; import mm from "micromatch"; -import { basename, dirname, join, relative } from "path"; +import { basename, join, relative } from "path"; import * as posix from "path/posix"; export type ResolveConfigOptions = { + config: RtplConfig; filter?: string[]; - outPath: string; - lockPath: string; }; -export function resolvePath(data: { - path: string; - outPath: string; - lockDir: string; -}) { - let path = join(data.outPath, data.path); - path = relative(data.lockDir, path); - return path.replace(/\\/g, "/"); -} - function resolveTag(tag: string, input: AbstractRes) { const defaultExtension = input.getDefaultExtension(); if ( @@ -67,9 +57,8 @@ function resolvePathLevels(res: AbstractRes, levels: string[]) { export async function resolveResources(options: { resources: unknown; - outPath?: string; - lockDir?: string; filter?: string[]; + config: RtplConfig; onValue?: ( key: string, value: AbstractRes, @@ -105,11 +94,7 @@ export async function resolveResources(options: { } const pathLevels = resolvePathLevels(input, levels); - const path = resolvePath({ - path: pathLevels.join("/"), - lockDir: options.lockDir ?? ".", - outPath: options.outPath ?? ".", - }); + const path = join(...pathLevels).replace(/\\/g, "/"); if ( patterns && @@ -134,50 +119,26 @@ export async function resolveResources(options: { return values; } -export async function readTplFile(path: string): Promise { - const info = await statIfExists(path); - - if (!info) throw new Error(`Invalid path: ${path}`); - - let filePath: string | undefined; - - if (info.isDirectory()) { - const paths = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"].map((ext) => - join(path, `rtpl${ext}`), - ); - for (const v of paths) { - if (await checkPath(v)) filePath = v; - } - if (!filePath) throw new Error(`Invalid path: ${path}`); - } else { - filePath = path; - } - - if (/\.[cm]?[jt]s$/i.test(filePath)) { - const object = await import(`file://${filePath}`); - return object.default ?? object; - } else { - throw new Error(`Invalid values path: ${path}`); - } +export async function parseTplFile(inPath: string): Promise { + const paths = expandPaths(inPath, { js: true, ts: true }); + const path = await findPath(paths); + if (!path) throw new Error(`Template file not found: ${inPath}`); + return readAnyFile(path); } export async function resolveTpl( tpl: MinimalTpl, options: ResolveConfigOptions, ) { - const lockDir = dirname(options.lockPath); - const outPath = options.outPath; - const filter = options.filter; - const resolveOptions = { - lockDir, - outPath, - filter, - }; - - const outFolder = "resources"; + const { config } = options; + const resourcesDir = relative(config.root, config.resources.path).replace( + /\\/g, + "/", + ); const resultItems: ResourcesResultItem[] = []; const inResources = await resolveResources({ - ...resolveOptions, + config, + filter: options.filter, resources: await tpl.resources(resultItems), onValue: async (path, res, actions) => { if (res.resolved === false) { @@ -195,13 +156,15 @@ export async function resolveTpl( await item.tpl.config.onResolve?.bind(rs)(item.resources, tplOptions); } - const secretsPath = getSecretsPath(lockDir); - const secrets = await parseSecretsFile(secretsPath); + const secrets = config.secrets.enabled + ? await parseSecretsFile(config.secrets.path) + : undefined; const resources = await resolveResources({ - ...resolveOptions, + config, + filter: options.filter, resources: inResources, onValue: async (path, res, actions) => { - await res.onReady(posix.join(outFolder, path), secrets); + await res.onReady(posix.join(resourcesDir, path), secrets); if (MinimalDirRes.isInstance(res)) { actions.add = false; actions.process = !res.resolved; @@ -216,7 +179,7 @@ export async function resolveTpl( if (isRootPath) { resources[newPath] = resources[path]; } else { - resources[posix.join(outFolder, newPath)] = resources[path]; + resources[posix.join(resourcesDir, newPath)] = resources[path]; } delete resources[path]; } diff --git a/packages/cli/src/utils/self/secrets.ts b/packages/cli/src/utils/self/secrets.ts index 4027bec..8e118c5 100644 --- a/packages/cli/src/utils/self/secrets.ts +++ b/packages/cli/src/utils/self/secrets.ts @@ -1,12 +1,7 @@ import { readIfExists } from "../fs.js"; -import { join } from "path"; export type Secrets = Record; -export function getSecretsPath(dir: string) { - return join(dir, "rtpl.secrets.json"); -} - export async function parseSecretsFile(path: string): Promise { const buffer = await readIfExists(path); return buffer ? JSON.parse(buffer.toString()) : {};