History

Add VS Code file icons and tighten workspace entry filtering

- render theme-aware VS Code icons for path suggestions in chat composer - exclude `.convex` and gitignored paths more reliably in workspace indexing - add tests plus a sync script and icon manifest/association data

Julius Marminge committed 8 months ago

Commit f04b2d91 parent 3109d1a9 files · +558 −51Browse files

Showing the changes to scripts (1 of 9 files). Show all

Unified · Split

scripts/sync-vscode-icons.mjs+153−0
@@ -0,0 +1,153 @@
1+#!/usr/bin/env node
2+
3+import fs from "node:fs/promises";
4+import os from "node:os";
5+import path from "node:path";
6+import vm from "node:vm";
7+import { execFile } from "node:child_process";
8+import { promisify } from "node:util";
9+
10+const execFileAsync = promisify(execFile);
11+
12+const VERSION = process.argv[2] ?? "12.17.0";
13+const VERSION_TAG = `v${VERSION}`;
14+const VSIX_URL = `https://open-vsx.org/api/vscode-icons-team/vscode-icons/${VERSION}/file/vscode-icons-team.vscode-icons-${VERSION}.vsix`;
15+const LANGUAGES_URL = `https://raw.githubusercontent.com/vscode-icons/vscode-icons/${VERSION_TAG}/src/iconsManifest/languages.ts`;
16+
17+const REPO_ROOT = process.cwd();
18+const MANIFEST_PATH = path.join(REPO_ROOT, "apps/web/src/vscode-icons-manifest.json");
19+const ASSOCIATIONS_PATH = path.join(
20+ REPO_ROOT,
21+ "apps/web/src/vscode-icons-language-associations.json",
22+);
23+
24+function normalizeExtension(value) {
25+ return value.trim().toLowerCase().replace(/^\./, "");
26+}
27+
28+function normalizeFileName(value) {
29+ return value.trim().toLowerCase();
30+}
31+
32+function putIfAbsent(target, key, value) {
33+ if (!(key in target)) {
34+ target[key] = value;
35+ }
36+}
37+
38+async function downloadVsix(tmpDir) {
39+ const vsixPath = path.join(tmpDir, `vscode-icons-${VERSION}.vsix`);
40+ const response = await fetch(VSIX_URL);
41+ if (!response.ok) {
42+ throw new Error(`Failed to download VSIX: ${response.status} ${response.statusText}`);
43+ }
44+ const bytes = Buffer.from(await response.arrayBuffer());
45+ await fs.writeFile(vsixPath, bytes);
46+ return vsixPath;
47+}
48+
49+async function extractManifestFromVsix(vsixPath) {
50+ const { stdout } = await execFileAsync("unzip", [
51+ "-p",
52+ vsixPath,
53+ "extension/dist/src/vsicons-icon-theme.json",
54+ ]);
55+ if (!stdout || stdout.trim().length === 0) {
56+ throw new Error("Could not extract vsicons-icon-theme.json from VSIX");
57+ }
58+ return JSON.parse(stdout);
59+}
60+
61+async function loadLanguagesCollection() {
62+ const response = await fetch(LANGUAGES_URL);
63+ if (!response.ok) {
64+ throw new Error(`Failed to fetch languages.ts: ${response.status} ${response.statusText}`);
65+ }
66+ const rawSource = await response.text();
67+ const source = rawSource
68+ .replace(/^import[^;]+;\s*/gm, "")
69+ .replace(/export const languages(?:\s*:\s*[^=]+)?\s*=/, "const languages =")
70+ .replace(/\}\s*satisfies\s*Record<[^;]+>;/, "};");
71+
72+ const context = {};
73+ vm.createContext(context);
74+ vm.runInContext(`${source}\n;globalThis.__languages = languages;`, context);
75+ const languages = context.__languages;
76+ if (!languages || typeof languages !== "object") {
77+ throw new Error("Failed to parse languages.ts into a collection");
78+ }
79+ return languages;
80+}
81+
82+function buildLanguageAssociations(manifest, languages) {
83+ const availableLanguageIds = new Set(Object.keys(manifest.languageIds ?? {}));
84+ const extensionToLanguageId = {};
85+ const fileNameToLanguageId = {};
86+
87+ for (const entry of Object.values(languages)) {
88+ if (!entry || typeof entry !== "object") continue;
89+ const idsRaw = entry.ids;
90+ const ids = Array.isArray(idsRaw) ? idsRaw : [idsRaw];
91+ const knownExtensions = Array.isArray(entry.knownExtensions) ? entry.knownExtensions : [];
92+ const knownFilenames = Array.isArray(entry.knownFilenames) ? entry.knownFilenames : [];
93+
94+ for (const idValue of ids) {
95+ if (typeof idValue !== "string") continue;
96+ const languageId = idValue.trim();
97+ if (languageId.length === 0) continue;
98+ if (!availableLanguageIds.has(languageId)) continue;
99+
100+ for (const extensionValue of knownExtensions) {
101+ if (typeof extensionValue !== "string") continue;
102+ const extension = normalizeExtension(extensionValue);
103+ if (extension.length === 0) continue;
104+ putIfAbsent(extensionToLanguageId, extension, languageId);
105+ }
106+
107+ for (const fileNameValue of knownFilenames) {
108+ if (typeof fileNameValue !== "string") continue;
109+ const fileName = normalizeFileName(fileNameValue);
110+ if (fileName.length === 0) continue;
111+ putIfAbsent(fileNameToLanguageId, fileName, languageId);
112+ }
113+ }
114+ }
115+
116+ return {
117+ version: VERSION,
118+ source: LANGUAGES_URL,
119+ generatedAt: new Date().toISOString(),
120+ extensionToLanguageId,
121+ fileNameToLanguageId,
122+ };
123+}
124+
125+async function main() {
126+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "t3-vscode-icons-sync-"));
127+ try {
128+ const vsixPath = await downloadVsix(tmpDir);
129+ const manifest = await extractManifestFromVsix(vsixPath);
130+ const languages = await loadLanguagesCollection();
131+ const associations = buildLanguageAssociations(manifest, languages);
132+
133+ await fs.writeFile(MANIFEST_PATH, `${JSON.stringify(manifest)}\n`, "utf8");
134+ await fs.writeFile(ASSOCIATIONS_PATH, `${JSON.stringify(associations)}\n`, "utf8");
135+
136+ process.stdout.write(
137+ [
138+ `Synced vscode-icons ${VERSION}`,
139+ `manifest: ${MANIFEST_PATH}`,
140+ `language associations: ${ASSOCIATIONS_PATH}`,
141+ `extension mappings: ${Object.keys(associations.extensionToLanguageId).length}`,
142+ `filename mappings: ${Object.keys(associations.fileNameToLanguageId).length}`,
143+ ].join("\n") + "\n",
144+ );
145+ } finally {
146+ await fs.rm(tmpDir, { recursive: true, force: true });
147+ }
148+}
149+
150+main().catch((error) => {
151+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
152+ process.exit(1);
153+});