@spotify-confidence/openfeature-server-provider-local 0.7.0 → 0.8.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.
@@ -0,0 +1,126 @@
1
+ import { EvaluationContext, EvaluationDetails, JsonValue } from "@openfeature/server-sdk";
2
+
3
+ //#region src/react/server.d.ts
4
+ interface ConfidenceProviderProps {
5
+ /** The evaluation context for flag resolution */
6
+ context: EvaluationContext;
7
+ /** Optional provider name. If not specified, uses the default provider. */
8
+ providerName?: string;
9
+ /** Flag names to resolve. If not specified, resolves all flags. */
10
+ flags?: string[];
11
+ /** Child components */
12
+ children: React.ReactNode;
13
+ }
14
+ /**
15
+ * React Server Component that resolves flags and provides them to client components.
16
+ *
17
+ * This component resolves all specified flags in a single call on the server and
18
+ * passes the results to client components via React Context. Client components
19
+ * can then access flag values using the `useFlag` and `useFlagDetails` hooks
20
+ * from `react-client`.
21
+ *
22
+ * Flags are resolved **without** logging exposure. Exposure is logged when client
23
+ * components call the hooks (automatically on mount, or manually via `expose()`).
24
+ *
25
+ * Must be used with a `ConfidenceServerProviderLocal` registered with OpenFeature.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * // app/layout.tsx
30
+ * import { ConfidenceProvider } from '@spotify-confidence/openfeature-server-provider-local/react-server';
31
+ *
32
+ * export default async function RootLayout({ children }: { children: React.ReactNode }) {
33
+ * const context = {
34
+ * targetingKey: 'user-123',
35
+ * country: 'US',
36
+ * };
37
+ *
38
+ * return (
39
+ * <html>
40
+ * <body>
41
+ * <ConfidenceProvider context={context} flags={['checkout-flow', 'promo-banner']}>
42
+ * {children}
43
+ * </ConfidenceProvider>
44
+ * </body>
45
+ * </html>
46
+ * );
47
+ * }
48
+ * ```
49
+ */
50
+ declare function ConfidenceProvider({
51
+ context,
52
+ providerName,
53
+ flags,
54
+ children
55
+ }: ConfidenceProviderProps): Promise<React.ReactElement>;
56
+ /**
57
+ * Evaluate a flag in a React Server Component and get full evaluation details.
58
+ *
59
+ * This function evaluates the flag and **immediately logs exposure** since server
60
+ * components render once without hydration. Use this when you need access to
61
+ * variant, reason, or error information.
62
+ *
63
+ * Supports dot notation to access nested properties within a flag value
64
+ * (e.g., 'my-flag.config.enabled').
65
+ *
66
+ * @param flagKey - The flag key, optionally with dot notation for nested access
67
+ * @param defaultValue - Default value returned if flag is not found or type doesn't match
68
+ * @param context - Evaluation context containing targetingKey and other attributes
69
+ * @param providerName - Optional named provider (uses default provider if not specified)
70
+ * @returns Promise resolving to EvaluationDetails with value, variant, reason, and error info
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * // app/page.tsx (Server Component)
75
+ * import { getFlagDetails } from '@spotify-confidence/openfeature-server-provider-local/react-server';
76
+ *
77
+ * export default async function Page() {
78
+ * const { value, variant, reason } = await getFlagDetails(
79
+ * 'checkout-flow.enabled',
80
+ * false,
81
+ * { targetingKey: 'user-123' }
82
+ * );
83
+ *
84
+ * console.log(`Resolved to variant ${variant} because: ${reason}`);
85
+ * return value ? <NewCheckout /> : <OldCheckout />;
86
+ * }
87
+ * ```
88
+ */
89
+ declare function getFlagDetails<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, providerName?: string): Promise<EvaluationDetails<T>>;
90
+ /**
91
+ * Evaluate a flag in a React Server Component and get the value.
92
+ *
93
+ * This function evaluates the flag and **immediately logs exposure** since server
94
+ * components render once without hydration. This is the simplest way to get a
95
+ * flag value in server components.
96
+ *
97
+ * Supports dot notation to access nested properties within a flag value
98
+ * (e.g., 'my-flag.config.enabled').
99
+ *
100
+ * @param flagKey - The flag key, optionally with dot notation for nested access
101
+ * @param defaultValue - Default value returned if flag is not found or type doesn't match
102
+ * @param context - Evaluation context containing targetingKey and other attributes
103
+ * @param providerName - Optional named provider (uses default provider if not specified)
104
+ * @returns Promise resolving to the flag value
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * // app/page.tsx (Server Component)
109
+ * import { getFlag } from '@spotify-confidence/openfeature-server-provider-local/react-server';
110
+ *
111
+ * export default async function Page() {
112
+ * const showNewLayout = await getFlag(
113
+ * 'page-layout.useNewDesign',
114
+ * false,
115
+ * { targetingKey: 'user-123' }
116
+ * );
117
+ *
118
+ * return showNewLayout ? <NewLayout /> : <OldLayout />;
119
+ * }
120
+ * ```
121
+ *
122
+ * @see getFlagDetails for accessing variant, reason, and error information
123
+ */
124
+ declare function getFlag<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, providerName?: string): Promise<T>;
125
+ //#endregion
126
+ export { ConfidenceProvider, ConfidenceProviderProps, getFlag, getFlagDetails };
package/dist/server.js ADDED
@@ -0,0 +1,113 @@
1
+ import { OpenFeature } from "@openfeature/server-sdk";
2
+ import { ConfidenceClientProvider } from "./client";
3
+ import "@bufbuild/protobuf/wire";
4
+ import { jsx } from "react/jsx-runtime";
5
+ const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
6
+ const debugBackend = loadDebug();
7
+ const logger = new class LoggerImpl {
8
+ childLoggers = /* @__PURE__ */ new Map();
9
+ debug = NOOP_LOG_FN;
10
+ info = NOOP_LOG_FN;
11
+ warn = NOOP_LOG_FN;
12
+ error = NOOP_LOG_FN;
13
+ constructor(name) {
14
+ this.name = name;
15
+ this.configure();
16
+ }
17
+ async configure(backend = debugBackend) {
18
+ const debug = await backend;
19
+ if (!debug) return;
20
+ const debugFn = this.debug = (debug(this.name + ":debug"));
21
+ const infoFn = this.info = (debug(this.name + ":info"));
22
+ const warnFn = this.warn = (debug(this.name + ":warn"));
23
+ const errorFn = this.error = (debug(this.name + ":error"));
24
+ switch (true) {
25
+ case debugFn.enabled: infoFn.enabled = true;
26
+ case infoFn.enabled: warnFn.enabled = true;
27
+ case warnFn.enabled: errorFn.enabled = true;
28
+ }
29
+ }
30
+ getLogger(name) {
31
+ let child = (this.childLoggers.get(name));
32
+ if (!child) {
33
+ child = new LoggerImpl(this.name + ":" + name);
34
+ this.childLoggers.set(name, child);
35
+ }
36
+ return child;
37
+ }
38
+ }("cnfd");
39
+ logger.getLogger.bind(logger);
40
+ async function loadDebug() {
41
+ try {
42
+ const { default: debug } = await import("debug");
43
+ if (typeof debug !== "function") return null;
44
+ return debug;
45
+ } catch (e) {
46
+ return null;
47
+ }
48
+ }
49
+ function devWarn(message) {
50
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") console.warn(message);
51
+ }
52
+ let ErrorCode = /* @__PURE__ */ function(ErrorCode$1) {
53
+ ErrorCode$1["PROVIDER_NOT_READY"] = "PROVIDER_NOT_READY";
54
+ ErrorCode$1["PROVIDER_FATAL"] = "PROVIDER_FATAL";
55
+ ErrorCode$1["FLAG_NOT_FOUND"] = "FLAG_NOT_FOUND";
56
+ ErrorCode$1["TYPE_MISMATCH"] = "TYPE_MISMATCH";
57
+ ErrorCode$1["GENERAL"] = "GENERAL";
58
+ return ErrorCode$1;
59
+ }({});
60
+ function error(errorCode, errorMessage) {
61
+ return {
62
+ flags: {},
63
+ resolveId: "",
64
+ resolveToken: "",
65
+ errorCode,
66
+ errorMessage
67
+ };
68
+ }
69
+ const PROVIDER_NAME = "ConfidenceServerProviderLocal";
70
+ function isConfidenceServerProviderLocal(provider) {
71
+ if (provider?.metadata?.name !== PROVIDER_NAME) {
72
+ devWarn(`ConfidenceProvider requires a ConfidenceServerProviderLocal, but got ${provider?.metadata?.name ?? "undefined"}. Make sure you have registered the provider with OpenFeature before rendering.`);
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ async function ConfidenceProvider({ context, providerName, flags = [], children }) {
78
+ const provider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
79
+ let bundle;
80
+ if (isConfidenceServerProviderLocal(provider)) bundle = await provider.resolve(context, flags);
81
+ else bundle = error(ErrorCode.GENERAL, `The registered OpenFeatureProvider (${providerName}) is not a ConfidenceServerProviderLocal: ${provider?.metadata?.name ?? "undefined"}`);
82
+ async function applyFlag(flagName) {
83
+ "use server";
84
+ const serverProvider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
85
+ if (!bundle.errorCode && isConfidenceServerProviderLocal(serverProvider)) serverProvider.applyFlag(bundle.resolveToken, flagName);
86
+ }
87
+ return /* @__PURE__ */ jsx(ConfidenceClientProvider, {
88
+ bundle,
89
+ apply: applyFlag,
90
+ children
91
+ });
92
+ }
93
+ async function getFlagDetails(flagKey, defaultValue, context, providerName) {
94
+ const provider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
95
+ if (!isConfidenceServerProviderLocal(provider)) return {
96
+ flagKey,
97
+ flagMetadata: {},
98
+ value: defaultValue,
99
+ reason: "ERROR",
100
+ errorCode: ErrorCode.GENERAL,
101
+ errorMessage: "Provider is not a ConfidenceServerProviderLocal"
102
+ };
103
+ const details = await provider.evaluate(flagKey, defaultValue, context);
104
+ return {
105
+ flagKey,
106
+ flagMetadata: {},
107
+ ...details
108
+ };
109
+ }
110
+ async function getFlag(flagKey, defaultValue, context, providerName) {
111
+ return (await getFlagDetails(flagKey, defaultValue, context, providerName)).value;
112
+ }
113
+ export { ConfidenceProvider, getFlag, getFlagDetails };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spotify-confidence/openfeature-server-provider-local",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Spotify Confidence Open Feature provider",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,6 +24,14 @@
24
24
  "types": "./dist/index.fetch.d.ts",
25
25
  "default": "./dist/index.fetch.js"
26
26
  },
27
+ "./react-server": {
28
+ "types": "./dist/server.d.ts",
29
+ "default": "./dist/server.js"
30
+ },
31
+ "./react-client": {
32
+ "types": "./dist/client.d.ts",
33
+ "default": "./dist/client.js"
34
+ },
27
35
  "./package.json": "./package.json"
28
36
  },
29
37
  "scripts": {
@@ -41,23 +49,38 @@
41
49
  "@openfeature/core": "^1.9.0",
42
50
  "@openfeature/server-sdk": "^1.19.0",
43
51
  "@spotify/prettier-config": "^15.0.0",
52
+ "@testing-library/dom": "^10.4.1",
53
+ "@testing-library/react": "^16.3.2",
44
54
  "@types/debug": "^4",
45
55
  "@types/node": "^24.0.1",
56
+ "@types/react": "^19",
57
+ "@types/react-dom": "^19",
46
58
  "@vitest/coverage-v8": "^3.2.4",
47
59
  "debug": "^4.4.3",
48
60
  "dotenv": "^17.2.2",
61
+ "happy-dom": "^20.3.4",
49
62
  "prettier": "^2.8.8",
63
+ "react": "^19",
64
+ "react-dom": "^19.2.3",
50
65
  "rolldown": "1.0.0-beta.38",
51
66
  "ts-proto": "^2.7.3",
52
67
  "tsdown": "latest",
53
68
  "vitest": "^3.2.4"
54
69
  },
55
70
  "peerDependencies": {
56
- "debug": "^4.4.3"
71
+ "@openfeature/core": "^1.0.0",
72
+ "debug": "^4.4.3",
73
+ "react": "^18.0.0 || ^19.0.0"
57
74
  },
58
75
  "peerDependenciesMeta": {
76
+ "@openfeature/core": {
77
+ "optional": true
78
+ },
59
79
  "debug": {
60
80
  "optional": true
81
+ },
82
+ "react": {
83
+ "optional": true
61
84
  }
62
85
  },
63
86
  "packageManager": "yarn@4.6.0",