1import * as NodeChildProcess from "node:child_process";
2import * as NodeFS from "node:fs";
3import * as NodePath from "node:path";
4import * as NodeURL from "node:url";
5
6import { getBuiltInSpriteSheet } from "@pierre/trees";
7
8const scriptDirectory = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
9const appDirectory = NodePath.resolve(scriptDirectory, "..");
10const repositoryRoot = NodePath.resolve(appDirectory, "../..");
11const outputDirectory = NodePath.join(appDirectory, "assets/file-icons");
12const generatedModulePath = NodePath.join(appDirectory, "src/lib/markdownFileIcons.generated.ts");
13const webIconSource = NodeFS.readFileSync(
14 NodePath.join(repositoryRoot, "apps/web/src/pierre-icons.ts"),
15 "utf8",
16);
17const customSprite = webIconSource.match(/const T3_FILE_ICON_SPRITE = `([\s\S]*?)`;/)?.[1];
18
19if (!customSprite) {
20 throw new Error("Could not read the T3 Pierre icon sprite from apps/web/src/pierre-icons.ts");
21}
22
23const 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
80const customIcons = {
81 agents: "t3-file-icon-agents",
82 pnpm: "t3-file-icon-pnpm",
83 video: "t3-file-icon-video",
84};
85
86function 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
98function 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
111NodeFS.rmSync(outputDirectory, { recursive: true, force: true });
112NodeFS.mkdirSync(outputDirectory, { recursive: true });
113
114const builtInSprite = getBuiltInSpriteSheet("complete");
115const builtInTokens = [...builtInSprite.matchAll(/<symbol id="file-tree-builtin-([^"]+)"/g)]
116 .map((match) => match[1])
117 .sort();
118
119for (const token of builtInTokens) {
120 renderIcon(
121 token,
122 symbolFromSprite(builtInSprite, `file-tree-builtin-${token}`),
123 colors[token] ?? colors.default,
124 );
125}
126for (const [token, symbolId] of Object.entries(customIcons)) {
127 renderIcon(token, symbolFromSprite(customSprite, symbolId), colors[token] ?? colors.default);
128}
129
130const tokens = [...new Set([...builtInTokens, ...Object.keys(customIcons)])].sort();
131const 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`;
134NodeFS.writeFileSync(generatedModulePath, generatedSource);