@spotify-confidence/openfeature-server-provider-local 0.7.0 → 0.8.1
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/CHANGELOG.md +24 -0
- package/README.md +15 -0
- package/dist/client.d.ts +129 -0
- package/dist/client.js +194 -0
- package/dist/confidence_resolver.wasm +0 -0
- package/dist/index.fetch.d.ts +69 -7
- package/dist/index.fetch.js +394 -126
- package/dist/index.inlined.d.ts +69 -7
- package/dist/index.inlined.js +395 -127
- package/dist/index.node.d.ts +69 -7
- package/dist/index.node.js +394 -126
- package/dist/server.d.ts +126 -0
- package/dist/server.js +114 -0
- package/package.json +27 -2
package/dist/server.d.ts
ADDED
|
@@ -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,114 @@
|
|
|
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
|
+
for (const child of this.childLoggers.values()) child.configure(debug);
|
|
30
|
+
}
|
|
31
|
+
getLogger(name) {
|
|
32
|
+
let child = (this.childLoggers.get(name));
|
|
33
|
+
if (!child) {
|
|
34
|
+
child = new LoggerImpl(this.name + ":" + name);
|
|
35
|
+
this.childLoggers.set(name, child);
|
|
36
|
+
}
|
|
37
|
+
return child;
|
|
38
|
+
}
|
|
39
|
+
}("cnfd");
|
|
40
|
+
logger.getLogger.bind(logger);
|
|
41
|
+
async function loadDebug() {
|
|
42
|
+
try {
|
|
43
|
+
const { default: debug } = await import("debug");
|
|
44
|
+
if (typeof debug !== "function") return null;
|
|
45
|
+
return debug;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function devWarn(message) {
|
|
51
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") console.warn(message);
|
|
52
|
+
}
|
|
53
|
+
let ErrorCode = /* @__PURE__ */ function(ErrorCode$1) {
|
|
54
|
+
ErrorCode$1["PROVIDER_NOT_READY"] = "PROVIDER_NOT_READY";
|
|
55
|
+
ErrorCode$1["PROVIDER_FATAL"] = "PROVIDER_FATAL";
|
|
56
|
+
ErrorCode$1["FLAG_NOT_FOUND"] = "FLAG_NOT_FOUND";
|
|
57
|
+
ErrorCode$1["TYPE_MISMATCH"] = "TYPE_MISMATCH";
|
|
58
|
+
ErrorCode$1["GENERAL"] = "GENERAL";
|
|
59
|
+
return ErrorCode$1;
|
|
60
|
+
}({});
|
|
61
|
+
function error(errorCode, errorMessage) {
|
|
62
|
+
return {
|
|
63
|
+
flags: {},
|
|
64
|
+
resolveId: "",
|
|
65
|
+
resolveToken: "",
|
|
66
|
+
errorCode,
|
|
67
|
+
errorMessage
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const PROVIDER_NAME = "ConfidenceServerProviderLocal";
|
|
71
|
+
function isConfidenceServerProviderLocal(provider) {
|
|
72
|
+
if (provider?.metadata?.name !== PROVIDER_NAME) {
|
|
73
|
+
devWarn(`ConfidenceProvider requires a ConfidenceServerProviderLocal, but got ${provider?.metadata?.name ?? "undefined"}. Make sure you have registered the provider with OpenFeature before rendering.`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
async function ConfidenceProvider({ context, providerName, flags = [], children }) {
|
|
79
|
+
const provider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
|
|
80
|
+
let bundle;
|
|
81
|
+
if (isConfidenceServerProviderLocal(provider)) bundle = await provider.resolve(context, flags);
|
|
82
|
+
else bundle = error(ErrorCode.GENERAL, `The registered OpenFeatureProvider (${providerName}) is not a ConfidenceServerProviderLocal: ${provider?.metadata?.name ?? "undefined"}`);
|
|
83
|
+
async function applyFlag(flagName) {
|
|
84
|
+
"use server";
|
|
85
|
+
const serverProvider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
|
|
86
|
+
if (!bundle.errorCode && isConfidenceServerProviderLocal(serverProvider)) serverProvider.applyFlag(bundle.resolveToken, flagName);
|
|
87
|
+
}
|
|
88
|
+
return /* @__PURE__ */ jsx(ConfidenceClientProvider, {
|
|
89
|
+
bundle,
|
|
90
|
+
apply: applyFlag,
|
|
91
|
+
children
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function getFlagDetails(flagKey, defaultValue, context, providerName) {
|
|
95
|
+
const provider = providerName ? OpenFeature.getProvider(providerName) : OpenFeature.getProvider();
|
|
96
|
+
if (!isConfidenceServerProviderLocal(provider)) return {
|
|
97
|
+
flagKey,
|
|
98
|
+
flagMetadata: {},
|
|
99
|
+
value: defaultValue,
|
|
100
|
+
reason: "ERROR",
|
|
101
|
+
errorCode: ErrorCode.GENERAL,
|
|
102
|
+
errorMessage: "Provider is not a ConfidenceServerProviderLocal"
|
|
103
|
+
};
|
|
104
|
+
const details = await provider.evaluate(flagKey, defaultValue, context);
|
|
105
|
+
return {
|
|
106
|
+
flagKey,
|
|
107
|
+
flagMetadata: {},
|
|
108
|
+
...details
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function getFlag(flagKey, defaultValue, context, providerName) {
|
|
112
|
+
return (await getFlagDetails(flagKey, defaultValue, context, providerName)).value;
|
|
113
|
+
}
|
|
114
|
+
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.
|
|
3
|
+
"version": "0.8.1",
|
|
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": {
|
|
@@ -32,6 +40,7 @@
|
|
|
32
40
|
"format": "prettier --config prettier.config.cjs -w .",
|
|
33
41
|
"format:check": "prettier --config prettier.config.cjs -c .",
|
|
34
42
|
"test": "vitest",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
35
44
|
"proto:gen": "rm -rf src/proto && mkdir -p src/proto && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto -I../../openfeature-provider/proto test-only.proto ../../openfeature-provider/proto/confidence/wasm/messages.proto ../../openfeature-provider/proto/confidence/wasm/wasm_api.proto ../../openfeature-provider/proto/confidence/flags/resolver/v1/types.proto ../../openfeature-provider/proto/confidence/flags/resolver/v1/api.proto ../../openfeature-provider/proto/confidence/flags/resolver/v1/internal_api.proto ../../openfeature-provider/proto/confidence/flags/types/v1/types.proto"
|
|
36
45
|
},
|
|
37
46
|
"dependencies": {
|
|
@@ -41,23 +50,39 @@
|
|
|
41
50
|
"@openfeature/core": "^1.9.0",
|
|
42
51
|
"@openfeature/server-sdk": "^1.19.0",
|
|
43
52
|
"@spotify/prettier-config": "^15.0.0",
|
|
53
|
+
"@testing-library/dom": "^10.4.1",
|
|
54
|
+
"@testing-library/react": "^16.3.2",
|
|
44
55
|
"@types/debug": "^4",
|
|
45
56
|
"@types/node": "^24.0.1",
|
|
57
|
+
"@types/react": "^19",
|
|
58
|
+
"@types/react-dom": "^19",
|
|
46
59
|
"@vitest/coverage-v8": "^3.2.4",
|
|
47
60
|
"debug": "^4.4.3",
|
|
48
61
|
"dotenv": "^17.2.2",
|
|
62
|
+
"happy-dom": "^20.3.4",
|
|
49
63
|
"prettier": "^2.8.8",
|
|
64
|
+
"react": "^19",
|
|
65
|
+
"react-dom": "^19.2.3",
|
|
50
66
|
"rolldown": "1.0.0-beta.38",
|
|
51
67
|
"ts-proto": "^2.7.3",
|
|
52
68
|
"tsdown": "latest",
|
|
69
|
+
"typescript": "^5.9.3",
|
|
53
70
|
"vitest": "^3.2.4"
|
|
54
71
|
},
|
|
55
72
|
"peerDependencies": {
|
|
56
|
-
"
|
|
73
|
+
"@openfeature/core": "^1.0.0",
|
|
74
|
+
"debug": "^4.4.3",
|
|
75
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
57
76
|
},
|
|
58
77
|
"peerDependenciesMeta": {
|
|
78
|
+
"@openfeature/core": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
59
81
|
"debug": {
|
|
60
82
|
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"react": {
|
|
85
|
+
"optional": true
|
|
61
86
|
}
|
|
62
87
|
},
|
|
63
88
|
"packageManager": "yarn@4.6.0",
|