diff --git a/.changeset/vue-integration.md b/.changeset/vue-integration.md
new file mode 100644
index 00000000..feaa655c
--- /dev/null
+++ b/.changeset/vue-integration.md
@@ -0,0 +1,5 @@
+---
+"@tryabby/vue": minor
+---
+
+feat(vue): add Vue integration
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..bdc32e23
--- /dev/null
+++ b/packages/vue/README.md
@@ -0,0 +1,136 @@
+# @tryabby/vue
+
+Vue 3 integration for [Abby](https://docs.tryabby.com).
+
+## Installation
+
+```bash
+pnpm add @tryabby/vue
+```
+
+## Usage
+
+```ts
+// abby.ts
+import { createAbby } from "@tryabby/vue";
+
+export const {
+ AbbyProvider,
+ useAbby,
+ useFeatureFlag,
+ useRemoteConfig,
+ __abby__,
+} = createAbby({
+ projectId: "YOUR_PROJECT_ID",
+ currentEnvironment: "production",
+ environments: ["development", "production"],
+ tests: {
+ "signup-button": {
+ variants: ["control", "variant-a"],
+ },
+ },
+ flags: ["new-dashboard"],
+ remoteConfig: {
+ headline: "String",
+ },
+});
+```
+
+Wrap your application in the `AbbyProvider` component:
+
+```vue
+
+
+
+
+
+
+
+```
+
+The provider can also be passed data that was fetched on the server, which
+avoids a request on the client and makes the values available during SSR:
+
+```vue
+
+
+
+```
+
+## Composables
+
+### `useAbby`
+
+Returns the selected variant of an A/B test as a computed ref, plus an
+`onAct` function that reports an action for the test.
+
+```vue
+
+
+
+ {{ buttonLabel }}
+
+```
+
+Variant values are rendered as an empty string on the first render to avoid
+SSR hydration mismatches. Once mounted, `variant.value` contains the
+selected variant.
+
+### `useFeatureFlag`
+
+Returns the value of a feature flag as a computed ref.
+
+```vue
+
+
+
+
+
+```
+
+### `useRemoteConfig`
+
+Returns the value of a remote config variable as a computed ref. The type of
+the value is inferred from the remote config definition (`String`, `Number`
+or `JSON`).
+
+```vue
+
+
+
+ {{ headline }}
+
+```
+
+## Other exports
+
+`createAbby` also returns the following helpers:
+
+- `useFeatureFlags()` - a computed ref containing all flags with their values
+- `useRemoteConfigVariables()` - a computed ref containing all remote config values
+- `getFeatureFlagValue(name)` - read a flag outside of a component
+- `getRemoteConfig(name)` - read a remote config value outside of a component
+- `getABTestValue(name, lookup?)` - read a test variant outside of a component
+- `getVariants(name)` - all variants of a test
+- `getABResetFunction(name)` - reset the persisted variant of a test
+- `updateUserProperties(user)` - update the user properties used for rules
+- `__abby__` - the underlying `Abby` instance
diff --git a/packages/vue/package.json b/packages/vue/package.json
new file mode 100644
index 00000000..cc13e95b
--- /dev/null
+++ b/packages/vue/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@tryabby/vue",
+ "version": "0.0.0",
+ "description": "",
+ "main": "dist/index.js",
+ "files": ["dist"],
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js"
+ }
+ },
+ "scripts": {
+ "build": "tsup src/",
+ "dev": "pnpm run build --watch",
+ "test": "vitest"
+ },
+ "homepage": "https://docs.tryabby.com",
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@types/js-cookie": "^3.0.3",
+ "@vue/server-renderer": "3.4.24",
+ "@vue/test-utils": "2.4.6",
+ "jsdom": "^20.0.3",
+ "msw": "^0.49.1",
+ "node-fetch": "^3.3.0",
+ "tsconfig": "workspace:*",
+ "tsup": "^6.5.0",
+ "typescript": "5.5.4",
+ "vite": "5.4.0",
+ "vitest": "2.0.5",
+ "vue": "3.4.24"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.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..fa179290
--- /dev/null
+++ b/packages/vue/src/context.ts
@@ -0,0 +1,403 @@
+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 ComputedRef,
+ type InjectionKey,
+ type PropType,
+ type ShallowRef,
+ computed,
+ defineComponent,
+ inject,
+ onMounted,
+ onUnmounted,
+ provide,
+ ref,
+ shallowRef,
+ watch,
+} from "vue";
+import {
+ FlagStorageService,
+ RemoteConfigStorageService,
+ TestStorageService,
+} from "./StorageService";
+
+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
+ >
+) {
+ // we need to return the config as a const so that the types are narrowed
+ const config = abbyConfig;
+
+ 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;
+
+ type AbbyContext = {
+ data: ShallowRef;
+ abby: typeof abby;
+ };
+
+ const AbbyContextKey: InjectionKey = Symbol("abby");
+
+ const useAbbyData = (): AbbyContext => {
+ const context = inject(AbbyContextKey, null);
+
+ if (!context) {
+ throw new Error(
+ "useAbbyData must be used within an AbbyProvider. Wrap a parent component in to fix this error."
+ );
+ }
+
+ return context;
+ };
+
+ /**
+ * Composable to access the value of an A/B test.
+ * @param name the name of the test
+ * @param lookupObject an optional object to map the selected variant to a value
+ * @returns the selected variant (as a computed ref) and an `onAct` function
+ */
+ 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 return an empty string on the first render to avoid SSR mismatches
+ // because the server doesn't know which variant to render
+ const isMounted = ref(false);
+ onMounted(() => {
+ isMounted.value = true;
+ });
+
+ const selectedVariant = computed(() => {
+ if (!isMounted.value) return "";
+ return (
+ data.value.tests[name as unknown as TestName]?.selectedVariant ?? ""
+ );
+ });
+
+ watch(
+ selectedVariant,
+ (variant) => {
+ if (!variant) return;
+
+ HttpService.sendData({
+ url: config.apiUrl,
+ type: AbbyEventType.PING,
+ data: {
+ projectId: config.projectId,
+ selectedVariant: variant,
+ testName: name as string,
+ },
+ });
+ },
+ { immediate: true }
+ );
+
+ const onAct = () => {
+ const variant = selectedVariant.value;
+ if (!variant) return;
+
+ HttpService.sendData({
+ url: config.apiUrl,
+ type: AbbyEventType.ACT,
+ data: {
+ projectId: config.projectId,
+ selectedVariant: variant,
+ testName: name as string,
+ },
+ });
+ };
+
+ const variant = computed>(() => {
+ const currentVariant = selectedVariant.value;
+
+ return (
+ lookupObject
+ ? lookupObject[currentVariant as TestVariant]
+ : currentVariant
+ ) as ABTestReturnValue;
+ });
+
+ return {
+ /**
+ * A computed ref containing the selected variant.
+ */
+ variant,
+ /**
+ * 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,
+ };
+ };
+
+ /**
+ * Composable to access the value of a feature flag.
+ * @param name the name of the feature flag
+ * @returns a computed ref containing the value of the flag
+ */
+ const useFeatureFlag = (name: FlagName): ComputedRef => {
+ const { data } = useAbbyData();
+ return computed(() => data.value.flags[name]?.value ?? false);
+ };
+
+ /**
+ * Returns a computed ref 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 ref 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 RemoteConfigValueStringToType<
+ RemoteConfig[RemoteConfigName]
+ >,
+ })
+ )
+ );
+ };
+
+ /**
+ * Composable to access the value of a remote config variable.
+ * @param remoteConfigName the name of the remote config variable
+ * @returns a computed ref containing the value of the remote config variable
+ */
+ 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 AbbyProvider = defineComponent({
+ name: "AbbyProvider",
+ props: {
+ initialData: {
+ type: Object as PropType,
+ default: undefined,
+ },
+ },
+ setup(props, { slots }) {
+ const data = shallowRef(
+ props.initialData ? abby.init(props.initialData) : abby.getProjectData()
+ );
+
+ provide(AbbyContextKey, { data, abby });
+
+ let unsubscribe: (() => void) | undefined;
+
+ onMounted(() => {
+ data.value = abby.getProjectData();
+ unsubscribe = abby.subscribe((newData) => {
+ data.value = newData;
+ });
+
+ if (props.initialData) return;
+
+ abby.loadProjectData().then((newData) => {
+ if (!newData) return;
+ data.value = newData;
+ });
+ });
+
+ onUnmounted(() => {
+ unsubscribe?.();
+ });
+
+ return () => slots.default?.() ?? null;
+ },
+ });
+
+ const getFeatureFlagValue = (name: FlagName) => {
+ return abby.getFeatureFlag(name);
+ };
+
+ const getRemoteConfig = <
+ T extends RemoteConfigName,
+ Config extends RemoteConfig[T],
+ >(
+ remoteConfigName: T
+ ): RemoteConfigValueStringToType => {
+ return abby.getRemoteConfig(remoteConfigName);
+ };
+
+ const getABTestValue = <
+ TestName extends keyof Tests,
+ TestVariant extends Tests[TestName]["variants"][number],
+ LookupValue,
+ const Lookup extends
+ | Record
+ | undefined = undefined,
+ >(
+ testName: TestName,
+ lookupObject?: Lookup
+ ): ABTestReturnValue => {
+ const variant = abby.getTestVariant(testName);
+ // Typescript loses 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;
+ };
+
+ /**
+ * Simple helper function to 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,
+ getABResetFunction,
+ getVariants,
+ useFeatureFlags,
+ useRemoteConfigVariables,
+ updateUserProperties,
+ };
+}
diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts
new file mode 100644
index 00000000..9e38bc3a
--- /dev/null
+++ b/packages/vue/src/index.ts
@@ -0,0 +1,5 @@
+export {
+ createAbby,
+ type ABTestReturnValue,
+} from "./context";
+export { type ABConfig, type AbbyConfig, defineConfig } from "@tryabby/core";
diff --git a/packages/vue/tests/helpers.ts b/packages/vue/tests/helpers.ts
new file mode 100644
index 00000000..90d0f766
--- /dev/null
+++ b/packages/vue/tests/helpers.ts
@@ -0,0 +1,39 @@
+import type { AbbyDataResponse } from "@tryabby/core";
+import { mount } from "@vue/test-utils";
+import { type Component, defineComponent } from "vue";
+
+/**
+ * Mounts the given provider and calls the composable inside of its slot
+ * so that `provide` / `inject` and lifecycle hooks work as expected.
+ */
+export function renderComposable(
+ composable: () => T,
+ Provider: Component,
+ options: { initialData?: AbbyDataResponse } = {}
+) {
+ let result!: T;
+
+ const TestComponent = defineComponent({
+ setup() {
+ result = composable();
+ return () => null;
+ },
+ });
+
+ const wrapper = mount(Provider, {
+ props: options.initialData ? { initialData: options.initialData } : {},
+ slots: {
+ default: TestComponent,
+ },
+ });
+
+ return { result, wrapper };
+}
+
+export function clearCookies() {
+ for (const cookie of document.cookie.split(";")) {
+ const name = cookie.split("=")[0]?.trim();
+ if (!name) continue;
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
+ }
+}
diff --git a/packages/vue/tests/mocks/handlers.ts b/packages/vue/tests/mocks/handlers.ts
new file mode 100644
index 00000000..b1fde5e2
--- /dev/null
+++ b/packages/vue/tests/mocks/handlers.ts
@@ -0,0 +1,43 @@
+import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core";
+import { rest } from "msw";
+
+const returnData: AbbyDataResponse = {
+ tests: [
+ {
+ name: "test",
+ weights: [1, 1, 1, 1],
+ },
+ {
+ name: "test2",
+ weights: [1, 0],
+ },
+ ],
+ flags: [
+ {
+ name: "flag1",
+ value: true,
+ },
+ {
+ name: "flag2",
+ value: false,
+ },
+ ],
+ remoteConfig: [
+ {
+ name: "remoteConfig1",
+ value: "FooBar",
+ },
+ ],
+};
+
+export const handlers = [
+ rest.get(
+ `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
+ (_req, res, ctx) => {
+ return res(ctx.json(returnData));
+ }
+ ),
+ rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
+ return res(ctx.json(returnData));
+ }),
+];
diff --git a/packages/vue/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..cb279bb9
--- /dev/null
+++ b/packages/vue/tests/setup.ts
@@ -0,0 +1,18 @@
+import fetch from "node-fetch";
+import { afterAll, afterEach, beforeAll } from "vitest";
+import { server } from "./mocks/server";
+
+/// @ts-ignore node-fetch is not typed as a drop-in replacement
+global.fetch = fetch;
+
+// Establish API mocking before all tests.
+beforeAll(() => server.listen());
+
+// Clean up after the tests are finished.
+afterAll(() => server.close());
+
+// Reset any request handlers that we may add during the tests,
+// so they don't affect other tests.
+afterEach(() => {
+ server.resetHandlers();
+});
diff --git a/packages/vue/tests/ssr.test.ts b/packages/vue/tests/ssr.test.ts
new file mode 100644
index 00000000..5ea63659
--- /dev/null
+++ b/packages/vue/tests/ssr.test.ts
@@ -0,0 +1,128 @@
+import { renderToString } from "@vue/server-renderer";
+import { describe, expect, it } from "vitest";
+import { createSSRApp, defineComponent, h } from "vue";
+import { createAbby } from "../src";
+
+describe("useAbby", () => {
+ it("doesn't 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-ignore the variant is an empty string before the component is mounted
+ variant.value === "" ? "SSR!" : "",
+ variant.value === "current" ? "Secret" : "",
+ variant.value === "new" ? "Very Secret" : "",
+ ]);
+ },
+ });
+
+ const app = createSSRApp({
+ render: () =>
+ h(AbbyProvider, null, {
+ default: () => h(ComponentWithTest),
+ }),
+ });
+
+ const serverSideDOM = await renderToString(app);
+
+ expect(serverSideDOM).toContain("SSR!");
+ expect(serverSideDOM).not.toContain("Secret");
+ expect(serverSideDOM).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 ? "Secret" : "",
+ test2.value ? "SUPER SECRET" : "",
+ ]);
+ },
+ });
+
+ const app = createSSRApp({
+ render: () =>
+ h(
+ AbbyProvider,
+ {
+ initialData: {
+ flags: [
+ { value: true, name: "test" },
+ { value: false, name: "test2" },
+ ],
+ tests: [],
+ remoteConfig: [],
+ },
+ },
+ { default: () => h(ComponentWithFF) }
+ ),
+ });
+
+ const serverSideDOM = await renderToString(app);
+
+ expect(serverSideDOM).toContain("Secret");
+ expect(serverSideDOM).not.toContain("SUPER SECRET");
+ });
+});
+
+describe("useRemoteConfig", () => {
+ it("renders the correct remote config 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", [remoteConfig1.value]);
+ },
+ });
+
+ const app = createSSRApp({
+ render: () =>
+ h(
+ AbbyProvider,
+ {
+ initialData: {
+ flags: [],
+ tests: [],
+ remoteConfig: [{ name: "remoteConfig1", value: "FooBar" }],
+ },
+ },
+ { default: () => h(ComponentWithRC) }
+ ),
+ });
+
+ const serverSideDOM = await renderToString(app);
+
+ expect(serverSideDOM).toContain("FooBar");
+ });
+});
diff --git a/packages/vue/tests/types.test.ts b/packages/vue/tests/types.test.ts
new file mode 100644
index 00000000..9e6afa0f
--- /dev/null
+++ b/packages/vue/tests/types.test.ts
@@ -0,0 +1,127 @@
+import { describe, expectTypeOf, it } from "vitest";
+import { createAbby } from "../src";
+import { renderComposable } from "./helpers";
+
+describe("useAbby", () => {
+ it("returns the correct types", () => {
+ 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,
+ },
+ },
+ });
+
+ expectTypeOf(useAbby).parameter(0).toEqualTypeOf<"test" | "test2">();
+
+ const { result: test1Result } = renderComposable(
+ () => useAbby("test"),
+ AbbyProvider
+ );
+
+ expectTypeOf(test1Result.variant.value).toEqualTypeOf<"ONLY_ONE_VARIANT">();
+
+ const { result: test2Result } = renderComposable(
+ () => useAbby("test2"),
+ AbbyProvider
+ );
+
+ expectTypeOf(test2Result.variant.value).toEqualTypeOf<
+ (typeof test2Variants)[number]
+ >();
+ });
+
+ it("produces proper types with a lookup object", () => {
+ const { AbbyProvider, useAbby } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ tests: {
+ test: {
+ variants: ["A", "B", "C"],
+ },
+ },
+ });
+
+ const { result } = renderComposable(
+ () =>
+ useAbby("test", {
+ A: "Hello",
+ B: "Bonjour",
+ C: "Hola",
+ }),
+ AbbyProvider
+ );
+
+ expectTypeOf(result.variant.value).toEqualTypeOf<
+ "Hello" | "Bonjour" | "Hola"
+ >();
+ });
+});
+
+describe("useFeatureFlag", () => {
+ it("returns the correct types", () => {
+ const { AbbyProvider, useFeatureFlag } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ flags: ["test"],
+ });
+
+ expectTypeOf(useFeatureFlag).parameter(0).toEqualTypeOf<"test">();
+
+ const { result } = renderComposable(
+ () => useFeatureFlag("test"),
+ AbbyProvider
+ );
+
+ expectTypeOf(result.value).toEqualTypeOf();
+ });
+});
+
+describe("useRemoteConfig", () => {
+ it("returns the correct types", () => {
+ 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 } = renderComposable(
+ () => useRemoteConfig("stringRc"),
+ AbbyProvider
+ );
+ const { result: numberRcResult } = renderComposable(
+ () => useRemoteConfig("numberRc"),
+ AbbyProvider
+ );
+ const { result: jsonRcResult } = renderComposable(
+ () => useRemoteConfig("jsonRc"),
+ AbbyProvider
+ );
+
+ 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..b35f82dc
--- /dev/null
+++ b/packages/vue/tests/useAbby.test.ts
@@ -0,0 +1,387 @@
+import { AbbyEventType, HttpService } from "@tryabby/core";
+import { flushPromises } from "@vue/test-utils";
+import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { nextTick } from "vue";
+import { createAbby } from "../src";
+import { TestStorageService } from "../src/StorageService";
+import { clearCookies, renderComposable } from "./helpers";
+
+const OLD_ENV = process.env;
+
+async function settle() {
+ await flushPromises();
+ await nextTick();
+ await flushPromises();
+}
+
+beforeEach(() => {
+ clearCookies();
+ vi.restoreAllMocks();
+ process.env = { ...OLD_ENV };
+});
+
+afterAll(() => {
+ process.env = OLD_ENV;
+});
+
+describe("useAbby", () => {
+ it("returns a valid variant and an onAct function", async () => {
+ const setSpy = vi.spyOn(TestStorageService, "set");
+
+ const { AbbyProvider, useAbby } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ tests: {
+ test: { variants: ["OldFooter", "NewFooter"] },
+ },
+ });
+
+ const { result } = renderComposable(() => useAbby("test"), AbbyProvider);
+ await settle();
+
+ expect(setSpy).toHaveBeenCalled();
+ expect(["OldFooter", "NewFooter"]).toContain(result.variant.value);
+ expect(typeof result.onAct).toBe("function");
+ });
+
+ it("uses the persisted value", async () => {
+ const persistedValue = "SimonsText";
+ 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: ["SimonsText", "MatthiasText"] },
+ },
+ });
+
+ const { result } = renderComposable(() => useAbby("test"), AbbyProvider);
+ await settle();
+
+ // the stored value should not be overwritten
+ expect(setSpy).not.toHaveBeenCalled();
+ expect(getSpy).toHaveBeenCalled();
+ expect(result.variant.value).toBe(persistedValue);
+ });
+
+ it("looks up the selected variant in the lookup object", async () => {
+ const persistedValue = "SimonsText";
+ const variants = ["SimonsText", "MatthiasText"] as const;
+
+ vi.spyOn(TestStorageService, "get").mockReturnValue(persistedValue);
+
+ const { AbbyProvider, useAbby } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ tests: {
+ test: { variants },
+ },
+ });
+
+ const { result } = renderComposable(
+ () =>
+ useAbby("test", {
+ SimonsText: "a",
+ MatthiasText: "b",
+ }),
+ AbbyProvider
+ );
+ await settle();
+
+ expect(result.variant.value).toBe("a");
+ });
+
+ it("pings the current variant on mount", async () => {
+ const spy = vi.spyOn(HttpService, "sendData");
+
+ const { AbbyProvider, useAbby } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ tests: {
+ test: { variants: ["A", "B", "C"] },
+ },
+ });
+
+ renderComposable(() => useAbby("test"), AbbyProvider);
+ await settle();
+
+ expect(spy).toHaveBeenCalledWith(
+ expect.objectContaining({ type: AbbyEventType.PING })
+ );
+ });
+
+ it("notifies 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 } = renderComposable(() => useAbby("test"), AbbyProvider);
+ await settle();
+
+ result.onAct();
+
+ expect(spy).toHaveBeenCalledWith(
+ expect.objectContaining({ type: AbbyEventType.ACT })
+ );
+ });
+
+ it("returns the variants of a test", () => {
+ 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 synchronously", () => {
+ 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]
+ );
+ });
+});
+
+describe("useFeatureFlag", () => {
+ it("returns the fetched feature flag value", async () => {
+ const { AbbyProvider, useFeatureFlag } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ flags: ["flag1", "flag2"],
+ });
+
+ const { result: flag1 } = renderComposable(
+ () => useFeatureFlag("flag1"),
+ AbbyProvider
+ );
+ const { result: flag2 } = renderComposable(
+ () => useFeatureFlag("flag2"),
+ AbbyProvider
+ );
+
+ expect(flag1.value).toBe(false);
+ expect(flag2.value).toBe(false);
+
+ await settle();
+
+ expect(flag1.value).toBe(true);
+ expect(flag2.value).toBe(false);
+ });
+
+ it("falls back to false for flags that are not returned by the server", async () => {
+ const { AbbyProvider, useFeatureFlag } = createAbby({
+ environments: [],
+ projectId: "123",
+ flags: ["unknownFlag"],
+ currentEnvironment: "a",
+ });
+
+ const { result } = renderComposable(
+ () => useFeatureFlag("unknownFlag"),
+ AbbyProvider
+ );
+
+ expect(result.value).toBe(false);
+ await settle();
+ expect(result.value).toBe(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 } = renderComposable(
+ () => useFeatureFlag("flag1"),
+ AbbyProvider
+ );
+ const { result: flag2 } = renderComposable(
+ () => useFeatureFlag("flag2"),
+ AbbyProvider
+ );
+
+ expect(flag1.value).toBe(false);
+ expect(flag2.value).toBe(true);
+
+ await settle();
+
+ expect(flag1.value).toBe(false);
+ expect(flag2.value).toBe(true);
+ });
+
+ it("gets the stored feature flag value with a function", async () => {
+ const { getFeatureFlagValue, __abby__ } = createAbby({
+ environments: [],
+ projectId: "123",
+ flags: ["flag1", "flag2"],
+ currentEnvironment: "a",
+ });
+
+ await __abby__.loadProjectData();
+
+ expect(getFeatureFlagValue("flag1")).toBe(true);
+ expect(getFeatureFlagValue("flag2")).toBe(false);
+ });
+
+ it("returns the list of feature flags", async () => {
+ const { AbbyProvider, useFeatureFlags } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ flags: ["flag1", "flag2"],
+ });
+
+ const { result } = renderComposable(() => useFeatureFlags(), AbbyProvider);
+ expect(result.value).toHaveLength(2);
+
+ await settle();
+
+ expect(result.value).toEqual([
+ { name: "flag1", value: true },
+ { name: "flag2", value: false },
+ ]);
+ });
+});
+
+describe("useRemoteConfig", () => {
+ it("returns the fetched remote config value", async () => {
+ const { AbbyProvider, useRemoteConfig } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ remoteConfig: {
+ remoteConfig1: "String",
+ },
+ });
+
+ const { result } = renderComposable(
+ () => useRemoteConfig("remoteConfig1"),
+ AbbyProvider
+ );
+
+ await settle();
+
+ expect(result.value).toBe("FooBar");
+ });
+
+ it("uses the default value when remote config is not set", async () => {
+ const { AbbyProvider, useRemoteConfig } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ remoteConfig: {
+ unsetRemoteConfig: "String",
+ },
+ settings: {
+ remoteConfig: {
+ defaultValues: {
+ String: "defaultValue",
+ },
+ },
+ },
+ });
+
+ const { result } = renderComposable(
+ () => useRemoteConfig("unsetRemoteConfig"),
+ AbbyProvider
+ );
+
+ expect(result.value).toBe("defaultValue");
+ });
+
+ it("uses the devOverride", () => {
+ process.env.NODE_ENV = "development";
+
+ const { AbbyProvider, useRemoteConfig } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ remoteConfig: {
+ remoteConfig1: "String",
+ },
+ settings: {
+ remoteConfig: {
+ devOverrides: {
+ remoteConfig1: "overwrittenValue",
+ },
+ },
+ },
+ });
+
+ const { result } = renderComposable(
+ () => useRemoteConfig("remoteConfig1"),
+ AbbyProvider
+ );
+
+ expect(result.value).toBe("overwrittenValue");
+ });
+
+ it("returns the list of remote config variables", async () => {
+ const { AbbyProvider, useRemoteConfigVariables } = createAbby({
+ environments: [""],
+ currentEnvironment: "",
+ projectId: "123",
+ remoteConfig: {
+ remoteConfig1: "String",
+ },
+ });
+
+ const { result } = renderComposable(
+ () => useRemoteConfigVariables(),
+ AbbyProvider
+ );
+ expect(result.value).toHaveLength(1);
+
+ await settle();
+
+ expect(result.value).toEqual([{ name: "remoteConfig1", value: "FooBar" }]);
+ });
+});
diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json
new file mode 100644
index 00000000..9eb7b119
--- /dev/null
+++ b/packages/vue/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "tsconfig/base.json",
+ "compilerOptions": {
+ "lib": ["ESNext", "DOM"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "target": "es2020",
+ "jsx": "preserve",
+ "types": ["vitest/globals"]
+ }
+}
diff --git a/packages/vue/tsup.config.ts b/packages/vue/tsup.config.ts
new file mode 100644
index 00000000..ef269755
--- /dev/null
+++ b/packages/vue/tsup.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+ dts: true,
+ clean: true,
+ external: ["vue"],
+ 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..0d431636
--- /dev/null
+++ b/packages/vue/vite.config.ts
@@ -0,0 +1,11 @@
+///
+
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "jsdom",
+ setupFiles: "./tests/setup.ts",
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69fa896e..9dd81aeb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1108,6 +1108,52 @@ importers:
packages/tsconfig: {}
+ packages/vue:
+ dependencies:
+ '@tryabby/core':
+ specifier: workspace:*
+ version: link:../core
+ js-cookie:
+ specifier: ^3.0.1
+ version: 3.0.5
+ devDependencies:
+ '@types/js-cookie':
+ specifier: ^3.0.3
+ version: 3.0.3
+ '@vue/server-renderer':
+ specifier: 3.4.24
+ version: 3.4.24(vue@3.4.24(typescript@5.5.4))
+ '@vue/test-utils':
+ specifier: 2.4.6
+ version: 2.4.6
+ jsdom:
+ specifier: ^20.0.3
+ version: 20.0.3
+ 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.4.41)(ts-node@10.9.1(@types/node@22.1.0)(typescript@5.5.4))(typescript@5.5.4)
+ typescript:
+ specifier: 5.5.4
+ version: 5.5.4
+ vite:
+ specifier: 5.4.0
+ version: 5.4.0(@types/node@22.1.0)(less@4.2.0)(sass@1.77.8)(terser@5.31.1)
+ vitest:
+ specifier: 2.0.5
+ version: 2.0.5(@types/node@22.1.0)(jsdom@20.0.3)(less@4.2.0)(sass@1.77.8)(terser@5.31.1)
+ vue:
+ specifier: 3.4.24
+ version: 3.4.24(typescript@5.5.4)
+
packages:
'@adobe/css-tools@4.2.0':
@@ -14389,6 +14435,56 @@ packages:
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+ '@one-ini/wasm@0.1.1':
+ resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
+
+ '@vue/compiler-sfc@3.4.24':
+ resolution: {integrity: sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==}
+
+ '@vue/compiler-ssr@3.4.24':
+ resolution: {integrity: sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==}
+
+ '@vue/reactivity@3.4.24':
+ resolution: {integrity: sha512-nup3fSYg4i4LtNvu9slF/HF/0dkMQYfepUdORBcMSsankzRPzE7ypAFurpwyRBfU1i7Dn1kcwpYsE1wETSh91g==}
+
+ '@vue/runtime-core@3.4.24':
+ resolution: {integrity: sha512-c7iMfj6cJMeAG3s5yOn9Rc5D9e2/wIuaozmGf/ICGCY3KV5H7mbTVdvEkd4ZshTq7RUZqj2k7LMJWVx+EBiY1g==}
+
+ '@vue/runtime-dom@3.4.24':
+ resolution: {integrity: sha512-uXKzuh/Emfad2Y7Qm0ABsLZZV6H3mAJ5ZVqmAOlrNQRf+T5mxpPGZBfec1hkP41t6h6FwF6RSGCs/gd8WbuySQ==}
+
+ '@vue/server-renderer@3.4.24':
+ resolution: {integrity: sha512-H+DLK4sQF6sRgzKyofmlEVBIV/9KrQU6HIV7nt6yIwSGGKvSwlV8pqJlebUKLpbXaNHugdSfAbP6YmXF69lxow==}
+ peerDependencies:
+ vue: 3.4.24
+
+ '@vue/test-utils@2.4.6':
+ resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ editorconfig@1.0.7:
+ resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ js-beautify@1.15.4:
+ resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ vue-component-type-helpers@2.2.12:
+ resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
+
+ vue@3.4.24:
+ resolution: {integrity: sha512-NPdx7dLGyHmKHGRRU5bMRYVE+rechR+KDU5R2tSTNG36PuMwbfAJ+amEvOAw7BPfZp5sQulNELSLm5YUkau+Sg==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
snapshots:
'@adobe/css-tools@4.2.0': {}
@@ -31399,3 +31495,200 @@ snapshots:
react: 18.2.0
zwitch@2.0.4: {}
+
+ '@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
+ webpack: 5.92.1(esbuild@0.21.5)
+ webpack-dev-server: 5.0.4(webpack@5.92.1)
+ transitivePeerDependencies:
+ - chokidar
+
+ '@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
+ webpack: 5.92.1(esbuild@0.21.5)
+
+ '@one-ini/wasm@0.1.1': {}
+
+ '@vue/compiler-sfc@3.4.24':
+ dependencies:
+ '@babel/parser': 7.25.3
+ '@vue/compiler-core': 3.4.24
+ '@vue/compiler-dom': 3.4.24
+ '@vue/compiler-ssr': 3.4.24
+ '@vue/shared': 3.4.24
+ estree-walker: 2.0.2
+ magic-string: 0.30.10
+ postcss: 8.4.41
+ source-map-js: 1.2.0
+
+ '@vue/compiler-ssr@3.4.24':
+ dependencies:
+ '@vue/compiler-dom': 3.4.24
+ '@vue/shared': 3.4.24
+
+ '@vue/reactivity@3.4.24':
+ dependencies:
+ '@vue/shared': 3.4.24
+
+ '@vue/runtime-core@3.4.24':
+ dependencies:
+ '@vue/reactivity': 3.4.24
+ '@vue/shared': 3.4.24
+
+ '@vue/runtime-dom@3.4.24':
+ dependencies:
+ '@vue/runtime-core': 3.4.24
+ '@vue/shared': 3.4.24
+ csstype: 3.2.3
+
+ '@vue/server-renderer@3.4.24(vue@3.4.24(typescript@5.5.4))':
+ dependencies:
+ '@vue/compiler-ssr': 3.4.24
+ '@vue/shared': 3.4.24
+ vue: 3.4.24(typescript@5.5.4)
+
+ '@vue/test-utils@2.4.6':
+ dependencies:
+ js-beautify: 1.15.4
+ vue-component-type-helpers: 2.2.12
+
+ 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
+ schema-utils: 4.1.0
+ 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
+ globby: 14.0.2
+ normalize-path: 3.0.0
+ schema-utils: 4.2.0
+ serialize-javascript: 6.0.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.41)
+ postcss: 8.4.41
+ postcss-modules-extract-imports: 3.1.0(postcss@8.4.41)
+ postcss-modules-local-by-default: 4.0.5(postcss@8.4.41)
+ postcss-modules-scope: 3.2.0(postcss@8.4.41)
+ postcss-modules-values: 4.0.0(postcss@8.4.41)
+ postcss-value-parser: 4.2.0
+ semver: 7.6.2
+ optionalDependencies:
+ webpack: 5.92.1(esbuild@0.21.5)
+
+ csstype@3.2.3: {}
+
+ editorconfig@1.0.7:
+ dependencies:
+ '@one-ini/wasm': 0.1.1
+ commander: 10.0.1
+ minimatch: 9.0.4
+ semver: 7.6.2
+
+ icss-utils@5.1.0(postcss@8.4.41):
+ dependencies:
+ postcss: 8.4.41
+
+ js-beautify@1.15.4:
+ dependencies:
+ config-chain: 1.1.13
+ editorconfig: 1.0.7
+ glob: 10.4.5
+ js-cookie: 3.0.5
+ nopt: 7.2.1
+
+ less-loader@12.2.0(less@4.2.0)(webpack@5.92.1):
+ dependencies:
+ less: 4.2.0
+ optionalDependencies:
+ 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:
+ 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
+ webpack: 5.92.1(esbuild@0.21.5)
+
+ next-themes@0.2.1(next@14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+ dependencies:
+ next: 14.1.1(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.8)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+
+ 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
+ postcss: 8.4.38
+ semver: 7.6.2
+ optionalDependencies:
+ webpack: 5.92.1(esbuild@0.21.5)
+ transitivePeerDependencies:
+ - typescript
+
+ postcss-modules-extract-imports@3.1.0(postcss@8.4.41):
+ dependencies:
+ postcss: 8.4.41
+
+ postcss-modules-local-by-default@4.0.5(postcss@8.4.41):
+ dependencies:
+ icss-utils: 5.1.0(postcss@8.4.41)
+ postcss: 8.4.41
+ postcss-selector-parser: 6.0.13
+ postcss-value-parser: 4.2.0
+
+ postcss-modules-scope@3.2.0(postcss@8.4.41):
+ dependencies:
+ postcss: 8.4.41
+ postcss-selector-parser: 6.0.13
+
+ postcss-modules-values@4.0.0(postcss@8.4.41):
+ dependencies:
+ icss-utils: 5.1.0(postcss@8.4.41)
+ postcss: 8.4.41
+
+ sass-loader@14.2.1(sass@1.77.6)(webpack@5.92.1):
+ dependencies:
+ neo-async: 2.6.2
+ optionalDependencies:
+ sass: 1.77.6
+ webpack: 5.92.1(esbuild@0.21.5)
+
+ source-map-loader@5.0.0(webpack@5.92.1):
+ dependencies:
+ iconv-lite: 0.6.3
+ source-map-js: 1.2.0
+ webpack: 5.92.1(esbuild@0.21.5)
+
+ vue-component-type-helpers@2.2.12: {}
+
+ vue@3.4.24(typescript@5.5.4):
+ dependencies:
+ '@vue/compiler-dom': 3.4.24
+ '@vue/compiler-sfc': 3.4.24
+ '@vue/runtime-dom': 3.4.24
+ '@vue/server-renderer': 3.4.24(vue@3.4.24(typescript@5.5.4))
+ '@vue/shared': 3.4.24
+ optionalDependencies:
+ typescript: 5.5.4
+
+ webpack-subresource-integrity@5.1.0(webpack@5.92.1):
+ dependencies:
+ typed-assert: 1.0.9
+ webpack: 5.92.1(esbuild@0.21.5)