From a421256ca60859d49166514f8c2fe18ceeed56b3 Mon Sep 17 00:00:00 2001 From: xyny Date: Mon, 5 Jan 2026 22:04:54 +0200 Subject: [PATCH 1/4] refactor: better jsonschema docs generation --- package.json | 1 + pnpm-lock.yaml | 8 ++ src/lib/jsonschemaToMarkdown.ts | 197 +++++++++++++++++++++++++++ src/plugins/moduleReferencePlugin.ts | 115 +++------------- 4 files changed, 222 insertions(+), 99 deletions(-) create mode 100644 src/lib/jsonschemaToMarkdown.ts diff --git a/package.json b/package.json index 946b2f3..eb649d7 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-mdx": "^3.4.1", "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.6.0", + "json-schema-typed": "^8.0.2", "markdown-it": "^14.1.0", "prettier": "3.2.5", "prettier-plugin-astro": "^0.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48cc949..7bcebd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: eslint-plugin-promise: specifier: ^6.6.0 version: 6.6.0(eslint@8.57.1) + json-schema-typed: + specifier: ^8.0.2 + version: 8.0.2 markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -2429,6 +2432,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -6833,6 +6839,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: diff --git a/src/lib/jsonschemaToMarkdown.ts b/src/lib/jsonschemaToMarkdown.ts new file mode 100644 index 0000000..2cea742 --- /dev/null +++ b/src/lib/jsonschemaToMarkdown.ts @@ -0,0 +1,197 @@ +import { type JSONSchema } from "json-schema-typed"; + +export function jsonschemaToMarkdown( + inSchema: Exclude, + options: { + level?: number; + levels?: string[]; + prefix?: string; + includeDescription?: boolean; + includeType?: boolean; + useLevel?: boolean; + root?: Exclude; + excludedProps?: string[] | undefined; + }, +): string { + const { + level = 0, + levels = [ + "##", + "###", + "####", + "#####", + "", + "-", + "\t-", + "\t\t-", + "\t\t\t-", + "\t\t\t\t-", + ], + prefix = "", + includeDescription = true, + includeType = true, + useLevel = true, + root = inSchema, + excludedProps = [], + } = options; + + const { type, required, schema } = getType(root, inSchema, prefix); + const levelStr = useLevel ? levels[level] : ""; + + if ( + type === "boolean" || + type === "number" || + type === "string" || + type === "integer" + ) { + if (schema.const !== undefined) { + return buildString({ + levelStr, + prefix, + includeType, + type: schema.const, + required, + includeDescription, + desc: inSchema.description ?? "", + }); + } + return buildString({ + levelStr, + prefix, + includeType, + type, + required, + includeDescription, + desc: inSchema.description ?? "", + }); + } else if (type === "array") { + const itemsDocs = jsonschemaToMarkdown( + schema.items as Exclude, + { + level, + useLevel: false, + root, + }, + ); + + return buildString({ + levelStr, + prefix, + includeType, + type, + required, + includeDescription, + desc: inSchema.description ?? "", + extraDocs: itemsDocs, + }); + } else if (type === "object") { + const props = schema.properties; + let propDocs = "\n"; + + for (const prop in props) { + if (excludedProps.includes(prop)) continue; + + const propSchema = props[prop] as Exclude; + const typeDocs = jsonschemaToMarkdown(propSchema, { + level: level + 1, + prefix: prop, + root, + }); + + propDocs += typeDocs + "\n"; + } + + return buildString({ + levelStr, + prefix, + includeType, + type, + required, + includeDescription, + desc: inSchema.description ?? "", + extraDocs: propDocs, + }); + } else if (type === "enum") { + let validDocs = "\n" + levels[level + 1] + " Valid values\n"; + + const enumTypes = schema.anyOf as Array>; + for (const enumType of enumTypes) { + const typeDocs = jsonschemaToMarkdown(enumType, { + level: level + 2, + root, + includeDescription: false, + }); + + validDocs += typeDocs + "\n"; + } + + return buildString({ + levelStr, + prefix, + includeType, + type, + required, + includeDescription, + desc: inSchema.description ?? "", + extraDocs: validDocs, + }); + } else { + return buildString({ + levelStr, + prefix, + includeType, + type, + required, + includeDescription, + desc: inSchema.description ?? "", + }); + } +} + +function getType( + root: Exclude, + schema: Exclude, + propName: string, +): { type: string; required: boolean; schema: Exclude } { + const required = (root.required ?? []).includes(propName); + + if ( + schema.type === "boolean" || + schema.type === "number" || + schema.type === "string" || + schema.type === "integer" || + schema.type === "array" || + schema.type === "object" + ) { + return { type: schema.type, required, schema }; + } else if (schema.$ref !== undefined && schema.$ref.startsWith("#/$defs/")) { + const defName = schema.$ref.split("#/$defs/")[1]; + const defSchema = root.$defs?.[defName] as Exclude; + if (defSchema !== undefined) { + return getType(root, defSchema, defName); + } + return { type: "unknown", required, schema: defSchema }; + } else if (schema.anyOf !== undefined) { + return { type: "enum", required, schema }; + } else { + return { type: "unknown", required, schema }; + } +} + +function buildString(options: { + levelStr: string; + prefix: string; + includeType: boolean; + type: string; + required: boolean; + includeDescription: boolean; + desc: string; + extraDocs?: string; +}): string { + return `${options.levelStr} \ +${options.prefix}${options.prefix !== "" && options.includeType ? "," : ""} \ +${options.required ? "required" : ""} \ +${options.includeType ? `\`${options.type}\`` : ""}${options.type !== "array" && options.includeDescription ? `\n${options.desc ?? (options.levelStr.includes("#") ? "_No description provided..._" : "")}\n` : ""} \ +${options.type === "array" ? "of" : ""} ${options.extraDocs ?? ""} +${options.type === "array" ? options.desc : ""} \n`; +} diff --git a/src/plugins/moduleReferencePlugin.ts b/src/plugins/moduleReferencePlugin.ts index f9760e9..263a1e5 100644 --- a/src/plugins/moduleReferencePlugin.ts +++ b/src/plugins/moduleReferencePlugin.ts @@ -5,6 +5,8 @@ import type { StarlightPlugin } from "@astrojs/starlight/types"; import path from "node:path"; import * as fs from "fs"; import type { Module } from "./modulesJsonGeneratorPlugin"; +import { jsonschemaToMarkdown } from "../lib/jsonschemaToMarkdown"; +import type { JSONSchema } from "json-schema-typed"; export default function moduleReferencePlugin(): StarlightPlugin { return { @@ -89,10 +91,7 @@ async function generateReferencePage( module.name + "@" + version.version, module.shortdesc ?? "", version.examples ?? [], - schema as { - properties: object; - required: string[]; - }, + schema as object, readme, rawUrlToEditUrl(version.readme ?? ""), { @@ -108,7 +107,7 @@ async function generateReferencePage( module.name, module.shortdesc ?? "", version.examples ?? [], - schema as { properties: object; required: string[] }, + schema as object, `:::note This documentation page is for the latest version (${version.version}) of this module. All available versions: ${module.versions.map((v) => `[${v.version}](${v.version})`).join(", ")}. @@ -153,9 +152,7 @@ function writeReferencePage( sidebar: Record, outputFile: string, ): void { - const optionReference = generateOptionReference( - schema as { properties: object; required: string[]; $defs: object }, - ); + const optionReference = generateOptionReference(schema); const content = `\ --- @@ -169,7 +166,7 @@ ${readme.replace(/^#{1}\s.*$/gm, "")} \`\`\`yaml ${examples[0]} \`\`\` -${optionReference !== "" ? `## Configuration options\n ${optionReference}` : ""} +${optionReference ?? ""} `; fs.writeFile(outputFile, content, (err) => { @@ -194,97 +191,17 @@ function rawUrlToEditUrl(url: string): string { return `${baseURL}/edit/${refAndFilePath}`; } -function generateOptionReference(schema: { - properties: object; - required: string[]; - $defs: object; -}): string { - if (schema === undefined) return ""; - - let out = ""; - - function generatePropReferences( - properties: object, - headerLevel: string, - ): string { - let out = ""; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - for (const [key, value] of Object.entries(properties)) { - if ( - key === "type" || - key === "no-cache" || - key === "env" || - key === "secrets" - ) - continue; - const object = - value.$ref !== undefined - ? schema.$defs[ - value.$ref.replace("#/$defs/", "") as keyof typeof schema.$defs - ] - : value; - const prop = object as { - type: string; - description?: string; - default?: string; - anyOf?: Array<{ type: string; const: string }>; - properties?: object; - items?: { - $ref?: string; - anyOf: Array<{ type: string; const: string }>; - }; - }; - const required = schema.required?.includes(key) ? "required" : "optional"; - const type = - prop.type ?? (object.anyOf !== undefined ? "enum" : "unknown"); - - out += `${headerLevel} \`${key}:\` (${required} ${type}) -${prop.description ?? "*No description provided...*"} - -${type === "object" ? generatePropReferences(prop.properties ?? {}, headerLevel + "#") : ""} -${type === "array" && prop.items?.anyOf !== undefined ? "Possible values: " + prop.items.anyOf?.map((v) => "`" + parseSchemaValue(v) + "`").join(", ") + "
" : ""} -${type === "enum" ? "Possible values: " + prop.anyOf?.map((v) => "`" + parseSchemaValue(v) + "`").join(", ") + "
" : ""} -${prop.default !== undefined ? `Default: \`${prop.default}\`` : ""} - \n`; - } - return out; - } - - function parseSchemaValue(v: any): string { - if (v.const !== undefined) return v.const; - - if (v.items !== undefined) { - if (v.items.$ref === "#/$defs/RecordString") return "string: string"; - if (v.items.type === "object") { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return `{ ${Object.entries(v.items.properties) - .map((p) => `${p[0]}: ${(p[1] as { type: string }).type}`) - .join(", ")} }`; - } - if (v.items.type === "string") { - return v.items.const; - } - if (v.items.$ref !== undefined) { - return parseSchemaValue( - schema.$defs[ - v.items.$ref.replace("#/$defs/", "") as keyof typeof schema.$defs - ], - ); - } - } +function generateOptionReference(schemaObj: object): string { + if (schemaObj === undefined) return ""; - if (v.$ref !== undefined) { - return parseSchemaValue( - schema.$defs[ - v.$ref.replace("#/$defs/", "") as keyof typeof schema.$defs - ], - ); - } - - return "Unknown type"; - } + const schema: JSONSchema = schemaObj; - out += generatePropReferences(schema.properties, "###"); + const docs = jsonschemaToMarkdown(schema, { + prefix: "Configuration options", + includeDescription: false, + includeType: false, + excludedProps: ["type", "no-cache", "env", "secrets"], + }); - return out; + return docs; } From 9ae902a3b5b05493193a23c885c57da2b47129d8 Mon Sep 17 00:00:00 2001 From: xyny Date: Sat, 24 Jan 2026 20:07:39 +0200 Subject: [PATCH 2/4] refactor: better string generation and hierarchy --- src/lib/jsonschemaToMarkdown.ts | 66 +++++++++++++-- src/plugins/moduleReferencePlugin.ts | 7 +- src/styles/global.css | 118 +++++++++++++++------------ 3 files changed, 126 insertions(+), 65 deletions(-) diff --git a/src/lib/jsonschemaToMarkdown.ts b/src/lib/jsonschemaToMarkdown.ts index 2cea742..bdee76a 100644 --- a/src/lib/jsonschemaToMarkdown.ts +++ b/src/lib/jsonschemaToMarkdown.ts @@ -20,7 +20,7 @@ export function jsonschemaToMarkdown( "###", "####", "#####", - "", + // "", "-", "\t-", "\t\t-", @@ -112,12 +112,12 @@ export function jsonschemaToMarkdown( extraDocs: propDocs, }); } else if (type === "enum") { - let validDocs = "\n" + levels[level + 1] + " Valid values\n"; + let validDocs = " with valid values:\n\n"; const enumTypes = schema.anyOf as Array>; for (const enumType of enumTypes) { const typeDocs = jsonschemaToMarkdown(enumType, { - level: level + 2, + level: level < 4 ? 4 : level + 1, root, includeDescription: false, }); @@ -188,10 +188,58 @@ function buildString(options: { desc: string; extraDocs?: string; }): string { - return `${options.levelStr} \ -${options.prefix}${options.prefix !== "" && options.includeType ? "," : ""} \ -${options.required ? "required" : ""} \ -${options.includeType ? `\`${options.type}\`` : ""}${options.type !== "array" && options.includeDescription ? `\n${options.desc ?? (options.levelStr.includes("#") ? "_No description provided..._" : "")}\n` : ""} \ -${options.type === "array" ? "of" : ""} ${options.extraDocs ?? ""} -${options.type === "array" ? options.desc : ""} \n`; + let result = options.levelStr + " "; + + if (options.prefix !== "") { + result += `\`${options.prefix}${options.type === "array" ? "[]:" : ":"}\``; + if (options.required) { + result += " (required)"; + } + if (options.levelStr.includes("#")) { + result += "\n\n"; + } else { + result += " "; + } + } + + if (options.includeType) { + result += `\`${options.type}\``; + } + + if (options.type === "array") { + result += " of "; + result += (options.extraDocs || "").trim(); + if (options.includeDescription) { + if (options.levelStr.includes("#")) { + result += "\n\n"; + } else { + result += " "; + } + result += options.desc || ""; + } + } else { + result += options.extraDocs || ""; + } + + if (options.type !== "array" && options.includeDescription) { + const description = + options.desc || + (options.levelStr.includes("#") ? "_No description provided..._" : ""); + + if (options.levelStr.includes("#")) { + result += "\n\n"; + } else { + result += " "; + } + result += `${description.trim()}`; + if (options.levelStr.includes("#")) { + result += "\n\n"; + } else { + result += " "; + } + } + + result += "\n\n"; + + return result; } diff --git a/src/plugins/moduleReferencePlugin.ts b/src/plugins/moduleReferencePlugin.ts index 263a1e5..b05bb8f 100644 --- a/src/plugins/moduleReferencePlugin.ts +++ b/src/plugins/moduleReferencePlugin.ts @@ -197,11 +197,14 @@ function generateOptionReference(schemaObj: object): string { const schema: JSONSchema = schemaObj; const docs = jsonschemaToMarkdown(schema, { - prefix: "Configuration options", includeDescription: false, includeType: false, + useLevel: false, excludedProps: ["type", "no-cache", "env", "secrets"], }); - return docs; + return ( + "## Configuration options\n\n" + + (docs.trim() !== "" ? docs : "_No options..._") + ); } diff --git a/src/styles/global.css b/src/styles/global.css index c9b7453..23ed9b6 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,53 +1,53 @@ @layer base, starlight, theme, components, utilities; -@import '@astrojs/starlight-tailwind'; -@import 'tailwindcss/theme.css' layer(theme); -@import 'tailwindcss/utilities.css' layer(utilities); +@import "@astrojs/starlight-tailwind"; +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/utilities.css" layer(utilities); /* Custom styles start here */ @theme { - --color-accent-50: #f0faff; - --color-accent-100: #e0f3ff; - --color-accent-200: #a8dfff; - --color-accent-300: #81cdf8; - --color-accent-400: #4babe7; - --color-accent-500: #2086d5; - --color-accent-600: #1466b8; - --color-accent-700: #124e97; - --color-accent-800: #113e78; - --color-accent-900: #163464; - --color-accent-950: #102042; + --color-accent-50: #f0faff; + --color-accent-100: #e0f3ff; + --color-accent-200: #a8dfff; + --color-accent-300: #81cdf8; + --color-accent-400: #4babe7; + --color-accent-500: #2086d5; + --color-accent-600: #1466b8; + --color-accent-700: #124e97; + --color-accent-800: #113e78; + --color-accent-900: #163464; + --color-accent-950: #102042; - --color-gray-200: #dfe2fc; - --color-gray-300: #d0d4f2; - --color-gray-400: #92a4c8; - --color-gray-600: #6773a8; - --color-gray-700: #4f5379; - --color-gray-800: #272a47; - --color-gray-900: #141629; + --color-gray-200: #dfe2fc; + --color-gray-300: #d0d4f2; + --color-gray-400: #92a4c8; + --color-gray-600: #6773a8; + --color-gray-700: #4f5379; + --color-gray-800: #272a47; + --color-gray-900: #141629; - --color-highlight-50: #fdf9ed; - --color-highlight-100: #f9eecc; - --color-highlight-200: #f3dd99; - --color-highlight-300: #ecc45d; - --color-highlight-400: #e7ae38; - --color-highlight-500: #e09020; - --color-highlight-550: #ca811c; - --color-highlight-600: #c66e19; - --color-highlight-700: #a44f19; - --color-highlight-800: #863e1a; - --color-highlight-900: #6e3419; - --color-highlight-950: #3f1a09; + --color-highlight-50: #fdf9ed; + --color-highlight-100: #f9eecc; + --color-highlight-200: #f3dd99; + --color-highlight-300: #ecc45d; + --color-highlight-400: #e7ae38; + --color-highlight-500: #e09020; + --color-highlight-550: #ca811c; + --color-highlight-600: #c66e19; + --color-highlight-700: #a44f19; + --color-highlight-800: #863e1a; + --color-highlight-900: #6e3419; + --color-highlight-950: #3f1a09; - --color-white: #fff; + --color-white: #fff; - --font-display: "Atkinson Hyperlegible", "sans-serif"; - --font-sans: "Rubik Variable", "system-ui", "sans-serif"; - --font-mono: "IBM Plex Mono", "monospace"; + --font-display: "Atkinson Hyperlegible", "sans-serif"; + --font-sans: "Rubik Variable", "system-ui", "sans-serif"; + --font-mono: "IBM Plex Mono", "monospace"; - --shadow-glow: 2px 2px 6px rgb(166, 223, 255, 0.15); - --shadow-glow-active: 5px 5px 12px rgb(166, 223, 255, 0.15); + --shadow-glow: 2px 2px 6px rgb(166, 223, 255, 0.15); + --shadow-glow-active: 5px 5px 12px rgb(166, 223, 255, 0.15); } h1, @@ -56,32 +56,42 @@ h3, h4, h5, h6 { - @apply font-display; + @apply font-display; } :root { - --sl-color-bg-nav: var(--sl-color-black); - --sl-color-bg-inline-code: var(--sl-color-gray-6); - --sl-color-bg-sidebar: var(--sl-color-bg); - --sl-color-hairline-shade: var(--sl-color-gray-6); + --sl-color-bg-nav: var(--sl-color-black); + --sl-color-bg-inline-code: var(--sl-color-gray-6); + --sl-color-bg-sidebar: var(--sl-color-bg); + --sl-color-hairline-shade: var(--sl-color-gray-6); } .sl-markdown-content code:not(:where(.not-content *)) { - color: var(--sl-color-gray-1); + color: var(--sl-color-gray-1); } .sl-markdown-content h2::before { - content: "## "; - opacity: 0.5; - font-weight: normal; + content: "## "; + opacity: 0.5; + font-weight: normal; } .sl-markdown-content h3::before { - content: "### "; - opacity: 0.5; - font-weight: normal; + content: "### "; + opacity: 0.5; + font-weight: normal; } .sl-markdown-content h4::before { - content: "#### "; - opacity: 0.5; - font-weight: normal; + content: "#### "; + opacity: 0.5; + font-weight: normal; +} +.sl-markdown-content h5::before { + content: "##### "; + opacity: 0.5; + font-weight: normal; +} +.sl-markdown-content h6::before { + content: "##### "; + opacity: 0.5; + font-weight: normal; } From 2cb7192ecea1c5db2a96d69539d5880700fedac6 Mon Sep 17 00:00:00 2001 From: xyny Date: Mon, 26 Jan 2026 20:31:22 +0200 Subject: [PATCH 3/4] feat: generate recipe docs with recipe schema --- .gitignore | 1 + astro.config.mjs | 256 +++++++++++++------------- src/content/docs/reference/recipe.mdx | 161 ---------------- src/lib/jsonschemaToMarkdown.ts | 2 + src/plugins/recipeReferencePlugin.ts | 48 +++++ 5 files changed, 183 insertions(+), 285 deletions(-) delete mode 100644 src/content/docs/reference/recipe.mdx create mode 100644 src/plugins/recipeReferencePlugin.ts diff --git a/.gitignore b/.gitignore index f8546bb..8ff0b63 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ pnpm-debug.log* public/modules.json src/content/docs/reference/modules/ src/content/docs/reference/github-action.md +src/content/docs/reference/recipe.mdx .wrangler diff --git a/astro.config.mjs b/astro.config.mjs index 9c9eccf..97d7ec0 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,143 +6,151 @@ import tailwindcss from "@tailwindcss/vite"; import moduleReferencePlugin from "./src/plugins/moduleReferencePlugin"; import githubActionReferencePlugin from "./src/plugins/githubActionReferencePlugin"; import modulesJsonGeneratorPlugin from "./src/plugins/modulesJsonGeneratorPlugin"; +import recipeReferencePlugin from "./src/plugins/recipeReferencePlugin"; // https://astro.build/config export default defineConfig({ site: "https://blue-build.org/", integrations: [ - starlight({ - title: "BlueBuild", - logo: { - replacesTitle: true, - dark: "./src/assets/logo-dark.svg", - light: "./src/assets/logo-light.svg", - alt: "BlueBuild. A minimal logo with a blue-billed duck holding a golden wrench in its beak.", - }, - editLink: { - baseUrl: "https://github.com/blue-build/website/edit/main/", - }, - social: [ - { icon: 'github', label: 'GitHub', href: 'https://github.com/blue-build/' }, + starlight({ + title: "BlueBuild", + logo: { + replacesTitle: true, + dark: "./src/assets/logo-dark.svg", + light: "./src/assets/logo-light.svg", + alt: "BlueBuild. A minimal logo with a blue-billed duck holding a golden wrench in its beak.", + }, + editLink: { + baseUrl: "https://github.com/blue-build/website/edit/main/", + }, + social: [ + { + icon: "github", + label: "GitHub", + href: "https://github.com/blue-build/", + }, + ], + sidebar: [ + { + label: "Learn", + items: [ + { + label: "Getting started", + link: "/learn/getting-started/", + }, + { + label: "Thinking like a distribution", + link: "/learn/mindset/", + }, + { + label: "Building on Universal Blue", + link: "/learn/universal-blue/", + }, + { + label: "How BlueBuild works", + link: "/learn/how/", + }, + { + label: "Troubleshooting, reporting bugs, and common issues", + link: "/learn/troubleshooting/", + }, + { + label: "Contributing", + link: "/learn/contributing/", + }, + { + label: "Scope", + link: "/learn/scope/", + }, ], - sidebar: [ - { - label: "Learn", - items: [ - { - label: "Getting started", - link: "/learn/getting-started/", - }, - { - label: "Thinking like a distribution", - link: "/learn/mindset/", - }, - { - label: "Building on Universal Blue", - link: "/learn/universal-blue/", - }, - { - label: "How BlueBuild works", - link: "/learn/how/", - }, - { - label: "Troubleshooting, reporting bugs, and common issues", - link: "/learn/troubleshooting/", - }, - { - label: "Contributing", - link: "/learn/contributing/", - }, - { - label: "Scope", - link: "/learn/scope/", - }, - ], - }, - { - label: "How-to", - autogenerate: { - directory: "how-to", - }, - }, - { - label: "Reference", - items: [ - { - label: "recipe.yml", - link: "/reference/recipe/", - }, - { - label: "blue-build/github-action", - link: "/reference/github-action/", - }, - { - label: "Module", - link: "/reference/module/", - }, - { - label: "Stages", - link: "/reference/stages/", - }, - { - label: "Modules", - autogenerate: { - directory: "reference/modules", - }, - }, - ], + }, + { + label: "How-to", + autogenerate: { + directory: "how-to", + }, + }, + { + label: "Reference", + items: [ + { + label: "recipe.yml", + link: "/reference/recipe/", + }, + { + label: "blue-build/github-action", + link: "/reference/github-action/", + }, + { + label: "Module", + link: "/reference/module/", + }, + { + label: "Stages", + link: "/reference/stages/", + }, + { + label: "Modules", + autogenerate: { + directory: "reference/modules", }, + }, ], - plugins: [ - modulesJsonGeneratorPlugin({ - moduleSources: [ - { - source: "https://api.github.com/repos/blue-build/modules/contents/modules", - }, - { - source: "https://api.github.com/repos/blue-build/cli/contents/template/templates/modules", - }, - ], - }), - moduleReferencePlugin(), - githubActionReferencePlugin({ - source: "https://raw.githubusercontent.com/blue-build/github-action/main/action.yml", - path: "reference/github-action.md", - }), - ], - customCss: [ - "@fontsource/atkinson-hyperlegible/400.css", - "@fontsource/atkinson-hyperlegible/700.css", - "@fontsource-variable/rubik", - "@fontsource/ibm-plex-mono", - "./src/styles/global.css", + }, + ], + plugins: [ + modulesJsonGeneratorPlugin({ + moduleSources: [ + { + source: + "https://api.github.com/repos/blue-build/modules/contents/modules", + }, + { + source: + "https://api.github.com/repos/blue-build/cli/contents/template/templates/modules", + }, ], - components: { - SocialIcons: "./src/components/NavLinks.astro", - Hero: "./src/components/Hero.astro", - Footer: "./src/components/Footer.astro", - Head: "./src/components/Head.astro", - Search: "./src/components/Search.astro", - Sidebar: "./src/components/Sidebar.astro", - MarkdownContent: "./src/components/MarkdownContent.astro", + }), + moduleReferencePlugin(), + recipeReferencePlugin(), + githubActionReferencePlugin({ + source: + "https://raw.githubusercontent.com/blue-build/github-action/main/action.yml", + path: "reference/github-action.md", + }), + ], + customCss: [ + "@fontsource/atkinson-hyperlegible/400.css", + "@fontsource/atkinson-hyperlegible/700.css", + "@fontsource-variable/rubik", + "@fontsource/ibm-plex-mono", + "./src/styles/global.css", + ], + components: { + SocialIcons: "./src/components/NavLinks.astro", + Hero: "./src/components/Hero.astro", + Footer: "./src/components/Footer.astro", + Head: "./src/components/Head.astro", + Search: "./src/components/Search.astro", + Sidebar: "./src/components/Sidebar.astro", + MarkdownContent: "./src/components/MarkdownContent.astro", + }, + head: [ + { + tag: "script", + attrs: { + defer: true, + src: "https://eu.umami.is/script.js", + "data-website-id": "fdbab42b-cab4-4f46-b06a-172700ea1e1c", }, - head: [ - { - tag: "script", - attrs: { - defer: true, - src: "https://eu.umami.is/script.js", - "data-website-id": - "fdbab42b-cab4-4f46-b06a-172700ea1e1c", - }, - }, - ], - }) - // icon(), + }, + ], + }), + // icon(), ], vite: { plugins: [tailwindcss()], }, -}); \ No newline at end of file +}); diff --git a/src/content/docs/reference/recipe.mdx b/src/content/docs/reference/recipe.mdx deleted file mode 100644 index db4b348..0000000 --- a/src/content/docs/reference/recipe.mdx +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: recipe.yml -description: A `recipe.yml` file is used to configure a custom image. ---- - -A `recipe.yml` file describes the build process of a custom image. The top-level keys set the metadata and base for the image, and modules are build steps that add things on top of the base. - -:::tip -You can add the lines below to the top of your recipe to get yaml completion in your favorite editor. -``` ---- -# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json -``` -::: - -### `name:` - -The image name. Used when publishing to GHCR as `ghcr.io//`. - -#### Example: - -```yaml -# recipes/recipe.yml -name: weird-os -``` - -### `description:` - -The image description. Published to GHCR in the image metadata. - -#### Example: - -```yaml -# recipes/recipe.yml -description: This is my personal OS image. -``` - -### `alt-tags:` (optional) - -Allows setting custom tags on the recipe's final image. Adding tags to this property will override the `latest` and timestamp tags. - -#### Example: - -```yaml -alt-tags: - - gts - - stable -``` - -### `base-image:` - -The [OCI](https://opencontainers.org/) image to base your custom image on. Only atomic Fedora images and those based on them are officially supported. Universal Blue is recommended. A list of uBlue images can be found on the [uBlue website](https://universal-blue.org/images/). BlueBuild-built images can be used as well. - -#### Example: - -```yaml -# recipes/recipe.yml -base-image: ghcr.io/ublue-os/silverblue-main -``` - -### `image-version:` - -The tag of the base image to build on. Used to select a version explicitly (`40`) or to always use the latest stable version (`latest`). A list of all available tags can be viewed by pasting your `base-image` url into your browser. - -#### Example: - -```yaml -# recipes/recipe.yml -image-version: 40 -``` - -### `blue-build-tag:` (optional) - -Version of the BlueBuild CLI to pull into your image. Supply the tag of the cli release container to pull, see [the list of available tags](https://github.com/blue-build/cli/pkgs/container/cli/versions?filters%5Bversion_type%5D=tagged) for reference. Useful for testing out pre-release versions of BlueBuild CLI. Default: `latest-installer`. Set to to `none` to opt out of installing the CLI into your image. - -### `nushell-version:` (optional) - -Version of nushell to pull to `/usr/libexec/bluebuild/nu/nu` for use by modules. Change only if you need a specific version of Nushell, changing this might break some BlueBuild modules. Set to to `none` to opt out of installing Nushell into your image (this will break modules that depend on it). - -### `platforms:` (optional) - -Specify a list of the platforms to build for your image. The resulting images will be added to a manifest list that allows your host's container runtime to pull the correct image architecture for your hardware. The process of building a multi-architecture image will end up using emulation. Consequently, image builds will take significantly longer and more space will be required on the build host since each platform that is being built is its own image. If `platforms:` is not specified, the build host's architecture will be used. - -The list of available architectures: - -- linux/amd64 -- linux/amd64/v2 -- linux/arm64 -- linux/arm -- linux/arm/v6 -- linux/arm/v7 -- linux/386 -- linux/loong64 -- linux/mips -- linux/mipsle -- linux/mips64 -- linux/mips64le -- linux/ppc64 -- linux/ppc64le -- linux/riscv64 -- linux/s390x - -#### Example: - -```yaml -platforms: - - linux/amd64 - - linux/arm64 -``` - -### `stages:` (optional) - -A list of [stages](/reference/stages/) that are executed before the build of the final image. This is useful for compiling programs from source without polluting the final bootable image. - -#### Example: - -```yaml -stages: - - name: bluebuild - from: docker.io/library/rust:1.77 - modules: # same as the top-level modules key, but executed in the custom stage - - type: script - no-cache: true - snippets: - - cargo install --locked --all-features blue-build -``` - -### `modules:` - -A list of [modules](/reference/module/) that is executed in order. Multiple of the same module can be included. - -Each item in this list should have at least a `type:` or be specified to be included from an external file in the `recipes/` directory with `from-file:`. - -#### Example: - -```yaml -# recipes/recipe.yml -modules: - - type: rpm-ostree - # rest of the module config... - - from-file: common-packages.yml -``` - -The included file can have one or multiple modules: - -```yaml -# recipes/common-packages.yml -# one module -type: rpm-ostree -# rest of the module config... -``` - -```yaml -# recipes/common-packages.yml -# multiple modules -modules: - - type: script - # rest of the module config... - - type: rpm-ostree - # rest of the module config... -``` diff --git a/src/lib/jsonschemaToMarkdown.ts b/src/lib/jsonschemaToMarkdown.ts index bdee76a..6ce33ef 100644 --- a/src/lib/jsonschemaToMarkdown.ts +++ b/src/lib/jsonschemaToMarkdown.ts @@ -171,6 +171,8 @@ function getType( return getType(root, defSchema, defName); } return { type: "unknown", required, schema: defSchema }; + } else if (schema.$ref !== undefined) { + return { type: "external", required, schema }; } else if (schema.anyOf !== undefined) { return { type: "enum", required, schema }; } else { diff --git a/src/plugins/recipeReferencePlugin.ts b/src/plugins/recipeReferencePlugin.ts new file mode 100644 index 0000000..4e56302 --- /dev/null +++ b/src/plugins/recipeReferencePlugin.ts @@ -0,0 +1,48 @@ +import type { StarlightPlugin } from "@astrojs/starlight/types"; +import * as fs from "fs"; +import { jsonschemaToMarkdown } from "../lib/jsonschemaToMarkdown"; +import type { JSONSchema } from "json-schema-typed"; + +const top = ` +--- +title: recipe.yml +description: A \`recipe.yml\` file is used to configure a custom image. +--- + +A \`recipe.yml\` file describes the build process of a custom image. The top-level keys set the metadata and base for the image, and modules are build steps that add things on top of the base. + +:::tip +You can add the lines below to the top of your recipe to get yaml completion in your favorite editor. +\`\`\` +--- +# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json +\`\`\` +::: + +## Reference +`; + +export default function recipeReferencePlugin(): StarlightPlugin { + return { + name: "recipeReferencePlugin", + hooks: { + async setup() { + const outputPath = "src/content/docs/reference/recipe.mdx"; + const schemaURL = "https://schema.blue-build.org/recipe-v1.json"; + console.log("Fetching recipe schema..."); + const schema = (await (await fetch(schemaURL)).json()) as Exclude< + JSONSchema, + boolean + >; + console.log("Recipe schema fetched."); + const markdown = jsonschemaToMarkdown(schema, { + includeDescription: false, + includeType: false, + useLevel: false, + }); + console.log("Recipe reference generated."); + await fs.promises.writeFile(outputPath, top + markdown); + }, + }, + }; +} From b3961342ff24a61ad4a99002272bea6fe9c2fd80 Mon Sep 17 00:00:00 2001 From: xyny Date: Tue, 27 Jan 2026 11:38:53 +0200 Subject: [PATCH 4/4] fix: support static enums (used by platforms[]:) --- src/lib/jsonschemaToMarkdown.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/lib/jsonschemaToMarkdown.ts b/src/lib/jsonschemaToMarkdown.ts index 6ce33ef..d51fbe9 100644 --- a/src/lib/jsonschemaToMarkdown.ts +++ b/src/lib/jsonschemaToMarkdown.ts @@ -135,6 +135,28 @@ export function jsonschemaToMarkdown( desc: inSchema.description ?? "", extraDocs: validDocs, }); + } else if (type === "staticEnum") { + let validDocs = " with valid values:\n\n"; + + const enumTypes = schema.enum ?? []; + for (const enumType of enumTypes) { + const levelStr = levels[level < 4 ? 4 : level + 1]; + + const typeDocs = levelStr + " `" + enumType + "`"; + + validDocs += typeDocs + "\n"; + } + + return buildString({ + levelStr, + prefix, + includeType, + type: "enum", + required, + includeDescription, + desc: inSchema.description ?? "", + extraDocs: validDocs, + }); } else { return buildString({ levelStr, @@ -155,7 +177,9 @@ function getType( ): { type: string; required: boolean; schema: Exclude } { const required = (root.required ?? []).includes(propName); - if ( + if (schema.enum !== undefined) { + return { type: "staticEnum", required, schema }; + } else if ( schema.type === "boolean" || schema.type === "number" || schema.type === "string" ||