@nanoflags/sdk 0.0.0-stage → 0.3.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 +202 -0
- package/NOTICE +4 -0
- package/README.md +345 -2
- package/dist/index-DZj6xbyg.d.cts +272 -0
- package/dist/index-DZj6xbyg.d.ts +272 -0
- package/dist/index.cjs +8 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/node.cjs +108 -0
- package/dist/node.d.cts +35 -0
- package/dist/node.d.ts +35 -0
- package/dist/node.js +101 -0
- package/dist/react.cjs +65 -0
- package/dist/react.d.cts +28 -0
- package/dist/react.d.ts +28 -0
- package/dist/react.js +62 -0
- package/dist/src-B58PLGGE.cjs +1506 -0
- package/dist/src-DUF_NkBY.js +1471 -0
- package/package.json +98 -3
package/dist/node.cjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_src = require("./src-B58PLGGE.cjs");
|
|
3
|
+
// src/node.ts
|
|
4
|
+
/**
|
|
5
|
+
* @nanoflags/sdk/node — test-session propagation for Node servers.
|
|
6
|
+
*
|
|
7
|
+
* The session of the request being handled lives in AsyncLocalStorage: it follows the request
|
|
8
|
+
* through awaits, timers and callbacks, is invisible to every other request, and disappears when
|
|
9
|
+
* the request's async chain ends. Nothing is stored on the client or in module-level variables.
|
|
10
|
+
*/
|
|
11
|
+
const storage = new (require("node:async_hooks")).AsyncLocalStorage();
|
|
12
|
+
/** Run `fn` with `token` as the current test session. An absent or malformed token runs `fn` outside any session. */
|
|
13
|
+
function withSession(token, fn) {
|
|
14
|
+
return storage.run(require_src.isSessionToken(token) ? {
|
|
15
|
+
token,
|
|
16
|
+
evaluators: /* @__PURE__ */ new Map()
|
|
17
|
+
} : null, fn);
|
|
18
|
+
}
|
|
19
|
+
/** Token of the test session the current request belongs to, if any. */
|
|
20
|
+
function currentSession() {
|
|
21
|
+
return storage.getStore()?.token;
|
|
22
|
+
}
|
|
23
|
+
/** Headers to add to downstream calls so the next service joins the same session. `{}` outside a session. */
|
|
24
|
+
function sessionHeaders() {
|
|
25
|
+
const token = currentSession();
|
|
26
|
+
return token === void 0 ? {} : { [require_src.SESSION_HEADER]: token };
|
|
27
|
+
}
|
|
28
|
+
function passthrough(client) {
|
|
29
|
+
return {
|
|
30
|
+
active: false,
|
|
31
|
+
id: null,
|
|
32
|
+
expiresAt: null,
|
|
33
|
+
get revision() {
|
|
34
|
+
return client.revision;
|
|
35
|
+
},
|
|
36
|
+
boolean: (k, f, c) => client.boolean(k, f, c),
|
|
37
|
+
string: (k, f, c) => client.string(k, f, c),
|
|
38
|
+
number: (k, f, c) => client.number(k, f, c),
|
|
39
|
+
json: (k, f, c) => client.json(k, f, c),
|
|
40
|
+
evaluate: (k, f, c) => client.evaluate(k, f, c)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Evaluator for the current request: session-scoped inside `withSession` / the middleware,
|
|
45
|
+
* otherwise equivalent to the client. Resolved once per request and client. Never rejects.
|
|
46
|
+
*/
|
|
47
|
+
async function sessionFlags(client, options) {
|
|
48
|
+
const scope = storage.getStore();
|
|
49
|
+
if (!scope) return passthrough(client);
|
|
50
|
+
const hit = scope.evaluators.get(client);
|
|
51
|
+
if (hit) return hit;
|
|
52
|
+
let evaluator;
|
|
53
|
+
try {
|
|
54
|
+
evaluator = await client.session(scope.token, options);
|
|
55
|
+
} catch {
|
|
56
|
+
evaluator = passthrough(client);
|
|
57
|
+
}
|
|
58
|
+
scope.evaluators.set(client, evaluator);
|
|
59
|
+
return evaluator;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Synchronous variant for code that runs after `sessionMiddleware` / `withSessionHandler` (which
|
|
63
|
+
* resolve the session before the handler starts). Outside a resolved session it evaluates like the client.
|
|
64
|
+
*/
|
|
65
|
+
function flagsFor(client) {
|
|
66
|
+
return storage.getStore()?.evaluators.get(client) ?? passthrough(client);
|
|
67
|
+
}
|
|
68
|
+
function headerValue(v) {
|
|
69
|
+
const first = Array.isArray(v) ? v[0] : v;
|
|
70
|
+
return typeof first === "string" ? first.trim() : void 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Express/Connect middleware. Reads `x-nanoflags-session`, resolves the session once, and runs the
|
|
74
|
+
* rest of the request inside it. `req.flags` (and `flagsFor(client)`) is the request's evaluator —
|
|
75
|
+
* session-scoped when the session is active, otherwise the plain client behaviour.
|
|
76
|
+
*/
|
|
77
|
+
function sessionMiddleware(client, options) {
|
|
78
|
+
return function nanoflagsSession(req, _res, next) {
|
|
79
|
+
withSession(headerValue(req.headers?.[require_src.SESSION_HEADER]), () => {
|
|
80
|
+
if (currentSession() === void 0) {
|
|
81
|
+
req.flags = passthrough(client);
|
|
82
|
+
return next();
|
|
83
|
+
}
|
|
84
|
+
sessionFlags(client, options).then((flags) => {
|
|
85
|
+
req.flags = flags;
|
|
86
|
+
next();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wrap a fetch-style handler (`(request, ...rest) => Response`): Node fetch servers, Hono, Next.js
|
|
93
|
+
* route handlers on the Node runtime. The handler runs inside the request's session.
|
|
94
|
+
*/
|
|
95
|
+
function withSessionHandler(client, handler, options) {
|
|
96
|
+
return (request, ...rest) => withSession(request.headers.get(require_src.SESSION_HEADER)?.trim(), async () => {
|
|
97
|
+
await sessionFlags(client, options);
|
|
98
|
+
return handler(request, ...rest);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
exports.SESSION_HEADER = require_src.SESSION_HEADER;
|
|
102
|
+
exports.currentSession = currentSession;
|
|
103
|
+
exports.flagsFor = flagsFor;
|
|
104
|
+
exports.sessionFlags = sessionFlags;
|
|
105
|
+
exports.sessionHeaders = sessionHeaders;
|
|
106
|
+
exports.sessionMiddleware = sessionMiddleware;
|
|
107
|
+
exports.withSession = withSession;
|
|
108
|
+
exports.withSessionHandler = withSessionHandler;
|
package/dist/node.d.cts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { d as SessionOptions, i as FlagEvaluator, l as SESSION_HEADER, o as NanoFlagsClient, u as SessionEvaluator } from "./index-DZj6xbyg.cjs";
|
|
2
|
+
type SessionClient = Pick<NanoFlagsClient, "session"> & FlagEvaluator & Pick<NanoFlagsClient, "revision">;
|
|
3
|
+
/** Run `fn` with `token` as the current test session. An absent or malformed token runs `fn` outside any session. */
|
|
4
|
+
export declare function withSession<T>(token: string | null | undefined, fn: () => T): T;
|
|
5
|
+
/** Token of the test session the current request belongs to, if any. */
|
|
6
|
+
export declare function currentSession(): string | undefined;
|
|
7
|
+
/** Headers to add to downstream calls so the next service joins the same session. `{}` outside a session. */
|
|
8
|
+
export declare function sessionHeaders(): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Evaluator for the current request: session-scoped inside `withSession` / the middleware,
|
|
11
|
+
* otherwise equivalent to the client. Resolved once per request and client. Never rejects.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sessionFlags(client: SessionClient, options?: SessionOptions): Promise<SessionEvaluator>;
|
|
14
|
+
/**
|
|
15
|
+
* Synchronous variant for code that runs after `sessionMiddleware` / `withSessionHandler` (which
|
|
16
|
+
* resolve the session before the handler starts). Outside a resolved session it evaluates like the client.
|
|
17
|
+
*/
|
|
18
|
+
export declare function flagsFor(client: SessionClient): SessionEvaluator;
|
|
19
|
+
export interface NodeRequestLike {
|
|
20
|
+
headers: Record<string, string | string[] | undefined>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Express/Connect middleware. Reads `x-nanoflags-session`, resolves the session once, and runs the
|
|
24
|
+
* rest of the request inside it. `req.flags` (and `flagsFor(client)`) is the request's evaluator —
|
|
25
|
+
* session-scoped when the session is active, otherwise the plain client behaviour.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sessionMiddleware(client: SessionClient, options?: SessionOptions): (req: NodeRequestLike & {
|
|
28
|
+
flags?: SessionEvaluator;
|
|
29
|
+
}, _res: unknown, next: (err?: unknown) => void) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a fetch-style handler (`(request, ...rest) => Response`): Node fetch servers, Hono, Next.js
|
|
32
|
+
* route handlers on the Node runtime. The handler runs inside the request's session.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withSessionHandler<A extends unknown[], R>(client: SessionClient, handler: (request: Request, ...rest: A) => R | Promise<R>, options?: SessionOptions): (request: Request, ...rest: A) => Promise<R>;
|
|
35
|
+
export { SESSION_HEADER };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { d as SessionOptions, i as FlagEvaluator, l as SESSION_HEADER, o as NanoFlagsClient, u as SessionEvaluator } from "./index-DZj6xbyg.js";
|
|
2
|
+
type SessionClient = Pick<NanoFlagsClient, "session"> & FlagEvaluator & Pick<NanoFlagsClient, "revision">;
|
|
3
|
+
/** Run `fn` with `token` as the current test session. An absent or malformed token runs `fn` outside any session. */
|
|
4
|
+
export declare function withSession<T>(token: string | null | undefined, fn: () => T): T;
|
|
5
|
+
/** Token of the test session the current request belongs to, if any. */
|
|
6
|
+
export declare function currentSession(): string | undefined;
|
|
7
|
+
/** Headers to add to downstream calls so the next service joins the same session. `{}` outside a session. */
|
|
8
|
+
export declare function sessionHeaders(): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Evaluator for the current request: session-scoped inside `withSession` / the middleware,
|
|
11
|
+
* otherwise equivalent to the client. Resolved once per request and client. Never rejects.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sessionFlags(client: SessionClient, options?: SessionOptions): Promise<SessionEvaluator>;
|
|
14
|
+
/**
|
|
15
|
+
* Synchronous variant for code that runs after `sessionMiddleware` / `withSessionHandler` (which
|
|
16
|
+
* resolve the session before the handler starts). Outside a resolved session it evaluates like the client.
|
|
17
|
+
*/
|
|
18
|
+
export declare function flagsFor(client: SessionClient): SessionEvaluator;
|
|
19
|
+
export interface NodeRequestLike {
|
|
20
|
+
headers: Record<string, string | string[] | undefined>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Express/Connect middleware. Reads `x-nanoflags-session`, resolves the session once, and runs the
|
|
24
|
+
* rest of the request inside it. `req.flags` (and `flagsFor(client)`) is the request's evaluator —
|
|
25
|
+
* session-scoped when the session is active, otherwise the plain client behaviour.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sessionMiddleware(client: SessionClient, options?: SessionOptions): (req: NodeRequestLike & {
|
|
28
|
+
flags?: SessionEvaluator;
|
|
29
|
+
}, _res: unknown, next: (err?: unknown) => void) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a fetch-style handler (`(request, ...rest) => Response`): Node fetch servers, Hono, Next.js
|
|
32
|
+
* route handlers on the Node runtime. The handler runs inside the request's session.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withSessionHandler<A extends unknown[], R>(client: SessionClient, handler: (request: Request, ...rest: A) => R | Promise<R>, options?: SessionOptions): (request: Request, ...rest: A) => Promise<R>;
|
|
35
|
+
export { SESSION_HEADER };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { i as SESSION_HEADER, o as isSessionToken } from "./src-DUF_NkBY.js";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
// src/node.ts
|
|
4
|
+
/**
|
|
5
|
+
* @nanoflags/sdk/node — test-session propagation for Node servers.
|
|
6
|
+
*
|
|
7
|
+
* The session of the request being handled lives in AsyncLocalStorage: it follows the request
|
|
8
|
+
* through awaits, timers and callbacks, is invisible to every other request, and disappears when
|
|
9
|
+
* the request's async chain ends. Nothing is stored on the client or in module-level variables.
|
|
10
|
+
*/
|
|
11
|
+
const storage = new AsyncLocalStorage();
|
|
12
|
+
/** Run `fn` with `token` as the current test session. An absent or malformed token runs `fn` outside any session. */
|
|
13
|
+
function withSession(token, fn) {
|
|
14
|
+
return storage.run(isSessionToken(token) ? {
|
|
15
|
+
token,
|
|
16
|
+
evaluators: /* @__PURE__ */ new Map()
|
|
17
|
+
} : null, fn);
|
|
18
|
+
}
|
|
19
|
+
/** Token of the test session the current request belongs to, if any. */
|
|
20
|
+
function currentSession() {
|
|
21
|
+
return storage.getStore()?.token;
|
|
22
|
+
}
|
|
23
|
+
/** Headers to add to downstream calls so the next service joins the same session. `{}` outside a session. */
|
|
24
|
+
function sessionHeaders() {
|
|
25
|
+
const token = currentSession();
|
|
26
|
+
return token === void 0 ? {} : { [SESSION_HEADER]: token };
|
|
27
|
+
}
|
|
28
|
+
function passthrough(client) {
|
|
29
|
+
return {
|
|
30
|
+
active: false,
|
|
31
|
+
id: null,
|
|
32
|
+
expiresAt: null,
|
|
33
|
+
get revision() {
|
|
34
|
+
return client.revision;
|
|
35
|
+
},
|
|
36
|
+
boolean: (k, f, c) => client.boolean(k, f, c),
|
|
37
|
+
string: (k, f, c) => client.string(k, f, c),
|
|
38
|
+
number: (k, f, c) => client.number(k, f, c),
|
|
39
|
+
json: (k, f, c) => client.json(k, f, c),
|
|
40
|
+
evaluate: (k, f, c) => client.evaluate(k, f, c)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Evaluator for the current request: session-scoped inside `withSession` / the middleware,
|
|
45
|
+
* otherwise equivalent to the client. Resolved once per request and client. Never rejects.
|
|
46
|
+
*/
|
|
47
|
+
async function sessionFlags(client, options) {
|
|
48
|
+
const scope = storage.getStore();
|
|
49
|
+
if (!scope) return passthrough(client);
|
|
50
|
+
const hit = scope.evaluators.get(client);
|
|
51
|
+
if (hit) return hit;
|
|
52
|
+
let evaluator;
|
|
53
|
+
try {
|
|
54
|
+
evaluator = await client.session(scope.token, options);
|
|
55
|
+
} catch {
|
|
56
|
+
evaluator = passthrough(client);
|
|
57
|
+
}
|
|
58
|
+
scope.evaluators.set(client, evaluator);
|
|
59
|
+
return evaluator;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Synchronous variant for code that runs after `sessionMiddleware` / `withSessionHandler` (which
|
|
63
|
+
* resolve the session before the handler starts). Outside a resolved session it evaluates like the client.
|
|
64
|
+
*/
|
|
65
|
+
function flagsFor(client) {
|
|
66
|
+
return storage.getStore()?.evaluators.get(client) ?? passthrough(client);
|
|
67
|
+
}
|
|
68
|
+
function headerValue(v) {
|
|
69
|
+
const first = Array.isArray(v) ? v[0] : v;
|
|
70
|
+
return typeof first === "string" ? first.trim() : void 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Express/Connect middleware. Reads `x-nanoflags-session`, resolves the session once, and runs the
|
|
74
|
+
* rest of the request inside it. `req.flags` (and `flagsFor(client)`) is the request's evaluator —
|
|
75
|
+
* session-scoped when the session is active, otherwise the plain client behaviour.
|
|
76
|
+
*/
|
|
77
|
+
function sessionMiddleware(client, options) {
|
|
78
|
+
return function nanoflagsSession(req, _res, next) {
|
|
79
|
+
withSession(headerValue(req.headers?.[SESSION_HEADER]), () => {
|
|
80
|
+
if (currentSession() === void 0) {
|
|
81
|
+
req.flags = passthrough(client);
|
|
82
|
+
return next();
|
|
83
|
+
}
|
|
84
|
+
sessionFlags(client, options).then((flags) => {
|
|
85
|
+
req.flags = flags;
|
|
86
|
+
next();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wrap a fetch-style handler (`(request, ...rest) => Response`): Node fetch servers, Hono, Next.js
|
|
93
|
+
* route handlers on the Node runtime. The handler runs inside the request's session.
|
|
94
|
+
*/
|
|
95
|
+
function withSessionHandler(client, handler, options) {
|
|
96
|
+
return (request, ...rest) => withSession(request.headers.get(SESSION_HEADER)?.trim(), async () => {
|
|
97
|
+
await sessionFlags(client, options);
|
|
98
|
+
return handler(request, ...rest);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export { SESSION_HEADER, currentSession, flagsFor, sessionFlags, sessionHeaders, sessionMiddleware, withSession, withSessionHandler };
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
let react = require("react");
|
|
4
|
+
// src/react.ts
|
|
5
|
+
/**
|
|
6
|
+
* @nanoflags/sdk/react — bindings built on useSyncExternalStore.
|
|
7
|
+
* Components re-render when the bundle or the client status changes; evaluation stays local.
|
|
8
|
+
*/
|
|
9
|
+
const NanoFlagsContext = (0, react.createContext)(null);
|
|
10
|
+
function NanoFlagsProvider({ client, context, children }) {
|
|
11
|
+
const serialized = JSON.stringify(context ?? {});
|
|
12
|
+
const value = (0, react.useMemo)(() => ({
|
|
13
|
+
client,
|
|
14
|
+
context: context ?? {}
|
|
15
|
+
}), [client, serialized]);
|
|
16
|
+
return (0, react.createElement)(NanoFlagsContext.Provider, { value }, children);
|
|
17
|
+
}
|
|
18
|
+
function useProvider() {
|
|
19
|
+
const value = (0, react.useContext)(NanoFlagsContext);
|
|
20
|
+
if (!value) throw new Error("useFlag/useFlagsStatus must be used inside <NanoFlagsProvider>.");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function useVersion(client) {
|
|
24
|
+
return (0, react.useSyncExternalStore)(client.subscribe, () => client.version, () => client.version);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Value of a flag for the provider context (shallow-merged with `ctxOverride`).
|
|
28
|
+
* The accessor is chosen from the type of `fallback`: boolean, string, number, otherwise JSON.
|
|
29
|
+
* A missing flag, a missing bundle or a type mismatch returns `fallback`.
|
|
30
|
+
*/
|
|
31
|
+
function useFlag(key, fallback, ctxOverride) {
|
|
32
|
+
const { client, context } = useProvider();
|
|
33
|
+
const version = useVersion(client);
|
|
34
|
+
const override = JSON.stringify(ctxOverride ?? null);
|
|
35
|
+
const fb = typeof fallback === "object" ? JSON.stringify(fallback) : fallback;
|
|
36
|
+
return (0, react.useMemo)(() => {
|
|
37
|
+
const ctx = ctxOverride ? {
|
|
38
|
+
...context,
|
|
39
|
+
...ctxOverride
|
|
40
|
+
} : context;
|
|
41
|
+
if (typeof fallback === "boolean") return client.boolean(key, fallback, ctx);
|
|
42
|
+
if (typeof fallback === "string") return client.string(key, fallback, ctx);
|
|
43
|
+
if (typeof fallback === "number") return client.number(key, fallback, ctx);
|
|
44
|
+
return client.json(key, fallback, ctx);
|
|
45
|
+
}, [
|
|
46
|
+
client,
|
|
47
|
+
version,
|
|
48
|
+
key,
|
|
49
|
+
fb,
|
|
50
|
+
context,
|
|
51
|
+
override
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
function useFlagsStatus() {
|
|
55
|
+
const { client } = useProvider();
|
|
56
|
+
const version = useVersion(client);
|
|
57
|
+
return (0, react.useMemo)(() => ({
|
|
58
|
+
status: client.status,
|
|
59
|
+
revision: client.revision,
|
|
60
|
+
hasBundle: client.status !== "not-ready"
|
|
61
|
+
}), [client, version]);
|
|
62
|
+
}
|
|
63
|
+
exports.NanoFlagsProvider = NanoFlagsProvider;
|
|
64
|
+
exports.useFlag = useFlag;
|
|
65
|
+
exports.useFlagsStatus = useFlagsStatus;
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nanoflags/sdk/react — bindings built on useSyncExternalStore.
|
|
3
|
+
* Components re-render when the bundle or the client status changes; evaluation stays local.
|
|
4
|
+
*/
|
|
5
|
+
import { C as JsonValue, n as ClientStatus, o as NanoFlagsClient, x as Context } from "./index-DZj6xbyg.cjs";
|
|
6
|
+
import { ReactElement, ReactNode } from "react";
|
|
7
|
+
export interface NanoFlagsProviderProps {
|
|
8
|
+
client: NanoFlagsClient;
|
|
9
|
+
/** Evaluation context shared by every `useFlag` below this provider. */
|
|
10
|
+
context?: Context;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function NanoFlagsProvider({ client, context, children }: NanoFlagsProviderProps): ReactElement;
|
|
14
|
+
type Widen<T> = T extends boolean ? boolean : T extends string ? string : T extends number ? number : T;
|
|
15
|
+
/**
|
|
16
|
+
* Value of a flag for the provider context (shallow-merged with `ctxOverride`).
|
|
17
|
+
* The accessor is chosen from the type of `fallback`: boolean, string, number, otherwise JSON.
|
|
18
|
+
* A missing flag, a missing bundle or a type mismatch returns `fallback`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useFlag<T extends JsonValue>(key: string, fallback: T, ctxOverride?: Context): Widen<T>;
|
|
21
|
+
export interface FlagsStatus {
|
|
22
|
+
status: ClientStatus;
|
|
23
|
+
revision: number | null;
|
|
24
|
+
/** True when a bundle is loaded (status "ready" or "stale"). */
|
|
25
|
+
hasBundle: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function useFlagsStatus(): FlagsStatus;
|
|
28
|
+
export {};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nanoflags/sdk/react — bindings built on useSyncExternalStore.
|
|
3
|
+
* Components re-render when the bundle or the client status changes; evaluation stays local.
|
|
4
|
+
*/
|
|
5
|
+
import { C as JsonValue, n as ClientStatus, o as NanoFlagsClient, x as Context } from "./index-DZj6xbyg.js";
|
|
6
|
+
import { ReactElement, ReactNode } from "react";
|
|
7
|
+
export interface NanoFlagsProviderProps {
|
|
8
|
+
client: NanoFlagsClient;
|
|
9
|
+
/** Evaluation context shared by every `useFlag` below this provider. */
|
|
10
|
+
context?: Context;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function NanoFlagsProvider({ client, context, children }: NanoFlagsProviderProps): ReactElement;
|
|
14
|
+
type Widen<T> = T extends boolean ? boolean : T extends string ? string : T extends number ? number : T;
|
|
15
|
+
/**
|
|
16
|
+
* Value of a flag for the provider context (shallow-merged with `ctxOverride`).
|
|
17
|
+
* The accessor is chosen from the type of `fallback`: boolean, string, number, otherwise JSON.
|
|
18
|
+
* A missing flag, a missing bundle or a type mismatch returns `fallback`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useFlag<T extends JsonValue>(key: string, fallback: T, ctxOverride?: Context): Widen<T>;
|
|
21
|
+
export interface FlagsStatus {
|
|
22
|
+
status: ClientStatus;
|
|
23
|
+
revision: number | null;
|
|
24
|
+
/** True when a bundle is loaded (status "ready" or "stale"). */
|
|
25
|
+
hasBundle: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function useFlagsStatus(): FlagsStatus;
|
|
28
|
+
export {};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, createElement, useContext, useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
// src/react.ts
|
|
4
|
+
/**
|
|
5
|
+
* @nanoflags/sdk/react — bindings built on useSyncExternalStore.
|
|
6
|
+
* Components re-render when the bundle or the client status changes; evaluation stays local.
|
|
7
|
+
*/
|
|
8
|
+
const NanoFlagsContext = createContext(null);
|
|
9
|
+
function NanoFlagsProvider({ client, context, children }) {
|
|
10
|
+
const serialized = JSON.stringify(context ?? {});
|
|
11
|
+
const value = useMemo(() => ({
|
|
12
|
+
client,
|
|
13
|
+
context: context ?? {}
|
|
14
|
+
}), [client, serialized]);
|
|
15
|
+
return createElement(NanoFlagsContext.Provider, { value }, children);
|
|
16
|
+
}
|
|
17
|
+
function useProvider() {
|
|
18
|
+
const value = useContext(NanoFlagsContext);
|
|
19
|
+
if (!value) throw new Error("useFlag/useFlagsStatus must be used inside <NanoFlagsProvider>.");
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function useVersion(client) {
|
|
23
|
+
return useSyncExternalStore(client.subscribe, () => client.version, () => client.version);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Value of a flag for the provider context (shallow-merged with `ctxOverride`).
|
|
27
|
+
* The accessor is chosen from the type of `fallback`: boolean, string, number, otherwise JSON.
|
|
28
|
+
* A missing flag, a missing bundle or a type mismatch returns `fallback`.
|
|
29
|
+
*/
|
|
30
|
+
function useFlag(key, fallback, ctxOverride) {
|
|
31
|
+
const { client, context } = useProvider();
|
|
32
|
+
const version = useVersion(client);
|
|
33
|
+
const override = JSON.stringify(ctxOverride ?? null);
|
|
34
|
+
const fb = typeof fallback === "object" ? JSON.stringify(fallback) : fallback;
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
const ctx = ctxOverride ? {
|
|
37
|
+
...context,
|
|
38
|
+
...ctxOverride
|
|
39
|
+
} : context;
|
|
40
|
+
if (typeof fallback === "boolean") return client.boolean(key, fallback, ctx);
|
|
41
|
+
if (typeof fallback === "string") return client.string(key, fallback, ctx);
|
|
42
|
+
if (typeof fallback === "number") return client.number(key, fallback, ctx);
|
|
43
|
+
return client.json(key, fallback, ctx);
|
|
44
|
+
}, [
|
|
45
|
+
client,
|
|
46
|
+
version,
|
|
47
|
+
key,
|
|
48
|
+
fb,
|
|
49
|
+
context,
|
|
50
|
+
override
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
function useFlagsStatus() {
|
|
54
|
+
const { client } = useProvider();
|
|
55
|
+
const version = useVersion(client);
|
|
56
|
+
return useMemo(() => ({
|
|
57
|
+
status: client.status,
|
|
58
|
+
revision: client.revision,
|
|
59
|
+
hasBundle: client.status !== "not-ready"
|
|
60
|
+
}), [client, version]);
|
|
61
|
+
}
|
|
62
|
+
export { NanoFlagsProvider, useFlag, useFlagsStatus };
|