Skip to content

Commit 2ba3e22

Browse files
committed
wip on vue sdk
removed some hooks + useBucket throws if plugin isn't installed
1 parent 0c6a15e commit 2ba3e22

14 files changed

Lines changed: 377 additions & 1 deletion

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: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
track: (eventName: string, attributes?: Record<string, any>) => Promise<void>;
50+
sendFeedback: (opts: Omit<Feedback, "userId" | "companyId">) => Promise<void>;
51+
requestFeedback: (
52+
opts: Omit<RequestFeedbackOptions, "userId" | "companyId">,
53+
) => void;
54+
};
55+
56+
export const BucketInjectionKey = Symbol() as InjectionKey<ProvideType>;
57+
58+
export const BucketPlugin = {
59+
install(app: App, options: BucketPluginOptions) {
60+
const bucketState = reactive<BucketState>({
61+
flags: {},
62+
isLoading: true,
63+
user: null,
64+
company: null,
65+
otherContext: null,
66+
});
67+
68+
const client = ref<BucketClient | null>(null);
69+
70+
const updateClient = () => {
71+
if (client.value) {
72+
client.value.stop();
73+
}
74+
75+
client.value = new BucketClient(
76+
options.publishableKey,
77+
{
78+
user: bucketState.user ?? undefined,
79+
company: bucketState.company ?? undefined,
80+
otherContext: bucketState.otherContext ?? undefined,
81+
},
82+
{
83+
host: options.host,
84+
sseHost: options.sseHost,
85+
flags: {
86+
...options.flagOptions,
87+
onUpdatedFlags: (flags) => {
88+
bucketState.flags = flags;
89+
},
90+
},
91+
feedback: options.feedback,
92+
logger: options.debug ? console : undefined,
93+
sdkVersion: SDK_VERSION,
94+
},
95+
);
96+
97+
client.value
98+
.initialize()
99+
.then(() => {
100+
bucketState.flags = client.value!.getFlags() ?? {};
101+
bucketState.isLoading = false;
102+
103+
// Update user attributes
104+
const { id: userId, ...userAttributes } = bucketState.user || {};
105+
if (userId) {
106+
client.value!.user(userAttributes).catch(() => {
107+
// ignore rejections. Logged inside
108+
});
109+
}
110+
111+
// Update company attributes
112+
const { id: companyId, ...companyAttributes } =
113+
bucketState.company || {};
114+
if (companyId) {
115+
client.value!.company(companyAttributes).catch(() => {
116+
// ignore rejections. Logged inside
117+
});
118+
}
119+
})
120+
.catch(() => {
121+
// initialize cannot actually throw, but this fixes lint warnings
122+
});
123+
};
124+
125+
const track = async (
126+
eventName: string,
127+
attributes?: Record<string, any>,
128+
) => {
129+
if (!bucketState.user?.id) {
130+
console.error("User is required to send events");
131+
return;
132+
}
133+
await client.value?.track(eventName, attributes);
134+
};
135+
136+
const sendFeedback = async (
137+
opts: Omit<Feedback, "userId" | "companyId">,
138+
) => {
139+
if (!bucketState.user?.id) {
140+
console.error("User is required to send feedback");
141+
return;
142+
}
143+
await client.value?.feedback({
144+
...opts,
145+
userId: String(bucketState.user.id),
146+
companyId: bucketState.company?.id
147+
? String(bucketState.company.id)
148+
: undefined,
149+
});
150+
};
151+
152+
const requestFeedback = (
153+
opts: Omit<RequestFeedbackOptions, "userId" | "companyId">,
154+
) => {
155+
if (!bucketState.user?.id) {
156+
console.error("User is required to request feedback");
157+
return;
158+
}
159+
client.value?.requestFeedback({
160+
...opts,
161+
userId: String(bucketState.user.id),
162+
companyId: bucketState.company?.id
163+
? String(bucketState.company.id)
164+
: undefined,
165+
});
166+
};
167+
168+
app.provide(BucketInjectionKey, {
169+
state: bucketState,
170+
track,
171+
sendFeedback,
172+
requestFeedback,
173+
});
174+
175+
updateClient();
176+
},
177+
};
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { computed, inject } from "vue";
2+
import { BucketInjectionKey } from "./BucketPlugin";
3+
import type { BucketFlags } from "./BucketPlugin";
4+
5+
export function useBucket() {
6+
const bucket = inject(BucketInjectionKey);
7+
if (!bucket) {
8+
throw new Error("Bucket not found. Make sure to provide the BucketPlugin.");
9+
}
10+
return bucket
11+
}
12+
13+
export function useFlagIsEnabled(flagKey: BucketFlags) {
14+
const bucket = useBucket();
15+
return computed(() => bucket.state.flags[flagKey] ?? false);
16+
}
17+
18+
export function useFlag(key: BucketFlags) {
19+
const bucket = useBucket();
20+
return computed(() => ({
21+
isLoading: bucket.state.isLoading,
22+
isEnabled: bucket.state.flags[key] ?? false,
23+
}));
24+
}
25+
26+
export function useFlags() {
27+
const bucket = useBucket();
28+
return computed(() => ({
29+
isLoading: bucket.state.isLoading,
30+
flags: bucket.state.flags,
31+
}));
32+
}
33+
34+
export function useTrack() {
35+
const bucket = useBucket();
36+
return bucket.track;
37+
}
38+
39+
export function useRequestFeedback() {
40+
const bucket = useBucket();
41+
return bucket.requestFeedback;
42+
}
43+
44+
export function useSendFeedback() {
45+
const bucket = useBucket();
46+
return bucket.sendFeedback;
47+
}

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)