Initial Commit
This commit is contained in:
commit
0534f88f70
14 changed files with 5590 additions and 0 deletions
74
.forgejo/workflows/release.yml
Normal file
74
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
name: Release Site
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
wry_ref:
|
||||
description: "Branch, tag, or commit to build from in the Wry repository"
|
||||
required: false
|
||||
default: "workspace-refactor"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout docs
|
||||
uses: https://data.forgejo.org/actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout Wry
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "${{ secrets.WRY_DEPLOY_KEY }}" > ~/.ssh/wry_deploy_key
|
||||
chmod 600 ~/.ssh/wry_deploy_key
|
||||
ssh-keyscan git.wry.land >> ~/.ssh/known_hosts
|
||||
|
||||
WRY_REF="${{ inputs.wry_ref }}"
|
||||
if [ -z "$WRY_REF" ]; then
|
||||
WRY_REF="workspace-refactor"
|
||||
fi
|
||||
|
||||
GIT_SSH_COMMAND="ssh -i ~/.ssh/wry_deploy_key -o IdentitiesOnly=yes" \
|
||||
git clone --depth 1 --branch "$WRY_REF" \
|
||||
ssh://forgejo@git.wry.land/kossLAN/wry.git _wry
|
||||
|
||||
- name: Setup Node
|
||||
uses: https://data.forgejo.org/actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build site
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
WRY_REPO_PATH="$PWD/_wry" pnpm run build
|
||||
|
||||
- name: Package release assets
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ forge.ref_name }}" pnpm run release:pack
|
||||
|
||||
- name: Upload Forgejo release
|
||||
uses: https://data.forgejo.org/actions/forgejo-release@v2.13.0
|
||||
with:
|
||||
direction: upload
|
||||
tag: ${{ forge.ref_name }}
|
||||
release-dir: dist/release
|
||||
release-notes-file: dist/release/RELEASE_NOTES.md
|
||||
override: true
|
||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Astro build and type caches
|
||||
.astro/
|
||||
dist/
|
||||
|
||||
# Local package-manager and tool caches
|
||||
.cache/
|
||||
.pnpm-store/
|
||||
|
||||
# Environment overrides
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Nix shell helpers
|
||||
.direnv/
|
||||
result
|
||||
result-*
|
||||
|
||||
# Release and sync scratch space
|
||||
_wry/
|
||||
.release-tmp/
|
||||
|
||||
# Generated by scripts/sync-specs.mjs
|
||||
public/spec/
|
||||
src/content/docs/index.md
|
||||
src/content/docs/reference/
|
||||
|
||||
# OS/editor noise
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
30
README.md
Normal file
30
README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Wry Docs
|
||||
|
||||
Static documentation site for the generated Wry TOML configuration specs.
|
||||
|
||||
Generated docs live under `src/content/docs/` and copied raw specs live under
|
||||
`public/spec/`. They are intentionally ignored and recreated by
|
||||
`pnpm run sync:specs`.
|
||||
|
||||
## Local development
|
||||
|
||||
The generator reads Wry specs from `../wry` by default. Override that with
|
||||
`WRY_REPO_PATH` when needed.
|
||||
|
||||
```shell
|
||||
nix develop
|
||||
pnpm install
|
||||
WRY_REPO_PATH=/home/koss/git/wry pnpm run dev
|
||||
```
|
||||
|
||||
## Release build
|
||||
|
||||
```shell
|
||||
WRY_REPO_PATH=/home/koss/git/wry pnpm run build
|
||||
TAG=v0.0.0-test pnpm run release:pack
|
||||
```
|
||||
|
||||
The release assets are written to `dist/release/`.
|
||||
|
||||
Forgejo Actions builds the same bundle on `v*` tags and uploads it to the
|
||||
matching Forgejo release.
|
||||
31
astro.config.mjs
Normal file
31
astro.config.mjs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
import starlight from "@astrojs/starlight";
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: "Wry Config Docs",
|
||||
description: "Generated documentation for the Wry TOML configuration format.",
|
||||
customCss: ["./src/styles/custom.css"],
|
||||
sidebar: [
|
||||
{
|
||||
label: "Start",
|
||||
items: [
|
||||
{ label: "Overview", slug: "" },
|
||||
{ label: "Raw Generated Specs", slug: "reference/raw-specs" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Reference",
|
||||
items: [
|
||||
{ label: "Top-Level Config", slug: "reference/config" },
|
||||
{
|
||||
label: "Configuration Types",
|
||||
items: [{ autogenerate: { directory: "reference/types" } }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1780243769,
|
||||
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
41
flake.nix
Normal file
41
flake.nix
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
description = "Development shell for Wry docs";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, ... }:
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.corepack
|
||||
pkgs.nodejs_24
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export COREPACK_HOME="$PWD/.cache/corepack"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
|
||||
mkdir -p "$COREPACK_HOME" "$PWD/.cache/corepack-bin"
|
||||
corepack enable --install-directory "$PWD/.cache/corepack-bin" >/dev/null
|
||||
corepack install >/dev/null
|
||||
|
||||
export PATH="$PWD/.cache/corepack-bin:$PATH"
|
||||
'';
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "wry-docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.5.2",
|
||||
"scripts": {
|
||||
"dev": "pnpm run sync:specs && astro dev",
|
||||
"sync:specs": "node scripts/sync-specs.mjs",
|
||||
"build": "pnpm run sync:specs && astro build",
|
||||
"check": "pnpm run sync:specs && astro check",
|
||||
"release:pack": "node scripts/pack-release.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.39.3",
|
||||
"astro": "^6.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.9",
|
||||
"tar": "^7.5.16",
|
||||
"typescript": "^6.0.3",
|
||||
"yazl": "^3.3.1"
|
||||
}
|
||||
}
|
||||
4761
pnpm-lock.yaml
generated
Normal file
4761
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
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);
|
||||
});
|
||||
10
src/content.config.ts
Normal file
10
src/content.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineCollection } from "astro:content";
|
||||
import { docsLoader } from "@astrojs/starlight/loaders";
|
||||
import { docsSchema } from "@astrojs/starlight/schema";
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({
|
||||
loader: docsLoader(),
|
||||
schema: docsSchema()
|
||||
})
|
||||
};
|
||||
44
src/styles/custom.css
Normal file
44
src/styles/custom.css
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
:root {
|
||||
--sl-content-width: 56rem;
|
||||
}
|
||||
|
||||
.schema-meta {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
margin: 1rem 0;
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--sl-color-gray-5);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--sl-color-gray-6);
|
||||
}
|
||||
|
||||
.schema-field {
|
||||
margin: 1.25rem 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 3px solid var(--sl-color-gray-5);
|
||||
}
|
||||
|
||||
.schema-field > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.schema-list {
|
||||
columns: 2 18rem;
|
||||
}
|
||||
|
||||
.schema-list li {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.raw-links {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.raw-links li {
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--sl-color-gray-5);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue