@variantlab/next 0.1.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 +108 -0
- package/dist/app-router.cjs +325 -0
- package/dist/app-router.cjs.map +1 -0
- package/dist/app-router.d.cts +41 -0
- package/dist/app-router.d.ts +41 -0
- package/dist/app-router.js +309 -0
- package/dist/app-router.js.map +1 -0
- package/dist/client/hooks.cjs +191 -0
- package/dist/client/hooks.cjs.map +1 -0
- package/dist/client/hooks.d.cts +62 -0
- package/dist/client/hooks.d.ts +62 -0
- package/dist/client/hooks.js +178 -0
- package/dist/client/hooks.js.map +1 -0
- package/dist/index.cjs +319 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +318 -0
- package/dist/index.d.ts +318 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/dist/pages-router.cjs +325 -0
- package/dist/pages-router.cjs.map +1 -0
- package/dist/pages-router.d.cts +26 -0
- package/dist/pages-router.d.ts +26 -0
- package/dist/pages-router.js +309 -0
- package/dist/pages-router.js.map +1 -0
- package/package.json +105 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ExperimentsConfig, VariantContext, Experiment } from '@variantlab/core';
|
|
2
|
+
export { UseExperimentResult, Variant, VariantErrorBoundary, VariantErrorBoundaryProps, VariantLabContext, VariantProps, VariantValue, VariantValueProps, useExperiment, useRouteExperiments, useSetVariant, useVariant, useVariantLabEngine, useVariantValue } from '@variantlab/react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared types for `@variantlab/next`.
|
|
7
|
+
*
|
|
8
|
+
* Kept in a single tiny file so every entrypoint (server barrel,
|
|
9
|
+
* app-router subpath, pages-router subpath, client provider) can
|
|
10
|
+
* import without pulling in code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props accepted by the Next `VariantLabProvider` Client Component.
|
|
15
|
+
*/
|
|
16
|
+
interface VariantLabProviderProps {
|
|
17
|
+
/** Validated `ExperimentsConfig` or a raw JSON module import. */
|
|
18
|
+
readonly config: unknown | ExperimentsConfig;
|
|
19
|
+
/** Runtime context (userId, locale, platform, …) applied before first render. */
|
|
20
|
+
readonly initialContext?: VariantContext;
|
|
21
|
+
/**
|
|
22
|
+
* Assignments computed on the server. Seeded into the engine cache so
|
|
23
|
+
* the first `getVariant` call on the client returns the same variant
|
|
24
|
+
* that was server-rendered, without re-evaluating targeting.
|
|
25
|
+
*/
|
|
26
|
+
readonly initialVariants?: Readonly<Record<string, string>>;
|
|
27
|
+
readonly children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Named `NextVariantLabProvider` internally to avoid colliding with the
|
|
32
|
+
* re-export name `VariantLabProvider` once tsup bundles
|
|
33
|
+
* `@variantlab/react` inline. Exposed publicly as `VariantLabProvider`
|
|
34
|
+
* via the barrel in `./hooks.ts`.
|
|
35
|
+
*/
|
|
36
|
+
declare function NextVariantLabProvider({ config, initialContext, initialVariants, children, }: VariantLabProviderProps): ReactNode;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Client-side hooks + components for `@variantlab/next`.
|
|
40
|
+
*
|
|
41
|
+
* Re-exports the public surface of `@variantlab/react` (hooks +
|
|
42
|
+
* components) plus a Next-specific `useNextRouteExperiments()` that
|
|
43
|
+
* reads `usePathname()` from `next/navigation` so consumers don't
|
|
44
|
+
* have to thread the route through by hand.
|
|
45
|
+
*
|
|
46
|
+
* NOTE: `@variantlab/react` is bundled *into* this file (not marked
|
|
47
|
+
* external) so the symbols have local bindings in the emitted JS.
|
|
48
|
+
* Next's client-reference loader cannot follow `export { X } from
|
|
49
|
+
* "<workspace-pkg>"` chains across `transpilePackages` boundaries
|
|
50
|
+
* in dev mode, so we eliminate the boundary entirely for the client
|
|
51
|
+
* entrypoint. The server entrypoint still treats `@variantlab/react`
|
|
52
|
+
* as external because it never crosses a client boundary.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Like `useRouteExperiments(route)`, but reads the current route from
|
|
57
|
+
* `next/navigation`'s `usePathname()` so you never need to pass it
|
|
58
|
+
* explicitly.
|
|
59
|
+
*/
|
|
60
|
+
declare function useNextRouteExperiments(): readonly Experiment[];
|
|
61
|
+
|
|
62
|
+
export { NextVariantLabProvider as VariantLabProvider, type VariantLabProviderProps, useNextRouteExperiments };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ExperimentsConfig, VariantContext, Experiment } from '@variantlab/core';
|
|
2
|
+
export { UseExperimentResult, Variant, VariantErrorBoundary, VariantErrorBoundaryProps, VariantLabContext, VariantProps, VariantValue, VariantValueProps, useExperiment, useRouteExperiments, useSetVariant, useVariant, useVariantLabEngine, useVariantValue } from '@variantlab/react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared types for `@variantlab/next`.
|
|
7
|
+
*
|
|
8
|
+
* Kept in a single tiny file so every entrypoint (server barrel,
|
|
9
|
+
* app-router subpath, pages-router subpath, client provider) can
|
|
10
|
+
* import without pulling in code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props accepted by the Next `VariantLabProvider` Client Component.
|
|
15
|
+
*/
|
|
16
|
+
interface VariantLabProviderProps {
|
|
17
|
+
/** Validated `ExperimentsConfig` or a raw JSON module import. */
|
|
18
|
+
readonly config: unknown | ExperimentsConfig;
|
|
19
|
+
/** Runtime context (userId, locale, platform, …) applied before first render. */
|
|
20
|
+
readonly initialContext?: VariantContext;
|
|
21
|
+
/**
|
|
22
|
+
* Assignments computed on the server. Seeded into the engine cache so
|
|
23
|
+
* the first `getVariant` call on the client returns the same variant
|
|
24
|
+
* that was server-rendered, without re-evaluating targeting.
|
|
25
|
+
*/
|
|
26
|
+
readonly initialVariants?: Readonly<Record<string, string>>;
|
|
27
|
+
readonly children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Named `NextVariantLabProvider` internally to avoid colliding with the
|
|
32
|
+
* re-export name `VariantLabProvider` once tsup bundles
|
|
33
|
+
* `@variantlab/react` inline. Exposed publicly as `VariantLabProvider`
|
|
34
|
+
* via the barrel in `./hooks.ts`.
|
|
35
|
+
*/
|
|
36
|
+
declare function NextVariantLabProvider({ config, initialContext, initialVariants, children, }: VariantLabProviderProps): ReactNode;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Client-side hooks + components for `@variantlab/next`.
|
|
40
|
+
*
|
|
41
|
+
* Re-exports the public surface of `@variantlab/react` (hooks +
|
|
42
|
+
* components) plus a Next-specific `useNextRouteExperiments()` that
|
|
43
|
+
* reads `usePathname()` from `next/navigation` so consumers don't
|
|
44
|
+
* have to thread the route through by hand.
|
|
45
|
+
*
|
|
46
|
+
* NOTE: `@variantlab/react` is bundled *into* this file (not marked
|
|
47
|
+
* external) so the symbols have local bindings in the emitted JS.
|
|
48
|
+
* Next's client-reference loader cannot follow `export { X } from
|
|
49
|
+
* "<workspace-pkg>"` chains across `transpilePackages` boundaries
|
|
50
|
+
* in dev mode, so we eliminate the boundary entirely for the client
|
|
51
|
+
* entrypoint. The server entrypoint still treats `@variantlab/react`
|
|
52
|
+
* as external because it never crosses a client boundary.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Like `useRouteExperiments(route)`, but reads the current route from
|
|
57
|
+
* `next/navigation`'s `usePathname()` so you never need to pass it
|
|
58
|
+
* explicitly.
|
|
59
|
+
*/
|
|
60
|
+
declare function useNextRouteExperiments(): readonly Experiment[];
|
|
61
|
+
|
|
62
|
+
export { NextVariantLabProvider as VariantLabProvider, type VariantLabProviderProps, useNextRouteExperiments };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, Component, useContext, useCallback, useSyncExternalStore, useRef, useMemo, useLayoutEffect } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { createEngine } from '@variantlab/core';
|
|
6
|
+
|
|
7
|
+
var VariantLabContext = createContext(null);
|
|
8
|
+
function VariantLabProvider({
|
|
9
|
+
engine,
|
|
10
|
+
initialContext,
|
|
11
|
+
children
|
|
12
|
+
}) {
|
|
13
|
+
const value = useMemo(() => engine, [engine]);
|
|
14
|
+
const appliedRef = useRef(null);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (initialContext === void 0) return;
|
|
17
|
+
const prev = appliedRef.current;
|
|
18
|
+
if (prev !== null && prev.engine === engine && prev.ctx === initialContext) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
engine.updateContext(initialContext);
|
|
22
|
+
appliedRef.current = { engine, ctx: initialContext };
|
|
23
|
+
}, [engine, initialContext]);
|
|
24
|
+
return /* @__PURE__ */ jsx(VariantLabContext.Provider, { value, children });
|
|
25
|
+
}
|
|
26
|
+
var VariantErrorBoundary = class extends Component {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(...arguments);
|
|
29
|
+
this.state = { error: null };
|
|
30
|
+
}
|
|
31
|
+
static getDerivedStateFromError(error) {
|
|
32
|
+
return { error };
|
|
33
|
+
}
|
|
34
|
+
componentDidCatch(error, _info) {
|
|
35
|
+
const engine = this.context;
|
|
36
|
+
if (engine !== null) {
|
|
37
|
+
try {
|
|
38
|
+
engine.reportCrash(this.props.experimentId, error);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
componentDidUpdate(prevProps) {
|
|
44
|
+
if (this.state.error !== null && prevProps.children !== this.props.children) {
|
|
45
|
+
this.setState({ error: null });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
render() {
|
|
49
|
+
const { error } = this.state;
|
|
50
|
+
if (error !== null) {
|
|
51
|
+
const { fallback } = this.props;
|
|
52
|
+
if (typeof fallback === "function") return fallback(error);
|
|
53
|
+
return fallback ?? null;
|
|
54
|
+
}
|
|
55
|
+
return this.props.children;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
VariantErrorBoundary.contextType = VariantLabContext;
|
|
59
|
+
function useVariantLabEngine() {
|
|
60
|
+
const engine = useContext(VariantLabContext);
|
|
61
|
+
if (engine === null) {
|
|
62
|
+
throw new Error("useVariantLabEngine: no <VariantLabProvider> found above this component.");
|
|
63
|
+
}
|
|
64
|
+
return engine;
|
|
65
|
+
}
|
|
66
|
+
function useVariant(experimentId) {
|
|
67
|
+
const engine = useVariantLabEngine();
|
|
68
|
+
const subscribe = useCallback(
|
|
69
|
+
(onStoreChange) => {
|
|
70
|
+
return engine.subscribe((event) => {
|
|
71
|
+
if (shouldNotify(event, experimentId)) onStoreChange();
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
[engine, experimentId]
|
|
75
|
+
);
|
|
76
|
+
const getSnapshot = useCallback(
|
|
77
|
+
() => engine.getVariant(experimentId),
|
|
78
|
+
[engine, experimentId]
|
|
79
|
+
);
|
|
80
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
81
|
+
}
|
|
82
|
+
function shouldNotify(event, experimentId) {
|
|
83
|
+
switch (event.type) {
|
|
84
|
+
case "configLoaded":
|
|
85
|
+
case "contextUpdated":
|
|
86
|
+
return true;
|
|
87
|
+
case "variantChanged":
|
|
88
|
+
case "rollback":
|
|
89
|
+
case "assignment":
|
|
90
|
+
return event.experimentId === experimentId;
|
|
91
|
+
default:
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function Variant({ experimentId, children, fallback }) {
|
|
96
|
+
const active = useVariant(experimentId);
|
|
97
|
+
const node = children[active];
|
|
98
|
+
if (node !== void 0) return node;
|
|
99
|
+
return fallback ?? null;
|
|
100
|
+
}
|
|
101
|
+
function useVariantValue(experimentId) {
|
|
102
|
+
const engine = useVariantLabEngine();
|
|
103
|
+
useVariant(experimentId);
|
|
104
|
+
return engine.getVariantValue(experimentId);
|
|
105
|
+
}
|
|
106
|
+
function VariantValue({
|
|
107
|
+
experimentId,
|
|
108
|
+
children
|
|
109
|
+
}) {
|
|
110
|
+
const value = useVariantValue(experimentId);
|
|
111
|
+
return children(value);
|
|
112
|
+
}
|
|
113
|
+
function useExperiment(experimentId) {
|
|
114
|
+
const engine = useVariantLabEngine();
|
|
115
|
+
const variant = useVariant(experimentId);
|
|
116
|
+
const value = engine.getVariantValue(experimentId);
|
|
117
|
+
const track = useCallback((_eventName, _properties) => {
|
|
118
|
+
}, []);
|
|
119
|
+
return { variant, value, track };
|
|
120
|
+
}
|
|
121
|
+
function useRouteExperiments(route) {
|
|
122
|
+
const engine = useVariantLabEngine();
|
|
123
|
+
const cacheRef = useRef(null);
|
|
124
|
+
const subscribe = useCallback(
|
|
125
|
+
(onStoreChange) => {
|
|
126
|
+
return engine.subscribe((event) => {
|
|
127
|
+
if (event.type === "configLoaded" || event.type === "contextUpdated") {
|
|
128
|
+
cacheRef.current = null;
|
|
129
|
+
onStoreChange();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
[engine]
|
|
134
|
+
);
|
|
135
|
+
const getSnapshot = useCallback(() => {
|
|
136
|
+
const cached = cacheRef.current;
|
|
137
|
+
if (cached !== null && cached.route === route) return cached.snapshot;
|
|
138
|
+
const snapshot = engine.getExperiments(route);
|
|
139
|
+
cacheRef.current = { route, snapshot };
|
|
140
|
+
return snapshot;
|
|
141
|
+
}, [engine, route]);
|
|
142
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
143
|
+
}
|
|
144
|
+
function useSetVariant() {
|
|
145
|
+
const engine = useVariantLabEngine();
|
|
146
|
+
return useCallback(
|
|
147
|
+
(experimentId, variantId) => {
|
|
148
|
+
engine.setVariant(experimentId, variantId);
|
|
149
|
+
},
|
|
150
|
+
[engine]
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
function NextVariantLabProvider({
|
|
154
|
+
config,
|
|
155
|
+
initialContext,
|
|
156
|
+
initialVariants,
|
|
157
|
+
children
|
|
158
|
+
}) {
|
|
159
|
+
const engine = useMemo(
|
|
160
|
+
() => createEngine(config, {
|
|
161
|
+
...initialContext !== void 0 ? { context: initialContext } : {},
|
|
162
|
+
...initialVariants !== void 0 ? { initialAssignments: initialVariants } : {}
|
|
163
|
+
}),
|
|
164
|
+
// Stable JSON module → stable identity → stable engine.
|
|
165
|
+
[config, initialContext, initialVariants]
|
|
166
|
+
);
|
|
167
|
+
return /* @__PURE__ */ jsx(VariantLabProvider, { engine, children });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/client/hooks.ts
|
|
171
|
+
function useNextRouteExperiments() {
|
|
172
|
+
const pathname = usePathname();
|
|
173
|
+
return useRouteExperiments(pathname ?? void 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { Variant, VariantErrorBoundary, VariantLabContext, NextVariantLabProvider as VariantLabProvider, VariantValue, useExperiment, useNextRouteExperiments, useRouteExperiments, useSetVariant, useVariant, useVariantLabEngine, useVariantValue };
|
|
177
|
+
//# sourceMappingURL=hooks.js.map
|
|
178
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../react/src/context.tsx","../../../react/src/components/error-boundary.tsx","../../../react/src/hooks/use-variant-lab-engine.ts","../../../react/src/hooks/use-variant.ts","../../../react/src/components/variant.tsx","../../../react/src/hooks/use-variant-value.ts","../../../react/src/components/variant-value.tsx","../../../react/src/hooks/use-experiment.ts","../../../react/src/hooks/use-route-experiments.ts","../../../react/src/hooks/use-set-variant.ts","../../src/client/provider.tsx","../../src/client/hooks.ts"],"names":["useCallback","useRef","useSyncExternalStore","useMemo","jsx"],"mappings":"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","file":"hooks.js","sourcesContent":["/**\n * React context and provider for variantlab.\n *\n * The provider is a thin wrapper that hands a pre-built `VariantEngine`\n * to descendants via React context. It intentionally does NOT own the\n * engine: callers construct it via `createEngine(...)` and pass it in.\n * That keeps the adapter testable (swap in a mock engine), SSR-safe\n * (server code can build an engine before render), and free of\n * lifecycle surprises — unmounting the provider never disposes the\n * engine because the provider didn't create it.\n *\n * `initialContext`, when supplied, is applied via `useLayoutEffect`\n * before the first browser paint. We use a ref to guard against\n * repeated application on re-renders so the engine cache is only\n * busted when the caller actually swaps engines.\n */\n\nimport type { VariantContext, VariantEngine } from \"@variantlab/core\";\nimport { createContext, type ReactNode, useLayoutEffect, useMemo, useRef } from \"react\";\n\n/** The raw context value. `null` until a provider wraps the tree. */\nexport const VariantLabContext = createContext<VariantEngine | null>(null);\n\nexport interface VariantLabProviderProps {\n /** A pre-constructed engine from `createEngine(...)`. */\n readonly engine: VariantEngine;\n /** Optional runtime context applied once on mount. */\n readonly initialContext?: VariantContext;\n readonly children?: ReactNode;\n}\n\n/**\n * Wraps descendants so the hooks in this package can locate the engine.\n *\n * The value handed to `Context.Provider` is memoized on `engine` alone\n * so that unrelated parent re-renders don't force a new reference\n * (which would cascade through every `useVariant` consumer).\n */\nexport function VariantLabProvider({\n engine,\n initialContext,\n children,\n}: VariantLabProviderProps): ReactNode {\n // Stable context value: only changes when the engine itself changes.\n const value = useMemo(() => engine, [engine]);\n\n // Track which (engine, initialContext) pair we've already applied so\n // re-renders don't re-invoke updateContext (which would clear the\n // engine cache and cause unnecessary reassignment events).\n const appliedRef = useRef<{\n readonly engine: VariantEngine;\n readonly ctx: VariantContext | undefined;\n } | null>(null);\n\n useLayoutEffect(() => {\n if (initialContext === undefined) return;\n const prev = appliedRef.current;\n if (prev !== null && prev.engine === engine && prev.ctx === initialContext) {\n return;\n }\n engine.updateContext(initialContext);\n appliedRef.current = { engine, ctx: initialContext };\n }, [engine, initialContext]);\n\n return <VariantLabContext.Provider value={value}>{children}</VariantLabContext.Provider>;\n}\n","/**\n * `<VariantErrorBoundary>` — crash insulation for experiment render trees.\n *\n * Wraps a variant's subtree in a class-based error boundary (the only\n * remaining legitimate use of class components in modern React). When\n * a child throws, it:\n *\n * 1. reports the crash to the engine via `engine.reportCrash(id, err)`,\n * which feeds the rollback counter defined in `config-format.md`;\n * 2. renders `fallback` (or `null`) for the rest of the current\n * commit cycle;\n * 3. resets itself so the next render can try again with the new\n * variant (likely the rolled-back default after N crashes).\n *\n * The engine lookup goes through `VariantLabContext` rather than a\n * prop so the ergonomics match the hook-first API. `contextType` on\n * the class makes the context value available as `this.context` in\n * `componentDidCatch`.\n */\n\nimport type { VariantEngine } from \"@variantlab/core\";\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport { VariantLabContext } from \"../context.js\";\n\nexport interface VariantErrorBoundaryProps {\n readonly experimentId: string;\n readonly fallback?: ReactNode | ((error: Error) => ReactNode);\n readonly children: ReactNode;\n}\n\ninterface VariantErrorBoundaryState {\n readonly error: Error | null;\n}\n\nexport class VariantErrorBoundary extends Component<\n VariantErrorBoundaryProps,\n VariantErrorBoundaryState\n> {\n static override contextType = VariantLabContext;\n declare context: VariantEngine | null;\n override state: VariantErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): VariantErrorBoundaryState {\n return { error };\n }\n\n override componentDidCatch(error: Error, _info: ErrorInfo): void {\n const engine = this.context;\n if (engine !== null) {\n try {\n engine.reportCrash(this.props.experimentId, error);\n } catch {\n // Swallow crash-reporting failures — we must not double-throw\n // inside an error boundary.\n }\n }\n }\n\n override componentDidUpdate(prevProps: VariantErrorBoundaryProps): void {\n // If the children prop changes after a recovery (e.g. the engine\n // rolled back and a parent re-rendered with a different subtree),\n // clear the error so we attempt to render the new tree.\n if (this.state.error !== null && prevProps.children !== this.props.children) {\n this.setState({ error: null });\n }\n }\n\n override render(): ReactNode {\n const { error } = this.state;\n if (error !== null) {\n const { fallback } = this.props;\n if (typeof fallback === \"function\") return fallback(error);\n return fallback ?? null;\n }\n return this.props.children;\n }\n}\n","/**\n * `useVariantLabEngine` — raw engine access escape hatch.\n *\n * Most consumers should reach for `useVariant` / `useVariantValue` /\n * `useExperiment` instead. This hook is for code that needs to call\n * engine methods directly (e.g. `setVariant` from a debug panel, or\n * `reportCrash` from a custom error boundary).\n *\n * Throws synchronously when called outside a `VariantLabProvider`\n * because any other behavior (returning `null`, lazy init) would hide\n * the real bug behind mysterious fallbacks elsewhere in the tree.\n */\n\nimport type { VariantEngine } from \"@variantlab/core\";\nimport { useContext } from \"react\";\nimport { VariantLabContext } from \"../context.js\";\n\nexport function useVariantLabEngine(): VariantEngine {\n const engine = useContext(VariantLabContext);\n if (engine === null) {\n throw new Error(\"useVariantLabEngine: no <VariantLabProvider> found above this component.\");\n }\n return engine;\n}\n","/**\n * `useVariant` — read the current variant id for an experiment.\n *\n * Subscribes to engine events via `useSyncExternalStore` so that the\n * component re-renders whenever the variant for this experiment\n * actually changes (manual override, rollback, config reload, context\n * update). Events that don't affect this experiment are filtered out\n * at the listener level — React's bail-out on identical snapshots\n * handles the rest.\n *\n * The snapshot callback is `engine.getVariant(id)` directly. The\n * engine memoizes the result in an internal cache, so repeated reads\n * in the same render pass are O(1) and side-effect free — which is\n * exactly what `useSyncExternalStore` requires.\n */\n\nimport type { EngineEvent } from \"@variantlab/core\";\nimport { useCallback, useSyncExternalStore } from \"react\";\nimport { useVariantLabEngine } from \"./use-variant-lab-engine.js\";\n\nexport function useVariant(experimentId: string): string {\n const engine = useVariantLabEngine();\n\n const subscribe = useCallback(\n (onStoreChange: () => void): (() => void) => {\n return engine.subscribe((event: EngineEvent) => {\n if (shouldNotify(event, experimentId)) onStoreChange();\n });\n },\n [engine, experimentId],\n );\n\n const getSnapshot = useCallback(\n (): string => engine.getVariant(experimentId),\n [engine, experimentId],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Decide whether an engine event should retrigger snapshot sampling.\n * Broad events (`configLoaded`, `contextUpdated`) always fire; targeted\n * events fire only when they name this experiment.\n */\nfunction shouldNotify(event: EngineEvent, experimentId: string): boolean {\n switch (event.type) {\n case \"configLoaded\":\n case \"contextUpdated\":\n return true;\n case \"variantChanged\":\n case \"rollback\":\n case \"assignment\":\n return event.experimentId === experimentId;\n default:\n return false;\n }\n}\n","/**\n * `<Variant>` — render-prop switch for \"render\" experiments.\n *\n * Takes a plain `Record<variantId, ReactNode>` as its children. The\n * shape intentionally looks like a lookup table so grep, codegen, and\n * TypeScript exhaustiveness checks all work without any custom\n * helpers. If the active variant isn't in the table we render\n * `fallback` if provided, else `null` — silent render failures are\n * preferable to crashing the tree when a config is missing a case.\n *\n * This component is tiny on purpose: the real logic lives in\n * `useVariant`, and everything here is table lookup plus a fallback.\n */\nimport type { ReactNode } from \"react\";\nimport { useVariant } from \"../hooks/use-variant.js\";\n\nexport interface VariantProps {\n readonly experimentId: string;\n readonly children: Readonly<Record<string, ReactNode>>;\n readonly fallback?: ReactNode;\n}\n\nexport function Variant({ experimentId, children, fallback }: VariantProps): ReactNode {\n const active = useVariant(experimentId);\n const node = children[active];\n if (node !== undefined) return node;\n return fallback ?? null;\n}\n","/**\n * `useVariantValue` — read a \"value\" experiment's typed payload.\n *\n * Thin wrapper around `useVariant` that looks up the matching\n * `variant.value` from the engine. Used for copy, pricing, feature\n * toggles, and any other experiment where the thing that varies is\n * data, not a React component.\n *\n * The generic `T` exists purely for type inference ergonomics. When\n * codegen is active it's filled in automatically from\n * `GeneratedExperiments[id][\"value\"]`; otherwise callers pass an\n * explicit type parameter.\n */\nimport { useVariant } from \"./use-variant.js\";\nimport { useVariantLabEngine } from \"./use-variant-lab-engine.js\";\n\nexport function useVariantValue<T = unknown>(experimentId: string): T {\n const engine = useVariantLabEngine();\n // Track the variant id so we re-render when it changes. The actual\n // value is fetched via `getVariantValue` which walks the variant\n // table; this is O(n_variants) which is fine for typical configs.\n useVariant(experimentId);\n return engine.getVariantValue<T>(experimentId);\n}\n","/**\n * `<VariantValue>` — function-as-child helper for \"value\" experiments.\n *\n * Lets callers declaratively render with a typed variant value without\n * having to import `useVariantValue` into a component that is\n * otherwise pure JSX. Useful inside dense markup where introducing a\n * hook would require pulling the surrounding code into a wrapper\n * component.\n */\nimport type { ReactNode } from \"react\";\nimport { useVariantValue } from \"../hooks/use-variant-value.js\";\n\nexport interface VariantValueProps<T> {\n readonly experimentId: string;\n readonly children: (value: T) => ReactNode;\n}\n\nexport function VariantValue<T = unknown>({\n experimentId,\n children,\n}: VariantValueProps<T>): ReactNode {\n const value = useVariantValue<T>(experimentId);\n return children(value);\n}\n","/**\n * `useExperiment` — combined read + tracker helper.\n *\n * Returns the variant id, the typed value, and a `track` function\n * bound to this experiment. `track` is a pass-through to the engine's\n * history ring buffer — it doesn't do network IO (core makes no\n * outbound calls, period) but it does give downstream telemetry\n * subscribers something to observe.\n *\n * The `track` closure is stable across re-renders via `useCallback`\n * so passing it into memoized children doesn't bust their memoization.\n */\nimport { useCallback } from \"react\";\nimport { useVariant } from \"./use-variant.js\";\nimport { useVariantLabEngine } from \"./use-variant-lab-engine.js\";\n\nexport interface UseExperimentResult<T> {\n readonly variant: string;\n readonly value: T;\n readonly track: (eventName: string, properties?: Record<string, unknown>) => void;\n}\n\nexport function useExperiment<T = unknown>(experimentId: string): UseExperimentResult<T> {\n const engine = useVariantLabEngine();\n const variant = useVariant(experimentId);\n const value = engine.getVariantValue<T>(experimentId);\n\n const track = useCallback((_eventName: string, _properties?: Record<string, unknown>): void => {\n // Telemetry forwarding is a phase-2 feature. For now `track` is\n // a no-op sink so callers can wire up the API today without\n // waiting for the pluggable telemetry layer.\n }, []);\n\n return { variant, value, track };\n}\n","/**\n * `useRouteExperiments` — list experiments active on a given route.\n *\n * Pure derivation from `engine.getExperiments(route)`. The challenge\n * is that `engine.getExperiments(route)` allocates a fresh filtered\n * array on every call, which would cause `useSyncExternalStore` to\n * flag an infinite update loop. We cache the snapshot in a ref and\n * invalidate it whenever a relevant engine event fires, so the\n * reference is stable between notifications.\n */\n\nimport type { EngineEvent, Experiment } from \"@variantlab/core\";\nimport { useCallback, useRef, useSyncExternalStore } from \"react\";\nimport { useVariantLabEngine } from \"./use-variant-lab-engine.js\";\n\ninterface Cached {\n readonly route: string | undefined;\n readonly snapshot: readonly Experiment[];\n}\n\nexport function useRouteExperiments(route?: string): readonly Experiment[] {\n const engine = useVariantLabEngine();\n const cacheRef = useRef<Cached | null>(null);\n\n const subscribe = useCallback(\n (onStoreChange: () => void): (() => void) => {\n return engine.subscribe((event: EngineEvent) => {\n if (event.type === \"configLoaded\" || event.type === \"contextUpdated\") {\n cacheRef.current = null;\n onStoreChange();\n }\n });\n },\n [engine],\n );\n\n const getSnapshot = useCallback((): readonly Experiment[] => {\n const cached = cacheRef.current;\n if (cached !== null && cached.route === route) return cached.snapshot;\n const snapshot = engine.getExperiments(route);\n cacheRef.current = { route, snapshot };\n return snapshot;\n }, [engine, route]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n","/**\n * `useSetVariant` — dev-only imperative variant override.\n *\n * Returns a stable setter bound to the current engine. Intended for\n * debug UIs (Storybook, the overlay, dev tools) and QA scripts, not\n * for production logic. Production code should update the config or\n * targeting, not force variants from components.\n */\nimport { useCallback } from \"react\";\nimport { useVariantLabEngine } from \"./use-variant-lab-engine.js\";\n\nexport function useSetVariant(): (experimentId: string, variantId: string) => void {\n const engine = useVariantLabEngine();\n return useCallback(\n (experimentId: string, variantId: string): void => {\n engine.setVariant(experimentId, variantId);\n },\n [engine],\n );\n}\n","\"use client\";\n\n/**\n * Next-specific `VariantLabProvider` Client Component.\n *\n * Wraps `@variantlab/react`'s provider. The engine is constructed via\n * `useMemo` keyed on the config/context/variants identity so a stable\n * imported JSON module yields a stable engine across renders and\n * StrictMode double-invocation.\n *\n * Crucially, `initialContext` is passed to `createEngine`, not to the\n * inner React provider. That avoids the inner provider's\n * `useLayoutEffect` branch, which would emit an SSR warning when this\n * component is rendered on the server as part of a Client Component\n * tree.\n *\n * `initialVariants` is forwarded via the new core option\n * `initialAssignments`, seeding the engine cache so the first\n * `getVariant` call short-circuits on the exact variants the server\n * rendered. Zero re-evaluation on first render → zero hydration\n * mismatches.\n */\n\nimport { createEngine } from \"@variantlab/core\";\nimport { VariantLabProvider as CoreVariantLabProvider } from \"@variantlab/react\";\nimport { type ReactNode, useMemo } from \"react\";\nimport type { VariantLabProviderProps } from \"../types.js\";\n\n/**\n * Named `NextVariantLabProvider` internally to avoid colliding with the\n * re-export name `VariantLabProvider` once tsup bundles\n * `@variantlab/react` inline. Exposed publicly as `VariantLabProvider`\n * via the barrel in `./hooks.ts`.\n */\nexport function NextVariantLabProvider({\n config,\n initialContext,\n initialVariants,\n children,\n}: VariantLabProviderProps): ReactNode {\n const engine = useMemo(\n () =>\n createEngine(config, {\n ...(initialContext !== undefined ? { context: initialContext } : {}),\n ...(initialVariants !== undefined ? { initialAssignments: initialVariants } : {}),\n }),\n // Stable JSON module → stable identity → stable engine.\n [config, initialContext, initialVariants],\n );\n return <CoreVariantLabProvider engine={engine}>{children}</CoreVariantLabProvider>;\n}\n","\"use client\";\n\n/**\n * Client-side hooks + components for `@variantlab/next`.\n *\n * Re-exports the public surface of `@variantlab/react` (hooks +\n * components) plus a Next-specific `useNextRouteExperiments()` that\n * reads `usePathname()` from `next/navigation` so consumers don't\n * have to thread the route through by hand.\n *\n * NOTE: `@variantlab/react` is bundled *into* this file (not marked\n * external) so the symbols have local bindings in the emitted JS.\n * Next's client-reference loader cannot follow `export { X } from\n * \"<workspace-pkg>\"` chains across `transpilePackages` boundaries\n * in dev mode, so we eliminate the boundary entirely for the client\n * entrypoint. The server entrypoint still treats `@variantlab/react`\n * as external because it never crosses a client boundary.\n */\n\nimport type { Experiment } from \"@variantlab/core\";\nimport { useRouteExperiments } from \"@variantlab/react\";\n// `next/navigation` is listed as a peer dep (via `next`) so this\n// import is resolved by Next's bundler at build time.\nimport { usePathname } from \"next/navigation\";\n\nexport type {\n UseExperimentResult,\n VariantErrorBoundaryProps,\n VariantProps,\n VariantValueProps,\n} from \"@variantlab/react\";\nexport {\n useExperiment,\n useRouteExperiments,\n useSetVariant,\n useVariant,\n useVariantLabEngine,\n useVariantValue,\n Variant,\n VariantErrorBoundary,\n VariantLabContext,\n VariantValue,\n} from \"@variantlab/react\";\nexport type { VariantLabProviderProps } from \"../types.js\";\nexport { NextVariantLabProvider as VariantLabProvider } from \"./provider.js\";\n\n/**\n * Like `useRouteExperiments(route)`, but reads the current route from\n * `next/navigation`'s `usePathname()` so you never need to pass it\n * explicitly.\n */\nexport function useNextRouteExperiments(): readonly Experiment[] {\n const pathname = usePathname();\n return useRouteExperiments(pathname ?? undefined);\n}\n"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@variantlab/core');
|
|
4
|
+
|
|
5
|
+
// src/types.ts
|
|
6
|
+
var DEFAULT_COOKIE_NAME = "__variantlab_sticky";
|
|
7
|
+
var DEFAULT_MAX_AGE = 60 * 60 * 24 * 365;
|
|
8
|
+
|
|
9
|
+
// src/server/cookie.ts
|
|
10
|
+
var MAX_COOKIE_HEADER_BYTES = 8192;
|
|
11
|
+
var MAX_PAYLOAD_BYTES = 4096;
|
|
12
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
13
|
+
var textEncoder = new TextEncoder();
|
|
14
|
+
var textDecoder = new TextDecoder();
|
|
15
|
+
function base64urlEncode(input) {
|
|
16
|
+
const bytes = textEncoder.encode(input);
|
|
17
|
+
let binary = "";
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
binary += String.fromCharCode(bytes[i]);
|
|
20
|
+
}
|
|
21
|
+
const b64 = btoa(binary);
|
|
22
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
|
+
}
|
|
24
|
+
function base64urlDecode(input) {
|
|
25
|
+
if (!/^[A-Za-z0-9\-_]*$/.test(input)) return null;
|
|
26
|
+
const pad = input.length % 4;
|
|
27
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "====".slice(pad === 0 ? 4 : pad);
|
|
28
|
+
try {
|
|
29
|
+
const binary = atob(padded);
|
|
30
|
+
const bytes = new Uint8Array(binary.length);
|
|
31
|
+
for (let i = 0; i < binary.length; i++) {
|
|
32
|
+
bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
}
|
|
34
|
+
return textDecoder.decode(bytes);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function encodePayload(payload) {
|
|
40
|
+
const json = JSON.stringify({ v: payload.v, u: payload.u, a: payload.a });
|
|
41
|
+
return base64urlEncode(json);
|
|
42
|
+
}
|
|
43
|
+
function decodePayload(raw) {
|
|
44
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
45
|
+
if (raw.length > MAX_PAYLOAD_BYTES) return null;
|
|
46
|
+
const json = base64urlDecode(raw);
|
|
47
|
+
if (json === null) return null;
|
|
48
|
+
if (json.length > MAX_PAYLOAD_BYTES) return null;
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(json);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
56
|
+
const obj = parsed;
|
|
57
|
+
if (obj["v"] !== 1) return null;
|
|
58
|
+
if (typeof obj["u"] !== "string" || obj["u"].length === 0 || obj["u"].length > 256) return null;
|
|
59
|
+
const rawA = obj["a"];
|
|
60
|
+
if (rawA === null || typeof rawA !== "object" || Array.isArray(rawA)) return null;
|
|
61
|
+
const src = rawA;
|
|
62
|
+
const a = /* @__PURE__ */ Object.create(null);
|
|
63
|
+
for (const key of Object.keys(src)) {
|
|
64
|
+
if (RESERVED_NAMES.has(key)) continue;
|
|
65
|
+
if (key.length === 0 || key.length > 128) continue;
|
|
66
|
+
const val = src[key];
|
|
67
|
+
if (typeof val !== "string" || val.length === 0 || val.length > 128) continue;
|
|
68
|
+
a[key] = val;
|
|
69
|
+
}
|
|
70
|
+
return { v: 1, u: obj["u"], a };
|
|
71
|
+
}
|
|
72
|
+
function parseCookieHeader(header) {
|
|
73
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
74
|
+
if (typeof header !== "string" || header.length === 0) return out;
|
|
75
|
+
if (header.length > MAX_COOKIE_HEADER_BYTES) return out;
|
|
76
|
+
let i = 0;
|
|
77
|
+
const n = header.length;
|
|
78
|
+
while (i < n) {
|
|
79
|
+
while (i < n && (header.charCodeAt(i) === 32 || header.charCodeAt(i) === 9)) i++;
|
|
80
|
+
const start = i;
|
|
81
|
+
while (i < n && header.charCodeAt(i) !== 59) i++;
|
|
82
|
+
const segment = header.slice(start, i);
|
|
83
|
+
if (i < n) i++;
|
|
84
|
+
if (segment.length === 0) continue;
|
|
85
|
+
const eq = segment.indexOf("=");
|
|
86
|
+
if (eq <= 0) continue;
|
|
87
|
+
const name = segment.slice(0, eq).trim();
|
|
88
|
+
if (name.length === 0) continue;
|
|
89
|
+
if (RESERVED_NAMES.has(name)) continue;
|
|
90
|
+
let value = segment.slice(eq + 1).trim();
|
|
91
|
+
if (value.length >= 2 && value.charCodeAt(0) === 34 && value.charCodeAt(value.length - 1) === 34) {
|
|
92
|
+
value = value.slice(1, -1);
|
|
93
|
+
}
|
|
94
|
+
if (out[name] === void 0) {
|
|
95
|
+
out[name] = safeDecodeURIComponent(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function safeDecodeURIComponent(s) {
|
|
101
|
+
try {
|
|
102
|
+
return decodeURIComponent(s);
|
|
103
|
+
} catch {
|
|
104
|
+
return s;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function serializeCookie(name, value, options = {}) {
|
|
108
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
109
|
+
const maxAge = options.maxAge ?? DEFAULT_MAX_AGE;
|
|
110
|
+
if (maxAge > 0) parts.push(`Max-Age=${Math.floor(maxAge)}`);
|
|
111
|
+
parts.push(`Path=${options.path ?? "/"}`);
|
|
112
|
+
const sameSite = options.sameSite ?? "lax";
|
|
113
|
+
parts.push(`SameSite=${capitalize(sameSite)}`);
|
|
114
|
+
if (options.httpOnly !== false) parts.push("HttpOnly");
|
|
115
|
+
if (options.secure === true) parts.push("Secure");
|
|
116
|
+
if (options.domain !== void 0) parts.push(`Domain=${options.domain}`);
|
|
117
|
+
return parts.join("; ");
|
|
118
|
+
}
|
|
119
|
+
function capitalize(s) {
|
|
120
|
+
if (s.length === 0) return s;
|
|
121
|
+
return s[0].toUpperCase() + s.slice(1);
|
|
122
|
+
}
|
|
123
|
+
function readCookieFromSource(source, name = DEFAULT_COOKIE_NAME) {
|
|
124
|
+
if (source === null || source === void 0) return void 0;
|
|
125
|
+
if (typeof source === "string") {
|
|
126
|
+
const parsed = parseCookieHeader(source);
|
|
127
|
+
return parsed[name];
|
|
128
|
+
}
|
|
129
|
+
if (typeof source.get === "function") {
|
|
130
|
+
const entry = source.get(name);
|
|
131
|
+
return entry === void 0 ? void 0 : entry.value;
|
|
132
|
+
}
|
|
133
|
+
if (typeof source.headers?.get === "function") {
|
|
134
|
+
const header = source.headers.get("cookie");
|
|
135
|
+
if (header === null) return void 0;
|
|
136
|
+
return parseCookieHeader(header)[name];
|
|
137
|
+
}
|
|
138
|
+
const pagesReq = source;
|
|
139
|
+
if (pagesReq.cookies !== void 0) {
|
|
140
|
+
const fromBag = pagesReq.cookies[name];
|
|
141
|
+
if (typeof fromBag === "string" && fromBag.length > 0) return fromBag;
|
|
142
|
+
}
|
|
143
|
+
if (pagesReq.headers !== void 0) {
|
|
144
|
+
const header = pagesReq.headers["cookie"];
|
|
145
|
+
if (typeof header === "string") return parseCookieHeader(header)[name];
|
|
146
|
+
if (Array.isArray(header) && typeof header[0] === "string") {
|
|
147
|
+
return parseCookieHeader(header[0])[name];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
function readPayloadFromSource(source, name = DEFAULT_COOKIE_NAME) {
|
|
153
|
+
const raw = readCookieFromSource(source, name);
|
|
154
|
+
return decodePayload(raw);
|
|
155
|
+
}
|
|
156
|
+
function generateUserId() {
|
|
157
|
+
const g = globalThis;
|
|
158
|
+
if (g.crypto?.randomUUID !== void 0) return g.crypto.randomUUID();
|
|
159
|
+
if (g.crypto?.getRandomValues !== void 0) {
|
|
160
|
+
const bytes = new Uint8Array(16);
|
|
161
|
+
g.crypto.getRandomValues(bytes);
|
|
162
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
163
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
164
|
+
const hex = [];
|
|
165
|
+
for (let i = 0; i < 16; i++) {
|
|
166
|
+
hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
167
|
+
}
|
|
168
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
169
|
+
}
|
|
170
|
+
return `u-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
171
|
+
}
|
|
172
|
+
function createVariantLabServer(rawConfig, options = {}) {
|
|
173
|
+
const config = core.validateConfig(rawConfig);
|
|
174
|
+
const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
175
|
+
const onError = options.onError ?? (() => {
|
|
176
|
+
});
|
|
177
|
+
function readPayload(source) {
|
|
178
|
+
try {
|
|
179
|
+
return readPayloadFromSource(source, cookieName);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
onError(error);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function buildContext(payload, extras) {
|
|
186
|
+
const userId = payload?.u ?? extras?.userId;
|
|
187
|
+
if (userId === void 0) return { ...extras };
|
|
188
|
+
return { ...extras, userId };
|
|
189
|
+
}
|
|
190
|
+
function getVariant(experimentId, source, context) {
|
|
191
|
+
try {
|
|
192
|
+
const payload = readPayload(source);
|
|
193
|
+
const engine = core.createEngine(config, {
|
|
194
|
+
context: buildContext(payload, context),
|
|
195
|
+
...payload?.a !== void 0 ? { initialAssignments: payload.a } : {}
|
|
196
|
+
});
|
|
197
|
+
return engine.getVariant(experimentId);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
onError(error);
|
|
200
|
+
return experimentDefault(config, experimentId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function getVariantValue(experimentId, source, context) {
|
|
204
|
+
try {
|
|
205
|
+
const payload = readPayload(source);
|
|
206
|
+
const engine = core.createEngine(config, {
|
|
207
|
+
context: buildContext(payload, context),
|
|
208
|
+
...payload?.a !== void 0 ? { initialAssignments: payload.a } : {}
|
|
209
|
+
});
|
|
210
|
+
return engine.getVariantValue(experimentId);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
onError(error);
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function writePayload(payload, secure) {
|
|
217
|
+
const value = encodePayload(payload);
|
|
218
|
+
return serializeCookie(cookieName, value, {
|
|
219
|
+
...options,
|
|
220
|
+
...secure !== void 0 ? { secure } : {}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function toProviderProps(source, contextExtras) {
|
|
224
|
+
const payload = readPayload(source);
|
|
225
|
+
const initialContext = buildContext(payload, contextExtras);
|
|
226
|
+
const initialVariants = payload?.a ? { ...payload.a } : {};
|
|
227
|
+
return { initialContext, initialVariants };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
config,
|
|
231
|
+
getVariant,
|
|
232
|
+
getVariantValue,
|
|
233
|
+
readPayload,
|
|
234
|
+
writePayload,
|
|
235
|
+
toProviderProps
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function experimentDefault(config, experimentId) {
|
|
239
|
+
const exp = config.experiments.find((e) => e.id === experimentId);
|
|
240
|
+
return exp?.default ?? "";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/server/get-variant-ssr.ts
|
|
244
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
245
|
+
function resolveServer(rawConfig, options) {
|
|
246
|
+
if (rawConfig !== null && typeof rawConfig === "object") {
|
|
247
|
+
const hit = cache.get(rawConfig);
|
|
248
|
+
if (hit !== void 0) return hit;
|
|
249
|
+
}
|
|
250
|
+
const server = createVariantLabServer(rawConfig, options);
|
|
251
|
+
if (rawConfig !== null && typeof rawConfig === "object") {
|
|
252
|
+
cache.set(rawConfig, server);
|
|
253
|
+
}
|
|
254
|
+
return server;
|
|
255
|
+
}
|
|
256
|
+
function getVariantSSR(experimentId, source, config, options) {
|
|
257
|
+
const server = resolveServer(config, options);
|
|
258
|
+
return server.getVariant(experimentId, source, options?.context);
|
|
259
|
+
}
|
|
260
|
+
function getVariantValueSSR(experimentId, source, config, options) {
|
|
261
|
+
const server = resolveServer(config, options);
|
|
262
|
+
return server.getVariantValue(experimentId, source, options?.context);
|
|
263
|
+
}
|
|
264
|
+
function variantLabMiddleware(rawConfig, options = {}) {
|
|
265
|
+
let frozen = true;
|
|
266
|
+
try {
|
|
267
|
+
core.validateConfig(rawConfig);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
options.onError?.(error);
|
|
270
|
+
frozen = false;
|
|
271
|
+
}
|
|
272
|
+
const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
273
|
+
const onError = options.onError ?? (() => {
|
|
274
|
+
});
|
|
275
|
+
return function apply(req, response) {
|
|
276
|
+
if (!frozen) return response;
|
|
277
|
+
try {
|
|
278
|
+
const header = req.headers.get("cookie");
|
|
279
|
+
const cookies = parseCookieHeader(header ?? "");
|
|
280
|
+
const existing = decodePayload(cookies[cookieName]);
|
|
281
|
+
if (existing !== null) return response;
|
|
282
|
+
const payload = {
|
|
283
|
+
v: 1,
|
|
284
|
+
u: generateUserId(),
|
|
285
|
+
a: {}
|
|
286
|
+
};
|
|
287
|
+
const secure = req.nextUrl.protocol === "https:";
|
|
288
|
+
const setCookie = serializeCookie(cookieName, encodePayload(payload), {
|
|
289
|
+
...options,
|
|
290
|
+
secure
|
|
291
|
+
});
|
|
292
|
+
response.headers.append("set-cookie", setCookie);
|
|
293
|
+
return response;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
onError(error);
|
|
296
|
+
return response;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/index.ts
|
|
302
|
+
var VERSION = "0.0.0";
|
|
303
|
+
|
|
304
|
+
exports.DEFAULT_COOKIE_NAME = DEFAULT_COOKIE_NAME;
|
|
305
|
+
exports.DEFAULT_MAX_AGE = DEFAULT_MAX_AGE;
|
|
306
|
+
exports.VERSION = VERSION;
|
|
307
|
+
exports.createVariantLabServer = createVariantLabServer;
|
|
308
|
+
exports.decodePayload = decodePayload;
|
|
309
|
+
exports.encodePayload = encodePayload;
|
|
310
|
+
exports.generateUserId = generateUserId;
|
|
311
|
+
exports.getVariantSSR = getVariantSSR;
|
|
312
|
+
exports.getVariantValueSSR = getVariantValueSSR;
|
|
313
|
+
exports.parseCookieHeader = parseCookieHeader;
|
|
314
|
+
exports.readCookieFromSource = readCookieFromSource;
|
|
315
|
+
exports.readPayloadFromSource = readPayloadFromSource;
|
|
316
|
+
exports.serializeCookie = serializeCookie;
|
|
317
|
+
exports.variantLabMiddleware = variantLabMiddleware;
|
|
318
|
+
//# sourceMappingURL=index.cjs.map
|
|
319
|
+
//# sourceMappingURL=index.cjs.map
|