History

fix(web): address open vsx review findings

t3-code[bot] committed 3 months ago

Commit 4e5bafc1 parent 64490d43 files · +43 −24Browse files

Unified · Split

3 files
  1. apps/web/src/components/settings/ThemeSearchDialog.tsx +4 −12
  2. apps/web/src/openVsxThemes.test.ts +19 −2
  3. apps/web/src/openVsxThemes.ts +20 −10
apps/web/src/components/settings/ThemeSearchDialog.tsx+4−12
@@ -1,4 +1,4 @@
1−import { CheckCircle2Icon, PlusIcon, SearchIcon, ShieldCheckIcon } from "lucide-react";
1+import { CheckCircle2Icon, PaletteIcon, PlusIcon, SearchIcon, ShieldCheckIcon } from "lucide-react";
22 import type { FormEvent } from "react";
33 import { useCallback, useEffect, useRef, useState } from "react";
44 import {
@@ -223,17 +223,9 @@ export function ThemeSearchDialog({ · 218 unmodified lines
223223 key={extension.id}
224224 >
225225 <div className="flex min-w-0 gap-3">
226− {extension.iconUrl ? (
227− <img
228− alt=""
229− className="size-10 shrink-0 rounded-lg bg-muted object-cover"
230− src={extension.iconUrl}
231− />
232− ) : (
233− <div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
234− <SearchIcon className="size-4" />
235− </div>
236− )}
226+ <div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
227+ <PaletteIcon className="size-4" />
228+ </div>
237229 <div className="min-w-0 flex-1">
238230 <h4 className="truncate text-sm font-medium">{extension.name}</h4>
239231 <p className="truncate text-muted-foreground text-xs">
apps/web/src/openVsxThemes.test.ts+19−2
@@ -80,6 +80,25 @@ describe("Open VSX themes", () => { · 79 unmodified lines
8080 expect(searchUrl.searchParams.get("size")).toBe("16");
8181 });
8282
83+ it("reports an unavailable search when every detail request fails", async () => {
84+ vi.stubGlobal(
85+ "fetch",
86+ vi.fn(async (input: RequestInfo | URL) => {
87+ if (String(input).includes("/-/search?")) {
88+ return new Response(
89+ JSON.stringify({ extensions: [{ namespace: "demo", name: "theme" }] }),
90+ { status: 200 },
91+ );
92+ }
93+ return new Response(null, { status: 429 });
94+ }),
95+ );
96+
97+ await expect(searchOpenVsxThemes("dracula")).rejects.toThrow(
98+ "Open VSX theme details are unavailable",
99+ );
100+ });
101+
83102 it("downloads a verified VSIX, reads JSONC includes, and pairs contributed variants", async () => {
84103 const zip = new JSZip();
85104 zip.file(
@@ -140,7 +159,6 @@ describe("Open VSX themes", () => { · 54 unmodified lines
140159 publisher: "demo",
141160 description: "",
142161 downloadCount: 1,
143− iconUrl: null,
144162 license: "MIT",
145163 manifestUrl: `${ASSET_ROOT}/package.json`,
146164 sha256Url: `${ASSET_ROOT}/demo.theme-1.0.0.sha256`,
@@ -184,7 +202,6 @@ describe("Open VSX themes", () => { · 37 unmodified lines
184202 publisher: "demo",
185203 description: "",
186204 downloadCount: 1,
187− iconUrl: null,
188205 license: "MIT",
189206 manifestUrl: `${ASSET_ROOT}/package.json`,
190207 sha256Url: `${ASSET_ROOT}/demo.theme-1.0.0.sha256`,
apps/web/src/openVsxThemes.ts+20−10
@@ -39,7 +39,6 @@ export type OpenVsxThemeExtension = { · 38 unmodified lines
3939 publisher: string;
4040 description: string;
4141 downloadCount: number;
42− iconUrl: string | null;
4342 manifestUrl: string;
4443 sha256Url: string;
4544 vsixUrl: string;
@@ -96,7 +95,6 @@ function extensionFromDetail(value: unknown): OpenVsxThemeExtension | null { · 50 unmodified lines
9695 typeof value.downloadCount === "number" && Number.isFinite(value.downloadCount)
9796 ? value.downloadCount
9897 : 0,
99− iconUrl: trustedOpenVsxUrl(value.files.icon),
10098 manifestUrl,
10199 sha256Url,
102100 vsixUrl,
@@ -141,7 +139,7 @@ export async function searchOpenVsxThemes( · 38 unmodified lines
141139 identities.slice(0, 16).map(async ([namespace, name]) => {
142140 const detailUrl = `https://open-vsx.org/api/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
143141 const detailResponse = await fetch(detailUrl, signal ? { signal } : {});
144− if (!detailResponse.ok) return null;
142+ if (!detailResponse.ok) throw new Error("Open VSX theme details are unavailable.");
145143 const detailBytes = await readCappedResponse(
146144 detailResponse,
147145 MAX_DETAIL_BYTES,
@@ -155,9 +153,11 @@ export async function searchOpenVsxThemes( · 7 unmodified lines
155153 }),
156154 );
157155 if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
158− return details
159− .flatMap((result) => (result.status === "fulfilled" && result.value ? [result.value] : []))
160− .slice(0, 8);
156+ const completedDetails = details.filter((result) => result.status === "fulfilled");
157+ if (identities.length > 0 && completedDetails.length === 0) {
158+ throw new Error("Open VSX theme details are unavailable right now.");
159+ }
160+ return completedDetails.flatMap((result) => (result.value ? [result.value] : [])).slice(0, 8);
161161 }
162162
163163 function parseJsoncObject(source: string, description: string): Record<string, unknown> {
@@ -331,18 +331,25 @@ async function readZipText(zip: JSZip, path: string, description: string): Promi · 167 unmodified lines
331331 async function loadThemeObject(
332332 zip: JSZip,
333333 path: string,
334+ cache: Map<string, Record<string, unknown>>,
334335 ancestors: ReadonlySet<string> = new Set(),
335336 ): Promise<Record<string, unknown>> {
336337 if (ancestors.size >= MAX_INCLUDE_DEPTH) throw new Error("Theme includes are nested too deeply.");
337338 if (ancestors.has(path)) throw new Error("Theme includes contain a cycle.");
339+ const cached = cache.get(path);
340+ if (cached) return cached;
341+
338342 const value = parseJsoncObject(await readZipText(zip, path, path), path);
339− if (typeof value.include !== "string") return value;
343+ if (typeof value.include !== "string") {
344+ cache.set(path, value);
345+ return value;
346+ }
340347
341348 const includePath = normalizePackagePath(value.include, path);
342349 const nextAncestors = new Set(ancestors);
343350 nextAncestors.add(path);
344− const base = await loadThemeObject(zip, includePath, nextAncestors);
345− return {
351+ const base = await loadThemeObject(zip, includePath, cache, nextAncestors);
352+ const resolved = {
346353 ...base,
347354 ...value,
348355 colors: {
@@ -350,6 +357,8 @@ async function loadThemeObject( · 1 unmodified line
350357 ...(isRecord(value.colors) ? value.colors : {}),
351358 },
352359 };
360+ cache.set(path, resolved);
361+ return resolved;
353362 }
354363
355364 async function readCappedResponse(
@@ -457,11 +466,12 @@ export async function importOpenVsxThemeExtension( · 101 unmodified lines
457466
458467 const parsed: Array<{ theme: ThemeDefinition; sourceName: string }> = [];
459468 const failures: string[] = [];
469+ const themeCache = new Map<string, Record<string, unknown>>();
460470 for (const contribution of contributions as ThemeContribution[]) {
461471 if (typeof contribution.path !== "string") continue;
462472 try {
463473 const path = normalizePackagePath(contribution.path);
464− const themeValue = await loadThemeObject(zip, path);
474+ const themeValue = await loadThemeObject(zip, path, themeCache);
465475 const type = contributionType(contribution.uiTheme);
466476 const label =
467477 typeof contribution.label === "string" && contribution.label.trim()