Ver Fonte

fix(client): lint conformance for the preference services

Replace optional chains on always-present globals with the repo's
typeof guards (store.ts precedent), drop the non-null assertion by
failing loud on an impossible registry miss, and fix two arrow-parens
slips; cover the no-localStorage boot path in both service suites.
imccyu há 1 mês atrás
pai
commit
2ee4cda066

+ 5 - 2
packages/client/locale/src/client/index.ts

@@ -161,8 +161,10 @@ export class LocaleService {
 
 /** Read the persisted locale id; unknown or unreadable values fall back to zh. */
 function restorePreference(): LocaleId {
+  // Non-browser runs (node e2e booting the client tree) have no localStorage.
+  if (typeof localStorage === 'undefined') return FALLBACK_LOCALE
   try {
-    const stored = globalThis.localStorage?.getItem(STORAGE_KEY)
+    const stored = localStorage.getItem(STORAGE_KEY)
     if (stored === 'zh' || stored === 'en') return stored
   } catch {
     // Storage access can throw (privacy mode); the default below covers it.
@@ -172,8 +174,9 @@ function restorePreference(): LocaleId {
 
 /** Persist the locale id; storage failures are non-fatal (preference resets next boot). */
 function persistPreference(id: LocaleId): void {
+  if (typeof localStorage === 'undefined') return
   try {
-    globalThis.localStorage?.setItem(STORAGE_KEY, id)
+    localStorage.setItem(STORAGE_KEY, id)
   } catch {
     // Storage access can throw (privacy mode / quota); the preference simply
     // does not survive the session.

+ 13 - 1
packages/client/locale/tests/locale.spec.ts

@@ -1,5 +1,5 @@
 // @vitest-environment jsdom
-import { beforeEach, describe, expect, it } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
 import { Context } from 'cordis'
 import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client'
 import { LocaleService, STORAGE_KEY } from '@deepseek-ai/dsh-client-locale/client'
@@ -80,6 +80,18 @@ describe('LocaleService', () => {
     expect(make().svc.getLocale().active).toBe('zh')
   })
 
+  it('runs without localStorage (node boots): defaults on read, no-op on write', () => {
+    vi.stubGlobal('localStorage', undefined)
+    try {
+      const { svc } = make()
+      expect(svc.getLocale().active).toBe('zh')
+      svc.setLocale('en')
+      expect(svc.getLocale().active).toBe('en')
+    } finally {
+      vi.unstubAllGlobals()
+    }
+  })
+
   it('exposes the two shipped locales with self-described labels', () => {
     const { svc } = make()
     expect(svc.getLocale().locales).toEqual([

+ 1 - 1
packages/client/ui-layout/src/client/index.ts

@@ -107,7 +107,7 @@ export function apply(ctx: ClientContext): void {
   ctx.effect(() => {
     const presenter = new ThemePresenter()
     presenter.apply(ctx.theme.getTheme())
-    const off = ctx.on('theme/change', snapshot => { presenter.apply(snapshot) })
+    const off = ctx.on('theme/change', (snapshot) => { presenter.apply(snapshot) })
     return () => {
       off()
       presenter.dispose()

+ 1 - 1
packages/client/ui-settings/src/client/index.ts

@@ -41,7 +41,7 @@ export function apply(ctx: ClientContext): void {
       return ctx.locale.bind(ref.slice(0, colon))(ref.slice(colon + 1))
     },
     sectionsVersion: () => ctx.slots.getVersion('settings.section'),
-    subscribeSections: (listener) => ctx.slots.subscribe('settings.section', listener),
+    subscribeSections: listener => ctx.slots.subscribe('settings.section', listener),
     sections: () => ctx.slots.entries('settings.section')
       .map(e => ({
         /* v8 ignore next -- list-slot registration requires id (SlotCore rejects an entry without one) */

+ 10 - 5
packages/client/ui-theme/src/client/index.ts

@@ -87,7 +87,8 @@ export class ThemeService {
   constructor(ctx: Context) {
     this.ctx = ctx
     this.preference = restorePreference()
-    this.media = globalThis.matchMedia?.('(prefers-color-scheme: dark)')
+    // Non-browser runs (node e2e booting the client tree) have no matchMedia.
+    this.media = typeof matchMedia === 'undefined' ? undefined : matchMedia('(prefers-color-scheme: dark)')
     this.snapshot = this.buildSnapshot()
     if (this.media !== undefined) {
       const media = this.media
@@ -157,8 +158,9 @@ export class ThemeService {
       : this.preference
     // Both built-ins always exist; a registered preference id resolves or has
     // been reset by its disposer, so the lookup cannot miss.
-    /* v8 ignore next -- the ?? arm needs a registry without light/dark, which register()/dispose() cannot produce */
-    const active = this.themes.find(t => t.id === resolvedId) ?? this.themes[0]!
+    const active = this.themes.find(t => t.id === resolvedId)
+    /* v8 ignore next 2 -- needs a registry without light/dark, which register()/dispose() cannot produce */
+    if (active === undefined) throw new Error(`theme registry lost "${resolvedId}"`)
     return Object.freeze({
       preference: this.preference,
       active,
@@ -176,8 +178,10 @@ export class ThemeService {
 
 /** Read the persisted preference; unknown or unreadable values fall back to the default. */
 function restorePreference(): ThemePreference {
+  // Non-browser runs (node e2e booting the client tree) have no localStorage.
+  if (typeof localStorage === 'undefined') return DEFAULT_PREFERENCE
   try {
-    const stored = globalThis.localStorage?.getItem(STORAGE_KEY)
+    const stored = localStorage.getItem(STORAGE_KEY)
     if (stored === 'light' || stored === 'dark' || stored === 'system') return stored
   } catch {
     // Storage access can throw (privacy mode); the default below covers it.
@@ -187,8 +191,9 @@ function restorePreference(): ThemePreference {
 
 /** Persist the preference; storage failures are non-fatal (preference resets next boot). */
 function persistPreference(preference: ThemePreference): void {
+  if (typeof localStorage === 'undefined') return
   try {
-    globalThis.localStorage?.setItem(STORAGE_KEY, preference)
+    localStorage.setItem(STORAGE_KEY, preference)
   } catch {
     // Storage access can throw (privacy mode / quota); the preference simply
     // does not survive the session.

+ 12 - 0
packages/client/ui-theme/tests/theme.spec.ts

@@ -88,6 +88,18 @@ describe('ThemeService', () => {
     expect(events.map(e => e.revision)).toEqual([1, 2, 3, 4])
   })
 
+  it('runs without localStorage (node boots): defaults on read, no-op on write', () => {
+    vi.stubGlobal('localStorage', undefined)
+    try {
+      const { theme } = make()
+      expect(theme.getTheme().preference).toBe('system')
+      theme.setTheme('dark')
+      expect(theme.getTheme().preference).toBe('dark')
+    } finally {
+      vi.unstubAllGlobals()
+    }
+  })
+
   describe('prefers-color-scheme resolution (stubbed matchMedia)', () => {
     type Listener = () => void
     const stubMedia = (initialMatches: boolean) => {