Skip to content

Commit c5c9e2b

Browse files
committed
wip on vue sdk
1 parent 0c6a15e commit c5c9e2b

File tree

14 files changed

+404
-1
lines changed

14 files changed

+404
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"private": true,
55
"license": "MIT",
66
"workspaces": [
7-
"packages/*"
7+
"packages/*",
8+
"packages/vue-sdk/example"
89
],
910
"scripts": {
1011
"dev": "lerna run dev --parallel",

packages/eslint-config/base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = [
1717
"**/*.jsx",
1818
"**/*.ts",
1919
"**/*.tsx",
20+
"**/*.vue",
2021
],
2122
plugins: {
2223
import: importsPlugin,

packages/vue-sdk/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# vue-sdk

packages/vue-sdk/eslint.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const base = require("@bucketco/eslint-config/base");
2+
const pluginVue = require("eslint-plugin-vue");
3+
4+
module.exports = [
5+
...base,
6+
{ ignores: ["dist/", "example/"] },
7+
...pluginVue.configs["flat/recommended"],
8+
];

packages/vue-sdk/package.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"name": "@bucketco/vue-sdk",
3+
"version": "1.0.0",
4+
"description": "Vue SDK for Bucket",
5+
"main": "dist/index.js",
6+
"module": "dist/index.esm.js",
7+
"types": "dist/index.d.ts",
8+
"scripts": {
9+
"dev": "vite",
10+
"build": "tsc --project tsconfig.build.json && vite build",
11+
"test": "vitest -c vite.config.mjs",
12+
"test:ci": "vitest run -c vite.config.mjs --reporter=default --reporter=junit --outputFile=junit.xml",
13+
"coverage": "vitest run --coverage",
14+
"lint": "eslint .",
15+
"lint:ci": "eslint --output-file eslint-report.json --format json .",
16+
"prettier": "prettier --check .",
17+
"format": "yarn lint --fix && yarn prettier --write",
18+
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.mjs && yarn build"
19+
},
20+
"keywords": [
21+
"vue",
22+
"bucket",
23+
"sdk",
24+
"feature flags"
25+
],
26+
"author": "Bucket",
27+
"license": "MIT",
28+
"peerDependencies": {
29+
"vue": "^3.0.0"
30+
},
31+
"dependencies": {
32+
"@bucketco/browser-sdk": "workspace:^"
33+
},
34+
"devDependencies": {
35+
"@types/node": "^22.1.0",
36+
"@vitejs/plugin-vue": "^5.1.2",
37+
"@vue/tsconfig": "^0.5.1",
38+
"jsdom": "^24.1.0",
39+
"prettier": "^3.3.3",
40+
"typescript": "^5.4.5",
41+
"vite": "^5.3.5",
42+
"vite-plugin-dts": "^4.0.0-beta.2",
43+
"vue": "^3.0.0"
44+
},
45+
"files": [
46+
"dist",
47+
"src"
48+
],
49+
"repository": {
50+
"type": "git",
51+
"url": "https://github.com/bucketco/bucket-javascript-sdk.git"
52+
},
53+
"bugs": {
54+
"url": "https://github.com/bucketco/bucket-javascript-sdk/issues"
55+
},
56+
"homepage": "https://github.com/bucketco/bucket-javascript-sdk/blob/main/packages/vue-sdk/README.md"
57+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// BucketPlugin.ts
2+
import { reactive, ref } from "vue";
3+
import type { App, InjectionKey } from "vue";
4+
import type {
5+
CompanyContext,
6+
Feedback,
7+
FeedbackOptions,
8+
FlagsOptions,
9+
RequestFeedbackOptions,
10+
UserContext,
11+
} from "@bucketco/browser-sdk";
12+
import { BucketClient } from "@bucketco/browser-sdk";
13+
14+
import { version } from "../package.json";
15+
16+
const SDK_VERSION = `vue-sdk/${version}`;
17+
18+
type OtherContext = Record<string, any>;
19+
20+
export interface Flags {}
21+
22+
export type BucketFlags = keyof (keyof Flags extends never
23+
? Record<string, boolean>
24+
: Flags);
25+
26+
export type FlagsResult = { [k in BucketFlags]?: boolean };
27+
28+
export interface BucketState {
29+
flags: FlagsResult;
30+
isLoading: boolean;
31+
user: UserContext | null;
32+
company: CompanyContext | null;
33+
otherContext: OtherContext | null;
34+
}
35+
36+
export interface BucketPluginOptions {
37+
publishableKey: string;
38+
flagOptions?: Omit<FlagsOptions, "fallbackFlags"> & {
39+
fallbackFlags?: BucketFlags[];
40+
};
41+
feedback?: FeedbackOptions;
42+
host?: string;
43+
sseHost?: string;
44+
debug?: boolean;
45+
}
46+
47+
type ProvideType = {
48+
state: BucketState;
49+
updateUser: (newUser?: UserContext) => void;
50+
updateCompany: (newCompany?: CompanyContext) => void;
51+
updateOtherContext: (otherContext?: OtherContext) => void;
52+
track: (eventName: string, attributes?: Record<string, any>) => Promise<void>;
53+
sendFeedback: (opts: Omit<Feedback, "userId" | "companyId">) => Promise<void>;
54+
requestFeedback: (
55+
opts: Omit<RequestFeedbackOptions, "userId" | "companyId">,
56+
) => void;
57+
};
58+
59+
export const BucketInjectionKey = Symbol() as InjectionKey<ProvideType>;
60+
61+
export const BucketPlugin = {
62+
install(app: App, options: BucketPluginOptions) {
63+
const bucketState = reactive<BucketState>({
64+
flags: {},
65+
isLoading: true,
66+
user: null,
67+
company: null,
68+
otherContext: null,
69+
});
70+
71+
const client = ref<BucketClient | null>(null);
72+
73+
const updateUser = (newUser?: UserContext) => {
74+
bucketState.user = newUser ?? null;
75+
updateClient();
76+
};
77+
78+
const updateCompany = (newCompany?: CompanyContext) => {
79+
bucketState.company = newCompany ?? null;
80+
updateClient();
81+
};
82+
83+
const updateOtherContext = (otherContext?: OtherContext) => {
84+
bucketState.otherContext = otherContext ?? null;
85+
updateClient();
86+
};
87+
88+
const updateClient = () => {
89+
if (client.value) {
90+
client.value.stop();
91+
}
92+
93+
client.value = new BucketClient(
94+
options.publishableKey,
95+
{
96+
user: bucketState.user ?? undefined,
97+
company: bucketState.company ?? undefined,
98+
otherContext: bucketState.otherContext ?? undefined,
99+
},
100+
{
101+
host: options.host,
102+
sseHost: options.sseHost,
103+
flags: {
104+
...options.flagOptions,
105+
onUpdatedFlags: (flags) => {
106+
bucketState.flags = flags;
107+
},
108+
},
109+
feedback: options.feedback,
110+
logger: options.debug ? console : undefined,
111+
sdkVersion: SDK_VERSION,
112+
},
113+
);
114+
115+
client.value
116+
.initialize()
117+
.then(() => {
118+
bucketState.flags = client.value!.getFlags() ?? {};
119+
bucketState.isLoading = false;
120+
121+
// Update user attributes
122+
const { id: userId, ...userAttributes } = bucketState.user || {};
123+
if (userId) {
124+
client.value!.user(userAttributes).catch(() => {
125+
// ignore rejections. Logged inside
126+
});
127+
}
128+
129+
// Update company attributes
130+
const { id: companyId, ...companyAttributes } =
131+
bucketState.company || {};
132+
if (companyId) {
133+
client.value!.company(companyAttributes).catch(() => {
134+
// ignore rejections. Logged inside
135+
});
136+
}
137+
})
138+
.catch(() => {
139+
// initialize cannot actually throw, but this fixes lint warnings
140+
});
141+
};
142+
143+
const track = async (
144+
eventName: string,
145+
attributes?: Record<string, any>,
146+
) => {
147+
if (!bucketState.user?.id) {
148+
console.error("User is required to send events");
149+
return;
150+
}
151+
await client.value?.track(eventName, attributes);
152+
};
153+
154+
const sendFeedback = async (
155+
opts: Omit<Feedback, "userId" | "companyId">,
156+
) => {
157+
if (!bucketState.user?.id) {
158+
console.error("User is required to send feedback");
159+
return;
160+
}
161+
await client.value?.feedback({
162+
...opts,
163+
userId: String(bucketState.user.id),
164+
companyId: bucketState.company?.id
165+
? String(bucketState.company.id)
166+
: undefined,
167+
});
168+
};
169+
170+
const requestFeedback = (
171+
opts: Omit<RequestFeedbackOptions, "userId" | "companyId">,
172+
) => {
173+
if (!bucketState.user?.id) {
174+
console.error("User is required to request feedback");
175+
return;
176+
}
177+
client.value?.requestFeedback({
178+
...opts,
179+
userId: String(bucketState.user.id),
180+
companyId: bucketState.company?.id
181+
? String(bucketState.company.id)
182+
: undefined,
183+
});
184+
};
185+
186+
app.provide(BucketInjectionKey, {
187+
state: bucketState,
188+
updateUser,
189+
updateCompany,
190+
updateOtherContext,
191+
track,
192+
sendFeedback,
193+
requestFeedback,
194+
});
195+
196+
updateClient();
197+
},
198+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!-- BucketProvider.vue -->
2+
<template>
3+
<slot v-if="!isLoading || !loadingComponent" />
4+
<component :is="loadingComponent" v-else />
5+
</template>
6+
7+
<script lang="ts">
8+
import { computed, defineComponent } from "vue";
9+
10+
import { useBucket } from "./useBucket";
11+
12+
export default defineComponent({
13+
name: "BucketProvider",
14+
props: {
15+
loadingComponent: {
16+
type: [Object, Function, String],
17+
default: null,
18+
},
19+
},
20+
setup() {
21+
const bucket = useBucket();
22+
const isLoading = computed(() => (bucket ? bucket?.state.isLoading : true));
23+
24+
return {
25+
isLoading,
26+
};
27+
},
28+
});
29+
</script>

packages/vue-sdk/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { BucketPlugin } from "./BucketPlugin";
2+
export * from "./useBucket";
3+
import BucketProvider from "./BucketProvider.vue";
4+
export { BucketProvider };

packages/vue-sdk/src/useBucket.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { computed, inject } from "vue";
2+
import { BucketInjectionKey } from "./BucketPlugin";
3+
import type { BucketFlags } from "./BucketPlugin";
4+
5+
export function useBucket() {
6+
return inject(BucketInjectionKey);
7+
}
8+
9+
export function useFlagIsEnabled(flagKey: BucketFlags) {
10+
const bucket = useBucket();
11+
return computed(() => bucket?.state.flags[flagKey] ?? false);
12+
}
13+
14+
export function useFlag(key: BucketFlags) {
15+
const bucket = useBucket();
16+
return computed(() => ({
17+
isLoading: bucket?.state.isLoading,
18+
isEnabled: bucket?.state.flags[key] ?? false,
19+
}));
20+
}
21+
22+
export function useFlags() {
23+
const bucket = useBucket();
24+
return computed(() => ({
25+
isLoading: bucket?.state.isLoading,
26+
flags: bucket?.state.flags,
27+
}));
28+
}
29+
30+
export function useUpdateContext() {
31+
const bucket = useBucket();
32+
return {
33+
updateUser: bucket?.updateUser,
34+
updateCompany: bucket?.updateCompany,
35+
updateOtherContext: bucket?.updateOtherContext,
36+
isLoading: computed(() => bucket?.state.isLoading),
37+
};
38+
}
39+
40+
export function useTrack() {
41+
const bucket = useBucket();
42+
return bucket?.track;
43+
}
44+
45+
export function useRequestFeedback() {
46+
const bucket = useBucket();
47+
return bucket?.requestFeedback;
48+
}
49+
50+
export function useSendFeedback() {
51+
const bucket = useBucket();
52+
return bucket?.sendFeedback;
53+
}

packages/vue-sdk/src/vue.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare module "*.vue" {
2+
import type { DefineComponent } from "vue";
3+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4+
const component: DefineComponent<object, object, any>;
5+
export default component;
6+
}

0 commit comments

Comments
 (0)