From 5c3a6c974bde2c5adf9aaa28c6a202020aa01285 Mon Sep 17 00:00:00 2001 From: cocvn157 <120394358+cocvn157@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:22:55 +0700 Subject: [PATCH 1/3] feat(vue): add @tryabby/vue package --- .changeset/vue-integration.md | 5 + packages/vue/.gitignore | 1 + packages/vue/README.md | 104 +++++ packages/vue/package.json | 53 +++ packages/vue/src/StorageService.ts | 76 ++++ packages/vue/src/createAbby.ts | 422 ++++++++++++++++++++ packages/vue/src/index.ts | 7 + packages/vue/test/mocks/handlers.ts | 43 ++ packages/vue/test/mocks/server.ts | 4 + packages/vue/test/setup.ts | 10 + packages/vue/test/ssr.test.ts | 179 +++++++++ packages/vue/test/types.test.ts | 155 ++++++++ packages/vue/test/useAbby.test.ts | 507 ++++++++++++++++++++++++ packages/vue/tsconfig.json | 7 + packages/vue/tsup.config.ts | 9 + packages/vue/vite.config.ts | 11 + pnpm-lock.yaml | 594 +++++++++++++++++++++++----- 17 files changed, 2081 insertions(+), 106 deletions(-) create mode 100644 .changeset/vue-integration.md create mode 100644 packages/vue/.gitignore create mode 100644 packages/vue/README.md create mode 100644 packages/vue/package.json create mode 100644 packages/vue/src/StorageService.ts create mode 100644 packages/vue/src/createAbby.ts create mode 100644 packages/vue/src/index.ts create mode 100644 packages/vue/test/mocks/handlers.ts create mode 100644 packages/vue/test/mocks/server.ts create mode 100644 packages/vue/test/setup.ts create mode 100644 packages/vue/test/ssr.test.ts create mode 100644 packages/vue/test/types.test.ts create mode 100644 packages/vue/test/useAbby.test.ts create mode 100644 packages/vue/tsconfig.json create mode 100644 packages/vue/tsup.config.ts create mode 100644 packages/vue/vite.config.ts diff --git a/.changeset/vue-integration.md b/.changeset/vue-integration.md new file mode 100644 index 00000000..dc7e5272 --- /dev/null +++ b/.changeset/vue-integration.md @@ -0,0 +1,5 @@ +--- +"@tryabby/vue": minor +--- + +Add the `@tryabby/vue` package with Vue 3 composables (`useAbby`, `useFeatureFlag`, `useRemoteConfig`), `AbbyVuePlugin`, `AbbyProvider`, and SSR/hydration support. diff --git a/packages/vue/.gitignore b/packages/vue/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/vue/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 00000000..171ce850 --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,104 @@ +# @tryabby/vue + +Vue 3 integration for [Abby](https://www.tryabby.com) (A/B testing, feature flags, and remote config). Supports Vue 3 and Nuxt 3 with full TypeScript inference. + +## Installation + +```bash +pnpm add @tryabby/vue +# or +npm install @tryabby/vue +``` + +## Setup + +Create your Abby instance in a separate file (e.g. `./abby.ts`): + +```ts +import { createAbby } from "@tryabby/vue"; + +export const abby = createAbby({ + projectId: "", + currentEnvironment: "production", + environments: ["development", "production"], + tests: { + "footer-abtest": { variants: ["current", "new"] }, + }, + flags: ["new-features"], + remoteConfig: { + "button-color": "String", + }, +}); +``` + +### Register the Vue Plugin or Provider + +Register `AbbyVuePlugin` app-wide with `app.use()`: + +```ts +import { createApp } from "vue"; +import { abby } from "./abby"; + +const app = createApp(App); +app.use(abby.AbbyVuePlugin); +app.mount("#app"); +``` + +Or wrap parts of your component tree with ``: + +```vue + + + +``` + +## Composables + +### `useAbby` + +```vue + + + +``` + +### `useFeatureFlag` + +```vue + + + +``` + +### `useRemoteConfig` + +```vue + + + +``` diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 00000000..2e62b35d --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,53 @@ +{ + "name": "@tryabby/vue", + "version": "0.1.0", + "description": "Vue 3 integration for Abby", + "homepage": "https://docs.tryabby.com", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": ["dist"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup src/", + "dev": "pnpm run build --watch", + "test": "vitest" + }, + "keywords": [ + "vue", + "vue3", + "nuxt", + "feature-flags", + "ab-testing", + "remote-config" + ], + "author": "", + "license": "ISC", + "peerDependencies": { + "vue": "^3.0.0" + }, + "devDependencies": { + "@types/js-cookie": "^3.0.3", + "@vue/server-renderer": "^3.4.0", + "@vue/test-utils": "^2.4.6", + "jsdom": "^20.0.3", + "msw": "^0.49.1", + "node-fetch": "^3.3.0", + "tsconfig": "workspace:*", + "tsup": "^6.5.0", + "typescript": "5.5.4", + "vite": "5.4.0", + "vitest": "2.0.5", + "vue": "^3.4.0" + }, + "dependencies": { + "@tryabby/core": "workspace:*", + "js-cookie": "^3.0.1" + } +} diff --git a/packages/vue/src/StorageService.ts b/packages/vue/src/StorageService.ts new file mode 100644 index 00000000..7633105e --- /dev/null +++ b/packages/vue/src/StorageService.ts @@ -0,0 +1,76 @@ +import { + type IStorageService, + type StorageServiceOptions, + getABStorageKey, + getFFStorageKey, + getRCStorageKey, +} from "@tryabby/core"; +import Cookie from "js-cookie"; + +const DEFAULT_COOKIE_AGE = 365; + +class ABStorageService implements IStorageService { + get(projectId: string, testName: string): string | null { + const retrievedValue = Cookie.get(getABStorageKey(projectId, testName)); + if (!retrievedValue) return null; + + return retrievedValue; + } + + set( + projectId: string, + testName: string, + value: string, + options?: StorageServiceOptions + ): void { + Cookie.set(getABStorageKey(projectId, testName), value, { + expires: options?.expiresInDays + ? options.expiresInDays + : DEFAULT_COOKIE_AGE, + }); + } + + remove(projectId: string, testName: string): void { + Cookie.remove(getABStorageKey(projectId, testName)); + } +} + +class FFStorageService implements IStorageService { + get(projectId: string, flagName: string): string | null { + const retrievedValue = Cookie.get(getFFStorageKey(projectId, flagName)); + if (!retrievedValue) return null; + + return retrievedValue; + } + + set(projectId: string, flagName: string, value: string): void { + Cookie.set(getFFStorageKey(projectId, flagName), value, { + expires: DEFAULT_COOKIE_AGE, + }); + } + + remove(projectId: string, flagName: string): void { + Cookie.remove(getFFStorageKey(projectId, flagName)); + } +} + +class RCStorageService implements IStorageService { + get(projectId: string, key: string): string | null { + const retrievedValue = Cookie.get(getRCStorageKey(projectId, key)); + return retrievedValue ?? null; + } + + set(projectId: string, key: string, value: string): void { + Cookie.set(getRCStorageKey(projectId, key), value, { + expires: DEFAULT_COOKIE_AGE, + }); + } + + remove(projectId: string, key: string): void { + Cookie.remove(getRCStorageKey(projectId, key)); + } +} + +export const TestStorageService = new ABStorageService(); +export const FlagStorageService = new FFStorageService(); +export const RemoteConfigStorageService = new RCStorageService(); diff --git a/packages/vue/src/createAbby.ts b/packages/vue/src/createAbby.ts new file mode 100644 index 00000000..1f6b6a91 --- /dev/null +++ b/packages/vue/src/createAbby.ts @@ -0,0 +1,422 @@ +import { + type ABConfig, + Abby, + type AbbyConfig, + type AbbyDataResponse, + AbbyEventType, + HttpService, + type RemoteConfigValueString, + type RemoteConfigValueStringToType, + type ValidatorType, +} from "@tryabby/core"; +import type { Infer } from "@tryabby/core/validation"; +import { + type App, + type Component, + type ComputedRef, + type InjectionKey, + type Plugin, + type PropType, + type ShallowRef, + computed, + defineComponent, + getCurrentInstance, + inject, + onMounted, + onUnmounted, + provide, + shallowRef, + watch, +} from "vue"; +import { + FlagStorageService, + RemoteConfigStorageService, + TestStorageService, +} from "./StorageService"; + +export type AbbyDevtoolProps = { + position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; + defaultShow?: boolean; + shortcut?: string | Array; +}; + +export type withDevtoolsFunction = ( + factory: { + create: (props: AbbyDevtoolProps & { abby: unknown }) => () => void; + }, + props?: Omit & { + dangerouslyForceShow?: boolean; + } +) => Component; + +export type ABTestReturnValue = Lookup extends undefined + ? TestVariant + : TestVariant extends keyof Lookup + ? Lookup[TestVariant] + : never; + +export function createAbby< + const FlagName extends string, + const TestName extends string, + const Tests extends Record, + const RemoteConfig extends Record, + const RemoteConfigName extends Extract, + const User extends Record = Record< + string, + ValidatorType + >, +>( + abbyConfig: AbbyConfig< + FlagName, + Tests, + string[], + RemoteConfigName, + RemoteConfig, + User + > +) { + const abby = new Abby< + FlagName, + TestName, + Tests, + RemoteConfig, + RemoteConfigName, + string[], + User + >( + abbyConfig, + { + get: (key: string) => { + if (typeof window === "undefined") return null; + return TestStorageService.get(abbyConfig.projectId, key); + }, + set: (key: string, value: any) => { + if ( + typeof window === "undefined" || + abbyConfig.cookies?.disableByDefault + ) + return; + TestStorageService.set(abbyConfig.projectId, key, value); + }, + }, + { + get: (key: string) => { + if (typeof window === "undefined") return null; + return FlagStorageService.get(abbyConfig.projectId, key); + }, + set: (key: string, value: any) => { + if (typeof window === "undefined") return; + FlagStorageService.set(abbyConfig.projectId, key, value); + }, + }, + { + get: (key: string) => { + if (typeof window === "undefined") return null; + return RemoteConfigStorageService.get(abbyConfig.projectId, key); + }, + set: (key: string, value: any) => { + if (typeof window === "undefined") return; + RemoteConfigStorageService.set(abbyConfig.projectId, key, value); + }, + } + ); + + const config = abbyConfig; + + type AbbyProjectData = ReturnType; + + const abbyState = shallowRef({ + tests: {}, + flags: {}, + remoteConfig: {}, + } as AbbyProjectData); + + abby.subscribe((data) => { + abbyState.value = data as AbbyProjectData; + }); + + let inited = false; + + const initData = (initialData?: AbbyDataResponse) => { + if (initialData) { + inited = true; + abbyState.value = abby.init(initialData); + return; + } + if (inited || typeof window === "undefined") return; + inited = true; + + abby.loadProjectData().then((data) => { + if (!data) return; + abbyState.value = data; + }); + }; + + const AbbyKey: InjectionKey> = Symbol( + `abby:${config.projectId}` + ); + + const resolveState = (): ShallowRef => { + const instance = getCurrentInstance(); + const injected = instance ? inject(AbbyKey, undefined) : undefined; + if (injected) return injected; + initData(); + return abbyState; + }; + + const useAbby = < + K extends keyof Tests, + TestVariant extends Tests[K]["variants"][number], + LookupValue, + const Lookup extends + | Record + | undefined = undefined, + >( + name: K, + lookupObject?: Lookup + ): { + variant: ComputedRef>; + onAct: () => void; + } => { + const state = resolveState(); + + const selectedVariant = computed( + () => + (state.value.tests[name as unknown as TestName]?.selectedVariant ?? + "") as TestVariant + ); + + const variant = computed( + () => + (lookupObject + ? (lookupObject as Record)[ + selectedVariant.value + ] + : selectedVariant.value) as ABTestReturnValue + ); + + const sendEvent = (type: AbbyEventType) => { + if (!name || !selectedVariant.value) return; + + HttpService.sendData({ + url: config.apiUrl, + type, + data: { + projectId: config.projectId, + selectedVariant: selectedVariant.value as string, + testName: name as string, + }, + }); + }; + + const onAct = () => sendEvent(AbbyEventType.ACT); + + watch(selectedVariant, () => sendEvent(AbbyEventType.PING)); + + if (getCurrentInstance()) { + onMounted(() => sendEvent(AbbyEventType.PING)); + } + + return { + onAct, + variant, + }; + }; + + const useFeatureFlag = (name: FlagName): ComputedRef => { + const state = resolveState(); + return computed(() => { + void state.value; + return abby.getFeatureFlag(name); + }); + }; + + const useFeatureFlags = (): ComputedRef< + Array<{ name: FlagName; value: boolean }> + > => { + const state = resolveState(); + return computed(() => + (Object.keys(state.value.flags) as Array).map((flagName) => ({ + name: flagName, + value: state.value.flags[flagName].value, + })) + ); + }; + + const useRemoteConfigVariables = (): ComputedRef< + Array<{ + name: RemoteConfigName; + value: RemoteConfigValueStringToType; + }> + > => { + const state = resolveState(); + return computed(() => + (Object.keys(state.value.remoteConfig) as Array).map( + (configName) => ({ + name: configName, + value: state.value.remoteConfig[configName] + .value as RemoteConfigValueStringToType< + RemoteConfig[RemoteConfigName] + >, + }) + ) + ); + }; + + const useRemoteConfig = < + T extends RemoteConfigName, + Config extends RemoteConfig[T], + >( + remoteConfigName: T + ): ComputedRef> => { + const state = resolveState(); + return computed(() => { + void state.value; + return abby.getRemoteConfig( + remoteConfigName + ) as RemoteConfigValueStringToType; + }); + }; + + const AbbyProvider = defineComponent({ + name: "AbbyProvider", + props: { + initialData: { + type: Object as PropType, + required: false, + default: undefined, + }, + }, + setup(props, { slots }) { + provide(AbbyKey, abbyState); + initData(props.initialData); + + return () => slots.default?.() ?? null; + }, + }); + + const AbbyVuePlugin: Plugin = { + install(app: App, options?: { initialData?: AbbyDataResponse }) { + app.provide(AbbyKey, abbyState); + initData(options?.initialData); + }, + }; + + const getFeatureFlagValue = (name: FlagName) => { + return abby.getFeatureFlag(name); + }; + + const getRemoteConfig = < + T extends RemoteConfigName, + Config extends RemoteConfig[T], + >( + remoteConfigName: T + ): RemoteConfigValueStringToType => { + return abby.getRemoteConfig(remoteConfigName); + }; + + const getABTestValue = < + TestName extends keyof Tests, + TestVariant extends Tests[TestName]["variants"][number], + LookupValue, + const Lookup extends + | Record + | undefined = undefined, + >( + testName: TestName, + lookupObject?: Lookup + ): ABTestReturnValue => { + const variant = abby.getTestVariant(testName); + if (lookupObject === undefined) { + return variant as any; + } + + return lookupObject[variant as TestVariant] as any; + }; + + const withDevtools: withDevtoolsFunction = (factory, props) => { + return defineComponent({ + name: "AbbyDevtools", + setup() { + let destroy: (() => void) | undefined; + + onMounted(() => { + if ( + !props?.dangerouslyForceShow && + process.env.NODE_ENV !== "development" + ) { + return; + } + + destroy = factory.create({ ...props, abby }); + }); + + onUnmounted(() => { + destroy?.(); + }); + + return () => null; + }, + }); + }; + + const getABResetFunction = (name: T) => { + return () => { + TestStorageService.remove(config.projectId, name as string); + }; + }; + + const getVariants = (name: T) => { + return abby.getVariants(name); + }; + + const updateUserProperties = ( + user: Partial<{ + -readonly [K in keyof User]: Infer; + }> + ) => { + abby.updateUserProperties(user); + }; + + const withAbby = any>( + handler?: Handler + ) => { + return async (event?: any) => { + const data = await handler?.(event); + + const __abby__data = await HttpService.getProjectData({ + url: config.apiUrl, + projectId: config.projectId, + environment: config.currentEnvironment, + }); + + return { + ...data, + __abby__data, + __abby_cookie: + event?.node?.req?.headers?.cookie ?? + event?.request?.headers?.get?.("cookie"), + }; + }; + }; + + return { + useAbby, + AbbyProvider, + AbbyVuePlugin, + AbbyPlugin: AbbyVuePlugin, + useFeatureFlag, + getFeatureFlagValue, + useRemoteConfig, + getRemoteConfig, + getABTestValue, + __abby__: abby, + withDevtools, + getABResetFunction, + getVariants, + useFeatureFlags, + useRemoteConfigVariables, + updateUserProperties, + withAbby, + }; +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 00000000..8a961e46 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,7 @@ +export { + createAbby, + type withDevtoolsFunction, + type AbbyDevtoolProps, + type ABTestReturnValue, +} from "./createAbby"; +export { type ABConfig, type AbbyConfig, defineConfig } from "@tryabby/core"; diff --git a/packages/vue/test/mocks/handlers.ts b/packages/vue/test/mocks/handlers.ts new file mode 100644 index 00000000..b1fde5e2 --- /dev/null +++ b/packages/vue/test/mocks/handlers.ts @@ -0,0 +1,43 @@ +import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core"; +import { rest } from "msw"; + +const returnData: AbbyDataResponse = { + tests: [ + { + name: "test", + weights: [1, 1, 1, 1], + }, + { + name: "test2", + weights: [1, 0], + }, + ], + flags: [ + { + name: "flag1", + value: true, + }, + { + name: "flag2", + value: false, + }, + ], + remoteConfig: [ + { + name: "remoteConfig1", + value: "FooBar", + }, + ], +}; + +export const handlers = [ + rest.get( + `${ABBY_BASE_URL}api/dashboard/:projectId/data`, + (_req, res, ctx) => { + return res(ctx.json(returnData)); + } + ), + rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => { + return res(ctx.json(returnData)); + }), +]; diff --git a/packages/vue/test/mocks/server.ts b/packages/vue/test/mocks/server.ts new file mode 100644 index 00000000..7b37f2ac --- /dev/null +++ b/packages/vue/test/mocks/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +export const server = setupServer(...handlers); diff --git a/packages/vue/test/setup.ts b/packages/vue/test/setup.ts new file mode 100644 index 00000000..8f7e81bc --- /dev/null +++ b/packages/vue/test/setup.ts @@ -0,0 +1,10 @@ +/// @ts-ignore it doesn't have types +import fetch from "node-fetch"; +import { server } from "./mocks/server"; + +/// @ts-ignore +global.fetch = fetch; + +beforeAll(() => server.listen()); +afterAll(() => server.close()); +afterEach(() => server.resetHandlers()); diff --git a/packages/vue/test/ssr.test.ts b/packages/vue/test/ssr.test.ts new file mode 100644 index 00000000..72a9e981 --- /dev/null +++ b/packages/vue/test/ssr.test.ts @@ -0,0 +1,179 @@ +import { renderToString } from "@vue/server-renderer"; +import { createApp, defineComponent, h } from "vue"; +import { createAbby } from "../src"; + +const OLD_ENV = process.env; + +beforeEach(() => { + vi.resetModules(); + process.env = { ...OLD_ENV }; +}); + +afterAll(() => { + process.env = OLD_ENV; +}); + +describe("useAbby", () => { + it("doesn't render a variant on the server", async () => { + const { AbbyVuePlugin, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "ssr-ab-test", + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const ComponentWithTest = defineComponent({ + setup() { + const { variant } = useAbby("showFooter"); + return () => { + const selectedVariant = variant.value as string; + return h("div", [ + selectedVariant === "" ? h("span", "SSR!") : null, + selectedVariant === "current" + ? h("span", { "data-testid": "current" }, "Secret") + : null, + selectedVariant === "new" + ? h("span", { "data-testid": "new" }, "Very Secret") + : null, + ]); + }; + }, + }); + + const app = createApp(ComponentWithTest); + app.use(AbbyVuePlugin); + const serverSideHTML = await renderToString(app); + + expect(serverSideHTML).toContain("SSR!"); + expect(serverSideHTML).not.toContain("data-testid="); + }); + + it("hydrates the variant from initialData on the server", async () => { + const { AbbyVuePlugin, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "ssr-ab-hydrate", + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const ComponentWithTest = defineComponent({ + setup() { + const { variant } = useAbby("showFooter"); + return () => + h("div", [ + variant.value === "current" + ? h("span", { "data-testid": "current" }, "Secret") + : null, + variant.value === "new" + ? h("span", { "data-testid": "new" }, "Very Secret") + : null, + ]); + }, + }); + + const app = createApp(ComponentWithTest); + app.use(AbbyVuePlugin, { + initialData: { + flags: [], + tests: [ + { + name: "showFooter", + weights: [1, 0], + }, + ], + remoteConfig: [], + }, + }); + const serverSideHTML = await renderToString(app); + + expect(serverSideHTML).toContain('data-testid="current"'); + expect(serverSideHTML).not.toContain("Very Secret"); + }); +}); + +describe("useFeatureFlag", () => { + it("renders the correct feature flag on the server", async () => { + const { AbbyVuePlugin, useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "ssr-ff", + flags: ["test", "test2"], + }); + + const ComponentWithFF = defineComponent({ + setup() { + const test = useFeatureFlag("test"); + const test2 = useFeatureFlag("test2"); + + return () => + h("div", [ + test.value ? h("span", { "data-testid": "test" }, "Secret") : null, + test2.value + ? h("span", { "data-testid": "test2" }, "SUPER SECRET") + : null, + ]); + }, + }); + + const app = createApp(ComponentWithFF); + app.use(AbbyVuePlugin, { + initialData: { + flags: [ + { + value: true, + name: "test", + }, + { + value: false, + name: "test2", + }, + ], + tests: [], + remoteConfig: [], + }, + }); + const serverSideHTML = await renderToString(app); + + expect(serverSideHTML).toContain("Secret"); + expect(serverSideHTML).not.toContain("SUPER SECRET"); + }); +}); + +describe("useRemoteConfig", () => { + it("renders the correct remoteConfig value on the server", async () => { + const { AbbyVuePlugin, useRemoteConfig } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "ssr-rc", + remoteConfig: { remoteConfig1: "String" }, + }); + + const ComponentWithRC = defineComponent({ + setup() { + const remoteConfig1 = useRemoteConfig("remoteConfig1"); + + return () => h("span", { "data-testid": "test" }, remoteConfig1.value); + }, + }); + + const app = createApp(ComponentWithRC); + app.use(AbbyVuePlugin, { + initialData: { + flags: [], + tests: [], + remoteConfig: [{ name: "remoteConfig1", value: "FooBar" }], + }, + }); + const serverSideHTML = await renderToString(app); + + expect(serverSideHTML).toContain("FooBar"); + }); +}); diff --git a/packages/vue/test/types.test.ts b/packages/vue/test/types.test.ts new file mode 100644 index 00000000..2bc74206 --- /dev/null +++ b/packages/vue/test/types.test.ts @@ -0,0 +1,155 @@ +import { createAbby } from "../src"; + +const OLD_ENV = process.env; + +beforeEach(() => { + vi.resetModules(); + process.env = { ...OLD_ENV }; +}); + +afterAll(() => { + process.env = OLD_ENV; +}); + +describe("useAbby", () => { + it("returns the correct types", () => { + const test2Variants = [ + "SimonsText", + "MatthiasText", + "TomsText", + "TimsText", + ] as const; + + const { useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants: ["ONLY_ONE_VARIANT"] }, + test2: { + variants: test2Variants, + }, + }, + }); + + expectTypeOf(useAbby).parameter(0).toEqualTypeOf<"test" | "test2">(); + + const { variant, onAct } = useAbby("test"); + + assertType<"ONLY_ONE_VARIANT">(variant.value); + expect(variant.value).toBeDefined(); + expectTypeOf(onAct).toEqualTypeOf<() => void>(); + + const { variant: test2Result } = useAbby("test2"); + + assertType<(typeof test2Variants)[number]>(test2Result.value); + expect(test2Result.value).toBeDefined(); + }); + + it("produces proper types with a lookup object", () => { + const { useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { + variants: ["A", "B", "C"], + }, + }, + }); + + const { variant } = useAbby("test", { + A: "Hello", + B: "Bonjour", + C: "Hola", + }); + + expectTypeOf(variant.value).toEqualTypeOf<"Hello" | "Bonjour" | "Hola">(); + }); +}); + +describe("useFeatureFlag", () => { + it("returns the correct types", () => { + const { useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + flags: ["test"], + }); + + expectTypeOf(useFeatureFlag).parameter(0).toEqualTypeOf<"test">(); + + const testFlagResult = useFeatureFlag("test"); + + expectTypeOf(testFlagResult.value).toEqualTypeOf(); + }); + + describe("getVariants", () => { + it("has the correct types", () => { + const { getVariants } = createAbby({ + environments: [], + projectId: "123", + currentEnvironment: "test", + tests: { + test: { + variants: ["ONLY_ONE_VARIANT"], + }, + }, + flags: ["test"], + }); + expectTypeOf(getVariants("test")).toEqualTypeOf< + readonly ["ONLY_ONE_VARIANT"] + >(); + }); + }); + + describe("getABTestValue", () => { + it("produces proper types with a lookup object", () => { + const { getABTestValue } = createAbby({ + environments: [], + projectId: "123", + currentEnvironment: "a", + tests: { + test: { + variants: ["A", "B", "C"], + }, + }, + }); + + const activeVariant = getABTestValue("test", { + A: "Hello", + B: "Bonjour", + C: "Hola", + }); + + expectTypeOf(activeVariant).toEqualTypeOf<"Hello" | "Bonjour" | "Hola">(); + }); + }); +}); + +describe("useRemoteConfig", () => { + it("uses correct typings", () => { + const { useRemoteConfig } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + remoteConfig: { + stringRc: "String", + numberRc: "Number", + jsonRc: "JSON", + }, + }); + + expectTypeOf(useRemoteConfig) + .parameter(0) + .toEqualTypeOf<"stringRc" | "numberRc" | "jsonRc">(); + + const stringRcResult = useRemoteConfig("stringRc"); + const numberRcResult = useRemoteConfig("numberRc"); + const jsonRcResult = useRemoteConfig("jsonRc"); + + expectTypeOf(stringRcResult.value).toEqualTypeOf(); + expectTypeOf(numberRcResult.value).toEqualTypeOf(); + expectTypeOf(jsonRcResult.value).toEqualTypeOf>(); + }); +}); diff --git a/packages/vue/test/useAbby.test.ts b/packages/vue/test/useAbby.test.ts new file mode 100644 index 00000000..1c0b5931 --- /dev/null +++ b/packages/vue/test/useAbby.test.ts @@ -0,0 +1,507 @@ +import { AbbyEventType, HttpService } from "@tryabby/core"; +import { flushPromises, mount } from "@vue/test-utils"; +import { type ComputedRef, defineComponent, h } from "vue"; +import { createAbby } from "../src"; +import { TestStorageService } from "../src/StorageService"; + +const OLD_ENV = process.env; + +beforeEach(() => { + document.cookie.split(";").forEach((cookie) => { + document.cookie = cookie + .replace(/^ +/, "") + .replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/`); + }); + vi.restoreAllMocks(); + vi.resetModules(); + process.env = { ...OLD_ENV }; +}); + +afterAll(() => { + process.env = OLD_ENV; +}); + +type AbbyTestResult = { + variant: ComputedRef; + onAct: () => void; +}; + +describe("useAbby", () => { + it("returns the correct amount of options", async () => { + const spy = vi.spyOn(TestStorageService, "set"); + + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants: ["OldFooter", "NewFooter"] }, + test2: { + variants: ["SimonsText", "MatthiasText", "TomsText", "TimsText"], + }, + }, + }); + + let result!: AbbyTestResult; + const Child = defineComponent({ + setup() { + result = useAbby("test") as AbbyTestResult; + return () => h("span", result.variant.value); + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + await flushPromises(); + + expect(spy).toHaveBeenCalled(); + expect(result.variant.value).toBeDefined(); + expect(result.variant.value).not.toEqual(""); + }); + + it("should use the persistedValue", async () => { + const persistedValue = "SimonsText"; + const variants = ["SimonsText", "MatthiasText", "TomsText", "TimsText"]; + + const getSpy = vi.spyOn(TestStorageService, "get"); + const setSpy = vi.spyOn(TestStorageService, "set"); + + getSpy.mockReturnValue(persistedValue); + + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants }, + }, + }); + + let result!: AbbyTestResult; + const Child = defineComponent({ + setup() { + result = useAbby("test") as AbbyTestResult; + return () => h("span", result.variant.value); + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + await flushPromises(); + + expect(setSpy).not.toHaveBeenCalled(); + expect(result.variant.value).toEqual(persistedValue); + }); + + it("looks up the selected variant in the lookup object", async () => { + const persistedValue = "SimonsText"; + const variants = [ + "SimonsText", + "MatthiasText", + "TomsText", + "TimsText", + ] as const; + + const getSpy = vi.spyOn(TestStorageService, "get"); + getSpy.mockReturnValue(persistedValue); + + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants }, + }, + }); + + let result!: AbbyTestResult; + const Child = defineComponent({ + setup() { + result = useAbby("test", { + SimonsText: "a", + MatthiasText: "b", + TomsText: "c", + TimsText: "d", + }) as AbbyTestResult; + return () => h("span", result.variant.value); + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + await flushPromises(); + + expect(result.variant.value).toEqual("a"); + }); + + it("should ping the current info on mount", async () => { + const spy = vi.spyOn(HttpService, "sendData"); + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants: ["A", "B", "C"] }, + }, + }); + + const Child = defineComponent({ + setup() { + useAbby("test"); + return () => null; + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + await flushPromises(); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ type: AbbyEventType.PING }) + ); + }); + + it("should notify the server with onAct", async () => { + const spy = vi.spyOn(HttpService, "sendData"); + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants: ["A", "B", "C"] }, + }, + }); + + let result!: AbbyTestResult; + const Child = defineComponent({ + setup() { + result = useAbby("test") as AbbyTestResult; + return () => null; + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + await flushPromises(); + + expect(spy).toHaveBeenCalledTimes(1); + + result.onAct(); + + expect(spy).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ type: AbbyEventType.ACT }) + ); + }); + + it("should return the correct feature flags", async () => { + const { AbbyProvider, useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + flags: ["flag1", "flag2"], + }); + + let flag1!: ComputedRef; + let flag2!: ComputedRef; + const Child = defineComponent({ + setup() { + flag1 = useFeatureFlag("flag1"); + flag2 = useFeatureFlag("flag2"); + return () => null; + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + + expect(flag1.value).toEqual(false); + expect(flag2.value).toEqual(false); + + await flushPromises(); + + expect(flag1.value).toEqual(true); + expect(flag2.value).toEqual(false); + }); + + it("uses the devOverrides", async () => { + process.env.NODE_ENV = "development"; + const { AbbyProvider, useFeatureFlag } = createAbby({ + environments: [], + projectId: "123", + flags: ["flag1", "flag2"], + currentEnvironment: "a", + settings: { + flags: { + devOverrides: { + flag1: false, + flag2: true, + }, + }, + }, + }); + + let flag1!: ComputedRef; + let flag2!: ComputedRef; + const Child = defineComponent({ + setup() { + flag1 = useFeatureFlag("flag1"); + flag2 = useFeatureFlag("flag2"); + return () => null; + }, + }); + + mount(AbbyProvider, { slots: { default: () => h(Child) } }); + + expect(flag1.value).toEqual(false); + expect(flag2.value).toEqual(true); + + await flushPromises(); + + expect(flag1.value).toEqual(false); + expect(flag2.value).toEqual(true); + }); + + it("gets the stored feature flag value using a function properly", async () => { + const { AbbyVuePlugin, getFeatureFlagValue } = createAbby({ + environments: [], + projectId: "123", + flags: ["flag1", "flag2"], + currentEnvironment: "a", + }); + + mount(defineComponent({ setup: () => () => null }), { + global: { plugins: [AbbyVuePlugin] }, + }); + + await flushPromises(); + + expect(getFeatureFlagValue("flag1")).toEqual(true); + expect(getFeatureFlagValue("flag2")).toEqual(false); + }); + + it("returns the correct possible variant values", () => { + const { getVariants } = createAbby({ + environments: [], + projectId: "123", + currentEnvironment: "a", + tests: { + test: { + variants: ["A", "B", "C"], + }, + }, + }); + expect(getVariants("test")).toEqual(["A", "B", "C"]); + }); + + it("uses the lookup object when retrieving a variant", () => { + const { getABTestValue } = createAbby({ + environments: [], + projectId: "123", + currentEnvironment: "a", + tests: { + test: { + variants: ["A", "B", "C"], + }, + }, + }); + + const activeVariant = getABTestValue("test"); + const lookupObject = { + A: 1, + B: 2, + C: 3, + }; + + expect(getABTestValue("test", lookupObject)).toEqual( + lookupObject[activeVariant] + ); + }); + + it("returns correct remoteConfigValue", async () => { + const { AbbyVuePlugin, useRemoteConfig } = createAbby({ + environments: [], + projectId: "123", + remoteConfig: { + remoteConfig1: "String", + }, + currentEnvironment: "a", + }); + + let remoteConfig!: ComputedRef; + mount( + defineComponent({ + setup() { + remoteConfig = useRemoteConfig("remoteConfig1"); + return () => null; + }, + }), + { global: { plugins: [AbbyVuePlugin] } } + ); + + await flushPromises(); + + expect(remoteConfig.value).toEqual("FooBar"); + }); + + it("uses defaultValues when remoteConfig is not set", async () => { + const { AbbyVuePlugin, useRemoteConfig } = createAbby({ + environments: [], + projectId: "123", + remoteConfig: { + unsetRemoteConfig: "String", + }, + settings: { + remoteConfig: { + defaultValues: { + String: "defaultValue", + }, + }, + }, + currentEnvironment: "a", + }); + + let remoteConfig!: ComputedRef; + mount( + defineComponent({ + setup() { + remoteConfig = useRemoteConfig("unsetRemoteConfig"); + return () => null; + }, + }), + { global: { plugins: [AbbyVuePlugin] } } + ); + + await flushPromises(); + + expect(remoteConfig.value).toEqual("defaultValue"); + }); + + it("uses devOverride for remoteConfig", () => { + process.env.NODE_ENV = "development"; + const { AbbyVuePlugin, useRemoteConfig } = createAbby({ + environments: [], + projectId: "123", + remoteConfig: { + remoteConfig1: "String", + }, + settings: { + remoteConfig: { + devOverrides: { + remoteConfig1: "overwrittenValue", + }, + }, + }, + currentEnvironment: "a", + }); + + let remoteConfig!: ComputedRef; + mount( + defineComponent({ + setup() { + remoteConfig = useRemoteConfig("remoteConfig1"); + return () => null; + }, + }), + { global: { plugins: [AbbyVuePlugin] } } + ); + + expect(remoteConfig.value).toEqual("overwrittenValue"); + }); + + it("isolates multiple createAbby instances", async () => { + const instanceA = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "instance-a", + tests: { test: { variants: ["A1", "A2"] } }, + }); + const instanceB = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "instance-b", + tests: { test: { variants: ["B1", "B2"] } }, + }); + + let result!: AbbyTestResult; + const Child = defineComponent({ + setup() { + result = instanceB.useAbby("test") as AbbyTestResult; + return () => null; + }, + }); + + mount(Child, { global: { plugins: [instanceA.AbbyVuePlugin] } }); + await flushPromises(); + + expect(["B1", "B2"]).toContain(result.variant.value); + }); + + it("works without a provider via the global singleton", async () => { + const { useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "no-provider", + tests: { test: { variants: ["GlobalA", "GlobalB"] } }, + }); + + const { variant, onAct } = useAbby("test"); + expect(variant.value).toBeDefined(); + + await flushPromises(); + + expect(["GlobalA", "GlobalB"]).toContain(variant.value); + expect(() => onAct()).not.toThrow(); + }); +}); + +describe("useFeatureFlags()", () => { + it("returns the correct list of feature flags", async () => { + const { AbbyVuePlugin, useFeatureFlags } = createAbby({ + environments: [], + currentEnvironment: "test", + projectId: "123", + flags: ["flag1"], + }); + + let flags!: ComputedRef>; + mount( + defineComponent({ + setup() { + flags = useFeatureFlags(); + return () => null; + }, + }), + { global: { plugins: [AbbyVuePlugin] } } + ); + + await flushPromises(); + + expect(flags.value).toContainEqual({ name: "flag1", value: true }); + }); +}); + +describe("useRemoteConfigVariables()", () => { + it("returns the correct list of remote config variables", async () => { + const { AbbyVuePlugin, useRemoteConfigVariables } = createAbby({ + environments: [], + currentEnvironment: "test", + projectId: "123", + remoteConfig: { + remoteConfig1: "String", + }, + }); + + let variables!: ComputedRef>; + mount( + defineComponent({ + setup() { + variables = useRemoteConfigVariables(); + return () => null; + }, + }), + { global: { plugins: [AbbyVuePlugin] } } + ); + + await flushPromises(); + + expect(variables.value).toContainEqual({ + name: "remoteConfig1", + value: expect.any(String), + }); + }); +}); diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 00000000..723665e9 --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "types": ["vitest/globals"] + } +} diff --git a/packages/vue/tsup.config.ts b/packages/vue/tsup.config.ts new file mode 100644 index 00000000..bf89044f --- /dev/null +++ b/packages/vue/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + dts: true, + clean: true, + format: ["cjs", "esm"], + sourcemap: true, + treeshake: true, +}); diff --git a/packages/vue/vite.config.ts b/packages/vue/vite.config.ts new file mode 100644 index 00000000..714605a1 --- /dev/null +++ b/packages/vue/vite.config.ts @@ -0,0 +1,11 @@ +/// + +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + setupFiles: "./test/setup.ts", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69fa896e..7a8287b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.26.1 prettier: specifier: latest - version: 3.5.2 + version: 3.9.10 turbo: specifier: ^2.0.4 version: 2.0.4 @@ -327,7 +327,7 @@ importers: version: 3.1.55(@next/env@14.2.4)(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8)) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.1.1(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 0.2.1(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) nextjs-cors: specifier: ^2.1.2 version: 2.1.2(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8)) @@ -620,7 +620,7 @@ importers: version: 2.4.2 tsup: specifier: ^6.5.0 - version: 6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4))(typescript@5.5.4) + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4))(typescript@5.5.4) unconfig: specifier: ^0.3.10 version: 0.3.10 @@ -681,7 +681,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -757,7 +757,7 @@ importers: version: 3.59.1 svelte-check: specifier: ^2.10.3 - version: 2.10.3(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1) + version: 2.10.3(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1) tslib: specifier: ^2.5.0 version: 2.5.3 @@ -827,7 +827,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -876,7 +876,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -940,7 +940,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1004,7 +1004,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1089,7 +1089,7 @@ importers: version: 3.59.1 svelte-check: specifier: ^2.10.3 - version: 2.10.3(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1) + version: 2.10.3(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1) tslib: specifier: ^2.5.0 version: 2.5.3 @@ -1108,6 +1108,52 @@ importers: packages/tsconfig: {} + packages/vue: + dependencies: + '@tryabby/core': + specifier: workspace:* + version: link:../core + js-cookie: + specifier: ^3.0.1 + version: 3.0.5 + devDependencies: + '@types/js-cookie': + specifier: ^3.0.3 + version: 3.0.3 + '@vue/server-renderer': + specifier: ^3.4.0 + version: 3.5.45 + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.5.1(@vue/compiler-dom@3.5.45)(@vue/server-renderer@3.5.45)(vue@3.5.45(typescript@5.5.4)) + jsdom: + specifier: ^20.0.3 + version: 20.0.3 + msw: + specifier: ^0.49.1 + version: 0.49.3(encoding@0.1.13)(typescript@5.5.4) + node-fetch: + specifier: ^3.3.0 + version: 3.3.2 + tsconfig: + specifier: workspace:* + version: link:../tsconfig + tsup: + specifier: ^6.5.0 + version: 6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4) + typescript: + specifier: 5.5.4 + version: 5.5.4 + vite: + specifier: 5.4.0 + version: 5.4.0(@types/node@22.1.0)(less@4.2.0)(sass@1.77.8)(terser@5.31.1) + vitest: + specifier: 2.0.5 + version: 2.0.5(@types/node@22.1.0)(jsdom@20.0.3)(less@4.2.0)(sass@1.77.8)(terser@5.31.1) + vue: + specifier: ^3.4.0 + version: 3.5.45(typescript@5.5.4) + packages: '@adobe/css-tools@4.2.0': @@ -1582,6 +1628,10 @@ packages: resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.13': + resolution: {integrity: sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.22.20': resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} @@ -1590,6 +1640,10 @@ packages: resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.22.5': resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} engines: {node: '>=6.9.0'} @@ -1656,11 +1710,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.24.4': - resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.24.7': resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} engines: {node: '>=6.0.0'} @@ -1671,6 +1720,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.13': + resolution: {integrity: sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3': resolution: {integrity: sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==} engines: {node: '>=6.9.0'} @@ -2661,10 +2715,6 @@ packages: resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} engines: {node: '>=6.9.0'} - '@babel/types@7.24.0': - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} - engines: {node: '>=6.9.0'} - '@babel/types@7.24.7': resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} @@ -2673,6 +2723,10 @@ packages: resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2892,7 +2946,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks@1.0.1': resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: - react: ^18.2.0 + react: '>=16.8.0' '@esbuild-plugins/node-globals-polyfill@0.2.3': resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} @@ -3750,6 +3804,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@jridgewell/trace-mapping@0.3.18': resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -3845,7 +3902,7 @@ packages: '@mdx-js/react@2.3.0': resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} peerDependencies: - react: ^18.2.0 + react: '>=16' '@mdx-js/react@3.0.0': resolution: {integrity: sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==} @@ -4295,6 +4352,9 @@ packages: resolution: {integrity: sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==} engines: {node: '>= 18'} + '@one-ini/wasm@0.2.1': + resolution: {integrity: sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==} + '@open-draft/until@1.0.3': resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} @@ -4474,6 +4534,7 @@ packages: '@opentelemetry/sdk-trace-base@1.26.0': resolution: {integrity: sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==} engines: {node: '>=14'} + deprecated: Use @opentelemetry/sdk-trace package peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -4671,8 +4732,8 @@ packages: '@radix-ui/react-direction@1.1.0': resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: - '@types/react': 18.0.26 - react: ^18.2.0 + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true @@ -5161,8 +5222,8 @@ packages: '@radix-ui/react-use-layout-effect@1.1.0': resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: - '@types/react': 18.0.26 - react: ^18.2.0 + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true @@ -5213,34 +5274,42 @@ packages: '@react-email/button@0.0.5': resolution: {integrity: sha512-k8yDJ5b9gFriy0543qR8zI2tyOBvob7YwaPxfVorDH0ajPucocjbzymopzxyHU13LMafdYbId/kPjoANwdSr+g==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/container@0.0.5': resolution: {integrity: sha512-zEPBl81MlOZXHYYpexyUCyQogtT6II2PmAJ8Y7hQ8xmNvdiPKdcIneCGqtYLYbBPATP9pGs4JNdL0SOG1BIqvw==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/head@0.0.3': resolution: {integrity: sha512-4V5GkOCp6MhuTSxqADtfl7zx6Ku6/fDR32DCV6XDRhBhr5TRwiPV/aOAf2eIWzOwLcLkrTMd4O0BPf97+uBaCA==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/hr@0.0.3': resolution: {integrity: sha512-dTV1jm8q5BBzdjNnAAq8RRInxg7Q0V+9dk+v5zutAYcd85UtkFa6YpS7h8ElhIRUiWCtpy/Bze1V9PiDFQko5Q==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/html@0.0.3': resolution: {integrity: sha512-NtHzGq38StO95W/rASYy+qdzFEpqUDmEe5RMygxeaV/TTZbtLLRqycwk5B/dxq4DfreZE/GL0vAg6u6sMVLTPA==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/img@0.0.3': resolution: {integrity: sha512-RHO7SpRwNo7+Z2h+RtNP0r4ofKYEwvPnKtCHYL4oIRPu3mEto3JAb4g3tVH3pjiHzY1aqSZUXmbCtZtoqdlCwQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/link@0.0.3': resolution: {integrity: sha512-ybkITKwNmFAdBYYtxsD5KxQKUhW8ICQmLW29DYi6yS1GUECA2v78noSnZMaTwNP01rOx0I1dqSgn20M/justdg==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/preview@0.0.3': resolution: {integrity: sha512-vEayk64Sa1m1PhaBte8ovIgbTOG5BHoYMmXsltcFoerPz1ZMY7MJwEGEbhiSh/PGt5CXFOsZEIKZuKIu69lUzQ==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/render@0.0.5': resolution: {integrity: sha512-EE9mCvR3lXeZEJaldCEaEc4msCwPQwZfXbhuPVl3SpRsiHiMK0wNm2X39vVpqzCzxrm0wljCoLruT7Klp9DZAw==} @@ -5249,10 +5318,12 @@ packages: '@react-email/section@0.0.4': resolution: {integrity: sha512-AaDejYo9nozfiGMxjGnR+CaC8jnILIowiqPFRVeEQOywS2LrL8iIB4YhrxvFjeaWgZmWqKdYDhq16uXWpi/eEA==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@react-email/text@0.0.3': resolution: {integrity: sha512-gDPeps1TKqTuvTGVCmjDziAhzdctrj8NvmUCwIsKRBRzGqKcjXJaSx/wVs4aAHO6erVpuswmkUsdo2d238YN/Q==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@remirror/core-constants@2.0.2': resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} @@ -6641,6 +6712,7 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-basic-ssl@1.1.0': resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==} @@ -6690,9 +6762,21 @@ packages: '@vue/compiler-core@3.4.24': resolution: {integrity: sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==} + '@vue/compiler-core@3.5.45': + resolution: {integrity: sha512-XGHJrM6ufZARAc/w+tyOswStw9tdKiqCKBScI4gGaNxD+aaGu/HQWgKJ+JItjRLPqenQ7iE7G8AJC7T5ew1p+g==} + '@vue/compiler-dom@3.4.24': resolution: {integrity: sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==} + '@vue/compiler-dom@3.5.45': + resolution: {integrity: sha512-LbP2d19GSOYGPmvMBBCVJVlv7TKtjM1Cfeyu3cYia720pknZP+1cLX7H5MVatvruT6MuJhiPuNbQBW/MDn/EwA==} + + '@vue/compiler-sfc@3.5.45': + resolution: {integrity: sha512-MKFjb2UX0M/l/jhfzqMLrI8VM4pfvFKG3pqFZkSZFHRLtNld/O4RtUPZFWSwhro/14qP88UQEkWzHxAZAQ0tsw==} + + '@vue/compiler-ssr@3.5.45': + resolution: {integrity: sha512-RF5sbZOfYz5AK00tOJUn2PwFXR2fS1u2HVzI8D9VF9rGAFxuGBqg2bB1kJ3ZHMprqSltXnT9MZE1IsqjSM8oew==} + '@vue/language-core@1.8.27': resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} peerDependencies: @@ -6701,9 +6785,34 @@ packages: typescript: optional: true + '@vue/reactivity@3.5.45': + resolution: {integrity: sha512-WqkyubNWpI4OYBOipSrjIssMdJO8K1i9Zts/Wm8Er10/0msOEwCyWWWvZnAI7WW3kQsTong6DJp8Cbb+vd4cPA==} + + '@vue/runtime-core@3.5.45': + resolution: {integrity: sha512-bzIO3+FABjpj85uIKSc+Qnj36/sxDNI3tcH9i6w6xKA1k6e0ejv0G0jMCfeJBvK3yGL5XbJ3DMZj+ClqyCHFYw==} + + '@vue/runtime-dom@3.5.45': + resolution: {integrity: sha512-ZvRTFMDgfqTDYWvJfoVpQTJx1RN5M6l7Pyd6wfdY2NaXzbPQVh9ezeQYFbNPP6bIcRVn2SqSSR25N/jSW69BFw==} + + '@vue/server-renderer@3.5.45': + resolution: {integrity: sha512-1fNXUHhhsWf4EVL4XixbGBU5mKdiIXJAB82feHRTyPPo6ZUEYWIJuEeUl3mVV6QDBp50HJOX4obLhxYXlrjrzA==} + '@vue/shared@3.4.24': resolution: {integrity: sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==} + '@vue/shared@3.5.45': + resolution: {integrity: sha512-ue9fgHmVNPlP7iW3+cxoBX/Cnlw53dHlVSw0JKGY6z1lObg8pHoj928M2nzG4/yVsMZ3NaRZ2s2z61uC16NDwQ==} + + '@vue/test-utils@2.5.1': + resolution: {integrity: sha512-V/4a99odJ7hapr0bRt35XxljGk8hefq31gSOnlKTcEq2fUeoj2qb+FcpJYfQu4qfYhW9Ug+O2+CZ3QmZ7gXDzA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} @@ -6786,6 +6895,10 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abbrev@5.0.0: + resolution: {integrity: sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -6799,6 +6912,7 @@ packages: acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -7141,6 +7255,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -7206,6 +7324,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -7576,6 +7698,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -7740,10 +7866,12 @@ packages: critters@0.0.24: resolution: {integrity: sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==} + deprecated: Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -7796,6 +7924,9 @@ packages: csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -8276,6 +8407,11 @@ packages: resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==} hasBin: true + editorconfig@3.0.2: + resolution: {integrity: sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==} + engines: {node: '>=20'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -8345,6 +8481,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -8812,6 +8952,7 @@ packages: formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -8988,28 +9129,35 @@ packages: glob@10.2.7: resolution: {integrity: sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@13.0.7: + resolution: {integrity: sha512-wfsFgXQ8A3SEI9XGf7Nl3Y9l7ffSrGh7SUCT7vU/2CzYPep9kjxAn1S40F9OPKxAMGCVl7HtNlJhrCJUeUq83g==} + engines: {node: 18 || 20 || >=22} + glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -9349,6 +9497,7 @@ packages: image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} + deprecated: Please migrate to image-size@2.x hasBin: true immer@9.0.21: @@ -9429,6 +9578,7 @@ packages: intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} + deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -9882,10 +10032,18 @@ packages: engines: {node: '>=12'} hasBin: true + js-beautify@2.0.3: + resolution: {integrity: sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==} + engines: {node: '>=14'} + hasBin: true + js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -10162,6 +10320,7 @@ packages: lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} @@ -10220,6 +10379,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -10261,6 +10424,9 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -10713,6 +10879,10 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -10774,6 +10944,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -10884,6 +11058,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -10978,6 +11157,7 @@ packages: next@13.4.5: resolution: {integrity: sha512-pfNsRLVM9e5Y1/z02VakJRfD6hMQkr24FaN2xc9GbcZDBxoOgiNAViSg5cXwlWCoMhtm4U315D7XYhgOr96Q3Q==} engines: {node: '>=16.8.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -10996,6 +11176,7 @@ packages: next@14.1.1: resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==} engines: {node: '>=18.17.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -11065,6 +11246,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch-native@1.2.0: resolution: {integrity: sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==} @@ -11131,6 +11313,11 @@ packages: resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} hasBin: true + nopt@10.0.1: + resolution: {integrity: sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + hasBin: true + nopt@6.0.0: resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -11516,6 +11703,10 @@ packages: resolution: {integrity: sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==} engines: {node: '>=16 || 14 >=14.17'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -11569,6 +11760,9 @@ packages: picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -11735,6 +11929,10 @@ packages: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -11793,8 +11991,8 @@ packages: engines: {node: '>=14'} hasBin: true - prettier@3.5.2: - resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} engines: {node: '>=14'} hasBin: true @@ -12198,6 +12396,7 @@ packages: recharts@2.12.7: resolution: {integrity: sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==} engines: {node: '>=14'} + deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 @@ -12587,6 +12786,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.6: + resolution: {integrity: sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -12771,6 +12975,10 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + source-map-loader@5.0.0: resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} engines: {node: '>= 18.12.0'} @@ -12791,6 +12999,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -13015,7 +13224,7 @@ packages: superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superjson@1.9.1: resolution: {integrity: sha512-oT3HA2nPKlU1+5taFgz/HDy+GEaY+CWEbLzaRJVD4gZ7zMVVC4GDNFdgvAZt6/VuIk6D2R7RtPAiCHwmdzlMmg==} @@ -13024,6 +13233,7 @@ packages: supertest@6.3.3: resolution: {integrity: sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==} engines: {node: '>=6.4.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@4.5.0: resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} @@ -13151,10 +13361,12 @@ packages: tar@6.1.15: resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me telejson@7.1.0: resolution: {integrity: sha512-jFJO4P5gPebZAERPkJsqMAQ0IMA1Hi0AoSfxpnUaV6j6R2SZqlpkbS20U6dEUtA3RUYt2Ak/mTlkQzHH9Rv/hA==} @@ -13369,6 +13581,7 @@ packages: tsconfck@3.1.0: resolution: {integrity: sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -13775,14 +13988,17 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uvu@0.5.6: @@ -13964,6 +14180,9 @@ packages: vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + vue-component-type-helpers@3.3.14: + resolution: {integrity: sha512-Pq3+OnbCiPCHk0R7KSZrx+j/XXaUn2kSQ1epxq60w8CPbByLjC1EJgV51n4WY851sTXUVzsNMuitywdkdVyKjA==} + vue-template-compiler@2.7.16: resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} @@ -13973,6 +14192,14 @@ packages: peerDependencies: typescript: '*' + vue@3.5.45: + resolution: {integrity: sha512-3cvsib8m9c0vB24+MDKsgcW4+Da4xaBl9Z/5Tbm2pmSIsRPxcil4cdXjXlVsmJ2g64XJo5h+IRd7rsdGT9pZaw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -14046,7 +14273,7 @@ packages: engines: {node: '>= 18.12.0'} hasBin: true peerDependencies: - webpack: 5.93.0 + webpack: ^5.0.0 webpack-cli: '*' peerDependenciesMeta: webpack: @@ -14099,6 +14326,7 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.2: resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} @@ -14708,10 +14936,10 @@ snapshots: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.21.8) '@babel/helpers': 7.24.4 - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@babel/template': 7.24.0 '@babel/traverse': 7.24.1 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 convert-source-map: 1.9.0 debug: 4.3.5 gensync: 1.0.0-beta.2 @@ -14748,10 +14976,10 @@ snapshots: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) '@babel/helpers': 7.24.4 - '@babel/parser': 7.24.4 + '@babel/parser': 7.25.3 '@babel/template': 7.24.0 '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/types': 7.25.2 convert-source-map: 2.0.0 debug: 4.3.5 gensync: 1.0.0-beta.2 @@ -14802,7 +15030,7 @@ snapshots: '@babel/generator@7.21.9': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.20 jsesc: 2.5.2 @@ -14816,7 +15044,7 @@ snapshots: '@babel/generator@7.24.4': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 @@ -14837,7 +15065,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-annotate-as-pure@7.24.7': dependencies: @@ -14845,7 +15073,7 @@ snapshots: '@babel/helper-builder-binary-assignment-operator-visitor@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: @@ -15008,7 +15236,7 @@ snapshots: '@babel/helper-function-name@7.23.0': dependencies: '@babel/template': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-function-name@7.24.7': dependencies: @@ -15017,7 +15245,7 @@ snapshots: '@babel/helper-hoist-variables@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-hoist-variables@7.24.7': dependencies: @@ -15025,7 +15253,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-member-expression-to-functions@7.24.8': dependencies: @@ -15036,11 +15264,11 @@ snapshots: '@babel/helper-module-imports@7.22.15': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-module-imports@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-module-imports@7.24.7': dependencies: @@ -15142,11 +15370,11 @@ snapshots: '@babel/helper-optimise-call-expression@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-optimise-call-expression@7.24.7': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-plugin-utils@7.22.5': {} @@ -15160,7 +15388,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-wrap-function': 7.22.5 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color @@ -15170,7 +15398,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-wrap-function': 7.22.5 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color @@ -15190,7 +15418,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.22.5 '@babel/template': 7.24.7 '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color @@ -15205,7 +15433,7 @@ snapshots: '@babel/helper-simple-access@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-simple-access@7.24.7': dependencies: @@ -15216,7 +15444,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.22.5': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-skip-transparent-expression-wrappers@7.24.7': dependencies: @@ -15227,7 +15455,7 @@ snapshots: '@babel/helper-split-export-declaration@7.22.6': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/helper-split-export-declaration@7.24.7': dependencies: @@ -15239,10 +15467,14 @@ snapshots: '@babel/helper-string-parser@7.24.8': {} + '@babel/helper-string-parser@7.29.13': {} + '@babel/helper-validator-identifier@7.22.20': {} '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.22.5': {} '@babel/helper-validator-option@7.23.5': {} @@ -15256,7 +15488,7 @@ snapshots: '@babel/helper-function-name': 7.23.0 '@babel/template': 7.22.15 '@babel/traverse': 7.23.6 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color @@ -15280,7 +15512,7 @@ snapshots: dependencies: '@babel/template': 7.24.7 '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 transitivePeerDependencies: - supports-color @@ -15322,16 +15554,12 @@ snapshots: '@babel/parser@7.21.9': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 '@babel/parser@7.23.6': dependencies: '@babel/types': 7.23.6 - '@babel/parser@7.24.4': - dependencies: - '@babel/types': 7.24.7 - '@babel/parser@7.24.7': dependencies: '@babel/types': 7.24.7 @@ -15340,6 +15568,10 @@ snapshots: dependencies: '@babel/types': 7.25.2 + '@babel/parser@7.29.13': + dependencies: + '@babel/types': 7.29.11 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -16806,7 +17038,7 @@ snapshots: '@babel/plugin-transform-unicode-escapes': 7.22.5(@babel/core@7.21.8) '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.21.8) '@babel/preset-modules': 0.1.5(@babel/core@7.21.8) - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.8) babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.8) babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.8) @@ -17001,7 +17233,7 @@ snapshots: '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.8) '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.21.8) - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 esutils: 2.0.3 '@babel/preset-modules@0.1.5(@babel/core@7.23.6)': @@ -17010,7 +17242,7 @@ snapshots: '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.23.6) '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.23.6) - '@babel/types': 7.24.7 + '@babel/types': 7.25.2 esutils: 2.0.3 '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7)': @@ -17057,14 +17289,14 @@ snapshots: '@babel/template@7.22.15': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@babel/types': 7.24.7 '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 '@babel/template@7.24.7': dependencies: @@ -17086,8 +17318,8 @@ snapshots: '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: @@ -17101,7 +17333,7 @@ snapshots: '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@babel/types': 7.24.7 debug: 4.3.5 globals: 11.12.0 @@ -17116,8 +17348,8 @@ snapshots: '@babel/helper-function-name': 7.24.7 '@babel/helper-hoist-variables': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: @@ -17162,12 +17394,6 @@ snapshots: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - '@babel/types@7.24.0': - dependencies: - '@babel/helper-string-parser': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 - '@babel/types@7.24.7': dependencies: '@babel/helper-string-parser': 7.24.7 @@ -17180,6 +17406,11 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.13 + '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} '@biomejs/biome@1.9.4': @@ -18088,6 +18319,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + '@jridgewell/trace-mapping@0.3.18': dependencies: '@jridgewell/resolve-uri': 3.1.0 @@ -18691,6 +18924,8 @@ snapshots: '@octokit/request-error': 6.1.4 '@octokit/webhooks-methods': 5.1.0 + '@one-ini/wasm@0.2.1': {} + '@open-draft/until@1.0.3': {} '@openpanel/nextjs@1.0.3(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -21315,7 +21550,7 @@ snapshots: '@types/babel__template@7.4.1': dependencies: - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@babel/types': 7.24.7 '@types/babel__template@7.4.4': @@ -21841,17 +22076,47 @@ snapshots: '@vue/compiler-core@3.4.24': dependencies: - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@vue/shared': 3.4.24 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 + '@vue/compiler-core@3.5.45': + dependencies: + '@babel/parser': 7.29.13 + '@vue/shared': 3.5.45 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.2 + '@vue/compiler-dom@3.4.24': dependencies: '@vue/compiler-core': 3.4.24 '@vue/shared': 3.4.24 + '@vue/compiler-dom@3.5.45': + dependencies: + '@vue/compiler-core': 3.5.45 + '@vue/shared': 3.5.45 + + '@vue/compiler-sfc@3.5.45': + dependencies: + '@babel/parser': 7.29.13 + '@vue/compiler-core': 3.5.45 + '@vue/compiler-dom': 3.5.45 + '@vue/compiler-ssr': 3.5.45 + '@vue/shared': 3.5.45 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.32 + source-map-js: 1.2.2 + + '@vue/compiler-ssr@3.5.45': + dependencies: + '@vue/compiler-dom': 3.5.45 + '@vue/shared': 3.5.45 + '@vue/language-core@1.8.27(typescript@5.5.4)': dependencies: '@volar/language-core': 1.11.1 @@ -21866,8 +22131,41 @@ snapshots: optionalDependencies: typescript: 5.5.4 + '@vue/reactivity@3.5.45': + dependencies: + '@vue/shared': 3.5.45 + + '@vue/runtime-core@3.5.45': + dependencies: + '@vue/reactivity': 3.5.45 + '@vue/shared': 3.5.45 + + '@vue/runtime-dom@3.5.45': + dependencies: + '@vue/reactivity': 3.5.45 + '@vue/runtime-core': 3.5.45 + '@vue/shared': 3.5.45 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.45': + dependencies: + '@vue/compiler-ssr': 3.5.45 + '@vue/runtime-dom': 3.5.45 + '@vue/shared': 3.5.45 + '@vue/shared@3.4.24': {} + '@vue/shared@3.5.45': {} + + '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.45)(@vue/server-renderer@3.5.45)(vue@3.5.45(typescript@5.5.4))': + dependencies: + '@vue/compiler-dom': 3.5.45 + js-beautify: 2.0.3 + vue: 3.5.45(typescript@5.5.4) + vue-component-type-helpers: 3.3.14 + optionalDependencies: + '@vue/server-renderer': 3.5.45 + '@web3-storage/multipart-parser@1.0.0': {} '@webassemblyjs/ast@1.12.1': @@ -21968,6 +22266,8 @@ snapshots: abbrev@2.0.0: {} + abbrev@5.0.0: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -22327,6 +22627,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} base64id@2.0.0: {} @@ -22407,6 +22709,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + braces@3.0.2: dependencies: fill-range: 7.0.1 @@ -22798,6 +23104,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -23018,6 +23326,8 @@ snapshots: csstype@3.1.2: {} + csstype@3.2.3: {} + csv-generate@3.4.3: {} csv-parse@4.16.3: {} @@ -23508,6 +23818,13 @@ snapshots: semver: 5.7.1 sigmund: 1.0.1 + editorconfig@3.0.2: + dependencies: + '@one-ini/wasm': 0.2.1 + commander: 14.0.3 + minimatch: 10.2.6 + semver: 7.8.6 + ee-first@1.1.1: {} ejs@3.1.9: @@ -23575,6 +23892,8 @@ snapshots: entities@4.5.0: {} + entities@7.0.1: {} + env-paths@2.2.1: {} envinfo@7.8.1: {} @@ -24460,6 +24779,12 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 + glob@13.0.7: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.1.6: dependencies: fs.realpath: 1.0.0 @@ -25507,8 +25832,18 @@ snapshots: glob: 8.1.0 nopt: 6.0.0 + js-beautify@2.0.3: + dependencies: + config-chain: 1.1.13 + editorconfig: 3.0.2 + glob: 13.0.7 + js-cookie: 3.0.8 + nopt: 10.0.1 + js-cookie@3.0.5: {} + js-cookie@3.0.8: {} + js-levenshtein@1.1.6: {} js-tokens@4.0.0: {} @@ -25527,7 +25862,7 @@ snapshots: jscodeshift@0.14.0(@babel/preset-env@7.21.5(@babel/core@7.21.8)): dependencies: '@babel/core': 7.24.4 - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.4) '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.4) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.4) @@ -25552,7 +25887,7 @@ snapshots: jscodeshift@0.14.0(@babel/preset-env@7.22.5(@babel/core@7.23.6)): dependencies: '@babel/core': 7.24.4 - '@babel/parser': 7.24.7 + '@babel/parser': 7.25.3 '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.4) '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.4) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.4) @@ -25961,6 +26296,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.3: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -26000,6 +26337,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + magic-string@0.30.8: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -26948,6 +27289,10 @@ snapshots: minimalistic-assert@1.0.1: {} + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + minimatch@3.0.8: dependencies: brace-expansion: 1.1.11 @@ -27010,6 +27355,8 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 @@ -27150,6 +27497,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.20: {} + nanoid@3.3.6: {} nanoid@3.3.7: {} @@ -27227,7 +27576,7 @@ snapshots: minimist: 1.2.8 next: 14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8) - next-themes@0.2.1(next@14.1.1(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + next-themes@0.2.1(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: next: 14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8) react: 18.2.0 @@ -27306,7 +27655,7 @@ snapshots: match-sorter: 6.3.1 next: 14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8) next-seo: 6.4.0(next@14.1.1(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - next-themes: 0.2.1(next@14.1.1(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next-themes: 0.2.1(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) nextra: 2.13.2(next@14.1.1(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -27475,6 +27824,10 @@ snapshots: dependencies: abbrev: 1.1.1 + nopt@10.0.1: + dependencies: + abbrev: 5.0.0 + nopt@6.0.0: dependencies: abbrev: 1.1.1 @@ -27922,6 +28275,11 @@ snapshots: lru-cache: 9.1.2 minipass: 5.0.0 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + path-to-regexp@0.1.7: {} path-to-regexp@6.2.1: {} @@ -27968,6 +28326,8 @@ snapshots: picocolors@1.0.1: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} picomatch@4.0.2: {} @@ -28033,20 +28393,20 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.24 - postcss-load-config@3.1.4(postcss@8.4.41)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4)): + postcss-load-config@3.1.4(postcss@8.5.32)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.4.41 + postcss: 8.5.32 ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.5.4) - postcss-load-config@3.1.4(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)): + postcss-load-config@3.1.4(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.4.41 + postcss: 8.5.32 ts-node: 10.9.1(@types/node@22.1.0)(typescript@5.5.4) postcss-load-config@4.0.1(postcss@8.4.24)(ts-node@10.9.1(@types/node@18.16.17)(typescript@5.5.4)): @@ -28066,12 +28426,12 @@ snapshots: ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.5.4) optional: true - postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)): + postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)): dependencies: lilconfig: 2.1.0 yaml: 2.3.1 optionalDependencies: - postcss: 8.4.41 + postcss: 8.5.32 ts-node: 10.9.1(@types/node@22.1.0)(typescript@5.5.4) optional: true @@ -28156,6 +28516,12 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + postgres-array@2.0.0: {} postgres-bytea@1.0.0: {} @@ -28197,7 +28563,7 @@ snapshots: prettier@3.0.0: {} - prettier@3.5.2: {} + prettier@3.9.10: {} pretty-format@27.5.1: dependencies: @@ -29118,6 +29484,8 @@ snapshots: semver@7.6.2: {} + semver@7.8.6: {} + send@0.18.0: dependencies: debug: 2.6.9 @@ -29393,6 +29761,8 @@ snapshots: source-map-js@1.2.0: {} + source-map-js@1.2.2: {} + source-map-loader@5.0.0(webpack@5.92.1(esbuild@0.21.5)): dependencies: iconv-lite: 0.6.3 @@ -29689,7 +30059,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@2.10.3(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1): + svelte-check@2.10.3(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1): dependencies: '@jridgewell/trace-mapping': 0.3.18 chokidar: 3.5.3 @@ -29698,7 +30068,7 @@ snapshots: picocolors: 1.0.0 sade: 1.8.1 svelte: 3.59.1 - svelte-preprocess: 4.10.7(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4) + svelte-preprocess: 4.10.7(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - '@babel/core' @@ -29712,7 +30082,7 @@ snapshots: - stylus - sugarss - svelte-check@2.10.3(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1): + svelte-check@2.10.3(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1): dependencies: '@jridgewell/trace-mapping': 0.3.18 chokidar: 3.5.3 @@ -29721,7 +30091,7 @@ snapshots: picocolors: 1.0.0 sade: 1.8.1 svelte: 3.59.1 - svelte-preprocess: 4.10.7(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4) + svelte-preprocess: 4.10.7(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - '@babel/core' @@ -29739,7 +30109,7 @@ snapshots: dependencies: svelte: 3.59.1 - svelte-preprocess@4.10.7(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4): + svelte-preprocess@4.10.7(@babel/core@7.23.6)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4): dependencies: '@types/pug': 2.0.6 '@types/sass': 1.45.0 @@ -29751,12 +30121,12 @@ snapshots: optionalDependencies: '@babel/core': 7.23.6 less: 4.2.0 - postcss: 8.4.41 - postcss-load-config: 4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) + postcss: 8.5.32 + postcss-load-config: 4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) sass: 1.77.8 typescript: 5.5.4 - svelte-preprocess@4.10.7(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.4.41)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4): + svelte-preprocess@4.10.7(@babel/core@7.24.9)(less@4.2.0)(postcss-load-config@4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.32)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4): dependencies: '@types/pug': 2.0.6 '@types/sass': 1.45.0 @@ -29768,8 +30138,8 @@ snapshots: optionalDependencies: '@babel/core': 7.24.9 less: 4.2.0 - postcss: 8.4.41 - postcss-load-config: 4.0.1(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) + postcss: 8.5.32 + postcss-load-config: 4.0.1(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) sass: 1.77.8 typescript: 5.5.4 @@ -30151,7 +30521,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4))(typescript@5.5.4): + tsup@6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4))(typescript@5.5.4): dependencies: bundle-require: 4.0.1(esbuild@0.17.19) cac: 6.7.14 @@ -30161,20 +30531,20 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 3.1.4(postcss@8.4.41)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4)) + postcss-load-config: 3.1.4(postcss@8.5.32)(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4)) resolve-from: 5.0.0 rollup: 3.25.0 source-map: 0.8.0-beta.0 sucrase: 3.32.0 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.4.41 + postcss: 8.5.32 typescript: 5.5.4 transitivePeerDependencies: - supports-color - ts-node - tsup@6.7.0(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4): + tsup@6.7.0(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4): dependencies: bundle-require: 4.0.1(esbuild@0.17.19) cac: 6.7.14 @@ -30184,14 +30554,14 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 3.1.4(postcss@8.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) + postcss-load-config: 3.1.4(postcss@8.5.32)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) resolve-from: 5.0.0 rollup: 3.25.0 source-map: 0.8.0-beta.0 sucrase: 3.32.0 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.4.41 + postcss: 8.5.32 typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -30905,6 +31275,8 @@ snapshots: vscode-textmate@8.0.0: {} + vue-component-type-helpers@3.3.14: {} + vue-template-compiler@2.7.16: dependencies: de-indent: 1.0.2 @@ -30917,6 +31289,16 @@ snapshots: semver: 7.6.0 typescript: 5.5.4 + vue@3.5.45(typescript@5.5.4): + dependencies: + '@vue/compiler-dom': 3.5.45 + '@vue/compiler-sfc': 3.5.45 + '@vue/runtime-dom': 3.5.45 + '@vue/server-renderer': 3.5.45 + '@vue/shared': 3.5.45 + optionalDependencies: + typescript: 5.5.4 + w3c-keyname@2.2.8: {} w3c-xmlserializer@4.0.0: From 7963b96092b4573bbe2d56d9150d7b51b48a0537 Mon Sep 17 00:00:00 2001 From: cocvn157 <120394358+cocvn157@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:28:19 +0700 Subject: [PATCH 2/3] fix(vue): isolate SSR request state, preserve selected variants for hydration, and remove raw cookie header from withAbby --- packages/vue/src/createAbby.ts | 326 ++++++++++++++++++++++++--------- packages/vue/test/ssr.test.ts | 66 +++++++ 2 files changed, 304 insertions(+), 88 deletions(-) diff --git a/packages/vue/src/createAbby.ts b/packages/vue/src/createAbby.ts index 1f6b6a91..424e50da 100644 --- a/packages/vue/src/createAbby.ts +++ b/packages/vue/src/createAbby.ts @@ -8,6 +8,7 @@ import { type RemoteConfigValueString, type RemoteConfigValueStringToType, type ValidatorType, + getABStorageKey, } from "@tryabby/core"; import type { Infer } from "@tryabby/core/validation"; import { @@ -55,6 +56,36 @@ export type ABTestReturnValue = Lookup extends undefined ? Lookup[TestVariant] : never; +export type AbbyInitialData = + AbbyDataResponse & { + selectedVariants?: Partial>; + }; + +/** + * Parses a raw HTTP `Cookie` header string into a key-value map. + */ +function parseCookieHeader(cookieHeader?: string | null): Map { + const cookies = new Map(); + if (!cookieHeader) return cookies; + + for (const part of cookieHeader.split(";")) { + const separatorIndex = part.indexOf("="); + if (separatorIndex === -1) continue; + const rawKey = part.slice(0, separatorIndex).trim(); + const rawValue = part.slice(separatorIndex + 1).trim(); + if (!rawKey) continue; + try { + cookies.set(decodeURIComponent(rawKey), decodeURIComponent(rawValue)); + } catch { + cookies.set(rawKey, rawValue); + } + } + return cookies; +} + +/** + * Creates a typed Abby instance, Vue composables, provider/plugin, and SSR helpers. + */ export function createAbby< const FlagName extends string, const TestName extends string, @@ -75,7 +106,9 @@ export function createAbby< User > ) { - const abby = new Abby< + const config = abbyConfig; + + type AbbyInstance = Abby< FlagName, TestName, Tests, @@ -83,85 +116,173 @@ export function createAbby< RemoteConfigName, string[], User - >( - abbyConfig, - { - get: (key: string) => { - if (typeof window === "undefined") return null; - return TestStorageService.get(abbyConfig.projectId, key); - }, - set: (key: string, value: any) => { - if ( - typeof window === "undefined" || - abbyConfig.cookies?.disableByDefault - ) - return; - TestStorageService.set(abbyConfig.projectId, key, value); - }, - }, - { - get: (key: string) => { - if (typeof window === "undefined") return null; - return FlagStorageService.get(abbyConfig.projectId, key); - }, - set: (key: string, value: any) => { - if (typeof window === "undefined") return; - FlagStorageService.set(abbyConfig.projectId, key, value); - }, - }, - { - get: (key: string) => { - if (typeof window === "undefined") return null; - return RemoteConfigStorageService.get(abbyConfig.projectId, key); + >; + type AbbyProjectData = ReturnType; + + type AbbyScopedContext = { + abby: AbbyInstance; + state: ShallowRef; + initData: (initialData?: AbbyInitialData) => void; + }; + + /** + * Creates an isolated Abby instance and reactive state container so SSR requests do not share mutable state. + */ + const createScopedContext = ( + requestVariants?: Map + ): AbbyScopedContext => { + const serverTestStore = new Map(requestVariants); + + const instanceConfig: AbbyConfig< + FlagName, + Tests, + string[], + RemoteConfigName, + RemoteConfig, + User + > = { + ...config, + cookies: config.cookies ? { ...config.cookies } : undefined, + }; + + const instance = new Abby< + FlagName, + TestName, + Tests, + RemoteConfig, + RemoteConfigName, + string[], + User + >( + instanceConfig, + { + get: (key: string) => { + if (typeof window === "undefined") { + return serverTestStore.get(key) ?? null; + } + return ( + TestStorageService.get(instanceConfig.projectId, key) ?? + serverTestStore.get(key) ?? + null + ); + }, + set: (key: string, value: any) => { + if (instanceConfig.cookies?.disableByDefault) return; + serverTestStore.set(key, String(value)); + if (typeof window !== "undefined") { + TestStorageService.set(instanceConfig.projectId, key, value); + } + }, }, - set: (key: string, value: any) => { - if (typeof window === "undefined") return; - RemoteConfigStorageService.set(abbyConfig.projectId, key, value); + { + get: (key: string) => { + if (typeof window === "undefined") return null; + return FlagStorageService.get(instanceConfig.projectId, key); + }, + set: (key: string, value: any) => { + if (typeof window === "undefined") return; + FlagStorageService.set(instanceConfig.projectId, key, value); + }, }, - } - ); - - const config = abbyConfig; + { + get: (key: string) => { + if (typeof window === "undefined") return null; + return RemoteConfigStorageService.get(instanceConfig.projectId, key); + }, + set: (key: string, value: any) => { + if (typeof window === "undefined") return; + RemoteConfigStorageService.set(instanceConfig.projectId, key, value); + }, + } + ); - type AbbyProjectData = ReturnType; + const state = shallowRef({ + tests: {}, + flags: {}, + remoteConfig: {}, + } as AbbyProjectData); - const abbyState = shallowRef({ - tests: {}, - flags: {}, - remoteConfig: {}, - } as AbbyProjectData); + instance.subscribe((data) => { + state.value = data as AbbyProjectData; + }); - abby.subscribe((data) => { - abbyState.value = data as AbbyProjectData; - }); + let inited = false; - let inited = false; + const initData = (initialData?: AbbyInitialData) => { + if (initialData) { + inited = true; + if ( + initialData.selectedVariants && + !instanceConfig.cookies?.disableByDefault + ) { + for (const [testName, selected] of Object.entries( + initialData.selectedVariants + )) { + if (typeof selected !== "string" || !selected) continue; + serverTestStore.set(testName, selected); + if ( + typeof window !== "undefined" && + !TestStorageService.get(instanceConfig.projectId, testName) + ) { + TestStorageService.set( + instanceConfig.projectId, + testName, + selected + ); + } + } + } + + const initialized = instance.init(initialData); + state.value = initialized; + + if (!instanceConfig.cookies?.disableByDefault) { + const serializedVariants: Partial> = { + ...initialData.selectedVariants, + }; + for (const [testName, testData] of Object.entries( + initialized.tests + )) { + const selected = (testData as { selectedVariant?: string }) + ?.selectedVariant; + if (selected) { + serializedVariants[testName as TestName] = selected; + } + } + initialData.selectedVariants = serializedVariants; + } + return; + } - const initData = (initialData?: AbbyDataResponse) => { - if (initialData) { + if (inited || typeof window === "undefined") return; inited = true; - abbyState.value = abby.init(initialData); - return; - } - if (inited || typeof window === "undefined") return; - inited = true; - abby.loadProjectData().then((data) => { - if (!data) return; - abbyState.value = data; - }); + instance.loadProjectData().then((data) => { + if (!data) return; + state.value = data; + }); + }; + + return { + abby: instance, + state, + initData, + }; }; - const AbbyKey: InjectionKey> = Symbol( + const defaultContext = createScopedContext(); + const abby = defaultContext.abby; + + const AbbyKey: InjectionKey = Symbol( `abby:${config.projectId}` ); - const resolveState = (): ShallowRef => { + const resolveContext = (): AbbyScopedContext => { const instance = getCurrentInstance(); const injected = instance ? inject(AbbyKey, undefined) : undefined; if (injected) return injected; - initData(); - return abbyState; + defaultContext.initData(); + return defaultContext; }; const useAbby = < @@ -178,7 +299,7 @@ export function createAbby< variant: ComputedRef>; onAct: () => void; } => { - const state = resolveState(); + const { state } = resolveContext(); const selectedVariant = computed( () => @@ -224,17 +345,17 @@ export function createAbby< }; const useFeatureFlag = (name: FlagName): ComputedRef => { - const state = resolveState(); + const ctx = resolveContext(); return computed(() => { - void state.value; - return abby.getFeatureFlag(name); + void ctx.state.value; + return ctx.abby.getFeatureFlag(name); }); }; const useFeatureFlags = (): ComputedRef< Array<{ name: FlagName; value: boolean }> > => { - const state = resolveState(); + const { state } = resolveContext(); return computed(() => (Object.keys(state.value.flags) as Array).map((flagName) => ({ name: flagName, @@ -249,7 +370,7 @@ export function createAbby< value: RemoteConfigValueStringToType; }> > => { - const state = resolveState(); + const { state } = resolveContext(); return computed(() => (Object.keys(state.value.remoteConfig) as Array).map( (configName) => ({ @@ -269,10 +390,10 @@ export function createAbby< >( remoteConfigName: T ): ComputedRef> => { - const state = resolveState(); + const ctx = resolveContext(); return computed(() => { - void state.value; - return abby.getRemoteConfig( + void ctx.state.value; + return ctx.abby.getRemoteConfig( remoteConfigName ) as RemoteConfigValueStringToType; }); @@ -282,28 +403,36 @@ export function createAbby< name: "AbbyProvider", props: { initialData: { - type: Object as PropType, + type: Object as PropType>, required: false, default: undefined, }, }, setup(props, { slots }) { - provide(AbbyKey, abbyState); - initData(props.initialData); + const scoped = + typeof window === "undefined" + ? createScopedContext() + : defaultContext; + provide(AbbyKey, scoped); + scoped.initData(props.initialData); return () => slots.default?.() ?? null; }, }); const AbbyVuePlugin: Plugin = { - install(app: App, options?: { initialData?: AbbyDataResponse }) { - app.provide(AbbyKey, abbyState); - initData(options?.initialData); + install(app: App, options?: { initialData?: AbbyInitialData }) { + const scoped = + typeof window === "undefined" + ? createScopedContext() + : defaultContext; + app.provide(AbbyKey, scoped); + scoped.initData(options?.initialData); }, }; const getFeatureFlagValue = (name: FlagName) => { - return abby.getFeatureFlag(name); + return resolveContext().abby.getFeatureFlag(name); }; const getRemoteConfig = < @@ -312,7 +441,7 @@ export function createAbby< >( remoteConfigName: T ): RemoteConfigValueStringToType => { - return abby.getRemoteConfig(remoteConfigName); + return resolveContext().abby.getRemoteConfig(remoteConfigName); }; const getABTestValue = < @@ -326,7 +455,7 @@ export function createAbby< testName: TestName, lookupObject?: Lookup ): ABTestReturnValue => { - const variant = abby.getTestVariant(testName); + const variant = resolveContext().abby.getTestVariant(testName); if (lookupObject === undefined) { return variant as any; } @@ -338,6 +467,7 @@ export function createAbby< return defineComponent({ name: "AbbyDevtools", setup() { + const ctx = resolveContext(); let destroy: (() => void) | undefined; onMounted(() => { @@ -348,7 +478,7 @@ export function createAbby< return; } - destroy = factory.create({ ...props, abby }); + destroy = factory.create({ ...props, abby: ctx.abby }); }); onUnmounted(() => { @@ -367,7 +497,7 @@ export function createAbby< }; const getVariants = (name: T) => { - return abby.getVariants(name); + return resolveContext().abby.getVariants(name); }; const updateUserProperties = ( @@ -375,7 +505,7 @@ export function createAbby< -readonly [K in keyof User]: Infer; }> ) => { - abby.updateUserProperties(user); + resolveContext().abby.updateUserProperties(user); }; const withAbby = any>( @@ -384,18 +514,38 @@ export function createAbby< return async (event?: any) => { const data = await handler?.(event); - const __abby__data = await HttpService.getProjectData({ + const rawProjectData = await HttpService.getProjectData({ url: config.apiUrl, projectId: config.projectId, environment: config.currentEnvironment, }); + let __abby__data: AbbyInitialData | null = rawProjectData + ? { ...rawProjectData } + : null; + + if (__abby__data && !config.cookies?.disableByDefault) { + const rawCookieHeader = + event?.node?.req?.headers?.cookie ?? + event?.request?.headers?.get?.("cookie"); + const parsedCookies = parseCookieHeader(rawCookieHeader); + const requestVariants = new Map(); + + for (const testName of Object.keys(config.tests ?? {})) { + const storageKey = getABStorageKey(config.projectId, testName); + const cookieVariant = parsedCookies.get(storageKey); + if (cookieVariant) { + requestVariants.set(testName, cookieVariant); + } + } + + const requestScoped = createScopedContext(requestVariants); + requestScoped.initData(__abby__data); + } + return { ...data, __abby__data, - __abby_cookie: - event?.node?.req?.headers?.cookie ?? - event?.request?.headers?.get?.("cookie"), }; }; }; diff --git a/packages/vue/test/ssr.test.ts b/packages/vue/test/ssr.test.ts index 72a9e981..2b77d4a1 100644 --- a/packages/vue/test/ssr.test.ts +++ b/packages/vue/test/ssr.test.ts @@ -97,6 +97,72 @@ describe("useAbby", () => { expect(serverSideHTML).toContain('data-testid="current"'); expect(serverSideHTML).not.toContain("Very Secret"); }); + + it("preserves the server-selected variant for client hydration with non-deterministic weights and isolates concurrent SSR renders", async () => { + const { AbbyVuePlugin, useAbby, useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "ssr-ab-isolated", + flags: ["beta"], + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const Component = defineComponent({ + setup() { + const { variant } = useAbby("showFooter"); + const beta = useFeatureFlag("beta"); + return () => + h("div", `${variant.value as string}:${beta.value ? "on" : "off"}`); + }, + }); + + const initialDataA = { + flags: [{ name: "beta", value: true }], + tests: [{ name: "showFooter", weights: [0.5, 0.5] }], + remoteConfig: [], + }; + const initialDataB = { + flags: [{ name: "beta", value: false }], + tests: [{ name: "showFooter", weights: [0, 1] }], + remoteConfig: [], + }; + + const appA = createApp(Component); + appA.use(AbbyVuePlugin, { initialData: initialDataA }); + + const appB = createApp(Component); + appB.use(AbbyVuePlugin, { initialData: initialDataB }); + + const [htmlA, htmlB] = await Promise.all([ + renderToString(appA), + renderToString(appB), + ]); + + expect(htmlB).toContain("new:off"); + const chosenVariantA = (initialDataA as any).selectedVariants?.showFooter; + expect(["current", "new"]).toContain(chosenVariantA); + expect(htmlA).toContain(`${chosenVariantA}:on`); + + // A second render hydrated with initialDataA (even if weights in payload differ) keeps chosenVariantA + const hydratedApp = createApp(Component); + hydratedApp.use(AbbyVuePlugin, { + initialData: { + ...initialDataA, + tests: [ + { + name: "showFooter", + weights: chosenVariantA === "current" ? [0, 1] : [1, 0], + }, + ], + }, + }); + const hydratedHtml = await renderToString(hydratedApp); + expect(hydratedHtml).toContain(`${chosenVariantA}:on`); + }); }); describe("useFeatureFlag", () => { From 139b9e81b3137901d0aa7a49be3765ff6324a8f3 Mon Sep 17 00:00:00 2001 From: cocvn157 <120394358+cocvn157@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:20:12 +0700 Subject: [PATCH 3/3] fix(vue): clone tests per SSR context, clear in-memory reset store, and scope SSR user properties --- packages/vue/src/createAbby.ts | 203 ++++++++++++++++---- packages/vue/test/ssr.test.ts | 332 +++++++++++++++++++++++++++++---- 2 files changed, 462 insertions(+), 73 deletions(-) diff --git a/packages/vue/src/createAbby.ts b/packages/vue/src/createAbby.ts index 424e50da..ffc846e2 100644 --- a/packages/vue/src/createAbby.ts +++ b/packages/vue/src/createAbby.ts @@ -56,10 +56,19 @@ export type ABTestReturnValue = Lookup extends undefined ? Lookup[TestVariant] : never; -export type AbbyInitialData = - AbbyDataResponse & { - selectedVariants?: Partial>; - }; +export type AbbyUserProperties< + User extends Record = Record, +> = Partial<{ + -readonly [K in keyof User]: Infer; +}>; + +export type AbbyInitialData< + TestName extends string = string, + User extends Record = Record, +> = AbbyDataResponse & { + selectedVariants?: Partial>; + userProperties?: AbbyUserProperties; +}; /** * Parses a raw HTTP `Cookie` header string into a key-value map. @@ -122,7 +131,8 @@ export function createAbby< type AbbyScopedContext = { abby: AbbyInstance; state: ShallowRef; - initData: (initialData?: AbbyInitialData) => void; + initData: (initialData?: AbbyInitialData) => void; + clearTest: (testName: string) => void; }; /** @@ -133,6 +143,19 @@ export function createAbby< ): AbbyScopedContext => { const serverTestStore = new Map(requestVariants); + const clonedTests = config.tests + ? (Object.fromEntries( + Object.entries(config.tests).map(([testName, testConfig]) => [ + testName, + { + ...testConfig, + variants: [...testConfig.variants], + weights: testConfig.weights ? [...testConfig.weights] : undefined, + }, + ]) + ) as Tests) + : undefined; + const instanceConfig: AbbyConfig< FlagName, Tests, @@ -142,6 +165,7 @@ export function createAbby< User > = { ...config, + tests: clonedTests as Tests, cookies: config.cookies ? { ...config.cookies } : undefined, }; @@ -157,6 +181,9 @@ export function createAbby< instanceConfig, { get: (key: string) => { + if (instanceConfig.cookies?.disableByDefault) { + return null; + } if (typeof window === "undefined") { return serverTestStore.get(key) ?? null; } @@ -208,9 +235,13 @@ export function createAbby< let inited = false; - const initData = (initialData?: AbbyInitialData) => { + const initData = (initialData?: AbbyInitialData) => { if (initialData) { inited = true; + if (initialData.userProperties) { + instance.updateUserProperties(initialData.userProperties); + } + if ( initialData.selectedVariants && !instanceConfig.cookies?.disableByDefault @@ -267,10 +298,15 @@ export function createAbby< abby: instance, state, initData, + clearTest: (testName: string) => { + serverTestStore.delete(testName); + }, }; }; const defaultContext = createScopedContext(); + let activeClientContext: AbbyScopedContext = defaultContext; + let activeServerContext: AbbyScopedContext | undefined; const abby = defaultContext.abby; const AbbyKey: InjectionKey = Symbol( @@ -281,8 +317,11 @@ export function createAbby< const instance = getCurrentInstance(); const injected = instance ? inject(AbbyKey, undefined) : undefined; if (injected) return injected; - defaultContext.initData(); - return defaultContext; + if (typeof window === "undefined") { + return activeServerContext ?? createScopedContext(); + } + activeClientContext.initData(); + return activeClientContext; }; const useAbby = < @@ -403,16 +442,29 @@ export function createAbby< name: "AbbyProvider", props: { initialData: { - type: Object as PropType>, + type: Object as PropType>, + required: false, + default: undefined, + }, + userProperties: { + type: Object as PropType>, required: false, default: undefined, }, }, setup(props, { slots }) { const scoped = - typeof window === "undefined" + typeof window === "undefined" || + props.initialData || + props.userProperties ? createScopedContext() : defaultContext; + if (props.userProperties) { + scoped.abby.updateUserProperties(props.userProperties); + } + if (typeof window !== "undefined") { + activeClientContext = scoped; + } provide(AbbyKey, scoped); scoped.initData(props.initialData); @@ -421,11 +473,25 @@ export function createAbby< }); const AbbyVuePlugin: Plugin = { - install(app: App, options?: { initialData?: AbbyInitialData }) { + install( + app: App, + options?: { + initialData?: AbbyInitialData; + userProperties?: AbbyUserProperties; + } + ) { const scoped = - typeof window === "undefined" + typeof window === "undefined" || + options?.initialData || + options?.userProperties ? createScopedContext() : defaultContext; + if (options?.userProperties) { + scoped.abby.updateUserProperties(options.userProperties); + } + if (typeof window !== "undefined") { + activeClientContext = scoped; + } app.provide(AbbyKey, scoped); scoped.initData(options?.initialData); }, @@ -491,8 +557,14 @@ export function createAbby< }; const getABResetFunction = (name: T) => { + const instance = getCurrentInstance(); + const injected = instance ? inject(AbbyKey, undefined) : undefined; return () => { - TestStorageService.remove(config.projectId, name as string); + const testKey = name as string; + TestStorageService.remove(config.projectId, testKey); + injected?.clearTest(testKey); + activeClientContext.clearTest(testKey); + defaultContext.clearTest(testKey); }; }; @@ -501,18 +573,62 @@ export function createAbby< }; const updateUserProperties = ( - user: Partial<{ - -readonly [K in keyof User]: Infer; - }> + user: AbbyUserProperties, + targetInitialData?: AbbyInitialData ) => { - resolveContext().abby.updateUserProperties(user); + if (targetInitialData) { + targetInitialData.userProperties = { + ...targetInitialData.userProperties, + ...user, + }; + } + const instance = getCurrentInstance(); + const injected = instance ? inject(AbbyKey, undefined) : undefined; + if (injected) { + injected.abby.updateUserProperties(user); + return; + } + if (typeof window === "undefined") { + activeServerContext?.abby.updateUserProperties(user); + return; + } + activeClientContext.abby.updateUserProperties(user); + if (activeClientContext !== defaultContext) { + defaultContext.abby.updateUserProperties(user); + } }; - const withAbby = any>( + const withAbby = < + Handler extends ( + event?: any, + helpers?: { + updateUserProperties: (user: AbbyUserProperties) => void; + } + ) => any, + >( handler?: Handler ) => { return async (event?: any) => { - const data = await handler?.(event); + let requestUserProperties: AbbyUserProperties | undefined; + const requestScoped = createScopedContext(); + const setRequestUserProperties = (user: AbbyUserProperties) => { + requestUserProperties = { + ...requestUserProperties, + ...user, + }; + requestScoped.abby.updateUserProperties(user); + }; + + const prevServerContext = activeServerContext; + activeServerContext = requestScoped; + let data: any; + try { + data = await handler?.(event, { + updateUserProperties: setRequestUserProperties, + }); + } finally { + activeServerContext = prevServerContext; + } const rawProjectData = await HttpService.getProjectData({ url: config.apiUrl, @@ -520,27 +636,38 @@ export function createAbby< environment: config.currentEnvironment, }); - let __abby__data: AbbyInitialData | null = rawProjectData - ? { ...rawProjectData } - : null; - - if (__abby__data && !config.cookies?.disableByDefault) { - const rawCookieHeader = - event?.node?.req?.headers?.cookie ?? - event?.request?.headers?.get?.("cookie"); - const parsedCookies = parseCookieHeader(rawCookieHeader); - const requestVariants = new Map(); - - for (const testName of Object.keys(config.tests ?? {})) { - const storageKey = getABStorageKey(config.projectId, testName); - const cookieVariant = parsedCookies.get(storageKey); - if (cookieVariant) { - requestVariants.set(testName, cookieVariant); + const __abby__data: AbbyInitialData | null = + rawProjectData + ? { + ...rawProjectData, + ...(requestUserProperties + ? { userProperties: requestUserProperties } + : {}), + } + : null; + + if (__abby__data) { + if (!config.cookies?.disableByDefault) { + const rawCookieHeader = + event?.node?.req?.headers?.cookie ?? + event?.request?.headers?.get?.("cookie"); + const parsedCookies = parseCookieHeader(rawCookieHeader); + const requestVariants = new Map(); + + for (const testName of Object.keys(config.tests ?? {})) { + const storageKey = getABStorageKey(config.projectId, testName); + const cookieVariant = parsedCookies.get(storageKey); + if (cookieVariant) { + requestVariants.set(testName, cookieVariant); + } } - } - const requestScoped = createScopedContext(requestVariants); - requestScoped.initData(__abby__data); + const hydratedContext = createScopedContext(requestVariants); + hydratedContext.initData(__abby__data); + } else if (__abby__data.userProperties) { + const hydratedContext = createScopedContext(); + hydratedContext.initData(__abby__data); + } } return { diff --git a/packages/vue/test/ssr.test.ts b/packages/vue/test/ssr.test.ts index 2b77d4a1..5cf9a82b 100644 --- a/packages/vue/test/ssr.test.ts +++ b/packages/vue/test/ssr.test.ts @@ -1,10 +1,38 @@ +import { HttpService, getABStorageKey } from "@tryabby/core"; +import * as v from "@tryabby/core/validation"; import { renderToString } from "@vue/server-renderer"; -import { createApp, defineComponent, h } from "vue"; +import { createApp, createSSRApp, defineComponent, h } from "vue"; import { createAbby } from "../src"; +import { TestStorageService } from "../src/StorageService"; const OLD_ENV = process.env; +async function runOnServer(fn: () => Promise | T): Promise { + const originalWindow = globalThis.window; + // Simulate Node.js SSR where window is undefined + Object.defineProperty(globalThis, "window", { + value: undefined, + configurable: true, + writable: true, + }); + try { + return await fn(); + } finally { + Object.defineProperty(globalThis, "window", { + value: originalWindow, + configurable: true, + writable: true, + }); + } +} + beforeEach(() => { + document.cookie.split(";").forEach((cookie) => { + document.cookie = cookie + .replace(/^ +/, "") + .replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/`); + }); + vi.restoreAllMocks(); vi.resetModules(); process.env = { ...OLD_ENV }; }); @@ -44,9 +72,11 @@ describe("useAbby", () => { }, }); - const app = createApp(ComponentWithTest); - app.use(AbbyVuePlugin); - const serverSideHTML = await renderToString(app); + const serverSideHTML = await runOnServer(async () => { + const app = createApp(ComponentWithTest); + app.use(AbbyVuePlugin); + return await renderToString(app); + }); expect(serverSideHTML).toContain("SSR!"); expect(serverSideHTML).not.toContain("data-testid="); @@ -79,30 +109,32 @@ describe("useAbby", () => { }, }); - const app = createApp(ComponentWithTest); - app.use(AbbyVuePlugin, { - initialData: { - flags: [], - tests: [ - { - name: "showFooter", - weights: [1, 0], - }, - ], - remoteConfig: [], - }, + const serverSideHTML = await runOnServer(async () => { + const app = createApp(ComponentWithTest); + app.use(AbbyVuePlugin, { + initialData: { + flags: [], + tests: [ + { + name: "showFooter", + weights: [1, 0], + }, + ], + remoteConfig: [], + }, + }); + return await renderToString(app); }); - const serverSideHTML = await renderToString(app); expect(serverSideHTML).toContain('data-testid="current"'); expect(serverSideHTML).not.toContain("Very Secret"); }); - it("preserves the server-selected variant for client hydration with non-deterministic weights and isolates concurrent SSR renders", async () => { + it("hydrates on the browser client for a first visit without existing cookies and isolates concurrent SSR renders", async () => { const { AbbyVuePlugin, useAbby, useFeatureFlag } = createAbby({ environments: [""], currentEnvironment: "", - projectId: "ssr-ab-isolated", + projectId: "ssr-ab-first-visit", flags: ["beta"], tests: { showFooter: { @@ -120,36 +152,41 @@ describe("useAbby", () => { }, }); - const initialDataA = { + const initialDataA: any = { flags: [{ name: "beta", value: true }], tests: [{ name: "showFooter", weights: [0.5, 0.5] }], remoteConfig: [], }; - const initialDataB = { + const initialDataB: any = { flags: [{ name: "beta", value: false }], tests: [{ name: "showFooter", weights: [0, 1] }], remoteConfig: [], }; - const appA = createApp(Component); - appA.use(AbbyVuePlugin, { initialData: initialDataA }); + const [htmlA, htmlB] = await runOnServer(async () => { + const appA = createSSRApp(Component); + appA.use(AbbyVuePlugin, { initialData: initialDataA }); - const appB = createApp(Component); - appB.use(AbbyVuePlugin, { initialData: initialDataB }); + const appB = createSSRApp(Component); + appB.use(AbbyVuePlugin, { initialData: initialDataB }); - const [htmlA, htmlB] = await Promise.all([ - renderToString(appA), - renderToString(appB), - ]); + return await Promise.all([renderToString(appA), renderToString(appB)]); + }); expect(htmlB).toContain("new:off"); - const chosenVariantA = (initialDataA as any).selectedVariants?.showFooter; + const chosenVariantA = initialDataA.selectedVariants?.showFooter; expect(["current", "new"]).toContain(chosenVariantA); expect(htmlA).toContain(`${chosenVariantA}:on`); + expect( + TestStorageService.get("ssr-ab-first-visit", "showFooter") + ).toBeNull(); - // A second render hydrated with initialDataA (even if weights in payload differ) keeps chosenVariantA - const hydratedApp = createApp(Component); - hydratedApp.use(AbbyVuePlugin, { + // Hydrate into a real browser DOM container on the client + const container = document.createElement("div"); + container.innerHTML = htmlA; + + const clientApp = createSSRApp(Component); + clientApp.use(AbbyVuePlugin, { initialData: { ...initialDataA, tests: [ @@ -160,8 +197,233 @@ describe("useAbby", () => { ], }, }); - const hydratedHtml = await renderToString(hydratedApp); - expect(hydratedHtml).toContain(`${chosenVariantA}:on`); + clientApp.mount(container); + + expect(container.textContent).toBe(`${chosenVariantA}:on`); + expect(TestStorageService.get("ssr-ab-first-visit", "showFooter")).toBe( + chosenVariantA + ); + clientApp.unmount(); + }); + + it("hydrates on the browser client when an existing request cookie is present", async () => { + const projectId = "ssr-ab-existing-cookie"; + vi.spyOn(HttpService, "getProjectData").mockResolvedValue({ + flags: [], + tests: [{ name: "showFooter", weights: [0, 1] }], + remoteConfig: [], + }); + + const { AbbyVuePlugin, useAbby, withAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId, + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const Component = defineComponent({ + setup() { + const { variant } = useAbby("showFooter"); + return () => h("div", variant.value as string); + }, + }); + + const cookieKey = getABStorageKey(projectId, "showFooter"); + const cookieHeader = `${cookieKey}=current`; + + const { serverHtml, initialData } = await runOnServer(async () => { + const loader = withAbby(async () => ({ ok: true })); + const payload = await loader({ + node: { req: { headers: { cookie: cookieHeader } } }, + }); + const serverApp = createSSRApp(Component); + serverApp.use(AbbyVuePlugin, { initialData: payload.__abby__data! }); + const html = await renderToString(serverApp); + return { serverHtml: html, initialData: payload.__abby__data! }; + }); + + expect(serverHtml).toContain("current"); + expect(initialData.selectedVariants?.showFooter).toBe("current"); + + // Browser client already has the cookie and hydrates the server HTML + TestStorageService.set(projectId, "showFooter", "current"); + const container = document.createElement("div"); + container.innerHTML = serverHtml; + + const clientApp = createSSRApp(Component); + clientApp.use(AbbyVuePlugin, { initialData }); + clientApp.mount(container); + + expect(container.textContent).toBe("current"); + clientApp.unmount(); + }); + + it("hydrates on the browser client with cookies.disableByDefault enabled", async () => { + const projectId = "ssr-ab-disabled-cookies"; + vi.spyOn(HttpService, "getProjectData").mockResolvedValue({ + flags: [], + tests: [{ name: "showFooter", weights: [0, 1] }], + remoteConfig: [], + }); + + const { AbbyVuePlugin, useAbby, withAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId, + cookies: { disableByDefault: true }, + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const Component = defineComponent({ + setup() { + const { variant } = useAbby("showFooter"); + return () => h("div", variant.value as string); + }, + }); + + const cookieKey = getABStorageKey(projectId, "showFooter"); + // Even if a stale cookie says "current", disabled cookies must ignore it and use highest weight ("new") + TestStorageService.set(projectId, "showFooter", "current"); + + const { serverHtml, initialData } = await runOnServer(async () => { + const loader = withAbby(); + const payload = await loader({ + node: { req: { headers: { cookie: `${cookieKey}=current` } } }, + }); + const serverApp = createSSRApp(Component); + serverApp.use(AbbyVuePlugin, { initialData: payload.__abby__data! }); + const html = await renderToString(serverApp); + return { serverHtml: html, initialData: payload.__abby__data! }; + }); + + expect(serverHtml).toContain("new"); + expect(initialData.selectedVariants).toBeUndefined(); + + const container = document.createElement("div"); + container.innerHTML = serverHtml; + + const clientApp = createSSRApp(Component); + clientApp.use(AbbyVuePlugin, { initialData }); + clientApp.mount(container); + + expect(container.textContent).toBe("new"); + clientApp.unmount(); + }); + + it("clears both cookie and in-memory variant when getABResetFunction is called", async () => { + const projectId = "ssr-ab-reset"; + const { AbbyVuePlugin, getABTestValue, getABResetFunction } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId, + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const container = document.createElement("div"); + const app = createApp(defineComponent({ render: () => h("div") })); + app.use(AbbyVuePlugin, { + initialData: { + flags: [], + tests: [{ name: "showFooter", weights: [0, 1] }], + remoteConfig: [], + selectedVariants: { showFooter: "current" }, + }, + }); + app.mount(container); + + expect(getABTestValue("showFooter")).toBe("current"); + expect(TestStorageService.get(projectId, "showFooter")).toBe("current"); + + const resetShowFooter = getABResetFunction("showFooter"); + resetShowFooter(); + + expect(TestStorageService.get(projectId, "showFooter")).toBeNull(); + // Next lookup draws using weights [0, 1] ("new") instead of returning stale "current" + expect(getABTestValue("showFooter")).toBe("new"); + app.unmount(); + }); + + it("scopes updateUserProperties per SSR request without leaking across requests", async () => { + vi.spyOn(HttpService, "getProjectData").mockResolvedValue({ + flags: [ + { + name: "proFeature", + value: false, + ruleSet: [ + { + propertyName: "plan", + propertyType: "string", + operator: "eq", + value: "pro", + thenValue: true, + }, + ], + }, + ], + tests: [], + remoteConfig: [], + }); + + const { AbbyVuePlugin, useFeatureFlag, withAbby, getFeatureFlagValue } = + createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "ssr-user-props", + flags: ["proFeature"], + user: { + plan: v.string(), + }, + }); + + const Component = defineComponent({ + setup() { + const pro = useFeatureFlag("proFeature"); + return () => h("div", pro.value ? "PRO" : "FREE"); + }, + }); + + const [htmlPro, htmlFree] = await runOnServer(async () => { + const proLoader = withAbby(async (_event, helpers) => { + helpers?.updateUserProperties({ plan: "pro" }); + return {}; + }); + const freeLoader = withAbby(async (_event, helpers) => { + helpers?.updateUserProperties({ plan: "free" }); + return {}; + }); + + const [proPayload, freePayload] = await Promise.all([ + proLoader(), + freeLoader(), + ]); + + const appPro = createSSRApp(Component); + appPro.use(AbbyVuePlugin, { initialData: proPayload.__abby__data! }); + + const appFree = createSSRApp(Component); + appFree.use(AbbyVuePlugin, { initialData: freePayload.__abby__data! }); + + return await Promise.all([ + renderToString(appPro), + renderToString(appFree), + ]); + }); + + expect(htmlPro).toContain("PRO"); + expect(htmlFree).toContain("FREE"); + expect(getFeatureFlagValue("proFeature")).toBe(false); }); });