From 6239e2d9a7f7cdbabc85564c7b58ff8bb6ff96be Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 2 Jun 2025 13:09:22 -0700 Subject: [PATCH] Add GrowthBook Adapter (#133) * initial wip adapter * default adapter * provider * provider test * tsc issues * tsc issues * tsc issues * tsc * feature type safety * lock * rm .js / .d.ts / .map * fix test * readme * edge adapter * readme typo * update gb sdk ver * lock * lock * fix changelog, package.json * tighten feature() return type, remove userId from callback * fix defaultValue handling * fix decide typo * Support EXPERIMENTATION_CONFIG Co-authored-by: Romain Beauxis * sticky bucket support, readme, cleanup * changeset * update readme * support client-side tracking, export types --------- Co-authored-by: Romain Beauxis --- .changeset/dry-crabs-crash.md | 5 + .gitignore | 1 + packages/adapter-growthbook/CHANGELOG.md | 1 + packages/adapter-growthbook/README.md | 69 +++++ packages/adapter-growthbook/package.json | 75 +++++ packages/adapter-growthbook/src/index.ts | 263 ++++++++++++++++++ .../src/provider/index.test.ts | 98 +++++++ .../adapter-growthbook/src/provider/index.ts | 193 +++++++++++++ packages/adapter-growthbook/tsconfig.json | 9 + packages/adapter-growthbook/tsup.config.js | 14 + packages/adapter-growthbook/vitest.config.ts | 6 + pnpm-lock.yaml | 81 +++++- 12 files changed, 802 insertions(+), 13 deletions(-) create mode 100644 .changeset/dry-crabs-crash.md create mode 100644 packages/adapter-growthbook/CHANGELOG.md create mode 100644 packages/adapter-growthbook/README.md create mode 100644 packages/adapter-growthbook/package.json create mode 100644 packages/adapter-growthbook/src/index.ts create mode 100644 packages/adapter-growthbook/src/provider/index.test.ts create mode 100644 packages/adapter-growthbook/src/provider/index.ts create mode 100644 packages/adapter-growthbook/tsconfig.json create mode 100644 packages/adapter-growthbook/tsup.config.js create mode 100644 packages/adapter-growthbook/vitest.config.ts diff --git a/.changeset/dry-crabs-crash.md b/.changeset/dry-crabs-crash.md new file mode 100644 index 00000000..24b1e130 --- /dev/null +++ b/.changeset/dry-crabs-crash.md @@ -0,0 +1,5 @@ +--- +'@flags-sdk/growthbook': minor +--- + +initialize diff --git a/.gitignore b/.gitignore index 95c34c99..87e77e14 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ npm-debug.log .turbo .DS_Store .vscode +.idea .vercel .env*.local .svelte-kit diff --git a/packages/adapter-growthbook/CHANGELOG.md b/packages/adapter-growthbook/CHANGELOG.md new file mode 100644 index 00000000..49cafd85 --- /dev/null +++ b/packages/adapter-growthbook/CHANGELOG.md @@ -0,0 +1 @@ +# @flags-sdk/growthbook diff --git a/packages/adapter-growthbook/README.md b/packages/adapter-growthbook/README.md new file mode 100644 index 00000000..f530e4a9 --- /dev/null +++ b/packages/adapter-growthbook/README.md @@ -0,0 +1,69 @@ +# Flags SDK — GrowthBook Provider + +The [GrowthBook provider](https://flags-sdk.dev/docs/api-reference/adapters/growthbook) for the [Flags SDK](https://flags-sdk.dev/) contains support for GrowthBook's Feature Flags and Experiments. + +## Setup + +The GrowthBook provider is available in the `@flags-sdk/growthbook` module. You can install it with + +```bash +pnpm i @flags-sdk/growthbook +``` + +## Provider Instance + +You can import the default adapter instance `growthbookAdapter` from `@flags-sdk/growthbook`: + +```ts +import { growthBookAdapter } from '@flags-sdk/growthbook'; +``` + +## Example + +```ts +import { flag } from 'flags/next'; +import { growthBookAdapter } from '@flags-sdk/growthbook'; + +export const summerBannerFlag = feature({ + key: 'summer-banner', + adapter: growthBookAdapter.feature(), +}); +``` + +## Experimentation (A/B Testing) + +In order to run GrowthBook experiments, you must define a back-end tracking callback function. This is called every time a user is put into an experiment and can be used to track the exposure event in your analytics system. We recommend defining this callback in your flag definition file (e.g. `flags.ts`). + +```ts +import { after } from 'next/server'; + +growthbookAdapter.setTrackingCallback((experiment, result) => { + // Safely fire and forget async calls (Next.js) + after(async () => { + console.log('Viewed Experiment', { + experimentId: experiment.key, + variationId: result.key, + }); + }); +}); +``` + +In some situations, you may prefer to use a front-end tracking callback. This requires a bit of implementation glue. See the Vercel [Next.js + GrowthBook example](https://github.com/vercel/examples/tree/main/flags-sdk/growthbook) for details. + +### Sticky Bucketing + +To implement sticky bucketing (required for Bandits), you may create any `StickyBucketService` instance and apply it to the adapter (recommended in `flags.ts`). We recommend using our Redis service for ease of implementation. + +```ts +import { RedisStickyBucketService } from '@growthbook/growthbook'; +import Redis from 'ioredis'; + +const redis = new Redis(process.env.REDIS_CONNECTION_URL); +const redisStickyBucketService = new RedisStickyBucketService({ redis }); + +growthbookAdapter.setStickyBucketService(redisStickyBucketService); +``` + +## Documentation + +Please check out the [GrowthBook provider documentation](https://flags-sdk.dev/docs/api-reference/adapters/growthbook) for more information. diff --git a/packages/adapter-growthbook/package.json b/packages/adapter-growthbook/package.json new file mode 100644 index 00000000..00feae81 --- /dev/null +++ b/packages/adapter-growthbook/package.json @@ -0,0 +1,75 @@ +{ + "name": "@flags-sdk/growthbook", + "version": "0.0.1", + "description": "GrowthBook provider for the Flags SDK", + "homepage": "https://flags-sdk.dev", + "bugs": { + "url": "https://github.com/vercel/flags/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vercel/flags.git" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./provider": { + "types": "./dist/provider/index.d.ts", + "import": "./dist/provider/index.js", + "require": "./dist/provider/index.cjs" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + ".": [ + "./dist/index.d.ts", + "./dist/index.d.cts" + ], + "provider": [ + "./dist/provider/index.d.ts", + "./dist/provider/index.d.cts" + ] + } + }, + "files": [ + "dist", + "CHANGELOG.md" + ], + "scripts": { + "build": "rimraf dist && tsup", + "dev": "tsup --watch --clean=false", + "eslint": "eslint-runner", + "eslint:fix": "eslint-runner --fix", + "test": "vitest --run", + "test:watch": "vitest", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@growthbook/growthbook": "^1.5.1", + "@vercel/edge-config": "^1.2.0", + "@vercel/functions": "^1.5.2" + }, + "devDependencies": { + "@types/node": "20.11.17", + "eslint-config-custom": "workspace:*", + "flags": "workspace:*", + "msw": "2.6.4", + "rimraf": "6.0.1", + "tsconfig": "workspace:*", + "tsup": "8.0.1", + "typescript": "5.6.3", + "vite": "5.1.1", + "vitest": "1.4.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/adapter-growthbook/src/index.ts b/packages/adapter-growthbook/src/index.ts new file mode 100644 index 00000000..7db6e4ac --- /dev/null +++ b/packages/adapter-growthbook/src/index.ts @@ -0,0 +1,263 @@ +import type { Adapter } from 'flags'; +import { createClient } from '@vercel/edge-config'; +import { + GrowthBookClient, + type Attributes, + type ClientOptions, + type InitOptions, + type UserContext, + type TrackingCallback, + type TrackingCallbackWithUser, + type FeatureApiResponse, + type StickyBucketService, + type StickyAssignmentsDocument, +} from '@growthbook/growthbook'; + +export { getProviderData } from './provider'; +export { + GrowthBookClient, + type Attributes, + type ClientOptions, + type InitOptions, + type UserContext, + type TrackingCallback, + type TrackingCallbackWithUser, + type FeatureApiResponse, + type StickyBucketService, + type StickyAssignmentsDocument, +}; + +type EdgeConfig = { + connectionString: string; + /** Defaults to `options.clientKey` **/ + itemKey?: string; +}; + +type AdapterResponse = { + feature: () => Adapter; + initialize: () => Promise; + setTrackingCallback: (cb: TrackingCallback) => void; + setStickyBucketService: (stickyBucketService: StickyBucketService) => void; + stickyBucketService?: StickyBucketService; + growthbook: GrowthBookClient; +}; + +/** + * Create a GrowthBook adapter for use with the Flags SDK. + */ +export function createGrowthbookAdapter(options: { + /** GrowthBook SDK key **/ + clientKey: string; + /** Callback to log experiment exposures **/ + trackingCallback?: TrackingCallback; + /** Override the features API endpoint for self-hosted users **/ + apiHost?: string; + /** Override the application URL for self-hosted users **/ + appOrigin?: string; + /** Optional GrowthBook SDK constructor options **/ + clientOptions?: ClientOptions; + /** Optional GrowthBook SDK init() options **/ + initOptions?: InitOptions; + /** Optional StickyBucketService (reduces variation hopping, required for Bandits) **/ + stickyBucketService?: StickyBucketService; + /** Provide Edge Config details to use the optional Edge Config adapter */ + edgeConfig?: EdgeConfig; +}): AdapterResponse { + let trackingCallback = options.trackingCallback; + let stickyBucketService = options.stickyBucketService; + + const growthbook = new GrowthBookClient({ + clientKey: options.clientKey, + apiHost: options.apiHost || 'https://cdn.growthbook.io', + ...(options.clientOptions || {}), + }); + + let _initializePromise: Promise | undefined; + + const initializeGrowthBook = async (): Promise => { + let payload: FeatureApiResponse | undefined; + if (options.edgeConfig) { + try { + const edgeConfigClient = createClient( + options.edgeConfig.connectionString, + ); + payload = await edgeConfigClient.get( + options.edgeConfig.itemKey || options.clientKey, + ); + if (!payload) { + console.error('No payload found in edge config'); + } + } catch (e) { + console.error('Error fetching edge config', e); + } + } + + await growthbook.init({ + streaming: false, + payload, + ...(options.initOptions || {}), + }); + }; + + /** + * Initialize the GrowthBook SDK. + * + * This must be called before checking feature flags or experiments. + * It is deduplicated to prevent multiple calls from being made. + * You can pre-initialize the SDK by calling `adapter.initialize()`, + * otherwise it will be initialized lazily when needed. + */ + const initialize = async (): Promise => { + if (!_initializePromise) { + _initializePromise = initializeGrowthBook(); + } + await _initializePromise; + return growthbook; + }; + + function origin(prefix: string) { + return (key: string) => { + const appOrigin = options.appOrigin || 'https://app.growthbook.io'; + return `${appOrigin}/${prefix}/${key}`; + }; + } + + /** + * Resolve a feature flag. + * + * Implements `decide` to resolve the feature with `GrowthBook.evalFeature` + * Implements `origin` to link to the flag in the GrowthBook app + */ + function feature( + opts: { + exposureLogging?: boolean; + } = { + exposureLogging: true, + }, + ): Adapter { + return { + origin: origin('features'), + decide: async ({ key, entities, defaultValue }) => { + await initialize(); + const userContext: UserContext = { + attributes: entities as Attributes, + trackingCallback: opts.exposureLogging ? trackingCallback : undefined, + }; + if (stickyBucketService && opts.exposureLogging) { + const { stickyBucketAssignmentDocs, saveStickyBucketAssignmentDoc } = + await growthbook.applyStickyBuckets( + userContext, + stickyBucketService, + ); + return (growthbook.evalFeature(key, { + ...userContext, + stickyBucketAssignmentDocs, + saveStickyBucketAssignmentDoc, + }).value ?? defaultValue) as T; + } + return (growthbook.evalFeature(key, userContext).value ?? + defaultValue) as T; + }, + }; + } + + function setTrackingCallback(cb: TrackingCallback) { + trackingCallback = cb; + } + + function setStickyBucketService(sbs: StickyBucketService) { + stickyBucketService = sbs; + } + + return { + feature, + initialize, + setTrackingCallback, + setStickyBucketService, + stickyBucketService, + growthbook, + }; +} + +let defaultGrowthbookAdapter: AdapterResponse | undefined; + +export function resetDefaultGrowthbookAdapter() { + defaultGrowthbookAdapter = undefined; +} + +/** + * Equivalent to `createGrowthbookAdapter` but with default environment variable names. + * + * Required: + * - `GROWTHBOOK_CLIENT_KEY` - GrowthBook SDK key + * + * Optional: + * - `GROWTHBOOK_API_HOST` - Override the SDK API endpoint for self-hosted users + * - `GROWTHBOOK_APP_ORIGIN` - Override the application URL for self-hosted users + * - `GROWTHBOOK_EDGE_CONNECTION_STRING` - Edge Config connection string + * - `EXPERIMENTATION_CONFIG` - fallback for GROWTHBOOK_EDGE_CONNECTION_STRING + * - `GROWTHBOOK_EDGE_CONFIG_ITEM_KEY` - Override the item key for Edge Config (defaults to GROWTHBOOK_CLIENT_KEY) + */ +export function getOrCreateDefaultGrowthbookAdapter(): AdapterResponse { + if (defaultGrowthbookAdapter) { + return defaultGrowthbookAdapter; + } + const clientKey = process.env.GROWTHBOOK_CLIENT_KEY as string; + if (!clientKey) { + throw new Error('Missing GROWTHBOOK_CLIENT_KEY env var'); + } + const apiHost = process.env.GROWTHBOOK_API_HOST; + const appOrigin = process.env.GROWTHBOOK_APP_ORIGIN; + const connectionString = + process.env.GROWTHBOOK_EDGE_CONNECTION_STRING || + process.env.EXPERIMENTATION_CONFIG; + const itemKey = process.env.GROWTHBOOK_EDGE_CONFIG_ITEM_KEY; + + let edgeConfig: EdgeConfig | undefined; + if (connectionString) { + edgeConfig = { + connectionString, + itemKey, + }; + } + + defaultGrowthbookAdapter = createGrowthbookAdapter({ + clientKey, + apiHost, + appOrigin, + edgeConfig, + }); + + return defaultGrowthbookAdapter; +} + +/** + * The default GrowthBook adapter. + * + * This is a convenience object that pre-initializes the GrowthBook SDK, provides + * an adapter function for features, and provides a hook to set the experiment exposure + * tracking callback. + * + * This is the recommended way to use the GrowthBook adapter. + * + * ```ts + * // flags.ts + * import { flag } from 'flags/next'; + * import { growthbookAdapter } from '@flags-sdk/growthbook'; + * + * const flag = flag({ + * key: 'my-flag', + * defaultValue: false, + * adapter: growthbookAdapter.feature(), + * }); + * ``` + */ +export const growthbookAdapter: AdapterResponse = { + feature: (...args) => getOrCreateDefaultGrowthbookAdapter().feature(...args), + initialize: () => getOrCreateDefaultGrowthbookAdapter().initialize(), + setTrackingCallback: (...args) => + getOrCreateDefaultGrowthbookAdapter().setTrackingCallback(...args), + setStickyBucketService: (...args) => + getOrCreateDefaultGrowthbookAdapter().setStickyBucketService(...args), + growthbook: getOrCreateDefaultGrowthbookAdapter().growthbook, +}; diff --git a/packages/adapter-growthbook/src/provider/index.test.ts b/packages/adapter-growthbook/src/provider/index.test.ts new file mode 100644 index 00000000..7b973df2 --- /dev/null +++ b/packages/adapter-growthbook/src/provider/index.test.ts @@ -0,0 +1,98 @@ +import { getProviderData } from '..'; +import { setupServer } from 'msw/node'; +import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; + +const restHandlers = [ + http.get('https://api.growthbook.io/api/v1/features', () => { + return HttpResponse.json({ + features: [ + { + id: 'showdemo', + defaultValue: 'true', + environments: { + production: { + enabled: true, + defaultValue: 'true', + rules: [ + { + id: 'fr_abc', + type: 'force', + enabled: true, + value: 'false', + condition: '{"id": "1"}', + coverage: 1, + scheduleRules: [], + savedGroupTargeting: [], + prerequisites: [], + }, + ], + }, + }, + description: 'Show demo banner', + valueType: 'boolean', + dateCreated: '2023-01-01T00:00:00.000Z', + dateUpdated: '2023-01-02T00:00:00.000Z', + origin: 'https://app.growthbook.io/features/showdemo', + }, + ], + limit: 10, + offset: 0, + count: 1, + total: 1, + hasMore: false, + nextOffset: 0, + }); + }), +]; + +const responseFixture = { + hints: [], + definitions: { + showdemo: { + description: 'Show demo banner', + origin: 'https://app.growthbook.io/features/showdemo', + createdAt: 1672531200000, + updatedAt: 1672617600000, + options: [ + { label: 'On', value: true }, + { label: 'Off', value: false }, + ], + }, + }, +}; + +const server = setupServer(...restHandlers); +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +afterAll(() => server.close()); +afterEach(() => server.resetHandlers()); + +describe('getProviderData', () => { + describe('when called with valid params', () => { + it('should fetch and return', async () => { + await expect( + getProviderData({ + apiKey: 'secret_user_a1b2c3d4e5', + }), + ).resolves.toEqual(responseFixture); + }); + }); + + describe('when called with invalid params', () => { + it('should return hints if missing API key', async () => { + await expect( + getProviderData({ + apiKey: '', + }), + ).resolves.toEqual({ + definitions: {}, + hints: [ + { + key: 'growthbook/missing-api-key', + text: 'Missing GrowthBook API Key', + }, + ], + }); + }); + }); +}); diff --git a/packages/adapter-growthbook/src/provider/index.ts b/packages/adapter-growthbook/src/provider/index.ts new file mode 100644 index 00000000..03a3f6b5 --- /dev/null +++ b/packages/adapter-growthbook/src/provider/index.ts @@ -0,0 +1,193 @@ +import type { JsonValue, ProviderData } from 'flags'; + +interface GrowthbookFeature { + id: string; + dateCreated: string; + dateUpdated: string; + archived: boolean; + description: string; + owner: string; + project: string; + valueType: 'string' | 'number' | 'boolean' | 'json'; + defaultValue: string; + tags: string[]; + environments: Record< + string, + { + enabled: boolean; + defaultValue: string; + rules: {}[]; + } + >; + prerequisites: []; + revision: {}; +} + +interface GrowthbookFeaturesResponse { + features: GrowthbookFeature[]; + limit: number; + offset: number; + count: number; + total: number; + hasMore: boolean; + nextOffset: number; +} + +export async function getProviderData(options: { + /** GrowthBook API Key or Personal Access Token **/ + apiKey: string; + /** Override the application API host for self-hosted users **/ + appApiHost?: string; + /** Override the application URL for self-hosted users **/ + appOrigin?: string; + /** Provide your GrowthBook SDK key to filter flag definitions **/ + clientKey?: string; +}): Promise { + const apiKey = options.apiKey; + const appApiHost = options.appApiHost || 'https://api.growthbook.io'; + const appOrigin = options.appOrigin || 'https://app.growthbook.io'; + const clientKey = options.clientKey; + + if (!apiKey) { + return { + definitions: {}, + hints: [ + { + key: 'growthbook/missing-api-key', + text: 'Missing GrowthBook API Key', + }, + ], + }; + } + + const hints: ProviderData['hints'] = []; + + const features = await getFeatures({ apiKey, appApiHost, clientKey }); + + if (features instanceof Error) { + return { + definitions: {}, + hints: [ + { + key: 'growthbook/fetch-failed', + text: features.message, + }, + ], + }; + } + + const definitions: ProviderData['definitions'] = {}; + for (const feature of features) { + if (feature.archived) continue; + + let options: { label: string; value: JsonValue }[] = []; + let invalidType: never; + + switch (feature.valueType) { + case 'boolean': + options = [ + { label: 'On', value: true }, + { label: 'Off', value: false }, + ]; + break; + case 'string': + options = [ + { label: `"${feature.defaultValue}"`, value: feature.defaultValue }, + ]; + break; + case 'number': + options = [ + { + label: String(feature.defaultValue), + value: Number(feature.defaultValue), + }, + ]; + break; + case 'json': + options = [ + { + label: 'JSON', + value: tryParseJSON(feature.defaultValue), + }, + ]; + break; + default: + invalidType = feature.valueType; + hints.push({ + key: 'growthbook/invalid-feature-type', + text: `Invalid feature type: ${feature.valueType}`, + }); + } + const typeLabel = + feature.valueType === 'json' + ? 'JSON' + : feature.valueType.charAt(0).toUpperCase() + + feature.valueType.slice(1); + + definitions[feature.id] = { + description: `[${typeLabel}] ${feature.description}`, + origin: `${appOrigin}/features/${feature.id}`, + options, + createdAt: new Date(feature.dateCreated).getTime(), + updatedAt: new Date(feature.dateUpdated).getTime(), + }; + } + + return { definitions, hints }; +} + +/** + * Fetch all Feature Flags. + */ +async function getFeatures(options: { + apiKey: string; + appApiHost: string; + clientKey?: string; +}): Promise { + try { + const features: GrowthbookFeaturesResponse['features'] = []; + + let offset = 0; + let hasMore = true; + + while (hasMore) { + const params = new URLSearchParams(); + if (offset) params.append('offset', String(offset)); + if (options.clientKey) params.append('clientKey', options.clientKey); + const qs = params.toString() ? `?${params.toString()}` : ''; + const url = `${options.appApiHost}/api/v1/features${qs}`; + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${options.apiKey}`, + }, + // @ts-expect-error some Next.js versions need this + cache: 'no-store', + }); + + if (response.status !== 200) { + await response.arrayBuffer(); // ensure stream is drained + return new Error( + `Failed to fetch GrowthBook (Received ${response.status} response)`, + ); + } + + const body = (await response.json()) as GrowthbookFeaturesResponse; + features.push(...body.features); + hasMore = body.hasMore; + offset = body.nextOffset; + } + + return features; + } catch (e) { + return e instanceof Error ? e : new Error(String(e)); + } +} + +function tryParseJSON(value: string): JsonValue { + try { + return JSON.parse(value); + } catch { + return {}; + } +} diff --git a/packages/adapter-growthbook/tsconfig.json b/packages/adapter-growthbook/tsconfig.json new file mode 100644 index 00000000..2b2de3db --- /dev/null +++ b/packages/adapter-growthbook/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "tsconfig/base.json", + "include": ["src"], + "compilerOptions": { + "lib": ["esnext"], + "resolveJsonModule": true, + "target": "ES2020" + } +} diff --git a/packages/adapter-growthbook/tsup.config.js b/packages/adapter-growthbook/tsup.config.js new file mode 100644 index 00000000..faa0ce0a --- /dev/null +++ b/packages/adapter-growthbook/tsup.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +// eslint-disable-next-line import/no-default-export -- [@vercel/style-guide@5 migration] +export default defineConfig({ + entry: ['src/index.ts', 'src/provider/index.ts'], + format: ['esm', 'cjs'], + splitting: true, + sourcemap: true, + minify: false, + clean: false, + skipNodeModulesBundle: true, + dts: true, + external: ['node_modules'], +}); diff --git a/packages/adapter-growthbook/vitest.config.ts b/packages/adapter-growthbook/vitest.config.ts new file mode 100644 index 00000000..4af8bf98 --- /dev/null +++ b/packages/adapter-growthbook/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [], + test: { environment: 'node' }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1afad952..985d722e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -387,6 +387,49 @@ importers: specifier: 1.4.0 version: 1.4.0(@types/node@20.11.17) + packages/adapter-growthbook: + dependencies: + '@growthbook/growthbook': + specifier: ^1.5.1 + version: 1.5.1 + '@vercel/edge-config': + specifier: ^1.2.0 + version: 1.4.0 + '@vercel/functions': + specifier: ^1.5.2 + version: 1.6.0 + devDependencies: + '@types/node': + specifier: 20.11.17 + version: 20.11.17 + eslint-config-custom: + specifier: workspace:* + version: link:../../tooling/eslint-config-custom + flags: + specifier: workspace:* + version: link:../flags + msw: + specifier: 2.6.4 + version: 2.6.4(@types/node@20.11.17)(typescript@5.6.3) + rimraf: + specifier: 6.0.1 + version: 6.0.1 + tsconfig: + specifier: workspace:* + version: link:../../tooling/tsconfig + tsup: + specifier: 8.0.1 + version: 8.0.1(typescript@5.6.3) + typescript: + specifier: 5.6.3 + version: 5.6.3 + vite: + specifier: 5.1.1 + version: 5.1.1(@types/node@20.11.17) + vitest: + specifier: 1.4.0 + version: 1.4.0(@types/node@20.11.17) + packages/adapter-happykit: dependencies: '@happykit/flags': @@ -711,7 +754,7 @@ importers: version: 5.10.0 react-dom: specifier: '*' - version: 19.0.0(react@19.2.0-canary-4448b187-20250515) + version: 19.0.0(react@19.2.0-canary-526dd340-20250602) devDependencies: '@arethetypeswrong/cli': specifier: 0.17.3 @@ -733,10 +776,10 @@ importers: version: 2.6.4(@types/node@20.11.17)(typescript@5.6.3) next: specifier: 15.1.4 - version: 15.1.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0)(react@19.2.0-canary-4448b187-20250515) + version: 15.1.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0)(react@19.2.0-canary-526dd340-20250602) react: specifier: canary - version: 19.2.0-canary-4448b187-20250515 + version: 19.2.0-canary-526dd340-20250602 tsconfig: specifier: workspace:* version: link:../../tooling/tsconfig @@ -2534,6 +2577,13 @@ packages: resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} dev: false + /@growthbook/growthbook@1.5.1: + resolution: {integrity: sha512-t0cj+3i2YtZ4Y/v1xl2fThogLMvjjUy3fx7ywGoLPgKZdhYy2HSXUGY1lLM/I0FMTEd2L2lkJHEzeUKpW6Ew3g==} + engines: {node: '>=10'} + dependencies: + dom-mutator: 0.6.0 + dev: false + /@happykit/flags@3.3.0(next@15.2.0)(react@19.0.0): resolution: {integrity: sha512-QyywkKYctdo0bnNf+bBd+3B/3Rsnjdaz+jtVA+jSqLxm0/lw51Z1+q2sEfc62MyNwQnY6w7eqajdwhuvH3VWAw==} peerDependencies: @@ -7471,6 +7521,11 @@ packages: dependencies: esutils: 2.0.3 + /dom-mutator@0.6.0: + resolution: {integrity: sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg==} + engines: {node: '>=10'} + dev: false + /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} @@ -11265,7 +11320,7 @@ packages: - babel-plugin-macros dev: false - /next@15.1.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0)(react@19.2.0-canary-4448b187-20250515): + /next@15.1.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.0.0)(react@19.2.0-canary-526dd340-20250602): resolution: {integrity: sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true @@ -11293,9 +11348,9 @@ packages: busboy: 1.6.0 caniuse-lite: 1.0.30001704 postcss: 8.4.31 - react: 19.2.0-canary-4448b187-20250515 - react-dom: 19.0.0(react@19.2.0-canary-4448b187-20250515) - styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.2.0-canary-4448b187-20250515) + react: 19.2.0-canary-526dd340-20250602 + react-dom: 19.0.0(react@19.2.0-canary-526dd340-20250602) + styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.2.0-canary-526dd340-20250602) optionalDependencies: '@next/swc-darwin-arm64': 15.1.4 '@next/swc-darwin-x64': 15.1.4 @@ -12150,12 +12205,12 @@ packages: scheduler: 0.25.0 dev: false - /react-dom@19.0.0(react@19.2.0-canary-4448b187-20250515): + /react-dom@19.0.0(react@19.2.0-canary-526dd340-20250602): resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: react: ^19.0.0 dependencies: - react: 19.2.0-canary-4448b187-20250515 + react: 19.2.0-canary-526dd340-20250602 scheduler: 0.25.0 /react-dom@19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028): @@ -12260,8 +12315,8 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react@19.2.0-canary-4448b187-20250515: - resolution: {integrity: sha512-XU13V8AnU5+2/PeqZela97gmLo1nxDOJJHvuDogNdrodwLF9vu3Fjxf/Y8uBfeZzhP3qm+psotuvqZa+fMIbdQ==} + /react@19.2.0-canary-526dd340-20250602: + resolution: {integrity: sha512-Ebiwh5tDLCZx7z5KYQQ3IPezOKbgN2ZPGXLh/akSEd3ve6NhMj/Zy+x7lnMVweFil29iC16ujVeLCGU5wf+R5w==} engines: {node: '>=0.10.0'} /read-cache@1.0.0: @@ -13151,7 +13206,7 @@ packages: react: 19.0.0-rc.1 dev: false - /styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.2.0-canary-4448b187-20250515): + /styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.2.0-canary-526dd340-20250602): resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -13166,7 +13221,7 @@ packages: dependencies: '@babel/core': 7.26.10 client-only: 0.0.1 - react: 19.2.0-canary-4448b187-20250515 + react: 19.2.0-canary-526dd340-20250602 dev: true /sucrase@3.35.0: