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/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/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 new file mode 100644 index 0000000..d51fbe9 --- /dev/null +++ b/src/lib/jsonschemaToMarkdown.ts @@ -0,0 +1,271 @@ +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 = " with valid values:\n\n"; + + const enumTypes = schema.anyOf as Array>; + for (const enumType of enumTypes) { + const typeDocs = jsonschemaToMarkdown(enumType, { + level: level < 4 ? 4 : level + 1, + root, + includeDescription: false, + }); + + validDocs += typeDocs + "\n"; + } + + return buildString({ + levelStr, + prefix, + includeType, + type, + required, + includeDescription, + 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, + 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.enum !== undefined) { + return { type: "staticEnum", required, schema }; + } else 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.$ref !== undefined) { + return { type: "external", required, schema }; + } 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 { + 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 f9760e9..b05bb8f 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,20 @@ 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, { + includeDescription: false, + includeType: false, + useLevel: false, + excludedProps: ["type", "no-cache", "env", "secrets"], + }); - return out; + return ( + "## Configuration options\n\n" + + (docs.trim() !== "" ? docs : "_No options..._") + ); } 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); + }, + }, + }; +} 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; }