Files
2026-09-09 13:09:19 -04:00

78 lines
2.9 KiB
JavaScript

import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { relative, dirname, join, sep } from "node:path";
import { randomBytes } from "node:crypto";
const arg = (k, d) => { const i = process.argv.indexOf(k); return i > -1 ? process.argv[i + 1] : d; };
const SRC = arg("--src", "content");
const TARGET = arg("--target", "_manifest.js");
// --- uuid v7: 48-bit ms timestamp (time-ordered) + version/variant + random ---
function uuidv7(){
const ts = Date.now();
const b = randomBytes(16);
b[0]=(ts/2**40)&0xff; b[1]=(ts/2**32)&0xff; b[2]=(ts/2**24)&0xff;
b[3]=(ts/2**16)&0xff; b[4]=(ts/2**8)&0xff; b[5]=ts&0xff;
b[6]=(b[6]&0x0f)|0x70; // version 7
b[8]=(b[8]&0x3f)|0x80; // variant 10
const h = b.toString("hex");
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
}
const ID_RE = /<!--\s*id:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s*-->/i;
const idLine = id => `<!-- id: ${id} -->`;
const files = readdirSync(SRC).filter(f => f.endsWith(".md")).sort();
// --- discovery: every card gets a stable v7 id. mint if absent (incl. user-removed),
// remint on collision (bootstrap audit). writes the id into the card at EOF. ---
const seen = new Set();
const idOf = {};
let minted = 0, reminted = 0;
for (const f of files) {
const p = join(SRC, f);
let md = readFileSync(p, "utf8");
const m = md.match(ID_RE);
let id = m ? m[1].toLowerCase() : null;
if (!id) { // absent → mint, append at bottom
id = uuidv7();
md = md.replace(/\s*$/, "") + `\n\n${idLine(id)}\n`;
writeFileSync(p, md); minted++;
} else if (seen.has(id)) { // collision → remint in place
id = uuidv7();
md = md.replace(ID_RE, idLine(id));
writeFileSync(p, md); reminted++;
}
seen.add(id);
idOf[f] = id;
}
// --- manifest ---
const parseTags = md => ((md.match(/^---\n([\s\S]*?)\n---/)?.[1]
.match(/tags:\s*\[([^\]]*)\]/)?.[1]) || "")
.split(",").map(s => s.trim()).filter(Boolean);
const allTags = new Set();
for (const f of files) parseTags(readFileSync(join(SRC, f), "utf8")).forEach(t => allTags.add(t));
const spec = f => {
let r = relative(dirname(TARGET) || ".", join(SRC, f)).split(sep).join("/");
return r.startsWith(".") ? r : "./" + r;
};
const imports = files.map((f, i) => `import c${i} from ${JSON.stringify(spec(f))};`).join("\n");
const items = files.map((f, i) =>
` { slug: ${JSON.stringify(f.replace(/\.md$/,""))}, id: ${JSON.stringify(idOf[f])}, md: c${i} }`).join(",\n");
const tags = [...allTags].sort();
writeFileSync(TARGET,
`// AUTO-GENERATED by gen.mjs — do not edit
${imports}
export const CARDS = [
${items}
];
// tag vocabulary, derived at build (not boot)
export const TAGS = ${JSON.stringify(tags)};
`);
console.log(`gen: ${files.length} card(s), ${tags.length} tag(s), +${minted} id(s), ${reminted} reminted -> ${TARGET}`);