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