Replace __DITTO_CONFIG__ global with import.meta.env.DITTO_CONFIG and remove ThemeSchemaCompat
Move build-time ditto.json injection from a Vite define global to import.meta.env.DITTO_CONFIG (a JSON string parsed and validated at runtime via DittoConfigSchema). Remove the global type declaration from vite-env.d.ts. Drop ThemeSchemaCompat and its legacy "black"/"pink" migration code from AppProvider and NostrSync — invalid theme values now simply fail Zod validation. Fix a latent bug where a partial feedSettings from ditto.json would replace the full hardcoded defaults; defaultConfig now deep-merges feedSettings.
This commit is contained in:
@@ -60,7 +60,7 @@ const builtinThemes = {
|
||||
};
|
||||
```
|
||||
|
||||
Self-hosters can override these at build time via `ditto.json` (injected through `__DITTO_CONFIG__` in `vite.config.ts`), or at runtime via the `ThemesConfig` in `AppConfig.themes`.
|
||||
Self-hosters can override these at build time via `ditto.json` (injected through `import.meta.env.DITTO_CONFIG` in `vite.config.ts`), or at runtime via the `ThemesConfig` in `AppConfig.themes`.
|
||||
|
||||
### ThemeConfig
|
||||
|
||||
|
||||
+19
-3
@@ -23,6 +23,7 @@ import { useNsecPasteGuard } from "@/hooks/useNsecPasteGuard";
|
||||
import type { AppConfig } from "@/contexts/AppContext";
|
||||
import { NWCProvider } from "@/contexts/NWCContext";
|
||||
import { PROTOCOL_MODE } from "@/lib/dmConstants";
|
||||
import { DittoConfigSchema, type DittoConfig } from "@/lib/schemas";
|
||||
import { EmotionDevProvider } from "@/blobbi/dev/EmotionDevContext";
|
||||
import AppRouter from "./AppRouter";
|
||||
|
||||
@@ -149,15 +150,30 @@ const hardcodedConfig: AppConfig = {
|
||||
imageQuality: 'compressed',
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse and validate build-time ditto.json overrides from the env string.
|
||||
* Returns an empty object when no config file was provided or validation fails.
|
||||
*/
|
||||
function parseDittoConfig(): DittoConfig {
|
||||
try {
|
||||
const json = JSON.parse(import.meta.env.DITTO_CONFIG);
|
||||
if (!json) return {};
|
||||
return DittoConfigSchema.parse(json);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge hardcoded defaults with build-time ditto.json overrides.
|
||||
* Deep-merges feedSettings so a partial override doesn't erase defaults.
|
||||
* Precedence (handled by AppProvider): user localStorage > build-time > hardcoded.
|
||||
*/
|
||||
const dittoConfig = parseDittoConfig();
|
||||
const defaultConfig: AppConfig = {
|
||||
...hardcodedConfig,
|
||||
...(typeof __DITTO_CONFIG__ !== "undefined" && __DITTO_CONFIG__
|
||||
? __DITTO_CONFIG__
|
||||
: {}),
|
||||
...dittoConfig,
|
||||
feedSettings: { ...hardcodedConfig.feedSettings, ...dittoConfig.feedSettings },
|
||||
};
|
||||
|
||||
export function App() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode, useLayoutEffect, useEffect, useRef } from 'react';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
import { AppContext, type AppConfig, type AppContextType, type Theme } from '@/contexts/AppContext';
|
||||
import { builtinThemes, themePresets, buildThemeCssFromCore, resolveTheme, resolveThemeConfig, type ThemeConfig, type ThemesConfig } from '@/themes';
|
||||
import { builtinThemes, buildThemeCssFromCore, resolveTheme, resolveThemeConfig, type ThemeConfig, type ThemesConfig } from '@/themes';
|
||||
import { AppConfigSchema } from '@/lib/schemas';
|
||||
import { loadAndApplyFont, loadAndApplyTitleFont } from '@/lib/fontLoader';
|
||||
import { hslToRgb, parseHsl, rgbToHex } from '@/lib/colorUtils';
|
||||
@@ -47,13 +47,6 @@ export function AppProvider(props: AppProviderProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate legacy theme values ("black", "pink") to "custom" + customTheme
|
||||
const legacyTheme = result.theme as string | undefined;
|
||||
if (legacyTheme && legacyTheme in themePresets) {
|
||||
result.theme = 'custom';
|
||||
result.customTheme = { colors: themePresets[legacyTheme].colors };
|
||||
}
|
||||
|
||||
// Migrate legacy blossomServers (string[]) to blossomServerMetadata
|
||||
if (!result.blossomServerMetadata) {
|
||||
const legacyServers = parsed.blossomServers;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { isSyncDone } from "@/hooks/useInitialSync";
|
||||
import { parseBlossomServerList } from "@/lib/appBlossom";
|
||||
import { ACTIVE_THEME_KIND, parseActiveProfileTheme } from "@/lib/themeEvent";
|
||||
import type { ThemeConfig } from "@/themes";
|
||||
import { themePresets } from "@/themes";
|
||||
|
||||
|
||||
/**
|
||||
* NostrSync - Syncs user's Nostr data
|
||||
@@ -285,19 +285,7 @@ export function NostrSync() {
|
||||
const updates = { ...current };
|
||||
|
||||
if (encryptedSettings.theme) {
|
||||
// Migrate legacy theme values ("black", "pink") from older encrypted settings
|
||||
const remoteTheme = encryptedSettings.theme as string;
|
||||
if (remoteTheme in themePresets) {
|
||||
if (
|
||||
current.theme !== "custom" ||
|
||||
JSON.stringify(current.customTheme?.colors) !==
|
||||
JSON.stringify(themePresets[remoteTheme].colors)
|
||||
) {
|
||||
updates.theme = "custom";
|
||||
updates.customTheme = { colors: themePresets[remoteTheme].colors };
|
||||
changed = true;
|
||||
}
|
||||
} else if (encryptedSettings.theme !== current.theme) {
|
||||
if (encryptedSettings.theme !== current.theme) {
|
||||
updates.theme = encryptedSettings.theme;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
+4
-11
@@ -8,11 +8,6 @@ import type { CoreThemeColors, ThemeConfig, ThemesConfig } from '@/themes';
|
||||
/** Zod schema for Theme validation */
|
||||
export const ThemeSchema = z.enum(['dark', 'light', 'system', 'custom']) satisfies z.ZodType<Theme>;
|
||||
|
||||
/**
|
||||
* Accepts current theme values as well as legacy values ("black", "pink")
|
||||
* from older configs. Consumers should migrate legacy values to "custom".
|
||||
*/
|
||||
export const ThemeSchemaCompat = z.enum(['dark', 'light', 'system', 'custom', 'black', 'pink']);
|
||||
|
||||
/** HSL value string like "258 70% 55%" */
|
||||
const HslValue = z.string().regex(/^\d/);
|
||||
@@ -208,10 +203,8 @@ export const SavedFeedSchema = z.object({
|
||||
/**
|
||||
* Zod schema for the full AppConfig stored in localStorage.
|
||||
*
|
||||
* Uses compat sub-schemas (ThemeSchemaCompat, ThemeConfigCompatSchema) so
|
||||
* legacy values parse successfully. Migration from legacy theme values
|
||||
* ("black", "pink") to "custom" + customTheme is handled downstream by
|
||||
* the AppProvider deserializer.
|
||||
* Uses ThemeConfigCompatSchema for the customTheme field so legacy
|
||||
* 19-token color objects still parse successfully.
|
||||
*/
|
||||
export const AppConfigSchema = z.object({
|
||||
appName: z.string().optional(),
|
||||
@@ -220,7 +213,7 @@ export const AppConfigSchema = z.object({
|
||||
clientName: z.string().optional(),
|
||||
client: z.string().optional(),
|
||||
magicMouse: z.boolean().optional(),
|
||||
theme: ThemeSchemaCompat,
|
||||
theme: ThemeSchema,
|
||||
customTheme: ThemeConfigCompatSchema.optional(),
|
||||
autoShareTheme: z.boolean(),
|
||||
themes: ThemesConfigSchema.optional(),
|
||||
@@ -298,7 +291,7 @@ export const ContentFilterSchema = z.object({
|
||||
* Uses looseObject to preserve unknown keys from newer app versions.
|
||||
*/
|
||||
export const EncryptedSettingsSchema = z.looseObject({
|
||||
theme: ThemeSchemaCompat.optional(),
|
||||
theme: ThemeSchema.optional(),
|
||||
customTheme: ThemeConfigCompatSchema.optional(),
|
||||
autoShareTheme: z.boolean().optional(),
|
||||
useAppRelays: z.boolean().optional(),
|
||||
|
||||
Vendored
+2
-6
@@ -15,10 +15,6 @@ interface ImportMetaEnv {
|
||||
readonly COMMIT_SHA: string;
|
||||
/** Git tag for the current commit (e.g., "v2.0.0"). Empty string if untagged (pre-release build). */
|
||||
readonly COMMIT_TAG: string;
|
||||
/** Build-time configuration injected from ditto.json as a JSON string. `"null"` when no config file was provided. */
|
||||
readonly DITTO_CONFIG: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build-time configuration injected by Vite from ditto.json.
|
||||
* `null` when no config file was provided at build time.
|
||||
*/
|
||||
declare const __DITTO_CONFIG__: Partial<import('@/contexts/AppContext').AppConfig> | null;
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ export default defineConfig(() => {
|
||||
...(publicDir ? [mergePublicDir(publicDir)] : []),
|
||||
],
|
||||
define: {
|
||||
__DITTO_CONFIG__: JSON.stringify(dittoConfig ?? null),
|
||||
'import.meta.env.DITTO_CONFIG': JSON.stringify(JSON.stringify(dittoConfig ?? null)),
|
||||
'import.meta.env.VERSION': JSON.stringify(pkg.version),
|
||||
'import.meta.env.BUILD_DATE': JSON.stringify(new Date().toISOString()),
|
||||
'import.meta.env.COMMIT_SHA': JSON.stringify(getCommitSha()),
|
||||
|
||||
Reference in New Issue
Block a user