| 1 | import * as NodeChildProcess from "node:child_process"; |
| 2 | import * as NodeFS from "node:fs"; |
| 3 | import * as NodePath from "node:path"; |
| 4 | import * as NodeURL from "node:url"; |
| 5 | |
| 6 | import { getBuiltInSpriteSheet } from "@pierre/trees"; |
| 7 | |
| 8 | const scriptDirectory = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); |
| 9 | const appDirectory = NodePath.resolve(scriptDirectory, ".."); |
| 10 | const repositoryRoot = NodePath.resolve(appDirectory, "../.."); |
| 11 | const outputDirectory = NodePath.join(appDirectory, "assets/file-icons"); |
| 12 | const generatedModulePath = NodePath.join(appDirectory, "src/lib/markdownFileIcons.generated.ts"); |
| 13 | const webIconSource = NodeFS.readFileSync( |
| 14 | NodePath.join(repositoryRoot, "apps/web/src/pierre-icons.ts"), |
| 15 | "utf8", |
| 16 | ); |
| 17 | const customSprite = webIconSource.match(/const T3_FILE_ICON_SPRITE = `([\s\S]*?)`;/)?.[1]; |
| 18 | |
| 19 | if (!customSprite) { |
| 20 | throw new Error("Could not read the T3 Pierre icon sprite from apps/web/src/pierre-icons.ts"); |
| 21 | } |
| 22 | |
| 23 | const colors = { |
| 24 | astro: "#a631be", |
| 25 | babel: "#d5a910", |
| 26 | bash: "#199f43", |
| 27 | biome: "#1a85d4", |
| 28 | bootstrap: "#693acf", |
| 29 | browserslist: "#d5a910", |
| 30 | bun: "#594c5b", |
| 31 | c: "#1a85d4", |
| 32 | claude: "#d47628", |
| 33 | cpp: "#1a85d4", |
| 34 | css: "#693acf", |
| 35 | database: "#a631be", |
| 36 | default: "#84848a", |
| 37 | docker: "#1a85d4", |
| 38 | eslint: "#693acf", |
| 39 | font: "#84848a", |
| 40 | git: "#ff8c5b", |
| 41 | go: "#1ca1c7", |
| 42 | graphql: "#d32a61", |
| 43 | html: "#d47628", |
| 44 | image: "#d32a61", |
| 45 | javascript: "#d5a910", |
| 46 | json: "#d47628", |
| 47 | markdown: "#199f43", |
| 48 | mcp: "#17a5af", |
| 49 | nextjs: "#84848a", |
| 50 | npm: "#d52c36", |
| 51 | oxc: "#1ca1c7", |
| 52 | postcss: "#d52c36", |
| 53 | prettier: "#17a5af", |
| 54 | python: "#1a85d4", |
| 55 | react: "#1ca1c7", |
| 56 | ruby: "#d52c36", |
| 57 | rust: "#d47628", |
| 58 | sass: "#d32a61", |
| 59 | stylelint: "#84848a", |
| 60 | svelte: "#d52c36", |
| 61 | svg: "#d47628", |
| 62 | svgo: "#199f43", |
| 63 | swift: "#d47628", |
| 64 | table: "#17a5af", |
| 65 | tailwind: "#1ca1c7", |
| 66 | terraform: "#693acf", |
| 67 | text: "#84848a", |
| 68 | typescript: "#1a85d4", |
| 69 | video: "#a631be", |
| 70 | vite: "#a631be", |
| 71 | vscode: "#1a85d4", |
| 72 | vue: "#199f43", |
| 73 | wasm: "#693acf", |
| 74 | webpack: "#1a85d4", |
| 75 | yml: "#d52c36", |
| 76 | zig: "#d47628", |
| 77 | zip: "#d47628", |
| 78 | }; |
| 79 | |
| 80 | const customIcons = { |
| 81 | agents: "t3-file-icon-agents", |
| 82 | pnpm: "t3-file-icon-pnpm", |
| 83 | video: "t3-file-icon-video", |
| 84 | }; |
| 85 | |
| 86 | function symbolFromSprite(sprite, id) { |
| 87 | const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| 88 | const match = sprite.match( |
| 89 | new RegExp(`<symbol id="${escapedId}"([^>]*)>([\\s\\S]*?)<\\/symbol>`), |
| 90 | ); |
| 91 | if (!match) throw new Error(`Missing Pierre icon symbol: ${id}`); |
| 92 | return { |
| 93 | body: match[2], |
| 94 | viewBox: match[1].match(/viewBox="([^"]+)"/)?.[1] ?? "0 0 16 16", |
| 95 | }; |
| 96 | } |
| 97 | |
| 98 | function renderIcon(token, symbol, color) { |
| 99 | const svgPath = NodePath.join(outputDirectory, `.pierre-${token}.svg`); |
| 100 | const pngPath = NodePath.join(outputDirectory, `pierre_${token}.png`); |
| 101 | NodeFS.writeFileSync( |
| 102 | svgPath, |
| 103 | `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="${symbol.viewBox}" style="color:${color}">${symbol.body}</svg>`, |
| 104 | ); |
| 105 | NodeChildProcess.execFileSync("sips", ["-s", "format", "png", svgPath, "--out", pngPath], { |
| 106 | stdio: "ignore", |
| 107 | }); |
| 108 | NodeFS.rmSync(svgPath); |
| 109 | } |
| 110 | |
| 111 | NodeFS.rmSync(outputDirectory, { recursive: true, force: true }); |
| 112 | NodeFS.mkdirSync(outputDirectory, { recursive: true }); |
| 113 | |
| 114 | const builtInSprite = getBuiltInSpriteSheet("complete"); |
| 115 | const builtInTokens = [...builtInSprite.matchAll(/<symbol id="file-tree-builtin-([^"]+)"/g)] |
| 116 | .map((match) => match[1]) |
| 117 | .sort(); |
| 118 | |
| 119 | for (const token of builtInTokens) { |
| 120 | renderIcon( |
| 121 | token, |
| 122 | symbolFromSprite(builtInSprite, `file-tree-builtin-${token}`), |
| 123 | colors[token] ?? colors.default, |
| 124 | ); |
| 125 | } |
| 126 | for (const [token, symbolId] of Object.entries(customIcons)) { |
| 127 | renderIcon(token, symbolFromSprite(customSprite, symbolId), colors[token] ?? colors.default); |
| 128 | } |
| 129 | |
| 130 | const tokens = [...new Set([...builtInTokens, ...Object.keys(customIcons)])].sort(); |
| 131 | const generatedSource = `import type { ImageSourcePropType } from "react-native";\n\nexport const MARKDOWN_FILE_ICON_SOURCES = {\n${tokens |
| 132 | .map((token) => ` ${token}: require("../../assets/file-icons/pierre_${token}.png"),`) |
| 133 | .join("\n")}\n} as const satisfies Readonly<Record<string, ImageSourcePropType>>;\n`; |
| 134 | NodeFS.writeFileSync(generatedModulePath, generatedSource); |