// 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(`]*)id="${name}"([^>]*)>([\\s\\S]*?)`)) 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+<").trim() return [[name, `${body}`] 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> = { ${icons.map(([name, svg]) => ` ${name}: ${JSON.stringify(svg)},`).join("\n")} } `, ) console.log(`${icons.length} icons: ${icons.map((icon) => icon[0]).join(", ")}`)