Eliminate duplicated schema by deriving DittoConfigSchema from AppConfigSchema

Delete the separate config/schema.ts that duplicated every Zod schema.
DittoConfigSchema is now simply AppConfigSchema.partial().strict(),
defined in src/lib/schemas.ts alongside everything else. vite.config.ts
imports it directly via a relative path.
This commit is contained in:
Alex Gleason
2026-02-26 17:57:55 -06:00
parent 3eb92f472c
commit be6980359f
4 changed files with 112 additions and 148 deletions
-114
View File
@@ -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<typeof DittoConfigSchema>;
+110 -32
View File
@@ -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<Theme>;
@@ -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<CoreThemeColors>;
/**
* 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<ThemesConfig>;
/**
* 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<ContentWarningPolicy>;
// ─── 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<typeof DittoConfigSchema>;
// ─── Content Filter Schemas ──────────────────────────────────────────
/** Zod schema for FilterRule validation */
+1 -1
View File
@@ -27,5 +27,5 @@
"@/*": ["./src/*"]
}
},
"include": ["src", "config"]
"include": ["src"]
}
+1 -1
View File
@@ -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.