import { execFileSync } from "node:child_process"; import { mkdir, readFile, rm, writeFile, copyFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; const root = process.cwd(); const wryRoot = path.resolve(process.env.WRY_REPO_PATH ?? path.join(root, "..", "wry")); const specDir = path.join(wryRoot, "crates", "toml-spec", "spec"); const schemaPath = path.join(specDir, "spec.generated.json"); const docsDir = path.join(root, "src", "content", "docs"); const referenceDir = path.join(docsDir, "reference"); const typesDir = path.join(referenceDir, "types"); const publicSpecDir = path.join(root, "public", "spec"); const copiedSpecs = ["spec.generated.json", "spec.generated.md", "spec.yaml"]; function frontmatter({ title, description }) { return [ "---", `title: ${JSON.stringify(title)}`, description ? `description: ${JSON.stringify(description)}` : undefined, "---", "" ] .filter(Boolean) .join("\n"); } function slugify(name) { return name .replace(/([a-z0-9])([A-Z])/g, "$1-$2") .replace(/[^A-Za-z0-9]+/g, "-") .replace(/^-|-$/g, "") .toLowerCase(); } function trimDescription(description) { return typeof description === "string" ? description.trim() : ""; } function escapePipe(value) { return String(value).replaceAll("|", "\\|").replaceAll("\n", "
"); } function inlineCode(value) { return `\`${String(value).replaceAll("`", "\\`")}\``; } function refName(ref) { const prefix = "#/$defs/"; return typeof ref === "string" && ref.startsWith(prefix) ? ref.slice(prefix.length) : null; } function linkRef(ref) { const name = refName(ref); if (!name) { return inlineCode(ref); } return `[${name}](/reference/types/${slugify(name)}/)`; } function schemaType(schema) { if (!schema || typeof schema !== "object") { return "unknown"; } if (schema.$ref) { return linkRef(schema.$ref); } if (schema.const !== undefined) { return `constant ${inlineCode(schema.const)}`; } if (Array.isArray(schema.enum)) { return "enum"; } if (schema.type === "array") { return `array of ${schemaType(schema.items)}`; } if (schema.type) { return Array.isArray(schema.type) ? schema.type.map(inlineCode).join(" or ") : inlineCode(schema.type); } if (schema.anyOf) { return "one of several forms"; } if (schema.oneOf) { return "one of several forms"; } if (schema.allOf) { return "combination"; } if (schema.properties) { return inlineCode("object"); } return "unspecified"; } function describeConstraints(schema) { const rows = []; if (schema.pattern) rows.push(["Pattern", inlineCode(schema.pattern)]); if (schema.minimum !== undefined) rows.push(["Minimum", inlineCode(schema.minimum)]); if (schema.maximum !== undefined) rows.push(["Maximum", inlineCode(schema.maximum)]); if (schema.exclusiveMinimum !== undefined) rows.push(["Exclusive minimum", inlineCode(schema.exclusiveMinimum)]); if (schema.exclusiveMaximum !== undefined) rows.push(["Exclusive maximum", inlineCode(schema.exclusiveMaximum)]); if (schema.minLength !== undefined) rows.push(["Minimum length", inlineCode(schema.minLength)]); if (schema.maxLength !== undefined) rows.push(["Maximum length", inlineCode(schema.maxLength)]); if (schema.minItems !== undefined) rows.push(["Minimum items", inlineCode(schema.minItems)]); if (schema.maxItems !== undefined) rows.push(["Maximum items", inlineCode(schema.maxItems)]); return rows; } function heading(level, text) { return `${"#".repeat(Math.min(level, 6))} ${text}`; } function variantLabel(schema, index) { const tag = schema?.properties?.type?.const; if (tag !== undefined) { return `${inlineCode(`type = "${tag}"`)}`; } if (schema?.const !== undefined) { return inlineCode(schema.const); } if (schema?.$ref) { return linkRef(schema.$ref); } if (schema?.type === "array") { return "array"; } if (schema?.type) { return Array.isArray(schema.type) ? schema.type.join(" or ") : schema.type; } return `form ${index + 1}`; } function renderSchema(schema, level = 2, seen = new Set()) { if (!schema || typeof schema !== "object") { return ""; } const out = []; const description = trimDescription(schema.description); if (description) { out.push(description, ""); } const constraints = describeConstraints(schema); if (schema.$ref || schema.type || schema.const !== undefined || constraints.length || Array.isArray(schema.enum)) { out.push('
'); out.push(`
Type: ${schemaType(schema)}
`); for (const [label, value] of constraints) { out.push(`
${label}: ${value}
`); } out.push("
", ""); } if (Array.isArray(schema.enum)) { out.push(heading(level, "Allowed Values"), ""); for (const value of schema.enum) { out.push(`- ${inlineCode(value)}`); } out.push(""); } const union = schema.anyOf ?? schema.oneOf; if (Array.isArray(union)) { out.push(heading(level, "Accepted Forms"), ""); union.forEach((variant, index) => { out.push(heading(level + 1, variantLabel(variant, index)), ""); out.push(renderSchema(variant, level + 2, seen)); }); } if (Array.isArray(schema.allOf)) { out.push(heading(level, "Combined Schemas"), ""); schema.allOf.forEach((variant, index) => { out.push(heading(level + 1, `Part ${index + 1}`), ""); out.push(renderSchema(variant, level + 2, seen)); }); } if (schema.type === "array" && schema.items) { out.push(heading(level, "Items"), ""); if (schema.items.$ref) { out.push(`Each item should be ${linkRef(schema.items.$ref)}.`, ""); } else { out.push(renderSchema(schema.items, level + 1, seen)); } } if (schema.properties && !seen.has(schema)) { seen.add(schema); const required = new Set(schema.required ?? []); const entries = Object.entries(schema.properties).filter(([name]) => name !== "type" || schema.properties.type.const === undefined); if (entries.length) { out.push(heading(level, "Fields"), ""); for (const [name, property] of entries) { out.push('
'); out.push(`

${inlineCode(name)} ${required.has(name) ? "(required)" : "(optional)"}

`); out.push(`

Type: ${schemaType(property)}

`); const propertyDescription = trimDescription(property.description); if (propertyDescription) { out.push("", propertyDescription); } const propertyConstraints = describeConstraints(property); if (propertyConstraints.length) { out.push("", "| Constraint | Value |", "| --- | --- |"); for (const [label, value] of propertyConstraints) { out.push(`| ${escapePipe(label)} | ${escapePipe(value)} |`); } } if (property.items?.$ref) { out.push("", `Each item should be ${linkRef(property.items.$ref)}.`); } if ((property.anyOf || property.oneOf || property.properties) && !property.$ref) { out.push("", renderSchema(property, level + 1, seen)); } out.push("
", ""); } } } return out.join("\n").replace(/\n{4,}/g, "\n\n\n"); } function tryGit(args) { try { return execFileSync("git", ["-C", wryRoot, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch { return ""; } } function buildInfo() { const commit = tryGit(["rev-parse", "--short=12", "HEAD"]); const branch = tryGit(["branch", "--show-current"]); return { sourcePath: wryRoot, sourceCommit: commit || null, sourceBranch: branch || null, generatedAt: new Date().toISOString() }; } function sourceSummary(info) { const rows = []; if (info.sourceBranch) rows.push(`- Source branch: \`${info.sourceBranch}\``); if (info.sourceCommit) rows.push(`- Source commit: \`${info.sourceCommit}\``); rows.push(`- Generated at: \`${info.generatedAt}\``); return rows.join("\n"); } async function main() { const rawSchema = await readFile(schemaPath, "utf8"); const schema = JSON.parse(rawSchema); const defs = schema.$defs ?? {}; const typeEntries = Object.keys(defs).sort((a, b) => a.localeCompare(b)); const info = buildInfo(); await rm(typesDir, { recursive: true, force: true }); await mkdir(typesDir, { recursive: true }); await mkdir(publicSpecDir, { recursive: true }); for (const name of copiedSpecs) { await copyFile(path.join(specDir, name), path.join(publicSpecDir, name)); } await writeFile(path.join(publicSpecDir, "build-info.json"), `${JSON.stringify(info, null, 2)}\n`); const index = `${frontmatter({ title: "Wry Config Docs", description: "Generated documentation for the Wry TOML configuration format." })} # Wry Config Docs This site is generated from Wry's TOML configuration specs. ${sourceSummary(info)} ## Reference - [Top-Level Config](/reference/config/) starts at the schema root. - [Configuration Types](/reference/types/) lists every generated type. - [Raw Generated Specs](/reference/raw-specs/) exposes the source artifacts copied from Wry. `; await writeFile(path.join(docsDir, "index.md"), index); const configSchema = defs.Config; if (!configSchema) { throw new Error("spec.generated.json does not contain $defs.Config"); } const configPage = `${frontmatter({ title: "Top-Level Config", description: "The root Wry TOML configuration schema." })} # Top-Level Config This page is generated from \`$defs.Config\` in \`spec.generated.json\`. ${renderSchema(configSchema, 2)} `; await writeFile(path.join(referenceDir, "config.md"), configPage); const rawPage = `${frontmatter({ title: "Raw Generated Specs", description: "Downloadable generated Wry configuration spec artifacts." })} # Raw Generated Specs These files are copied directly from Wry during \`pnpm run sync:specs\`. `; await writeFile(path.join(referenceDir, "raw-specs.md"), rawPage); const typeIndex = `${frontmatter({ title: "Configuration Types", description: "Alphabetized generated type reference." })} # Configuration Types This index is generated from the \`$defs\` section of \`spec.generated.json\`. `; await writeFile(path.join(typesDir, "index.md"), typeIndex); for (const name of typeEntries) { const type = defs[name]; const page = `${frontmatter({ title: name, description: trimDescription(type.description).split("\n")[0] || `Generated schema reference for ${name}.` })} # \`${name}\` Generated from \`$defs.${name}\` in \`spec.generated.json\`. ${renderSchema(type, 2)} `; await writeFile(path.join(typesDir, `${slugify(name)}.md`), page); } console.log(`Generated ${typeEntries.length} type pages from ${schemaPath}`); } main().catch((error) => { console.error(error); process.exit(1); });