From ea4de23379a9aa1439a665d9bbaefafc049465ce Mon Sep 17 00:00:00 2001 From: bounty-agent Date: Sun, 27 Sep 2026 16:27:20 +0530 Subject: [PATCH 1/5] feat(vue): add @tryabby/vue integration (createAbby, composables, SSR support, tests) --- packages/vue/.gitignore | 1 + packages/vue/README.md | 95 +++++ packages/vue/package.json | 47 +++ packages/vue/src/StorageService.ts | 76 ++++ packages/vue/src/context.ts | 436 ++++++++++++++++++++++ packages/vue/src/helpers.ts | 72 ++++ packages/vue/src/index.ts | 6 + packages/vue/tests/helpers.test.ts | 38 ++ packages/vue/tests/mocks/handlers.ts | 42 +++ packages/vue/tests/mocks/server.ts | 5 + packages/vue/tests/setup.ts | 23 ++ packages/vue/tests/ssr.test.ts | 183 +++++++++ packages/vue/tests/types.test.ts | 154 ++++++++ packages/vue/tests/useAbby.test.ts | 534 +++++++++++++++++++++++++++ packages/vue/tsconfig.json | 10 + packages/vue/tsup.config.ts | 12 + packages/vue/vite.config.ts | 17 + pnpm-lock.yaml | 494 ++++++++++++++++++++++--- 18 files changed, 2184 insertions(+), 61 deletions(-) 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/context.ts create mode 100644 packages/vue/src/helpers.ts create mode 100644 packages/vue/src/index.ts create mode 100644 packages/vue/tests/helpers.test.ts create mode 100644 packages/vue/tests/mocks/handlers.ts create mode 100644 packages/vue/tests/mocks/server.ts create mode 100644 packages/vue/tests/setup.ts create mode 100644 packages/vue/tests/ssr.test.ts create mode 100644 packages/vue/tests/types.test.ts create mode 100644 packages/vue/tests/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/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..0e765dc5 --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,95 @@ +# `@tryabby/vue` + +Vue 3 integration for [Abby](https://docs.tryabby.com) — A/B testing, feature flags and remote config. + +Mirrors `@tryabby/react`: the same `createAbby` factory, the same type-level +quality, and the same runtime behaviour (including rendering an empty variant +on the first pass so SSR output never mismatches the server). + +## Installation + +```sh +pnpm add @tryabby/vue +``` + +`vue@^3` is a peer dependency. + +## Usage + +```ts +import { createAbby } from "@tryabby/vue"; + +export const { + AbbyProvider, + useAbby, + useFeatureFlag, + useFeatureFlags, + useRemoteConfig, + useRemoteConfigVariables, + getABTestValue, + getFeatureFlagValue, + getRemoteConfig, + getABResetFunction, + getVariants, + updateUserProperties, +} = createAbby({ + projectId: "YOUR_PROJECT_ID", + environments: ["production"], + currentEnvironment: "production", + tests: { + footer: { variants: ["old", "new"] }, + }, + flags: ["newCheckout"], + remoteConfig: { + welcomeMessage: "String", + }, +}); +``` + +Wrap your app once (preferably with `initialData` fetched on the server): + +```vue + + + +``` + +Use the composables in any descendant component (all reactive `computed` refs): + +```vue + + + +``` + +Plain (non-reactive) getters — `getABTestValue`, `getFeatureFlagValue`, +`getRemoteConfig`, `getVariants`, `getABResetFunction` — work anywhere, +including outside components. + +## API parity with `@tryabby/react` + +| React | Vue | +| --- | --- | +| `AbbyProvider` (`initialData` prop) | `AbbyProvider` (`initialData` prop) | +| `useAbby(name, lookup?)` ? `{ variant, onAct }` | `useAbby(name, lookup?)` ? `{ variant: ComputedRef, onAct }` | +| `useFeatureFlag(name)` | `useFeatureFlag(name)` ? `ComputedRef` | +| `useFeatureFlags()` | `useFeatureFlags()` ? `ComputedRef>` | +| `useRemoteConfig(name)` | `useRemoteConfig(name)` ? `ComputedRef<…>` | +| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | +| `withDevtools(factory, props)` ? hook | `withDevtools(factory, props)` ? component | diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 00000000..4227d365 --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,47 @@ +{ + "name": "@tryabby/vue", + "version": "7.0.1", + "description": "Vue 3 integration for Abby (A/B testing, feature flags and remote config)", + "main": "dist/index.js", + "files": ["dist"], + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "scripts": { + "build": "tsup src/", + "dev": "pnpm run build --watch", + "test": "vitest" + }, + "homepage": "https://docs.tryabby.com", + "keywords": ["abby", "vue", "ab-testing", "feature-flags", "remote-config"], + "author": "", + "license": "ISC", + "devDependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@tryabby/devtools": "workspace:*", + "@types/js-cookie": "^3.0.3", + "@vue/server-renderer": "^3.4.0", + "@vue/test-utils": "^2.5.0", + "js-cookie": "^3.0.1", + "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" + }, + "peerDependencies": { + "vue": "^3.0.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/context.ts b/packages/vue/src/context.ts new file mode 100644 index 00000000..4068c8ec --- /dev/null +++ b/packages/vue/src/context.ts @@ -0,0 +1,436 @@ +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 { AbbyDevtoolProps, DevtoolsFactory } from "@tryabby/devtools"; +import { + type Component, + type ComputedRef, + type InjectionKey, + type PropType, + type Ref, + computed, + defineComponent, + inject, + onMounted, + onUnmounted, + provide, + ref, + watch, +} from "vue"; +import { + FlagStorageService, + RemoteConfigStorageService, + TestStorageService, +} from "./StorageService"; + +export type withDevtoolsFunction = ( + factory: DevtoolsFactory, + 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" || config.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); + }, + } + ); + + type AbbyProjectData = ReturnType; + + const AbbyDataKey: InjectionKey> = Symbol("abby-data"); + + const useAbbyData = () => { + const data = inject(AbbyDataKey); + + if (!data) { + throw new Error( + "useAbbyData must be used within an AbbyProvider. Wrap a parent component in to fix this error." + ); + } + + return data; + }; + + // we need to return the config as a const so that the types are narrowed + const config = abbyConfig; + + 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 data = useAbbyData(); + + // always render an empty string on the first render to avoid SSR mismatches + // because the server does not know which variant to render + const selectedVariant = ref(""); + + const readSelectedVariant = () => + data.value.tests[name as unknown as TestName]?.selectedVariant; + + // listen to changes for the current variant + watch(readSelectedVariant, (newVariant) => { + // should never be undefined after mount + if (newVariant !== undefined) { + selectedVariant.value = newVariant; + } + }); + + // lazily resolve the variant on the client after mount + // (onMounted never runs during SSR, so the server keeps rendering "") + onMounted(() => { + selectedVariant.value = + abby.getProjectData().tests[name as unknown as TestName] + ?.selectedVariant ?? ""; + }); + + watch(selectedVariant, (variant) => { + if (!name || !variant) return; + + HttpService.sendData({ + url: config.apiUrl, + type: AbbyEventType.PING, + data: { + projectId: config.projectId, + selectedVariant: variant, + testName: name as string, + }, + }); + }); + + const onAct = () => { + if (!selectedVariant.value) return; + + HttpService.sendData({ + url: config.apiUrl, + type: AbbyEventType.ACT, + data: { + projectId: config.projectId, + selectedVariant: selectedVariant.value, + testName: name as string, + }, + }); + }; + + const variant = computed( + () => + (lookupObject + ? lookupObject[selectedVariant.value as TestVariant] + : // Typescript fails here. If we cast selectedVariant to TestVariant + // it still assumes that it is a string. So we cast it to any instead + (selectedVariant.value as any)) as ABTestReturnValue< + Lookup, + TestVariant + > + ); + + return { + /** + * This function can be called to indicate that something that + * uses the selected variant has been rendered. + * It will automatically send the selected variant to the server. + */ + onAct: onAct, + variant, + }; + }; + + const useFeatureFlag = (name: FlagName): ComputedRef => { + const data = useAbbyData(); + return computed(() => data.value.flags[name].value); + }; + + /** + * Returns a computed Array of all flags with their name and value + */ + const useFeatureFlags = (): ComputedRef< + Array<{ name: FlagName; value: boolean }> + > => { + const data = useAbbyData(); + return computed(() => + (Object.keys(data.value.flags) as Array).map((flagName) => ({ + name: flagName, + value: data.value.flags[flagName].value, + })) + ); + }; + + /** + * Returns a computed Array of all remote config variables with their name and value + */ + const useRemoteConfigVariables = (): ComputedRef< + Array<{ + name: RemoteConfigName; + value: RemoteConfigValueStringToType; + }> + > => { + const data = useAbbyData(); + return computed( + () => + (Object.keys(data.value.remoteConfig) as Array).map( + (configName) => ({ + name: configName, + value: data.value.remoteConfig[configName].value, + }) + ) as Array<{ + name: RemoteConfigName; + value: RemoteConfigValueStringToType; + }> + ); + }; + + const AbbyProvider = defineComponent({ + name: "AbbyProvider", + props: { + initialData: { + type: Object as PropType, + required: false, + default: undefined, + }, + }, + setup(props, { slots }) { + const initial: AbbyProjectData = props.initialData + ? abby.init(props.initialData) + : abby.getProjectData(); + // cast: ref() only wraps the root value, so no nested unwrapping + // happens at runtime; the cast keeps the LocalData typing intact + const data = ref(initial) as Ref; + + let isMounted = false; + + // load the project data if it has not been passed in + onMounted(() => { + if (props.initialData || isMounted) return; + isMounted = true; + + // seed the data with the initial data + abby.loadProjectData().then((loaded) => { + if (!loaded) return; + data.value = loaded; + }); + }); + + // subscribe to changes in the project data + const unsubscribe = abby.subscribe((newData) => { + data.value = newData as AbbyProjectData; + }); + onUnmounted(() => { + unsubscribe(); + }); + + provide(AbbyDataKey, data); + + return () => slots.default?.(); + }, + }); + + const getFeatureFlagValue = (name: FlagName) => { + return abby.getFeatureFlag(name); + }; + + const useRemoteConfig = < + T extends RemoteConfigName, + Config extends RemoteConfig[T], + >( + remoteConfigName: T + ): ComputedRef> => { + const data = useAbbyData(); + return computed( + () => + data.value.remoteConfig[remoteConfigName] + .value as RemoteConfigValueStringToType + ); + }; + + 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); + // Typescript looses its typing here, so we cast as any in favor of having + // better type inference for the user + if (lookupObject === undefined) { + return variant as any; + } + + return lookupObject[variant as TestVariant] as any; + }; + + const withDevtools: withDevtoolsFunction = (factory, props) => { + // hacky way to make sure SSR works: onMounted never runs on the server + return defineComponent({ + name: "AbbyDevtools", + setup() { + let inited = false; + let destroy: (() => void) | undefined; + + onMounted(() => { + if (inited) { + return; + } + + if ( + !props?.dangerouslyForceShow && + process.env.NODE_ENV !== "development" + ) { + return; + } + + inited = true; + + destroy = factory.create({ ...props, abby }); + }); + + onUnmounted(() => { + inited = false; + destroy?.(); + }); + + return () => null; + }, + }); + }; + + /** + * Simple helper function create a function that resets an AB test + * @param name the name of the test + * @returns A function that can be called to reset the test + */ + const getABResetFunction = (name: T) => { + return () => { + TestStorageService.remove(config.projectId, name as string); + }; + }; + + /** + * Simple helper function to get a list of all variants for a given test + * @param name the name of the test + * @returns an array of all variants + */ + const getVariants = (name: T) => { + return abby.getVariants(name); + }; + + const updateUserProperties = ( + user: Partial<{ + -readonly [K in keyof User]: Infer; + }> + ) => { + abby.updateUserProperties(user); + }; + + return { + useAbby, + AbbyProvider, + useFeatureFlag, + getFeatureFlagValue, + useRemoteConfig, + getRemoteConfig, + getABTestValue, + __abby__: abby, + withDevtools, + getABResetFunction, + getVariants, + useFeatureFlags, + useRemoteConfigVariables, + updateUserProperties, + }; +} diff --git a/packages/vue/src/helpers.ts b/packages/vue/src/helpers.ts new file mode 100644 index 00000000..af6ef456 --- /dev/null +++ b/packages/vue/src/helpers.ts @@ -0,0 +1,72 @@ +// taken from https://stackoverflow.com/questions/52489261/typescript-can-i-define-an-n-length-tuple-type +export type Tuple = N extends N + ? number extends N + ? T[] + : _TupleOf + : never; +type _TupleOf = R["length"] extends N + ? R + : _TupleOf; + +// taken from: https://stackoverflow.com/questions/18230217/javascript-generate-a-random-number-within-a-range-using-crypto-getrandomvalues +// crypto.getRandomValues() is better than Math.random() because it's cryptographically secure +// fallback to Math.random() if crypto.getRandomValues() is not available +export function getRandomDecimal() { + if (typeof window !== "undefined" && window.crypto) { + const randomBuffer = new Uint32Array(1); + + window.crypto.getRandomValues(randomBuffer); + + return randomBuffer[0] / (0xffffffff + 1); + } + return Math.random(); +} + +function weightedRand2>(spec: T): keyof T { + let i: keyof T; + let sum = 0; + const r = getRandomDecimal(); + + for (i in spec) { + sum += spec[i]; + if (r <= sum) break; + } + // biome-ignore lint/style/noNonNullAssertion:> + return i!; +} + +export function getWeightedRandomVariant< + Variants extends ReadonlyArray, +>( + variants: Variants, + weights?: Tuple +): Variants[number] { + const validatedWeights = validateWeights(variants, weights); + return weightedRand2( + variants.reduce( + (acc, variant, index) => { + acc[variant as Variants[number]] = validatedWeights[index]; + return acc; + }, + {} as Record + ) + ); +} + +const getDefaultWeights = >( + variants: Variants +) => { + return Array.from({ length: variants.length }).fill( + 1 / variants.length + ); +}; + +export function validateWeights< + Variants extends ReadonlyArray, + Weights extends Tuple, +>(variants: Variants, weights?: Weights): Weights { + const sum = weights?.reduce((acc, weight) => acc + weight, 0); + return weights != null && sum === 1 && variants.length === weights.length + ? weights + : (getDefaultWeights(variants) as Weights); +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 00000000..1990f905 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,6 @@ +export { + createAbby, + type withDevtoolsFunction, + type ABTestReturnValue, +} from "./context"; +export { type ABConfig, type AbbyConfig, defineConfig } from "@tryabby/core"; diff --git a/packages/vue/tests/helpers.test.ts b/packages/vue/tests/helpers.test.ts new file mode 100644 index 00000000..5a3becb3 --- /dev/null +++ b/packages/vue/tests/helpers.test.ts @@ -0,0 +1,38 @@ +import { getRandomDecimal, getWeightedRandomVariant } from "../src/helpers"; + +describe("getWeightedRandomVariant", () => { + it("should only return valid values", () => { + const variants = ["A", "B", "C"] as const; + + const variant = getWeightedRandomVariant(variants); + + expect(variant).toBeDefined(); + }); + + it("should work with weights", () => { + const variants = ["A", "B", "C"] as const; + + for (let i = 0; i < 100; i++) { + const variant = getWeightedRandomVariant(variants, [0, 0, 1]); + + expect(variant).toBe("C"); + } + + for (let i = 0; i < 100; i++) { + const variant = getWeightedRandomVariant(variants, [0.5, 0.5, 0]); + + expect(["A", "B"].includes(variant)).toBeTruthy(); + expect(variant).not.toBe("C"); + } + }); +}); + +describe("getRandomDecimal", () => { + it("should return a number between 0 and 1", () => { + for (let i = 0; i < 100; i++) { + const decimal = getRandomDecimal(); + expect(decimal).toBeGreaterThanOrEqual(0); + expect(decimal).toBeLessThanOrEqual(1); + } + }); +}); diff --git a/packages/vue/tests/mocks/handlers.ts b/packages/vue/tests/mocks/handlers.ts new file mode 100644 index 00000000..f2028cb7 --- /dev/null +++ b/packages/vue/tests/mocks/handlers.ts @@ -0,0 +1,42 @@ +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/tests/mocks/server.ts b/packages/vue/tests/mocks/server.ts new file mode 100644 index 00000000..9bb8a870 --- /dev/null +++ b/packages/vue/tests/mocks/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +// This configures a request mocking server with the given request handlers. +export const server = setupServer(...handlers); diff --git a/packages/vue/tests/setup.ts b/packages/vue/tests/setup.ts new file mode 100644 index 00000000..ba50b31c --- /dev/null +++ b/packages/vue/tests/setup.ts @@ -0,0 +1,23 @@ +/// @ts-ignore it does not have types +import matchers from "@testing-library/jest-dom/matchers"; +import fetch from "node-fetch"; +import { afterAll, afterEach, beforeAll, expect } from "vitest"; +import { server } from "./mocks/server"; + +/// @ts-ignore +global.fetch = fetch; + +// Establish API mocking before all tests. +beforeAll(() => server.listen()); + +// Clean up after the tests are finished. +afterAll(() => server.close()); + +// extends Vitest expect method with methods from jest-dom +expect.extend(matchers); + +afterEach(() => { + // Reset any request handlers that we may add during the tests, + // so they do not affect other tests. + server.resetHandlers(); +}); diff --git a/packages/vue/tests/ssr.test.ts b/packages/vue/tests/ssr.test.ts new file mode 100644 index 00000000..60c6b695 --- /dev/null +++ b/packages/vue/tests/ssr.test.ts @@ -0,0 +1,183 @@ +import { createSSRApp, defineComponent, h } from "vue"; +import { renderToString } from "@vue/server-renderer"; +import { createAbby } from "../src"; + +const OLD_ENV = process.env; + +beforeEach(() => { + vi.resetModules(); // Most important - it clears the cache + process.env = { ...OLD_ENV }; // Make a copy +}); + +afterAll(() => { + process.env = OLD_ENV; // Restore old environment +}); + +describe("useAbby", () => { + it("does not render a variant on the server", async () => { + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + showFooter: { + variants: ["current", "new"], + }, + }, + }); + + const ComponentWithTest = defineComponent({ + setup() { + const { variant } = useAbby("showFooter"); + return () => + h("div", [ + // @ts-expect-error this is just the types for SSR + variant.value === "" ? h("span", "SSR!") : null, + variant.value === "current" + ? h("span", { "data-testid": "current" }, "Secret") + : null, + variant.value === "new" + ? h("span", { "data-testid": "new" }, "Very Secret") + : null, + ]); + }, + }); + + const app = createSSRApp({ + render: () => + h( + AbbyProvider, + { + initialData: { + flags: [], + tests: [ + { + name: "showFooter", + weights: [0.5, 0.5], + }, + ], + remoteConfig: [], + }, + }, + { + default: () => h(ComponentWithTest), + } + ), + }); + + const serverSideHTML = await renderToString(app); + + expect(serverSideHTML).toContain("SSR!"); + expect(serverSideHTML).not.toContain("Secret"); + expect(serverSideHTML).not.toContain("Very Secret"); + }); +}); + +describe("useFeatureFlag", () => { + it("renders the correct feature flag on the server", async () => { + const { AbbyProvider, useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + 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 = createSSRApp({ + render: () => + h( + AbbyProvider, + { + initialData: { + flags: [ + { + value: true, + name: "test", + }, + { + value: false, + name: "test2", + }, + ], + tests: [], + remoteConfig: [], + }, + }, + { + default: () => h(ComponentWithFF), + } + ), + }); + + 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 { AbbyProvider, useRemoteConfig } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + remoteConfig: { remoteConfig1: "String" }, + }); + + const ComponentWithRC = defineComponent({ + setup() { + const remoteConfig1 = useRemoteConfig("remoteConfig1"); + return () => + h("div", [ + h("span", { "data-testid": "test" }, String(remoteConfig1.value)), + ]); + }, + }); + + const app = createSSRApp({ + render: () => + h( + AbbyProvider, + { + initialData: { + flags: [ + { + value: true, + name: "test", + }, + { + value: false, + name: "test2", + }, + ], + tests: [], + remoteConfig: [{ name: "remoteConfig1", value: "FooBar" }], + }, + }, + { + default: () => h(ComponentWithRC), + } + ), + }); + + const serverSideHTML = await renderToString(app); + + expect(serverSideHTML).toContain("FooBar"); + }); +}); diff --git a/packages/vue/tests/types.test.ts b/packages/vue/tests/types.test.ts new file mode 100644 index 00000000..adb54e9f --- /dev/null +++ b/packages/vue/tests/types.test.ts @@ -0,0 +1,154 @@ +import { mount } from "@vue/test-utils"; +import { type Component, defineComponent, h, nextTick } from "vue"; +import { createAbby } from "../src"; + +const OLD_ENV = process.env; + +beforeEach(() => { + vi.resetModules(); // Most important - it clears the cache + process.env = { ...OLD_ENV }; // Make a copy +}); + +afterAll(() => { + process.env = OLD_ENV; // Restore old environment +}); + +function mountComposable(AbbyProvider: Component, useFn: () => T) { + let result!: T; + const Probe = defineComponent({ + setup() { + result = useFn(); + return () => h("div"); + }, + }); + const wrapper = mount( + defineComponent({ + render() { + return h(AbbyProvider as any, null, { + default: () => h(Probe), + }); + }, + }) + ); + return { wrapper, result: () => result }; +} + +describe("useAbby", () => { + it("returns the correct types", async () => { + const test2Variants = [ + "SimonsText", + "MatthiasText", + "TomsText", + "TimsText", + ] as const; + + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants: ["ONLY_ONE_VARIANT"] }, + test2: { + variants: test2Variants, + }, + }, + }); + + const { result: test1Result } = mountComposable(AbbyProvider, () => + useAbby("test") + ); + await nextTick(); + + assertType<"ONLY_ONE_VARIANT">(test1Result().variant.value); + + expect(test1Result().variant.value).toBeDefined(); + + const { result: test2Result } = mountComposable(AbbyProvider, () => + useAbby("test2") + ); + await nextTick(); + + assertType<(typeof test2Variants)[number]>(test2Result().variant.value); + + expect(test2Result().variant.value).toBeDefined(); + }); +}); + +describe("useFeatureFlag", () => { + it("returns the correct types", async () => { + const { AbbyProvider, useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + flags: ["test"], + }); + + const { result: testFlagResult } = mountComposable(AbbyProvider, () => + useFeatureFlag("test") + ); + await nextTick(); + + expectTypeOf(testFlagResult().value).toEqualTypeOf(); + expectTypeOf(useFeatureFlag).parameter(0).toEqualTypeOf<"test">(); + }); + + describe("getVariants", () => { + it("has the correct types", () => { + const { getVariants } = createAbby({ + environments: [], + projectId: "123", + currentEnvironment: "test", + tests: { + test: { + variants: ["ONLY_ONE_VARIANT"], + }, + }, + flags: ["test"], + settings: { + flags: { + devOverrides: { + test: true, + }, + }, + }, + }); + expectTypeOf(getVariants("test")).toEqualTypeOf< + readonly ["ONLY_ONE_VARIANT"] + >(); + }); + }); +}); + +describe("useRemoteConfig", () => { + it("uses correct typings", async () => { + const { AbbyProvider, useRemoteConfig } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + remoteConfig: { + stringRc: "String", + numberRc: "Number", + jsonRc: "JSON", + }, + }); + + expectTypeOf(useRemoteConfig) + .parameter(0) + .toEqualTypeOf<"stringRc" | "numberRc" | "jsonRc">(); + + const { result: stringRcResult } = mountComposable(AbbyProvider, () => + useRemoteConfig("stringRc") + ); + const { result: numberRcResult } = mountComposable(AbbyProvider, () => + useRemoteConfig("numberRc") + ); + const { result: jsonRcResult } = mountComposable(AbbyProvider, () => + useRemoteConfig("jsonRc") + ); + await nextTick(); + + expectTypeOf(stringRcResult().value).toEqualTypeOf(); + expectTypeOf(numberRcResult().value).toEqualTypeOf(); + expectTypeOf(jsonRcResult().value).toEqualTypeOf>(); + }); +}); diff --git a/packages/vue/tests/useAbby.test.ts b/packages/vue/tests/useAbby.test.ts new file mode 100644 index 00000000..d778c355 --- /dev/null +++ b/packages/vue/tests/useAbby.test.ts @@ -0,0 +1,534 @@ +import { flushPromises, mount } from "@vue/test-utils"; +import { AbbyEventType, HttpService } from "@tryabby/core"; +import { type Component, defineComponent, h, nextTick } from "vue"; +import { createAbby } from "../src"; +import { TestStorageService } from "../src/StorageService"; + +const OLD_ENV = process.env; + +function clearCookies() { + for (const cookie of document.cookie.split(";")) { + const name = cookie.split("=")[0]?.trim(); + if (name) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; + } + } +} + +beforeEach(() => { + clearCookies(); + vi.resetModules(); // Most important - it clears the cache + process.env = { ...OLD_ENV }; // Make a copy +}); + +afterAll(() => { + process.env = OLD_ENV; // Restore old environment +}); + +/** + * Mounts a probe component that calls `useFn` inside `setup()` + * as a descendant of the given AbbyProvider. + */ +function mountComposable(AbbyProvider: Component, useFn: () => T) { + let result!: T; + const Probe = defineComponent({ + setup() { + result = useFn(); + return () => h("div"); + }, + }); + const wrapper = mount( + defineComponent({ + render() { + return h(AbbyProvider as any, null, { + default: () => h(Probe), + }); + }, + }) + ); + return { wrapper, result: () => result }; +} + +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"], + }, + }, + }); + + const { result } = mountComposable(AbbyProvider, () => useAbby("test")); + await nextTick(); + + expect(spy).toHaveBeenCalled(); + expect(result().variant.value).toBeDefined(); + expect(result().onAct).toBeDefined(); + }); + + 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 }, + }, + }); + + const { result } = mountComposable(AbbyProvider, () => useAbby("test")); + await nextTick(); + + // the stored value should not be overwritten + expect(setSpy).not.toHaveBeenCalled(); + + // value set in localstorage + 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 }, + }, + }); + + const { result } = mountComposable(AbbyProvider, () => + useAbby("test", { + SimonsText: "a", + MatthiasText: "b", + TomsText: "c", + TimsText: "d", + }) + ); + await nextTick(); + + // value set in localstorage + 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"] }, + }, + }); + + mountComposable(AbbyProvider, () => useAbby("test")); + await nextTick(); + + 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"] }, + }, + }); + + const { result } = mountComposable(AbbyProvider, () => useAbby("test")); + // let the mount PING flush first + await nextTick(); + + result().onAct(); + await nextTick(); + + // first call is tested above + 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"], + }); + + const { result: flag1 } = mountComposable(AbbyProvider, () => + useFeatureFlag("flag1") + ); + + // wait for the flag to be fetched + await vi.waitFor(() => expect(flag1().value).toEqual(true)); + + const { result: flag2 } = mountComposable(AbbyProvider, () => + useFeatureFlag("flag2") + ); + await flushPromises(); + + // wait for the flag to be fetched + await vi.waitFor(() => expect(flag2().value).toEqual(false)); + }); + + it("should respect the default values for feature flags", async () => { + const { AbbyProvider, useFeatureFlag } = createAbby({ + environments: [], + projectId: "123", + flags: ["flag1", "flag2"], + currentEnvironment: "a", + }); + + const { result: flag1 } = mountComposable(AbbyProvider, () => + useFeatureFlag("flag1") + ); + // the flag has not been fetched yet on first render, + // so the default value is returned (mirrors the React test, + // which asserts synchronously) + expect(flag1().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, + }, + }, + }, + }); + + const { result: flag1 } = mountComposable(AbbyProvider, () => + useFeatureFlag("flag1") + ); + await flushPromises(); + + expect(flag1().value).toEqual(false); + + const { result: flag2 } = mountComposable(AbbyProvider, () => + useFeatureFlag("flag2") + ); + await flushPromises(); + + expect(flag2().value).toEqual(true); + }); + + it("gets the stored feature flag value using a function properly", async () => { + const { AbbyProvider, getFeatureFlagValue } = createAbby({ + environments: [], + projectId: "123", + flags: ["flag1", "flag2"], + currentEnvironment: "a", + }); + + // mount the provider so the project data is fetched from the server + mountComposable(AbbyProvider, () => null); + + // await server fetch + await vi.waitFor(() => 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("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">(); + }); + + it("produces proper types with a lookup object in the composable", async () => { + const { AbbyProvider, useAbby } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { + variants: ["A", "B", "C"], + }, + }, + }); + + const { result } = mountComposable(AbbyProvider, () => + useAbby("test", { + A: "Hello", + B: "Bonjour", + C: "Hola", + }) + ); + await nextTick(); + + expectTypeOf(result().variant.value).toEqualTypeOf< + "Hello" | "Bonjour" | "Hola" + >(); + }); + + it("returns correct remoteConfigValue", async () => { + const { AbbyProvider, useRemoteConfig } = createAbby({ + environments: [], + projectId: "123", + remoteConfig: { + remoteConfig1: "String", + }, + currentEnvironment: "a", + }); + + const { result } = mountComposable(AbbyProvider, () => + useRemoteConfig("remoteConfig1") + ); + + // await server fetch + await vi.waitFor(() => expect(result().value).toEqual("FooBar")); + }); + + it("uses defaultValues when remoteConfig is not set", async () => { + const { AbbyProvider, useRemoteConfig } = createAbby({ + environments: [], + projectId: "123", + remoteConfig: { + unsetRemoteConfig: "String", + }, + settings: { + remoteConfig: { + defaultValues: { + String: "defaultValue", + }, + }, + }, + currentEnvironment: "a", + }); + + const { result } = mountComposable(AbbyProvider, () => + useRemoteConfig("unsetRemoteConfig") + ); + + // await server fetch + await vi.waitFor(() => expect(result().value).toEqual("defaultValue")); + }); + + it("uses devOverride for remoteConfig", async () => { + process.env.NODE_ENV = "development"; + const { AbbyProvider, useRemoteConfig } = createAbby({ + environments: [], + projectId: "123", + remoteConfig: { + remoteConfig1: "String", + }, + settings: { + remoteConfig: { + devOverrides: { + remoteConfig1: "overwrittenValue", + }, + }, + }, + currentEnvironment: "a", + }); + + const { result } = mountComposable(AbbyProvider, () => + useRemoteConfig("remoteConfig1") + ); + + // await server fetch + await vi.waitFor(() => expect(result().value).toEqual("overwrittenValue")); + }); +}); + +it("has the correct types", async () => { + const { AbbyProvider, useAbby, useFeatureFlag } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "123", + tests: { + test: { variants: ["OldFooter", "NewFooter"] }, + test2: { + variants: ["SimonsText", "MatthiasText", "TomsText", "TimsText"], + }, + }, + flags: ["flag1"], + }); + + expectTypeOf(useAbby).parameter(0).toEqualTypeOf<"test" | "test2">(); + + const { result } = mountComposable(AbbyProvider, () => useAbby("test")); + await nextTick(); + + expectTypeOf(result().variant.value).toEqualTypeOf< + "OldFooter" | "NewFooter" + >(); + + expectTypeOf(useFeatureFlag).parameters.toEqualTypeOf<["flag1"]>(); + + const { result: ffResult } = mountComposable(AbbyProvider, () => + useFeatureFlag("flag1") + ); + await nextTick(); + + expectTypeOf(ffResult().value).toEqualTypeOf(); +}); + +describe("useFeatureFlags()", () => { + it("returns the correct list of feature flags", async () => { + const { AbbyProvider, useFeatureFlags } = createAbby({ + environments: [], + currentEnvironment: "test", + projectId: "123", + tests: { + test: { variants: ["OldFooter", "NewFooter"] }, + test2: { + variants: ["SimonsText", "MatthiasText", "TomsText", "TimsText"], + }, + }, + flags: ["flag1"], + }); + + expectTypeOf(useFeatureFlags).parameter(0).toEqualTypeOf(); + + const { result } = mountComposable(AbbyProvider, () => useFeatureFlags()); + await nextTick(); + + expectTypeOf(result().value).toEqualTypeOf< + Array<{ + name: "flag1"; + value: boolean; + }> + >(); + + expect(result().value).toHaveLength(1); + // wait for the fetch + render to go through + await vi.waitFor(() => + expect(result().value.at(0)).toEqual({ + name: "flag1", + value: true, + }) + ); + }); +}); + +describe("useRemoteConfigVariables()", () => { + it("returns the correct list of remote config variables", async () => { + const { AbbyProvider, useRemoteConfigVariables } = createAbby({ + environments: [], + currentEnvironment: "test", + projectId: "123", + remoteConfig: { + remoteConfig1: "String", + }, + }); + + expectTypeOf(useRemoteConfigVariables) + .parameter(0) + .toEqualTypeOf(); + + const { result } = mountComposable(AbbyProvider, () => + useRemoteConfigVariables() + ); + await nextTick(); + + expectTypeOf(result().value).toEqualTypeOf< + Array<{ + name: "remoteConfig1"; + value: string; + }> + >(); + + expect(result().value).toHaveLength(1); + // wait for the fetch + render to go through + await vi.waitFor(() => + expect(result().value.at(0)).toEqual({ + name: "remoteConfig1", + value: expect.any(String), + }) + ); + }); +}); diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 00000000..38b90efb --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "bundler", + "target": "es2017", + "types": ["vitest/globals"] + } +} diff --git a/packages/vue/tsup.config.ts b/packages/vue/tsup.config.ts new file mode 100644 index 00000000..e6fa8e2b --- /dev/null +++ b/packages/vue/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + dts: true, + // we want to bundle the shared package because it is not published to npm + // it is hacky :) + noExternal: ["shared/src/types"], + 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..a030f9a6 --- /dev/null +++ b/packages/vue/vite.config.ts @@ -0,0 +1,17 @@ +/// + +import { defineConfig } from "vite"; + +export default defineConfig({ + resolve: { + // use the full build so @vue/test-utils can compile `template` strings + alias: { + vue: "vue/dist/vue.esm-bundler.js", + }, + }, + test: { + globals: true, + environment: "jsdom", + setupFiles: "./tests/setup.ts", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69fa896e..e5899115 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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.28)(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.28)(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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(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.28)(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.28)(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.28)(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.28)(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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(sass@1.77.8)(svelte@3.59.1) tslib: specifier: ^2.5.0 version: 2.5.3 @@ -1108,6 +1108,58 @@ importers: packages/tsconfig: {} + packages/vue: + dependencies: + '@tryabby/core': + specifier: workspace:* + version: link:../core + js-cookie: + specifier: ^3.0.1 + version: 3.0.5 + devDependencies: + '@testing-library/jest-dom': + specifier: ^5.16.5 + version: 5.16.5 + '@tryabby/devtools': + specifier: workspace:* + version: link:../devtools + '@types/js-cookie': + specifier: ^3.0.3 + version: 3.0.3 + '@vue/server-renderer': + specifier: ^3.4.0 + version: 3.5.43 + '@vue/test-utils': + specifier: ^2.5.0 + version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.43(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.28)(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.43(typescript@5.5.4) + packages: '@adobe/css-tools@4.2.0': @@ -1582,6 +1634,10 @@ packages: resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + 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 +1646,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'} @@ -1671,6 +1731,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + 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'} @@ -2673,6 +2738,10 @@ packages: resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2892,7 +2961,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 +3819,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 +3917,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 +4367,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==} @@ -4671,8 +4746,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 +5236,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 @@ -6690,9 +6765,21 @@ packages: '@vue/compiler-core@3.4.24': resolution: {integrity: sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==} + '@vue/compiler-core@3.5.43': + resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==} + '@vue/compiler-dom@3.4.24': resolution: {integrity: sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==} + '@vue/compiler-dom@3.5.43': + resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==} + + '@vue/compiler-sfc@3.5.43': + resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==} + + '@vue/compiler-ssr@3.5.43': + resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==} + '@vue/language-core@1.8.27': resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} peerDependencies: @@ -6701,9 +6788,34 @@ packages: typescript: optional: true + '@vue/reactivity@3.5.43': + resolution: {integrity: sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==} + + '@vue/runtime-core@3.5.43': + resolution: {integrity: sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==} + + '@vue/runtime-dom@3.5.43': + resolution: {integrity: sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==} + + '@vue/server-renderer@3.5.43': + resolution: {integrity: sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==} + '@vue/shared@3.4.24': resolution: {integrity: sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==} + '@vue/shared@3.5.43': + resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==} + + '@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 +6898,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'} @@ -7141,6 +7257,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 +7326,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 +7700,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==} @@ -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'} @@ -8994,6 +9134,10 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} deprecated: Glob versions prior to v9 are no longer supported @@ -9882,10 +10026,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'} @@ -10220,6 +10372,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 +10417,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 +10872,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 +10937,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 +11051,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + 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} @@ -11131,6 +11303,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 +11693,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 +11750,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 +11919,10 @@ packages: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -12587,6 +12775,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -12771,6 +12964,10 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-loader@5.0.0: resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} engines: {node: '>= 18.12.0'} @@ -13964,6 +14161,9 @@ packages: vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + vue-component-type-helpers@3.3.11: + resolution: {integrity: sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==} + vue-template-compiler@2.7.16: resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} @@ -13973,6 +14173,14 @@ packages: peerDependencies: typescript: '*' + vue@3.5.43: + resolution: {integrity: sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -14046,7 +14254,7 @@ packages: engines: {node: '>= 18.12.0'} hasBin: true peerDependencies: - webpack: 5.93.0 + webpack: ^5.0.0 webpack-cli: '*' peerDependenciesMeta: webpack: @@ -14411,7 +14619,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1801.3(chokidar@3.6.0) - '@angular-devkit/build-webpack': 0.1801.3(chokidar@3.6.0)(webpack-dev-server@5.0.4(webpack@5.92.1))(webpack@5.92.1(esbuild@0.21.5)) + '@angular-devkit/build-webpack': 0.1801.3(chokidar@3.6.0)(webpack-dev-server@5.0.4(webpack@5.92.1))(webpack@5.92.1) '@angular-devkit/core': 18.1.3(chokidar@3.6.0) '@angular/build': 18.1.3(@angular/compiler-cli@18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4))(@types/node@20.3.1)(chokidar@3.6.0)(less@4.2.0)(postcss@8.4.38)(tailwindcss@3.3.2(ts-node@10.9.1(@types/node@20.3.1)(typescript@5.5.4)))(terser@5.29.2)(typescript@5.5.4) '@angular/compiler-cli': 18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4) @@ -14425,15 +14633,15 @@ snapshots: '@babel/preset-env': 7.24.7(@babel/core@7.24.7) '@babel/runtime': 7.24.7 '@discoveryjs/json-ext': 0.5.7 - '@ngtools/webpack': 18.1.3(@angular/compiler-cli@18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4))(typescript@5.5.4)(webpack@5.92.1(esbuild@0.21.5)) + '@ngtools/webpack': 18.1.3(@angular/compiler-cli@18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4))(typescript@5.5.4)(webpack@5.92.1) '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.3.2(@types/node@20.3.1)(less@4.2.0)(sass@1.77.6)(terser@5.29.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.19(postcss@8.4.38) - babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1(esbuild@0.21.5)) + babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1) browserslist: 4.23.1 - copy-webpack-plugin: 12.0.2(webpack@5.92.1(esbuild@0.21.5)) + copy-webpack-plugin: 12.0.2(webpack@5.92.1) critters: 0.0.24 - css-loader: 7.1.2(webpack@5.92.1(esbuild@0.21.5)) + css-loader: 7.1.2(webpack@5.92.1) esbuild-wasm: 0.21.5 fast-glob: 3.3.2 http-proxy-middleware: 3.0.0 @@ -14442,11 +14650,11 @@ snapshots: jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 less: 4.2.0 - less-loader: 12.2.0(less@4.2.0)(webpack@5.92.1(esbuild@0.21.5)) - license-webpack-plugin: 4.0.2(webpack@5.92.1(esbuild@0.21.5)) + less-loader: 12.2.0(less@4.2.0)(webpack@5.92.1) + license-webpack-plugin: 4.0.2(webpack@5.92.1) loader-utils: 3.3.1 magic-string: 0.30.10 - mini-css-extract-plugin: 2.9.0(webpack@5.92.1(esbuild@0.21.5)) + mini-css-extract-plugin: 2.9.0(webpack@5.92.1) mrmime: 2.0.0 open: 10.1.0 ora: 5.4.1 @@ -14454,13 +14662,13 @@ snapshots: picomatch: 4.0.2 piscina: 4.6.1 postcss: 8.4.38 - postcss-loader: 8.1.1(postcss@8.4.38)(typescript@5.5.4)(webpack@5.92.1(esbuild@0.21.5)) + postcss-loader: 8.1.1(postcss@8.4.38)(typescript@5.5.4)(webpack@5.92.1) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.77.6 - sass-loader: 14.2.1(sass@1.77.6)(webpack@5.92.1(esbuild@0.21.5)) + sass-loader: 14.2.1(sass@1.77.6)(webpack@5.92.1) semver: 7.6.2 - source-map-loader: 5.0.0(webpack@5.92.1(esbuild@0.21.5)) + source-map-loader: 5.0.0(webpack@5.92.1) source-map-support: 0.5.21 terser: 5.29.2 tree-kill: 1.2.2 @@ -14473,7 +14681,7 @@ snapshots: webpack-dev-middleware: 7.2.1(webpack@5.92.1) webpack-dev-server: 5.0.4(webpack@5.92.1) webpack-merge: 5.10.0 - webpack-subresource-integrity: 5.1.0(webpack@5.92.1(esbuild@0.21.5)) + webpack-subresource-integrity: 5.1.0(webpack@5.92.1) optionalDependencies: esbuild: 0.21.5 karma: 6.4.0 @@ -14497,7 +14705,7 @@ snapshots: - utf-8-validate - webpack-cli - '@angular-devkit/build-webpack@0.1801.3(chokidar@3.6.0)(webpack-dev-server@5.0.4(webpack@5.92.1))(webpack@5.92.1(esbuild@0.21.5))': + '@angular-devkit/build-webpack@0.1801.3(chokidar@3.6.0)(webpack-dev-server@5.0.4(webpack@5.92.1))(webpack@5.92.1)': dependencies: '@angular-devkit/architect': 0.1801.3(chokidar@3.6.0) rxjs: 7.8.1 @@ -15239,10 +15447,14 @@ snapshots: '@babel/helper-string-parser@7.24.8': {} + '@babel/helper-string-parser@7.29.7': {} + '@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': {} @@ -15340,6 +15552,10 @@ snapshots: dependencies: '@babel/types': 7.25.2 + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -17180,6 +17396,11 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} '@biomejs/biome@1.9.4': @@ -18088,6 +18309,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 @@ -18462,7 +18685,7 @@ snapshots: '@next/swc-win32-x64-msvc@14.1.1': optional: true - '@ngtools/webpack@18.1.3(@angular/compiler-cli@18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4))(typescript@5.5.4)(webpack@5.92.1(esbuild@0.21.5))': + '@ngtools/webpack@18.1.3(@angular/compiler-cli@18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4))(typescript@5.5.4)(webpack@5.92.1)': dependencies: '@angular/compiler-cli': 18.1.3(@angular/compiler@18.1.3(@angular/core@18.1.3(rxjs@7.8.1)(zone.js@0.12.0)))(typescript@5.5.4) typescript: 5.5.4 @@ -18691,6 +18914,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)': @@ -21847,11 +22072,41 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.0 + '@vue/compiler-core@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.43 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.4.24': dependencies: '@vue/compiler-core': 3.4.24 '@vue/shared': 3.4.24 + '@vue/compiler-dom@3.5.43': + dependencies: + '@vue/compiler-core': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/compiler-sfc@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.43 + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-ssr': 3.5.43 + '@vue/shared': 3.5.43 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.43': + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + '@vue/language-core@1.8.27(typescript@5.5.4)': dependencies: '@volar/language-core': 1.11.1 @@ -21866,8 +22121,41 @@ snapshots: optionalDependencies: typescript: 5.5.4 + '@vue/reactivity@3.5.43': + dependencies: + '@vue/shared': 3.5.43 + + '@vue/runtime-core@3.5.43': + dependencies: + '@vue/reactivity': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/runtime-dom@3.5.43': + dependencies: + '@vue/reactivity': 3.5.43 + '@vue/runtime-core': 3.5.43 + '@vue/shared': 3.5.43 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.43': + dependencies: + '@vue/compiler-ssr': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/shared': 3.5.43 + '@vue/shared@3.4.24': {} + '@vue/shared@3.5.43': {} + + '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.43(typescript@5.5.4))': + dependencies: + '@vue/compiler-dom': 3.5.43 + js-beautify: 2.0.3 + vue: 3.5.43(typescript@5.5.4) + vue-component-type-helpers: 3.3.11 + optionalDependencies: + '@vue/server-renderer': 3.5.43 + '@web3-storage/multipart-parser@1.0.0': {} '@webassemblyjs/ast@1.12.1': @@ -21968,6 +22256,8 @@ snapshots: abbrev@2.0.0: {} + abbrev@5.0.0: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -22234,7 +22524,7 @@ snapshots: dependencies: '@babel/core': 7.24.4 - babel-loader@9.1.3(@babel/core@7.24.7)(webpack@5.92.1(esbuild@0.21.5)): + babel-loader@9.1.3(@babel/core@7.24.7)(webpack@5.92.1): dependencies: '@babel/core': 7.24.7 find-cache-dir: 4.0.0 @@ -22327,6 +22617,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} base64id@2.0.0: {} @@ -22407,6 +22699,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 +23094,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -22907,7 +23205,7 @@ snapshots: dependencies: is-what: 4.1.15 - copy-webpack-plugin@12.0.2(webpack@5.92.1(esbuild@0.21.5)): + copy-webpack-plugin@12.0.2(webpack@5.92.1): dependencies: fast-glob: 3.3.2 glob-parent: 6.0.2 @@ -22981,7 +23279,7 @@ snapshots: crypto-random-string@2.0.0: {} - css-loader@7.1.2(webpack@5.92.1(esbuild@0.21.5)): + css-loader@7.1.2(webpack@5.92.1): dependencies: icss-utils: 5.1.0(postcss@8.4.38) postcss: 8.4.38 @@ -23018,6 +23316,8 @@ snapshots: csstype@3.1.2: {} + csstype@3.2.3: {} + csv-generate@3.4.3: {} csv-parse@4.16.3: {} @@ -23508,6 +23808,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.5 + ee-first@1.1.1: {} ejs@3.1.9: @@ -23575,6 +23882,8 @@ snapshots: entities@4.5.0: {} + entities@7.0.1: {} + env-paths@2.2.1: {} envinfo@7.8.1: {} @@ -24460,6 +24769,12 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 + glob@13.0.6: + 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 +25822,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.6 + 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: {} @@ -25781,7 +26106,7 @@ snapshots: leac@0.6.0: {} - less-loader@12.2.0(less@4.2.0)(webpack@5.92.1(esbuild@0.21.5)): + less-loader@12.2.0(less@4.2.0)(webpack@5.92.1): dependencies: less: 4.2.0 optionalDependencies: @@ -25813,7 +26138,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.92.1(esbuild@0.21.5)): + license-webpack-plugin@4.0.2(webpack@5.92.1): dependencies: webpack-sources: 3.2.3 optionalDependencies: @@ -25961,6 +26286,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.3: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -26000,6 +26327,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 @@ -26919,7 +27250,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.0(webpack@5.92.1(esbuild@0.21.5)): + mini-css-extract-plugin@2.9.0(webpack@5.92.1): dependencies: schema-utils: 4.1.0 tapable: 2.2.1 @@ -26948,6 +27279,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 +27345,8 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 @@ -27150,6 +27487,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.19: {} + nanoid@3.3.6: {} nanoid@3.3.7: {} @@ -27475,6 +27814,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 +28265,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 +28316,8 @@ snapshots: picocolors@1.0.1: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} picomatch@4.0.2: {} @@ -28033,20 +28383,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.28)(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.28 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.28)(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.28 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,16 +28416,16 @@ 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.28)(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.28 ts-node: 10.9.1(@types/node@22.1.0)(typescript@5.5.4) optional: true - postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.5.4)(webpack@5.92.1(esbuild@0.21.5)): + postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.5.4)(webpack@5.92.1): dependencies: cosmiconfig: 9.0.0(typescript@5.5.4) jiti: 1.21.6 @@ -28156,6 +28506,12 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-bytea@1.0.0: {} @@ -29018,7 +29374,7 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.7.1 - sass-loader@14.2.1(sass@1.77.6)(webpack@5.92.1(esbuild@0.21.5)): + sass-loader@14.2.1(sass@1.77.6)(webpack@5.92.1): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -29118,6 +29474,8 @@ snapshots: semver@7.6.2: {} + semver@7.8.5: {} + send@0.18.0: dependencies: debug: 2.6.9 @@ -29393,7 +29751,9 @@ snapshots: source-map-js@1.2.0: {} - source-map-loader@5.0.0(webpack@5.92.1(esbuild@0.21.5)): + source-map-js@1.2.1: {} + + source-map-loader@5.0.0(webpack@5.92.1): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.0 @@ -29689,7 +30049,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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(sass@1.77.8)(svelte@3.59.1): dependencies: '@jridgewell/trace-mapping': 0.3.18 chokidar: 3.5.3 @@ -29698,7 +30058,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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - '@babel/core' @@ -29712,7 +30072,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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(sass@1.77.8)(svelte@3.59.1): dependencies: '@jridgewell/trace-mapping': 0.3.18 chokidar: 3.5.3 @@ -29721,7 +30081,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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(sass@1.77.8)(svelte@3.59.1)(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - '@babel/core' @@ -29739,7 +30099,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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(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 +30111,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.28 + postcss-load-config: 4.0.1(postcss@8.5.28)(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.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)))(postcss@8.5.28)(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 +30128,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.28 + postcss-load-config: 4.0.1(postcss@8.5.28)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4)) sass: 1.77.8 typescript: 5.5.4 @@ -30151,7 +30511,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.28)(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 +30521,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.28)(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.28 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.28)(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 +30544,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.28)(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.28 typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -30905,6 +31265,8 @@ snapshots: vscode-textmate@8.0.0: {} + vue-component-type-helpers@3.3.11: {} + vue-template-compiler@2.7.16: dependencies: de-indent: 1.0.2 @@ -30917,6 +31279,16 @@ snapshots: semver: 7.6.0 typescript: 5.5.4 + vue@3.5.43(typescript@5.5.4): + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-sfc': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/server-renderer': 3.5.43 + '@vue/shared': 3.5.43 + optionalDependencies: + typescript: 5.5.4 + w3c-keyname@2.2.8: {} w3c-xmlserializer@4.0.0: @@ -31041,7 +31413,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack-subresource-integrity@5.1.0(webpack@5.92.1(esbuild@0.21.5)): + webpack-subresource-integrity@5.1.0(webpack@5.92.1): dependencies: typed-assert: 1.0.9 webpack: 5.92.1(esbuild@0.21.5) From 01d7c4d0e1ba3ee68ee2f668cfb2796afb6515eb Mon Sep 17 00:00:00 2001 From: bounty-agent Date: Sun, 27 Sep 2026 20:43:12 +0530 Subject: [PATCH 2/5] demo(vue): add plain-Node CJS SSR demo + rendered output (issue #68) --- packages/vue/demo/index.html | 60 ++++++++++++++++ packages/vue/demo/ssr-demo.cjs | 109 ++++++++++++++++++++++++++++++ packages/vue/demo/ssr-output.html | 12 ++++ 3 files changed, 181 insertions(+) create mode 100644 packages/vue/demo/index.html create mode 100644 packages/vue/demo/ssr-demo.cjs create mode 100644 packages/vue/demo/ssr-output.html diff --git a/packages/vue/demo/index.html b/packages/vue/demo/index.html new file mode 100644 index 00000000..d2b1dc05 --- /dev/null +++ b/packages/vue/demo/index.html @@ -0,0 +1,60 @@ + + + + + @tryabby/vue demo (issue #68) + + + + +

@tryabby/vue demo

+
loading…
+ + + diff --git a/packages/vue/demo/ssr-demo.cjs b/packages/vue/demo/ssr-demo.cjs new file mode 100644 index 00000000..6c8ceecb --- /dev/null +++ b/packages/vue/demo/ssr-demo.cjs @@ -0,0 +1,109 @@ +// packages/vue/demo/ssr-demo.cjs — plain Node CJS SSR demo for @tryabby/vue (issue #68). +// No TypeScript, no browser, no ESM import maps. Run from abby repo root: +// node packages/vue/demo/ssr-demo.cjs +// Requires the BUILT packages/vue/dist/index.js (CJS) + vue CJS + @vue/server-renderer. +const path = require("path"); +const fs = require("fs"); + +// 1) Vue CJS: require('vue') resolves to vue/dist/vue.cjs.js via its package.json. +// Fallback to the explicit path inside packages/vue/node_modules. +let vue; +try { + vue = require("vue"); +} catch (e) { + vue = require( + path.join(__dirname, "..", "node_modules", "vue", "dist", "vue.cjs.js") + ); +} +const { createSSRApp, h } = vue; + +// 2) Server renderer (CJS) +let serverRenderer; +try { + serverRenderer = require("@vue/server-renderer"); +} catch (e) { + serverRenderer = require( + path.join( + __dirname, + "..", + "node_modules", + "@vue", + "server-renderer", + "dist", + "server-renderer.cjs.js" + ) + ); +} +const { renderToString } = serverRenderer; + +// 3) Built @tryabby/vue CJS bundle +const { createAbby } = require("../dist/index.js"); + +// 4) Same demo config as the browser demo (demo/index.html) +const { AbbyProvider, useAbby, useFeatureFlag, useRemoteConfig } = createAbby({ + environments: [""], + currentEnvironment: "", + projectId: "demo", + tests: { footer: { variants: ["OldFooter", "NewFooter"] } }, + flags: ["newHeader"], + remoteConfig: { welcomeText: "String" }, +}); + +// Deterministic: weights [0,1] always select NewFooter on the client. +// NOTE: useAbby renders "" on the server by design (SSR mismatch guard; +// onMounted never runs during SSR). Flags + remoteConfig DO render on SSR. +const initialData = { + tests: [{ name: "footer", weights: [0, 1] }], + flags: [{ name: "newHeader", value: true }], + remoteConfig: [{ name: "welcomeText", value: "Hello from Abby Vue!" }], +}; + +// Child component with h() only (NO templates) +const Demo = { + setup() { + const { variant, onAct } = useAbby("footer"); + const header = useFeatureFlag("newHeader"); + const welcome = useRemoteConfig("welcomeText"); + void onAct; + return () => + h("div", [ + h("div", { class: "card" }, [ + 'useAbby("footer") -> variant: ', + h("b", { id: "variant" }, String(variant.value)), + ]), + h("div", { class: "card" }, [ + 'useFeatureFlag("newHeader") -> ', + h("b", { id: "flag" }, String(header.value)), + ]), + h("div", { class: "card" }, [ + 'useRemoteConfig("welcomeText") -> ', + h("b", { id: "welcome" }, String(welcome.value)), + ]), + ]); + }, +}; + +async function main() { + const app = createSSRApp({ + render: () => + h(AbbyProvider, { initialData }, { default: () => h(Demo) }), + }); + const html = await renderToString(app); + console.log(html); + const doc = + "\n" + + '\n\n\n' + + "@tryabby/vue SSR demo (issue #68)\n" + + "\n" + + "\n\n

@tryabby/vue SSR demo

\n" + + '
' + + html + + "
\n\n\n"; + fs.writeFileSync(path.join(__dirname, "ssr-output.html"), doc, "utf8"); + console.log("[ssr-demo] wrote " + path.join(__dirname, "ssr-output.html")); +} + +main().catch((err) => { + console.error("[ssr-demo] FAILED:", err && err.stack ? err.stack : err); + process.exit(1); +}); diff --git a/packages/vue/demo/ssr-output.html b/packages/vue/demo/ssr-output.html new file mode 100644 index 00000000..5a0c9166 --- /dev/null +++ b/packages/vue/demo/ssr-output.html @@ -0,0 +1,12 @@ + + + + +@tryabby/vue SSR demo (issue #68) + + + +

@tryabby/vue SSR demo

+
useAbby("footer") -> variant:
useFeatureFlag("newHeader") -> true
useRemoteConfig("welcomeText") -> Hello from Abby Vue!
+ + From 0f9f38dc4cade437a4334699eb3c4a407358669d Mon Sep 17 00:00:00 2001 From: bounty-agent Date: Sun, 27 Sep 2026 20:46:19 +0530 Subject: [PATCH 3/5] demo: working browser demo (importmap+shim) with screenshot evidence --- packages/vue/demo/browser-demo.png | Bin 0 -> 35713 bytes packages/vue/demo/index.html | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 packages/vue/demo/browser-demo.png diff --git a/packages/vue/demo/browser-demo.png b/packages/vue/demo/browser-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..cd509e5a0fbed6bde31feed60f0edc174ddc7df9 GIT binary patch literal 35713 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 NuA-w{bl>*HzXAKw@ACiv literal 0 HcmV?d00001 diff --git a/packages/vue/demo/index.html b/packages/vue/demo/index.html index d2b1dc05..d940c935 100644 --- a/packages/vue/demo/index.html +++ b/packages/vue/demo/index.html @@ -13,13 +13,16 @@ "@vue/runtime-dom": "/node_modules/.pnpm/@vue+runtime-dom@3.5.43/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js", "@vue/runtime-core": "/node_modules/.pnpm/@vue+runtime-dom@3.5.43/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js", "@vue/shared": "/node_modules/.pnpm/@vue+runtime-dom@3.5.43/node_modules/@vue/shared/dist/shared.esm-bundler.js", - "@vue/reactivity": "/node_modules/.pnpm/@vue+runtime-dom@3.5.43/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js" + "@vue/reactivity": "/node_modules/.pnpm/@vue+runtime-dom@3.5.43/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js", + "@vue/compiler-dom": "/node_modules/.pnpm/@vue+compiler-dom@3.5.43/node_modules/@vue/compiler-dom/dist/compiler-dom.esm-bundler.js", + "@vue/compiler-core": "/node_modules/.pnpm/@vue+compiler-core@3.5.43/node_modules/@vue/compiler-core/dist/compiler-core.esm-bundler.js" }}

@tryabby/vue demo

loading…
+ ``` -Plain (non-reactive) getters — `getABTestValue`, `getFeatureFlagValue`, -`getRemoteConfig`, `getVariants`, `getABResetFunction` — work anywhere, +Plain (non-reactive) getters � `getABTestValue`, `getFeatureFlagValue`, +`getRemoteConfig`, `getVariants`, `getABResetFunction` � work anywhere, including outside components. +## SSR / Nuxt notes + +- `variant` is `undefined` on the very first render (the server never runs + `onMounted`, so no variant is selected yet). Guard with `v-if="variant"`. +- One `createAbby()` result holds mutable project data on its core instance. + On servers handling concurrent requests (Nuxt SSR), call `createAbby()` + per request (e.g. inside a request-scoped plugin) instead of sharing one + module-level instance, so request A passing `initialData` can never leak + into request B that omits it. SPA / client-only usage can share one + module-level instance (same as `@tryabby/react`). + ## API parity with `@tryabby/react` | React | Vue | @@ -89,7 +100,10 @@ including outside components. | `AbbyProvider` (`initialData` prop) | `AbbyProvider` (`initialData` prop) | | `useAbby(name, lookup?)` ? `{ variant, onAct }` | `useAbby(name, lookup?)` ? `{ variant: ComputedRef, onAct }` | | `useFeatureFlag(name)` | `useFeatureFlag(name)` ? `ComputedRef` | -| `useFeatureFlags()` | `useFeatureFlags()` ? `ComputedRef>` | -| `useRemoteConfig(name)` | `useRemoteConfig(name)` ? `ComputedRef<…>` | -| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | +| `useFeatureFlags()` | `useFeatureFlags()` ? `ComputedRef>` | +| `useRemoteConfig(name)` | `useRemoteConfig(name)` ? `ComputedRef<�>` | +| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | +| `withDevtools(factory, props)` ? hook | `withDevtools(factory, props)` ? component | +Config(name)` | `useRemoteConfig(name)` ? `ComputedRef<�>` | +| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | | `withDevtools(factory, props)` ? hook | `withDevtools(factory, props)` ? component | diff --git a/packages/vue/package.json b/packages/vue/package.json index 4227d365..0a85151d 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -6,10 +6,12 @@ "files": ["dist"], "module": "dist/index.mjs", "types": "dist/index.d.ts", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } }, "scripts": { "build": "tsup src/", diff --git a/packages/vue/src/context.ts b/packages/vue/src/context.ts index 4068c8ec..51aeed20 100644 --- a/packages/vue/src/context.ts +++ b/packages/vue/src/context.ts @@ -7,6 +7,7 @@ import { HttpService, type RemoteConfigValueString, type RemoteConfigValueStringToType, + type StorageServiceOptions, type ValidatorType, } from "@tryabby/core"; import type { Infer } from "@tryabby/core/validation"; @@ -219,7 +220,14 @@ export function createAbby< const useFeatureFlag = (name: FlagName): ComputedRef => { const data = useAbbyData(); - return computed(() => data.value.flags[name].value); + return computed(() => { + // accessing data.value keeps this subscribed to provider updates; + // the core fallback covers dev overrides, defaults and fallback values + // when the response omits the flag instead of crashing on undefined. + const record = data.value.flags[name]; + if (record !== undefined) return record.value; + return abby.getFeatureFlag(name); + }); }; /** diff --git a/packages/vue/tests/types.test.ts b/packages/vue/tests/types.test.ts index adb54e9f..1665a9f0 100644 --- a/packages/vue/tests/types.test.ts +++ b/packages/vue/tests/types.test.ts @@ -59,7 +59,8 @@ describe("useAbby", () => { ); await nextTick(); - assertType<"ONLY_ONE_VARIANT">(test1Result().variant.value); + // undefined covers the SSR/first render before the client resolves a variant + assertType<"ONLY_ONE_VARIANT" | undefined>(test1Result().variant.value); expect(test1Result().variant.value).toBeDefined(); @@ -68,7 +69,9 @@ describe("useAbby", () => { ); await nextTick(); - assertType<(typeof test2Variants)[number]>(test2Result().variant.value); + assertType<(typeof test2Variants)[number] | undefined>( + test2Result().variant.value + ); expect(test2Result().variant.value).toBeDefined(); }); diff --git a/packages/vue/tests/useAbby.test.ts b/packages/vue/tests/useAbby.test.ts index d778c355..05d014ca 100644 --- a/packages/vue/tests/useAbby.test.ts +++ b/packages/vue/tests/useAbby.test.ts @@ -173,9 +173,9 @@ describe("useAbby", () => { result().onAct(); await nextTick(); - // first call is tested above - expect(spy).toHaveBeenNthCalledWith( - 2, + // call history is restored between tests, so assert the ACT ping as the + // last sendData call of THIS test (mount PING flushes first) + expect(spy).toHaveBeenLastCalledWith( expect.objectContaining({ type: AbbyEventType.ACT }) ); }); From 61ec8e394cd334a2ed198e993353e961e45dd08c Mon Sep 17 00:00:00 2001 From: bounty-agent Date: Mon, 28 Sep 2026 13:28:08 +0530 Subject: [PATCH 5/5] docs: fix corrupted chars, precise SSR variant note, repair parity table --- packages/vue/README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/vue/README.md b/packages/vue/README.md index c48316d5..033097a1 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -1,6 +1,6 @@ # `@tryabby/vue` -Vue 3 integration for [Abby](https://docs.tryabby.com) � A/B testing, feature flags and remote config. +Vue 3 integration for [Abby](https://docs.tryabby.com) - A/B testing, feature flags and remote config. Mirrors `@tryabby/react`: the same `createAbby` factory, the same type-level quality, and the same runtime behaviour (including rendering an empty variant @@ -78,14 +78,16 @@ const welcomeMessage = useRemoteConfig("welcomeMessage"); ``` -Plain (non-reactive) getters � `getABTestValue`, `getFeatureFlagValue`, -`getRemoteConfig`, `getVariants`, `getABResetFunction` � work anywhere, +Plain (non-reactive) getters - `getABTestValue`, `getFeatureFlagValue`, +`getRemoteConfig`, `getVariants`, `getABResetFunction` - work anywhere, including outside components. ## SSR / Nuxt notes -- `variant` is `undefined` on the very first render (the server never runs - `onMounted`, so no variant is selected yet). Guard with `v-if="variant"`. +- `variant` starts as `""` on the very first render (the server never runs + `onMounted`, so no variant is selected yet). Without a lookup object that + empty string is the value; with a lookup object lacking a `""` key the + value is `undefined` until the client resolves. Guard with `v-if="variant"`. - One `createAbby()` result holds mutable project data on its core instance. On servers handling concurrent requests (Nuxt SSR), call `createAbby()` per request (e.g. inside a request-scoped plugin) instead of sharing one @@ -100,10 +102,7 @@ including outside components. | `AbbyProvider` (`initialData` prop) | `AbbyProvider` (`initialData` prop) | | `useAbby(name, lookup?)` ? `{ variant, onAct }` | `useAbby(name, lookup?)` ? `{ variant: ComputedRef, onAct }` | | `useFeatureFlag(name)` | `useFeatureFlag(name)` ? `ComputedRef` | -| `useFeatureFlags()` | `useFeatureFlags()` ? `ComputedRef>` | -| `useRemoteConfig(name)` | `useRemoteConfig(name)` ? `ComputedRef<�>` | -| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | -| `withDevtools(factory, props)` ? hook | `withDevtools(factory, props)` ? component | -Config(name)` | `useRemoteConfig(name)` ? `ComputedRef<�>` | -| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | +| `useFeatureFlags()` | `useFeatureFlags()` ? `ComputedRef>` | +| `useRemoteConfig(name)` | `useRemoteConfig(name)` ? `ComputedRef<...>` | +| `useRemoteConfigVariables()` | `useRemoteConfigVariables()` ? `ComputedRef>` | | `withDevtools(factory, props)` ? hook | `withDevtools(factory, props)` ? component |