History

fix(web): preserve community theme provenance

t3-code[bot] committed 3 months ago

Commit dd8a0db1 parent 62d13075 files · +75 −24Browse files

Unified · Split

5 files
  1. apps/web/src/components/settings/ThemeSearchSection.tsx +34 −11
  2. apps/web/src/openVsxThemes.test.ts +6 −0
  3. apps/web/src/openVsxThemes.ts +2 −2
  4. apps/web/src/themePalette.test.ts +16 −4
  5. apps/web/src/themePalette.ts +17 −7
apps/web/src/components/settings/ThemeSearchSection.tsx+34−11
@@ -10,6 +10,7 @@ import { · 9 unmodified lines
1010 getCustomThemes,
1111 installCustomTheme,
1212 removeCustomTheme,
13+ updateCustomTheme,
1314 type ThemeDefinition,
1415 } from "../../themePalette";
1516 import { Button } from "../ui/button";
@@ -95,26 +96,40 @@ export function ThemeSearchSection({ · 79 unmodified lines
9596 try {
9697 const themes = await importOpenVsxThemeExtension(extension, controller.signal);
9798 if (controller.signal.aborted) return;
98− const existingIds = new Set(getCustomThemes().map((theme) => theme.id));
99− const missingThemes = themes.filter((theme) => !existingIds.has(theme.id));
100− if (missingThemes.length === 0) {
101− throw new Error(`${extension.name} is already added.`);
102− }
103−
104− const installed: ThemeDefinition[] = [];
99+ const existingById = new Map(getCustomThemes().map((theme) => [theme.id, theme]));
100+ const installedIds: string[] = [];
101+ const replaced: ThemeDefinition[] = [];
102+ const imported: ThemeDefinition[] = [];
105103 try {
106− for (const theme of missingThemes) installed.push(installCustomTheme(theme));
104+ for (const theme of themes) {
105+ const existing = existingById.get(theme.id);
106+ if (existing) {
107+ replaced.push(existing);
108+ imported.push(updateCustomTheme(theme));
109+ } else {
110+ const installed = installCustomTheme(theme);
111+ installedIds.push(installed.id);
112+ imported.push(installed);
113+ }
114+ }
107115 } catch (cause) {
108− for (const theme of installed) {
116+ for (const themeId of installedIds) {
109117 try {
110− removeCustomTheme(theme.id);
118+ removeCustomTheme(themeId);
119+ } catch {
120+ // Best effort rollback. The original storage failure is clearer.
121+ }
122+ }
123+ for (const theme of replaced) {
124+ try {
125+ updateCustomTheme(theme);
111126 } catch {
112127 // Best effort rollback. The original storage failure is clearer.
113128 }
114129 }
115130 throw cause;
116131 }
117− onInstalled(installed);
132+ onInstalled(imported);
118133 } catch (cause) {
119134 if (!controller.signal.aborted) {
120135 setError(cause instanceof Error ? cause.message : "That theme could not be added.");
@@ -161,6 +176,14 @@ export function ThemeSearchSection({ · 40 unmodified lines
161176 </Button>
162177 </form>
163178
179+ <div className="sr-only" role="status">
180+ {isSearching
181+ ? "Finding themes..."
182+ : results
183+ ? `${results.length} supported ${results.length === 1 ? "theme" : "themes"} found.`
184+ : ""}
185+ </div>
186+
164187 {error ? (
165188 <div
166189 aria-live="polite"
apps/web/src/openVsxThemes.test.ts+6−0
@@ -313,6 +313,12 @@ describe("Open VSX themes", () => { · 312 unmodified lines
313313 await expect(importOpenVsxThemeExtension(extension)).rejects.toThrow(
314314 "does not match its advertised license",
315315 );
316+
317+ Reflect.deleteProperty(packagedManifest, "license");
318+ await rebuildPackage();
319+ await expect(importOpenVsxThemeExtension(extension)).rejects.toThrow(
320+ "does not match its advertised license",
321+ );
316322 });
317323
318324 it("stops import work when the request is cancelled", async () => {
apps/web/src/openVsxThemes.ts+2−2
@@ -651,8 +651,8 @@ export async function importOpenVsxThemeExtension( · 650 unmodified lines
651651 throw new Error("That extension package does not match the selected Open VSX theme.");
652652 }
653653 if (
654− typeof packagedManifest.license === "string" &&
655− packagedManifest.license.trim() !== extension.license
654+ typeof packagedManifest.license !== "string" ||
655+ packagedManifest.license.trim().toLowerCase() !== extension.license.toLowerCase()
656656 ) {
657657 throw new Error("That extension package does not match its advertised license.");
658658 }
apps/web/src/themePalette.test.ts+16−4
@@ -195,13 +195,25 @@ describe("theme files", () => { · 194 unmodified lines
195195 });
196196
197197 it("serializes a theme back into the importable file shape", () => {
198− const serialized = serializeThemeFile(T3_CHAT_THEME);
198+ const theme = {
199+ ...parseThemeFile({
200+ version: THEME_FILE_VERSION,
201+ id: "community-demo",
202+ name: "Community Demo",
203+ appearance: "dark",
204+ colors: { canvas: "#111111" },
205+ }),
206+ collection: { id: "open-vsx:demo.theme", label: "Demo Theme" },
207+ };
208+ const serialized = serializeThemeFile(theme);
199209 expect(JSON.parse(serialized)).toMatchObject({
200210 version: THEME_FILE_VERSION,
201− id: T3_CHAT_THEME.id,
202− name: T3_CHAT_THEME.label,
203− appearance: "light",
211+ id: theme.id,
212+ name: theme.label,
213+ appearance: "dark",
214+ collection: theme.collection,
204215 });
216+ expect(parseThemeFile(JSON.parse(serialized)).collection).toEqual(theme.collection);
205217 });
206218
207219 it("keeps sidebar artwork opt-in through theme files", () => {
apps/web/src/themePalette.ts+17−7
@@ -112,6 +112,7 @@ export type ThemeFile = Readonly<{ · 111 unmodified lines
112112 appearance: ThemeAppearance;
113113 colors: ThemeColorOverrides;
114114 variants?: ThemeVariantOverrides;
115+ collection?: ThemeCollection;
115116 sidebarArtwork?: boolean;
116117 managed?: boolean;
117118 }>;
@@ -175,6 +176,15 @@ function isThemeLabel(value: unknown): value is string { · 57 unmodified lines
175176 return typeof value === "string" && value.trim().length > 0 && value.trim().length <= 48;
176177 }
177178
179+function parseThemeCollection(value: unknown): ThemeCollection | undefined {
180+ return isRecord(value) &&
181+ typeof value.id === "string" &&
182+ /^[a-z0-9][a-z0-9.:-]{0,127}$/i.test(value.id) &&
183+ isThemeLabel(value.label)
184+ ? { id: value.id, label: value.label.trim() }
185+ : undefined;
186+}
187+
178188 function parseStoredThemeColors(value: unknown, appearance: ThemeAppearance): ThemeColors | null {
179189 if (!isRecord(value)) return null;
180190
@@ -219,13 +229,7 @@ function parseStoredTheme(value: unknown): ThemeDefinition | null { · 38 unmodified lines
219229 if (!colors) return null;
220230 const variants = parseStoredThemeVariants(value.variants, value.appearance);
221231 if (value.variants !== undefined && variants === null) return null;
222− const collection =
223− isRecord(value.collection) &&
224− typeof value.collection.id === "string" &&
225− /^[a-z0-9][a-z0-9.:-]{0,127}$/i.test(value.collection.id) &&
226− isThemeLabel(value.collection.label)
227− ? { id: value.collection.id, label: value.collection.label.trim() }
228− : undefined;
232+ const collection = parseThemeCollection(value.collection);
229233
230234 return {
231235 id: value.id,
@@ -1547,6 +1551,10 @@ export function parseThemeFile(value: unknown): ThemeDefinition { · 1315 unmodified lines
15471551 }
15481552
15491553 const overrides = parseThemeColorOverrides(rawColors);
1554+ const collection = parseThemeCollection(value.collection);
1555+ if (value.collection !== undefined && !collection) {
1556+ throw new Error("Theme collections need a valid id and label.");
1557+ }
15501558
15511559 const fallback = getDefaultThemeColors(appearance);
15521560 const variants: Partial<Record<ThemeAppearance, ThemeColors>> = {};
@@ -1573,6 +1581,7 @@ export function parseThemeFile(value: unknown): ThemeDefinition { · 20 unmodified lines
15731581 appearance,
15741582 colors: { ...fallback, ...overrides },
15751583 ...(Object.keys(variants).length > 0 ? { variants } : {}),
1584+ ...(collection ? { collection } : {}),
15761585 ...(value.sidebarArtwork === true ? { sidebarArtwork: true } : {}),
15771586 ...(value.managed === true ? { managed: true } : {}),
15781587 };
@@ -1586,6 +1595,7 @@ export function serializeThemeFile(theme: ThemeDefinition): string { · 7 unmodified lines
15861595 appearance: theme.appearance,
15871596 colors: theme.colors,
15881597 ...(theme.variants ? { variants: theme.variants } : {}),
1598+ ...(theme.collection ? { collection: theme.collection } : {}),
15891599 ...(theme.sidebarArtwork ? { sidebarArtwork: true } : {}),
15901600 ...(theme.managed ? { managed: true } : {}),
15911601 };