From 5f8793a56ae790df60469a8c2bb5ac020d127bd7 Mon Sep 17 00:00:00 2001 From: Dov Benyomin Sohacheski Date: Thu, 2 Jul 2026 04:36:17 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Render=20the=20command=20reference?= =?UTF-8?q?=20with=20version=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generate the ws-cli command reference as a markdown partial directly, instead of emitting tags backed by a data loader. Each command renders its heading, description, usage, and options table inline, with a version badge in the title — a tip "v" or, once a command is deprecated, a warning "deprecated v". Drops CommandSection.vue and commands.data.ts. --- .vitepress/data/commands.data.ts | 65 ------------------- .../theme/components/CommandSection.vue | 56 ---------------- .vitepress/theme/index.ts | 2 - scripts/generate-commands.mjs | 64 +++++++++++++++++- 4 files changed, 63 insertions(+), 124 deletions(-) delete mode 100644 .vitepress/data/commands.data.ts delete mode 100644 .vitepress/theme/components/CommandSection.vue diff --git a/.vitepress/data/commands.data.ts b/.vitepress/data/commands.data.ts deleted file mode 100644 index 1c408b1..0000000 --- a/.vitepress/data/commands.data.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { createMarkdownRenderer, defineLoader } from 'vitepress' -import type { MarkdownRenderer } from 'vitepress' -import fs from 'node:fs' -import { resolve } from 'node:path' -import { load } from 'js-yaml' - -export interface CommandOption { - name: string - shorthand?: string - default?: string - usage?: string -} - -export interface Command { - name: string - synopsis?: string - description?: string - usage?: string - aliases?: string[] - example?: string - options?: CommandOption[] - commands?: Command[] - md: { - description?: string - } -} - -declare const data: Record -export { data } - -const file = resolve(__dirname, './commands.yaml') - -function flatten( - cmd: Command, - md: MarkdownRenderer, - into: Record -): void { - into[cmd.name] = { - ...cmd, - md: { - description: cmd.description ? md.renderInline(cmd.description) : undefined - } - } - - for (const child of cmd.commands ?? []) { - flatten(child, md, into) - } -} - -export default defineLoader({ - watch: [file], - async load(): Promise { - const md = await createMarkdownRenderer('.') - - const root = load(fs.readFileSync(file, 'utf8')) as Command - - const commands: Record = {} - - for (const group of root.commands ?? []) { - flatten(group, md, commands) - } - - return commands - } -}) diff --git a/.vitepress/theme/components/CommandSection.vue b/.vitepress/theme/components/CommandSection.vue deleted file mode 100644 index 477c405..0000000 --- a/.vitepress/theme/components/CommandSection.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index b14becb..0d8fd12 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,7 +1,6 @@ import { h, Fragment } from 'vue' import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme-without-fonts' -import CommandSection from './components/CommandSection.vue' import DockerIcon from './components/DockerIcon.vue' import EnvVar from './components/EnvVar.vue' import EnvVarSection from './components/EnvVarSection.vue' @@ -32,7 +31,6 @@ export default { }) }, enhanceApp({ app, router, siteData }) { - app.component('CommandSection', CommandSection) app.component('EnvVar', EnvVar) app.component('EnvVarSection', EnvVarSection) } diff --git a/scripts/generate-commands.mjs b/scripts/generate-commands.mjs index 87f130c..5ec2b5b 100644 --- a/scripts/generate-commands.mjs +++ b/scripts/generate-commands.mjs @@ -6,10 +6,72 @@ const root = load( fs.readFileSync(resolve('.vitepress/data/commands.yaml'), 'utf8') ) +const escape = value => + (value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + +const cell = value => escape(value).replaceAll('|', '\\|') + +const flag = ({ name, shorthand }) => + shorthand ? `--${name}, -${shorthand}` : `--${name}` + +const blockquote = description => + escape(description) + .split('\n') + .map(line => (line === '' ? '>' : `> ${line}`)) + .join('\n') + +const optionsTable = options => { + const rows = ['| Flag | Description | Default |', '| --- | --- | --- |'] + + for (const option of options) { + const value = option.default ? `\`${option.default}\`` : '' + + rows.push(`| \`${flag(option)}\` | ${cell(option.usage)} | ${value} |`) + } + + return rows.join('\n') +} + +const section = command => { + const badges = [] + + if (command.aliases?.length) { + badges.push(``) + } + + if (command.since) { + badges.push(``) + } + + if (command.deprecated) { + badges.push(``) + } + + const title = [`\`${command.name}\``, ...badges].join(' ') + const lines = [`### ${title}`, ''] + + if (command.description) { + lines.push(blockquote(command.description), '') + } + + if (command.usage) { + lines.push(`\`${command.usage}\``, '') + } + + if (command.options?.length) { + lines.push(optionsTable(command.options), '') + } + + return lines.join('\n').trimEnd() +} + const sections = [] const walk = command => { - sections.push(``, '---', '') + sections.push(section(command), '', '---', '') for (const child of command.commands ?? []) { walk(child)