staticql
Version:
Type-safe query engine for static content including Markdown, YAML, JSON, and more.
108 lines (107 loc) • 4.29 kB
JavaScript
import path from "path";
import { parseByType } from "../parser/index.js";
import { GitDiffProvider } from "./providers/index.js";
import { SourceConfigResolver as Resolver, } from "../SourceConfigResolver.js";
import { resolveField } from "../utils/field.js";
import { asArray } from "../utils/normalize.js";
export async function extractDiff(opts) {
const { config, customIndexers = {}, diffProvider } = opts;
const provider = diffProvider ?? new GitDiffProvider(opts.baseDir);
const baseRef = opts.baseRef ?? "origin/main";
const headRef = opts.headRef ?? "HEAD";
const resolver = new Resolver(config.sources);
const resolved = resolver.resolveAll();
const results = [];
/* -------- helpers -------- */
const parse = async (text, ext) => {
if (!text)
return [];
if (ext === ".md") {
return asArray(await parseByType("markdown", { rawContent: text }));
}
if (ext === ".yaml" || ext === ".yml") {
return asArray(await parseByType("yaml", { rawContent: text }));
}
if (ext === ".json") {
return asArray(await parseByType("json", { rawContent: text }));
}
return [];
};
/* -------- git diff -------- */
const diffLines = await provider.diffLines(baseRef, headRef);
/* -------- main loop ------- */
for (const { status: stat, path: filePath } of diffLines) {
const filePathBase = Resolver.extractBaseDir(filePath.replace(/\/$/, "").replace(`${opts.baseDir}`, "")).replace(/^\//, "");
const rsc = resolved.find((s) => {
return Resolver.patternTest(s.pattern, filePathBase);
});
if (!rsc)
continue;
const ext = path.extname(filePath).toLowerCase();
const headText = ["A", "M"].includes(stat)
? await provider.gitShow(headRef, filePath)
: null;
const baseText = ["D", "M"].includes(stat)
? await provider.gitShow(baseRef, filePath)
: null;
const headRecs = headText ? await parse(headText, ext) : [];
const baseRecs = baseText ? await parse(baseText, ext) : [];
headRecs.forEach((rec) => {
if (!rec.slug) {
rec.slug = Resolver.getSlugFromPath(rsc.pattern, filePathBase);
}
});
baseRecs.forEach((rec) => {
if (!rec.slug) {
rec.slug = Resolver.getSlugFromPath(rsc.pattern, filePathBase);
}
});
if (stat === "A")
headRecs.forEach((rec) => emit("A", rec, rsc));
if (stat === "D")
baseRecs.forEach((rec) => emit("D", rec, rsc));
if (stat === "M")
processModified(headRecs, baseRecs, rsc);
}
return results;
/* ===== local fns ============================================= */
function buildFields(rec, rsc) {
const out = {};
for (const key of Object.keys(rsc.indexes ?? {})) {
const customKey = `${rsc.name}.${key}`;
const customFn = customIndexers[customKey];
if (customFn) {
// customIndex
out[key] = customFn(rec);
}
else {
// index / relation localKey
out[key] = resolveField(rec, key);
}
}
return out;
}
function emit(status, rec, rsc, oldRec) {
const fields = buildFields(rec, rsc);
if (status === "M" &&
oldRec &&
JSON.stringify(fields) === JSON.stringify(buildFields(oldRec, rsc)))
return;
// slug as String for src/Indexer.ts:getStatus
fields["slug"] = rec.slug;
results.push({ status, source: rsc.name, slug: rec.slug, fields });
}
function processModified(head, base, rsc) {
const hm = new Map(head.map((r) => [r.slug, r]));
const bm = new Map(base.map((r) => [r.slug, r]));
for (const s of hm.keys())
if (!bm.has(s))
emit("A", hm.get(s), rsc);
for (const s of bm.keys())
if (!hm.has(s))
emit("D", bm.get(s), rsc);
for (const s of hm.keys())
if (bm.has(s))
emit("M", hm.get(s), rsc, bm.get(s));
}
}