| @@ -39,7 +39,6 @@ export type OpenVsxThemeExtension = { · 38 unmodified lines |
| 39 | 39 | publisher: string; |
| 40 | 40 | description: string; |
| 41 | 41 | downloadCount: number; |
| 42 | | − iconUrl: string | null; |
| 43 | 42 | manifestUrl: string; |
| 44 | 43 | sha256Url: string; |
| 45 | 44 | vsixUrl: string; |
| @@ -96,7 +95,6 @@ function extensionFromDetail(value: unknown): OpenVsxThemeExtension | null { · 50 unmodified lines |
| 96 | 95 | typeof value.downloadCount === "number" && Number.isFinite(value.downloadCount) |
| 97 | 96 | ? value.downloadCount |
| 98 | 97 | : 0, |
| 99 | | − iconUrl: trustedOpenVsxUrl(value.files.icon), |
| 100 | 98 | manifestUrl, |
| 101 | 99 | sha256Url, |
| 102 | 100 | vsixUrl, |
| @@ -141,7 +139,7 @@ export async function searchOpenVsxThemes( · 38 unmodified lines |
| 141 | 139 | identities.slice(0, 16).map(async ([namespace, name]) => { |
| 142 | 140 | const detailUrl = `https://open-vsx.org/api/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`; |
| 143 | 141 | 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."); |
| 145 | 143 | const detailBytes = await readCappedResponse( |
| 146 | 144 | detailResponse, |
| 147 | 145 | MAX_DETAIL_BYTES, |
| @@ -155,9 +153,11 @@ export async function searchOpenVsxThemes( · 7 unmodified lines |
| 155 | 153 | }), |
| 156 | 154 | ); |
| 157 | 155 | 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); |
| 161 | 161 | } |
| 162 | 162 | |
| 163 | 163 | 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 |
| 331 | 331 | async function loadThemeObject( |
| 332 | 332 | zip: JSZip, |
| 333 | 333 | path: string, |
| 334 | + cache: Map<string, Record<string, unknown>>, |
| 334 | 335 | ancestors: ReadonlySet<string> = new Set(), |
| 335 | 336 | ): Promise<Record<string, unknown>> { |
| 336 | 337 | if (ancestors.size >= MAX_INCLUDE_DEPTH) throw new Error("Theme includes are nested too deeply."); |
| 337 | 338 | if (ancestors.has(path)) throw new Error("Theme includes contain a cycle."); |
| 339 | + const cached = cache.get(path); |
| 340 | + if (cached) return cached; |
| 341 | + |
| 338 | 342 | 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 | + } |
| 340 | 347 | |
| 341 | 348 | const includePath = normalizePackagePath(value.include, path); |
| 342 | 349 | const nextAncestors = new Set(ancestors); |
| 343 | 350 | 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 = { |
| 346 | 353 | ...base, |
| 347 | 354 | ...value, |
| 348 | 355 | colors: { |
| @@ -350,6 +357,8 @@ async function loadThemeObject( · 1 unmodified line |
| 350 | 357 | ...(isRecord(value.colors) ? value.colors : {}), |
| 351 | 358 | }, |
| 352 | 359 | }; |
| 360 | + cache.set(path, resolved); |
| 361 | + return resolved; |
| 353 | 362 | } |
| 354 | 363 | |
| 355 | 364 | async function readCappedResponse( |
| @@ -457,11 +466,12 @@ export async function importOpenVsxThemeExtension( · 101 unmodified lines |
| 457 | 466 | |
| 458 | 467 | const parsed: Array<{ theme: ThemeDefinition; sourceName: string }> = []; |
| 459 | 468 | const failures: string[] = []; |
| 469 | + const themeCache = new Map<string, Record<string, unknown>>(); |
| 460 | 470 | for (const contribution of contributions as ThemeContribution[]) { |
| 461 | 471 | if (typeof contribution.path !== "string") continue; |
| 462 | 472 | try { |
| 463 | 473 | const path = normalizePackagePath(contribution.path); |
| 464 | | − const themeValue = await loadThemeObject(zip, path); |
| 474 | + const themeValue = await loadThemeObject(zip, path, themeCache); |
| 465 | 475 | const type = contributionType(contribution.uiTheme); |
| 466 | 476 | const label = |
| 467 | 477 | typeof contribution.label === "string" && contribution.label.trim() |