mimic-tree
Version:
mimic-tree is a Node.js library for creating dynamic data trees that mimic the structure of source data. It reproduces the original folder paths and file structures like JSON or YAML with different values, maintaining the integrity of the original structu
244 lines (241 loc) • 7.74 kB
JavaScript
import { resolve } from 'path';
import { promises } from 'fs';
import globby from 'globby';
import { PromisePool } from '@supercharge/promise-pool';
// src/lib/type.ts
function isPrimitive(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null || value === void 0;
}
function isPureObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
var defaultConfig = {
cwd: process.cwd(),
extensions: ["json"],
cache: true,
parallel: false
};
var { readFile } = promises;
async function loadContext(config) {
const baseDir = resolve(config.cwd ?? process.cwd(), config.src);
const [founds, ignores] = await Promise.all([
globFiles(baseDir, config),
ignoreFiles(baseDir, config)
]);
const files = founds.filter((file) => !ignores.includes(file));
return {
files: await loadAllFiles(baseDir, files, config),
config
};
}
var extensionAliases = {
yaml: ["yml", "yaml"],
json: ["json"]
};
Object.freeze(extensionAliases);
var extensions = Object.keys(extensionAliases);
Object.freeze(extensions);
function globFiles(baseDir, config) {
return globby(baseDir, {
expandDirectories: {
extensions: (config.extensions ?? ["json"]).flatMap((ext) => extensionAliases[ext])
},
gitignore: false,
cwd: config.cwd
});
}
async function ignoreFiles(baseDir, config) {
if ((config.ignore ?? []).length <= 0) {
return [];
}
return globby(config.ignore, {
gitignore: true,
cwd: config.cwd
});
}
async function loadAllFiles(baseDir, files, config) {
const loader = new JsonLoader(baseDir, config);
await loader.init();
const load = async (filePath) => loader.load(filePath);
const results = await Promise.all(files.map(load));
if (results.some((result) => result.error)) {
throw new Error();
}
const outputBaseDir = resolve(config.cwd ?? process.cwd(), config.dist);
return results.map((result) => ({
...result,
outputFile: resolve(outputBaseDir, result.filePath, result.fileName).replace(outputBaseDir, ".")
}));
}
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(fullpath) {
return "./" + fullpath.replace(/^\.\//, "").replace(new RegExp(`^${this.baseDir.replace(/^\.\//, "")}/`), "").replace(getFileName(fullpath), "");
}
#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.baseDir, filePath);
const fileName = getFileName(filePath);
const extension = getExtension(filePath);
this.#assertSupportedExtension(extension);
const fixedFilePath = this.#getPath(filePath);
const raw = await readFile(importPath, "utf-8");
try {
return {
filePath: fixedFilePath,
fileName,
extension,
json: this.#parse(raw, extension)
};
} catch (e) {
return {
filePath: fixedFilePath,
fileName,
extension,
error: this.#errorToString(e)
};
}
}
};
function getExtension(filePath) {
const ext = filePath.split(".").pop() ?? "";
const found = Object.entries(extensionAliases).find(
([, aliases]) => aliases.includes(ext)
);
return found?.[0] ?? ext;
}
function getFileName(filePath) {
return filePath.split("/").pop() ?? "";
}
var ROOT_KEY = "[ROOT]";
async function convert(fileContext, config) {
const convertTargets = convertFlattenKV("", fileContext.json);
const { results, errors } = await createWorker(config).for(convertTargets).process(async ({ key, value }) => {
const { convert: convert2 } = config;
const context = { key, value, file: fileContext };
const result = await convert2(value, context);
return { key, value: result };
});
if (errors.length > 0) {
throw errors[0];
}
return {
...fileContext,
json: toObject(results)
};
}
function convertFlattenKV(keyPrefix, value) {
if (isPrimitive(value)) {
return [{ key: keyPrefix || ROOT_KEY, value }];
}
if (Array.isArray(value)) {
return value.flatMap((v, i) => convertFlattenKV(`${keyPrefix}[${i}]`, v));
}
if (isPureObject(value)) {
const requiredDot = keyPrefix !== "" && !keyPrefix.endsWith("]");
return Object.entries(value).flatMap(
([k, v]) => convertFlattenKV(`${keyPrefix}${requiredDot ? "." : ""}${k}`, v)
);
}
return [];
}
function toObject(flattenKVs) {
if (flattenKVs.length <= 1) {
return void 0;
}
if (flattenKVs.length === 1 && flattenKVs[0].key === ROOT_KEY) {
return flattenKVs[0].value;
}
if (flattenKVs[0].key.startsWith("[")) {
const indexes = flattenKVs.map(({ key }) => key.match(/^\[(\d+)\]/)[1]).map(Number).filter((v, i, self) => self.indexOf(v) === i);
const pickByIndex = (index) => flattenKVs.filter(({ key }) => key.startsWith(`[${index}]`));
return indexes.reduce((acc, index) => {
const picked = pickByIndex(index).map(({ key, value }) => ({
key: key.replace(/^\[\d+\]/, ""),
value
}));
return [...acc, toObject(picked)];
}, []);
}
const keys = flattenKVs.map(({ key }) => key.includes(".") ? key.split(".")[0] : key).filter((v, i, self) => self.indexOf(v) === i);
return keys.reduce((acc, key) => {
const exactMatch = flattenKVs.find(({ key: k }) => k === key);
if (exactMatch) {
return { ...acc, [key]: exactMatch.value };
}
const picked = flattenKVs.filter(({ key: k }) => k.startsWith(`${key}.`)).map(({ key: key2, value }) => ({ key: key2.replace(/^.+\./, ""), value }));
return { ...acc, [key]: toObject(picked) };
}, {});
}
function createWorker({ parallel }) {
let pool = PromisePool;
if (typeof parallel === "number") {
pool = pool.withConcurrency(parallel);
}
return pool;
}
var { writeFile, mkdir } = promises;
async function exportFile(fileContext, config) {
const outputData = await stringifyStrategy[fileContext.extension](fileContext);
await write(fileContext, outputData, config);
}
async function write(fileContext, data, config) {
const outputBaseDir = resolve(config.cwd ?? process.cwd(), config.dist);
const filePath = fileContext.outputFile.split("/").slice(0, -1).join("/");
await mkdir(resolve(outputBaseDir, filePath), { recursive: true });
await writeFile(resolve(outputBaseDir, fileContext.outputFile), data);
}
var stringifyStrategy = {
json: (fileContext) => JSON.stringify(fileContext.json, null, 2),
yaml: async (fileContext) => {
const yaml = await import('yaml');
return yaml.stringify(fileContext.json);
}
};
// src/index.ts
async function mimicTree(config) {
const fixedConfig = {
...defaultConfig,
...config
};
const context = await loadContext(fixedConfig);
for (const f of context.files) {
const result = await convert(f, context.config);
await exportFile(result, context.config);
}
}
export { mimicTree };
//# sourceMappingURL=out.js.map
//# sourceMappingURL=index.js.map