fix(migration): normalize legacy theme values under v5 marker

Two related bugs surfaced during end-to-end verification:

1. The 'isAlreadyV5' short-circuit in both the main-side and renderer-side
   v4-to-v5 transforms returned the persisted object as-is when a
   migration.version=5 marker was present. A previously-shipped v4 build
   had written theme: 'ayu-light' (and similar) under the v5 marker;
   the transform trusted the marker and passed the invalid theme through,
   which then failed the renderer's zod schema on every launch and
   reset user settings to defaults.

   Fix: in both transforms, when isAlreadyV5 matches, normalize theme
   against the v5 enum (light/dark/system) and validate the full result
   with the v5 schema before returning. Out-of-range values are replaced
   with the v5 default ('system').

2. The renderer's settings-store onRehydrateStorage callback called
   useSettingsStore.setState() to reset the store on validation failure.
   At that moment the store is still being constructed, and setState
   could hit a TDZ ReferenceError (the one we already wrapped in a
   try/catch in v5.0.0, which only hid the symptom).

   Fix: return the normalized state object from onRehydrateStorage
   instead. Zustand's persist middleware applies the returned value
   *after* construction completes, so there is no TDZ.

Tests: 334 vitest + 208 jest = 542 passing.
E2E: 12/12 verify-features.mjs steps green; no console errors.

Amit Haridas
This commit is contained in:
2026-06-08 07:45:06 +05:30
parent c5d4b113bd
commit e25a5e1d75
5 changed files with 107 additions and 15 deletions
+17 -1
View File
@@ -11,6 +11,7 @@ export const v4SettingsSchema = z.object({
});
const v5OnlyFields = ['updateChannel', 'autoCheckUpdates', 'firstRun'];
const v5ThemeValues = ['light', 'dark', 'system'] as const;
function isAlreadyV5(data: unknown): boolean {
if (!data || typeof data !== 'object') return false;
@@ -18,8 +19,23 @@ function isAlreadyV5(data: unknown): boolean {
return v5OnlyFields.some((f) => f in (data as Record<string, unknown>));
}
function normalizeAlreadyV5(data: Record<string, unknown>): Record<string, unknown> {
// Some earlier v5 builds wrote a legacy theme value (e.g. "ayu-light")
// under the v5 marker. Trusting the marker blindly broke the renderer's
// zod schema on every launch. Always normalize theme against the v5 enum
// before returning, so persisted files are always valid v5.
const out = { ...data };
if (typeof out.theme !== 'string' || !v5ThemeValues.includes(out.theme as (typeof v5ThemeValues)[number])) {
out.theme = 'system';
}
return out;
}
export function migrateV4ToV5(v4: unknown): z.infer<typeof settingsSchema> {
if (isAlreadyV5(v4)) return v4 as z.infer<typeof settingsSchema>;
if (isAlreadyV5(v4)) {
const normalized = normalizeAlreadyV5(v4 as Record<string, unknown>);
return settingsSchema.parse(normalized);
}
const parsed = v4SettingsSchema.parse(v4);
const defaults = settingsSchema.parse({});
return {