@zachacious/protoc-gen-connect-vue 1.0.21 → 1.0.22

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.
Files changed (2) hide show
  1. package/README.md +161 -103
  2. package/package.json +19 -2
package/README.md CHANGED
@@ -1,22 +1,28 @@
1
- # **protoc-gen-connect-vue**
1
+ # protoc-gen-connect-vue
2
2
 
3
- protoc-gen-connect-vue is a code generation plugin for [ConnectRPC](https://connectrpc.com/) tailored specifically for **Vue.js**. It generates a type-safe SDK that combines the power of **ConnectRPC** with the reactivity of the Vue Composition API and the caching capabilities of **TanStack Query (Vue-Query)**.
3
+ [![npm version](https://img.shields.io/npm/v/@zachacious/protoc-gen-connect-vue?style=flat-square&color=cb3837&logo=npm)](https://www.npmjs.com/package/@zachacious/protoc-gen-connect-vue)
4
+ [![license](https://img.shields.io/npm/l/@zachacious/protoc-gen-connect-vue?style=flat-square&color=blue)](LICENSE)
5
+ ![Vue 3](https://img.shields.io/badge/Vue-3-42b883?style=flat-square&logo=vuedotjs&logoColor=white)
4
6
 
5
- Unlike the official @connectrpc/connect-query which is architected for React, this plugin is built from the ground up for Vue developers, providing first-class support for ref, computed, and manual resource invalidation.
7
+ A [ConnectRPC](https://connectrpc.com/) codegen plugin that emits a Vue client SDK: a reactive transport, async wrappers for every RPC, and TanStack Query (vue-query) hooks on top of them.
6
8
 
7
- Protoc-gen-connect-vue attempts to create the full SDK - The client and API Wrappers(standard and Vue-Query). If you are building a Vue.js project with a ConnectRPC(web) backend. This aims to be an all in one solution so you are ready to go client-side.
9
+ The official `@connectrpc/connect-query` targets React. This covers the same ground for Vue — `ref`, `computed`, and cache invalidation that behaves the way a Vue app expects.
8
10
 
9
- > Note: I made this for use in personal and internal work projects. If you find it useful, let me know.
11
+ > I built this for my own projects and internal work. If it's useful to you, let me know.
10
12
 
11
- ## **Features**
13
+ ## What gets generated
12
14
 
13
- - **Reactive Client:** Automatically re-initializes the transport when your base URL or auth tokens change.
14
- - **TanStack Query Integration:** Generates useQuery, useMutation, and useInfiniteQuery hooks for every RPC.
15
- - **Manual Wrappers:** Provides standard async wrappers for actions that don't fit the "query" pattern.
16
- - **Message Initializers:** A createEmpty utility to generate default objects for any Protobuf message type.
17
- - **Query Key Factory:** Centralized query keys to simplify manual cache invalidation.
15
+ Three files, into whatever directory you point the plugin at:
18
16
 
19
- ## **Compatibility**
17
+ | File | Contents |
18
+ | --- | --- |
19
+ | `client.ts` | Reactive transport, auth resolver, error callback, credentials mode, `globalQueryConfig` |
20
+ | `api.ts` | `useApi()` with a wrapper + hook per RPC, plus `queryKeys` and `createEmpty` |
21
+ | `index.ts` | Re-exports both |
22
+
23
+ Every RPC gets an async wrapper (`getUser(req)`) and a hook (`useGetUser`). Paginated RPCs get a third: `useGetUserInfinite`.
24
+
25
+ ## Compatibility
20
26
 
21
27
  | Dependency | Supported versions |
22
28
  | --- | --- |
@@ -25,180 +31,232 @@ Protoc-gen-connect-vue attempts to create the full SDK - The client and API Wrap
25
31
  | `@tanstack/vue-query` | v5 |
26
32
  | Vue | 3 |
27
33
 
28
- The generated client configures credentials through a `fetch` override rather than the version-specific `credentials` transport option, so the same generated code type-checks against both connect-web v1.6+ and v2. See the [CHANGELOG](CHANGELOG.md) for the 1.0.21 upgrade notes.
34
+ Credentials are applied through a `fetch` override rather than the version-specific `credentials` transport option, so the generated code type-checks against both connect-web v1.6+ and v2.
29
35
 
30
- ## **Installation**
36
+ ## Install
31
37
 
32
- ```bash
33
- # install plugin either globally or locally
38
+ The plugin itself:
34
39
 
40
+ ```bash
35
41
  npm install -g @zachacious/protoc-gen-connect-vue
36
42
  ```
37
43
 
38
- ```Bash
39
- # install dependencies
44
+ Runtime dependencies in your Vue app:
40
45
 
41
- npm install protoc-gen-connect-vue @tanstack/vue-query @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf
46
+ ```bash
47
+ npm install @tanstack/vue-query @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf
42
48
  ```
43
49
 
44
- ## **Generation**
50
+ If you'd rather not install globally, add it as a dev dependency and point buf at the binary path (`local: ./node_modules/.bin/protoc-gen-connect-vue`).
45
51
 
46
- Add the plugin to your buf.gen.yaml:
52
+ ## Generate
47
53
 
48
- ```YAML
54
+ ```yaml
49
55
  plugins:
50
- # ...
56
+ # ...
51
57
  - remote: buf.build/bufbuild/es
52
58
  out: web/src/api/gen
53
59
  opt:
54
60
  - target=ts
55
61
 
56
- # Make sure this plugin goes after protoc-gen-es
57
- # It should probably go last
58
-
59
- # npm install -g @zachacious/protoc-gen-connect-vue
60
- # https://www.npmjs.com/package/@zachacious/protoc-gen-connect-vue
62
+ # Must run after protoc-gen-es. Last is a safe place for it.
61
63
  - local: protoc-gen-connect-vue
62
64
  out: web/src/api
63
65
  opt: target=ts
64
66
  ```
65
67
 
66
- ## **Setup**
68
+ The two `out` paths are load-bearing. The generated client imports message types from `./gen/<proto>_pb` relative to its own directory, so the protoc-gen-es output has to sit in a `gen/` folder directly inside this plugin's output directory. The pairing above (`web/src/api/gen` and `web/src/api`) satisfies that.
69
+
70
+ ## How your RPCs are classified
71
+
72
+ The generator infers intent from method names and message shapes. Knowing the rules saves you from wondering why a method got the hook it did.
73
+
74
+ **Query vs. mutation.** A unary method whose name starts with `Create`, `Update`, `Delete`, `Remove`, `Patch`, `Post`, `Set`, or `Add` becomes a mutation. Every other unary method becomes a query. Name a mutating RPC `ArchiveUser` and you'll get a query hook that auto-fetches.
75
+
76
+ **Cache resource.** The leading verb (`Get`, `List`, `ListAll`, `Search`, or any mutation verb) is stripped from the method name and what's left becomes the resource key. `GetUser`, `ListUsers`, and `UpdateUser` land on `User`, `Users`, and `User` respectively — note that `ListUsers` does *not* share a bucket with `GetUser`. If you want list queries invalidated by a single-item mutation, name them consistently.
67
77
 
68
- ### **1. Configure the Provider**
78
+ **Pagination.** A query gets an infinite hook when both its request and response carry pagination fields. The request is searched for `page_token`, `page_number`, `next_page_token`, `offset`, or `cursor`; the response for `next_page_token`, `next_page`, or `next_cursor`. Fields nested one to three levels deep inside a message named `page`/`paging`/`meta` count too.
69
79
 
70
- In your main.ts, provide the QueryClient to your Vue application.
80
+ **Streaming.** Only unary methods are supported. Streaming methods are classified as mutations and the generated hook will not work on them.
71
81
 
72
- ```TypeScript
82
+ **Services.** Only the first service found in the schema is generated. A multi-service proto needs a separate buf plugin invocation per service.
73
83
 
74
- import { createApp } from 'vue';
75
- import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
76
- import { globalQueryConfig } from './api/generated/client';
77
- import App from './App.vue';
84
+ ## Setup
85
+
86
+ ### 1. Register the query client
87
+
88
+ ```ts
89
+ // main.ts
90
+ import { createApp } from "vue";
91
+ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
92
+ import { globalQueryConfig } from "@/api";
93
+ import App from "./App.vue";
78
94
 
79
95
  const app = createApp(App);
80
96
  const queryClient = new QueryClient(globalQueryConfig);
81
97
 
82
98
  app.use(VueQueryPlugin, { queryClient });
83
- app.mount('#app');
99
+ app.mount("#app");
84
100
  ```
85
101
 
86
- ### **2. Runtime Configuration**
102
+ `globalQueryConfig` sets a 5-minute `staleTime`, a 30-minute `gcTime`, and `networkMode: "offlineFirst"`. Drop it and pass your own config if those don't suit you.
87
103
 
88
- Set up your authentication and base URL, typically in an identity store or main entry point.
104
+ ### 2. Runtime configuration
89
105
 
90
- ```TypeScript
106
+ The base URL defaults to `http://localhost:3000`, so set it before your first request — in an auth store, or at the top of `main.ts`.
91
107
 
92
- import { setBaseUrl, setAuthResolver, setSDKErrorCallback } from '@/api/generated';
108
+ ```ts
109
+ import { setBaseUrl, setAuthResolver, setSDKErrorCallback } from "@/api";
93
110
 
94
- setBaseUrl('https://api.example.com');
111
+ setBaseUrl("https://api.example.com");
95
112
 
96
- // Supports async token resolution
97
- setAuthResolver(async () => {
98
- const token = localStorage.getItem('token');
99
- return token ? token : null;
100
- });
113
+ // Called on every request. Sync or async.
114
+ setAuthResolver(async () => localStorage.getItem("token"));
101
115
 
102
116
  setSDKErrorCallback((err, url) => {
103
- console.error(`API Error at ${url}: ${err.message}`);
117
+ console.error(`API error at ${url}: ${err.message}`);
104
118
  });
105
119
  ```
106
120
 
107
- ### **3. Credentials / Cookies**
121
+ A non-null token is sent as the **`x-api-key`** header, not `Authorization: Bearer`. Your backend needs to read that header, or you need to edit the generated interceptor.
108
122
 
109
- The generated transport sends cookies cross-origin by default (`credentials: "include"`), applied through a `fetch` override so it works with both `@connectrpc/connect-web` v1 (>= 1.6) and v2 — v2 removed the `credentials` transport option, and the `fetch` override is the recommended replacement.
123
+ Changing the base URL rebuilds the transport, and any component holding a client picks it up through `computed`. The auth resolver is different — it's called fresh on every request, so a token change needs no rebuild and no re-render.
110
124
 
111
- If your backend's CORS configuration doesn't allow credentials, change the mode at runtime:
125
+ ### 3. Cookies and credentials
112
126
 
113
- ```TypeScript
127
+ The transport sends cookies cross-origin by default (`credentials: "include"`). If your backend's CORS setup doesn't allow credentials, change the mode at startup:
114
128
 
115
- import { setFetchCredentials } from '@/api/generated';
129
+ ```ts
130
+ import { setFetchCredentials } from "@/api";
116
131
 
117
132
  // "include" (default) | "same-origin" | "omit"
118
- setFetchCredentials('same-origin');
133
+ setFetchCredentials("same-origin");
119
134
  ```
120
135
 
121
- > **Upgrading from 1.0.20 or earlier:** previous releases passed a `fetchOptions` option that connect-web silently ignored, so credentials were never actually applied. From 1.0.21 the `"include"` default genuinely takes effect — if a cross-origin backend's CORS isn't configured for credentials, set `"same-origin"` at startup to keep the old behavior. Details in the [CHANGELOG](CHANGELOG.md).
136
+ It takes effect on the next request; no transport rebuild.
137
+
138
+ > **Upgrading from 1.0.20 or earlier:** those releases passed a `fetchOptions` key that connect-web ignored, so the `"include"` default never reached the wire and your app ran on the browser default of `"same-origin"`. From 1.0.21 it takes effect. Cross-origin backends serving `Access-Control-Allow-Origin: *`, or without `Access-Control-Allow-Credentials: true`, will start failing CORS after you regenerate — fix the backend or call `setFetchCredentials("same-origin")`. Full notes in the [CHANGELOG](CHANGELOG.md).
139
+
140
+ ## Usage
122
141
 
123
- ## **Usage**
142
+ `useApi()` calls `useQueryClient()` internally, so it has to run inside `setup()` with `VueQueryPlugin` installed.
124
143
 
125
- ### **Using Hooks and Manual Wrappers**
144
+ ### Queries
126
145
 
127
- The generated SDK provides both automated hooks and manual async functions.
146
+ Hooks take the full request message, not a bare id. Wrap it in `computed` to make it reactive — vue-query unwraps refs in both the key and the request.
128
147
 
129
- ```html
148
+ ```vue
130
149
  <script setup lang="ts">
131
- import { ref } from "vue";
132
- import { useApi } from "@/api/generated";
133
-
134
- const { getUser, useGetUser } = useApi();
135
-
136
- // 1. Reactive Hook (Auto-fetches)
137
- const userId = ref("123");
138
- const { data, isLoading, error } = useGetUser(userId);
139
-
140
- // 2. Manual Action
141
- const handleUpdate = async () => {
142
- const { data, error } = await updateUser({ id: "123", name: "New Name" });
143
- if (!error) {
144
- // Note: The SDK automatically invalidates related queries on success
145
- console.log("Update successful");
146
- }
147
- };
150
+ import { computed, ref } from "vue";
151
+ import { useApi } from "@/api";
152
+
153
+ const { useGetUser } = useApi();
154
+
155
+ const userId = ref("123");
156
+ const { data, isLoading, error } = useGetUser(
157
+ computed(() => ({ id: userId.value })),
158
+ );
148
159
  </script>
149
160
 
150
161
  <template>
151
- <div v-if="isLoading">Loading...</div>
162
+ <div v-if="isLoading">Loading…</div>
152
163
  <div v-else-if="error">{{ error }}</div>
153
- <div v-else>
154
- <h1>{{ data?.name }}</h1>
155
- <button @click="handleUpdate">Update Profile</button>
156
- </div>
164
+ <h1 v-else>{{ data?.name }}</h1>
157
165
  </template>
158
166
  ```
159
167
 
160
- ### **Initializing New Data (createEmpty)**
168
+ A second argument is spread into the underlying `useQuery` options, so `enabled`, `select`, `retry` and friends all work.
169
+
170
+ ### Mutations
171
+
172
+ Mutation hooks take options only; the request goes to `mutate`. On success they invalidate the whole resource bucket.
173
+
174
+ ```vue
175
+ <script setup lang="ts">
176
+ import { useApi } from "@/api";
177
+
178
+ const { useUpdateUser } = useApi();
179
+ const { mutate, isPending } = useUpdateUser();
180
+
181
+ const save = () => mutate({ id: "123", name: "New Name" });
182
+ </script>
183
+ ```
184
+
185
+ Passing your own `onSuccess` replaces the generated one and skips the invalidation. Invalidate yourself in that callback, or use `onSettled` instead.
186
+
187
+ ### Async wrappers
188
+
189
+ Every RPC also gets a plain async function for cases that don't fit a hook — a submit handler, a store action, anything imperative. It returns `{ data, error }` instead of throwing, where `error` is the `ConnectError` message string.
190
+
191
+ ```ts
192
+ const { updateUser } = useApi();
193
+
194
+ const { data, error } = await updateUser({ id: "123", name: "New Name" });
195
+ if (error) {
196
+ // handle it
197
+ }
198
+ ```
161
199
 
162
- Protobuf messages often require specific default values (e.g., empty strings instead of undefined). The createEmpty utility ensures your local state matches the expected Protobuf structure.
200
+ Wrappers invalidate their resource on success — including the wrappers for read methods. Calling `getUser()` imperatively invalidates `["User"]` and will refetch any mounted `useGetUser`. Use the hook for reads unless you specifically want that.
163
201
 
164
- ```TypeScript
202
+ ### Infinite queries
165
203
 
166
- import { useApi } from '@/api/generated';
204
+ ```ts
205
+ const { useListUsersInfinite } = useApi();
206
+
207
+ const { data, fetchNextPage, hasNextPage } = useListUsersInfinite({ limit: 20 });
208
+ ```
209
+
210
+ The page token is written into the request at the path the generator discovered, and read back off the response the same way. `initialPageParam` defaults to `""` and can be overridden through the options argument.
211
+
212
+ ### createEmpty
213
+
214
+ Protobuf messages want concrete defaults — empty strings rather than `undefined`. `createEmpty` builds one for any message type in the service graph, with optional overrides.
215
+
216
+ ```ts
217
+ import { useApi } from "@/api";
167
218
 
168
219
  const { createEmpty } = useApi();
169
220
 
170
- // Create an empty Customer object with all Protobuf defaults
171
- // Useful when you need to create and empty instance instead of using a nullable object
172
221
  const newCustomer = ref(createEmpty.Customer({ name: "Initial Name" }));
173
222
  ```
174
223
 
175
- ### **Manual Cache Invalidation (queryKeys)**
176
-
177
- If you need to manually refresh or invalidate a specific query, use the queryKeys factory to ensure the key matches the one used by the generated hooks.
224
+ ### Manual invalidation
178
225
 
179
- ```TypeScript
226
+ `queryKeys` produces `[resource, functionName, input]`. Since TanStack matches on key prefixes, invalidating a resource is the reliable move:
180
227
 
181
- import { useQueryClient } from '@tanstack/vue-query';
182
- import { queryKeys } from '@/api/generated';
228
+ ```ts
229
+ import { useQueryClient } from "@tanstack/vue-query";
230
+ import { queryKeys } from "@/api";
183
231
 
184
232
  const queryClient = useQueryClient();
185
233
 
186
- const refreshUser = (id: string) => {
187
- queryClient.invalidateQueries({
188
- queryKey: queryKeys.getUser(id)
189
- });
190
- };
234
+ // Everything derived from the User resource
235
+ queryClient.invalidateQueries({ queryKey: ["User"] });
236
+
237
+ // A single query — the input must deep-equal what the hook was called with
238
+ queryClient.invalidateQueries({ queryKey: queryKeys.getUser({ id: "123" }) });
191
239
  ```
192
240
 
193
- ## **Global Loading State**
241
+ `queryKeys` and `createEmpty` are exported at module scope as well as from `useApi()`, so you can use them outside components.
194
242
 
195
- The SDK exports a reactive isGlobalLoading computed property that tracks if any RPC (query or mutation) is currently in flight.
243
+ ### Global loading state
196
244
 
197
- ```TypeScript
245
+ `isGlobalLoading` is a computed that's true while any query or mutation is in flight. Handy for a top-level progress bar.
198
246
 
247
+ ```ts
199
248
  const { isGlobalLoading } = useApi();
200
249
  ```
201
250
 
202
- ---
251
+ ## Known limitations
252
+
253
+ - One service per invocation.
254
+ - Unary methods only.
255
+ - Hook inputs and options are typed `any`. Request and response types are checked on the async wrappers, not on the hooks.
256
+ - Method naming drives both the mutation classification and the cache buckets. Unconventional names produce surprising hooks.
257
+ - A user-supplied `onSuccess` on a mutation hook overrides the automatic invalidation rather than running after it.
258
+ - `BigInt.prototype.toJSON` is patched globally by the generated client so 64-bit fields survive serialization.
259
+
260
+ ## License
203
261
 
204
- License MIT
262
+ MIT
package/package.json CHANGED
@@ -1,7 +1,24 @@
1
1
  {
2
2
  "name": "@zachacious/protoc-gen-connect-vue",
3
- "version": "1.0.21",
4
- "description": "Smart TanStack Query & ConnectRPC SDK generator for Vue.js",
3
+ "version": "1.0.22",
4
+ "description": "Type-safe Vue 3 SDK generator for ConnectRPC with TanStack Query hooks",
5
+ "keywords": [
6
+ "protoc",
7
+ "protoc-plugin",
8
+ "protobuf",
9
+ "connectrpc",
10
+ "connect-rpc",
11
+ "grpc",
12
+ "grpc-web",
13
+ "vue",
14
+ "vue3",
15
+ "vue-query",
16
+ "tanstack-query",
17
+ "codegen",
18
+ "sdk-generator",
19
+ "typescript",
20
+ "buf"
21
+ ],
5
22
  "type": "module",
6
23
  "license": "MIT",
7
24
  "author": "Zachacious",