mirror of
https://github.com/meshtastic/web.git
synced 2025-12-24 00:00:01 -05:00
Added feature flags system (#803)
* added feature flag system * Update packages/web/src/core/services/featureFlags.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove process.env --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,8 @@ import { Outlet } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { MapProvider } from "react-map-gl/maplibre";
|
||||
// Import feature flags and dev overrides
|
||||
import "@core/services/dev-overrides.ts";
|
||||
|
||||
export function App() {
|
||||
const { getDevice } = useDeviceStore();
|
||||
@@ -31,7 +33,6 @@ export function App() {
|
||||
setConnectDialogOpen(open);
|
||||
}}
|
||||
/>
|
||||
{/* <Toaster /> */}
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<DeviceWrapper device={device}>
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
type FooterProps = {
|
||||
@@ -6,6 +7,18 @@ type FooterProps = {
|
||||
};
|
||||
|
||||
const Footer = ({ className, ...props }: FooterProps) => {
|
||||
const version = React.useMemo(
|
||||
() => String(import.meta.env.VITE_VERSION)?.toUpperCase() || "",
|
||||
[],
|
||||
);
|
||||
const commitHash = React.useMemo(
|
||||
() =>
|
||||
String(import.meta.env.VITE_COMMIT_HASH)
|
||||
?.toUpperCase()
|
||||
.slice(0, 7) || "unk",
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
@@ -16,13 +29,13 @@ const Footer = ({ className, ...props }: FooterProps) => {
|
||||
>
|
||||
<div className="px-2">
|
||||
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
|
||||
{String(import.meta.env.VITE_VERSION)?.toUpperCase()}
|
||||
{version}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40 mx-2">
|
||||
-
|
||||
</span>
|
||||
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
|
||||
{`#${String(import.meta.env.VITE_COMMIT_HASH)?.toUpperCase()}`}
|
||||
{`#${commitHash}`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="ml-auto mr-auto text-gray-500 dark:text-gray-400">
|
||||
|
||||
22
packages/web/src/core/hooks/useFeatureFlags.ts
Normal file
22
packages/web/src/core/hooks/useFeatureFlags.ts
Normal file
@@ -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),
|
||||
);
|
||||
}
|
||||
10
packages/web/src/core/services/dev-overrides.ts
Normal file
10
packages/web/src/core/services/dev-overrides.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
89
packages/web/src/core/services/featureFlags.ts
Normal file
89
packages/web/src/core/services/featureFlags.ts
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user