import fs from "fs"; import path from "path"; import { AppData } from "../data/app-data"; export type DataFileFormat = "ts" | "js" | "json" | "yaml"; export type ResolvedDataFile = { path: string; basename: string; extension: (typeof AppData.SupportedDataFileExtensions)[number]; format: DataFileFormat; }; export type LoadedDataFile = ResolvedDataFile & { data: T; }; function extensionToFormat( extension: (typeof AppData.SupportedDataFileExtensions)[number], ): DataFileFormat { switch (extension) { case ".ts": return "ts"; case ".js": return "js"; case ".json": return "json"; case ".yaml": case ".yml": return "yaml"; } } /** * Resolve a basename (no extension) to an existing file among supported formats. * Priority: .ts > .js > .json > .yaml > .yml * Errors if multiple matches exist. */ export function resolveDataFile( dir: string, baseName: string, ): ResolvedDataFile | null { const matches: ResolvedDataFile[] = []; for (const extension of AppData.SupportedDataFileExtensions) { const filePath = path.join(dir, `${baseName}${extension}`); if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { matches.push({ path: filePath, basename: `${baseName}${extension}`, extension, format: extensionToFormat(extension), }); } } if (matches.length === 0) { return null; } if (matches.length > 1) { const found = matches.map((m) => m.basename).join(", "); throw new Error( `Multiple \`${baseName}\` files found (${found}). Keep only one of: ${AppData.SupportedDataFileExtensions.join(", ")}`, ); } return matches[0]!; } export function supportedDataFileNames(baseName: string): string { return AppData.SupportedDataFileExtensions.map( (ext) => `\`${baseName}${ext}\``, ).join(", "); } export function isSchemaFileName(name: string): boolean { const base = path.basename(name); if (base === AppData.DbSchemaFileName) { return true; } return AppData.SupportedDataFileExtensions.some( (ext) => base === `${AppData.DbSchemaFileName}${ext}`, ); } /** * Load a data file as a plain object. * - .ts / .js: require() and use default export (or module itself) * - .json: JSON.parse * - .yaml / .yml: Bun.YAML.parse */ export function loadDataFile(resolved: ResolvedDataFile): T { if (resolved.format === "ts" || resolved.format === "js") { const imported = require(resolved.path); const data = imported && typeof imported === "object" && "default" in imported ? imported.default : imported; if (data == null) { throw new Error( `No default export from \`${resolved.path}\`. Please export a default module.`, ); } return data as T; } const text = fs.readFileSync(resolved.path, "utf-8"); if (resolved.format === "json") { return JSON.parse(text) as T; } return Bun.YAML.parse(text) as T; } export function resolveAndLoadDataFile( dir: string, baseName: string, ): LoadedDataFile | null { const resolved = resolveDataFile(dir, baseName); if (!resolved) { return null; } return { ...resolved, data: loadDataFile(resolved), }; }