Initial Commit
This commit is contained in:
commit
0534f88f70
14 changed files with 5590 additions and 0 deletions
144
scripts/pack-release.mjs
Normal file
144
scripts/pack-release.mjs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { createReadStream, createWriteStream } from "node:fs";
|
||||
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import * as tar from "tar";
|
||||
import { ZipFile } from "yazl";
|
||||
|
||||
const root = process.cwd();
|
||||
const distDir = path.join(root, "dist");
|
||||
const releaseDir = path.join(distDir, "release");
|
||||
|
||||
function releaseTag() {
|
||||
const value = process.env.TAG ?? process.env.FORGEJO_REF_NAME ?? process.env.GITHUB_REF_NAME ?? "";
|
||||
return value.replace(/^refs\/tags\//, "") || "dev";
|
||||
}
|
||||
|
||||
function cleanTag(tag) {
|
||||
return tag.replace(/[^A-Za-z0-9._-]+/g, "-");
|
||||
}
|
||||
|
||||
async function ensureBuilt() {
|
||||
const index = path.join(distDir, "index.html");
|
||||
try {
|
||||
const info = await stat(index);
|
||||
if (!info.isFile()) throw new Error();
|
||||
} catch {
|
||||
throw new Error("dist/index.html is missing. Run `pnpm run build` before `pnpm run release:pack`.");
|
||||
}
|
||||
}
|
||||
|
||||
async function distEntries() {
|
||||
const entries = await readdir(distDir);
|
||||
return entries.filter((entry) => entry !== "release");
|
||||
}
|
||||
|
||||
async function addZipPath(zip, source, name) {
|
||||
const info = await stat(source);
|
||||
if (info.isDirectory()) {
|
||||
const children = await readdir(source);
|
||||
if (children.length === 0) {
|
||||
zip.addEmptyDirectory(name);
|
||||
return;
|
||||
}
|
||||
for (const child of children) {
|
||||
await addZipPath(zip, path.join(source, child), `${name}/${child}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
zip.addFile(source, name);
|
||||
}
|
||||
|
||||
async function writeZip(file, entries, bundleName) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const output = createWriteStream(file);
|
||||
const zip = new ZipFile();
|
||||
output.on("close", resolve);
|
||||
output.on("error", reject);
|
||||
zip.outputStream.on("error", reject);
|
||||
zip.outputStream.pipe(output);
|
||||
Promise.all(entries.map((entry) => addZipPath(zip, path.join(distDir, entry), `${bundleName}/${entry}`)))
|
||||
.then(() => zip.end())
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function sha256(file) {
|
||||
const hash = createHash("sha256");
|
||||
await new Promise((resolve, reject) => {
|
||||
createReadStream(file)
|
||||
.on("data", (chunk) => hash.update(chunk))
|
||||
.on("error", reject)
|
||||
.on("end", resolve);
|
||||
});
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function readBuildInfo() {
|
||||
try {
|
||||
return JSON.parse(await readFile(path.join(distDir, "spec", "build-info.json"), "utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await ensureBuilt();
|
||||
const tag = cleanTag(releaseTag());
|
||||
const bundleName = `wry-docs-${tag}`;
|
||||
const entries = await distEntries();
|
||||
|
||||
await rm(releaseDir, { recursive: true, force: true });
|
||||
await mkdir(releaseDir, { recursive: true });
|
||||
|
||||
const tarball = path.join(releaseDir, `${bundleName}.tar.gz`);
|
||||
const zipfile = path.join(releaseDir, `${bundleName}.zip`);
|
||||
|
||||
await tar.c(
|
||||
{
|
||||
cwd: distDir,
|
||||
file: tarball,
|
||||
gzip: true,
|
||||
portable: true,
|
||||
prefix: `${bundleName}/`
|
||||
},
|
||||
entries
|
||||
);
|
||||
await writeZip(zipfile, entries, bundleName);
|
||||
|
||||
const assets = [tarball, zipfile];
|
||||
const sums = [];
|
||||
for (const file of assets) {
|
||||
sums.push(`${await sha256(file)} ${path.basename(file)}`);
|
||||
}
|
||||
await writeFile(path.join(releaseDir, "SHA256SUMS"), `${sums.join("\n")}\n`);
|
||||
|
||||
const info = await readBuildInfo();
|
||||
const notes = [
|
||||
`# Wry Docs ${tag}`,
|
||||
"",
|
||||
"Static documentation bundle generated from Wry's TOML configuration specs.",
|
||||
"",
|
||||
"## Source",
|
||||
"",
|
||||
`- Wry branch: \`${info.sourceBranch ?? "unknown"}\``,
|
||||
`- Wry commit: \`${info.sourceCommit ?? "unknown"}\``,
|
||||
`- Generated at: \`${info.generatedAt ?? "unknown"}\``,
|
||||
"",
|
||||
"## Assets",
|
||||
"",
|
||||
`- \`${path.basename(tarball)}\``,
|
||||
`- \`${path.basename(zipfile)}\``,
|
||||
"- `SHA256SUMS`",
|
||||
""
|
||||
].join("\n");
|
||||
await writeFile(path.join(releaseDir, "RELEASE_NOTES.md"), notes);
|
||||
|
||||
console.log(`Wrote release assets to ${releaseDir}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
353
scripts/sync-specs.mjs
Normal file
353
scripts/sync-specs.mjs
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
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", "<br>");
|
||||
}
|
||||
|
||||
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('<div class="schema-meta">');
|
||||
out.push(`<div><strong>Type:</strong> ${schemaType(schema)}</div>`);
|
||||
for (const [label, value] of constraints) {
|
||||
out.push(`<div><strong>${label}:</strong> ${value}</div>`);
|
||||
}
|
||||
out.push("</div>", "");
|
||||
}
|
||||
|
||||
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('<div class="schema-field">');
|
||||
out.push(`<p><strong>${inlineCode(name)}</strong> ${required.has(name) ? "(required)" : "(optional)"}</p>`);
|
||||
out.push(`<p><strong>Type:</strong> ${schemaType(property)}</p>`);
|
||||
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("</div>", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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\`.
|
||||
|
||||
<ul class="raw-links">
|
||||
<li><a href="/spec/spec.generated.json">spec.generated.json</a><br />Machine-readable JSON Schema.</li>
|
||||
<li><a href="/spec/spec.generated.md">spec.generated.md</a><br />Generated Markdown reference from Wry.</li>
|
||||
<li><a href="/spec/spec.yaml">spec.yaml</a><br />Canonical TOML spec input.</li>
|
||||
<li><a href="/spec/build-info.json">build-info.json</a><br />Source checkout metadata for this generated build.</li>
|
||||
</ul>
|
||||
`;
|
||||
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\`.
|
||||
|
||||
<ul class="schema-list">
|
||||
${typeEntries.map((name) => ` <li><a href="/reference/types/${slugify(name)}/">${name}</a></li>`).join("\n")}
|
||||
</ul>
|
||||
`;
|
||||
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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue