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 <romain@growthbook.io>

* sticky bucket support, readme, cleanup

* changeset

* update readme

* support client-side tracking, export types

---------

Co-authored-by: Romain Beauxis <romain@growthbook.io>
This commit is contained in:
Bryce
2025-06-02 13:09:22 -07:00
committed by GitHub
parent ac549a05ed
commit 6239e2d9a7
12 changed files with 802 additions and 13 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@flags-sdk/growthbook': minor
---
initialize
+1
View File
@@ -10,6 +10,7 @@ npm-debug.log
.turbo
.DS_Store
.vscode
.idea
.vercel
.env*.local
.svelte-kit
+1
View File
@@ -0,0 +1 @@
# @flags-sdk/growthbook
+69
View File
@@ -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<boolean>({
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.
+75
View File
@@ -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"
}
}
+263
View File
@@ -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: <T>() => Adapter<T, Attributes>;
initialize: () => Promise<GrowthBookClient>;
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<void> | undefined;
const initializeGrowthBook = async (): Promise<void> => {
let payload: FeatureApiResponse | undefined;
if (options.edgeConfig) {
try {
const edgeConfigClient = createClient(
options.edgeConfig.connectionString,
);
payload = await edgeConfigClient.get<FeatureApiResponse>(
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<GrowthBookClient> => {
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<T>(
opts: {
exposureLogging?: boolean;
} = {
exposureLogging: true,
},
): Adapter<T, Attributes> {
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<T>(key, {
...userContext,
stickyBucketAssignmentDocs,
saveStickyBucketAssignmentDoc,
}).value ?? defaultValue) as T;
}
return (growthbook.evalFeature<T>(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,
};
@@ -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',
},
],
});
});
});
});
@@ -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<ProviderData> {
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<GrowthbookFeature[] | Error> {
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 {};
}
}
@@ -0,0 +1,9 @@
{
"extends": "tsconfig/base.json",
"include": ["src"],
"compilerOptions": {
"lib": ["esnext"],
"resolveJsonModule": true,
"target": "ES2020"
}
}
@@ -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'],
});
@@ -0,0 +1,6 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [],
test: { environment: 'node' },
});
+68 -13
View File
@@ -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: