From f3103324877211d439671f4b04cb5e2bf12676bc Mon Sep 17 00:00:00 2001 From: viurs19 Date: Fri, 6 Nov 2026 16:23:41 +0000 Subject: [PATCH 1/3] feat(vue): add typed Vue 3 integration --- .changeset/add-vue-integration.md | 5 + packages/vue/.gitignore | 2 + packages/vue/README.md | 42 +++++ packages/vue/package.json | 43 +++++ packages/vue/src/index.ts | 270 +++++++++++++++++++++++++++++ packages/vue/tests/runtime.test.ts | 120 +++++++++++++ packages/vue/tests/ssr.test.ts | 41 +++++ packages/vue/tests/types.test.ts | 51 ++++++ packages/vue/tsconfig.json | 8 + packages/vue/tsup.config.ts | 10 ++ packages/vue/vite.config.ts | 10 ++ pnpm-lock.yaml | 266 ++++++++++++++++++++++++---- 12 files changed, 830 insertions(+), 38 deletions(-) create mode 100644 .changeset/add-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/index.ts create mode 100644 packages/vue/tests/runtime.test.ts create mode 100644 packages/vue/tests/ssr.test.ts create mode 100644 packages/vue/tests/types.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/add-vue-integration.md b/.changeset/add-vue-integration.md new file mode 100644 index 00000000..b541f9c5 --- /dev/null +++ b/.changeset/add-vue-integration.md @@ -0,0 +1,5 @@ +--- +"@tryabby/vue": minor +--- + +Add the typed Vue 3 integration for A/B tests, feature flags, and remote config. diff --git a/packages/vue/.gitignore b/packages/vue/.gitignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/packages/vue/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 00000000..f1b4e79a --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,42 @@ +# `@tryabby/vue` + +Typed Vue 3 composables for Abby A/B tests, feature flags, and remote config. + +```ts +import { createAbby } from "@tryabby/vue"; + +export const abby = createAbby({ + projectId: "project-id", + environments: ["development", "production"], + currentEnvironment: "development", + tests: { checkout: { variants: ["control", "new"] } }, + flags: ["newNav"], + remoteConfig: { headline: "String" }, +}); +``` + +Wrap the application (or a Nuxt layout) in the returned provider: + +```vue + + + +``` + +Use the typed composables in descendants. Values are Vue computed refs and are automatically unwrapped in templates. + +```vue + +``` + +For SSR, pass server-loaded `AbbyDataResponse` as `initialData`. A/B variants intentionally render empty until client mount to avoid hydration mismatches; hydrated feature flags and remote config can render on the server. diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 00000000..47227031 --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,43 @@ +{ + "name": "@tryabby/vue", + "version": "7.0.1", + "description": "Vue 3 integration for Abby", + "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 run", + "typecheck": "tsc --noEmit" + }, + "homepage": "https://docs.tryabby.com", + "license": "ISC", + "peerDependencies": { + "vue": "^3.3.0" + }, + "dependencies": { + "@tryabby/core": "workspace:*", + "js-cookie": "^3.0.5" + }, + "devDependencies": { + "@types/js-cookie": "^3.0.3", + "@vue/server-renderer": "3.4.24", + "@vue/test-utils": "2.4.6", + "jsdom": "^20.0.3", + "tsconfig": "workspace:*", + "tsup": "^6.5.0", + "typescript": "5.5.4", + "vite": "5.4.0", + "vitest": "2.0.5", + "vue": "3.4.24" + } +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 00000000..6b15a4d4 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,270 @@ +import { + type ABConfig, + Abby, + type AbbyConfig, + type AbbyDataResponse, + AbbyEventType, + HttpService, + type RemoteConfigValueString, + type RemoteConfigValueStringToType, + type StorageServiceOptions, + type ValidatorType, + getABStorageKey, + getFFStorageKey, + getRCStorageKey, +} from "@tryabby/core"; +import Cookie from "js-cookie"; +import { + type ComputedRef, + type InjectionKey, + type PropType, + type Ref, + computed, + defineComponent, + inject, + onMounted, + onScopeDispose, + provide, + ref, + watch, +} from "vue"; + +export { type ABConfig, type AbbyConfig, defineConfig } from "@tryabby/core"; + +export type ABTestReturnValue = Lookup extends undefined + ? Variant + : Variant extends keyof Lookup + ? Lookup[Variant] + : never; + +function cookieStorage( + projectId: string, + keyFor: (projectId: string, name: string) => string +) { + return { + get(name: string): string | null { + if (typeof document === "undefined") return null; + return Cookie.get(keyFor(projectId, name)) ?? null; + }, + set(name: string, value: string, options?: StorageServiceOptions): void { + if (typeof document === "undefined") return; + Cookie.set(keyFor(projectId, name), value, { + expires: options?.expiresInDays ?? 365, + }); + }, + }; +} + +export function createAbby< + const FlagName extends string, + const TestName extends string, + const Tests extends Record, + const RemoteConfig extends Record, + const User extends Record = Record< + string, + ValidatorType + >, +>( + abbyConfig: AbbyConfig< + FlagName, + Tests, + string[], + Extract, + RemoteConfig, + User + > +) { + type RemoteConfigName = Extract; + type Client = Abby< + FlagName, + TestName, + Tests, + RemoteConfig, + RemoteConfigName, + string[], + User + >; + type Context = { + client: Client; + revision: Ref; + mounted: Ref; + ready: Ref; + }; + + const injectionKey: InjectionKey = Symbol("tryabby-vue"); + + const createClient = (): Client => { + // Core enriches tests with weights and can change cookie consent. Each + // provider gets a fresh config so Vue SSR requests cannot leak state. + const config = { + ...abbyConfig, + tests: abbyConfig.tests + ? (Object.fromEntries( + (Object.entries(abbyConfig.tests) as Array<[string, ABConfig]>).map( + ([name, test]) => [ + name, + { ...test, variants: [...test.variants] }, + ] + ) + ) as unknown as Tests) + : undefined, + flags: abbyConfig.flags ? [...abbyConfig.flags] : undefined, + remoteConfig: abbyConfig.remoteConfig + ? { ...abbyConfig.remoteConfig } + : undefined, + cookies: abbyConfig.cookies ? { ...abbyConfig.cookies } : undefined, + } as AbbyConfig< + FlagName, + Tests, + string[], + RemoteConfigName, + RemoteConfig, + User + >; + + return new Abby< + FlagName, + TestName, + Tests, + RemoteConfig, + RemoteConfigName, + string[], + User + >( + config, + cookieStorage(config.projectId, getABStorageKey), + cookieStorage(config.projectId, getFFStorageKey), + cookieStorage(config.projectId, getRCStorageKey) + ); + }; + + const AbbyProvider = defineComponent({ + name: "AbbyProvider", + props: { + initialData: Object as PropType, + }, + emits: ["error"], + setup(props, { emit, slots }) { + const client = createClient(); + const revision = ref(0); + const mounted = ref(false); + const ready = ref(false); + + if (props.initialData) { + client.init(props.initialData); + ready.value = true; + } + + const unsubscribe = client.subscribe(() => { + revision.value += 1; + }); + onScopeDispose(unsubscribe); + provide(injectionKey, { client, revision, mounted, ready }); + + onMounted(async () => { + mounted.value = true; + if (props.initialData) return; + + try { + await client.loadProjectData(); + } catch (error) { + emit("error", error); + } finally { + ready.value = true; + revision.value += 1; + } + }); + + return () => slots.default?.(); + }, + }); + + const useContext = (): Context => { + const context = inject(injectionKey, null); + if (!context) { + throw new Error( + "Abby composables must be used inside this createAbby's AbbyProvider." + ); + } + return context; + }; + + const useAbby = < + Name extends keyof Tests, + Variant extends Tests[Name]["variants"][number], + LookupValue, + const Lookup extends Record | undefined = undefined, + >( + name: Name, + lookup?: Lookup + ): { + variant: ComputedRef>; + onAct: () => void; + } => { + const context = useContext(); + const selected = computed(() => { + context.revision.value; + if (!context.mounted.value || !context.ready.value) return ""; + return context.client.getTestVariant(name) as Variant; + }); + + const send = (type: AbbyEventType): void => { + if (!selected.value) return; + void HttpService.sendData({ + url: abbyConfig.apiUrl, + type, + data: { + projectId: abbyConfig.projectId, + testName: name as string, + selectedVariant: selected.value, + }, + }); + }; + + watch( + selected, + (variant, previous) => { + if (variant && variant !== previous) send(AbbyEventType.PING); + }, + { flush: "post" } + ); + + const variant = computed(() => { + if (!selected.value) return ""; + return lookup === undefined + ? selected.value + : lookup[selected.value as Variant]; + }) as ComputedRef>; + + return { + variant, + onAct: () => send(AbbyEventType.ACT), + }; + }; + + const useFeatureFlag = (name: FlagName): ComputedRef => { + const context = useContext(); + return computed(() => { + context.revision.value; + return context.client.getFeatureFlag(name); + }); + }; + + const useRemoteConfig = ( + name: Name + ): ComputedRef> => { + const context = useContext(); + return computed(() => { + context.revision.value; + return context.client.getRemoteConfig(name); + }); + }; + + return { + AbbyProvider, + useAbby, + useFeatureFlag, + useRemoteConfig, + useAbbyClient: () => useContext().client, + }; +} diff --git a/packages/vue/tests/runtime.test.ts b/packages/vue/tests/runtime.test.ts new file mode 100644 index 00000000..0ab21432 --- /dev/null +++ b/packages/vue/tests/runtime.test.ts @@ -0,0 +1,120 @@ +import { + type AbbyDataResponse, + AbbyEventType, + HttpService, +} from "@tryabby/core"; +import { mount } from "@vue/test-utils"; +import { defineComponent, nextTick } from "vue"; +import { createAbby } from "../src"; + +const initialData: AbbyDataResponse = { + tests: [{ name: "checkout", weights: [0, 1] }], + flags: [{ name: "newNav", value: true }], + remoteConfig: [{ name: "headline", value: "Ship faster" }], +}; + +function createSdk() { + return createAbby({ + projectId: "vue-test", + environments: ["test"], + currentEnvironment: "test", + cookies: { disableByDefault: true }, + tests: { + checkout: { variants: ["control", "streamlined"] }, + }, + flags: ["newNav"], + remoteConfig: { + headline: "String", + }, + }); +} + +describe("@tryabby/vue", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("exposes the selected variant and sends PING and ACT events", async () => { + const sendData = vi + .spyOn(HttpService, "sendData") + .mockResolvedValue(undefined as never); + const sdk = createSdk(); + const Consumer = defineComponent({ + setup() { + return sdk.useAbby("checkout"); + }, + template: '', + }); + + const wrapper = mount(sdk.AbbyProvider, { + props: { initialData }, + slots: { default: Consumer }, + }); + await nextTick(); + + expect(wrapper.text()).toBe("streamlined"); + expect(sendData).toHaveBeenCalledWith( + expect.objectContaining({ type: AbbyEventType.PING }) + ); + + await wrapper.get("button").trigger("click"); + expect(sendData).toHaveBeenLastCalledWith( + expect.objectContaining({ + type: AbbyEventType.ACT, + data: expect.objectContaining({ + projectId: "vue-test", + testName: "checkout", + selectedVariant: "streamlined", + }), + }) + ); + }); + + it("reacts to core feature-flag and remote-config updates", async () => { + vi.spyOn(HttpService, "sendData").mockResolvedValue(undefined as never); + const sdk = createSdk(); + const Consumer = defineComponent({ + setup() { + const client = sdk.useAbbyClient(); + return { + client, + flag: sdk.useFeatureFlag("newNav"), + headline: sdk.useRemoteConfig("headline"), + }; + }, + template: + '
{{ flag }}{{ headline }}
', + }); + + const wrapper = mount(sdk.AbbyProvider, { + props: { initialData }, + slots: { default: Consumer }, + }); + + expect(wrapper.get(".flag").text()).toBe("true"); + expect(wrapper.get(".headline").text()).toBe("Ship faster"); + + const consumer = wrapper.findComponent(Consumer); + consumer.vm.client.updateFlag("newNav", false); + consumer.vm.client.updateRemoteConfig("headline", "Updated"); + await nextTick(); + + expect(wrapper.get(".flag").text()).toBe("false"); + expect(wrapper.get(".headline").text()).toBe("Updated"); + }); + + it("requires composables to use their matching provider", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + const sdk = createSdk(); + const Consumer = defineComponent({ + setup() { + sdk.useFeatureFlag("newNav"); + return () => null; + }, + }); + + expect(() => mount(Consumer)).toThrow( + "Abby composables must be used inside this createAbby's AbbyProvider." + ); + }); +}); diff --git a/packages/vue/tests/ssr.test.ts b/packages/vue/tests/ssr.test.ts new file mode 100644 index 00000000..5604ad6d --- /dev/null +++ b/packages/vue/tests/ssr.test.ts @@ -0,0 +1,41 @@ +import { renderToString } from "@vue/server-renderer"; +import { createSSRApp, defineComponent, h } from "vue"; +import { createAbby } from "../src"; + +it("renders hydrated flags and config but no server-selected variant", async () => { + const sdk = createAbby({ + projectId: "ssr-test", + environments: ["test"], + currentEnvironment: "test", + cookies: { disableByDefault: true }, + tests: { checkout: { variants: ["control", "streamlined"] } }, + flags: ["newNav"], + remoteConfig: { headline: "String" }, + }); + const Consumer = defineComponent({ + setup() { + return { + ...sdk.useAbby("checkout"), + flag: sdk.useFeatureFlag("newNav"), + headline: sdk.useRemoteConfig("headline"), + }; + }, + template: "
{{ variant }}|{{ flag }}|{{ headline }}
", + }); + const app = createSSRApp({ + render: () => + h( + sdk.AbbyProvider, + { + initialData: { + tests: [{ name: "checkout", weights: [0, 1] }], + flags: [{ name: "newNav", value: true }], + remoteConfig: [{ name: "headline", value: "SSR" }], + }, + }, + () => h(Consumer) + ), + }); + + expect(await renderToString(app)).toContain("
|true|SSR
"); +}); diff --git a/packages/vue/tests/types.test.ts b/packages/vue/tests/types.test.ts new file mode 100644 index 00000000..956d4f11 --- /dev/null +++ b/packages/vue/tests/types.test.ts @@ -0,0 +1,51 @@ +import { expectTypeOf } from "vitest"; +import { createAbby } from "../src"; + +const sdk = createAbby({ + projectId: "types", + environments: ["development", "production"], + currentEnvironment: "development", + tests: { + checkout: { variants: ["control", "streamlined"] }, + }, + flags: ["newNav"], + remoteConfig: { + headline: "String", + maxItems: "Number", + metadata: "JSON", + }, +}); + +test("preserves names, variants, lookup values, and remote-config types", () => { + const checkTypes = () => { + const experiment = sdk.useAbby("checkout"); + const mapped = sdk.useAbby("checkout", { + control: "old", + streamlined: "new", + }); + const flag = sdk.useFeatureFlag("newNav"); + const headline = sdk.useRemoteConfig("headline"); + const maxItems = sdk.useRemoteConfig("maxItems"); + const metadata = sdk.useRemoteConfig("metadata"); + + expectTypeOf(experiment.variant.value).toEqualTypeOf< + "control" | "streamlined" + >(); + expectTypeOf(mapped.variant.value).toEqualTypeOf<"old" | "new">(); + expectTypeOf(flag.value).toEqualTypeOf(); + expectTypeOf(headline.value).toEqualTypeOf(); + expectTypeOf(maxItems.value).toEqualTypeOf(); + expectTypeOf(metadata.value).toEqualTypeOf>(); + + // @ts-expect-error unknown test + sdk.useAbby("missing"); + // @ts-expect-error unknown flag + sdk.useFeatureFlag("missing"); + // @ts-expect-error unknown remote config + sdk.useRemoteConfig("missing"); + // @ts-expect-error lookup must cover every variant + sdk.useAbby("checkout", { control: "old" }); + }; + + expectTypeOf(checkTypes).toBeFunction(); +}); diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 00000000..482642d5 --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "types": ["vitest/globals"] + }, + "include": ["src", "tests", "tsup.config.ts", "vite.config.ts"] +} diff --git a/packages/vue/tsup.config.ts b/packages/vue/tsup.config.ts new file mode 100644 index 00000000..685d4095 --- /dev/null +++ b/packages/vue/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + dts: true, + clean: true, + format: ["cjs", "esm"], + sourcemap: true, + treeshake: true, + external: ["vue"], +}); diff --git a/packages/vue/vite.config.ts b/packages/vue/vite.config.ts new file mode 100644 index 00000000..36af284e --- /dev/null +++ b/packages/vue/vite.config.ts @@ -0,0 +1,10 @@ +/// + +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69fa896e..398eb440 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)) @@ -1108,6 +1108,46 @@ importers: packages/tsconfig: {} + packages/vue: + dependencies: + '@tryabby/core': + specifier: workspace:* + version: link:../core + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + devDependencies: + '@types/js-cookie': + specifier: ^3.0.3 + version: 3.0.3 + '@vue/server-renderer': + specifier: 3.4.24 + version: 3.4.24(vue@3.4.24(typescript@5.5.4)) + '@vue/test-utils': + specifier: 2.4.6 + version: 2.4.6 + jsdom: + specifier: ^20.0.3 + version: 20.0.3 + tsconfig: + specifier: workspace:* + 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) + 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.24 + version: 3.4.24(typescript@5.5.4) + packages: '@adobe/css-tools@4.2.0': @@ -1656,11 +1696,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'} @@ -2892,7 +2927,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==} @@ -3845,7 +3880,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 +4330,9 @@ packages: resolution: {integrity: sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==} engines: {node: '>= 18'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@open-draft/until@1.0.3': resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} @@ -4474,6 +4512,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 +4710,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 +5200,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 +5252,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 +5296,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 +6690,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==} @@ -6693,6 +6743,12 @@ packages: '@vue/compiler-dom@3.4.24': resolution: {integrity: sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==} + '@vue/compiler-sfc@3.4.24': + resolution: {integrity: sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==} + + '@vue/compiler-ssr@3.4.24': + resolution: {integrity: sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==} + '@vue/language-core@1.8.27': resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} peerDependencies: @@ -6701,9 +6757,26 @@ packages: typescript: optional: true + '@vue/reactivity@3.4.24': + resolution: {integrity: sha512-nup3fSYg4i4LtNvu9slF/HF/0dkMQYfepUdORBcMSsankzRPzE7ypAFurpwyRBfU1i7Dn1kcwpYsE1wETSh91g==} + + '@vue/runtime-core@3.4.24': + resolution: {integrity: sha512-c7iMfj6cJMeAG3s5yOn9Rc5D9e2/wIuaozmGf/ICGCY3KV5H7mbTVdvEkd4ZshTq7RUZqj2k7LMJWVx+EBiY1g==} + + '@vue/runtime-dom@3.4.24': + resolution: {integrity: sha512-uXKzuh/Emfad2Y7Qm0ABsLZZV6H3mAJ5ZVqmAOlrNQRf+T5mxpPGZBfec1hkP41t6h6FwF6RSGCs/gd8WbuySQ==} + + '@vue/server-renderer@3.4.24': + resolution: {integrity: sha512-H+DLK4sQF6sRgzKyofmlEVBIV/9KrQU6HIV7nt6yIwSGGKvSwlV8pqJlebUKLpbXaNHugdSfAbP6YmXF69lxow==} + peerDependencies: + vue: 3.4.24 + '@vue/shared@3.4.24': resolution: {integrity: sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==} + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} @@ -6799,6 +6872,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 @@ -7740,10 +7814,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 +7872,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 +8355,11 @@ packages: resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==} hasBin: true + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -8812,6 +8896,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 +9073,31 @@ 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@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 +9437,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 +9518,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,6 +9972,11 @@ packages: engines: {node: '>=12'} hasBin: true + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -10162,6 +10257,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==} @@ -10978,6 +11074,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 +11093,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 +11163,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==} @@ -11793,8 +11892,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 +12297,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 @@ -12791,6 +12891,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 +13116,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 +13125,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 +13253,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 +13473,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 +13880,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 +14072,9 @@ packages: vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + vue-template-compiler@2.7.16: resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} @@ -13973,6 +14084,14 @@ packages: peerDependencies: typescript: '*' + vue@3.4.24: + resolution: {integrity: sha512-NPdx7dLGyHmKHGRRU5bMRYVE+rechR+KDU5R2tSTNG36PuMwbfAJ+amEvOAw7BPfZp5sQulNELSLm5YUkau+Sg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -14046,7 +14165,7 @@ packages: engines: {node: '>= 18.12.0'} hasBin: true peerDependencies: - webpack: 5.93.0 + webpack: ^5.0.0 webpack-cli: '*' peerDependenciesMeta: webpack: @@ -14099,6 +14218,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,7 +14828,7 @@ 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 @@ -14748,7 +14868,7 @@ 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 @@ -15328,10 +15448,6 @@ snapshots: 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 @@ -17057,13 +17173,13 @@ 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/parser': 7.25.3 '@babel/types': 7.24.7 '@babel/template@7.24.7': @@ -17086,7 +17202,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 @@ -17101,7 +17217,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,7 +17232,7 @@ 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/parser': 7.25.3 '@babel/types': 7.24.7 debug: 4.3.5 globals: 11.12.0 @@ -18691,6 +18807,8 @@ snapshots: '@octokit/request-error': 6.1.4 '@octokit/webhooks-methods': 5.1.0 + '@one-ini/wasm@0.1.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 +21433,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,7 +21959,7 @@ 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 @@ -21852,6 +21970,23 @@ snapshots: '@vue/compiler-core': 3.4.24 '@vue/shared': 3.4.24 + '@vue/compiler-sfc@3.4.24': + dependencies: + '@babel/parser': 7.25.3 + '@vue/compiler-core': 3.4.24 + '@vue/compiler-dom': 3.4.24 + '@vue/compiler-ssr': 3.4.24 + '@vue/shared': 3.4.24 + estree-walker: 2.0.2 + magic-string: 0.30.10 + postcss: 8.4.41 + source-map-js: 1.2.0 + + '@vue/compiler-ssr@3.4.24': + dependencies: + '@vue/compiler-dom': 3.4.24 + '@vue/shared': 3.4.24 + '@vue/language-core@1.8.27(typescript@5.5.4)': dependencies: '@volar/language-core': 1.11.1 @@ -21866,8 +22001,34 @@ snapshots: optionalDependencies: typescript: 5.5.4 + '@vue/reactivity@3.4.24': + dependencies: + '@vue/shared': 3.4.24 + + '@vue/runtime-core@3.4.24': + dependencies: + '@vue/reactivity': 3.4.24 + '@vue/shared': 3.4.24 + + '@vue/runtime-dom@3.4.24': + dependencies: + '@vue/runtime-core': 3.4.24 + '@vue/shared': 3.4.24 + csstype: 3.2.3 + + '@vue/server-renderer@3.4.24(vue@3.4.24(typescript@5.5.4))': + dependencies: + '@vue/compiler-ssr': 3.4.24 + '@vue/shared': 3.4.24 + vue: 3.4.24(typescript@5.5.4) + '@vue/shared@3.4.24': {} + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + '@web3-storage/multipart-parser@1.0.0': {} '@webassemblyjs/ast@1.12.1': @@ -23018,6 +23179,8 @@ snapshots: csstype@3.1.2: {} + csstype@3.2.3: {} + csv-generate@3.4.3: {} csv-parse@4.16.3: {} @@ -23508,6 +23671,13 @@ snapshots: semver: 5.7.1 sigmund: 1.0.1 + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.4 + semver: 7.6.2 + ee-first@1.1.1: {} ejs@3.1.9: @@ -25507,6 +25677,14 @@ snapshots: glob: 8.1.0 nopt: 6.0.0 + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + js-cookie@3.0.5: {} js-levenshtein@1.1.6: {} @@ -25527,7 +25705,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 +25730,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) @@ -27227,7 +27405,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 +27484,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) @@ -28197,7 +28375,7 @@ snapshots: prettier@3.0.0: {} - prettier@3.5.2: {} + prettier@3.9.10: {} pretty-format@27.5.1: dependencies: @@ -30905,6 +31083,8 @@ snapshots: vscode-textmate@8.0.0: {} + vue-component-type-helpers@2.2.12: {} + vue-template-compiler@2.7.16: dependencies: de-indent: 1.0.2 @@ -30917,6 +31097,16 @@ snapshots: semver: 7.6.0 typescript: 5.5.4 + vue@3.4.24(typescript@5.5.4): + dependencies: + '@vue/compiler-dom': 3.4.24 + '@vue/compiler-sfc': 3.4.24 + '@vue/runtime-dom': 3.4.24 + '@vue/server-renderer': 3.4.24(vue@3.4.24(typescript@5.5.4)) + '@vue/shared': 3.4.24 + optionalDependencies: + typescript: 5.5.4 + w3c-keyname@2.2.8: {} w3c-xmlserializer@4.0.0: From a55b9aa5e71a3f31fa9e4b5d53bb6ec6c6dd1d90 Mon Sep 17 00:00:00 2001 From: viurs19 Date: Fri, 6 Nov 2026 16:53:02 +0000 Subject: [PATCH 2/3] fix(core): validate persisted variants before selection --- packages/core/src/index.ts | 2 +- .../core/tests/persisted-variants.test.ts | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 packages/core/tests/persisted-variants.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e47e6657..6bb19cd0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -538,7 +538,7 @@ export class Abby< const persistedValue = this.persistantTestStorage?.get(key as string); - if (persistedValue != null) { + if (persistedValue != null && variants.includes(persistedValue)) { this.log("getTestVariant() => persistedValue:", persistedValue); return persistedValue; diff --git a/packages/core/tests/persisted-variants.test.ts b/packages/core/tests/persisted-variants.test.ts new file mode 100644 index 00000000..aa0d747c --- /dev/null +++ b/packages/core/tests/persisted-variants.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it, vi } from "vitest"; +import { Abby } from "../src/index"; + +function createClient(persistedValue: string | null, disableByDefault = false) { + const storage = { + get: vi.fn(() => persistedValue), + set: vi.fn(), + }; + const client = new Abby( + { + projectId: "persisted-variants-test", + environments: ["test"], + currentEnvironment: "test", + cookies: { disableByDefault }, + tests: { checkout: { variants: ["control", "current"] } }, + }, + storage + ); + client.init({ + flags: [], + tests: [{ name: "checkout", weights: [0, 1] }], + remoteConfig: [], + }); + return { client, storage }; +} + +describe("persisted variant selection", () => { + it("preserves a persisted variant that is still configured", () => { + const { client, storage } = createClient("control"); + expect(client.getTestVariant("checkout")).toBe("control"); + expect(storage.set).not.toHaveBeenCalled(); + }); + + it("replaces a removed persisted variant using the configured weights", () => { + const { client, storage } = createClient("removed"); + expect(client.getTestVariant("checkout")).toBe("current"); + expect(storage.set).toHaveBeenCalledWith("checkout", "current", { + expiresInDays: undefined, + }); + }); + + it("ignores a removed persisted variant without writing when cookies are disabled", () => { + const { client, storage } = createClient("removed", true); + expect(client.getTestVariant("checkout")).toBe("current"); + expect(storage.set).not.toHaveBeenCalled(); + }); + + it("selects a configured variant when no persisted value exists", () => { + const { client } = createClient(null); + expect(client.getTestVariant("checkout")).toBe("current"); + }); +}); From 06c98a6275fcbd7ef2f22da89ca2b6d426c477f5 Mon Sep 17 00:00:00 2001 From: viurs19 Date: Fri, 6 Nov 2026 17:04:39 +0000 Subject: [PATCH 3/3] docs: add JSDoc comments to satisfy CodeRabbit coverage --- packages/core/src/index.ts | 11 +++--- packages/vue/src/index.ts | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6bb19cd0..df9edefe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -517,11 +517,12 @@ export class Abby< } /** - * Function to get the value of a test variant. This includes - * the overrides from the dev tools and the local overrides if in development mode - * if there is no override or a stored variant we generate a new one and store it - * @param key The name of the test - * @returns the value of the test variant + * Selects a test variant, honoring local overrides in development mode. + * Reuses a persisted value only if it remains in the configured variants. + * Otherwise selects by weight and persists the result when cookies are enabled; + * with cookies disabled, uses the highest-weight variant or the first variant. + * @param key - The configured A/B test name. + * @returns The override, valid persisted variant, or newly selected variant. */ getTestVariant(key: T): Tests[T]["variants"][number] { this.log("getTestVariant()", key); diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 6b15a4d4..2c1970a9 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -37,15 +37,33 @@ export type ABTestReturnValue = Lookup extends undefined ? Lookup[Variant] : never; +/** + * Creates project-scoped cookie storage that is safe to access during SSR. + * @param projectId - Project identifier used to namespace cookie keys. + * @param keyFor - Builds the cookie key for a project and entry name. + * @returns Storage methods that read and write cookies only in the browser. + */ function cookieStorage( projectId: string, keyFor: (projectId: string, name: string) => string ) { return { + /** + * Reads a persisted value from its project-scoped cookie. + * @param name - Test, feature flag, or remote-config entry name. + * @returns The cookie value, or null if unavailable or rendering on the server. + */ get(name: string): string | null { if (typeof document === "undefined") return null; return Cookie.get(keyFor(projectId, name)) ?? null; }, + /** + * Persists an entry in a browser cookie; does nothing during SSR. + * @param name - Test, feature flag, or remote-config entry name. + * @param value - Serialized value to persist. + * @param options - Optional cookie expiration settings; defaults to 365 days. + * @returns Nothing. + */ set(name: string, value: string, options?: StorageServiceOptions): void { if (typeof document === "undefined") return; Cookie.set(keyFor(projectId, name), value, { @@ -55,6 +73,12 @@ function cookieStorage( }; } +/** + * Creates a typed Vue integration with a provider and project-specific composables. + * Each provider owns a separate Core client to avoid sharing mutable SSR state. + * @param abbyConfig - Project configuration, including tests, flags, and config types. + * @returns The provider, typed composables, and a composable for the Core client. + */ export function createAbby< const FlagName extends string, const TestName extends string, @@ -93,6 +117,10 @@ export function createAbby< const injectionKey: InjectionKey = Symbol("tryabby-vue"); + /** + * Creates an isolated Core client with copied mutable configuration and cookies. + * @returns A new client for one provider instance. + */ const createClient = (): Client => { // Core enriches tests with weights and can change cookie consent. Each // provider gets a fresh config so Vue SSR requests cannot leak state. @@ -138,12 +166,24 @@ export function createAbby< ); }; + /** + * Provides an isolated Abby client to descendants and loads project data on mount. + * The initialData prop optionally supplies server-loaded project data. + */ const AbbyProvider = defineComponent({ name: "AbbyProvider", props: { initialData: Object as PropType, }, emits: ["error"], + /** + * Initializes provider state, subscriptions, and client-side data loading. + * @param props - Provider props containing optional initial project data. + * @param context - Vue setup context containing the error emitter and slots. + * @param context.emit - Emits project-loading errors to the parent. + * @param context.slots - Descendant content rendered within the provider. + * @returns A render function for the default slot. + */ setup(props, { emit, slots }) { const client = createClient(); const revision = ref(0); @@ -179,6 +219,11 @@ export function createAbby< }, }); + /** + * Resolves the context belonging to this integration's nearest provider. + * @returns The provider's client and reactive lifecycle state. + * @throws When called outside the matching AbbyProvider. + */ const useContext = (): Context => { const context = inject(injectionKey, null); if (!context) { @@ -189,6 +234,13 @@ export function createAbby< return context; }; + /** + * Tracks a test variant and exposes an action callback for conversion events. + * The variant is empty until client mount and project-data initialization finish. + * @param name - Configured A/B test name. + * @param lookup - Optional mapping from every variant to a typed display value. + * @returns A computed variant or mapped value, plus the onAct event callback. + */ const useAbby = < Name extends keyof Tests, Variant extends Tests[Name]["variants"][number], @@ -208,6 +260,11 @@ export function createAbby< return context.client.getTestVariant(name) as Variant; }); + /** + * Sends an analytics event only after a nonempty variant has been selected. + * @param type - Analytics event kind, such as PING or ACT. + * @returns Nothing; the HTTP request is dispatched without awaiting completion. + */ const send = (type: AbbyEventType): void => { if (!selected.value) return; void HttpService.sendData({ @@ -238,10 +295,19 @@ export function createAbby< return { variant, + /** + * Records an action for the currently selected variant. + * @returns Nothing; no event is sent before a variant is available. + */ onAct: () => send(AbbyEventType.ACT), }; }; + /** + * Reads a feature flag reactively from the matching provider's Core client. + * @param name - Configured feature-flag name. + * @returns A computed boolean that updates when the Core client notifies listeners. + */ const useFeatureFlag = (name: FlagName): ComputedRef => { const context = useContext(); return computed(() => { @@ -250,6 +316,11 @@ export function createAbby< }); }; + /** + * Reads a remote-config value reactively while preserving its configured type. + * @param name - Configured remote-config entry name. + * @returns A computed value typed according to the entry's configured value kind. + */ const useRemoteConfig = ( name: Name ): ComputedRef> => { @@ -265,6 +336,11 @@ export function createAbby< useAbby, useFeatureFlag, useRemoteConfig, + /** + * Exposes the Core client owned by the matching provider. + * @returns The provider-scoped Abby client for imperative updates. + * @throws When called outside the matching AbbyProvider. + */ useAbbyClient: () => useContext().client, }; }