@flagward/react 0.2.0
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/LICENSE +21 -0
- package/README.md +196 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +73 -0
- package/dist/provider.js.map +1 -0
- package/dist/useFlag.d.ts +9 -0
- package/dist/useFlag.d.ts.map +1 -0
- package/dist/useFlag.js +30 -0
- package/dist/useFlag.js.map +1 -0
- package/dist/useFlags.d.ts +9 -0
- package/dist/useFlags.d.ts.map +1 -0
- package/dist/useFlags.js +28 -0
- package/dist/useFlags.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Suárez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# @flagward/react
|
|
2
|
+
|
|
3
|
+
React SDK for Flagward - Feature Flags as a Service
|
|
4
|
+
|
|
5
|
+
> Published as `flagward-sdk-react` up to 0.2.0. Same package, same API — it
|
|
6
|
+
> moved into the `@flagward` scope so every SDK in this family shares one
|
|
7
|
+
> namespace the project actually owns. Change the name in your `package.json`
|
|
8
|
+
> and the import; nothing else.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @flagward/react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { FlagwardProvider, useFlag } from "@flagward/react";
|
|
20
|
+
|
|
21
|
+
function App() {
|
|
22
|
+
return (
|
|
23
|
+
<FlagwardProvider apiKey="your-api-key">
|
|
24
|
+
<Dashboard />
|
|
25
|
+
</FlagwardProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Dashboard() {
|
|
30
|
+
const { value: showNewUI } = useFlag("show-new-ui");
|
|
31
|
+
|
|
32
|
+
return showNewUI ? <NewDashboard /> : <OldDashboard />;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Module format
|
|
37
|
+
|
|
38
|
+
This package ships as **ESM only** (`"type": "module"`, with an `exports` map
|
|
39
|
+
pointing at `dist/index.js`). It has no CommonJS build: `require("@flagward/react")`
|
|
40
|
+
fails with a clear `ERR_PACKAGE_PATH_NOT_EXPORTED`/`ERR_REQUIRE_ESM` error
|
|
41
|
+
rather than silently loading broken code. Next.js, Vite, and any other
|
|
42
|
+
bundler-based toolchain resolve ESM packages natively, which covers the
|
|
43
|
+
realistic ways a React SDK gets consumed. If you have a pure CommonJS
|
|
44
|
+
build pipeline with no ESM support, `import()` the package dynamically or
|
|
45
|
+
open an issue.
|
|
46
|
+
|
|
47
|
+
## How flags are evaluated
|
|
48
|
+
|
|
49
|
+
This SDK evaluates **locally, in the browser**. On startup it downloads the
|
|
50
|
+
full set of flags and their targeting rules for the environment your API key
|
|
51
|
+
belongs to, then evaluates each flag against the rules on the client. A
|
|
52
|
+
Server-Sent Events (SSE) stream keeps that local copy fresh as flags change on
|
|
53
|
+
the server, without a page reload (see "Losing the network" below for what
|
|
54
|
+
happens when that stream drops).
|
|
55
|
+
|
|
56
|
+
This has a direct consequence you must know before writing a targeting rule:
|
|
57
|
+
**the rules are visible in the browser.** Anything you put in a rule
|
|
58
|
+
(condition values, user attribute names, percentages, etc.) is downloaded as
|
|
59
|
+
plain JSON to every client and can be read by opening devtools. Do not encode
|
|
60
|
+
secrets, internal identifiers you don't want exposed, or anything
|
|
61
|
+
security-sensitive in a flag's targeting rules — treat them the same way you'd
|
|
62
|
+
treat any other client-side configuration.
|
|
63
|
+
|
|
64
|
+
## Hooks
|
|
65
|
+
|
|
66
|
+
### `useFlag(key)`
|
|
67
|
+
|
|
68
|
+
Evaluates a single flag by key.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const { value, isLoading, error } = useFlag("new-dashboard");
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
- `value`: `boolean | string | undefined`
|
|
76
|
+
- `isLoading`: `boolean`
|
|
77
|
+
- `error`: `Error | null`
|
|
78
|
+
|
|
79
|
+
### `useFlags()`
|
|
80
|
+
|
|
81
|
+
Returns all flags and helper functions.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
const { flags, getFlag, isLoading } = useFlags();
|
|
85
|
+
|
|
86
|
+
const showBanner = getFlag("show-banner");
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Provider
|
|
90
|
+
|
|
91
|
+
Wrap your app with `FlagwardProvider`:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<FlagwardProvider
|
|
95
|
+
apiKey="your-api-key"
|
|
96
|
+
host="http://localhost:8000" // optional, defaults to localhost:8000
|
|
97
|
+
context={{ userId: "123" }} // optional, used to evaluate targeting rules
|
|
98
|
+
logLevel="warn" // optional, see Error reporting below
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</FlagwardProvider>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Losing the network
|
|
105
|
+
|
|
106
|
+
Flags are read once and evaluated locally, so a client that loses its
|
|
107
|
+
connection keeps working: every flag answers from its last known value.
|
|
108
|
+
|
|
109
|
+
What it cannot do while disconnected is notice a change. When the browser
|
|
110
|
+
reports the network is back, the SDK re-reads the flags and reopens the update
|
|
111
|
+
stream if the browser had given up on it, so a change made during the outage is
|
|
112
|
+
picked up rather than waiting for the next reload.
|
|
113
|
+
|
|
114
|
+
A stream that is still open is left alone: replacing a working connection would
|
|
115
|
+
drop events for no reason.
|
|
116
|
+
|
|
117
|
+
Returning to a backgrounded tab does the same. A machine waking from sleep
|
|
118
|
+
drops its connection without the browser ever reporting the network as gone, so
|
|
119
|
+
`online` never fires and the tab comes back holding whatever it knew before it
|
|
120
|
+
slept. Both signals share one in-flight refresh, so arriving together does not
|
|
121
|
+
produce two.
|
|
122
|
+
|
|
123
|
+
## Error reporting
|
|
124
|
+
|
|
125
|
+
The SDK never throws into your application. A rejected API key, an unreachable
|
|
126
|
+
server or an unknown flag all resolve to `undefined`, so your own fallback
|
|
127
|
+
decides what the user sees and the page keeps rendering.
|
|
128
|
+
|
|
129
|
+
Because none of that surfaces on its own, the SDK reports it to the console
|
|
130
|
+
instead:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
[Flagward] The API key was rejected. Check that it matches an environment in
|
|
134
|
+
your Flagward dashboard.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Each distinct problem is reported once per page load, so a component that
|
|
138
|
+
re-renders a hundred times does not produce a hundred lines.
|
|
139
|
+
|
|
140
|
+
`logLevel` controls how much is reported:
|
|
141
|
+
|
|
142
|
+
| Value | Reports |
|
|
143
|
+
|-------|---------|
|
|
144
|
+
| `"warn"` (default) | everything: unknown flags, dropped streams, network failures |
|
|
145
|
+
| `"error"` | only what cannot work at all: a missing or rejected API key, a missing provider |
|
|
146
|
+
| `"silent"` | nothing |
|
|
147
|
+
|
|
148
|
+
## The core
|
|
149
|
+
|
|
150
|
+
Everything that is not React lives in
|
|
151
|
+
[`@flagward/core`](https://www.npmjs.com/package/@flagward/core) — the client,
|
|
152
|
+
the rule evaluator and the reporting — and is re-exported from this package, so
|
|
153
|
+
installing this one is enough. Reach for the core directly only outside React.
|
|
154
|
+
|
|
155
|
+
## Standalone Client
|
|
156
|
+
|
|
157
|
+
Use the client directly without React:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { FlagwardClient } from "@flagward/react";
|
|
161
|
+
|
|
162
|
+
const client = new FlagwardClient({
|
|
163
|
+
apiKey: "your-api-key",
|
|
164
|
+
host: "http://localhost:8000",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await client.init();
|
|
168
|
+
|
|
169
|
+
// Every flag's configured on/off state, from the local snapshot.
|
|
170
|
+
// Targeting rules are NOT applied here -- there is no user context to
|
|
171
|
+
// apply them against.
|
|
172
|
+
const flags = client.cachedFlags;
|
|
173
|
+
|
|
174
|
+
// One flag with targeting rules applied against the context you pass.
|
|
175
|
+
// Throws if the key does not exist in this environment.
|
|
176
|
+
const showBanner = client.evaluate("show-banner", { plan: "premium" });
|
|
177
|
+
|
|
178
|
+
// The same, without context and without throwing: an unknown key reads
|
|
179
|
+
// as undefined and your own fallback decides what happens.
|
|
180
|
+
const maintenance = client.getFlag("maintenance-mode");
|
|
181
|
+
|
|
182
|
+
// Optional: keep the snapshot fresh via the SSE stream.
|
|
183
|
+
client.connect();
|
|
184
|
+
|
|
185
|
+
// Close it when you are done -- an open stream holds a connection.
|
|
186
|
+
client.disconnect();
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`cachedFlags` and `evaluate` answer different questions. The first says how a
|
|
190
|
+
flag is configured; the second says what it resolves to for a particular user.
|
|
191
|
+
Reaching for `cachedFlags` when you meant `evaluate` silently ignores every
|
|
192
|
+
targeting rule on the flag.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FlagwardClient } from "@flagward/core";
|
|
2
|
+
import type { FlagDataMap, UserContext } from "@flagward/core";
|
|
3
|
+
export interface FlagwardContextValue {
|
|
4
|
+
client: FlagwardClient | null;
|
|
5
|
+
/** The flag data this render was produced from. */
|
|
6
|
+
flagsData: FlagDataMap;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
context: UserContext;
|
|
10
|
+
}
|
|
11
|
+
export declare const FlagwardContext: import("react").Context<FlagwardContextValue>;
|
|
12
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,mDAAmD;IACnD,SAAS,EAAE,WAAW,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,+CAM1B,CAAC"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAatC,MAAM,CAAC,MAAM,eAAe,GAAG,aAAa,CAAuB;IACjE,MAAM,EAAE,IAAI;IACZ,SAAS,EAAE,EAAE;IACb,SAAS,EAAE,KAAK;IAChB,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,EAAE;CACZ,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { FlagwardClient, evaluateFlag, toFlagMap, } from "@flagward/core";
|
|
2
|
+
export type { Condition, Flag, FlagData, FlagDataMap, FlagMap, FlagwardClientOptions, LogLevel, Logger, Rule, UserContext, } from "@flagward/core";
|
|
3
|
+
export { FlagwardProvider } from "./provider.js";
|
|
4
|
+
export type { FlagwardProviderProps } from "./provider.js";
|
|
5
|
+
export { useFlag } from "./useFlag.js";
|
|
6
|
+
export type { UseFlagResult } from "./useFlag.js";
|
|
7
|
+
export { useFlags } from "./useFlags.js";
|
|
8
|
+
export type { UseFlagsResult } from "./useFlags.js";
|
|
9
|
+
export { SDK_VERSION } from "./version.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,SAAS,GACV,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,WAAW,EACX,OAAO,EACP,qBAAqB,EACrB,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The framework-agnostic core, re-exported so a consumer of this package never
|
|
2
|
+
// has to install @flagward/core to reach the client, the evaluator or the types.
|
|
3
|
+
export { FlagwardClient, evaluateFlag, toFlagMap, } from "@flagward/core";
|
|
4
|
+
// Provider
|
|
5
|
+
export { FlagwardProvider } from "./provider.js";
|
|
6
|
+
// Hooks
|
|
7
|
+
export { useFlag } from "./useFlag.js";
|
|
8
|
+
export { useFlags } from "./useFlags.js";
|
|
9
|
+
// The version this adapter registers with
|
|
10
|
+
export { SDK_VERSION } from "./version.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,iFAAiF;AACjF,OAAO,EACL,cAAc,EACd,YAAY,EACZ,SAAS,GACV,MAAM,gBAAgB,CAAC;AAcxB,WAAW;AACX,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD,QAAQ;AACR,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,0CAA0C;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LogLevel, UserContext } from "@flagward/core";
|
|
2
|
+
export interface FlagwardProviderProps {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
context?: UserContext;
|
|
6
|
+
/** How much the SDK reports to the console. Defaults to "warn". */
|
|
7
|
+
logLevel?: LogLevel;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function FlagwardProvider({ apiKey, host, context: userContext, logLevel, children, }: FlagwardProviderProps): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,QAAQ,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAczE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,IAAI,EACJ,OAAO,EAAE,WAAgB,EACzB,QAAQ,EACR,QAAQ,GACT,EAAE,qBAAqB,+BAsEvB"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { FlagwardClient } from "@flagward/core";
|
|
4
|
+
import { FlagwardContext } from "./context.js";
|
|
5
|
+
import { SDK_VERSION } from "./version.js";
|
|
6
|
+
/**
|
|
7
|
+
* How this adapter names itself at registration.
|
|
8
|
+
*
|
|
9
|
+
* Not yet one of the server's stored choices, which is why the dashboard shows
|
|
10
|
+
* it as its own row rather than folding it into JavaScript. It is named here
|
|
11
|
+
* so that, once the server records adapters, a React application is already
|
|
12
|
+
* distinguishable from a Vue one without changing what anybody has installed.
|
|
13
|
+
*/
|
|
14
|
+
const SDK_TYPE = "REACT";
|
|
15
|
+
export function FlagwardProvider({ apiKey, host, context: userContext = {}, logLevel, children, }) {
|
|
16
|
+
const [client] = useState(() => new FlagwardClient({
|
|
17
|
+
apiKey,
|
|
18
|
+
host,
|
|
19
|
+
logLevel,
|
|
20
|
+
sdkType: SDK_TYPE,
|
|
21
|
+
sdkVersion: SDK_VERSION,
|
|
22
|
+
}));
|
|
23
|
+
// The flag data lives in React state, not in the client's mutable field,
|
|
24
|
+
// so what a hook renders is what triggered the render.
|
|
25
|
+
const [flagsData, setFlagsData] = useState({});
|
|
26
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
27
|
+
const [error, setError] = useState(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const initClient = async () => {
|
|
30
|
+
try {
|
|
31
|
+
await client.init();
|
|
32
|
+
setFlagsData(client.snapshot);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// Startup failing is never fatal to the host application: the tree
|
|
36
|
+
// still renders and every flag reads as undefined, so each caller's
|
|
37
|
+
// own fallback decides what the user sees.
|
|
38
|
+
client.logger.warn("init-failed", "Could not load flags, so every flag falls back to undefined and " +
|
|
39
|
+
"your own defaults apply. The cause is reported above.");
|
|
40
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
// Outside the try on purpose: a startup that failed is exactly when
|
|
44
|
+
// the stream and the connectivity watcher matter most, and leaving
|
|
45
|
+
// them behind the success path meant a client that started offline
|
|
46
|
+
// could never recover.
|
|
47
|
+
client.connect();
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
initClient();
|
|
52
|
+
const unsubscribe = client.subscribe((snapshot) => {
|
|
53
|
+
setFlagsData(snapshot);
|
|
54
|
+
// Flags arriving means the client is talking to the server again. Leaving
|
|
55
|
+
// a past failure in state would keep every consumer that checks `error`
|
|
56
|
+
// showing a problem that is over.
|
|
57
|
+
setError(null);
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
unsubscribe();
|
|
61
|
+
client.destroy();
|
|
62
|
+
};
|
|
63
|
+
}, [client]);
|
|
64
|
+
const value = {
|
|
65
|
+
client,
|
|
66
|
+
flagsData,
|
|
67
|
+
isLoading,
|
|
68
|
+
error,
|
|
69
|
+
context: userContext,
|
|
70
|
+
};
|
|
71
|
+
return (_jsx(FlagwardContext.Provider, { value: value, children: children }));
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAA6B,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,OAAO,CAAC;AAWzB,MAAM,UAAU,gBAAgB,CAAC,EAC/B,MAAM,EACN,IAAI,EACJ,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,QAAQ,EACR,QAAQ,GACc;IACtB,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CACvB,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC;QACvB,MAAM;QACN,IAAI;QACJ,QAAQ;QACR,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,WAAW;KACxB,CAAC,CACH,CAAC;IACF,yEAAyE;IACzE,uDAAuD;IACvD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAc,EAAE,CAAC,CAAC;IAC5D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAEvD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE;YAC5B,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBACpB,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAChC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,mEAAmE;gBACnE,oEAAoE;gBACpE,2CAA2C;gBAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAChB,aAAa,EACb,kEAAkE;oBAChE,uDAAuD,CAC1D,CAAC;gBACF,QAAQ,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAChE,CAAC;oBAAS,CAAC;gBACT,oEAAoE;gBACpE,mEAAmE;gBACnE,mEAAmE;gBACnE,uBAAuB;gBACvB,MAAM,CAAC,OAAO,EAAE,CAAC;gBACjB,YAAY,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACH,CAAC,CAAC;QAEF,UAAU,EAAE,CAAC;QAEb,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE;YAChD,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvB,0EAA0E;YAC1E,wEAAwE;YACxE,kCAAkC;YAClC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,WAAW,EAAE,CAAC;YACd,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,KAAK,GAAyB;QAClC,MAAM;QACN,SAAS;QACT,SAAS;QACT,KAAK;QACL,OAAO,EAAE,WAAW;KACrB,CAAC;IAEF,OAAO,CACL,KAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACnC,QAAQ,GACgB,CAC5B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UserContext } from "@flagward/core";
|
|
2
|
+
export interface UseFlagResult {
|
|
3
|
+
/** The flag's value, or undefined while loading or if it does not exist. */
|
|
4
|
+
value: boolean | undefined;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
error: Error | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function useFlag(key: string, flagContext?: UserContext): UseFlagResult;
|
|
9
|
+
//# sourceMappingURL=useFlag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFlag.d.ts","sourceRoot":"","sources":["../src/useFlag.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,aAAa,CAmC7E"}
|
package/dist/useFlag.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { FlagwardContext } from "./context.js";
|
|
3
|
+
import { createLogger, evaluateFlag } from "@flagward/core";
|
|
4
|
+
export function useFlag(key, flagContext) {
|
|
5
|
+
const context = useContext(FlagwardContext);
|
|
6
|
+
const logger = context.client?.logger ?? createLogger();
|
|
7
|
+
// Merge provider context with flag-specific context
|
|
8
|
+
const mergedContext = { ...context.context, ...flagContext };
|
|
9
|
+
let value;
|
|
10
|
+
if (!context.client) {
|
|
11
|
+
logger.error("no-provider", `useFlag("${key}") was called outside FlagwardProvider, so it can only ` +
|
|
12
|
+
"return undefined. Wrap the tree in <FlagwardProvider>.");
|
|
13
|
+
}
|
|
14
|
+
else if (!context.isLoading) {
|
|
15
|
+
// Evaluated against the data this render was produced from, so the value
|
|
16
|
+
// shown and the update that caused it can never disagree.
|
|
17
|
+
value = evaluateFlag(context.flagsData[key], mergedContext);
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
logger.warn(`unknown-flag:${key}`, `Flag "${key}" is not in this environment, so it reads as undefined. ` +
|
|
20
|
+
"Check the key, and that the flag exists in the environment this " +
|
|
21
|
+
"API key belongs to.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
value,
|
|
26
|
+
isLoading: context.isLoading,
|
|
27
|
+
error: context.error,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=useFlag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFlag.js","sourceRoot":"","sources":["../src/useFlag.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAU5D,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,WAAyB;IAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,IAAI,YAAY,EAAE,CAAC;IAExD,oDAAoD;IACpD,MAAM,aAAa,GAAG,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,WAAW,EAAE,CAAC;IAE7D,IAAI,KAA0B,CAAC;IAE/B,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,KAAK,CACV,aAAa,EACb,YAAY,GAAG,yDAAyD;YACtE,wDAAwD,CAC3D,CAAC;IACJ,CAAC;SAAM,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QAC9B,yEAAyE;QACzE,0DAA0D;QAC1D,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC,CAAC;QAE5D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CACT,gBAAgB,GAAG,EAAE,EACrB,SAAS,GAAG,0DAA0D;gBACpE,kEAAkE;gBAClE,qBAAqB,CACxB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FlagMap, UserContext } from "@flagward/core";
|
|
2
|
+
export interface UseFlagsResult {
|
|
3
|
+
flags: FlagMap;
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
getFlag: (key: string, flagContext?: UserContext) => boolean | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function useFlags(): UseFlagsResult;
|
|
9
|
+
//# sourceMappingURL=useFlags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFlags.d.ts","sourceRoot":"","sources":["../src/useFlags.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,WAAW,KAAK,OAAO,GAAG,SAAS,CAAC;CAC1E;AAED,wBAAgB,QAAQ,IAAI,cAAc,CAkCzC"}
|
package/dist/useFlags.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { FlagwardContext } from "./context.js";
|
|
3
|
+
import { createLogger, evaluateFlag, toFlagMap } from "@flagward/core";
|
|
4
|
+
export function useFlags() {
|
|
5
|
+
const context = useContext(FlagwardContext);
|
|
6
|
+
const logger = context.client?.logger ?? createLogger();
|
|
7
|
+
if (!context.client) {
|
|
8
|
+
logger.error("no-provider", "useFlags() was called outside FlagwardProvider, so it can only return " +
|
|
9
|
+
"an empty set. Wrap the tree in <FlagwardProvider>.");
|
|
10
|
+
}
|
|
11
|
+
const getFlag = (key, flagContext) => {
|
|
12
|
+
const mergedContext = { ...context.context, ...flagContext };
|
|
13
|
+
const value = evaluateFlag(context.flagsData[key], mergedContext);
|
|
14
|
+
if (value === undefined) {
|
|
15
|
+
logger.warn(`unknown-flag:${key}`, `Flag "${key}" is not in this environment, so it reads as undefined. ` +
|
|
16
|
+
"Check the key, and that the flag exists in the environment this " +
|
|
17
|
+
"API key belongs to.");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
flags: toFlagMap(context.flagsData, context.context),
|
|
23
|
+
isLoading: context.isLoading,
|
|
24
|
+
error: context.error,
|
|
25
|
+
getFlag,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=useFlags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFlags.js","sourceRoot":"","sources":["../src/useFlags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAUvE,MAAM,UAAU,QAAQ;IACtB,MAAM,OAAO,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,IAAI,YAAY,EAAE,CAAC;IAExD,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,KAAK,CACV,aAAa,EACb,wEAAwE;YACtE,oDAAoD,CACvD,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,WAAyB,EAAuB,EAAE;QAC9E,MAAM,aAAa,GAAG,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,WAAW,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC,CAAC;QAElE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CACT,gBAAgB,GAAG,EAAE,EACrB,SAAS,GAAG,0DAA0D;gBACpE,kEAAkE;gBAClE,qBAAqB,CACxB,CAAC;QACJ,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC;QACpD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,OAAO;KACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version this package reports when it registers with the server.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED by scripts/sync-version.mjs from package.json. Do not edit: the
|
|
5
|
+
* next build overwrites it, and a test fails if the two disagree.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SDK_VERSION = "0.2.0";
|
|
8
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,UAAU,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version this package reports when it registers with the server.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED by scripts/sync-version.mjs from package.json. Do not edit: the
|
|
5
|
+
* next build overwrites it, and a test fails if the two disagree.
|
|
6
|
+
*/
|
|
7
|
+
export const SDK_VERSION = "0.2.0";
|
|
8
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flagward/react",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React SDK for Flagward - Feature Flags as a Service",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"prebuild": "node ../../scripts/sync-version.mjs",
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"dev": "tsc --watch",
|
|
22
|
+
"lint": "tsc --noEmit",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"test:run": "vitest run",
|
|
25
|
+
"prepublishOnly": "npm run build --prefix ../core && npm run lint && npm run test:run && npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"feature-flags",
|
|
29
|
+
"flags",
|
|
30
|
+
"react",
|
|
31
|
+
"sdk",
|
|
32
|
+
"flagward"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/basb7/flagward-sdk-js.git",
|
|
38
|
+
"directory": "packages/react"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/basb7/flagward-sdk-js/tree/main/packages/react#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/basb7/flagward-sdk-js/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@flagward/core": "^0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=18.0.0",
|
|
52
|
+
"react-dom": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@testing-library/jest-dom": "^6.0.0",
|
|
56
|
+
"@testing-library/react": "^16.0.0",
|
|
57
|
+
"@types/node": "^22.20.1",
|
|
58
|
+
"@types/react": "^19.0.0",
|
|
59
|
+
"@types/react-dom": "^19.0.0",
|
|
60
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
61
|
+
"jsdom": "^25.0.0",
|
|
62
|
+
"react": "^19.0.0",
|
|
63
|
+
"react-dom": "^19.0.0",
|
|
64
|
+
"typescript": "^5.7.0",
|
|
65
|
+
"vitest": "^3.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|