UNPKG

json-fusion

Version:

Json-Fusion is a Node.js library designed to streamline the process of loading, merging, and reflecting directory hierarchies in JSON files. With Json-Fusion, you can consolidate information scattered across different JSON files, and reflect your filesyst

205 lines (200 loc) 5.76 kB
import { promises } from 'fs'; import { resolve } from 'path'; import globby from 'globby'; import { all } from 'deepmerge'; // src/lib/assert.ts function checkBaseDir(baseDir) { if (!baseDir) { throw new Error("baseDir is required"); } if (baseDir.includes("*")) { throw new Error(`baseDir must not contain wildcard. (baseDir: ${baseDir})`); } } async function finalize(result, config) { const finalized = await stringifyMap[config.exportType ?? "json"](result); if (config.exportType !== "object" && config.export) { await outputFile(finalized, config); } return finalized; } async function outputFile(result, config) { const exportPath = resolve(config.cwd ?? process.cwd(), config.export); await promises.mkdir(exportPath, { recursive: true }); await promises.writeFile(exportPath, JSON.stringify(result, null, 2)); } var stringifyMap = { json: (result) => JSON.stringify(result, null, 2), yaml: async (result) => { const { stringify } = await import('yaml'); return stringify(result, { indent: 2 }); }, object: (result) => result }; // src/lib/error.ts var JsonFusionError = class extends Error { name = "JsonFusionError"; constructor(message, reasons) { super(createMessage(message, reasons)); } }; function createMessage(message, reasons) { return `${message} ${reasons.map( ({ message: message2, filePath }) => [" -", message2, filePath ? `(filePath: ${filePath})` : ""].join(" ") ).join("\n")}`; } // src/lib/loader.ts var { readFile } = promises; async function loadContext(baseDir, config) { const [founds, ignores] = await Promise.all([ globFiles(baseDir, config), ignoreFiles(baseDir, config) ]); const files = founds.filter((file) => !ignores.includes(file)); return { files, config, jsons: await loadJsons(files, baseDir, config) }; } function globFiles(baseDir, config) { return globby(baseDir, { expandDirectories: { extensions: (config.extensions ?? ["json"]).flatMap((ext) => extensionAliases[ext]) }, gitignore: false, cwd: config.cwd }); } var extensionAliases = { yaml: ["yml", "yaml"], json: ["json"] }; var extensions = Object.values(extensionAliases).flat(); function getExtension(filePath) { const ext = filePath.split(".").pop() ?? ""; const found = Object.entries(extensionAliases).find( ([, aliases]) => aliases.includes(ext) ); return found?.[0] ?? ext; } async function ignoreFiles(baseDir, config) { if ((config.ignore ?? []).length <= 0) { return []; } return globby(config.ignore, { gitignore: true, cwd: config.cwd }); } async function loadJsons(files, baseDir, config) { const loader = new JsonLoader(baseDir, config); await loader.init(); const result = await Promise.all(files.map((path) => loader.load(path))); const errors = result.filter((item) => !!item.error); if (errors.length > 0) { throw new JsonFusionError("Failed to load jsons", errors.map(toErrorReason)); } return result; } var cwd = process.cwd(); var JsonLoader = class { constructor(baseDir, config) { this.baseDir = baseDir; this.config = config; } #parsers = { json: (raw) => JSON.parse(raw) }; async init() { if (this.config.extensions?.includes("yaml")) { this.#parsers.yaml = await this.#createYamlParser(); } } async #createYamlParser() { const { parse } = await import('yaml'); return (raw) => parse(raw, { prettyErrors: true }); } #assertSupportedExtension(ext) { if (!(this.config.extensions ?? ["json"]).includes(ext)) { throw new Error(`Unsupported file extension: ${ext}`); } } #getPath(filePath) { return filePath.replace(/^\.\//, "").replace(new RegExp(`^${this.baseDir.replace(/^\.\//, "")}/`), "").replace(new RegExp(`\\.(${extensions.join("|")})$`), ""); } #parse(raw, ext) { const parser = this.#parsers[ext]; if (!parser) { throw new Error(`Unsupported file extension: ${ext}`); } return parser(raw); } #errorToString(e) { if (e instanceof Error) { return `${e.name}: ${e.message}`; } return String(e); } async load(filePath) { const importPath = resolve(this.config.cwd ?? cwd, filePath); const ext = getExtension(filePath); this.#assertSupportedExtension(ext); const path = this.#getPath(filePath); const raw = await readFile(importPath, "utf-8"); try { return { filePath, path, json: this.#parse(raw, ext) }; } catch (e) { return { filePath, path, error: this.#errorToString(e) }; } } }; function toErrorReason(result) { return { message: result.error, filePath: result.filePath }; } function mergeJson(context) { const jsons = context.jsons.sort((a, b) => a.path.localeCompare(b.path)).map(({ path, json }) => fixHierarchy(path, json, context.config)); return all(jsons); } function fixHierarchy(path, json, config) { const keys = path.split("/").filter((key3) => key3 !== ""); if (!config.noSpreadIndex && keys[keys.length - 1] === "index") { keys.pop(); } if (keys.length === 0) { if (typeof json !== "object") { throw new Error("root json must be object"); } return json; } return keys.reduceRight((acc, key3) => { return { [key3]: acc }; }, json); } // src/index.ts async function main(baseDir, config) { checkBaseDir(baseDir); const context = await loadContext(baseDir, config); const result = mergeJson(context); return await finalize(result, config); } function jsonFusion(baseDir, config = {}) { return main(baseDir, config); } export { JsonFusionError, jsonFusion }; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.js.map