@wynn-dev/better-fetch-rpc 0.0.0
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/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/index.cjs +64 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +56 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nguyên
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
<a id="readme-top"></a>
|
|
2
|
+
|
|
3
|
+
<!-- PROJECT SHIELDS -->
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/better-fetch-rpc)
|
|
6
|
+
[](https://www.npmjs.com/package/better-fetch-rpc)
|
|
7
|
+
[](https://github.com/nbnguyen75/better-fetch-rpc/graphs/contributors)
|
|
8
|
+
[](https://github.com/nbnguyen75/better-fetch-rpc/network/members)
|
|
9
|
+
[](https://github.com/nbnguyen75/better-fetch-rpc/stargazers)
|
|
10
|
+
[](https://github.com/nbnguyen75/better-fetch-rpc/issues)
|
|
11
|
+
[](https://github.com/nbnguyen75/better-fetch-rpc/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
<!-- PROJECT LOGO -->
|
|
14
|
+
<br />
|
|
15
|
+
<div align="center">
|
|
16
|
+
<h3 align="center">better-fetch-rpc</h3>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
A type-safe, RPC-style wrapper around <code>fetch</code> — call your API like a local function, with full type inference.
|
|
20
|
+
<br />
|
|
21
|
+
<a href="https://github.com/nbnguyen75/better-fetch-rpc"><strong>Explore the docs »</strong></a>
|
|
22
|
+
<br />
|
|
23
|
+
<br />
|
|
24
|
+
<a href="https://github.com/nbnguyen75/better-fetch-rpc/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
|
25
|
+
·
|
|
26
|
+
<a href="https://github.com/nbnguyen75/better-fetch-rpc/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- TABLE OF CONTENTS -->
|
|
31
|
+
<details>
|
|
32
|
+
<summary>Table of Contents</summary>
|
|
33
|
+
<ol>
|
|
34
|
+
<li>
|
|
35
|
+
<a href="#about-the-project">About The Project</a>
|
|
36
|
+
<ul>
|
|
37
|
+
<li><a href="#built-with">Built With</a></li>
|
|
38
|
+
</ul>
|
|
39
|
+
</li>
|
|
40
|
+
<li>
|
|
41
|
+
<a href="#getting-started">Getting Started</a>
|
|
42
|
+
<ul>
|
|
43
|
+
<li><a href="#prerequisites">Prerequisites</a></li>
|
|
44
|
+
<li><a href="#installation">Installation</a></li>
|
|
45
|
+
</ul>
|
|
46
|
+
</li>
|
|
47
|
+
<li>
|
|
48
|
+
<a href="#usage">Usage</a>
|
|
49
|
+
<ul>
|
|
50
|
+
<li><a href="#1-define-your-routes">Define your routes</a></li>
|
|
51
|
+
<li><a href="#2-create-the-client-and-call-it">Create the client and call it</a></li>
|
|
52
|
+
<li><a href="#response-shapes">Response shapes</a></li>
|
|
53
|
+
<li><a href="#typing-the-error-channel">Typing the error channel</a></li>
|
|
54
|
+
<li><a href="#reusing-endpoint-types">Reusing endpoint types</a></li>
|
|
55
|
+
<li><a href="#client-options">Client options</a></li>
|
|
56
|
+
<li><a href="#schema-compatibility">Schema compatibility</a></li>
|
|
57
|
+
<li><a href="#runtime-response-validation">Runtime response validation</a></li>
|
|
58
|
+
<li><a href="#tanstack-query">TanStack Query</a></li>
|
|
59
|
+
</ul>
|
|
60
|
+
</li>
|
|
61
|
+
<li><a href="#api-reference">API Reference</a></li>
|
|
62
|
+
<li><a href="#roadmap">Roadmap</a></li>
|
|
63
|
+
<li><a href="#contributing">Contributing</a></li>
|
|
64
|
+
<li><a href="#license">License</a></li>
|
|
65
|
+
<li><a href="#contact">Contact</a></li>
|
|
66
|
+
</ol>
|
|
67
|
+
</details>
|
|
68
|
+
|
|
69
|
+
<!-- ABOUT THE PROJECT -->
|
|
70
|
+
|
|
71
|
+
## About The Project
|
|
72
|
+
|
|
73
|
+
**better-fetch-rpc** is a type-safe RPC-style wrapper around `fetch`. Instead of hand-writing URL strings, headers, and response parsing for every request, you define your API routes once and call them like local functions — with types inferred end-to-end.
|
|
74
|
+
|
|
75
|
+
Schema types (zod, valibot, arktype, …) are understood through [Standard Schema](https://standardschema.dev) and can optionally validate responses at runtime. The package itself has zero runtime dependencies besides its `@better-fetch/fetch` peer.
|
|
76
|
+
|
|
77
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
78
|
+
|
|
79
|
+
### Built With
|
|
80
|
+
|
|
81
|
+
- [![TypeScript][TypeScript-badge]][TypeScript-url]
|
|
82
|
+
- [![Node.js][Node-badge]][Node-url]
|
|
83
|
+
- [better-fetch](https://github.com/better-auth/better-fetch) — fetch engine with native Standard Schema validation
|
|
84
|
+
|
|
85
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
86
|
+
|
|
87
|
+
<!-- GETTING STARTED -->
|
|
88
|
+
|
|
89
|
+
## Getting Started
|
|
90
|
+
|
|
91
|
+
### Prerequisites
|
|
92
|
+
|
|
93
|
+
- Node.js ≥ 18
|
|
94
|
+
|
|
95
|
+
### Installation
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npm install better-fetch-rpc
|
|
99
|
+
# or
|
|
100
|
+
pnpm add better-fetch-rpc
|
|
101
|
+
# or
|
|
102
|
+
yarn add better-fetch-rpc
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`@better-fetch/fetch` is a peer dependency and is installed automatically by
|
|
106
|
+
npm, pnpm, and yarn. Runtime response validation needs a peer of at least
|
|
107
|
+
`v1.1.21` (the first version with Standard Schema `output` support). A schema
|
|
108
|
+
library (zod, valibot, …) is only needed if you use runtime response
|
|
109
|
+
validation.
|
|
110
|
+
|
|
111
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
112
|
+
|
|
113
|
+
<!-- USAGE EXAMPLES -->
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
### 1. Define your routes
|
|
118
|
+
|
|
119
|
+
A router maps paths to HTTP methods (`$get`, `$post`, `$put`, `$patch`,
|
|
120
|
+
`$delete`). Each endpoint declares its `headers`, `params`, `query`, `body`,
|
|
121
|
+
and `response` — as plain TypeScript types or as schemas from any
|
|
122
|
+
Standard Schema library:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import type { EnsureRouter } from 'better-fetch-rpc';
|
|
126
|
+
|
|
127
|
+
import { z } from 'zod';
|
|
128
|
+
|
|
129
|
+
const noteSchema = z.object({ id: z.string(), title: z.string() });
|
|
130
|
+
|
|
131
|
+
type Note = z.infer<typeof noteSchema>;
|
|
132
|
+
type ApiSuccessResponse<T> = { success: true; data: T };
|
|
133
|
+
|
|
134
|
+
type Router = EnsureRouter<{
|
|
135
|
+
'/api/v1/notes': {
|
|
136
|
+
$get: {
|
|
137
|
+
response: ApiSuccessResponse<Note[]>;
|
|
138
|
+
query?: { limit?: number } | undefined;
|
|
139
|
+
};
|
|
140
|
+
$post: {
|
|
141
|
+
response: ApiSuccessResponse<Note>;
|
|
142
|
+
body: { title: string };
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
'/api/v1/notes/:id': {
|
|
146
|
+
$get: {
|
|
147
|
+
response: typeof noteSchema;
|
|
148
|
+
params: { id: string };
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
}>;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Endpoints that declare nothing — or only optional fields — accept zero
|
|
155
|
+
arguments; required fields must be passed.
|
|
156
|
+
|
|
157
|
+
### 2. Create the client and call it
|
|
158
|
+
|
|
159
|
+
Path segments become properties. `:id`-style segments are indexed with the
|
|
160
|
+
literal key and filled from `params` (which better-fetch also substitutes
|
|
161
|
+
into the URL):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { createRpcClient } from 'better-fetch-rpc';
|
|
165
|
+
|
|
166
|
+
const api = createRpcClient<Router>('https://api.example.com');
|
|
167
|
+
|
|
168
|
+
// Fully typed request + response
|
|
169
|
+
const { data, error } = await api.api.v1.notes[':id'].$get({
|
|
170
|
+
params: { id: '123' },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (error) {
|
|
174
|
+
console.error(error);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(data); // typed from the route definition
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Response shapes
|
|
181
|
+
|
|
182
|
+
By default every call resolves to `{ data, error }` — exactly one of them is
|
|
183
|
+
non-null. Pass `throw: true` to receive the response data directly and let
|
|
184
|
+
transport failures reject instead:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const throwing = createRpcClient<Router>('https://api.example.com', { throw: true });
|
|
188
|
+
|
|
189
|
+
const notes = await throwing.api.v1.notes.$get({ query: { limit: 10 } });
|
|
190
|
+
// ^ typed as the $get response (no envelope)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Typing the error channel
|
|
194
|
+
|
|
195
|
+
Pass your server's error shape as the second generic so `error` is typed:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
type ApiError = { errorCode: string; message: string };
|
|
199
|
+
|
|
200
|
+
const api = createRpcClient<Router, ApiError>('https://api.example.com');
|
|
201
|
+
|
|
202
|
+
const { error } = await api.api.v1.notes.$get();
|
|
203
|
+
if (error) {
|
|
204
|
+
console.error(error.errorCode); // string
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Reusing endpoint types
|
|
209
|
+
|
|
210
|
+
`InferRequestType` / `InferResponseType` extract an endpoint's options and
|
|
211
|
+
resolved value — handy for wrapping calls in your own functions:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import type { InferRequestType, InferResponseType } from 'better-fetch-rpc';
|
|
215
|
+
|
|
216
|
+
type GetNotesRequest = InferRequestType<typeof api.api.v1.notes.$get>;
|
|
217
|
+
type GetNotesResponse = InferResponseType<typeof api.api.v1.notes.$get>['data'];
|
|
218
|
+
|
|
219
|
+
export async function getNotes(args: GetNotesRequest): Promise<GetNotesResponse> {
|
|
220
|
+
const result = await api.api.v1.notes.$get(args);
|
|
221
|
+
if (result.error) throw new Error('Request failed');
|
|
222
|
+
return result.data;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Client options
|
|
227
|
+
|
|
228
|
+
The second argument accepts everything
|
|
229
|
+
[`@better-fetch/fetch`](https://github.com/better-auth/better-fetch) accepts
|
|
230
|
+
(`auth`, `headers`, `retry`, `timeout`, …) — except `baseURL`/`body`, which
|
|
231
|
+
the client owns — plus `schemas` (see below):
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const api = createRpcClient<Router>('https://api.example.com', {
|
|
235
|
+
auth: { token: () => getToken(), type: 'Bearer' },
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Schema compatibility
|
|
240
|
+
|
|
241
|
+
Request/response types are inferred through [Standard Schema](https://standardschema.dev),
|
|
242
|
+
so any compliant library works — no hard dependency on a specific one:
|
|
243
|
+
|
|
244
|
+
- zod ≥ 3.24
|
|
245
|
+
- valibot ≥ 1.0
|
|
246
|
+
- arktype ≥ 2.0
|
|
247
|
+
- …anything exposing `~standard`
|
|
248
|
+
|
|
249
|
+
Plain TypeScript types work too — schemas are optional, not required.
|
|
250
|
+
|
|
251
|
+
### Runtime response validation
|
|
252
|
+
|
|
253
|
+
Types alone can't verify what the server actually sends. To validate responses
|
|
254
|
+
at runtime, define routes as a const once — feeding both the router type and
|
|
255
|
+
the runtime schemas — and pass it via the `schemas` option:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { z } from 'zod';
|
|
259
|
+
import { ValidationError, createRpcClient } from 'better-fetch-rpc';
|
|
260
|
+
|
|
261
|
+
const noteSchema = z.object({ id: z.string(), title: z.string() });
|
|
262
|
+
|
|
263
|
+
const routes = {
|
|
264
|
+
'/api/v1/notes/:id': {
|
|
265
|
+
$get: { response: noteSchema },
|
|
266
|
+
},
|
|
267
|
+
} as const;
|
|
268
|
+
|
|
269
|
+
type Router = EnsureRouter<typeof routes>;
|
|
270
|
+
|
|
271
|
+
const api = createRpcClient<Router>('https://api.example.com', { schemas: routes });
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const { data, error } = await api.api.v1.notes[':id'].$get({ params: { id: '123' } });
|
|
275
|
+
if (error) {
|
|
276
|
+
console.error('Request failed:', error);
|
|
277
|
+
} else {
|
|
278
|
+
console.log(data.title); // validated: guaranteed to match noteSchema
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error instanceof ValidationError) {
|
|
282
|
+
console.error('Server broke the contract:', error.issues);
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Rules:
|
|
289
|
+
|
|
290
|
+
- Only routes with a runtime schema are validated; everything else passes
|
|
291
|
+
through untouched (inference-only, zero cost).
|
|
292
|
+
- Validation failures **always throw** `ValidationError` (re-exported for
|
|
293
|
+
convenience) — in both `throw: true` and default modes, just like network
|
|
294
|
+
errors. The `{ data, error }` channel is reserved for server responses.
|
|
295
|
+
- Schemas declared as bare types (no runtime instance) are inference-only;
|
|
296
|
+
only `typeof mySchema` entries paired with `schemas` can be validated.
|
|
297
|
+
|
|
298
|
+
### TanStack Query
|
|
299
|
+
|
|
300
|
+
The `throw: true` client pairs naturally with TanStack Query v5: transport and
|
|
301
|
+
validation failures reject, so they land in Query's `error` / `onError`
|
|
302
|
+
channel with no envelope unwrapping. Define `queryOptions` factories once,
|
|
303
|
+
then consume them in hooks:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
307
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
308
|
+
|
|
309
|
+
const throwing = createRpcClient<Router>('https://api.example.com', { throw: true });
|
|
310
|
+
|
|
311
|
+
export const noteKeys = {
|
|
312
|
+
all: ['notes'] as const,
|
|
313
|
+
list: (params?: { limit?: number }) => [...noteKeys.all, 'list', params] as const,
|
|
314
|
+
detail: (id: string) => [...noteKeys.all, 'detail', id] as const,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export function noteDetailQueryOptions(id: string) {
|
|
318
|
+
return queryOptions({
|
|
319
|
+
queryFn: () => throwing.api.v1.notes[':id'].$get({ params: { id } }),
|
|
320
|
+
queryKey: noteKeys.detail(id),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function useNote(id: string) {
|
|
325
|
+
return useQuery(noteDetailQueryOptions(id));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function useCreateNote() {
|
|
329
|
+
const queryClient = useQueryClient();
|
|
330
|
+
return useMutation({
|
|
331
|
+
mutationFn: (title: string) => throwing.api.v1.notes.$post({ body: { title } }),
|
|
332
|
+
onSuccess: () => {
|
|
333
|
+
void queryClient.invalidateQueries({ queryKey: noteKeys.all });
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`ValidationError` from a validated route surfaces the same way — as the
|
|
340
|
+
query/mutation `error` — because validation failures always throw.
|
|
341
|
+
|
|
342
|
+
_For more examples and the full API reference, please refer to the [Documentation](https://github.com/nbnguyen75/better-fetch-rpc)._
|
|
343
|
+
|
|
344
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
345
|
+
|
|
346
|
+
<!-- API REFERENCE -->
|
|
347
|
+
|
|
348
|
+
## API Reference
|
|
349
|
+
|
|
350
|
+
### Values
|
|
351
|
+
|
|
352
|
+
| Export | Description |
|
|
353
|
+
| ------------------ | --------------------------------------------------------------------------------------------- |
|
|
354
|
+
| `createRpcClient` | `createRpcClient<Router, Error = unknown>(baseURL?, options?)` — builds the typed RPC client. |
|
|
355
|
+
| `ValidationError` | Re-exported from `@better-fetch/fetch`. Thrown on response validation failure; has `.issues`. |
|
|
356
|
+
| `isStandardSchema` | Type guard for Standard Schema instances. |
|
|
357
|
+
|
|
358
|
+
### Types
|
|
359
|
+
|
|
360
|
+
| Export | Description |
|
|
361
|
+
| ----------------------- | ----------------------------------------------------------------------- |
|
|
362
|
+
| `EnsureRouter<T>` | Constrains a route map to the router shape. |
|
|
363
|
+
| `BaseRouter` | `Record<string, MethodMap>` — the base constraint for routers. |
|
|
364
|
+
| `EndpointDef` | One endpoint: `headers` / `params` / `query` / `body` / `response`. |
|
|
365
|
+
| `MethodMap` | `$get` / `$post` / `$put` / `$patch` / `$delete` endpoint map. |
|
|
366
|
+
| `ProxyTree<R, T, E>` | The client type derived from a router. |
|
|
367
|
+
| `RpcResponse<D, E>` | `{ data: D; error: null } \| { data: null; error: E }`. |
|
|
368
|
+
| `RpcSchemas<R>` | Runtime schemas mirror for the `schemas` option. |
|
|
369
|
+
| `RouteSchemas` | Per-endpoint schemas: `{ response?: StandardSchemaV1 }`. |
|
|
370
|
+
| `CreateRpcClientOption` | Client options (better-fetch options + `schemas`). |
|
|
371
|
+
| `RequestOptions` | Per-call options: `headers` / `params` / `query` / `body`. |
|
|
372
|
+
| `HttpMethod` | `'DELETE' \| 'PATCH' \| 'POST' \| 'GET' \| 'PUT'`. |
|
|
373
|
+
| `InferRequestType<F>` | Extracts an endpoint function's options type. |
|
|
374
|
+
| `InferResponseType<F>` | Extracts an endpoint function's resolved value type. |
|
|
375
|
+
| `StandardSchemaV1` | Vendored Standard Schema interface (no dependency needed to reference). |
|
|
376
|
+
| `InferStandardInput` | Infer a schema's input type. |
|
|
377
|
+
| `InferStandardOutput` | Infer a schema's output type. |
|
|
378
|
+
|
|
379
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
380
|
+
|
|
381
|
+
<!-- ROADMAP -->
|
|
382
|
+
|
|
383
|
+
## Roadmap
|
|
384
|
+
|
|
385
|
+
- [x] Core RPC client (GET/POST/PUT/PATCH/DELETE)
|
|
386
|
+
- [x] Opt-in Standard Schema runtime response validation
|
|
387
|
+
- [ ] Request/response interceptors
|
|
388
|
+
- [ ] Built-in retry & timeout handling
|
|
389
|
+
|
|
390
|
+
See the [open issues](https://github.com/nbnguyen75/better-fetch-rpc/issues) for a full list of proposed features and known issues.
|
|
391
|
+
|
|
392
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
393
|
+
|
|
394
|
+
<!-- CONTRIBUTING -->
|
|
395
|
+
|
|
396
|
+
## Contributing
|
|
397
|
+
|
|
398
|
+
Contributions make the open source community amazing. Any contributions you make are **greatly appreciated**.
|
|
399
|
+
|
|
400
|
+
If you have a suggestion, fork the repo and open a pull request, or open an issue with the tag "enhancement". Don't forget to star the project!
|
|
401
|
+
|
|
402
|
+
1. Fork the Project
|
|
403
|
+
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
404
|
+
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
405
|
+
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
406
|
+
5. Open a Pull Request
|
|
407
|
+
|
|
408
|
+
### Changesets
|
|
409
|
+
|
|
410
|
+
Every PR that should trigger a release must include a changeset: run
|
|
411
|
+
`pnpm changeset` and commit the generated file. The release workflow turns
|
|
412
|
+
them into version bumps and `CHANGELOG.md` entries automatically.
|
|
413
|
+
|
|
414
|
+
### Package managers
|
|
415
|
+
|
|
416
|
+
Use **pnpm** or **bun** — never npm. After any dependency change, run both
|
|
417
|
+
`pnpm install` and `bun install` so `pnpm-lock.yaml` and `bun.lock` stay in
|
|
418
|
+
sync. Never mix managers in one `node_modules`: switching means deleting
|
|
419
|
+
`node_modules` and reinstalling. CI runs pnpm only, and `pnpm test` is the
|
|
420
|
+
source of truth (`bun run test` executes vitest under Bun, which is
|
|
421
|
+
unsupported — use Bun for `bun dist/index.js` smoke runs instead).
|
|
422
|
+
|
|
423
|
+
### Emergency local publish
|
|
424
|
+
|
|
425
|
+
Releases normally ship from CI via OIDC trusted publishing. If CI is
|
|
426
|
+
unavailable, `pnpm publish` works locally with an automation token, but the
|
|
427
|
+
release will lack a provenance attestation — prefer CI.
|
|
428
|
+
|
|
429
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
430
|
+
|
|
431
|
+
<!-- LICENSE -->
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
436
|
+
|
|
437
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
438
|
+
|
|
439
|
+
<!-- CONTACT -->
|
|
440
|
+
|
|
441
|
+
## Contact
|
|
442
|
+
|
|
443
|
+
Nguyên (Wynn) - [GitHub @nbnguyen75](https://github.com/nbnguyen75)
|
|
444
|
+
|
|
445
|
+
Project Link: [https://github.com/nbnguyen75/better-fetch-rpc](https://github.com/nbnguyen75/better-fetch-rpc)
|
|
446
|
+
|
|
447
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
448
|
+
|
|
449
|
+
<!-- MARKDOWN LINKS & IMAGES -->
|
|
450
|
+
|
|
451
|
+
[TypeScript-badge]: https://img.shields.io/badge/typescript-3178C6?style=for-the-badge&logo=typescript&logoColor=white
|
|
452
|
+
[TypeScript-url]: https://www.typescriptlang.org/
|
|
453
|
+
[Node-badge]: https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=node.js&logoColor=white
|
|
454
|
+
[Node-url]: https://nodejs.org/
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _better_fetch_fetch = require("@better-fetch/fetch");
|
|
3
|
+
//#region src/standard-schema.ts
|
|
4
|
+
function isStandardSchema(value) {
|
|
5
|
+
if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
|
|
6
|
+
const standard = value["~standard"];
|
|
7
|
+
return typeof standard === "object" && standard !== null;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/rpc.ts
|
|
11
|
+
const METHOD_KEY_MAP = {
|
|
12
|
+
$delete: "DELETE",
|
|
13
|
+
$patch: "PATCH",
|
|
14
|
+
$post: "POST",
|
|
15
|
+
$get: "GET",
|
|
16
|
+
$put: "PUT"
|
|
17
|
+
};
|
|
18
|
+
const METHOD_TO_KEY = {
|
|
19
|
+
DELETE: "$delete",
|
|
20
|
+
GET: "$get",
|
|
21
|
+
PATCH: "$patch",
|
|
22
|
+
POST: "$post",
|
|
23
|
+
PUT: "$put"
|
|
24
|
+
};
|
|
25
|
+
function createProxyClient(makeRequest, segments = []) {
|
|
26
|
+
return new Proxy(() => {}, { get(_target, prop) {
|
|
27
|
+
if (prop in METHOD_KEY_MAP) {
|
|
28
|
+
const method = METHOD_KEY_MAP[prop];
|
|
29
|
+
if (!method) return void 0;
|
|
30
|
+
const path = "/" + segments.join("/");
|
|
31
|
+
return (options) => makeRequest(method, path, options);
|
|
32
|
+
}
|
|
33
|
+
return createProxyClient(makeRequest, [...segments, prop]);
|
|
34
|
+
} });
|
|
35
|
+
}
|
|
36
|
+
function createRpcClient(baseURL, option = {}) {
|
|
37
|
+
const { schemas, ...fetchOption } = option;
|
|
38
|
+
const $fetchBase = (0, _better_fetch_fetch.createFetch)({
|
|
39
|
+
...baseURL ? { baseURL } : {},
|
|
40
|
+
...fetchOption
|
|
41
|
+
});
|
|
42
|
+
const schemaIndex = schemas;
|
|
43
|
+
const makeRequest = (method, path, options) => {
|
|
44
|
+
const responseSchema = schemaIndex?.[path]?.[METHOD_TO_KEY[method]]?.response;
|
|
45
|
+
return $fetchBase(path, {
|
|
46
|
+
headers: options?.headers,
|
|
47
|
+
params: options?.params,
|
|
48
|
+
query: options?.query,
|
|
49
|
+
body: options?.body,
|
|
50
|
+
method,
|
|
51
|
+
...isStandardSchema(responseSchema) ? { output: responseSchema } : {}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
return createProxyClient(makeRequest);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
Object.defineProperty(exports, "ValidationError", {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
get: function() {
|
|
60
|
+
return _better_fetch_fetch.ValidationError;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
exports.createRpcClient = createRpcClient;
|
|
64
|
+
exports.isStandardSchema = isStandardSchema;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CreateFetchOption, ValidationError } from "@better-fetch/fetch";
|
|
2
|
+
//#region src/standard-schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Standard Schema V1 — vendored type definitions (no runtime code).
|
|
5
|
+
*
|
|
6
|
+
* Source: https://github.com/standard-schema/standard-schema (v1 spec).
|
|
7
|
+
* Vendored so this package stays dependency-free: any schema library that
|
|
8
|
+
* implements the spec (zod ≥ 3.24, valibot ≥ 1.0, arktype ≥ 2.0, …) is
|
|
9
|
+
* structurally compatible without this package depending on it.
|
|
10
|
+
*
|
|
11
|
+
* Only the shapes needed for type inference and (future) response
|
|
12
|
+
* validation are included. Kept flat (no namespaces) to stay compatible
|
|
13
|
+
* with `erasableSyntaxOnly`.
|
|
14
|
+
*/
|
|
15
|
+
interface StandardSchemaPathSegment {
|
|
16
|
+
readonly key: PropertyKey;
|
|
17
|
+
}
|
|
18
|
+
interface StandardSchemaIssue {
|
|
19
|
+
readonly message: string;
|
|
20
|
+
readonly path?: ReadonlyArray<PropertyKey | StandardSchemaPathSegment> | undefined;
|
|
21
|
+
}
|
|
22
|
+
interface StandardSchemaSuccessResult<Output> {
|
|
23
|
+
readonly value: Output;
|
|
24
|
+
readonly issues?: undefined;
|
|
25
|
+
}
|
|
26
|
+
interface StandardSchemaFailureResult {
|
|
27
|
+
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
28
|
+
}
|
|
29
|
+
type StandardSchemaResult<Output> = StandardSchemaFailureResult | StandardSchemaSuccessResult<Output>;
|
|
30
|
+
interface StandardSchemaTypes<Input = unknown, Output = Input> {
|
|
31
|
+
readonly input: Input;
|
|
32
|
+
readonly output: Output;
|
|
33
|
+
}
|
|
34
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
35
|
+
readonly '~standard': {
|
|
36
|
+
readonly version: 1;
|
|
37
|
+
readonly vendor: string;
|
|
38
|
+
readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
39
|
+
readonly types?: StandardSchemaTypes<Input, Output> | undefined;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
type InferStandardInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
|
|
43
|
+
type InferStandardOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
|
|
44
|
+
export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/rpc.d.ts
|
|
47
|
+
type InferSchema<T> = T extends StandardSchemaV1 ? InferStandardOutput<T> : T;
|
|
48
|
+
type EndpointDef = {
|
|
49
|
+
headers?: Record<string, string | undefined> | StandardSchemaV1;
|
|
50
|
+
query?: Record<string, unknown> | StandardSchemaV1 | undefined;
|
|
51
|
+
params?: Record<string, unknown> | StandardSchemaV1;
|
|
52
|
+
response?: unknown;
|
|
53
|
+
body?: unknown;
|
|
54
|
+
};
|
|
55
|
+
type MethodMap = {
|
|
56
|
+
$delete?: EndpointDef;
|
|
57
|
+
$patch?: EndpointDef;
|
|
58
|
+
$post?: EndpointDef;
|
|
59
|
+
$get?: EndpointDef;
|
|
60
|
+
$put?: EndpointDef;
|
|
61
|
+
};
|
|
62
|
+
type InferRequest<T extends EndpointDef> = { [K in keyof T as K extends 'body' | 'headers' | 'params' | 'query' ? T[K] extends never ? never : K : never]: InferSchema<T[K]>; };
|
|
63
|
+
type RequestArgs<Endpoint extends EndpointDef> = Record<string, never> extends InferRequest<Endpoint> ? [options?: InferRequest<Endpoint>] : [options: InferRequest<Endpoint>];
|
|
64
|
+
type RequestOptions = {
|
|
65
|
+
headers?: Record<string, string | undefined>;
|
|
66
|
+
params?: Record<string, unknown>;
|
|
67
|
+
query?: Record<string, unknown>;
|
|
68
|
+
body?: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
type Split<S extends string> = S extends `${infer Head}/${infer Tail}` ? Head extends '' ? Split<Tail> : [Head, ...Split<Tail>] : S extends '' ? [] : [S];
|
|
71
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
72
|
+
type RpcResponse<Data, Error = unknown> = {
|
|
73
|
+
error: Error;
|
|
74
|
+
data: null;
|
|
75
|
+
} | {
|
|
76
|
+
error: null;
|
|
77
|
+
data: Data;
|
|
78
|
+
};
|
|
79
|
+
type MethodClient<Methods extends MethodMap, Throw extends boolean, Error = unknown> = { [M in keyof Methods as Methods[M] extends EndpointDef ? M : never]: Methods[M] extends EndpointDef ? (...args: RequestArgs<Methods[M]>) => Promise<Throw extends true ? InferResponse<Methods[M]> : RpcResponse<InferResponse<Methods[M]>, Error>> : never; };
|
|
80
|
+
type BuildBranch<Segments extends ReadonlyArray<string>, Leaf> = Segments extends readonly [infer Head extends string, ...infer Rest extends ReadonlyArray<string>] ? Rest extends readonly [] ? { [K in Head]: Leaf; } : { [K in Head]: BuildBranch<Rest, Leaf>; } : never;
|
|
81
|
+
type BaseRouter = Record<string, MethodMap>;
|
|
82
|
+
type ProxyTree<Router extends BaseRouter, Throw extends boolean = false, Error = unknown> = UnionToIntersection<{ [Path in keyof Router]: Path extends string ? BuildBranch<Split<Path>, MethodClient<Router[Path], Throw, Error>> : never; }[keyof Router]>;
|
|
83
|
+
type HttpMethod = 'DELETE' | 'PATCH' | 'POST' | 'GET' | 'PUT';
|
|
84
|
+
type InferRequestType<T extends (...args: Array<never>) => unknown> = Parameters<T>[0];
|
|
85
|
+
type InferResponseType<T extends (...args: Array<never>) => unknown> = Awaited<ReturnType<T>>;
|
|
86
|
+
type InferResponse<T extends EndpointDef> = InferSchema<T['response']>;
|
|
87
|
+
type EnsureRouter<T extends BaseRouter> = T;
|
|
88
|
+
type RouteSchemas = {
|
|
89
|
+
response?: StandardSchemaV1 | undefined;
|
|
90
|
+
};
|
|
91
|
+
type RpcSchemas<Router extends BaseRouter> = { [Path in keyof Router]?: { [Method in keyof Router[Path]]?: RouteSchemas | undefined; }; };
|
|
92
|
+
type CreateRpcClientOption<Router extends BaseRouter = BaseRouter> = Omit<CreateFetchOption, 'baseURL' | 'body'> & {
|
|
93
|
+
schemas?: RpcSchemas<Router> | undefined;
|
|
94
|
+
};
|
|
95
|
+
export declare function createRpcClient<Router extends BaseRouter, Error = unknown>(baseURL: undefined | string, option: CreateRpcClientOption<Router> & {
|
|
96
|
+
throw: true;
|
|
97
|
+
}): ProxyTree<Router, true, Error>;
|
|
98
|
+
export declare function createRpcClient<Router extends BaseRouter, Error = unknown>(baseURL?: string, option?: CreateRpcClientOption<Router>): ProxyTree<Router, false, Error>;
|
|
99
|
+
//#endregion
|
|
100
|
+
export { type BaseRouter, type CreateRpcClientOption, type EndpointDef, type EnsureRouter, type HttpMethod, type InferRequestType, type InferResponseType, type InferStandardInput, type InferStandardOutput, type MethodMap, type ProxyTree, type RequestOptions, type RouteSchemas, type RpcResponse, type RpcSchemas, type StandardSchemaFailureResult, type StandardSchemaIssue, type StandardSchemaPathSegment, type StandardSchemaResult, type StandardSchemaSuccessResult, type StandardSchemaTypes, type StandardSchemaV1, ValidationError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CreateFetchOption, ValidationError } from "@better-fetch/fetch";
|
|
2
|
+
//#region src/standard-schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Standard Schema V1 — vendored type definitions (no runtime code).
|
|
5
|
+
*
|
|
6
|
+
* Source: https://github.com/standard-schema/standard-schema (v1 spec).
|
|
7
|
+
* Vendored so this package stays dependency-free: any schema library that
|
|
8
|
+
* implements the spec (zod ≥ 3.24, valibot ≥ 1.0, arktype ≥ 2.0, …) is
|
|
9
|
+
* structurally compatible without this package depending on it.
|
|
10
|
+
*
|
|
11
|
+
* Only the shapes needed for type inference and (future) response
|
|
12
|
+
* validation are included. Kept flat (no namespaces) to stay compatible
|
|
13
|
+
* with `erasableSyntaxOnly`.
|
|
14
|
+
*/
|
|
15
|
+
interface StandardSchemaPathSegment {
|
|
16
|
+
readonly key: PropertyKey;
|
|
17
|
+
}
|
|
18
|
+
interface StandardSchemaIssue {
|
|
19
|
+
readonly message: string;
|
|
20
|
+
readonly path?: ReadonlyArray<PropertyKey | StandardSchemaPathSegment> | undefined;
|
|
21
|
+
}
|
|
22
|
+
interface StandardSchemaSuccessResult<Output> {
|
|
23
|
+
readonly value: Output;
|
|
24
|
+
readonly issues?: undefined;
|
|
25
|
+
}
|
|
26
|
+
interface StandardSchemaFailureResult {
|
|
27
|
+
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
28
|
+
}
|
|
29
|
+
type StandardSchemaResult<Output> = StandardSchemaFailureResult | StandardSchemaSuccessResult<Output>;
|
|
30
|
+
interface StandardSchemaTypes<Input = unknown, Output = Input> {
|
|
31
|
+
readonly input: Input;
|
|
32
|
+
readonly output: Output;
|
|
33
|
+
}
|
|
34
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
35
|
+
readonly '~standard': {
|
|
36
|
+
readonly version: 1;
|
|
37
|
+
readonly vendor: string;
|
|
38
|
+
readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
|
|
39
|
+
readonly types?: StandardSchemaTypes<Input, Output> | undefined;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
type InferStandardInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
|
|
43
|
+
type InferStandardOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
|
|
44
|
+
export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/rpc.d.ts
|
|
47
|
+
type InferSchema<T> = T extends StandardSchemaV1 ? InferStandardOutput<T> : T;
|
|
48
|
+
type EndpointDef = {
|
|
49
|
+
headers?: Record<string, string | undefined> | StandardSchemaV1;
|
|
50
|
+
query?: Record<string, unknown> | StandardSchemaV1 | undefined;
|
|
51
|
+
params?: Record<string, unknown> | StandardSchemaV1;
|
|
52
|
+
response?: unknown;
|
|
53
|
+
body?: unknown;
|
|
54
|
+
};
|
|
55
|
+
type MethodMap = {
|
|
56
|
+
$delete?: EndpointDef;
|
|
57
|
+
$patch?: EndpointDef;
|
|
58
|
+
$post?: EndpointDef;
|
|
59
|
+
$get?: EndpointDef;
|
|
60
|
+
$put?: EndpointDef;
|
|
61
|
+
};
|
|
62
|
+
type InferRequest<T extends EndpointDef> = { [K in keyof T as K extends 'body' | 'headers' | 'params' | 'query' ? T[K] extends never ? never : K : never]: InferSchema<T[K]>; };
|
|
63
|
+
type RequestArgs<Endpoint extends EndpointDef> = Record<string, never> extends InferRequest<Endpoint> ? [options?: InferRequest<Endpoint>] : [options: InferRequest<Endpoint>];
|
|
64
|
+
type RequestOptions = {
|
|
65
|
+
headers?: Record<string, string | undefined>;
|
|
66
|
+
params?: Record<string, unknown>;
|
|
67
|
+
query?: Record<string, unknown>;
|
|
68
|
+
body?: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
type Split<S extends string> = S extends `${infer Head}/${infer Tail}` ? Head extends '' ? Split<Tail> : [Head, ...Split<Tail>] : S extends '' ? [] : [S];
|
|
71
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
72
|
+
type RpcResponse<Data, Error = unknown> = {
|
|
73
|
+
error: Error;
|
|
74
|
+
data: null;
|
|
75
|
+
} | {
|
|
76
|
+
error: null;
|
|
77
|
+
data: Data;
|
|
78
|
+
};
|
|
79
|
+
type MethodClient<Methods extends MethodMap, Throw extends boolean, Error = unknown> = { [M in keyof Methods as Methods[M] extends EndpointDef ? M : never]: Methods[M] extends EndpointDef ? (...args: RequestArgs<Methods[M]>) => Promise<Throw extends true ? InferResponse<Methods[M]> : RpcResponse<InferResponse<Methods[M]>, Error>> : never; };
|
|
80
|
+
type BuildBranch<Segments extends ReadonlyArray<string>, Leaf> = Segments extends readonly [infer Head extends string, ...infer Rest extends ReadonlyArray<string>] ? Rest extends readonly [] ? { [K in Head]: Leaf; } : { [K in Head]: BuildBranch<Rest, Leaf>; } : never;
|
|
81
|
+
type BaseRouter = Record<string, MethodMap>;
|
|
82
|
+
type ProxyTree<Router extends BaseRouter, Throw extends boolean = false, Error = unknown> = UnionToIntersection<{ [Path in keyof Router]: Path extends string ? BuildBranch<Split<Path>, MethodClient<Router[Path], Throw, Error>> : never; }[keyof Router]>;
|
|
83
|
+
type HttpMethod = 'DELETE' | 'PATCH' | 'POST' | 'GET' | 'PUT';
|
|
84
|
+
type InferRequestType<T extends (...args: Array<never>) => unknown> = Parameters<T>[0];
|
|
85
|
+
type InferResponseType<T extends (...args: Array<never>) => unknown> = Awaited<ReturnType<T>>;
|
|
86
|
+
type InferResponse<T extends EndpointDef> = InferSchema<T['response']>;
|
|
87
|
+
type EnsureRouter<T extends BaseRouter> = T;
|
|
88
|
+
type RouteSchemas = {
|
|
89
|
+
response?: StandardSchemaV1 | undefined;
|
|
90
|
+
};
|
|
91
|
+
type RpcSchemas<Router extends BaseRouter> = { [Path in keyof Router]?: { [Method in keyof Router[Path]]?: RouteSchemas | undefined; }; };
|
|
92
|
+
type CreateRpcClientOption<Router extends BaseRouter = BaseRouter> = Omit<CreateFetchOption, 'baseURL' | 'body'> & {
|
|
93
|
+
schemas?: RpcSchemas<Router> | undefined;
|
|
94
|
+
};
|
|
95
|
+
export declare function createRpcClient<Router extends BaseRouter, Error = unknown>(baseURL: undefined | string, option: CreateRpcClientOption<Router> & {
|
|
96
|
+
throw: true;
|
|
97
|
+
}): ProxyTree<Router, true, Error>;
|
|
98
|
+
export declare function createRpcClient<Router extends BaseRouter, Error = unknown>(baseURL?: string, option?: CreateRpcClientOption<Router>): ProxyTree<Router, false, Error>;
|
|
99
|
+
//#endregion
|
|
100
|
+
export { type BaseRouter, type CreateRpcClientOption, type EndpointDef, type EnsureRouter, type HttpMethod, type InferRequestType, type InferResponseType, type InferStandardInput, type InferStandardOutput, type MethodMap, type ProxyTree, type RequestOptions, type RouteSchemas, type RpcResponse, type RpcSchemas, type StandardSchemaFailureResult, type StandardSchemaIssue, type StandardSchemaPathSegment, type StandardSchemaResult, type StandardSchemaSuccessResult, type StandardSchemaTypes, type StandardSchemaV1, ValidationError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ValidationError, createFetch } from "@better-fetch/fetch";
|
|
2
|
+
//#region src/standard-schema.ts
|
|
3
|
+
function isStandardSchema(value) {
|
|
4
|
+
if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
|
|
5
|
+
const standard = value["~standard"];
|
|
6
|
+
return typeof standard === "object" && standard !== null;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/rpc.ts
|
|
10
|
+
const METHOD_KEY_MAP = {
|
|
11
|
+
$delete: "DELETE",
|
|
12
|
+
$patch: "PATCH",
|
|
13
|
+
$post: "POST",
|
|
14
|
+
$get: "GET",
|
|
15
|
+
$put: "PUT"
|
|
16
|
+
};
|
|
17
|
+
const METHOD_TO_KEY = {
|
|
18
|
+
DELETE: "$delete",
|
|
19
|
+
GET: "$get",
|
|
20
|
+
PATCH: "$patch",
|
|
21
|
+
POST: "$post",
|
|
22
|
+
PUT: "$put"
|
|
23
|
+
};
|
|
24
|
+
function createProxyClient(makeRequest, segments = []) {
|
|
25
|
+
return new Proxy(() => {}, { get(_target, prop) {
|
|
26
|
+
if (prop in METHOD_KEY_MAP) {
|
|
27
|
+
const method = METHOD_KEY_MAP[prop];
|
|
28
|
+
if (!method) return void 0;
|
|
29
|
+
const path = "/" + segments.join("/");
|
|
30
|
+
return (options) => makeRequest(method, path, options);
|
|
31
|
+
}
|
|
32
|
+
return createProxyClient(makeRequest, [...segments, prop]);
|
|
33
|
+
} });
|
|
34
|
+
}
|
|
35
|
+
function createRpcClient(baseURL, option = {}) {
|
|
36
|
+
const { schemas, ...fetchOption } = option;
|
|
37
|
+
const $fetchBase = createFetch({
|
|
38
|
+
...baseURL ? { baseURL } : {},
|
|
39
|
+
...fetchOption
|
|
40
|
+
});
|
|
41
|
+
const schemaIndex = schemas;
|
|
42
|
+
const makeRequest = (method, path, options) => {
|
|
43
|
+
const responseSchema = schemaIndex?.[path]?.[METHOD_TO_KEY[method]]?.response;
|
|
44
|
+
return $fetchBase(path, {
|
|
45
|
+
headers: options?.headers,
|
|
46
|
+
params: options?.params,
|
|
47
|
+
query: options?.query,
|
|
48
|
+
body: options?.body,
|
|
49
|
+
method,
|
|
50
|
+
...isStandardSchema(responseSchema) ? { output: responseSchema } : {}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
return createProxyClient(makeRequest);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ValidationError, createRpcClient, isStandardSchema };
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wynn-dev/better-fetch-rpc",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Type-safe RPC-style client built on @better-fetch/fetch — define routes once, call them like local functions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"better-fetch",
|
|
7
|
+
"fetch",
|
|
8
|
+
"rpc",
|
|
9
|
+
"typed-client",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/nbnguyen75/better-fetch-rpc#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nbnguyen75/better-fetch-rpc/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Nguyên (Wynn)",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/nbnguyen75/better-fetch-rpc.git"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "oxlint",
|
|
44
|
+
"lint:fix": "oxlint --fix",
|
|
45
|
+
"format": "oxfmt --check",
|
|
46
|
+
"format:fix": "oxfmt",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"release": "bumpp --commit --tag --push",
|
|
49
|
+
"prepublishOnly": "bun run test & bun run build",
|
|
50
|
+
"prepare": "husky"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@better-fetch/fetch": "^1.3.2",
|
|
54
|
+
"@changesets/cli": "^3.0.3",
|
|
55
|
+
"@types/bun": "^1.4.2",
|
|
56
|
+
"@types/node": "^26.6.2",
|
|
57
|
+
"bumpp": "^12.3.0",
|
|
58
|
+
"husky": "^9.1.7",
|
|
59
|
+
"lint-staged": "^17.6.0",
|
|
60
|
+
"oxfmt": "^0.70.0",
|
|
61
|
+
"oxlint": "^1.85.0",
|
|
62
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
63
|
+
"tsdown": "^0.23.0",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"valibot": "^1.1.0",
|
|
66
|
+
"vitest": "^3.2.4",
|
|
67
|
+
"zod": "^4.6.5"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@better-fetch/fetch": "^1.1.21"
|
|
71
|
+
},
|
|
72
|
+
"lint-staged": {
|
|
73
|
+
"src/*": [
|
|
74
|
+
"oxlint --fix"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|