@zachacious/protoc-gen-connect-vue 1.0.20 → 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.
- package/README.md +179 -94
- package/dist/generator.js +6 -5
- package/package.json +19 -2
- package/templates/client.ts.mustache +23 -1
package/README.md
CHANGED
|
@@ -1,177 +1,262 @@
|
|
|
1
|
-
#
|
|
1
|
+
# protoc-gen-connect-vue
|
|
2
2
|
|
|
3
|
-
protoc-gen-connect-vue
|
|
3
|
+
[](https://www.npmjs.com/package/@zachacious/protoc-gen-connect-vue)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+

|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
11
|
+
> I built this for my own projects and internal work. If it's useful to you, let me know.
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## What gets generated
|
|
12
14
|
|
|
13
|
-
|
|
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
|
-
|
|
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 |
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
Every RPC gets an async wrapper (`getUser(req)`) and a hook (`useGetUser`). Paginated RPCs get a third: `useGetUserInfinite`.
|
|
24
|
+
|
|
25
|
+
## Compatibility
|
|
26
|
+
|
|
27
|
+
| Dependency | Supported versions |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `@connectrpc/connect` / `@connectrpc/connect-web` | v1.6+ and v2 |
|
|
30
|
+
| `@bufbuild/protobuf` / `protoc-gen-es` codegen | v2 |
|
|
31
|
+
| `@tanstack/vue-query` | v5 |
|
|
32
|
+
| Vue | 3 |
|
|
33
|
+
|
|
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.
|
|
23
35
|
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
The plugin itself:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
24
41
|
npm install -g @zachacious/protoc-gen-connect-vue
|
|
25
42
|
```
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
# install dependencies
|
|
44
|
+
Runtime dependencies in your Vue app:
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
```bash
|
|
47
|
+
npm install @tanstack/vue-query @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf
|
|
31
48
|
```
|
|
32
49
|
|
|
33
|
-
|
|
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`).
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
## Generate
|
|
36
53
|
|
|
37
|
-
```
|
|
54
|
+
```yaml
|
|
38
55
|
plugins:
|
|
39
|
-
# ...
|
|
56
|
+
# ...
|
|
40
57
|
- remote: buf.build/bufbuild/es
|
|
41
58
|
out: web/src/api/gen
|
|
42
59
|
opt:
|
|
43
60
|
- target=ts
|
|
44
61
|
|
|
45
|
-
#
|
|
46
|
-
# It should probably go last
|
|
47
|
-
|
|
48
|
-
# npm install -g @zachacious/protoc-gen-connect-vue
|
|
49
|
-
# https://www.npmjs.com/package/@zachacious/protoc-gen-connect-vue
|
|
62
|
+
# Must run after protoc-gen-es. Last is a safe place for it.
|
|
50
63
|
- local: protoc-gen-connect-vue
|
|
51
64
|
out: web/src/api
|
|
52
65
|
opt: target=ts
|
|
53
66
|
```
|
|
54
67
|
|
|
55
|
-
|
|
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.
|
|
56
77
|
|
|
57
|
-
|
|
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.
|
|
58
79
|
|
|
59
|
-
|
|
80
|
+
**Streaming.** Only unary methods are supported. Streaming methods are classified as mutations and the generated hook will not work on them.
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
**Services.** Only the first service found in the schema is generated. A multi-service proto needs a separate buf plugin invocation per service.
|
|
62
83
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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";
|
|
67
94
|
|
|
68
95
|
const app = createApp(App);
|
|
69
96
|
const queryClient = new QueryClient(globalQueryConfig);
|
|
70
97
|
|
|
71
98
|
app.use(VueQueryPlugin, { queryClient });
|
|
72
|
-
app.mount(
|
|
99
|
+
app.mount("#app");
|
|
73
100
|
```
|
|
74
101
|
|
|
75
|
-
|
|
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.
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
### 2. Runtime configuration
|
|
78
105
|
|
|
79
|
-
|
|
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`.
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
```ts
|
|
109
|
+
import { setBaseUrl, setAuthResolver, setSDKErrorCallback } from "@/api";
|
|
82
110
|
|
|
83
|
-
setBaseUrl(
|
|
111
|
+
setBaseUrl("https://api.example.com");
|
|
84
112
|
|
|
85
|
-
//
|
|
86
|
-
setAuthResolver(async () =>
|
|
87
|
-
const token = localStorage.getItem('token');
|
|
88
|
-
return token ? token : null;
|
|
89
|
-
});
|
|
113
|
+
// Called on every request. Sync or async.
|
|
114
|
+
setAuthResolver(async () => localStorage.getItem("token"));
|
|
90
115
|
|
|
91
116
|
setSDKErrorCallback((err, url) => {
|
|
92
|
-
|
|
117
|
+
console.error(`API error at ${url}: ${err.message}`);
|
|
93
118
|
});
|
|
94
119
|
```
|
|
95
120
|
|
|
96
|
-
|
|
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.
|
|
122
|
+
|
|
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.
|
|
124
|
+
|
|
125
|
+
### 3. Cookies and credentials
|
|
126
|
+
|
|
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:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { setFetchCredentials } from "@/api";
|
|
131
|
+
|
|
132
|
+
// "include" (default) | "same-origin" | "omit"
|
|
133
|
+
setFetchCredentials("same-origin");
|
|
134
|
+
```
|
|
135
|
+
|
|
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
|
|
141
|
+
|
|
142
|
+
`useApi()` calls `useQueryClient()` internally, so it has to run inside `setup()` with `VueQueryPlugin` installed.
|
|
97
143
|
|
|
98
|
-
###
|
|
144
|
+
### Queries
|
|
99
145
|
|
|
100
|
-
|
|
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.
|
|
101
147
|
|
|
102
|
-
```
|
|
148
|
+
```vue
|
|
103
149
|
<script setup lang="ts">
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// 2. Manual Action
|
|
114
|
-
const handleUpdate = async () => {
|
|
115
|
-
const { data, error } = await updateUser({ id: "123", name: "New Name" });
|
|
116
|
-
if (!error) {
|
|
117
|
-
// Note: The SDK automatically invalidates related queries on success
|
|
118
|
-
console.log("Update successful");
|
|
119
|
-
}
|
|
120
|
-
};
|
|
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
|
+
);
|
|
121
159
|
</script>
|
|
122
160
|
|
|
123
161
|
<template>
|
|
124
|
-
<div v-if="isLoading">Loading
|
|
162
|
+
<div v-if="isLoading">Loading…</div>
|
|
125
163
|
<div v-else-if="error">{{ error }}</div>
|
|
126
|
-
<
|
|
127
|
-
<h1>{{ data?.name }}</h1>
|
|
128
|
-
<button @click="handleUpdate">Update Profile</button>
|
|
129
|
-
</div>
|
|
164
|
+
<h1 v-else>{{ data?.name }}</h1>
|
|
130
165
|
</template>
|
|
131
166
|
```
|
|
132
167
|
|
|
133
|
-
|
|
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
|
+
```
|
|
199
|
+
|
|
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.
|
|
134
201
|
|
|
135
|
-
|
|
202
|
+
### Infinite queries
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const { useListUsersInfinite } = useApi();
|
|
206
|
+
|
|
207
|
+
const { data, fetchNextPage, hasNextPage } = useListUsersInfinite({ limit: 20 });
|
|
208
|
+
```
|
|
136
209
|
|
|
137
|
-
|
|
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.
|
|
138
211
|
|
|
139
|
-
|
|
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";
|
|
140
218
|
|
|
141
219
|
const { createEmpty } = useApi();
|
|
142
220
|
|
|
143
|
-
// Create an empty Customer object with all Protobuf defaults
|
|
144
|
-
// Useful when you need to create and empty instance instead of using a nullable object
|
|
145
221
|
const newCustomer = ref(createEmpty.Customer({ name: "Initial Name" }));
|
|
146
222
|
```
|
|
147
223
|
|
|
148
|
-
###
|
|
149
|
-
|
|
150
|
-
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
|
|
151
225
|
|
|
152
|
-
|
|
226
|
+
`queryKeys` produces `[resource, functionName, input]`. Since TanStack matches on key prefixes, invalidating a resource is the reliable move:
|
|
153
227
|
|
|
154
|
-
|
|
155
|
-
import {
|
|
228
|
+
```ts
|
|
229
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
230
|
+
import { queryKeys } from "@/api";
|
|
156
231
|
|
|
157
232
|
const queryClient = useQueryClient();
|
|
158
233
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
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" }) });
|
|
164
239
|
```
|
|
165
240
|
|
|
166
|
-
|
|
241
|
+
`queryKeys` and `createEmpty` are exported at module scope as well as from `useApi()`, so you can use them outside components.
|
|
167
242
|
|
|
168
|
-
|
|
243
|
+
### Global loading state
|
|
169
244
|
|
|
170
|
-
|
|
245
|
+
`isGlobalLoading` is a computed that's true while any query or mutation is in flight. Handy for a top-level progress bar.
|
|
171
246
|
|
|
247
|
+
```ts
|
|
172
248
|
const { isGlobalLoading } = useApi();
|
|
173
249
|
```
|
|
174
250
|
|
|
175
|
-
|
|
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
|
|
176
261
|
|
|
177
|
-
|
|
262
|
+
MIT
|
package/dist/generator.js
CHANGED
|
@@ -77276,7 +77276,7 @@ ${lanes.join(`
|
|
|
77276
77276
|
if (!links2.hasReportedStatementInAmbientContext) {
|
|
77277
77277
|
return links2.hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, Diagnostics.Statements_are_not_allowed_in_ambient_contexts);
|
|
77278
77278
|
}
|
|
77279
|
-
}
|
|
77279
|
+
}
|
|
77280
77280
|
}
|
|
77281
77281
|
return false;
|
|
77282
77282
|
}
|
|
@@ -86926,7 +86926,7 @@ ${lanes.join(`
|
|
|
86926
86926
|
], 2));
|
|
86927
86927
|
setParentRecursive(requireStatement, false);
|
|
86928
86928
|
statements = insertStatementAfterCustomPrologue(statements.slice(), requireStatement);
|
|
86929
|
-
}
|
|
86929
|
+
}
|
|
86930
86930
|
}
|
|
86931
86931
|
}
|
|
86932
86932
|
if (statements !== visited.statements) {
|
|
@@ -246201,7 +246201,7 @@ ${lanes.join(`
|
|
|
246201
246201
|
if (!links2.hasReportedStatementInAmbientContext) {
|
|
246202
246202
|
return links2.hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, Diagnostics.Statements_are_not_allowed_in_ambient_contexts);
|
|
246203
246203
|
}
|
|
246204
|
-
}
|
|
246204
|
+
}
|
|
246205
246205
|
}
|
|
246206
246206
|
return false;
|
|
246207
246207
|
}
|
|
@@ -255665,7 +255665,7 @@ ${lanes.join(`
|
|
|
255665
255665
|
], 2));
|
|
255666
255666
|
setParentRecursive(requireStatement, false);
|
|
255667
255667
|
statements = insertStatementAfterCustomPrologue(statements.slice(), requireStatement);
|
|
255668
|
-
}
|
|
255668
|
+
}
|
|
255669
255669
|
}
|
|
255670
255670
|
}
|
|
255671
255671
|
if (statements !== visited.statements) {
|
|
@@ -336813,6 +336813,7 @@ var KNOWN_WKT = [
|
|
|
336813
336813
|
];
|
|
336814
336814
|
var currentDir = new URL(".", import.meta.url).pathname;
|
|
336815
336815
|
var templateDir = path.join(currentDir, "..", "templates");
|
|
336816
|
+
var pkg = JSON.parse(fs.readFileSync(path.join(currentDir, "..", "package.json"), "utf-8"));
|
|
336816
336817
|
var templates = {
|
|
336817
336818
|
client: fs.readFileSync(path.join(templateDir, "client.ts.mustache"), "utf-8"),
|
|
336818
336819
|
api: fs.readFileSync(path.join(templateDir, "api.ts.mustache"), "utf-8"),
|
|
@@ -336922,7 +336923,7 @@ function processService(service) {
|
|
|
336922
336923
|
}
|
|
336923
336924
|
var plugin = createEcmaScriptPlugin({
|
|
336924
336925
|
name: "protoc-gen-connect-vue",
|
|
336925
|
-
version:
|
|
336926
|
+
version: `v${pkg.version}`,
|
|
336926
336927
|
generateTs: (schema) => {
|
|
336927
336928
|
const service = schema.files.flatMap((f) => f.services)[0];
|
|
336928
336929
|
if (!service)
|
package/package.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zachacious/protoc-gen-connect-vue",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
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",
|
|
@@ -32,6 +32,23 @@ export type ErrorCallback = (error: ConnectError, url: string) => void;
|
|
|
32
32
|
let onError: ErrorCallback = () => {};
|
|
33
33
|
export const setSDKErrorCallback = (cb: ErrorCallback) => { onError = cb; };
|
|
34
34
|
|
|
35
|
+
// --- FETCH CREDENTIALS ---
|
|
36
|
+
// connect-web v1 exposed a `credentials` option on the transport; v2 removed it
|
|
37
|
+
// in favor of overriding `fetch`. Wrapping fetch works on both major versions.
|
|
38
|
+
const credentialsMode = ref<RequestCredentials>("include");
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set the fetch credentials mode ("include" | "same-origin" | "omit").
|
|
42
|
+
* Defaults to "include" so cookie-based auth works cross-origin.
|
|
43
|
+
* Takes effect on the next request; no transport rebuild required.
|
|
44
|
+
*/
|
|
45
|
+
export const setFetchCredentials = (mode: RequestCredentials) => {
|
|
46
|
+
credentialsMode.value = mode;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const fetchWithCredentials: typeof globalThis.fetch = (input, init) =>
|
|
50
|
+
globalThis.fetch(input, { ...init, credentials: credentialsMode.value });
|
|
51
|
+
|
|
35
52
|
const transportInterceptor: Interceptor = (next) => async (req) => {
|
|
36
53
|
const token = await resolveToken();
|
|
37
54
|
if (token) req.header.set("x-api-key", token);
|
|
@@ -54,7 +71,7 @@ export const useGrpcClient = () => {
|
|
|
54
71
|
const transport = computed(() => createConnectTransport({
|
|
55
72
|
baseUrl: baseUrl.value,
|
|
56
73
|
interceptors: [transportInterceptor],
|
|
57
|
-
|
|
74
|
+
fetch: fetchWithCredentials,
|
|
58
75
|
}));
|
|
59
76
|
|
|
60
77
|
const client = computed(() => createClient({{serviceName}}, transport.value));
|
|
@@ -103,6 +120,11 @@ async function handleUpdate() {
|
|
|
103
120
|
<p>{{ manualResult }}</p>
|
|
104
121
|
</template>
|
|
105
122
|
|
|
123
|
+
// Cookies are sent cross-origin by default (credentials: "include").
|
|
124
|
+
// If your backend's CORS config doesn't allow credentials, opt out with:
|
|
125
|
+
// import { setFetchCredentials } from '@/api';
|
|
126
|
+
// setFetchCredentials("same-origin");
|
|
127
|
+
|
|
106
128
|
// use this to set the error interceptor - for instance to log to sentry or something
|
|
107
129
|
import { setAuthResolver, setSDKErrorCallback } from '@/api';
|
|
108
130
|
import { useAuthStore } from '@/stores/auth';
|