@zachacious/protoc-gen-connect-vue 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@zachacious/protoc-gen-connect-vue",
3
+ "version": "1.0.2",
4
+ "description": "Smart TanStack Query & ConnectRPC SDK generator for Vue.js",
5
+ "type": "module",
6
+ "main": "dist/generator.js",
7
+ "bin": {
8
+ "protoc-gen-connect-vue": "bin/protoc-gen-connect-vue"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "bin",
13
+ "templates"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "bun build ./src/generator.ts --outdir ./dist --target node --bundle",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@bufbuild/protobuf": "^2.10.2",
24
+ "@bufbuild/protoplugin": "^2.10.2",
25
+ "mustache": "^4.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/mustache": "^4.2.6",
29
+ "@types/node": "^25.0.3",
30
+ "ts-node": "^10.9.2",
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }
@@ -0,0 +1,56 @@
1
+ import { computed } from "vue";
2
+ import { useQuery, useMutation, useInfiniteQuery, useQueryClient, useIsFetching, useIsMutating } from "@tanstack/vue-query";
3
+ import { ConnectError } from "@connectrpc/connect";
4
+ import { useGrpcClient } from "./client";
5
+
6
+ import {
7
+ {{#rpcs}}
8
+ {{queryDefinitionName}} as {{queryDefinitionName}}Def,
9
+ {{/rpcs}}
10
+ } from "./gen/{{{connectQueryFile}}}";
11
+
12
+ {{#wktImports}}import type { {{.}} } from "@bufbuild/protobuf";{{/wktImports}}
13
+ {{#localImports}}import type { {{.}} } from "./gen/{{{protoPbFile}}}";{{/localImports}}
14
+ {{#externalImports}}
15
+ import type { {{#types}}{{.}}, {{/types}} } from "{{{path}}}";
16
+ {{/externalImports}}
17
+
18
+ export type APIResponse<T> = { error: string | null; data: T | null; };
19
+
20
+ async function callConnect<ReqT, ResT, DataT>(
21
+ connectMethod: (req: ReqT) => Promise<ResT>,
22
+ request: ReqT,
23
+ dataExtractor: (res: ResT) => DataT
24
+ ): Promise<APIResponse<DataT>> {
25
+ try {
26
+ const res = await connectMethod(request);
27
+ return { error: null, data: dataExtractor(res) };
28
+ } catch (err) {
29
+ return {
30
+ error: err instanceof ConnectError ? err.message : "Unknown error",
31
+ data: null
32
+ };
33
+ }
34
+ }
35
+
36
+ export const useApi = () => {
37
+ const { client } = useGrpcClient();
38
+ const queryClient = useQueryClient();
39
+
40
+ // Global background activity tracking
41
+ const isFetching = useIsFetching();
42
+ const isMutating = useIsMutating();
43
+ const isGlobalLoading = computed(() => isFetching.value > 0 || isMutating.value > 0);
44
+
45
+ {{#rpcs}}
46
+ {{> rpc}}
47
+ {{/rpcs}}
48
+
49
+ return {
50
+ {{#rpcs}}
51
+ {{functionName}},
52
+ {{hookName}},
53
+ {{/rpcs}}
54
+ isGlobalLoading,
55
+ };
56
+ };
@@ -0,0 +1,128 @@
1
+ import { {{serviceName}} } from './gen/{{{protoPbFile}}}';
2
+ import { createClient, ConnectError, type Interceptor } from "@connectrpc/connect";
3
+ import { createConnectTransport } from "@connectrpc/connect-web";
4
+
5
+ let baseUrl = 'http://localhost:3000';
6
+
7
+ /**
8
+ * Configure the API endpoint at runtime.
9
+ */
10
+ export const setBaseUrl = (url: string) => {
11
+ baseUrl = url;
12
+ };
13
+
14
+ // --- AUTH RESOLVER ---
15
+ export type AuthResolver = () => string | null | Promise<string | null>;
16
+ let resolveToken: AuthResolver = () => null;
17
+ export const setAuthResolver = (resolver: AuthResolver) => { resolveToken = resolver; };
18
+
19
+ // --- ERROR CALLBACK ---
20
+ export type ErrorCallback = (error: ConnectError, url: string) => void;
21
+ let onError: ErrorCallback = () => {};
22
+ export const setSDKErrorCallback = (cb: ErrorCallback) => { onError = cb; };
23
+
24
+ const transportInterceptor: Interceptor = (next) => async (req) => {
25
+ const token = await resolveToken();
26
+ if (token) req.header.set("x-api-key", token);
27
+
28
+ try {
29
+ return await next(req);
30
+ } catch (err) {
31
+ if (err instanceof ConnectError) {
32
+ onError(err, req.url);
33
+ }
34
+ throw err;
35
+ }
36
+ };
37
+
38
+ export const transport = createConnectTransport({
39
+ baseUrl,
40
+ interceptors: [transportInterceptor],
41
+ });
42
+
43
+ export const client = createClient({{serviceName}}, transport);
44
+
45
+ export const globalQueryConfig = {
46
+ defaultOptions: {
47
+ queries: {
48
+ staleTime: 1000 * 60 * 5,
49
+ gcTime: 1000 * 60 * 30,
50
+ networkMode: 'offlineFirst',
51
+ },
52
+ },
53
+ };
54
+
55
+ export const useGrpcClient = () => ({ client });
56
+
57
+ /*
58
+
59
+ // the following is an example of how to use the hybrid api
60
+
61
+ // this generated sdk uses both raw wrappers and tanstack query
62
+
63
+ <script setup lang="ts">
64
+ import { useApi } from "@/api";
65
+ import { ref } from "vue";
66
+
67
+ const api = useApi();
68
+
69
+ // 1. Using the TanStack Hook (Auto-refetches)
70
+ const { data, isLoading } = api.useGetProfileQuery({ userId: "1" });
71
+
72
+ // 2. Using the Manual Wrapper (For actions/buttons)
73
+ const manualResult = ref("");
74
+ async function handleUpdate() {
75
+ const { data, error } = await api.updateProfile({ name: "New User" });
76
+ manualResult.value = error ? error : "Success!";
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <div v-if="isLoading">Loading...</div>
82
+ <div v-else>{{ data?.name }}</div>
83
+
84
+ <button @click="handleUpdate">Update Manually</button>
85
+ <p>{{ manualResult }}</p>
86
+ </template>
87
+
88
+ // use this to set the error interceptor - for instance to log to sentry or something
89
+ import { setAuthResolver, setSDKErrorCallback } from '@/api';
90
+ import { useAuthStore } from '@/stores/auth';
91
+
92
+ const auth = useAuthStore();
93
+
94
+ // Dynamic Auth: Returns current token from Pinia
95
+ setAuthResolver(() => auth.token);
96
+
97
+ // Flexible Error Handling:
98
+ setSDKErrorCallback((err, url) => {
99
+ if (process.env.NODE_ENV === 'development') {
100
+ console.error(`SDK Error at ${url}:`, err.message);
101
+ }
102
+
103
+ // Custom Production Logic
104
+ if (err.code === 16) { // Unauthenticated
105
+ auth.logout();
106
+ window.location.href = '/login';
107
+ }
108
+ });
109
+
110
+
111
+ // how to setup the auth interceptors
112
+ // to make sure the endpoints get the token
113
+
114
+ // src/stores/auth.ts
115
+ import { defineStore } from 'pinia';
116
+ import { setAuthResolver } from '@/api/client';
117
+
118
+ export const useAuthStore = defineStore('auth', () => {
119
+ const token = ref(null);
120
+
121
+ // Register with the SDK
122
+ setAuthResolver(() => token.value);
123
+
124
+ return { token };
125
+ });
126
+
127
+
128
+ */
@@ -0,0 +1,5 @@
1
+ // @generated
2
+ // DO NOT EDIT
3
+
4
+ export * from "./api";
5
+ export * from "./client";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Standard Async: {{functionName}}
3
+ */
4
+ const {{functionName}} = async (req: {{inputType}}): Promise<APIResponse<{{outputType}}>> => {
5
+ const res = await callConnect(client.{{functionName}}, req, (res) => res);
6
+ if (!res.error) queryClient.invalidateQueries({ queryKey: ["{{resource}}"] });
7
+ return res;
8
+ };
9
+
10
+ /**
11
+ * TanStack Hook: {{hookName}}
12
+ */
13
+ const {{hookName}} = (
14
+ {{#isQuery}}
15
+ input: {{inputType}},
16
+ options: any = {}
17
+ {{/isQuery}}
18
+ {{^isQuery}}
19
+ options: any = {}
20
+ {{/isQuery}}
21
+ ) => {
22
+ {{#isPaginated}}
23
+ return useInfiniteQuery({
24
+ ...{{queryDefinitionName}}Def,
25
+ ...options,
26
+ queryKey: ["{{resource}}", "{{functionName}}", input],
27
+ initialPageParam: 1,
28
+ getNextPageParam: options.getNextPageParam || ((lastPage: any) => lastPage.nextPage ?? undefined),
29
+ });
30
+ {{/isPaginated}}
31
+ {{^isPaginated}}
32
+ {{#isQuery}}
33
+ return useQuery({
34
+ ...{{queryDefinitionName}}Def,
35
+ ...options,
36
+ queryKey: ["{{resource}}", "{{functionName}}", input],
37
+ });
38
+ {{/isQuery}}
39
+ {{^isQuery}}
40
+ return useMutation({
41
+ ...{{queryDefinitionName}}Def,
42
+ ...options,
43
+ onSuccess: async (data, variables, context) => {
44
+ await queryClient.invalidateQueries({ queryKey: ["{{resource}}"] });
45
+ if (options.onSuccess) return options.onSuccess(data, variables, context);
46
+ },
47
+ });
48
+ {{/isQuery}}
49
+ {{/isPaginated}}
50
+ };