@flagdown/react 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,50 @@
1
1
  # @flagdown/react
2
2
 
3
- Read boolean feature flags from a Flagdown project in React 18+.
3
+ Use the `Feature` component to switch between new UI and a fallback in React 18+.
4
+
5
+ ```sh
6
+ npm install @flagdown/react@0.0.5
7
+ ```
4
8
 
5
9
  ```tsx
6
- import { FlagdownProvider, useFlag } from '@flagdown/react';
10
+ import { Feature } from '@flagdown/react';
7
11
 
8
- function Checkout() {
9
- const enabled = useFlag('new-checkout');
10
- return enabled ? <NewCheckout /> : <CurrentCheckout />;
12
+ export function Checkout() {
13
+ return (
14
+ <Feature
15
+ flag="new-checkout"
16
+ fallback={<OldCheckout />}
17
+ >
18
+ <NewCheckout />
19
+ </Feature>
20
+ );
11
21
  }
22
+ ```
23
+
24
+ `Feature` renders its children when the named flag is on. When it is off, unknown,
25
+ still loading, or unavailable, it renders `fallback`. Omit `fallback` to render
26
+ nothing in those cases. Only the selected branch mounts; `Feature` adds no DOM
27
+ wrapper.
28
+
29
+ Configure the project once at your app root:
30
+
31
+ ```tsx
32
+ import { FlagdownProvider } from '@flagdown/react';
12
33
 
13
34
  <FlagdownProvider
14
- apiUrl="https://your-flagdown-api.example.com"
35
+ apiUrl="https://api.flagdown.dev"
15
36
  clientToken="COPY_PUBLIC_CLIENT_TOKEN_FROM_DASHBOARD"
16
37
  >
17
- <Checkout />
38
+ <App />
18
39
  </FlagdownProvider>
19
40
  ```
20
41
 
21
- Create a project and flag in the dashboard, then copy the generated integration
22
- snippet. `apiUrl` is the Worker base URL without `/v1`. Install the SDK build that
23
- includes the remote adapter; the previously published 0.0.2 build only provides
24
- an injectable evaluator. This source must be released before registry installs
25
- can use remote reads.
42
+ Create a project and flag in the dashboard, then copy its integration snippet.
43
+ `apiUrl` is the API base URL without `/v1`. `Feature` must be inside the provider.
26
44
 
27
45
  Flags refresh every 10 seconds, without reloading the page. Set
28
46
  `refreshIntervalMs` to change this (minimum 1000ms). Requests time out after 10
29
- seconds. Initial values, unknown flags and refresh failures evaluate `false`;
47
+ seconds. Initial values, unknown flags and refresh failures evaluate off;
30
48
  a successful later refresh restores values. An optional `onError(error)` callback
31
49
  can report failures. Changing project configuration immediately discards the
32
50
  previous project's values. Unmounting cancels in-flight work and future refreshes.
@@ -35,14 +53,17 @@ Client tokens are public, project-scoped read configuration. They cannot manage
35
53
  flags. Replacing a token in the dashboard invalidates the previous token; update
36
54
  apps using it. Client-side flags are not an authorization boundary.
37
55
 
38
- For tests or local values, the existing evaluator port remains available:
56
+ `Feature` is the SDK's flag consumption API. The evaluation context and evaluator
57
+ are internal; the package exposes no flag hooks or direct evaluation functions.
58
+ Use `FlagdownProvider` to configure your project. A provider without project
59
+ configuration renders each feature's fallback.
39
60
 
40
- ```tsx
41
- <FlagdownProvider evaluator={{ isEnabled: key => key === 'new-checkout' }}>
42
- <Checkout />
43
- </FlagdownProvider>
44
- ```
61
+ API transport types are generated from `openapi/flagdown-api.json` with
62
+ `openapi-typescript` before SDK typechecking and building. They stay internal;
63
+ the public exports remain `Feature`, `FlagdownProvider` and their prop types.
64
+ The runtime still rejects malformed or non-boolean flag values.
45
65
 
46
- An explicit evaluator takes precedence over remote configuration. A provider
47
- without either configuration evaluates every flag off. `useFlag` must be called
48
- inside a provider.
66
+ For SDK development, run `npm run --workspace @flagdown/react generate:types`
67
+ to regenerate `src/generated/api.d.ts`. This file is ignored by git and is never
68
+ the source of truth. Backend CI checks candidate contracts against the current
69
+ SDK before publishing the generated specification to this repository.
package/dist/index.d.ts CHANGED
@@ -1,20 +1,15 @@
1
1
  import * as react from 'react';
2
- import { PropsWithChildren } from 'react';
2
+ import { PropsWithChildren, ReactNode } from 'react';
3
3
 
4
- /** A port for resolving a feature flag's enabled state. */
5
- interface FlagEvaluator {
6
- isEnabled(flagKey: string): boolean;
7
- }
8
- /**
9
- * Temporary adapter used until the SDK is connected to the Flagdown API.
10
- *
11
- * It deliberately keeps every flag disabled so applications can install the
12
- * provider and hook safely before remote evaluation is available.
13
- */
14
- declare const disabledFlagEvaluator: FlagEvaluator;
4
+ type FeatureProps = PropsWithChildren<{
5
+ /** Key of the flag controlling this feature. */
6
+ flag: string;
7
+ /** Rendered while the flag is off, unknown, loading, or unavailable. */
8
+ fallback?: ReactNode;
9
+ }>;
10
+ declare function Feature({ flag, fallback, children }: FeatureProps): react.JSX.Element;
15
11
 
16
12
  interface FlagdownProviderProps extends PropsWithChildren {
17
- evaluator?: FlagEvaluator;
18
13
  apiUrl?: string;
19
14
  /** Public, read-only project token copied from the dashboard. */
20
15
  clientToken?: string;
@@ -22,8 +17,6 @@ interface FlagdownProviderProps extends PropsWithChildren {
22
17
  refreshIntervalMs?: number;
23
18
  onError?: (error: Error) => void;
24
19
  }
25
- declare function FlagdownProvider({ children, evaluator, apiUrl, clientToken, refreshIntervalMs, onError }: FlagdownProviderProps): react.JSX.Element;
26
-
27
- declare function useFlag(flagKey: string): boolean;
20
+ declare function FlagdownProvider({ children, apiUrl, clientToken, refreshIntervalMs, onError }: FlagdownProviderProps): react.JSX.Element;
28
21
 
29
- export { type FlagEvaluator, FlagdownProvider, type FlagdownProviderProps, disabledFlagEvaluator, useFlag };
22
+ export { Feature, type FeatureProps, FlagdownProvider, type FlagdownProviderProps };
package/dist/index.js CHANGED
@@ -1,47 +1,60 @@
1
- // src/FlagdownProvider.tsx
2
- import { useEffect, useMemo, useState, useRef } from "react";
1
+ // src/Feature.tsx
2
+ import { useContext } from "react";
3
3
 
4
4
  // src/FlagdownContext.tsx
5
- import { createContext, useContext } from "react";
5
+ import { createContext } from "react";
6
6
  var FlagdownContext = createContext(void 0);
7
- function useFlag(flagKey) {
7
+
8
+ // src/Feature.tsx
9
+ import { Fragment, jsx } from "react/jsx-runtime";
10
+ function Feature({ flag, fallback = null, children }) {
8
11
  const evaluator = useContext(FlagdownContext);
9
- if (!evaluator) {
10
- throw new Error("useFlag must be used within a FlagdownProvider");
11
- }
12
- return evaluator.isEnabled(flagKey);
12
+ if (!evaluator) throw new Error("Feature must be used within a FlagdownProvider");
13
+ return /* @__PURE__ */ jsx(Fragment, { children: evaluator.isEnabled(flag) ? children : fallback });
13
14
  }
14
15
 
16
+ // src/FlagdownProvider.tsx
17
+ import { useEffect, useMemo, useState, useRef } from "react";
18
+
15
19
  // src/flags.ts
16
20
  var disabledFlagEvaluator = {
17
21
  isEnabled: () => false
18
22
  };
19
23
 
24
+ // src/api-types.ts
25
+ var clientFlagsPath = "/v1/client/flags";
26
+
20
27
  // src/remote.ts
21
28
  async function fetchFlagState(apiUrl, clientToken, signal) {
22
- const response = await fetch(apiUrl.replace(/\/$/, "") + "/v1/client/flags", {
23
- headers: { "x-flagdown-client-token": clientToken },
29
+ const headers = { "x-flagdown-client-token": clientToken };
30
+ const response = await fetch(apiUrl.replace(/\/$/, "") + clientFlagsPath, {
31
+ headers,
24
32
  cache: "no-store",
25
33
  signal
26
34
  });
27
35
  if (!response.ok) throw new Error(`Flagdown request failed (${response.status})`);
28
36
  const data = await response.json();
29
- if (!data || typeof data !== "object" || !("flags" in data) || !data.flags || typeof data.flags !== "object" || Array.isArray(data.flags) || Object.values(data.flags).some((value) => typeof value !== "boolean")) {
37
+ if (!data || typeof data !== "object" || !("flags" in data) || !data.flags || typeof data.flags !== "object" || Array.isArray(data.flags)) {
30
38
  throw new Error("Invalid Flagdown response");
31
39
  }
32
- return data.flags;
40
+ const flags = Object.fromEntries(Object.entries(data.flags).map(([key, enabled]) => {
41
+ if (typeof enabled !== "boolean") throw new Error("Invalid Flagdown response");
42
+ return [key, enabled];
43
+ }));
44
+ const result = { flags };
45
+ return result.flags;
33
46
  }
34
47
 
35
48
  // src/FlagdownProvider.tsx
36
- import { jsx } from "react/jsx-runtime";
37
- function FlagdownProvider({ children, evaluator, apiUrl, clientToken, refreshIntervalMs = 1e4, onError }) {
49
+ import { jsx as jsx2 } from "react/jsx-runtime";
50
+ function FlagdownProvider({ children, apiUrl, clientToken, refreshIntervalMs = 1e4, onError }) {
38
51
  const errorHandler = useRef(onError);
39
52
  useEffect(() => {
40
53
  errorHandler.current = onError;
41
54
  }, [onError]);
42
55
  const [snapshot, setSnapshot] = useState();
43
56
  useEffect(() => {
44
- if (evaluator || !apiUrl || !clientToken) return;
57
+ if (!apiUrl || !clientToken) return;
45
58
  let active = true;
46
59
  let timer;
47
60
  let controller;
@@ -70,12 +83,11 @@ function FlagdownProvider({ children, evaluator, apiUrl, clientToken, refreshInt
70
83
  clearTimeout(timer);
71
84
  controller?.abort();
72
85
  };
73
- }, [apiUrl, clientToken, evaluator, refreshIntervalMs]);
74
- const value = useMemo(() => evaluator || (snapshot && snapshot.apiUrl === apiUrl && snapshot.clientToken === clientToken ? { isEnabled: (key) => Object.hasOwn(snapshot.flags, key) && snapshot.flags[key] === true } : disabledFlagEvaluator), [evaluator, snapshot, apiUrl, clientToken]);
75
- return /* @__PURE__ */ jsx(FlagdownContext.Provider, { value, children });
86
+ }, [apiUrl, clientToken, refreshIntervalMs]);
87
+ const value = useMemo(() => snapshot && snapshot.apiUrl === apiUrl && snapshot.clientToken === clientToken ? { isEnabled: (key) => Object.hasOwn(snapshot.flags, key) && snapshot.flags[key] === true } : disabledFlagEvaluator, [snapshot, apiUrl, clientToken]);
88
+ return /* @__PURE__ */ jsx2(FlagdownContext.Provider, { value, children });
76
89
  }
77
90
  export {
78
- FlagdownProvider,
79
- disabledFlagEvaluator,
80
- useFlag
91
+ Feature,
92
+ FlagdownProvider
81
93
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flagdown/react",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "React SDK for evaluating Flagdown feature flags.",
@@ -28,9 +28,10 @@
28
28
  "LICENSE"
29
29
  ],
30
30
  "scripts": {
31
- "build": "tsup src/index.ts --format esm --dts --clean",
31
+ "generate:types": "node scripts/generate-api-types.mjs",
32
+ "build": "node scripts/generate-api-types.mjs && tsup src/index.ts --format esm --dts --clean",
32
33
  "test": "bun test",
33
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "node scripts/generate-api-types.mjs && tsc --noEmit"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "react": ">=18",
@@ -38,6 +39,7 @@
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/react": "^18",
42
+ "openapi-typescript": "7.13.0",
41
43
  "react": "^18",
42
44
  "react-dom": "^18",
43
45
  "tsup": "^8",