@firekid/hurl 1.0.6 → 1.1.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 +1 -1
- package/README.md +259 -160
- package/dist/index.d.mts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +5 -1
- package/dist/index.mjs +5 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,26 +1,73 @@
|
|
|
1
|
-
# hurl
|
|
1
|
+
# @firekid/hurl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmjs.com/package/@firekid/hurl)
|
|
4
|
+
[](https://npmjs.com/package/@firekid/hurl)
|
|
5
|
+
[](https://bundlephobia.com/package/@firekid/hurl)
|
|
6
|
+
[](https://www.typescriptlang.org)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://github.com/firekid-is-him/hurl/actions)
|
|
9
|
+
[](https://github.com/firekid-is-him/hurl/stargazers)
|
|
10
|
+
[](https://hurl.firekidofficial.name.ng)
|
|
11
|
+
|
|
12
|
+
**`@firekid/hurl`** is a modern, zero-dependency HTTP client for Node.js 18+, Cloudflare Workers, Vercel Edge Functions, Deno, and Bun — built on native fetch with retries, interceptors, auth helpers, in-memory caching, request deduplication, and full TypeScript support.
|
|
4
13
|
|
|
5
14
|
```bash
|
|
6
15
|
npm install @firekid/hurl
|
|
7
16
|
```
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
---
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## Why hurl?
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
Most HTTP clients make you choose between features and bundle size, or between Node.js support and edge compatibility. `@firekid/hurl` does neither.
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
```ts
|
|
25
|
+
import hurl from '@firekid/hurl'
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
// Retry automatically on failure
|
|
28
|
+
const res = await hurl.get('https://api.example.com/users', { retry: 3 })
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
// Auth, timeout, caching — all in one call
|
|
31
|
+
const data = await hurl.get('/users', {
|
|
32
|
+
auth: { type: 'bearer', token: process.env.API_TOKEN },
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
cache: { ttl: 60000 },
|
|
35
|
+
})
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
// Parallel requests
|
|
38
|
+
const [users, posts] = await hurl.all([
|
|
39
|
+
hurl.get('/users'),
|
|
40
|
+
hurl.get('/posts'),
|
|
41
|
+
])
|
|
42
|
+
```
|
|
22
43
|
|
|
23
|
-
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Comparison
|
|
47
|
+
|
|
48
|
+
| Feature | **hurl** | axios | ky | got | node-fetch |
|
|
49
|
+
|---|:---:|:---:|:---:|:---:|:---:|
|
|
50
|
+
| Zero dependencies | ✅ | ❌ | ✅ | ❌ | ✅ |
|
|
51
|
+
| Bundle size | **~9KB** | ~35KB | ~5KB | ~45KB | ~8KB |
|
|
52
|
+
| Node.js 18+ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
53
|
+
| Cloudflare Workers | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
54
|
+
| Vercel Edge | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
55
|
+
| Deno / Bun | ✅ | ⚠️ | ✅ | ⚠️ | ❌ |
|
|
56
|
+
| Built-in retries | ✅ | ❌ | ✅ | ✅ | ❌ |
|
|
57
|
+
| Interceptors | ✅ | ✅ | ✅ | ❌ | ❌ |
|
|
58
|
+
| Auth helpers | ✅ | ⚠️ | ❌ | ❌ | ❌ |
|
|
59
|
+
| In-memory cache | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
60
|
+
| Request deduplication | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
61
|
+
| Upload progress | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
62
|
+
| Download progress | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
63
|
+
| Proxy support | ✅ | ✅ | ❌ | ✅ | ❌ |
|
|
64
|
+
| CommonJS + ESM | ✅ | ✅ | ❌ | ❌ | ✅ |
|
|
65
|
+
| TypeScript (built-in) | ✅ | ⚠️ | ✅ | ✅ | ⚠️ |
|
|
66
|
+
| Throws on 4xx/5xx | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
67
|
+
|
|
68
|
+
> ✅ Full support ⚠️ Partial / via plugin ❌ Not supported
|
|
69
|
+
|
|
70
|
+
---
|
|
24
71
|
|
|
25
72
|
## Installation
|
|
26
73
|
|
|
@@ -30,6 +77,8 @@ yarn add @firekid/hurl
|
|
|
30
77
|
pnpm add @firekid/hurl
|
|
31
78
|
```
|
|
32
79
|
|
|
80
|
+
---
|
|
81
|
+
|
|
33
82
|
## Quick Start
|
|
34
83
|
|
|
35
84
|
```ts
|
|
@@ -45,6 +94,8 @@ res.timing // { start, end, duration }
|
|
|
45
94
|
res.fromCache // boolean
|
|
46
95
|
```
|
|
47
96
|
|
|
97
|
+
---
|
|
98
|
+
|
|
48
99
|
## HTTP Methods
|
|
49
100
|
|
|
50
101
|
```ts
|
|
@@ -58,6 +109,8 @@ hurl.options<T>(url, options?)
|
|
|
58
109
|
hurl.request<T>(url, options?)
|
|
59
110
|
```
|
|
60
111
|
|
|
112
|
+
---
|
|
113
|
+
|
|
61
114
|
## Global Defaults
|
|
62
115
|
|
|
63
116
|
```ts
|
|
@@ -72,60 +125,33 @@ hurl.defaults.get()
|
|
|
72
125
|
hurl.defaults.reset()
|
|
73
126
|
```
|
|
74
127
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
All methods accept a `HurlRequestOptions` object.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
type HurlRequestOptions = {
|
|
81
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
82
|
-
headers?: Record<string, string>
|
|
83
|
-
body?: unknown
|
|
84
|
-
query?: Record<string, string | number | boolean>
|
|
85
|
-
timeout?: number
|
|
86
|
-
retry?: RetryConfig | number
|
|
87
|
-
auth?: AuthConfig
|
|
88
|
-
proxy?: ProxyConfig
|
|
89
|
-
cache?: CacheConfig
|
|
90
|
-
signal?: AbortSignal
|
|
91
|
-
followRedirects?: boolean
|
|
92
|
-
maxRedirects?: number
|
|
93
|
-
onUploadProgress?: ProgressCallback
|
|
94
|
-
onDownloadProgress?: ProgressCallback
|
|
95
|
-
stream?: boolean
|
|
96
|
-
debug?: boolean
|
|
97
|
-
requestId?: string
|
|
98
|
-
deduplicate?: boolean
|
|
99
|
-
}
|
|
100
|
-
```
|
|
128
|
+
---
|
|
101
129
|
|
|
102
130
|
## Authentication
|
|
103
131
|
|
|
104
132
|
```ts
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
133
|
+
// Bearer token
|
|
134
|
+
hurl.defaults.set({ auth: { type: 'bearer', token: 'my-token' } })
|
|
108
135
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
136
|
+
// Basic auth
|
|
137
|
+
hurl.defaults.set({ auth: { type: 'basic', username: 'admin', password: 'secret' } })
|
|
112
138
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})
|
|
139
|
+
// API key (header)
|
|
140
|
+
hurl.defaults.set({ auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' } })
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
})
|
|
142
|
+
// API key (query param)
|
|
143
|
+
hurl.defaults.set({ auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' } })
|
|
120
144
|
```
|
|
121
145
|
|
|
122
|
-
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Retry & Backoff
|
|
123
149
|
|
|
124
150
|
```ts
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
151
|
+
// Simple — retry 3 times with exponential backoff
|
|
152
|
+
await hurl.get('/users', { retry: 3 })
|
|
128
153
|
|
|
154
|
+
// Full config
|
|
129
155
|
await hurl.get('/users', {
|
|
130
156
|
retry: {
|
|
131
157
|
count: 3,
|
|
@@ -136,9 +162,11 @@ await hurl.get('/users', {
|
|
|
136
162
|
})
|
|
137
163
|
```
|
|
138
164
|
|
|
139
|
-
|
|
165
|
+
Retries are not triggered for abort errors. If no `on` array is provided, retries fire on network errors, timeout errors, and any 5xx status.
|
|
166
|
+
|
|
167
|
+
---
|
|
140
168
|
|
|
141
|
-
## Timeout
|
|
169
|
+
## Timeout & Abort
|
|
142
170
|
|
|
143
171
|
```ts
|
|
144
172
|
await hurl.get('/users', { timeout: 5000 })
|
|
@@ -148,9 +176,12 @@ setTimeout(() => controller.abort(), 3000)
|
|
|
148
176
|
await hurl.get('/users', { signal: controller.signal })
|
|
149
177
|
```
|
|
150
178
|
|
|
179
|
+
---
|
|
180
|
+
|
|
151
181
|
## Interceptors
|
|
152
182
|
|
|
153
183
|
```ts
|
|
184
|
+
// Request interceptor
|
|
154
185
|
const remove = hurl.interceptors.request.use((url, options) => {
|
|
155
186
|
return {
|
|
156
187
|
url,
|
|
@@ -160,65 +191,51 @@ const remove = hurl.interceptors.request.use((url, options) => {
|
|
|
160
191
|
},
|
|
161
192
|
}
|
|
162
193
|
})
|
|
194
|
+
remove() // unregister
|
|
163
195
|
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
// Response interceptor
|
|
166
197
|
hurl.interceptors.response.use((response) => {
|
|
167
198
|
console.log(response.status, response.timing.duration)
|
|
168
199
|
return response
|
|
169
200
|
})
|
|
170
201
|
|
|
202
|
+
// Error interceptor
|
|
171
203
|
hurl.interceptors.error.use((error) => {
|
|
172
204
|
if (error.status === 401) redirectToLogin()
|
|
173
205
|
return error
|
|
174
206
|
})
|
|
175
207
|
|
|
208
|
+
// Clear all
|
|
176
209
|
hurl.interceptors.request.clear()
|
|
177
210
|
hurl.interceptors.response.clear()
|
|
178
211
|
hurl.interceptors.error.clear()
|
|
179
212
|
```
|
|
180
213
|
|
|
181
|
-
|
|
214
|
+
---
|
|
182
215
|
|
|
183
|
-
|
|
184
|
-
const form = new FormData()
|
|
185
|
-
form.append('file', file)
|
|
186
|
-
|
|
187
|
-
await hurl.post('/upload', form, {
|
|
188
|
-
onUploadProgress: ({ loaded, total, percent }) => {
|
|
189
|
-
console.log(`${percent}%`)
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
```
|
|
216
|
+
## Caching
|
|
193
217
|
|
|
194
|
-
|
|
218
|
+
Caching applies to GET requests only. Responses are stored in memory with a TTL in milliseconds.
|
|
195
219
|
|
|
196
220
|
```ts
|
|
197
|
-
await hurl.get('/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
})
|
|
221
|
+
await hurl.get('/users', { cache: { ttl: 60000 } })
|
|
222
|
+
await hurl.get('/users', { cache: { ttl: 60000, key: 'all-users' } })
|
|
223
|
+
await hurl.get('/users', { cache: { ttl: 60000, bypass: true } })
|
|
202
224
|
```
|
|
203
225
|
|
|
204
|
-
## Caching
|
|
205
|
-
|
|
206
|
-
Caching only applies to GET requests. Responses are stored in memory with a TTL in milliseconds.
|
|
207
|
-
|
|
208
226
|
```ts
|
|
209
|
-
|
|
210
|
-
cache: { ttl: 60000 }
|
|
211
|
-
})
|
|
227
|
+
import { clearCache, invalidateCache } from '@firekid/hurl'
|
|
212
228
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
})
|
|
229
|
+
// Clear the entire cache
|
|
230
|
+
clearCache()
|
|
216
231
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
// Invalidate a single entry by URL or custom key
|
|
233
|
+
invalidateCache('https://api.example.com/users')
|
|
234
|
+
invalidateCache('all-users') // if you used a custom cache key
|
|
220
235
|
```
|
|
221
236
|
|
|
237
|
+
---
|
|
238
|
+
|
|
222
239
|
## Request Deduplication
|
|
223
240
|
|
|
224
241
|
When `deduplicate` is true and the same GET URL is called multiple times simultaneously, only one network request is made.
|
|
@@ -228,23 +245,68 @@ const [a, b] = await Promise.all([
|
|
|
228
245
|
hurl.get('/users', { deduplicate: true }),
|
|
229
246
|
hurl.get('/users', { deduplicate: true }),
|
|
230
247
|
])
|
|
248
|
+
// only one network request fired
|
|
231
249
|
```
|
|
232
250
|
|
|
233
|
-
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Upload & Download Progress
|
|
234
254
|
|
|
235
255
|
```ts
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
// Upload
|
|
257
|
+
const form = new FormData()
|
|
258
|
+
form.append('file', file)
|
|
259
|
+
|
|
260
|
+
await hurl.post('/upload', form, {
|
|
261
|
+
onUploadProgress: ({ loaded, total, percent }) => {
|
|
262
|
+
console.log(`Uploading: ${percent}%`)
|
|
263
|
+
}
|
|
238
264
|
})
|
|
239
265
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
// Download
|
|
267
|
+
await hurl.get('/large-file', {
|
|
268
|
+
onDownloadProgress: ({ loaded, total, percent }) => {
|
|
269
|
+
console.log(`Downloading: ${percent}%`)
|
|
244
270
|
}
|
|
245
271
|
})
|
|
246
272
|
```
|
|
247
273
|
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Proxy
|
|
277
|
+
|
|
278
|
+
Native fetch does not support programmatic proxy configuration out of the box. Proxy support depends on your Node.js version:
|
|
279
|
+
|
|
280
|
+
**Node.js 18** — install `undici@6` (v7 dropped Node 18 support), use `ProxyAgent`:
|
|
281
|
+
```ts
|
|
282
|
+
// npm install undici@6
|
|
283
|
+
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
|
284
|
+
setGlobalDispatcher(new ProxyAgent('http://proxy.example.com:8080'))
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Node.js 20** — `undici` is bundled with `ProxyAgent` support:
|
|
288
|
+
```ts
|
|
289
|
+
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
|
290
|
+
setGlobalDispatcher(new ProxyAgent('http://proxy.example.com:8080'))
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Node.js 22.3+** — supports `EnvHttpProxyAgent` which reads `HTTP_PROXY`/`HTTPS_PROXY` env vars automatically:
|
|
294
|
+
```ts
|
|
295
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'
|
|
296
|
+
setGlobalDispatcher(new EnvHttpProxyAgent())
|
|
297
|
+
// now set HTTP_PROXY=http://proxy.example.com:8080 in your env
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Node.js 24+** — native fetch respects env vars when `NODE_USE_ENV_PROXY=1` is set:
|
|
301
|
+
```bash
|
|
302
|
+
NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 node app.js
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The `proxy` option in `HurlRequestOptions` is reserved for a future release where this will be handled automatically.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
---
|
|
309
|
+
|
|
248
310
|
## Parallel Requests
|
|
249
311
|
|
|
250
312
|
```ts
|
|
@@ -254,6 +316,8 @@ const [users, posts] = await hurl.all([
|
|
|
254
316
|
])
|
|
255
317
|
```
|
|
256
318
|
|
|
319
|
+
---
|
|
320
|
+
|
|
257
321
|
## Isolated Instances
|
|
258
322
|
|
|
259
323
|
```ts
|
|
@@ -266,22 +330,26 @@ const api = hurl.create({
|
|
|
266
330
|
|
|
267
331
|
await api.get('/users')
|
|
268
332
|
|
|
333
|
+
// Extend with overrides
|
|
269
334
|
const adminApi = api.extend({
|
|
270
335
|
headers: { 'x-role': 'admin' }
|
|
271
336
|
})
|
|
272
337
|
```
|
|
273
338
|
|
|
274
|
-
|
|
339
|
+
---
|
|
275
340
|
|
|
276
|
-
|
|
341
|
+
## Error Handling
|
|
277
342
|
|
|
278
|
-
|
|
279
|
-
await hurl.get('/users', { debug: true })
|
|
280
|
-
```
|
|
343
|
+
`hurl` throws a `HurlError` on HTTP errors (4xx/5xx), network failures, timeouts, aborts, and parse failures. It never resolves silently on bad status codes.
|
|
281
344
|
|
|
282
|
-
|
|
345
|
+
If you want to handle 4xx/5xx responses yourself without a try/catch, set `throwOnError: false` — the response will resolve normally and you can check `res.status` yourself.
|
|
283
346
|
|
|
284
|
-
|
|
347
|
+
```ts
|
|
348
|
+
const res = await hurl.get('/users', { throwOnError: false })
|
|
349
|
+
if (res.status === 404) {
|
|
350
|
+
console.log('not found')
|
|
351
|
+
}
|
|
352
|
+
```
|
|
285
353
|
|
|
286
354
|
```ts
|
|
287
355
|
import hurl, { HurlError } from '@firekid/hurl'
|
|
@@ -301,18 +369,32 @@ try {
|
|
|
301
369
|
}
|
|
302
370
|
```
|
|
303
371
|
|
|
372
|
+
---
|
|
373
|
+
|
|
304
374
|
## TypeScript
|
|
305
375
|
|
|
306
376
|
```ts
|
|
307
377
|
type User = { id: number; name: string }
|
|
308
378
|
|
|
309
379
|
const res = await hurl.get<User[]>('/users')
|
|
310
|
-
res.data
|
|
380
|
+
res.data // User[]
|
|
311
381
|
|
|
312
382
|
const created = await hurl.post<User>('/users', { name: 'John' })
|
|
313
|
-
created.data.id
|
|
383
|
+
created.data.id // number
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Debug Mode
|
|
389
|
+
|
|
390
|
+
Logs the full request (method, url, headers, body, query, timeout, retry config) and response (status, timing, headers, data) to the console. Errors and retries are also logged.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
await hurl.get('/users', { debug: true })
|
|
314
394
|
```
|
|
315
395
|
|
|
396
|
+
---
|
|
397
|
+
|
|
316
398
|
## Response Shape
|
|
317
399
|
|
|
318
400
|
```ts
|
|
@@ -331,79 +413,96 @@ type HurlResponse<T> = {
|
|
|
331
413
|
}
|
|
332
414
|
```
|
|
333
415
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
`hurl` runs anywhere the Fetch API is available.
|
|
337
|
-
|
|
338
|
-
- Node.js 18 and above
|
|
339
|
-
- Cloudflare Workers
|
|
340
|
-
- Vercel Edge Functions
|
|
341
|
-
- Deno
|
|
342
|
-
- Bun
|
|
343
|
-
|
|
344
|
-
Exports both ESM (`import`) and CommonJS (`require`).
|
|
345
|
-
|
|
346
|
-
## API Reference
|
|
347
|
-
|
|
348
|
-
### hurl.get(url, options?)
|
|
349
|
-
Sends a GET request. Returns `Promise<HurlResponse<T>>`.
|
|
416
|
+
---
|
|
350
417
|
|
|
351
|
-
|
|
352
|
-
Sends a POST request. Body is auto-serialized to JSON if it is a plain object. Returns `Promise<HurlResponse<T>>`.
|
|
353
|
-
|
|
354
|
-
### hurl.put(url, body?, options?)
|
|
355
|
-
Sends a PUT request. Returns `Promise<HurlResponse<T>>`.
|
|
356
|
-
|
|
357
|
-
### hurl.patch(url, body?, options?)
|
|
358
|
-
Sends a PATCH request. Returns `Promise<HurlResponse<T>>`.
|
|
418
|
+
## Request Options
|
|
359
419
|
|
|
360
|
-
|
|
361
|
-
|
|
420
|
+
```ts
|
|
421
|
+
type HurlRequestOptions = {
|
|
422
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
423
|
+
headers?: Record<string, string>
|
|
424
|
+
body?: unknown
|
|
425
|
+
query?: Record<string, string | number | boolean>
|
|
426
|
+
timeout?: number
|
|
427
|
+
retry?: RetryConfig | number
|
|
428
|
+
auth?: AuthConfig
|
|
429
|
+
proxy?: ProxyConfig
|
|
430
|
+
cache?: CacheConfig
|
|
431
|
+
signal?: AbortSignal
|
|
432
|
+
followRedirects?: boolean
|
|
433
|
+
maxRedirects?: number
|
|
434
|
+
onUploadProgress?: ProgressCallback
|
|
435
|
+
onDownloadProgress?: ProgressCallback
|
|
436
|
+
stream?: boolean
|
|
437
|
+
debug?: boolean
|
|
438
|
+
requestId?: string
|
|
439
|
+
deduplicate?: boolean
|
|
440
|
+
}
|
|
441
|
+
```
|
|
362
442
|
|
|
363
|
-
|
|
364
|
-
Sends a HEAD request. Returns `Promise<HurlResponse<void>>`.
|
|
443
|
+
---
|
|
365
444
|
|
|
366
|
-
|
|
367
|
-
Sends an OPTIONS request. Returns `Promise<HurlResponse<T>>`.
|
|
445
|
+
## Environment Support
|
|
368
446
|
|
|
369
|
-
|
|
370
|
-
Sends a request with the method specified in options. Defaults to GET. Returns `Promise<HurlResponse<T>>`.
|
|
447
|
+
`@firekid/hurl` runs anywhere the Fetch API is available. No adapters, no polyfills needed.
|
|
371
448
|
|
|
372
|
-
|
|
373
|
-
|
|
449
|
+
| Runtime | Support |
|
|
450
|
+
|---|:---:|
|
|
451
|
+
| Node.js 18+ | ✅ |
|
|
452
|
+
| Cloudflare Workers | ✅ |
|
|
453
|
+
| Vercel Edge Functions | ✅ |
|
|
454
|
+
| Deno | ✅ |
|
|
455
|
+
| Bun | ✅ |
|
|
374
456
|
|
|
375
|
-
|
|
376
|
-
Creates a new isolated instance with its own defaults, interceptors, and state. Does not share anything with the parent instance.
|
|
457
|
+
Exports both ESM (`import`) and CommonJS (`require`).
|
|
377
458
|
|
|
378
|
-
|
|
379
|
-
Creates a new instance that inherits the current defaults and merges in the provided ones.
|
|
459
|
+
---
|
|
380
460
|
|
|
381
|
-
|
|
382
|
-
Sets global defaults for the current instance. Merged into every request.
|
|
461
|
+
## Why Not Axios?
|
|
383
462
|
|
|
384
|
-
|
|
385
|
-
Returns the current defaults object.
|
|
463
|
+
**axios** is 35KB, has no native edge runtime support, no built-in retry, no deduplication, and carries `XMLHttpRequest` baggage from a different era of the web.
|
|
386
464
|
|
|
387
|
-
|
|
388
|
-
Resets defaults to the values provided when the instance was created.
|
|
465
|
+
**got** dropped CommonJS in v12 — if your project uses `require()`, you're stuck on an old version.
|
|
389
466
|
|
|
390
|
-
|
|
391
|
-
Registers a request interceptor. Returns a function that removes the interceptor when called.
|
|
467
|
+
**ky** is browser-first. No Node.js, no proxy, no streaming.
|
|
392
468
|
|
|
393
|
-
|
|
394
|
-
Registers a response interceptor. Returns a function that removes the interceptor when called.
|
|
469
|
+
**node-fetch** is a polyfill. Node.js has had native fetch since v18. You don't need it anymore.
|
|
395
470
|
|
|
396
|
-
|
|
397
|
-
Registers an error interceptor. Returns a function that removes the interceptor when called.
|
|
471
|
+
**request** has been deprecated since 2020.
|
|
398
472
|
|
|
399
|
-
|
|
400
|
-
Clears the entire in-memory response cache.
|
|
473
|
+
**`@firekid/hurl`** is built for how Node.js and the edge work today — native fetch, zero dependencies, everything included, works everywhere.
|
|
401
474
|
|
|
402
|
-
|
|
403
|
-
import { clearCache } from '@firekid/hurl'
|
|
404
|
-
clearCache()
|
|
405
|
-
```
|
|
475
|
+
---
|
|
406
476
|
|
|
407
|
-
##
|
|
477
|
+
## API Reference
|
|
408
478
|
|
|
409
|
-
|
|
479
|
+
| Method | Description |
|
|
480
|
+
|---|---|
|
|
481
|
+
| `hurl.get(url, options?)` | GET request → `Promise<HurlResponse<T>>` |
|
|
482
|
+
| `hurl.post(url, body?, options?)` | POST request, body auto-serialized to JSON |
|
|
483
|
+
| `hurl.put(url, body?, options?)` | PUT request |
|
|
484
|
+
| `hurl.patch(url, body?, options?)` | PATCH request |
|
|
485
|
+
| `hurl.delete(url, options?)` | DELETE request |
|
|
486
|
+
| `hurl.head(url, options?)` | HEAD request → `Promise<HurlResponse<void>>` |
|
|
487
|
+
| `hurl.options(url, options?)` | OPTIONS request |
|
|
488
|
+
| `hurl.request(url, options?)` | Generic request, method from options |
|
|
489
|
+
| `hurl.all(requests)` | Run requests in parallel |
|
|
490
|
+
| `hurl.create(defaults?)` | New isolated instance |
|
|
491
|
+
| `hurl.extend(defaults?)` | New instance inheriting current defaults |
|
|
492
|
+
| `hurl.defaults.set(defaults)` | Set global defaults |
|
|
493
|
+
| `hurl.defaults.get()` | Get current defaults |
|
|
494
|
+
| `hurl.defaults.reset()` | Reset defaults to instance creation values |
|
|
495
|
+
| `hurl.interceptors.request.use(fn)` | Register request interceptor |
|
|
496
|
+
| `hurl.interceptors.response.use(fn)` | Register response interceptor |
|
|
497
|
+
| `hurl.interceptors.error.use(fn)` | Register error interceptor |
|
|
498
|
+
| `clearCache()` | Clear in-memory response cache |
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Contributors
|
|
503
|
+
|
|
504
|
+
[](https://github.com/HeavstalTech) **[HeavstalTech](https://github.com/HeavstalTech)** — signal fix, cache hardening, test suite
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
Built with ♥️ by [Firekid](https://github.com/Firekid-is-him) · [MIT License](./LICENSE)
|
package/dist/index.d.mts
CHANGED
|
@@ -30,11 +30,35 @@ type CacheConfig = {
|
|
|
30
30
|
key?: string;
|
|
31
31
|
bypass?: boolean;
|
|
32
32
|
};
|
|
33
|
+
type CircuitBreakerConfig = {
|
|
34
|
+
threshold: number;
|
|
35
|
+
cooldown: number;
|
|
36
|
+
key?: string;
|
|
37
|
+
fallback?: () => unknown;
|
|
38
|
+
};
|
|
33
39
|
type ProgressCallback = (e: {
|
|
34
40
|
loaded: number;
|
|
35
41
|
total: number;
|
|
36
42
|
percent: number;
|
|
37
43
|
}) => void;
|
|
44
|
+
type SSEEvent = {
|
|
45
|
+
data: string;
|
|
46
|
+
event: string;
|
|
47
|
+
id: string;
|
|
48
|
+
retry?: number;
|
|
49
|
+
};
|
|
50
|
+
type SSEOptions = {
|
|
51
|
+
method?: 'GET' | 'POST';
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
body?: unknown;
|
|
54
|
+
query?: Record<string, string | number | boolean>;
|
|
55
|
+
auth?: AuthConfig;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
onOpen?: () => void;
|
|
58
|
+
onMessage: (event: SSEEvent) => void;
|
|
59
|
+
onError?: (error: Error) => void;
|
|
60
|
+
onDone?: () => void;
|
|
61
|
+
};
|
|
38
62
|
type HurlRequestOptions = {
|
|
39
63
|
method?: Method;
|
|
40
64
|
headers?: Record<string, string>;
|
|
@@ -45,6 +69,7 @@ type HurlRequestOptions = {
|
|
|
45
69
|
auth?: AuthConfig;
|
|
46
70
|
proxy?: ProxyConfig;
|
|
47
71
|
cache?: CacheConfig;
|
|
72
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
48
73
|
signal?: AbortSignal;
|
|
49
74
|
followRedirects?: boolean;
|
|
50
75
|
onUploadProgress?: ProgressCallback;
|
|
@@ -80,7 +105,7 @@ type RequestInterceptor = (url: string, options: HurlRequestOptions) => Promise<
|
|
|
80
105
|
};
|
|
81
106
|
type ResponseInterceptor<T = unknown> = (response: HurlResponse<T>) => Promise<HurlResponse<T>> | HurlResponse<T>;
|
|
82
107
|
type ErrorInterceptor = (error: HurlError) => Promise<HurlError | HurlResponse> | HurlError | HurlResponse;
|
|
83
|
-
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR';
|
|
108
|
+
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR' | 'CIRCUIT_OPEN';
|
|
84
109
|
declare class HurlError extends Error {
|
|
85
110
|
type: HurlErrorType;
|
|
86
111
|
status?: number;
|
|
@@ -109,6 +134,9 @@ type HurlInstance = {
|
|
|
109
134
|
head(url: string, options?: HurlRequestOptions): Promise<HurlResponse<void>>;
|
|
110
135
|
options<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
111
136
|
request<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
137
|
+
sse(url: string, options: SSEOptions): {
|
|
138
|
+
close(): void;
|
|
139
|
+
};
|
|
112
140
|
all<T extends unknown[]>(requests: {
|
|
113
141
|
[K in keyof T]: Promise<T[K]>;
|
|
114
142
|
}): Promise<T>;
|
|
@@ -135,9 +163,16 @@ type HurlInstance = {
|
|
|
135
163
|
extend(defaults?: HurlDefaults): HurlInstance;
|
|
136
164
|
};
|
|
137
165
|
|
|
166
|
+
declare function invalidateCache(key: string): void;
|
|
138
167
|
declare function clearCache(): void;
|
|
139
168
|
|
|
169
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
170
|
+
declare function getCircuitStats(key: string): {
|
|
171
|
+
state: CircuitState;
|
|
172
|
+
failures: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
140
175
|
declare function createInstance(initialDefaults?: HurlDefaults): HurlInstance;
|
|
141
176
|
declare const hurl: HurlInstance;
|
|
142
177
|
|
|
143
|
-
export { type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, clearCache, createInstance, hurl as default };
|
|
178
|
+
export { type CircuitBreakerConfig, type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, type SSEEvent, type SSEOptions, clearCache, createInstance, hurl as default, getCircuitStats, invalidateCache };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,11 +30,35 @@ type CacheConfig = {
|
|
|
30
30
|
key?: string;
|
|
31
31
|
bypass?: boolean;
|
|
32
32
|
};
|
|
33
|
+
type CircuitBreakerConfig = {
|
|
34
|
+
threshold: number;
|
|
35
|
+
cooldown: number;
|
|
36
|
+
key?: string;
|
|
37
|
+
fallback?: () => unknown;
|
|
38
|
+
};
|
|
33
39
|
type ProgressCallback = (e: {
|
|
34
40
|
loaded: number;
|
|
35
41
|
total: number;
|
|
36
42
|
percent: number;
|
|
37
43
|
}) => void;
|
|
44
|
+
type SSEEvent = {
|
|
45
|
+
data: string;
|
|
46
|
+
event: string;
|
|
47
|
+
id: string;
|
|
48
|
+
retry?: number;
|
|
49
|
+
};
|
|
50
|
+
type SSEOptions = {
|
|
51
|
+
method?: 'GET' | 'POST';
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
body?: unknown;
|
|
54
|
+
query?: Record<string, string | number | boolean>;
|
|
55
|
+
auth?: AuthConfig;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
onOpen?: () => void;
|
|
58
|
+
onMessage: (event: SSEEvent) => void;
|
|
59
|
+
onError?: (error: Error) => void;
|
|
60
|
+
onDone?: () => void;
|
|
61
|
+
};
|
|
38
62
|
type HurlRequestOptions = {
|
|
39
63
|
method?: Method;
|
|
40
64
|
headers?: Record<string, string>;
|
|
@@ -45,6 +69,7 @@ type HurlRequestOptions = {
|
|
|
45
69
|
auth?: AuthConfig;
|
|
46
70
|
proxy?: ProxyConfig;
|
|
47
71
|
cache?: CacheConfig;
|
|
72
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
48
73
|
signal?: AbortSignal;
|
|
49
74
|
followRedirects?: boolean;
|
|
50
75
|
onUploadProgress?: ProgressCallback;
|
|
@@ -80,7 +105,7 @@ type RequestInterceptor = (url: string, options: HurlRequestOptions) => Promise<
|
|
|
80
105
|
};
|
|
81
106
|
type ResponseInterceptor<T = unknown> = (response: HurlResponse<T>) => Promise<HurlResponse<T>> | HurlResponse<T>;
|
|
82
107
|
type ErrorInterceptor = (error: HurlError) => Promise<HurlError | HurlResponse> | HurlError | HurlResponse;
|
|
83
|
-
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR';
|
|
108
|
+
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR' | 'CIRCUIT_OPEN';
|
|
84
109
|
declare class HurlError extends Error {
|
|
85
110
|
type: HurlErrorType;
|
|
86
111
|
status?: number;
|
|
@@ -109,6 +134,9 @@ type HurlInstance = {
|
|
|
109
134
|
head(url: string, options?: HurlRequestOptions): Promise<HurlResponse<void>>;
|
|
110
135
|
options<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
111
136
|
request<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
137
|
+
sse(url: string, options: SSEOptions): {
|
|
138
|
+
close(): void;
|
|
139
|
+
};
|
|
112
140
|
all<T extends unknown[]>(requests: {
|
|
113
141
|
[K in keyof T]: Promise<T[K]>;
|
|
114
142
|
}): Promise<T>;
|
|
@@ -135,9 +163,16 @@ type HurlInstance = {
|
|
|
135
163
|
extend(defaults?: HurlDefaults): HurlInstance;
|
|
136
164
|
};
|
|
137
165
|
|
|
166
|
+
declare function invalidateCache(key: string): void;
|
|
138
167
|
declare function clearCache(): void;
|
|
139
168
|
|
|
169
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
170
|
+
declare function getCircuitStats(key: string): {
|
|
171
|
+
state: CircuitState;
|
|
172
|
+
failures: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
140
175
|
declare function createInstance(initialDefaults?: HurlDefaults): HurlInstance;
|
|
141
176
|
declare const hurl: HurlInstance;
|
|
142
177
|
|
|
143
|
-
export { type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, clearCache, createInstance, hurl as default };
|
|
178
|
+
export { type CircuitBreakerConfig, type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, type SSEEvent, type SSEOptions, clearCache, createInstance, hurl as default, getCircuitStats, invalidateCache };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var B=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var He=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var Oe=(r,e)=>{for(var t in e)B(r,t,{get:e[t],enumerable:!0})},xe=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of He(e))!we.call(r,o)&&o!==t&&B(r,o,{get:()=>e[o],enumerable:!(n=Te(e,o))||n.enumerable});return r};var Ce=r=>xe(B({},"__esModule",{value:!0}),r);var _e={};Oe(_e,{HurlError:()=>d,clearCache:()=>ue,createInstance:()=>v,default:()=>Be,getCircuitStats:()=>me,invalidateCache:()=>ie});module.exports=Ce(_e);var d=class extends Error{constructor(e){super(e.message),this.name="HurlError",this.type=e.type,this.status=e.status,this.statusText=e.statusText,this.data=e.data,this.headers=e.headers,this.requestId=e.requestId,this.retries=e.retries??0}};function j(r){return new d({message:`HTTP ${r.status}: ${r.statusText}`,type:"HTTP_ERROR",...r})}function z(r,e){return new d({message:r,type:"NETWORK_ERROR",requestId:e})}function X(r,e){return new d({message:`Request timed out after ${r}ms`,type:"TIMEOUT_ERROR",requestId:e})}function J(r){return new d({message:"Request was aborted",type:"ABORT_ERROR",requestId:r})}function _(r,e){return new d({message:`Failed to parse response: ${r}`,type:"PARSE_ERROR",requestId:e})}function Y(r,e){return new d({message:`Circuit breaker is open for "${r}"`,type:"CIRCUIT_OPEN",requestId:e})}async function V(r,e){let t=r.body?.getReader(),n=parseInt(r.headers.get("content-length")??"0",10);if(!t)return new ArrayBuffer(0);let o=[],i=0;for(;;){let{done:u,value:a}=await t.read();if(u)break;o.push(a),i+=a.byteLength,e({loaded:i,total:n,percent:n>0?Math.round(i/n*100):0})}let l=new Uint8Array(i),s=0;for(let u of o)l.set(u,s),s+=u.byteLength;return l.buffer}function Z(r,e){let t=0;typeof r=="string"?t=new TextEncoder().encode(r).byteLength:r instanceof ArrayBuffer?t=r.byteLength:r instanceof Blob&&(t=r.size);let n=0;return(r instanceof ReadableStream?r:new Response(r).body).pipeThrough(new TransformStream({transform(i,l){n+=i.byteLength,e({loaded:n,total:t,percent:t>0?Math.round(n/t*100):0}),l.enqueue(i)}}))}function N(r){let e={};return r.forEach((t,n)=>{e[n]=t}),e}async function Q(r,e,t,n,o){if(t==="HEAD"||r.status===204||r.headers.get("content-length")==="0")return null;if(n)return r.body;let i=r.headers.get("content-type")??"",l=i.includes("application/octet-stream")||i.includes("image/")||i.includes("video/")||i.includes("audio/");if(o&&l)try{return await V(r,o)}catch(s){throw _(s.message,e)}try{return i.includes("application/json")?await r.json():i.includes("text/")?await r.text():l?await r.arrayBuffer():await r.text()}catch(s){throw _(s.message,e)}}function ee(r,e,t,n){let o=Date.now();return{data:r,status:e.status,statusText:e.statusText,headers:N(e.headers),requestId:t,timing:{start:n,end:o,duration:o-n},fromCache:!1}}function Pe(r){return typeof globalThis<"u"&&globalThis.Buffer?globalThis.Buffer.from(r).toString("base64"):btoa(encodeURIComponent(r).replace(/%([0-9A-F]{2})/g,(e,t)=>String.fromCharCode(parseInt(t,16))))}function q(r,e,t){if(t.type==="bearer"&&(r.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=Pe(`${t.username}:${t.password}`);r.Authorization=`Basic ${n}`}t.type==="apikey"&&(t.in==="query"?e[t.key]=t.value:r[t.key]=t.value)}function re(r){return r==null?null:typeof r=="number"?{count:r,delay:300,backoff:"exponential"}:r}function te(r,e,t){return t>=e.count||r.type==="ABORT_ERROR"?!1:e.on&&r.status?e.on.includes(r.status):!!(r.type==="NETWORK_ERROR"||r.type==="TIMEOUT_ERROR"||r.status&&r.status>=500)}async function ne(r,e){let t=r.delay??300,n=r.backoff==="exponential"?t*Math.pow(2,e):t*(e+1);await new Promise(o=>setTimeout(o,n))}var E=new Map,Ie=1e3;function L(r,e){return e?.key??r}function oe(r){let e=E.get(r);if(!e)return null;if(Date.now()>e.expiresAt)return E.delete(r),null;let t=e.response.data;return t instanceof ArrayBuffer&&(t=t.slice(0)),{...e.response,data:t,fromCache:!0}}function se(r,e,t){if(E.size>=Ie&&!E.has(r)){let n=E.keys().next().value;n!==void 0&&E.delete(n)}E.set(r,{response:e,expiresAt:Date.now()+t.ttl})}function ie(r){E.delete(r)}function ue(){E.clear()}var $=new Map;function ae(r){return $.get(r)??null}function le(r,e){$.set(r,e),e.finally(()=>$.delete(r))}function ce(r,e){console.group(`[hurl] \u2192 ${e.method??"GET"} ${r}`),e.headers&&Object.keys(e.headers).length>0&&console.log("headers:",e.headers),e.query&&console.log("query:",e.query),e.body&&console.log("body:",e.body),e.timeout&&console.log("timeout:",e.timeout),e.retry&&console.log("retry:",e.retry),console.groupEnd()}function F(r){let e=r.status>=400?"\u{1F534}":r.status>=300?"\u{1F7E1}":"\u{1F7E2}";console.group(`[hurl] ${e} ${r.status} ${r.statusText} (${r.timing.duration}ms)`),console.log("requestId:",r.requestId),r.fromCache&&console.log("served from cache"),console.log("headers:",r.headers),console.log("data:",r.data),console.groupEnd()}function fe(r){console.group("[hurl] \u{1F534} Error"),console.error(r),console.groupEnd()}var M=new Map;function A(r){return M.has(r)||M.set(r,{state:"CLOSED",failures:0,openedAt:0}),M.get(r)}function pe(r,e){let t=A(r);return t.state==="OPEN"?Date.now()-t.openedAt>=e.cooldown?(t.state="HALF_OPEN","HALF_OPEN"):"OPEN":t.state}function de(r){let e=A(r);e.state="CLOSED",e.failures=0}function ge(r,e){let t=A(r);t.failures+=1,t.failures>=e.threshold&&(t.state="OPEN",t.openedAt=Date.now())}function me(r){let e=A(r);return{state:e.state,failures:e.failures}}function ke(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(36).slice(2,10)}function G(r,e,t){let n;if(e.startsWith("http://")||e.startsWith("https://")){if(r){let i=new URL(r).origin,l=new URL(e).origin;if(i!==l)throw new Error(`Absolute URL "${e}" does not match baseUrl origin "${i}". Pass the full URL without baseUrl, or use a path-relative URL.`)}n=e}else{if(e.startsWith("//"))throw new Error("Protocol-relative URLs are not supported. Use an explicit https:// or http:// scheme.");n=r?`${r.replace(/\/$/,"")}/${e.replace(/^\//,"")}`:e}if(!t||Object.keys(t).length===0)return n;let o=new URLSearchParams;for(let[i,l]of Object.entries(t))o.set(i,String(l));return`${n}?${o.toString()}`}function Se(r){return r instanceof ReadableStream||r!==null&&typeof r=="object"&&typeof r.pipe=="function"}function qe(r,e){let t={...e.headers,...r.headers},n=r.body;return n!=null&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&!(n instanceof ArrayBuffer)&&!Se(n)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function Ae(r){if(r!=null)return r instanceof FormData||r instanceof Blob||r instanceof ArrayBuffer||typeof r=="string"||r instanceof ReadableStream||typeof r.pipe=="function"?r:JSON.stringify(r)}function De(r,e){if(e)return e;try{return new URL(r).origin}catch{return r}}async function Re(r,e,t){let n=e.requestId??ke(),o=e.method??"GET",i=Date.now(),l=re(e.retry??t.retry),s=e.debug??t.debug??!1,u=e.throwOnError??t.throwOnError??!0,a={...t.query,...e.query},H=qe(e,t),g=e.timeout??t.timeout,T=e.auth??t.auth;T&&q(H,a,T);let m=G(t.baseUrl??"",r,Object.keys(a).length>0?a:void 0);(e.proxy??t.proxy)&&s&&console.warn("[hurl] proxy option is not yet implemented. Node 18: npm install undici@6, use ProxyAgent + setGlobalDispatcher. Node 20: use ProxyAgent + setGlobalDispatcher from undici. Node 22.3+: use EnvHttpProxyAgent + setGlobalDispatcher from undici. Node 24+: set NODE_USE_ENV_PROXY=1 with HTTP_PROXY env var. See README for details.");let c=e.cache??t.cache,p=!!c&&!c.bypass&&o==="GET";if(p){let f=L(m,c),y=oe(f);if(y)return s&&F(y),y}let I=e.deduplicate??t.deduplicate??!1;if(I&&o==="GET"){let f=ae(m);if(f)return f}let R=e.circuitBreaker??t.circuitBreaker,w=R?De(m,R.key):"";if(R&&pe(w,R)==="OPEN"){if(s&&console.warn(`[hurl] circuit breaker OPEN for "${w}", fast-failing`),R.fallback){let y=Date.now();return{data:R.fallback(),status:0,statusText:"Circuit Open",headers:{},requestId:n,timing:{start:i,end:y,duration:y-i},fromCache:!1}}throw Y(w,n)}s&&ce(m,{...e,method:o});let x=async f=>{let y=null,K=!1,k=new AbortController,O=e.signal,S=null;O&&(O.aborted?k.abort(O.reason):(S=()=>k.abort(O.reason),O.addEventListener("abort",S,{once:!0}))),g&&(y=setTimeout(()=>{K=!0,k.abort()},g));try{let h=Ae(e.body),b=e.onUploadProgress??t.onUploadProgress;h!==void 0&&b&&(e.body instanceof FormData?s&&console.warn("[hurl] onUploadProgress is not supported for FormData bodies. Use XMLHttpRequest for FormData upload progress."):h=Z(h,b));let P=await fetch(m,{method:o,headers:H,body:h,signal:k.signal,redirect:e.followRedirects??!0?"follow":"manual"}),W=await Q(P,n,o,e.stream??!1,e.onDownloadProgress??t.onDownloadProgress);if(!P.ok&&u)throw j({status:P.status,statusText:P.statusText,data:W,headers:N(P.headers),requestId:n,retries:f});let U=ee(W,P,n,i);return p&&c&&se(L(m,c),U,c),s&&F(U),U}catch(h){let b;if(h instanceof d?b=h:h.name==="AbortError"||h.code==="ABORT_ERR"?b=K?X(g,n):J(n):b=z(h.message,n),b.retries=f,l&&te(b,l,f))return s&&console.log(`[hurl] retrying (${f+1}/${l.count})...`),await ne(l,f),x(f+1);throw s&&fe(b),b}finally{y&&clearTimeout(y),S&&O&&O.removeEventListener("abort",S)}},C=R?x(0).then(f=>(de(w),f),f=>{throw f instanceof d&&f.type!=="ABORT_ERROR"&&f.type!=="CIRCUIT_OPEN"&&ge(w,R),f}):x(0);return I&&o==="GET"&&le(m,C),C}function D(){let r=[];return{use(e){return r.push(e),()=>{let t=r.indexOf(e);t!==-1&&r.splice(t,1)}},clear(){r.length=0},getAll(){return[...r]}}}async function ye(r,e,t){let n={url:e,options:t};for(let o of r)n=await o(n.url,n.options);return n}async function he(r,e){let t=e;for(let n of r)t=await n(t);return t}async function be(r,e){let t=e;for(let n of r)t instanceof d&&(t=await n(t));return t}function ve(r){let e={event:"message",id:"",data:""},t=r.split(`
|
|
2
|
+
`);for(let n of t)if(n.startsWith("data:")){let o=n.slice(5).trim();if(o==="[DONE]")return e._done=!0,e;e.data=(e.data?e.data+`
|
|
3
|
+
`:"")+o}else if(n.startsWith("event:"))e.event=n.slice(6).trim();else if(n.startsWith("id:"))e.id=n.slice(3).trim();else if(n.startsWith("retry:")){let o=parseInt(n.slice(6).trim(),10);isNaN(o)||(e.retry=o)}return e}function Ee(r,e,t){let n=new AbortController;e.signal&&(e.signal.aborted?n.abort():e.signal.addEventListener("abort",()=>n.abort(),{once:!0}));let o={...t.headers,...e.headers,Accept:"text/event-stream","Cache-Control":"no-cache"},i={...t.query,...e.query},l=e.auth??t.auth;l&&q(o,i,l),e.body!==void 0&&e.body!==null&&(o["Content-Type"]=o["Content-Type"]??"application/json");let s=G(t.baseUrl??"",r,Object.keys(i).length>0?i:void 0),u=e.method??"GET",a=e.body===void 0||e.body===null?void 0:typeof e.body=="string"?e.body:JSON.stringify(e.body);async function H(){let g;try{g=await fetch(s,{method:u,headers:o,body:a,signal:n.signal})}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)));return}if(!g.ok){e.onError?.(new Error(`HTTP ${g.status}: ${g.statusText}`));return}if(!g.body){e.onError?.(new Error("Response body is empty"));return}e.onOpen?.();let T=g.body.getReader(),m=new TextDecoder,c="";try{for(;;){let{done:p,value:I}=await T.read();if(p)break;c+=m.decode(I,{stream:!0});let R=c.split(`
|
|
4
|
+
|
|
5
|
+
`);c=R.pop()??"";for(let w of R){let x=w.trim();if(!x)continue;let C=ve(x);if(C._done){e.onDone?.(),n.abort();return}C.data!==void 0&&e.onMessage(C)}}}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)))}finally{T.releaseLock()}e.onDone?.()}return H(),{close(){n.abort()}}}function v(r={}){let e={...r},t=D(),n=D(),o=D();async function i(s,u={}){let a=s,H=u,g=t.getAll(),T=n.getAll(),m=o.getAll();if(g.length>0){let c=await ye(g,s,u);a=c.url,H=c.options}try{let c=await Re(a,H,e);return T.length>0?await he(T,c):c}catch(c){if(c instanceof d&&m.length>0){let p=await be(m,c);if(!(p instanceof d))return p;throw p}throw c}}return{request:i,get(s,u){return i(s,{...u,method:"GET"})},post(s,u,a){return i(s,{...a,method:"POST",body:u})},put(s,u,a){return i(s,{...a,method:"PUT",body:u})},patch(s,u,a){return i(s,{...a,method:"PATCH",body:u})},delete(s,u){return i(s,{...u,method:"DELETE"})},head(s,u){return i(s,{...u,method:"HEAD"})},options(s,u){return i(s,{...u,method:"OPTIONS"})},sse(s,u){return Ee(s,u,e)},all(s){return Promise.all(s)},defaults:{set(s){e={...e,...s}},get(){return{...e}},reset(){e={...r}}},interceptors:{request:{use:t.use.bind(t),clear:t.clear.bind(t)},response:{use:n.use.bind(n),clear:n.clear.bind(n)},error:{use:o.use.bind(o),clear:o.clear.bind(o)}},create(s){return v({...e,...s})},extend(s){let u=v({...e,...s});return t.getAll().forEach(a=>u.interceptors.request.use(a)),n.getAll().forEach(a=>u.interceptors.response.use(a)),o.getAll().forEach(a=>u.interceptors.error.use(a)),u}}}var Ue=v(),Be=Ue;0&&(module.exports={HurlError,clearCache,createInstance,getCircuitStats,invalidateCache});
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=class extends Error{constructor(e){super(e.message),this.name="HurlError",this.type=e.type,this.status=e.status,this.statusText=e.statusText,this.data=e.data,this.headers=e.headers,this.requestId=e.requestId,this.retries=e.retries??0}};function W(r){return new d({message:`HTTP ${r.status}: ${r.statusText}`,type:"HTTP_ERROR",...r})}function j(r,e){return new d({message:r,type:"NETWORK_ERROR",requestId:e})}function z(r,e){return new d({message:`Request timed out after ${r}ms`,type:"TIMEOUT_ERROR",requestId:e})}function X(r){return new d({message:"Request was aborted",type:"ABORT_ERROR",requestId:r})}function U(r,e){return new d({message:`Failed to parse response: ${r}`,type:"PARSE_ERROR",requestId:e})}function J(r,e){return new d({message:`Circuit breaker is open for "${r}"`,type:"CIRCUIT_OPEN",requestId:e})}async function Y(r,e){let t=r.body?.getReader(),n=parseInt(r.headers.get("content-length")??"0",10);if(!t)return new ArrayBuffer(0);let s=[],i=0;for(;;){let{done:u,value:a}=await t.read();if(u)break;s.push(a),i+=a.byteLength,e({loaded:i,total:n,percent:n>0?Math.round(i/n*100):0})}let l=new Uint8Array(i),o=0;for(let u of s)l.set(u,o),o+=u.byteLength;return l.buffer}function V(r,e){let t=0;typeof r=="string"?t=new TextEncoder().encode(r).byteLength:r instanceof ArrayBuffer?t=r.byteLength:r instanceof Blob&&(t=r.size);let n=0;return(r instanceof ReadableStream?r:new Response(r).body).pipeThrough(new TransformStream({transform(i,l){n+=i.byteLength,e({loaded:n,total:t,percent:t>0?Math.round(n/t*100):0}),l.enqueue(i)}}))}function B(r){let e={};return r.forEach((t,n)=>{e[n]=t}),e}async function Z(r,e,t,n,s){if(t==="HEAD"||r.status===204||r.headers.get("content-length")==="0")return null;if(n)return r.body;let i=r.headers.get("content-type")??"",l=i.includes("application/octet-stream")||i.includes("image/")||i.includes("video/")||i.includes("audio/");if(s&&l)try{return await Y(r,s)}catch(o){throw U(o.message,e)}try{return i.includes("application/json")?await r.json():i.includes("text/")?await r.text():l?await r.arrayBuffer():await r.text()}catch(o){throw U(o.message,e)}}function Q(r,e,t,n){let s=Date.now();return{data:r,status:e.status,statusText:e.statusText,headers:B(e.headers),requestId:t,timing:{start:n,end:s,duration:s-n},fromCache:!1}}function ye(r){return typeof globalThis<"u"&&globalThis.Buffer?globalThis.Buffer.from(r).toString("base64"):btoa(encodeURIComponent(r).replace(/%([0-9A-F]{2})/g,(e,t)=>String.fromCharCode(parseInt(t,16))))}function q(r,e,t){if(t.type==="bearer"&&(r.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=ye(`${t.username}:${t.password}`);r.Authorization=`Basic ${n}`}t.type==="apikey"&&(t.in==="query"?e[t.key]=t.value:r[t.key]=t.value)}function ee(r){return r==null?null:typeof r=="number"?{count:r,delay:300,backoff:"exponential"}:r}function re(r,e,t){return t>=e.count||r.type==="ABORT_ERROR"?!1:e.on&&r.status?e.on.includes(r.status):!!(r.type==="NETWORK_ERROR"||r.type==="TIMEOUT_ERROR"||r.status&&r.status>=500)}async function te(r,e){let t=r.delay??300,n=r.backoff==="exponential"?t*Math.pow(2,e):t*(e+1);await new Promise(s=>setTimeout(s,n))}var E=new Map,he=1e3;function _(r,e){return e?.key??r}function ne(r){let e=E.get(r);if(!e)return null;if(Date.now()>e.expiresAt)return E.delete(r),null;let t=e.response.data;return t instanceof ArrayBuffer&&(t=t.slice(0)),{...e.response,data:t,fromCache:!0}}function oe(r,e,t){if(E.size>=he&&!E.has(r)){let n=E.keys().next().value;n!==void 0&&E.delete(n)}E.set(r,{response:e,expiresAt:Date.now()+t.ttl})}function be(r){E.delete(r)}function Ee(){E.clear()}var N=new Map;function se(r){return N.get(r)??null}function ie(r,e){N.set(r,e),e.finally(()=>N.delete(r))}function ue(r,e){console.group(`[hurl] \u2192 ${e.method??"GET"} ${r}`),e.headers&&Object.keys(e.headers).length>0&&console.log("headers:",e.headers),e.query&&console.log("query:",e.query),e.body&&console.log("body:",e.body),e.timeout&&console.log("timeout:",e.timeout),e.retry&&console.log("retry:",e.retry),console.groupEnd()}function L(r){let e=r.status>=400?"\u{1F534}":r.status>=300?"\u{1F7E1}":"\u{1F7E2}";console.group(`[hurl] ${e} ${r.status} ${r.statusText} (${r.timing.duration}ms)`),console.log("requestId:",r.requestId),r.fromCache&&console.log("served from cache"),console.log("headers:",r.headers),console.log("data:",r.data),console.groupEnd()}function ae(r){console.group("[hurl] \u{1F534} Error"),console.error(r),console.groupEnd()}var $=new Map;function A(r){return $.has(r)||$.set(r,{state:"CLOSED",failures:0,openedAt:0}),$.get(r)}function le(r,e){let t=A(r);return t.state==="OPEN"?Date.now()-t.openedAt>=e.cooldown?(t.state="HALF_OPEN","HALF_OPEN"):"OPEN":t.state}function ce(r){let e=A(r);e.state="CLOSED",e.failures=0}function fe(r,e){let t=A(r);t.failures+=1,t.failures>=e.threshold&&(t.state="OPEN",t.openedAt=Date.now())}function Te(r){let e=A(r);return{state:e.state,failures:e.failures}}function He(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(36).slice(2,10)}function F(r,e,t){let n;if(e.startsWith("http://")||e.startsWith("https://")){if(r){let i=new URL(r).origin,l=new URL(e).origin;if(i!==l)throw new Error(`Absolute URL "${e}" does not match baseUrl origin "${i}". Pass the full URL without baseUrl, or use a path-relative URL.`)}n=e}else{if(e.startsWith("//"))throw new Error("Protocol-relative URLs are not supported. Use an explicit https:// or http:// scheme.");n=r?`${r.replace(/\/$/,"")}/${e.replace(/^\//,"")}`:e}if(!t||Object.keys(t).length===0)return n;let s=new URLSearchParams;for(let[i,l]of Object.entries(t))s.set(i,String(l));return`${n}?${s.toString()}`}function we(r){return r instanceof ReadableStream||r!==null&&typeof r=="object"&&typeof r.pipe=="function"}function Oe(r,e){let t={...e.headers,...r.headers},n=r.body;return n!=null&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&!(n instanceof ArrayBuffer)&&!we(n)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function xe(r){if(r!=null)return r instanceof FormData||r instanceof Blob||r instanceof ArrayBuffer||typeof r=="string"||r instanceof ReadableStream||typeof r.pipe=="function"?r:JSON.stringify(r)}function Ce(r,e){if(e)return e;try{return new URL(r).origin}catch{return r}}async function pe(r,e,t){let n=e.requestId??He(),s=e.method??"GET",i=Date.now(),l=ee(e.retry??t.retry),o=e.debug??t.debug??!1,u=e.throwOnError??t.throwOnError??!0,a={...t.query,...e.query},H=Oe(e,t),g=e.timeout??t.timeout,T=e.auth??t.auth;T&&q(H,a,T);let m=F(t.baseUrl??"",r,Object.keys(a).length>0?a:void 0);(e.proxy??t.proxy)&&o&&console.warn("[hurl] proxy option is not yet implemented. Node 18: npm install undici@6, use ProxyAgent + setGlobalDispatcher. Node 20: use ProxyAgent + setGlobalDispatcher from undici. Node 22.3+: use EnvHttpProxyAgent + setGlobalDispatcher from undici. Node 24+: set NODE_USE_ENV_PROXY=1 with HTTP_PROXY env var. See README for details.");let c=e.cache??t.cache,p=!!c&&!c.bypass&&s==="GET";if(p){let f=_(m,c),y=ne(f);if(y)return o&&L(y),y}let I=e.deduplicate??t.deduplicate??!1;if(I&&s==="GET"){let f=se(m);if(f)return f}let R=e.circuitBreaker??t.circuitBreaker,w=R?Ce(m,R.key):"";if(R&&le(w,R)==="OPEN"){if(o&&console.warn(`[hurl] circuit breaker OPEN for "${w}", fast-failing`),R.fallback){let y=Date.now();return{data:R.fallback(),status:0,statusText:"Circuit Open",headers:{},requestId:n,timing:{start:i,end:y,duration:y-i},fromCache:!1}}throw J(w,n)}o&&ue(m,{...e,method:s});let x=async f=>{let y=null,G=!1,k=new AbortController,O=e.signal,S=null;O&&(O.aborted?k.abort(O.reason):(S=()=>k.abort(O.reason),O.addEventListener("abort",S,{once:!0}))),g&&(y=setTimeout(()=>{G=!0,k.abort()},g));try{let h=xe(e.body),b=e.onUploadProgress??t.onUploadProgress;h!==void 0&&b&&(e.body instanceof FormData?o&&console.warn("[hurl] onUploadProgress is not supported for FormData bodies. Use XMLHttpRequest for FormData upload progress."):h=V(h,b));let P=await fetch(m,{method:s,headers:H,body:h,signal:k.signal,redirect:e.followRedirects??!0?"follow":"manual"}),K=await Z(P,n,s,e.stream??!1,e.onDownloadProgress??t.onDownloadProgress);if(!P.ok&&u)throw W({status:P.status,statusText:P.statusText,data:K,headers:B(P.headers),requestId:n,retries:f});let v=Q(K,P,n,i);return p&&c&&oe(_(m,c),v,c),o&&L(v),v}catch(h){let b;if(h instanceof d?b=h:h.name==="AbortError"||h.code==="ABORT_ERR"?b=G?z(g,n):X(n):b=j(h.message,n),b.retries=f,l&&re(b,l,f))return o&&console.log(`[hurl] retrying (${f+1}/${l.count})...`),await te(l,f),x(f+1);throw o&&ae(b),b}finally{y&&clearTimeout(y),S&&O&&O.removeEventListener("abort",S)}},C=R?x(0).then(f=>(ce(w),f),f=>{throw f instanceof d&&f.type!=="ABORT_ERROR"&&f.type!=="CIRCUIT_OPEN"&&fe(w,R),f}):x(0);return I&&s==="GET"&&ie(m,C),C}function D(){let r=[];return{use(e){return r.push(e),()=>{let t=r.indexOf(e);t!==-1&&r.splice(t,1)}},clear(){r.length=0},getAll(){return[...r]}}}async function de(r,e,t){let n={url:e,options:t};for(let s of r)n=await s(n.url,n.options);return n}async function ge(r,e){let t=e;for(let n of r)t=await n(t);return t}async function me(r,e){let t=e;for(let n of r)t instanceof d&&(t=await n(t));return t}function Pe(r){let e={event:"message",id:"",data:""},t=r.split(`
|
|
2
|
+
`);for(let n of t)if(n.startsWith("data:")){let s=n.slice(5).trim();if(s==="[DONE]")return e._done=!0,e;e.data=(e.data?e.data+`
|
|
3
|
+
`:"")+s}else if(n.startsWith("event:"))e.event=n.slice(6).trim();else if(n.startsWith("id:"))e.id=n.slice(3).trim();else if(n.startsWith("retry:")){let s=parseInt(n.slice(6).trim(),10);isNaN(s)||(e.retry=s)}return e}function Re(r,e,t){let n=new AbortController;e.signal&&(e.signal.aborted?n.abort():e.signal.addEventListener("abort",()=>n.abort(),{once:!0}));let s={...t.headers,...e.headers,Accept:"text/event-stream","Cache-Control":"no-cache"},i={...t.query,...e.query},l=e.auth??t.auth;l&&q(s,i,l),e.body!==void 0&&e.body!==null&&(s["Content-Type"]=s["Content-Type"]??"application/json");let o=F(t.baseUrl??"",r,Object.keys(i).length>0?i:void 0),u=e.method??"GET",a=e.body===void 0||e.body===null?void 0:typeof e.body=="string"?e.body:JSON.stringify(e.body);async function H(){let g;try{g=await fetch(o,{method:u,headers:s,body:a,signal:n.signal})}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)));return}if(!g.ok){e.onError?.(new Error(`HTTP ${g.status}: ${g.statusText}`));return}if(!g.body){e.onError?.(new Error("Response body is empty"));return}e.onOpen?.();let T=g.body.getReader(),m=new TextDecoder,c="";try{for(;;){let{done:p,value:I}=await T.read();if(p)break;c+=m.decode(I,{stream:!0});let R=c.split(`
|
|
4
|
+
|
|
5
|
+
`);c=R.pop()??"";for(let w of R){let x=w.trim();if(!x)continue;let C=Pe(x);if(C._done){e.onDone?.(),n.abort();return}C.data!==void 0&&e.onMessage(C)}}}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)))}finally{T.releaseLock()}e.onDone?.()}return H(),{close(){n.abort()}}}function M(r={}){let e={...r},t=D(),n=D(),s=D();async function i(o,u={}){let a=o,H=u,g=t.getAll(),T=n.getAll(),m=s.getAll();if(g.length>0){let c=await de(g,o,u);a=c.url,H=c.options}try{let c=await pe(a,H,e);return T.length>0?await ge(T,c):c}catch(c){if(c instanceof d&&m.length>0){let p=await me(m,c);if(!(p instanceof d))return p;throw p}throw c}}return{request:i,get(o,u){return i(o,{...u,method:"GET"})},post(o,u,a){return i(o,{...a,method:"POST",body:u})},put(o,u,a){return i(o,{...a,method:"PUT",body:u})},patch(o,u,a){return i(o,{...a,method:"PATCH",body:u})},delete(o,u){return i(o,{...u,method:"DELETE"})},head(o,u){return i(o,{...u,method:"HEAD"})},options(o,u){return i(o,{...u,method:"OPTIONS"})},sse(o,u){return Re(o,u,e)},all(o){return Promise.all(o)},defaults:{set(o){e={...e,...o}},get(){return{...e}},reset(){e={...r}}},interceptors:{request:{use:t.use.bind(t),clear:t.clear.bind(t)},response:{use:n.use.bind(n),clear:n.clear.bind(n)},error:{use:s.use.bind(s),clear:s.clear.bind(s)}},create(o){return M({...e,...o})},extend(o){let u=M({...e,...o});return t.getAll().forEach(a=>u.interceptors.request.use(a)),n.getAll().forEach(a=>u.interceptors.response.use(a)),s.getAll().forEach(a=>u.interceptors.error.use(a)),u}}}var Ie=M(),Ir=Ie;export{d as HurlError,Ee as clearCache,M as createInstance,Ir as default,Te as getCircuitStats,be as invalidateCache};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firekid/hurl",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Zero-dependency HTTP client for Node.js and edge runtimes. Built on fetch. The modern replacement for axios, request, got, node-fetch, and ky. Works on Cloudflare Workers, Vercel Edge, Deno, and Bun.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -106,4 +106,4 @@
|
|
|
106
106
|
"typescript": "^5.0.0",
|
|
107
107
|
"vitest": "^1.0.0"
|
|
108
108
|
}
|
|
109
|
-
}
|
|
109
|
+
}
|