Files
opencode/services/site/script/docs-file-icons.ts

75 lines
3.6 KiB
TypeScript

// Writes src/component/site/docs-file-icons.ts: the file-type icons the docs use, cut from the
// desktop app's sprite (@opencode/ui), so the site bundles a dozen icons instead of 1,089.
// The names come from the shared mapping (chooseIconName) applied to every code block title and
// every file and folder in a file tree ("├── name" lines) in the docs content, and the filenames on
// /dev/docs-components, plus each icon's _light variant.
// Run after adding a new kind of file to the docs: bun script/docs-file-icons.ts
import { chooseIconName } from "@opencode/ui/icons/file-type-name"
import { readdirSync } from "node:fs"
import path from "node:path"
const app = path.resolve(import.meta.dir, "..")
const content = path.resolve(app, "src/docs/content")
const sprite = await Bun.file(path.resolve(app, "../../packages/ui/src/components/file-icons/sprite.svg")).text()
const walk = (dir: string): string[] =>
readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
entry.isDirectory()
? walk(path.join(dir, entry.name))
: entry.name.endsWith(".mdx")
? [path.join(dir, entry.name)]
: [],
)
const sources = await Promise.all(walk(content).map((file) => Bun.file(file).text()))
const titles = sources.flatMap((text) =>
[...text.matchAll(/```\w*\s+title="([^"]+)"/g)].map((match) => match[1].replace(/ \(excerpt\)$/, "")),
)
// File trees: the name after the "├── " prefix, without the note after it (two spaces or "←").
const tree = sources
.flatMap((text) => [...text.matchAll(/```text\n([\s\S]*?)```/g)].map((match) => match[1]))
.filter((block) => /[├└]──/.test(block))
.flatMap((block) => block.split("\n"))
.map((line) => line.replace(/^[│├└─\s]+/, "").replace(/(\s{2,}|\s+←).*$/, ""))
.filter(Boolean)
.flatMap((name) =>
name
.replace(/\/$/, "")
.split("/")
.map((part, index, parts) => ({
name: part,
directory: index < parts.length - 1 || name.endsWith("/"),
})),
)
// Filenames and folders shown on the site that aren't titles in the docs content.
const site = [".env", ".github/workflows/opencode.yml", "mise.toml", "install.sh", "count-lines.ts", "release.md"]
const folders = [".opencode", "agents", "commands", "plugins", "tools", ".github", "workflows"]
const names = new Set([
...[...titles, ...site].map((name) => chooseIconName(name, "file", false)),
...folders.map((name) => chooseIconName(name, "directory", false)),
...tree.map((entry) => chooseIconName(entry.name, entry.directory ? "directory" : "file", false)),
// The fallbacks.
chooseIconName("unknown", "file", false),
chooseIconName("unknown", "directory", false),
])
const symbol = (name: string) => sprite.match(new RegExp(`<symbol([^>]*)id="${name}"([^>]*)>([\\s\\S]*?)</symbol>`))
const icons = [...names, ...[...names].map((name) => `${name}_light`)].flatMap((name) => {
const match = symbol(name)
if (!match) return []
const attrs = `${match[1]}${match[2]}`.replace(/\s+/g, " ").trim()
const body = match[3].replace(/\s+/g, " ").replace(/>\s+</g, "><").trim()
return [[name, `<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${body}</svg>`] as const]
})
await Bun.write(
path.join(app, "src/component/site/docs-file-icons.ts"),
`// Generated by script/docs-file-icons.ts from @opencode/ui's file-type sprite. Do not edit.
import type { IconName } from "@opencode/ui/icons/file-type"
export const docsFileIcons: Partial<Record<IconName, string>> = {
${icons.map(([name, svg]) => ` ${name}: ${JSON.stringify(svg)},`).join("\n")}
}
`,
)
console.log(`${icons.length} icons: ${icons.map((icon) => icon[0]).join(", ")}`)