/// import React from "react"; import { Flags, FlagScalar, GetValueFromKeyPath, GetValueFromKeyPathString, KeyPath, KeyPathString } from "./types"; import { Backend } from "./backends"; const MISSING_CONTEXT = Symbol(); const NOOP = () => null; const isFlagScalar = (value: any): value is FlagScalar => { return typeof value === "string" || typeof value === "number" || typeof value === "boolean"; }; export const createFlags = () => { type B = Backend; type ProviderProps = React.PropsWithChildren<{ backend: B; }>; type KeyPathStringFlagProps> = { keyPath: K; defaultValue: GetValueFromKeyPathString; render(value: GetValueFromKeyPathString): React.ReactNode; fallback?(): React.ReactNode; }; type KeyPathFlagProps> = { keyPath: KP; defaultValue: GetValueFromKeyPath; render(value: GetValueFromKeyPath): React.ReactNode; fallback?(): React.ReactNode; }; const calleeStr = (keyPath: string[], defaultValue: any, format: "hook" | "component") => () => { const keyPathStr = JSON.stringify(keyPath); const defaultValueStr = JSON.stringify(defaultValue); return format == "hook" ? `useFlag(${keyPathStr}, ${defaultValueStr})` : ``; }; const Context = React.createContext(MISSING_CONTEXT); Context.displayName = "Flag"; const FlagBackendProvider: React.FC = ({ backend, children }) => { return {children}; }; FlagBackendProvider.displayName = "FlagBackendProvider"; const internalUseFlag = (keyPath: string | string[], defaultValue: any, displayCallee: () => string) => { const keyPath_ = (Array.isArray(keyPath) ? keyPath : keyPath.split(".")) as KeyPath; if (defaultValue === undefined) { throw new Error(`Calling \`${displayCallee()}\` requires that you provide a default value that matches the type of the flag.`); } const expectedType = typeof defaultValue; const backend = React.useContext(Context); if (backend === MISSING_CONTEXT) { if (process.env.NODE_ENV !== "development") { return defaultValue; } throw new Error(`Calling \`${displayCallee()}\` requires that the application is wrapped in a \`\``); } const ext = backend.toExternalStore(keyPath_, defaultValue); let result = React.useSyncExternalStore(ext.subscribe, ext.getSnapshot, ext.getServerSnapshot); if ((result === undefined || result === null) && process.env.NODE_ENV === "development") { console.warn(`\`${displayCallee()}\` does not return anything from backend "${backend.name}".`); } result ??= defaultValue; if (!isFlagScalar(result)) { throw new Error( `Calling \`${displayCallee()}\` requires that the result is a boolean, number or string. Instead returned ${JSON.stringify( result )}.` ); } if (typeof result !== expectedType) { if (process.env.NODE_ENV === "development") { console.warn( `Expected result of \`${displayCallee()}\` to be a ${expectedType} (based on the default value of ${JSON.stringify( defaultValue )}). Instead returned ${JSON.stringify(result)}. Falling back to default value.` ); } return defaultValue; } return result; }; function Flag>(props: KeyPathStringFlagProps): JSX.Element; function Flag>(props: KeyPathFlagProps): JSX.Element; function Flag({ keyPath, defaultValue, render, fallback }: any): JSX.Element { fallback ??= NOOP; const flag = internalUseFlag(keyPath, defaultValue, calleeStr(keyPath, defaultValue, "component")); return flag === false ? fallback() : render(flag); } Flag.displayName = "Flag"; function useFlag>(keyPath: K, defaultValue: GetValueFromKeyPathString): GetValueFromKeyPathString; function useFlag>(keyPath: KP, defaultValue: GetValueFromKeyPath): GetValueFromKeyPath; function useFlag(keyPath: any, defaultValue: any) { React.useDebugValue(keyPath); return internalUseFlag(keyPath, defaultValue, calleeStr(keyPath, defaultValue, "hook")); } return { FlagBackendProvider, Flag, useFlag, }; };