diff --git a/config/schema.ts b/config/schema.ts deleted file mode 100644 index 242f84fb..00000000 --- a/config/schema.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Canonical Zod schemas for Ditto configuration. - * - * This file is deliberately free of path-alias (`@/`) imports so it can be - * consumed from both the Vite config (Node / esbuild context) and from - * runtime application code via `src/lib/schemas.ts`. - * - * These are the "strict" versions of each schema. Legacy/compat wrappers - * (for old localStorage formats, encrypted settings, etc.) are layered on - * top in `src/lib/schemas.ts`. - */ -import { z } from 'zod'; - -// ─── Primitives ─────────────────────────────────────────────────────── - -/** HSL value string like "258 70% 55%" */ -export const HslValue = z.string().regex(/^\d/); - -export const CoreThemeColorsSchema = z.object({ - background: HslValue, - text: HslValue, - primary: HslValue, -}); - -export const ThemeFontSchema = z.object({ - family: z.string(), - url: z.string().optional(), -}); - -export const ThemeBackgroundSchema = z.object({ - url: z.string(), - mode: z.enum(['cover', 'tile']).optional(), - dimensions: z.string().optional(), - mimeType: z.string().optional(), - blurhash: z.string().optional(), -}); - -export const ThemeConfigSchema = z.object({ - title: z.string().optional(), - colors: CoreThemeColorsSchema, - font: ThemeFontSchema.optional(), - background: ThemeBackgroundSchema.optional(), -}); - -export const ThemesConfigSchema = z.object({ - light: ThemeConfigSchema, - dark: ThemeConfigSchema, -}); - -export const ThemeSchema = z.enum(['dark', 'light', 'system', 'custom']); -export const ContentWarningPolicySchema = z.enum(['blur', 'hide', 'show']); - -export const RelayMetadataSchema = z.object({ - relays: z.array(z.object({ - url: z.string().url(), - read: z.boolean(), - write: z.boolean(), - })), - updatedAt: z.number(), -}); - -export const FeedSettingsSchema = z.object({ - feedIncludePosts: z.boolean().optional(), - feedIncludeReposts: z.boolean().optional(), - feedIncludeArticles: z.boolean().optional(), - showArticles: z.boolean().optional(), - showVines: z.boolean().optional(), - showPolls: z.boolean().optional(), - showTreasures: z.boolean().optional(), - showTreasureGeocaches: z.boolean().optional(), - showTreasureFoundLogs: z.boolean().optional(), - showColors: z.boolean().optional(), - showPacks: z.boolean().optional(), - showStreams: z.boolean().optional(), - feedIncludeVines: z.boolean().optional(), - feedIncludePolls: z.boolean().optional(), - feedIncludeTreasureGeocaches: z.boolean().optional(), - feedIncludeTreasureFoundLogs: z.boolean().optional(), - feedIncludeColors: z.boolean().optional(), - feedIncludePacks: z.boolean().optional(), - feedIncludeStreams: z.boolean().optional(), - showDecks: z.boolean().optional(), - feedIncludeDecks: z.boolean().optional(), - showProfileThemes: z.boolean().optional(), - feedIncludeProfileThemes: z.boolean().optional(), - showCustomProfileThemes: z.boolean().optional(), -}).passthrough(); - -// ─── DittoConfigSchema (build-time ditto.json) ─────────────────────── - -/** - * Schema for the build-time `ditto.json` configuration file. - * All fields are optional — only the values provided will override - * the hardcoded defaults at build time. - */ -export const DittoConfigSchema = z.object({ - theme: ThemeSchema.optional(), - customTheme: ThemeConfigSchema.optional(), - themes: ThemesConfigSchema.optional(), - relayMetadata: RelayMetadataSchema.optional(), - useAppRelays: z.boolean().optional(), - feedSettings: FeedSettingsSchema.optional(), - sidebarOrder: z.array(z.string()).optional(), - nip85StatsPubkey: z.string().optional(), - blossomServers: z.array(z.string().url()).optional(), - defaultZapComment: z.string().optional(), - faviconUrl: z.string().optional(), - linkPreviewUrl: z.string().optional(), - corsProxy: z.string().optional(), - contentWarningPolicy: ContentWarningPolicySchema.optional(), -}).strict(); - -/** Inferred type for the build-time configuration. */ -export type DittoConfig = z.infer; diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index ff939b2e..e04f0fb2 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -3,30 +3,7 @@ import { z } from 'zod'; import type { Theme, ContentWarningPolicy } from '@/contexts/AppContext'; import type { CoreThemeColors, ThemeConfig, ThemesConfig } from '@/themes'; -// Re-export canonical schemas from the shared config module so existing -// consumers (`AppProvider`, `useEncryptedSettings`, etc.) keep working -// without changing their import paths. -export { - CoreThemeColorsSchema, - ThemeFontSchema, - ThemeBackgroundSchema, - ThemeConfigSchema, - ThemesConfigSchema, - ContentWarningPolicySchema, - RelayMetadataSchema, - FeedSettingsSchema, -} from '../../config/schema'; - -import { - CoreThemeColorsSchema, - ThemeConfigSchema, - ThemesConfigSchema, - ContentWarningPolicySchema, - RelayMetadataSchema, - FeedSettingsSchema, -} from '../../config/schema'; - -// ─── Type-constrained re-exports ───────────────────────────────────── +// ─── Theme Schemas ─────────────────────────────────────────────────── /** Zod schema for Theme validation */ export const ThemeSchema = z.enum(['dark', 'light', 'system', 'custom']) satisfies z.ZodType; @@ -37,7 +14,15 @@ export const ThemeSchema = z.enum(['dark', 'light', 'system', 'custom']) satisfi */ export const ThemeSchemaCompat = z.enum(['dark', 'light', 'system', 'custom', 'black', 'pink']); -// ─── Legacy / Compat Schemas ───────────────────────────────────────── +/** HSL value string like "258 70% 55%" */ +const HslValue = z.string().regex(/^\d/); + +/** Zod schema for CoreThemeColors (the 3 core colors) */ +export const CoreThemeColorsSchema = z.object({ + background: HslValue, + text: HslValue, + primary: HslValue, +}) satisfies z.ZodType; /** * Legacy schema that accepts the old 19-token ThemeTokens format. @@ -45,9 +30,9 @@ export const ThemeSchemaCompat = z.enum(['dark', 'light', 'system', 'custom', 'b * Extracts core colors from legacy format. */ export const LegacyThemeTokensSchema = z.object({ - background: z.string().regex(/^\d/), - foreground: z.string().regex(/^\d/), - primary: z.string().regex(/^\d/), + background: HslValue, + foreground: HslValue, + primary: HslValue, }).passthrough(); /** @@ -55,10 +40,10 @@ export const LegacyThemeTokensSchema = z.object({ * Strips the secondary field and normalizes to CoreThemeColors. */ export const LegacyFourColorSchema = z.object({ - background: z.string().regex(/^\d/), - text: z.string().regex(/^\d/), - primary: z.string().regex(/^\d/), - secondary: z.string().regex(/^\d/), + background: HslValue, + text: HslValue, + primary: HslValue, + secondary: HslValue, }).transform(({ background, text, primary }): CoreThemeColors => ({ background, text, @@ -79,6 +64,37 @@ export const ThemeColorsCompatSchema = z.union([ })), ]); +// ─── ThemeConfig Schemas ────────────────────────────────────────────── + +/** Zod schema for ThemeFont */ +export const ThemeFontSchema = z.object({ + family: z.string(), + url: z.string().optional(), +}); + +/** Zod schema for ThemeBackground */ +export const ThemeBackgroundSchema = z.object({ + url: z.string(), + mode: z.enum(['cover', 'tile']).optional(), + dimensions: z.string().optional(), + mimeType: z.string().optional(), + blurhash: z.string().optional(), +}); + +/** Zod schema for the full ThemeConfig */ +export const ThemeConfigSchema = z.object({ + title: z.string().optional(), + colors: CoreThemeColorsSchema, + font: ThemeFontSchema.optional(), + background: ThemeBackgroundSchema.optional(), +}); + +/** Zod schema for ThemesConfig (light + dark theme configs) */ +export const ThemesConfigSchema = z.object({ + light: z.lazy(() => ThemeConfigSchema), + dark: z.lazy(() => ThemeConfigSchema), +}) satisfies z.ZodType; + /** * Compat schema that accepts either the new ThemeConfig format or the old * bare CoreThemeColors format (and all legacy color variants), normalizing @@ -90,6 +106,54 @@ export const ThemeConfigCompatSchema = z.union([ ThemeColorsCompatSchema.transform((colors): ThemeConfig => ({ colors })), ]); +/** Zod schema for ContentWarningPolicy validation */ +export const ContentWarningPolicySchema = z.enum(['blur', 'hide', 'show']) satisfies z.ZodType; + +// ─── Feed & Relay Schemas ──────────────────────────────────────────── + +export const RelayMetadataSchema = z.object({ + relays: z.array(z.object({ + url: z.string().url(), + read: z.boolean(), + write: z.boolean(), + })), + updatedAt: z.number(), +}); + +/** + * Zod schema for FeedSettings validation. + * All fields use .optional() so data with missing keys + * (from older encrypted settings) doesn't reject the whole object. + * Uses looseObject to preserve extra keys from newer encrypted settings. + * Missing fields get filled in by the defaultConfig merge downstream. + */ +export const FeedSettingsSchema = z.looseObject({ + feedIncludePosts: z.boolean().optional(), + feedIncludeReposts: z.boolean().optional(), + feedIncludeArticles: z.boolean().optional(), + showArticles: z.boolean().optional(), + showVines: z.boolean().optional(), + showPolls: z.boolean().optional(), + showTreasures: z.boolean().optional(), + showTreasureGeocaches: z.boolean().optional(), + showTreasureFoundLogs: z.boolean().optional(), + showColors: z.boolean().optional(), + showPacks: z.boolean().optional(), + showStreams: z.boolean().optional(), + feedIncludeVines: z.boolean().optional(), + feedIncludePolls: z.boolean().optional(), + feedIncludeTreasureGeocaches: z.boolean().optional(), + feedIncludeTreasureFoundLogs: z.boolean().optional(), + feedIncludeColors: z.boolean().optional(), + feedIncludePacks: z.boolean().optional(), + feedIncludeStreams: z.boolean().optional(), + showDecks: z.boolean().optional(), + feedIncludeDecks: z.boolean().optional(), + showProfileThemes: z.boolean().optional(), + feedIncludeProfileThemes: z.boolean().optional(), + showCustomProfileThemes: z.boolean().optional(), +}); + // ─── AppConfigSchema ───────────────────────────────────────────────── /** @@ -120,6 +184,20 @@ export const AppConfigSchema = z.object({ contentWarningPolicy: ContentWarningPolicySchema, }); +// ─── DittoConfigSchema (build-time ditto.json) ─────────────────────── + +/** + * Schema for the build-time `ditto.json` configuration file. + * Derived from AppConfigSchema with all fields made optional and strict + * mode enabled so unknown keys are rejected. + */ +export const DittoConfigSchema = AppConfigSchema + .partial() + .strict(); + +/** Inferred type for the build-time configuration. */ +export type DittoConfig = z.infer; + // ─── Content Filter Schemas ────────────────────────────────────────── /** Zod schema for FilterRule validation */ diff --git a/tsconfig.json b/tsconfig.json index f47f2461..a7596daf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,5 +27,5 @@ "@/*": ["./src/*"] } }, - "include": ["src", "config"] + "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 25fceb8b..44d935d2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,7 +4,7 @@ import path from "node:path"; import react from "@vitejs/plugin-react-swc"; import { defineConfig } from "vitest/config"; -import { DittoConfigSchema } from "./config/schema"; +import { DittoConfigSchema } from "./src/lib/schemas"; /** * Load and validate the build-time ditto.json configuration file.