Repository navigation
feat(vue): add @tryabby/vue integration with useAbby, useFeatureFlag, and useRemoteConfig (#68) #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thinhnx128
wants to merge
6
commits into
tryabby:main
Choose a base branch
from
thinhnx128:feat/vue-integration-68
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+797
−0
Open
feat(vue): add @tryabby/vue integration with useAbby, useFeatureFlag, and useRemoteConfig (#68) #208
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
1631c55
feat(vue): add @tryabby/vue integration with useAbby, useFeatureFlag,…
thinhnx128 7f5179f
fix(vue): align AbbyCoreClient with @tryabby/core, ensure SSR hydrati…
thinhnx128 a0d8c21
chore(vue): add package.json and tsconfig.json for @tryabby/vue works…
thinhnx128 154b126
fix(vue): export createAbbyPlugin, run tests in jsdom with multi-subs…
thinhnx128 ba4be2a
fix(vue): add vue to devDependencies and resolve real vue package in …
thinhnx128 9c1fe84
chore(vue): sync pnpm-lock.yaml with packages/vue devDependencies (vu…
thinhnx128 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| { | ||
| "name": "@tryabby/vue", | ||
| "version": "1.0.0", | ||
| "description": "Vue 3 composables 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", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.mjs", | ||
| "require": "./dist/index.js" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "build": "tsup src/index.ts --format cjs,esm --dts --external vue", | ||
| "dev": "pnpm run build --watch", | ||
| "test": "vitest run --environment jsdom" | ||
| }, | ||
| "homepage": "https://docs.tryabby.com", | ||
| "keywords": ["abby", "vue", "ab-testing", "feature-flags", "remote-config"], | ||
| "author": "", | ||
| "license": "ISC", | ||
| "devDependencies": { | ||
| "jsdom": "^20.0.3", | ||
| "tsconfig": "workspace:*", | ||
| "tsup": "^6.5.0", | ||
| "typescript": "5.5.4", | ||
| "vite": "5.4.0", | ||
| "vitest": "2.0.5", | ||
| "vue": "^3.4.38" | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| }, | ||
| "peerDependencies": { | ||
| "vue": "^3.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "vue": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "dependencies": { | ||
| "@tryabby/core": "workspace:*" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| import { AbbyEventType, HttpService } from "@tryabby/core"; | ||
| import { | ||
| type App, | ||
| type ComputedRef, | ||
| type InjectionKey, | ||
| type Plugin, | ||
| computed, | ||
| getCurrentInstance, | ||
| inject, | ||
| onMounted, | ||
| onUnmounted, | ||
| provide, | ||
| ref, | ||
| } from "vue"; | ||
|
|
||
| export interface AbbyCoreClient< | ||
| TConfig extends Record<string, any> = Record<string, any>, | ||
| TFlag extends string = string, | ||
| TTest extends string = string, | ||
| > { | ||
| getTestVariant?: (testId: TTest) => string; | ||
| getVariant?: (testId: TTest) => string; | ||
| onAct?: (testId: TTest, selectedVariant?: string) => void; | ||
| getConfig?: () => { | ||
| projectId: string; | ||
| apiUrl?: string; | ||
| currentEnvironment?: string; | ||
| }; | ||
| getFeatureFlag: (flagKey: TFlag) => boolean; | ||
| getRemoteConfig: <K extends Extract<keyof TConfig, string>>( | ||
| configKey: K | ||
| ) => TConfig[K]; | ||
| subscribe?: (listener: (data?: any) => void) => () => void; | ||
| } | ||
|
|
||
| export const ABBY_INJECTION_KEY: InjectionKey<AbbyCoreClient<any, any, any>> = | ||
| Symbol("AbbyVueClient"); | ||
|
|
||
| function resolveTestVariant< | ||
| TConfig extends Record<string, any>, | ||
| TFlag extends string, | ||
| TTest extends string, | ||
| >(client: AbbyCoreClient<TConfig, TFlag, TTest>, testId: TTest): string { | ||
| if (typeof client.getTestVariant === "function") { | ||
| return client.getTestVariant(testId) ?? ""; | ||
| } | ||
| if (typeof client.getVariant === "function") { | ||
| return client.getVariant(testId) ?? ""; | ||
| } | ||
| throw new Error( | ||
| "[@tryabby/vue] Provided Abby client must implement getTestVariant(testId) or getVariant(testId)." | ||
| ); | ||
| } | ||
|
|
||
| export function createAbbyPlugin< | ||
| TConfig extends Record<string, any> = Record<string, any>, | ||
| TFlag extends string = string, | ||
| TTest extends string = string, | ||
| >(client: AbbyCoreClient<TConfig, TFlag, TTest>): Plugin { | ||
| return { | ||
| install(app: App) { | ||
| app.provide(ABBY_INJECTION_KEY, client); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export function provideAbby< | ||
| TConfig extends Record<string, any> = Record<string, any>, | ||
| TFlag extends string = string, | ||
| TTest extends string = string, | ||
| >( | ||
| client: AbbyCoreClient<TConfig, TFlag, TTest> | ||
| ): AbbyCoreClient<TConfig, TFlag, TTest> { | ||
| provide(ABBY_INJECTION_KEY, client); | ||
| return client; | ||
| } | ||
|
|
||
| export function useAbbyClient< | ||
| TConfig extends Record<string, any> = Record<string, any>, | ||
| TFlag extends string = string, | ||
| TTest extends string = string, | ||
| >(): AbbyCoreClient<TConfig, TFlag, TTest> { | ||
| const client = inject(ABBY_INJECTION_KEY); | ||
| if (!client) { | ||
| throw new Error( | ||
| "[@tryabby/vue] Abby client not found. Register via app.use(createAbbyPlugin(client)) or provideAbby(client)." | ||
| ); | ||
| } | ||
| return client as AbbyCoreClient<TConfig, TFlag, TTest>; | ||
| } | ||
|
|
||
| export function useAbby<TTest extends string = string>( | ||
| testId: TTest | ||
| ): { | ||
| variant: ComputedRef<string>; | ||
| onAct: () => void; | ||
| } { | ||
| const client = useAbbyClient<Record<string, any>, string, TTest>(); | ||
| const version = ref(0); | ||
| const isMounted = ref(false); | ||
|
|
||
| let unsubscribe: (() => void) | undefined; | ||
|
|
||
| if (getCurrentInstance()) { | ||
| onMounted(() => { | ||
| isMounted.value = true; | ||
| if (typeof client.subscribe === "function") { | ||
| unsubscribe = client.subscribe(() => { | ||
| version.value += 1; | ||
| }); | ||
| } | ||
| }); | ||
| onUnmounted(() => { | ||
| unsubscribe?.(); | ||
| unsubscribe = undefined; | ||
| }); | ||
| } | ||
|
|
||
| const variant = computed(() => { | ||
| void version.value; | ||
| if (!isMounted.value) { | ||
| return ""; | ||
| } | ||
| return resolveTestVariant(client, testId); | ||
| }); | ||
|
|
||
| const onAct = () => { | ||
| const currentVariant = variant.value; | ||
| if (!currentVariant) return; | ||
| if (typeof client.onAct === "function") { | ||
| client.onAct(testId, currentVariant); | ||
| return; | ||
| } | ||
| const cfg = | ||
| typeof client.getConfig === "function" ? client.getConfig() : undefined; | ||
| if (cfg?.projectId) { | ||
| HttpService.sendData({ | ||
| url: cfg.apiUrl, | ||
| type: AbbyEventType.ACT, | ||
| data: { | ||
| projectId: cfg.projectId, | ||
| selectedVariant: currentVariant, | ||
| testName: testId, | ||
| }, | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| return { variant, onAct }; | ||
| } | ||
|
|
||
| export function useFeatureFlag<TFlag extends string = string>( | ||
| flagKey: TFlag | ||
| ): ComputedRef<boolean> { | ||
| const client = useAbbyClient<Record<string, any>, TFlag, string>(); | ||
| const version = ref(0); | ||
|
|
||
| let unsubscribe: (() => void) | undefined; | ||
| if (getCurrentInstance()) { | ||
| onMounted(() => { | ||
| if (typeof client.subscribe === "function") { | ||
| unsubscribe = client.subscribe(() => { | ||
| version.value += 1; | ||
| }); | ||
| } | ||
| }); | ||
| onUnmounted(() => { | ||
| unsubscribe?.(); | ||
| unsubscribe = undefined; | ||
| }); | ||
| } | ||
|
|
||
| return computed(() => { | ||
| void version.value; | ||
| return Boolean(client.getFeatureFlag(flagKey)); | ||
| }); | ||
| } | ||
|
|
||
| export function useRemoteConfig< | ||
| TConfig extends Record<string, any> = Record<string, any>, | ||
| K extends Extract<keyof TConfig, string> = Extract<keyof TConfig, string>, | ||
| >(configKey: K): ComputedRef<TConfig[K]> { | ||
| const client = useAbbyClient<TConfig, string, string>(); | ||
| const version = ref(0); | ||
|
|
||
| let unsubscribe: (() => void) | undefined; | ||
| if (getCurrentInstance()) { | ||
| onMounted(() => { | ||
| if (typeof client.subscribe === "function") { | ||
| unsubscribe = client.subscribe(() => { | ||
| version.value += 1; | ||
| }); | ||
| } | ||
| }); | ||
| onUnmounted(() => { | ||
| unsubscribe?.(); | ||
| unsubscribe = undefined; | ||
| }); | ||
| } | ||
|
|
||
| return computed(() => { | ||
| void version.value; | ||
| return client.getRemoteConfig(configKey); | ||
| }); | ||
| } | ||
|
|
||
| export function createAbbyVue< | ||
| TConfig extends Record<string, any> = Record<string, any>, | ||
| TFlag extends string = string, | ||
| TTest extends string = string, | ||
| >(client: AbbyCoreClient<TConfig, TFlag, TTest>) { | ||
| return { | ||
| plugin: createAbbyPlugin(client), | ||
| provideAbby: () => provideAbby(client), | ||
| useAbby: (testId: TTest) => useAbby<TTest>(testId), | ||
| useFeatureFlag: (flagKey: TFlag) => useFeatureFlag<TFlag>(flagKey), | ||
| useRemoteConfig: <K extends Extract<keyof TConfig, string>>(configKey: K) => | ||
| useRemoteConfig<TConfig, K>(configKey), | ||
| }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.