@firtoz/hono-fetcher 1.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/README.md +400 -0
- package/package.json +69 -0
- package/src/honoDoFetcher.ts +57 -0
- package/src/honoFetcher.ts +187 -0
- package/src/index.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# @firtoz/hono-fetcher
|
|
2
|
+
|
|
3
|
+
Type-safe Hono API client with full TypeScript inference for routes, params, and payloads.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Fully Type-Safe** - Complete TypeScript inference for routes, parameters, request bodies, and responses
|
|
8
|
+
- 🎯 **Path Parameters** - Automatic extraction and validation of path parameters (`:id`, `:slug`, etc.)
|
|
9
|
+
- 📝 **Request Bodies** - Type-safe JSON and form data support with automatic serialization
|
|
10
|
+
- 🌐 **Cloudflare Workers** - First-class support for Durable Objects with `honoDoFetcher`
|
|
11
|
+
- 🚀 **Zero Runtime Overhead** - All type inference happens at compile time
|
|
12
|
+
- 🔄 **Full HTTP Methods** - Support for GET, POST, PUT, DELETE, and PATCH
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @firtoz/hono-fetcher
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Peer Dependencies
|
|
21
|
+
|
|
22
|
+
This package requires the following peer dependencies:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun add hono
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For Durable Object support:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun add @cloudflare/workers-types
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Basic Usage
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Hono } from 'hono';
|
|
40
|
+
import { honoFetcher } from '@firtoz/hono-fetcher';
|
|
41
|
+
|
|
42
|
+
// Define your Hono app
|
|
43
|
+
const app = new Hono()
|
|
44
|
+
.get('/users/:id', (c) => {
|
|
45
|
+
const id = c.req.param('id');
|
|
46
|
+
return c.json({ id, name: `User ${id}` });
|
|
47
|
+
})
|
|
48
|
+
.post('/users', async (c) => {
|
|
49
|
+
const body = await c.req.json<{ name: string }>();
|
|
50
|
+
return c.json({ id: '123', ...body });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create a typed fetcher
|
|
54
|
+
const api = honoFetcher<typeof app>(app.request);
|
|
55
|
+
|
|
56
|
+
// Use it with full type safety!
|
|
57
|
+
const response = await api.get({
|
|
58
|
+
url: '/users/:id',
|
|
59
|
+
params: { id: '123' }, // ✅ Type-safe params
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const user = await response.json(); // ✅ Inferred type: { id: string; name: string }
|
|
63
|
+
|
|
64
|
+
// POST with body
|
|
65
|
+
await api.post({
|
|
66
|
+
url: '/users',
|
|
67
|
+
body: { name: 'John' }, // ✅ Type-safe body
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Remote API Usage
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { honoFetcher } from '@firtoz/hono-fetcher';
|
|
75
|
+
|
|
76
|
+
// For a remote API, you need to define the app type
|
|
77
|
+
// (Usually exported from your backend)
|
|
78
|
+
const api = honoFetcher<typeof app>((url, init) => {
|
|
79
|
+
return fetch(`https://api.example.com${url}`, init);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Durable Objects
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { honoDoFetcher, honoDoFetcherWithName } from '@firtoz/hono-fetcher/honoDoFetcher';
|
|
87
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
88
|
+
import { Hono } from 'hono';
|
|
89
|
+
|
|
90
|
+
// Define your Durable Object with a Hono app
|
|
91
|
+
export class ChatRoomDO extends DurableObject {
|
|
92
|
+
app = new Hono()
|
|
93
|
+
.get('/messages', (c) => {
|
|
94
|
+
return c.json({ messages: [] });
|
|
95
|
+
})
|
|
96
|
+
.post('/messages', async (c) => {
|
|
97
|
+
const { text } = await c.req.json<{ text: string }>();
|
|
98
|
+
return c.json({ id: '1', text });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
fetch(request: Request) {
|
|
102
|
+
return this.app.fetch(request);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// In your worker
|
|
107
|
+
export default {
|
|
108
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
109
|
+
// Option 1: From a stub
|
|
110
|
+
const stub = env.CHAT_ROOM.get(env.CHAT_ROOM.idFromName('room-1'));
|
|
111
|
+
const api = honoDoFetcher(stub);
|
|
112
|
+
|
|
113
|
+
// Option 2: Directly with name
|
|
114
|
+
const api2 = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
|
|
115
|
+
|
|
116
|
+
// Use it!
|
|
117
|
+
const response = await api.get({ url: '/messages' });
|
|
118
|
+
return response;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
### `honoFetcher<T>(fetcher)`
|
|
126
|
+
|
|
127
|
+
Creates a type-safe API client from a Hono app type.
|
|
128
|
+
|
|
129
|
+
#### Parameters
|
|
130
|
+
|
|
131
|
+
- `fetcher: (url: string, init?: RequestInit) => Response | Promise<Response>` - Function that performs the actual fetch
|
|
132
|
+
|
|
133
|
+
#### Returns
|
|
134
|
+
|
|
135
|
+
A typed fetcher with methods for each HTTP verb: `get`, `post`, `put`, `delete`, `patch`
|
|
136
|
+
|
|
137
|
+
#### Example
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const api = honoFetcher<typeof app>(app.request);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Method Signature
|
|
144
|
+
|
|
145
|
+
All methods follow this signature:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
method({
|
|
149
|
+
url: string; // The route path
|
|
150
|
+
params?: object; // Path parameters (required if route has :params)
|
|
151
|
+
body?: object; // Request body (for POST/PUT/PATCH)
|
|
152
|
+
form?: object; // Form data (for POST/PUT/PATCH)
|
|
153
|
+
init?: RequestInit; // Additional fetch options
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Path Parameters
|
|
158
|
+
|
|
159
|
+
Routes with path parameters (`:id`, `:slug`, etc.) require the `params` field:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const app = new Hono()
|
|
163
|
+
.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
|
|
164
|
+
.get('/posts/:id/comments/:commentId', (c) =>
|
|
165
|
+
c.json({
|
|
166
|
+
postId: c.req.param('id'),
|
|
167
|
+
commentId: c.req.param('commentId')
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const api = honoFetcher<typeof app>(app.request);
|
|
172
|
+
|
|
173
|
+
// Single parameter
|
|
174
|
+
await api.get({
|
|
175
|
+
url: '/users/:id',
|
|
176
|
+
params: { id: '123' } // ✅ Required and type-safe
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Multiple parameters
|
|
180
|
+
await api.get({
|
|
181
|
+
url: '/posts/:id/comments/:commentId',
|
|
182
|
+
params: { id: '1', commentId: '42' } // ✅ Both required
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Request Bodies
|
|
187
|
+
|
|
188
|
+
#### JSON Bodies
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const app = new Hono()
|
|
192
|
+
.post('/users', async (c) => {
|
|
193
|
+
const { name, email } = await c.req.json<{ name: string; email: string }>();
|
|
194
|
+
return c.json({ id: '1', name, email });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const api = honoFetcher<typeof app>(app.request);
|
|
198
|
+
|
|
199
|
+
await api.post({
|
|
200
|
+
url: '/users',
|
|
201
|
+
body: { name: 'John', email: 'john@example.com' } // ✅ Type-safe
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Form Data
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { zValidator } from '@hono/zod-validator';
|
|
209
|
+
import { z } from 'zod';
|
|
210
|
+
|
|
211
|
+
const app = new Hono()
|
|
212
|
+
.post('/upload',
|
|
213
|
+
zValidator('form', z.object({
|
|
214
|
+
title: z.string(),
|
|
215
|
+
count: z.coerce.number()
|
|
216
|
+
})),
|
|
217
|
+
async (c) => {
|
|
218
|
+
const data = c.req.valid('form');
|
|
219
|
+
return c.json({ success: true, data });
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const api = honoFetcher<typeof app>(app.request);
|
|
224
|
+
|
|
225
|
+
await api.post({
|
|
226
|
+
url: '/upload',
|
|
227
|
+
form: { title: 'Hello', count: '5' } // ✅ Automatically sent as FormData
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Custom Headers and Options
|
|
232
|
+
|
|
233
|
+
Pass additional `fetch` options via the `init` parameter:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
await api.get({
|
|
237
|
+
url: '/users/:id',
|
|
238
|
+
params: { id: '123' },
|
|
239
|
+
init: {
|
|
240
|
+
headers: {
|
|
241
|
+
'Authorization': 'Bearer token',
|
|
242
|
+
'X-Custom-Header': 'value'
|
|
243
|
+
},
|
|
244
|
+
signal: abortController.signal
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Durable Objects API
|
|
250
|
+
|
|
251
|
+
### `honoDoFetcher<T>(stub)`
|
|
252
|
+
|
|
253
|
+
Creates a typed fetcher for a Durable Object stub.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const stub = env.MY_DO.get(env.MY_DO.idFromName('example'));
|
|
257
|
+
const api = honoDoFetcher(stub);
|
|
258
|
+
|
|
259
|
+
await api.get({ url: '/status' });
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### `honoDoFetcherWithName<T>(namespace, name)`
|
|
263
|
+
|
|
264
|
+
Convenience method to create a fetcher from a namespace and name.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const api = honoDoFetcherWithName(env.MY_DO, 'example');
|
|
268
|
+
await api.get({ url: '/status' });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `honoDoFetcherWithId<T>(namespace, id)`
|
|
272
|
+
|
|
273
|
+
Convenience method to create a fetcher from a namespace and hex ID string.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
|
|
277
|
+
await api.get({ url: '/status' });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Type Exports
|
|
281
|
+
|
|
282
|
+
### `TypedHonoFetcher<T>`
|
|
283
|
+
|
|
284
|
+
The main fetcher type with methods for all available HTTP verbs.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import type { TypedHonoFetcher } from '@firtoz/hono-fetcher';
|
|
288
|
+
|
|
289
|
+
function createApi(): TypedHonoFetcher<typeof app> {
|
|
290
|
+
return honoFetcher<typeof app>(app.request);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### `JsonResponse<T>`
|
|
295
|
+
|
|
296
|
+
Extended `Response` type with properly typed `json()` method.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import type { JsonResponse } from '@firtoz/hono-fetcher';
|
|
300
|
+
|
|
301
|
+
const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
|
|
302
|
+
const data = await response.json(); // Type: { id: string }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### `ParsePathParams<T>`
|
|
306
|
+
|
|
307
|
+
Utility type to extract path parameters from a route string.
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import type { ParsePathParams } from '@firtoz/hono-fetcher';
|
|
311
|
+
|
|
312
|
+
type Params = ParsePathParams<'/users/:id/posts/:postId'>;
|
|
313
|
+
// Type: { id: string; postId: string }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### `DOWithHonoApp`
|
|
317
|
+
|
|
318
|
+
Type for Durable Objects that expose a Hono app.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import type { DOWithHonoApp } from '@firtoz/hono-fetcher/honoDoFetcher';
|
|
322
|
+
|
|
323
|
+
export class MyDO extends DurableObject implements DOWithHonoApp {
|
|
324
|
+
app = new Hono()
|
|
325
|
+
.get('/status', (c) => c.json({ status: 'ok' }));
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Advanced Usage
|
|
330
|
+
|
|
331
|
+
### Sharing Types Between Frontend and Backend
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
// backend/app.ts
|
|
335
|
+
export const app = new Hono()
|
|
336
|
+
.get('/users/:id', (c) => c.json({ id: c.req.param('id'), name: 'User' }))
|
|
337
|
+
.post('/users', async (c) => {
|
|
338
|
+
const body = await c.req.json<{ name: string }>();
|
|
339
|
+
return c.json({ id: '1', ...body });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
export type AppType = typeof app;
|
|
343
|
+
|
|
344
|
+
// frontend/api.ts
|
|
345
|
+
import type { AppType } from '../backend/app';
|
|
346
|
+
import { honoFetcher } from '@firtoz/hono-fetcher';
|
|
347
|
+
|
|
348
|
+
export const api = honoFetcher<AppType>((url, init) => {
|
|
349
|
+
return fetch(`https://api.example.com${url}`, init);
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Error Handling
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
try {
|
|
357
|
+
const response = await api.post({
|
|
358
|
+
url: '/users',
|
|
359
|
+
body: { name: 'John' }
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
const error = await response.json();
|
|
364
|
+
console.error('API error:', error);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const user = await response.json();
|
|
369
|
+
console.log('Created user:', user);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error('Network error:', error);
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Middleware and Authentication
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const createAuthenticatedFetcher = <T extends Hono>(token: string) => {
|
|
379
|
+
return honoFetcher<T>((url, init) => {
|
|
380
|
+
return fetch(`https://api.example.com${url}`, {
|
|
381
|
+
...init,
|
|
382
|
+
headers: {
|
|
383
|
+
...init?.headers,
|
|
384
|
+
'Authorization': `Bearer ${token}`
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const api = createAuthenticatedFetcher<typeof app>(userToken);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT
|
|
396
|
+
|
|
397
|
+
## Contributing
|
|
398
|
+
|
|
399
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on how to contribute to this package.
|
|
400
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firtoz/hono-fetcher",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Type-safe Hono API client with full TypeScript inference for routes, params, and payloads",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"module": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"require": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"./honoDoFetcher": {
|
|
15
|
+
"types": "./src/honoDoFetcher.ts",
|
|
16
|
+
"import": "./src/honoDoFetcher.ts",
|
|
17
|
+
"require": "./src/honoDoFetcher.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"!src/**/*.test.ts",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome lint src --write",
|
|
28
|
+
"format": "biome format src --write",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"test:watch": "bun test --watch"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"typescript",
|
|
34
|
+
"hono",
|
|
35
|
+
"api-client",
|
|
36
|
+
"type-safe",
|
|
37
|
+
"fetcher",
|
|
38
|
+
"http-client",
|
|
39
|
+
"durable-objects",
|
|
40
|
+
"cloudflare"
|
|
41
|
+
],
|
|
42
|
+
"author": "Firtina Ozbalikchi <firtoz@github.com>",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/firtoz/fullstack-toolkit.git",
|
|
48
|
+
"directory": "packages/hono-fetcher"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/firtoz/fullstack-toolkit/issues"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@cloudflare/workers-types": "^4.20251004.0",
|
|
55
|
+
"hono": "^4.9.9"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@hono/node-server": "^1.14.1",
|
|
65
|
+
"@hono/zod-validator": "^0.4.1",
|
|
66
|
+
"bun-types": "^1.2.23",
|
|
67
|
+
"zod": "^3.24.1"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Hono, Schema } from "hono";
|
|
2
|
+
import type { ExtractSchema } from "hono/types";
|
|
3
|
+
import { honoFetcher, type TypedHonoFetcher } from "./honoFetcher";
|
|
4
|
+
|
|
5
|
+
const DUMMY_URL = "http://dummy-url";
|
|
6
|
+
|
|
7
|
+
export type DOWithHonoApp<S extends Schema = Schema> =
|
|
8
|
+
Rpc.DurableObjectBranded & {
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: We need to be able to pass in any schema
|
|
10
|
+
app: Hono<any, S>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DOSchemaMap<T extends DOWithHonoApp> = T extends DOWithHonoApp
|
|
14
|
+
? ExtractSchema<T["app"]>
|
|
15
|
+
: never;
|
|
16
|
+
|
|
17
|
+
export type DOSchemaKeys<T extends DOWithHonoApp> = string &
|
|
18
|
+
keyof DOSchemaMap<T>;
|
|
19
|
+
|
|
20
|
+
export type DOStubSchema<T extends DurableObjectStub> =
|
|
21
|
+
T extends DurableObjectStub<infer S>
|
|
22
|
+
? S extends DOWithHonoApp
|
|
23
|
+
? ExtractSchema<S["app"]>
|
|
24
|
+
: never
|
|
25
|
+
: never;
|
|
26
|
+
|
|
27
|
+
export type TypedDoFetcher<T extends DurableObjectStub> = TypedHonoFetcher<
|
|
28
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
|
|
29
|
+
Hono<any, DOStubSchema<T>>
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export const honoDoFetcher = <const T extends DurableObjectStub<DOWithHonoApp>>(
|
|
33
|
+
durableObject: T,
|
|
34
|
+
): TypedDoFetcher<T> => {
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
|
|
36
|
+
return honoFetcher<Hono<any, DOStubSchema<T>>>((url, init) => {
|
|
37
|
+
return durableObject.fetch(`${DUMMY_URL}${url}`, init);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const honoDoFetcherWithName = <
|
|
42
|
+
const T extends Rpc.DurableObjectBranded & DOWithHonoApp,
|
|
43
|
+
>(
|
|
44
|
+
namespace: DurableObjectNamespace<T>,
|
|
45
|
+
name: string,
|
|
46
|
+
): TypedDoFetcher<DurableObjectStub<T>> => {
|
|
47
|
+
return honoDoFetcher(namespace.get(namespace.idFromName(name)));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const honoDoFetcherWithId = <
|
|
51
|
+
const T extends Rpc.DurableObjectBranded & DOWithHonoApp,
|
|
52
|
+
>(
|
|
53
|
+
namespace: DurableObjectNamespace<T>,
|
|
54
|
+
id: string,
|
|
55
|
+
): TypedDoFetcher<DurableObjectStub<T>> => {
|
|
56
|
+
return honoDoFetcher(namespace.get(namespace.idFromString(id)));
|
|
57
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { ExtractSchema } from "hono/types";
|
|
3
|
+
|
|
4
|
+
export type ParsePathParams<T extends string> =
|
|
5
|
+
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
|
|
6
|
+
? { [K in Param | keyof ParsePathParams<`/${Rest}`>]: string }
|
|
7
|
+
: T extends `${infer _Start}/:${infer Param}`
|
|
8
|
+
? { [K in Param]: string }
|
|
9
|
+
: never;
|
|
10
|
+
|
|
11
|
+
export type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
|
|
12
|
+
|
|
13
|
+
export type HonoSchemaKeys<T extends Hono> = string & keyof ExtractSchema<T>;
|
|
14
|
+
|
|
15
|
+
type FilterKeysByMethod<
|
|
16
|
+
TApp extends ExtractSchema<unknown>,
|
|
17
|
+
TMethod extends HttpMethod,
|
|
18
|
+
> = {
|
|
19
|
+
[K in keyof TApp as TApp[K] extends { [key in `$${TMethod}`]: unknown }
|
|
20
|
+
? K
|
|
21
|
+
: never]: TApp[K];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type HonoSchema<TApp extends Hono> = {
|
|
25
|
+
[M in HttpMethod]: FilterKeysByMethod<ExtractSchema<TApp>, M>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type JsonResponse<T> = Omit<Response, "json"> & {
|
|
29
|
+
json: () => Promise<T>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type HasPathParams<T extends string> = T extends `${string}:${string}`
|
|
33
|
+
? true
|
|
34
|
+
: false;
|
|
35
|
+
|
|
36
|
+
type FetcherParams<SchemaPath extends string> =
|
|
37
|
+
HasPathParams<SchemaPath> extends true
|
|
38
|
+
? {
|
|
39
|
+
params: ParsePathParams<SchemaPath>;
|
|
40
|
+
init?: RequestInit;
|
|
41
|
+
}
|
|
42
|
+
: {
|
|
43
|
+
params?: never;
|
|
44
|
+
init?: RequestInit;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// biome-ignore lint/complexity/noBannedTypes: We need an empty object to remove the body and form keys from the request object
|
|
48
|
+
type EmptyObject = {};
|
|
49
|
+
|
|
50
|
+
type TypedMethodFetcher<T extends Hono, M extends HttpMethod> = <
|
|
51
|
+
SchemaPath extends string & keyof HonoSchema<T>[M],
|
|
52
|
+
>(
|
|
53
|
+
request: {
|
|
54
|
+
url: SchemaPath;
|
|
55
|
+
} & FetcherParams<SchemaPath> &
|
|
56
|
+
(M extends "get" | "delete" ? EmptyObject : BodyParams<T, M, SchemaPath>),
|
|
57
|
+
) => Promise<SchemaOutput<T, M, SchemaPath>>;
|
|
58
|
+
|
|
59
|
+
type SchemaOutput<
|
|
60
|
+
T extends Hono,
|
|
61
|
+
M extends HttpMethod,
|
|
62
|
+
SchemaPath extends string & keyof HonoSchema<T>[M],
|
|
63
|
+
DollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` &
|
|
64
|
+
keyof HonoSchema<T>[M][SchemaPath],
|
|
65
|
+
> = "output" extends keyof HonoSchema<T>[M][SchemaPath][DollarM]
|
|
66
|
+
? JsonResponse<HonoSchema<T>[M][SchemaPath][DollarM]["output"]>
|
|
67
|
+
: never;
|
|
68
|
+
|
|
69
|
+
type BodyParams<
|
|
70
|
+
TApp extends Hono,
|
|
71
|
+
TMethod extends HttpMethod,
|
|
72
|
+
SchemaPath extends string & keyof HonoSchema<TApp>[TMethod],
|
|
73
|
+
DollarMethod extends `$${TMethod}` &
|
|
74
|
+
keyof HonoSchema<TApp>[TMethod][SchemaPath] = `$${TMethod}` &
|
|
75
|
+
keyof HonoSchema<TApp>[TMethod][SchemaPath],
|
|
76
|
+
> = "input" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]
|
|
77
|
+
? "json" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]
|
|
78
|
+
? "form" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]
|
|
79
|
+
?
|
|
80
|
+
| {
|
|
81
|
+
body: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["json"];
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
form: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["form"];
|
|
85
|
+
}
|
|
86
|
+
: {
|
|
87
|
+
body: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["json"];
|
|
88
|
+
}
|
|
89
|
+
: "form" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]
|
|
90
|
+
? {
|
|
91
|
+
form: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["form"];
|
|
92
|
+
}
|
|
93
|
+
: { body?: unknown } | { form?: unknown }
|
|
94
|
+
: EmptyObject;
|
|
95
|
+
|
|
96
|
+
type AvailableMethods<T extends Hono> = {
|
|
97
|
+
[M in HttpMethod]: keyof HonoSchema<T>[M] extends never ? never : M;
|
|
98
|
+
}[HttpMethod];
|
|
99
|
+
|
|
100
|
+
export type BaseTypedHonoFetcher<T extends Hono> = {
|
|
101
|
+
[M in AvailableMethods<T>]: TypedMethodFetcher<T, M>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
|
|
105
|
+
fetcher: (
|
|
106
|
+
request: string,
|
|
107
|
+
init?: RequestInit,
|
|
108
|
+
) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>,
|
|
109
|
+
method: M,
|
|
110
|
+
): TypedMethodFetcher<T, M> => {
|
|
111
|
+
return (async (request) => {
|
|
112
|
+
let finalUrl: string = request.url;
|
|
113
|
+
|
|
114
|
+
const { init = {}, params } = request;
|
|
115
|
+
|
|
116
|
+
if (params && typeof params === "object") {
|
|
117
|
+
finalUrl = Object.entries(params).reduce((acc, [key, value]) => {
|
|
118
|
+
return acc.replace(`:${key}`, value as string);
|
|
119
|
+
}, finalUrl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const requestAsOptionalFormBody = request as {
|
|
123
|
+
form?: unknown;
|
|
124
|
+
body?: unknown;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let body: BodyInit | undefined;
|
|
128
|
+
if (requestAsOptionalFormBody.form) {
|
|
129
|
+
const formData = new FormData();
|
|
130
|
+
for (const [key, value] of Object.entries(
|
|
131
|
+
requestAsOptionalFormBody.form,
|
|
132
|
+
)) {
|
|
133
|
+
formData.append(key, value as string);
|
|
134
|
+
}
|
|
135
|
+
body = formData;
|
|
136
|
+
} else if (requestAsOptionalFormBody.body) {
|
|
137
|
+
body = JSON.stringify(requestAsOptionalFormBody.body) as BodyInit;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// biome-ignore lint/suspicious/noExplicitAny: Different runtimes have incompatible HeadersInit types
|
|
141
|
+
const newHeaders = new Headers(init.headers as any);
|
|
142
|
+
|
|
143
|
+
if (body && !requestAsOptionalFormBody.form) {
|
|
144
|
+
newHeaders.set("Content-Type", "application/json");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
return await fetcher(finalUrl, {
|
|
149
|
+
method: method.toUpperCase(),
|
|
150
|
+
headers: newHeaders,
|
|
151
|
+
...(body ? { body } : {}),
|
|
152
|
+
...init,
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`Error ${method}ing`, error);
|
|
156
|
+
throw new Error(`Failed to ${method} ${finalUrl}: ${error}`);
|
|
157
|
+
}
|
|
158
|
+
}) as TypedMethodFetcher<T, M>;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export type TypedHonoFetcher<T extends Hono> = BaseTypedHonoFetcher<T>;
|
|
162
|
+
|
|
163
|
+
export const honoFetcher = <T extends Hono>(
|
|
164
|
+
fetcher: (
|
|
165
|
+
request: string,
|
|
166
|
+
init?: RequestInit,
|
|
167
|
+
) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>,
|
|
168
|
+
): TypedHonoFetcher<T> => {
|
|
169
|
+
const methods = ["get", "post", "put", "delete", "patch"] as const;
|
|
170
|
+
|
|
171
|
+
const result = methods.reduce(
|
|
172
|
+
(acc, method) => {
|
|
173
|
+
(
|
|
174
|
+
acc as TypedHonoFetcher<T> & {
|
|
175
|
+
[M in typeof method]: TypedMethodFetcher<T, M>;
|
|
176
|
+
}
|
|
177
|
+
)[method] = createMethodFetcher(fetcher, method) as TypedMethodFetcher<
|
|
178
|
+
T,
|
|
179
|
+
typeof method
|
|
180
|
+
>;
|
|
181
|
+
return acc;
|
|
182
|
+
},
|
|
183
|
+
{} as TypedHonoFetcher<T>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
honoFetcher,
|
|
3
|
+
type BaseTypedHonoFetcher,
|
|
4
|
+
type HonoSchemaKeys,
|
|
5
|
+
type HttpMethod,
|
|
6
|
+
type JsonResponse,
|
|
7
|
+
type ParsePathParams,
|
|
8
|
+
type TypedHonoFetcher,
|
|
9
|
+
} from "./honoFetcher";
|
|
10
|
+
export {
|
|
11
|
+
honoDoFetcher,
|
|
12
|
+
honoDoFetcherWithId,
|
|
13
|
+
honoDoFetcherWithName,
|
|
14
|
+
type DOSchemaKeys,
|
|
15
|
+
type DOSchemaMap,
|
|
16
|
+
type DOStubSchema,
|
|
17
|
+
type DOWithHonoApp,
|
|
18
|
+
type TypedDoFetcher,
|
|
19
|
+
} from "./honoDoFetcher";
|