Browse Source
* added feature flag system * Update packages/web/src/core/services/featureFlags.ts Co-authored-by: Copilot <[email protected]> * remove process.env --------- Co-authored-by: Copilot <[email protected]>pull/804/head
committed by
GitHub
5 changed files with 138 additions and 3 deletions
@ -0,0 +1,22 @@ |
|||
import { |
|||
type FlagKey, |
|||
type Flags, |
|||
featureFlags, |
|||
} from "@core/services/featureFlags.ts"; |
|||
import * as React from "react"; |
|||
|
|||
export function useFeatureFlags(): Flags { |
|||
return React.useSyncExternalStore( |
|||
(cb) => featureFlags.subscribe(cb), |
|||
() => featureFlags.all(), |
|||
() => featureFlags.all(), |
|||
); |
|||
} |
|||
|
|||
export function useFeatureFlag(key: FlagKey): boolean { |
|||
return React.useSyncExternalStore( |
|||
(cb) => featureFlags.subscribe(cb), |
|||
() => featureFlags.get(key), |
|||
() => featureFlags.get(key), |
|||
); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import { featureFlags } from "@core/services/featureFlags"; |
|||
|
|||
const isDev = typeof import.meta !== "undefined" && import.meta.env?.DEV; |
|||
console.log(`Dev mode: ${isDev}`); |
|||
|
|||
if (isDev) { |
|||
featureFlags.setOverrides({ |
|||
persistNodeDB: true, |
|||
}); |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
import { z } from "zod"; |
|||
|
|||
/** Map feature keys -> env var names (Vite exposes only VITE_*). */ |
|||
export const FLAG_ENV = { |
|||
persistNodeDB: "VITE_PERSIST_NODE_DB", |
|||
persistMessages: "VITE_PERSIST_MESSAGES", |
|||
} as const; |
|||
|
|||
export type FlagKey = keyof typeof FLAG_ENV; |
|||
export type Flags = Record<FlagKey, boolean>; |
|||
|
|||
type Listener = () => void; |
|||
|
|||
const present = z |
|||
.string() |
|||
.optional() |
|||
.transform((v) => v !== undefined); |
|||
|
|||
const mutableShape: Record<string, z.ZodTypeAny> = {}; |
|||
for (const envName of Object.values(FLAG_ENV)) { |
|||
mutableShape[envName] = present; |
|||
} |
|||
const EnvSchema = z.object(mutableShape); |
|||
|
|||
class FeatureFlags { |
|||
private base: Flags; |
|||
private overrides: Partial<Flags> = {}; |
|||
private listeners = new Set<Listener>(); |
|||
|
|||
constructor(base: Flags) { |
|||
this.base = base; |
|||
} |
|||
|
|||
get(key: FlagKey): boolean { |
|||
return this.overrides[key] ?? this.base[key]; |
|||
} |
|||
|
|||
/** Get all flags */ |
|||
all(): Flags { |
|||
return { ...this.base, ...this.overrides }; |
|||
} |
|||
|
|||
/** Optional dev/test override. Pass null to clear. */ |
|||
setOverride(key: FlagKey, val: boolean | null) { |
|||
if (val === null) { |
|||
delete this.overrides[key]; |
|||
} else { |
|||
this.overrides[key] = val; |
|||
} |
|||
this.emit(); |
|||
} |
|||
|
|||
/** Set many at once */ |
|||
setOverrides(partial: Partial<Flags>) { |
|||
for (const [k, v] of Object.entries(partial)) { |
|||
this.setOverride(k as FlagKey, v as boolean); |
|||
if (v === null) { |
|||
delete this.overrides[k as FlagKey]; |
|||
} else { |
|||
this.overrides[k as FlagKey] = v as boolean; |
|||
} |
|||
} |
|||
this.emit(); |
|||
} |
|||
|
|||
subscribe(fn: Listener) { |
|||
this.listeners.add(fn); |
|||
return () => this.listeners.delete(fn); |
|||
} |
|||
|
|||
private emit() { |
|||
for (const listener of this.listeners) { |
|||
listener(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export function createFeatureFlags(env: Record<string, unknown>): FeatureFlags { |
|||
const parsed = EnvSchema.parse(env); |
|||
const base = Object.fromEntries( |
|||
(Object.keys(FLAG_ENV) as FlagKey[]).map((k) => [ |
|||
k, |
|||
parsed[FLAG_ENV[k]] as boolean, |
|||
]), |
|||
) as Flags; |
|||
return new FeatureFlags(base); |
|||
} |
|||
|
|||
export const featureFlags = createFeatureFlags(import.meta.env); |
|||
Loading…
Reference in new issue