├── .all-contributorsrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── package.json ├── pnpm-lock.yaml ├── src ├── api.test.ts ├── api.ts ├── constants.ts ├── index.ts ├── internals.ts ├── primitives.test.ts ├── primitives.ts ├── test.d.ts └── types.ts ├── tsconfig.json └── vitest.config.mts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "make-service", 3 | "projectOwner": "gustavoguichard", 4 | "files": ["README.md"], 5 | "commitType": "docs", 6 | "commitConvention": "angular", 7 | "contributorsPerLine": 7, 8 | "contributors": [ 9 | { 10 | "login": "gustavoguichard", 11 | "name": "Guga Guichard", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/566971?v=4", 13 | "profile": "https://github.com/gustavoguichard", 14 | "contributions": [ 15 | "code", 16 | "projectManagement", 17 | "promotion", 18 | "maintenance", 19 | "doc", 20 | "bug", 21 | "infra", 22 | "question", 23 | "research", 24 | "review", 25 | "ideas", 26 | "example" 27 | ] 28 | }, 29 | { 30 | "login": "danielweinmann", 31 | "name": "Daniel Weinmann", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/204765?v=4", 33 | "profile": "https://www.linkedin.com/in/danielweinmann", 34 | "contributions": ["code", "promotion", "ideas", "doc", "bug", "review"] 35 | }, 36 | { 37 | "login": "iamandrewluca", 38 | "name": "Andrei Luca", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/1881266?v=4", 40 | "profile": "https://luca.md", 41 | "contributions": [ 42 | "doc", 43 | "code", 44 | "promotion", 45 | "maintenance", 46 | "bug", 47 | "ideas" 48 | ] 49 | }, 50 | { 51 | "login": "diogob", 52 | "name": "Diogo Biazus", 53 | "avatar_url": "https://avatars.githubusercontent.com/u/20662?v=4", 54 | "profile": "https://github.com/diogob", 55 | "contributions": ["code", "doc"] 56 | }, 57 | { 58 | "login": "garusis", 59 | "name": "Marcos Javier Alvarez Maestre", 60 | "avatar_url": "https://avatars.githubusercontent.com/u/15615652?v=4", 61 | "profile": "https://github.com/garusis", 62 | "contributions": ["code", "bug"] 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | name: Run tests 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: 🛑 Cancel Previous Runs 13 | uses: styfle/cancel-workflow-action@0.9.1 14 | 15 | - name: ⬇️ Checkout repo 16 | uses: actions/checkout@v3 17 | 18 | - name: 📦 Manually Install pnpm 19 | run: npm install -g pnpm@10 20 | 21 | - name: 🪡 Install Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '22' 25 | cache: 'pnpm' 26 | 27 | - name: 📦 Install Dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: ⚡ Run tests 31 | run: pnpm run test 32 | 33 | - name: 🚦 Lint 34 | run: pnpm run lint 35 | 36 | - name: 🧙🏿TSC 37 | run: pnpm run tsc 38 | 39 | - name: 📥 Generate npm package 40 | run: pnpm run build 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules 3 | dist/ 4 | npm 5 | tsc/ 6 | .DS_Store 7 | .idea 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gustavo Guichard 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://img.shields.io/npm/v/make-service)](https://www.npmjs.org/package/make-service) 2 | ![Library size](https://img.shields.io/bundlephobia/minzip/make-service) 3 | [![All Contributors](https://img.shields.io/github/all-contributors/gustavoguichard/make-service?color=0375b6&style=flat-square)](#contributors) 4 | 5 | # make-service 6 | 7 | A type-safe thin wrapper around the `fetch` API to better interact with external APIs. 8 | 9 | It adds a set of little features and allows you to parse responses with [standard-schema libraries](https://standardschema.dev). 10 | 11 | ## Features 12 | - 🤩 Type-safe return of `response.json()` and `response.text()`. Defaults to `unknown` instead of `any`. 13 | - 🚦 Easily setup an API with a `baseURL` and common options like `headers` for every request. 14 | - 🏗️ Compose URL from the base by just calling the endpoints and an object-like `query`. 15 | - 🐾 Replaces URL wildcards with a **strongly-typed** object of `params`. 16 | - 🧙‍♀️ Automatically stringifies the `body` of a request so you can give it a JSON-like structure. 17 | - 🐛 Accepts a `trace` function for debugging. 18 | - 🔥 It can transform responses and payloads back and forth to (e.g.) support interchangeability of casing styles (kebab-case -> camelCase -> snake_case -> kebab-case). 19 | 20 | ## Example 21 | 22 | ```ts 23 | const service = makeService("https://example.com/api", { 24 | headers: { 25 | Authorization: "Bearer 123", 26 | }, 27 | }); 28 | 29 | const response = await service.get("/users") 30 | const users = await response.json(usersSchema); 31 | // ^? User[] 32 | ``` 33 | 34 | # Table of Contents 35 | - [Installation](#installation) 36 | - [API](#api) 37 | - [makeService](#makeservice) 38 | - [Type-checking the response body](#type-checking-the-response-body) 39 | - [Runtime type-checking and parsing the response body](#runtime-type-checking-and-parsing-the-response-body) 40 | - [Dealing with parsing errors](#dealing-with-parsing-errors) 41 | - [Supported HTTP Verbs](#supported-http-verbs) 42 | - [Headers](#headers) 43 | - [Passing a function as `headers`](#passing-a-function-as-headers) 44 | - [Deleting a previously set header](#deleting-a-previously-set-header) 45 | - [Base URL](#base-url) 46 | - [Transformers](#transformers) 47 | - [Request transformers](#request-transformers) 48 | - [Response transformers](#response-transformers) 49 | - [Body](#body) 50 | - [Query](#query) 51 | - [Params](#params) 52 | - [Trace](#trace) 53 | - [makeFetcher](#makefetcher) 54 | - [enhancedFetch](#enhancedfetch) 55 | - [typedResponse](#typedresponse) 56 | - [Transform the payload](#transform-the-payload) 57 | - [Other available primitives](#other-available-primitives) 58 | - [addQueryToURL](#addquerytourl) 59 | - [ensureStringBody](#ensurestringbody) 60 | - [makeGetApiURL](#makegetapiurl) 61 | - [mergeHeaders](#mergeheaders) 62 | - [replaceURLParams](#replaceurlparams) 63 | - [Contributors](#contributors) 64 | - [Acknowledgements](#acknowledgements) 65 | 66 | # Installation 67 | 68 | ```sh 69 | npm install make-service 70 | ``` 71 | Or you can use it with Deno: 72 | 73 | ```ts 74 | import { makeService } from "https://deno.land/x/make_service/mod.ts"; 75 | ``` 76 | 77 | # API 78 | 79 | This library exports the `makeService` function and some primitives used to build it. You can use the primitives as you wish but the `makeService` will have all the features combined. 80 | 81 | ## makeService 82 | The main function of this lib is built on top of the primitives described in the following sections. It allows you to create a service object with a `baseURL` and common options like `headers` for every request. 83 | 84 | This service object can be called with every HTTP method and it will return a [`typedResponse`](#typedresponse). 85 | 86 | ```ts 87 | import { makeService } from 'make-service' 88 | 89 | const service = makeService("https://example.com/api", { 90 | headers :{ 91 | authorization: "Bearer 123", 92 | }, 93 | }) 94 | 95 | const response = await service.get("/users") 96 | const json = await response.json() 97 | // ^? unknown 98 | ``` 99 | 100 | On the example above, the request will be sent with the following arguments: 101 | 102 | ```ts 103 | // "https://example.com/api/users" 104 | // { 105 | // method: 'GET', 106 | // headers: { 107 | // 'authorization': 'Bearer 123', 108 | // } 109 | // } 110 | ``` 111 | 112 | ### Type-checking the response body 113 | The `response` object returned by the `service` can be type-casted with a given generic type. This will type-check the `response.json()` and `response.text()` methods. 114 | 115 | ```ts 116 | const response = await service.get("/users") 117 | const users = await response.json<{ data: User[] }>() 118 | // ^? { data: User[] } 119 | const content = await response.text<`${string}@${string}`>() 120 | // ^? `${string}@${string}` 121 | ``` 122 | 123 | ### Runtime type-checking and parsing the response body 124 | 125 | Its [`typedResponse`](#typedresponse) can also be parsed with a standard schema parser. Here follows a little more complex example with Zod: 126 | ```ts 127 | const response = await service.get("/users") 128 | const json = await response.json( 129 | z.object({ 130 | data: z.object({ 131 | users: z.array(z.object({ 132 | name: z.string() 133 | })) 134 | }) 135 | }) 136 | // transformed and caught 137 | .transform(({ data: { users } }) => users) 138 | .catch([]) 139 | ) 140 | // type of json will be { name: string }[] 141 | 142 | const content = await response.text(z.string().email()) 143 | // It will throw an error if the response.text is not a valid email 144 | ``` 145 | You can transform any `Response` in a `TypedResponse` like that by using the [`typedResponse`](#typedresponse) function. 146 | 147 | #### Dealing with parsing errors 148 | If the response body does not match the given schema, it will throw a **ParseResponseError** which will have a message carrying all the parsing issues and its messages. You can catch it to inspect the issues: 149 | 150 | ```ts 151 | try { 152 | const response = await service.get("/users") 153 | return await response.json(userSchema) 154 | } catch(error) { 155 | if (error instanceof ParseResponseError) { 156 | console.log(error.issues) 157 | } 158 | } 159 | ``` 160 | 161 | ### Supported HTTP Verbs 162 | Other than the `get` it also accepts more HTTP verbs: 163 | ```ts 164 | await service.get("/users") 165 | await service.post("/users", { body: { name: "John" } }) 166 | await service.put("/users/1", { body: { name: "John" } }) 167 | await service.patch("/users/1", { body: { name: "John" } }) 168 | await service.delete("/users/1") 169 | await service.head("/users") 170 | await service.options("/users") 171 | ``` 172 | 173 | ### Headers 174 | The `headers` argument can be a `Headers` object, a `Record`, or an array of `[key, value]` tuples (entries). 175 | The `headers` option on `baseOptions` and the `headers` argument will be merged together, with the `headers` argument taking precedence. 176 | 177 | ```ts 178 | import { makeService } from 'make-service' 179 | 180 | const service = makeService("https://example.com/api", { 181 | headers: new Headers({ 182 | authorization: "Bearer 123", 183 | accept: "*/*", 184 | }), 185 | }) 186 | 187 | const response = await service.get("/users", { 188 | headers: [['accept', 'application/json']], 189 | }) 190 | 191 | // It will call "https://example.com/api/users" 192 | // with headers: { authorization: "Bearer 123", accept: "application/json" } 193 | ``` 194 | 195 | #### Passing a function as `headers` 196 | The `headers` option on `baseOptions` can be a sync or async function that will run in every request before it gets merged with the other headers. 197 | This is particularly useful when you need to send a refreshed token or add a timestamp to the request. 198 | 199 | ```ts 200 | import { makeService } from 'make-service' 201 | 202 | declare getAuthorizationToken: () => Promise 203 | 204 | const service = makeService("https://example.com/api", { 205 | headers: async () => ({ 206 | authorization: await getAuthorizationToken(), 207 | }), 208 | }) 209 | 210 | ``` 211 | 212 | #### Deleting a previously set header 213 | In case you want to delete a header previously set you can pass `undefined` or `'undefined'` as its value: 214 | ```ts 215 | const service = makeService("https://example.com/api", { 216 | headers: { authorization: "Bearer 123" }, 217 | }) 218 | 219 | const response = await service.get("/users", { 220 | headers: new Headers({ authorization: 'undefined' }), 221 | }) 222 | // headers will be empty. 223 | ``` 224 | Note: Don't forget headers are case insensitive. 225 | ```ts 226 | const headers = new Headers({ 'Content-Type': 'application/json' }) 227 | Object.fromEntries(headers) // equals to: { 'content-type': 'application/json' } 228 | ``` 229 | 230 | All the features above are done by using the [`mergeHeaders`](#mergeheaders) function internally. 231 | 232 | ### Base URL 233 | The service function can receive a `string` or `URL` as base `url` and it will be able to merge them correctly with the given path: 234 | 235 | ```ts 236 | import { makeService } from 'make-service' 237 | 238 | const service = makeService(new URL("https://example.com/api")) 239 | 240 | const response = await service.get("/users?admin=true") 241 | 242 | // It will call "https://example.com/api/users?admin=true" 243 | ``` 244 | You can use the [`makeGetApiUrl`](#makegetapiurl) method to do that kind of URL composition. 245 | 246 | ### Transformers 247 | `makeService` can also receive `requestTransformer` and `responseTransformer` as options that will be applied to all requests. 248 | 249 | #### Request transformers 250 | You can transform the request in any way you want passing a transformer function as a parameter. This will be applied to all requests for that service. 251 | A useful example is to implement a global request timeout for all endpoints of a service: 252 | 253 | ```ts 254 | function timeoutRequestIn30Seconds( 255 | request: EnhancedRequestInit, 256 | ): EnhancedRequestInit { 257 | const terminator = new AbortController() 258 | terminator.signal.throwIfAborted() 259 | setTimeout(() => terminator.abort(), 30000) 260 | 261 | return { 262 | ...request, 263 | signal: terminator.signal, 264 | } 265 | } 266 | 267 | const service = makeService('https://example.com/api', { requestTransformer: timeoutRequestIn30Seconds }) 268 | 269 | const response = await service.get("/users") 270 | 271 | // It will call "https://example.com/api/users" aborting (and throwing an exception) if it takes more than 30 seconds. 272 | ``` 273 | 274 | Please note that the `headers` option will be applied _after_ the request transformer runs. If you're using a request transformer, we recommend adding custom headers inside your transformer instead of using both options. 275 | 276 | #### Response transformers 277 | You can also transform the response in any way you want, like: 278 | 279 | ```ts 280 | const service = makeService('https://example.com/api', { 281 | responseTransformer: (response) => ({ ...response, statusText: 'It worked!' }), 282 | }) 283 | 284 | const response = await service.get("/users") 285 | 286 | // response.statusText will be 'It worked!' 287 | ``` 288 | 289 | ### Body 290 | The function can also receive a `body` object that will be stringified and sent as the request body: 291 | 292 | ```ts 293 | import { makeService } from 'make-service' 294 | 295 | const service = makeService("https://example.com/api") 296 | const response = await service.post("/users", { 297 | body: { person: { firstName: "John", lastName: "Doe" } }, 298 | }) 299 | 300 | // It will make a POST request to "https://example.com/api/users" 301 | // with stringified body: "{\"person\":{\"firstName\":\"John\",\"lastName\":\"Doe\"}}" 302 | ``` 303 | 304 | You can also pass any other accepted `BodyInit` values as body, such as `FormData`, `URLSearchParams`, `Blob`, `ReadableStream`, `ArrayBuffer`, etc. 305 | 306 | ```ts 307 | import { makeService } from 'make-service' 308 | 309 | const service = makeService("https://example.com/api") 310 | const formData = new FormData([["name", "John"], ["lastName", "Doe"]]) 311 | const response = await service.post("/users", { 312 | body: formData, 313 | }) 314 | ``` 315 | This is achieved by using the [`ensureStringBody`](#ensurestringbody) function internally. 316 | 317 | ### Query 318 | The service can also receive an `query` object that can be a `string`, a `URLSearchParams`, or an array of entries and it'll add that to the path as queryString: 319 | 320 | ```ts 321 | import { makeService } from 'make-service' 322 | 323 | const service = makeService(new URL("https://example.com/api")) 324 | 325 | const response = await service.get("/users?admin=true", { 326 | query: new URLSearchParams({ page: "2" }), 327 | }) 328 | 329 | // It will call "https://example.com/api/users?admin=true&page=2" 330 | 331 | // It could also be: 332 | const response = await service.get("/users?admin=true", { 333 | query: [["page", "2"]], 334 | }) 335 | // or: 336 | const response = await service.get("/users?admin=true", { 337 | query: "page=2", 338 | }) 339 | ``` 340 | This is achieved by using the [`addQueryToURL`](#addquerytourl) function internally. 341 | 342 | ### Params 343 | The function can also receive a `params` object that will be used to replace the `:param` wildcards in the path: 344 | 345 | ```ts 346 | import { makeService } from 'make-service' 347 | 348 | const service = makeService(new URL("https://example.com/api")) 349 | const response = await service.get("/users/:id/article/:articleId", { 350 | params: { id: "2", articleId: "3" }, 351 | }) 352 | 353 | // It will call "https://example.com/api/users/2/article/3" 354 | ``` 355 | The `params` object will not type-check if the given object doesn't follow the path structure. 356 | ```ts 357 | // @ts-expect-error 358 | service.get("/users/:id", { params: { id: "2", foobar: "foo" } }) 359 | ``` 360 | 361 | This is achieved by using the [`replaceURLParams`](#replaceurlparams) function internally. 362 | 363 | ### Trace 364 | The function can also receive a `trace` function that will be called with the final `url` and `requestInit` arguments. 365 | Therefore you can know what are the actual arguments that will be passed to the `fetch` API. 366 | 367 | ```ts 368 | import { makeService } from 'make-service' 369 | 370 | const service = makeService("https://example.com/api") 371 | const response = await service.get("/users/:id", { 372 | params: { id: "2" }, 373 | query: { page: "2"}, 374 | headers: { Accept: "application/json", "Content-type": "application/json" }, 375 | trace: (url, requestInit) => { 376 | console.log("The request was sent to " + url) 377 | console.log("with the following params: " + JSON.stringify(requestInit)) 378 | }, 379 | }) 380 | 381 | // It will log: 382 | // "The request was sent to https://example.com/api/users/2?page=2" 383 | // with the following params: { headers: { "Accept": "application/json", "Content-type": "application/json" } } 384 | ``` 385 | 386 | ## makeFetcher 387 | This method is the same as [`makeService`](#make-service) but it doesn't expose the HTTP methods as properties of the returned object. 388 | This is good for when you want to have a service setup but don't know the methods you'll be calling in advance, like in a proxy. 389 | 390 | ```ts 391 | import { makeFetcher } from 'make-service' 392 | 393 | const fetcher = makeFetcher("https://example.com/api") 394 | const response = await fetcher("/users", { method: "POST", body: { email: "john@doe.com" } }) 395 | const json = await response.json() 396 | // ^? unknown 397 | ``` 398 | 399 | Other than having to pass the method in the `RequestInit` this is going to have all the features of [`makeService`](#make-service). 400 | 401 | ## enhancedFetch 402 | 403 | A wrapper around the `fetch` service. 404 | It returns a [`TypedResponse`](#typedresponse) instead of a `Response`. 405 | 406 | ```ts 407 | import { enhancedFetch } from 'make-service' 408 | 409 | const response = await enhancedFetch("https://example.com/api/users", { 410 | method: 'POST', 411 | body: { some: { object: { as: { body } } } } 412 | }) 413 | const json = await response.json() 414 | // ^? unknown 415 | // You can pass it a generic or schema to type the result 416 | ``` 417 | 418 | This function accepts the same arguments as the `fetch` API - with exception of [JSON-like body](#body) -, and it also accepts an object of [`params`](#params) to replace URL wildcards, an object-like [`query`](#query), and a [`trace`](#trace) function. Those are all described above in [`makeService`](#make-service). 419 | 420 | This slightly different `RequestInit` is typed as `EnhancedRequestInit`. 421 | 422 | ```ts 423 | import { enhancedFetch } from 'make-service' 424 | 425 | await enhancedFetch("https://example.com/api/users/:role", { 426 | method: 'POST', 427 | body: { some: { object: { as: { body } } } }, 428 | query: { page: "1" }, 429 | params: { role: "admin" }, 430 | trace: console.log, 431 | }) 432 | 433 | // The trace function will be called with the following arguments: 434 | // "https://example.com/api/users/admin?page=1" 435 | // { 436 | // method: 'POST', 437 | // body: '{"some":{"object":{"as":{"body":{}}}}}', 438 | // } 439 | // Response {} 440 | ``` 441 | 442 | The `trace` function can also return a `Promise` in order to send traces to an external service or database. 443 | 444 | ## typedResponse 445 | 446 | A type-safe wrapper around the `Response` object. It adds a `json` and `text` method that will parse the response with a given standard schema library. If you don't provide a schema, it will return `unknown` instead of `any`, then you can also give it a generic to type cast the result. 447 | 448 | ```ts 449 | import { typedResponse } from 'make-service' 450 | import type { TypedResponse } from 'make-service' 451 | 452 | // With JSON 453 | const response: TypedResponse = typedResponse(new Response(JSON.stringify({ foo: "bar" }))) 454 | const json = await response.json() 455 | // ^? unknown 456 | const json = await response.json<{ foo: string }>() 457 | // ^? { foo: string } 458 | const json = await response.json(z.object({ foo: z.string() })) 459 | // ^? { foo: string } 460 | 461 | // With text 462 | const response: TypedResponse = typedResponse(new Response("foo")) 463 | const text = await response.text() 464 | // ^? string 465 | const text = await response.text<`foo${string}`>() 466 | // ^? `foo${string}` 467 | const text = await response.text(z.string().email()) 468 | // ^? string 469 | ``` 470 | 471 | # Transform the payload 472 | The combination of `make-service` and [`string-ts`](https://github.com/gustavoguichard/string-ts) libraries makes it easy to work with APIs that follow a different convention for object key's casing, so you can transform the request body before sending it or the response body after returning from the server. 473 | The resulting type will be **properly typed** 🤩. 474 | ```ts 475 | import { makeService } from 'make-service' 476 | import { deepCamelKeys, deepKebabKeys } from 'string-ts' 477 | 478 | const service = makeService("https://example.com/api") 479 | const response = service.get("/users") 480 | const json = await response.json( 481 | z 482 | .array(z.object({ "first-name": z.string(), contact: z.object({ "home-address": z.string() }) })) 483 | ) 484 | const users = deepCamelKeys(json) 485 | console.log(users) 486 | // ^? { firstName: string, contact: { homeAddress: string } }[] 487 | 488 | const body = deepKebabKeys({ firstName: "John", contact: { homeAddress: "123 Main St" } }) 489 | // ^? { "first-name": string, contact: { "home-address": string } } 490 | service.patch("/users/:id", { body, params: { id: "1" } }) 491 | ``` 492 | 493 | # Other available primitives 494 | This little library has plenty of other useful functions that you can use to build your own services and interactions with external APIs. 495 | 496 | ## addQueryToURL 497 | It receives a URL instance or URL string and an object-like query and returns a new URL with the query appended to it. 498 | 499 | It will preserve the original query if it exists and will also preserve the type of the given URL. 500 | 501 | ```ts 502 | import { addQueryToURL } from 'make-service' 503 | 504 | addQueryToURL("https://example.com/api/users", { page: "2" }) 505 | // https://example.com/api/users?page=2 506 | 507 | addQueryToURL( 508 | "https://example.com/api/users?role=admin", 509 | { page: "2" }, 510 | ) 511 | // https://example.com/api/users?role=admin&page=2 512 | 513 | addQueryToURL( 514 | new URL("https://example.com/api/users"), 515 | { page: "2" }, 516 | ) 517 | // https://example.com/api/users?page=2 518 | 519 | addQueryToURL( 520 | new URL("https://example.com/api/users?role=admin"), 521 | { page: "2" }, 522 | ) 523 | // https://example.com/api/users?role=admin&page=2 524 | ``` 525 | 526 | ## ensureStringBody 527 | It accepts any value considered a `BodyInit` (the type of the body in `fetch`, such as `ReadableStream` | `XMLHttpRequestBodyInit` | `null`) and also accepts a JSON-like structure such as a number, string, boolean, array or object. 528 | 529 | In case it detects a JSON-like structure it will return a stringified version of that payload. Otherwise the type will be preserved. 530 | 531 | ```ts 532 | import { ensureStringBody } from 'make-service' 533 | 534 | ensureStringBody({ foo: "bar" }) 535 | // '{"foo":"bar"}' 536 | ensureStringBody("foo") 537 | // 'foo' 538 | ensureStringBody(1) 539 | // '1' 540 | ensureStringBody(true) 541 | // 'true' 542 | ensureStringBody(null) 543 | // null 544 | ensureStringBody(new ReadableStream()) 545 | // ReadableStream 546 | 547 | // and so on... 548 | ``` 549 | 550 | ## makeGetApiURL 551 | It creates an URL builder for your API. It works similarly to [`makeFetcher`](#makefetcher) but will return the URL instead of a response. 552 | 553 | You create a `getApiURL` function by giving it a `baseURL` and then it accepts a path and an optional [query](#query) that will be merged into the final URL. 554 | 555 | ```ts 556 | import { makeGetApiURL } from 'make-service' 557 | 558 | const getApiURL = makeGetApiURL("https://example.com/api") 559 | const url = getApiURL("/users?admin=true", { page: "2" }) 560 | 561 | // "https://example.com/api/users?admin=true&page=2" 562 | ``` 563 | 564 | Notice the extra slashes are gonna be added or removed as needed. 565 | ```ts 566 | makeGetApiURL("https://example.com/api/")("/users") 567 | // "https://example.com/api/users" 568 | makeGetApiURL("https://example.com/api")("users") 569 | // "https://example.com/api/users" 570 | ``` 571 | 572 | ## mergeHeaders 573 | It merges multiple `HeadersInit` objects into a single `Headers` instance. 574 | They can be of any type that is accepted by the `Headers` constructor, like a `Headers` instance, a plain object, or an array of entries. 575 | 576 | ```ts 577 | import { mergeHeaders } from 'make-service' 578 | 579 | const headers1 = new Headers({ "Content-Type": "application/json" }) 580 | const headers2 = { Accept: "application/json" } 581 | const headers3 = [["accept", "*/*"]] 582 | 583 | const merged = mergeHeaders(headers1, headers2, headers3) 584 | // ^? Headers({ "content-Type": "application/json", "accept": "*/*" }) 585 | ``` 586 | 587 | It will delete previous headers if `undefined` or `"undefined"` is given: 588 | 589 | ```ts 590 | import { mergeHeaders } from 'make-service' 591 | 592 | const headers1 = new Headers({ "Content-Type": "application/json", Accept: "application/json" }) 593 | const headers2 = { accept: undefined } 594 | const headers3 = [["content-type", "undefined"]] 595 | 596 | const merged = mergeHeaders(headers1, headers2, headers3) 597 | // ^? Headers({}) 598 | ``` 599 | 600 | ## replaceURLParams 601 | This function replaces URL wildcards with the given params. 602 | ```ts 603 | import { replaceURLParams } from 'make-service' 604 | 605 | const url = replaceURLParams( 606 | "https://example.com/users/:id/posts/:postId", 607 | { id: "2", postId: "3" }, 608 | ) 609 | // It will return: "https://example.com/users/2/posts/3" 610 | ``` 611 | 612 | The params will be **strongly-typed** which means they will be validated against the URL structure and will not type-check if the given object does not match that structure. 613 | 614 | # Contributors 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 |
Guga Guichard
Guga Guichard

💻 📆 📣 🚧 📖 🐛 🚇 💬 🔬 👀 🤔 💡
Daniel Weinmann
Daniel Weinmann

💻 📣 🤔 📖 🐛 👀
Andrei Luca
Andrei Luca

📖 💻 📣 🚧 🐛 🤔
Diogo Biazus
Diogo Biazus

💻 📖
Marcos Javier Alvarez Maestre
Marcos Javier Alvarez Maestre

💻 🐛
630 | 631 | 632 | 633 | 634 | 635 | 636 | # Acknowledgements 637 | This library is part of a code I've been carrying around for a while through many projects. 638 | 639 | - [Seasoned](https://github.com/seasonedcc) - for backing my work and allowing me testing it on big codebases when I started sketching this API. 640 | - [croods](https://github.com/seasonedcc/croods) by [@danielweinmann](https://github.com/danielweinmann) - a react data-layer library from pre-ReactQuery/pre-SWR era - gave me ideas and experience dealing with APIs after spending a lot of time in that codebase. 641 | - [zod](https://zod.dev/) by [@colinhacks](https://github.com/colinhacks) changed my mindset about how to deal with external data. 642 | - [zod-fetch](https://github.com/mattpocock/zod-fetch) by [@mattpocock](https://github.com/mattpocock) for the inspiration, when I realized I had a similar solution that could be extracted and be available for everyone to use. 643 | 644 | I really appreciate your feedback and contributions. If you have any questions, feel free to open an issue or contact me on [Twitter](https://twitter.com/gugaguichard). 645 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["dist"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space", 15 | "indentWidth": 2 16 | }, 17 | "organizeImports": { 18 | "enabled": true 19 | }, 20 | "linter": { 21 | "enabled": true, 22 | "rules": { 23 | "recommended": true, 24 | "correctness": { 25 | "noUnusedImports": { 26 | "level": "warn", 27 | "fix": "safe" 28 | } 29 | }, 30 | "style": { 31 | "noUnusedTemplateLiteral": { 32 | "level": "error", 33 | "fix": "safe" 34 | }, 35 | "useTemplate": { 36 | "level": "error", 37 | "fix": "safe" 38 | } 39 | }, 40 | "suspicious": { 41 | "noExplicitAny": "off", 42 | "noShadowRestrictedNames": "off" 43 | } 44 | } 45 | }, 46 | "javascript": { 47 | "formatter": { 48 | "quoteStyle": "single", 49 | "semicolons": "asNeeded", 50 | "trailingCommas": "es5" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "make-service", 3 | "version": "4.0.0", 4 | "description": "Some utilities to extend the 'fetch' API to better interact with external APIs.", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "author": "Gustavo Guichard <@gugaguichard>", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "tsup ./src/index.ts --format esm,cjs --dts", 12 | "dev": "tsup ./src/index.ts --format esm,cjs --watch --dts", 13 | "lint": "node_modules/.bin/biome check --write --error-on-warnings", 14 | "tsc": "tsc --noEmit", 15 | "test": "vitest run" 16 | }, 17 | "devDependencies": { 18 | "@biomejs/biome": "^1.9.4", 19 | "@standard-schema/spec": "^1.0.0", 20 | "@types/node": "^22.1.0", 21 | "arktype": "^2.0.4", 22 | "jsdom": "^24.1.1", 23 | "string-ts": "^2.2.0", 24 | "tsup": "^8.2.4", 25 | "typescript": "^5.5.4", 26 | "vitest": "latest", 27 | "zod": "4.0.0-beta.20250420T053007" 28 | }, 29 | "files": [ 30 | "README.md", 31 | "./dist/*" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/gustavoguichard/make-service.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/gustavoguichard/make-service/issues" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@biomejs/biome': 12 | specifier: ^1.9.4 13 | version: 1.9.4 14 | '@standard-schema/spec': 15 | specifier: ^1.0.0 16 | version: 1.0.0 17 | '@types/node': 18 | specifier: ^22.1.0 19 | version: 22.13.1 20 | arktype: 21 | specifier: ^2.0.4 22 | version: 2.0.4 23 | jsdom: 24 | specifier: ^24.1.1 25 | version: 24.1.3 26 | string-ts: 27 | specifier: ^2.2.0 28 | version: 2.2.1 29 | tsup: 30 | specifier: ^8.2.4 31 | version: 8.3.6(postcss@8.5.1)(typescript@5.7.3) 32 | typescript: 33 | specifier: ^5.5.4 34 | version: 5.7.3 35 | vitest: 36 | specifier: latest 37 | version: 3.0.5(@types/node@22.13.1)(jsdom@24.1.3) 38 | zod: 39 | specifier: 4.0.0-beta.20250420T053007 40 | version: 4.0.0-beta.20250420T053007 41 | 42 | packages: 43 | 44 | '@ark/schema@0.39.0': 45 | resolution: {integrity: sha512-LQbQUb3Sj461LgklXObAyUJNtsUUCBxZlO2HqRLYvRSqpStm0xTMrXn51DwBNNxeSULvKVpXFwoxiSec9kwKww==} 46 | 47 | '@ark/util@0.39.0': 48 | resolution: {integrity: sha512-90APHVklk8BP4kku7hIh1BgrhuyKYqoZ4O7EybtFRo7cDl9mIyc/QUbGvYDg//73s0J2H0I/gW9pzroA1R4IBQ==} 49 | 50 | '@asamuzakjp/css-color@2.8.3': 51 | resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} 52 | 53 | '@biomejs/biome@1.9.4': 54 | resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} 55 | engines: {node: '>=14.21.3'} 56 | hasBin: true 57 | 58 | '@biomejs/cli-darwin-arm64@1.9.4': 59 | resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} 60 | engines: {node: '>=14.21.3'} 61 | cpu: [arm64] 62 | os: [darwin] 63 | 64 | '@biomejs/cli-darwin-x64@1.9.4': 65 | resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} 66 | engines: {node: '>=14.21.3'} 67 | cpu: [x64] 68 | os: [darwin] 69 | 70 | '@biomejs/cli-linux-arm64-musl@1.9.4': 71 | resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} 72 | engines: {node: '>=14.21.3'} 73 | cpu: [arm64] 74 | os: [linux] 75 | 76 | '@biomejs/cli-linux-arm64@1.9.4': 77 | resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} 78 | engines: {node: '>=14.21.3'} 79 | cpu: [arm64] 80 | os: [linux] 81 | 82 | '@biomejs/cli-linux-x64-musl@1.9.4': 83 | resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} 84 | engines: {node: '>=14.21.3'} 85 | cpu: [x64] 86 | os: [linux] 87 | 88 | '@biomejs/cli-linux-x64@1.9.4': 89 | resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} 90 | engines: {node: '>=14.21.3'} 91 | cpu: [x64] 92 | os: [linux] 93 | 94 | '@biomejs/cli-win32-arm64@1.9.4': 95 | resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} 96 | engines: {node: '>=14.21.3'} 97 | cpu: [arm64] 98 | os: [win32] 99 | 100 | '@biomejs/cli-win32-x64@1.9.4': 101 | resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} 102 | engines: {node: '>=14.21.3'} 103 | cpu: [x64] 104 | os: [win32] 105 | 106 | '@csstools/color-helpers@5.0.1': 107 | resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} 108 | engines: {node: '>=18'} 109 | 110 | '@csstools/css-calc@2.1.1': 111 | resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} 112 | engines: {node: '>=18'} 113 | peerDependencies: 114 | '@csstools/css-parser-algorithms': ^3.0.4 115 | '@csstools/css-tokenizer': ^3.0.3 116 | 117 | '@csstools/css-color-parser@3.0.7': 118 | resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} 119 | engines: {node: '>=18'} 120 | peerDependencies: 121 | '@csstools/css-parser-algorithms': ^3.0.4 122 | '@csstools/css-tokenizer': ^3.0.3 123 | 124 | '@csstools/css-parser-algorithms@3.0.4': 125 | resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} 126 | engines: {node: '>=18'} 127 | peerDependencies: 128 | '@csstools/css-tokenizer': ^3.0.3 129 | 130 | '@csstools/css-tokenizer@3.0.3': 131 | resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} 132 | engines: {node: '>=18'} 133 | 134 | '@esbuild/aix-ppc64@0.24.2': 135 | resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} 136 | engines: {node: '>=18'} 137 | cpu: [ppc64] 138 | os: [aix] 139 | 140 | '@esbuild/android-arm64@0.24.2': 141 | resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} 142 | engines: {node: '>=18'} 143 | cpu: [arm64] 144 | os: [android] 145 | 146 | '@esbuild/android-arm@0.24.2': 147 | resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} 148 | engines: {node: '>=18'} 149 | cpu: [arm] 150 | os: [android] 151 | 152 | '@esbuild/android-x64@0.24.2': 153 | resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} 154 | engines: {node: '>=18'} 155 | cpu: [x64] 156 | os: [android] 157 | 158 | '@esbuild/darwin-arm64@0.24.2': 159 | resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} 160 | engines: {node: '>=18'} 161 | cpu: [arm64] 162 | os: [darwin] 163 | 164 | '@esbuild/darwin-x64@0.24.2': 165 | resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} 166 | engines: {node: '>=18'} 167 | cpu: [x64] 168 | os: [darwin] 169 | 170 | '@esbuild/freebsd-arm64@0.24.2': 171 | resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} 172 | engines: {node: '>=18'} 173 | cpu: [arm64] 174 | os: [freebsd] 175 | 176 | '@esbuild/freebsd-x64@0.24.2': 177 | resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} 178 | engines: {node: '>=18'} 179 | cpu: [x64] 180 | os: [freebsd] 181 | 182 | '@esbuild/linux-arm64@0.24.2': 183 | resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} 184 | engines: {node: '>=18'} 185 | cpu: [arm64] 186 | os: [linux] 187 | 188 | '@esbuild/linux-arm@0.24.2': 189 | resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} 190 | engines: {node: '>=18'} 191 | cpu: [arm] 192 | os: [linux] 193 | 194 | '@esbuild/linux-ia32@0.24.2': 195 | resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} 196 | engines: {node: '>=18'} 197 | cpu: [ia32] 198 | os: [linux] 199 | 200 | '@esbuild/linux-loong64@0.24.2': 201 | resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} 202 | engines: {node: '>=18'} 203 | cpu: [loong64] 204 | os: [linux] 205 | 206 | '@esbuild/linux-mips64el@0.24.2': 207 | resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} 208 | engines: {node: '>=18'} 209 | cpu: [mips64el] 210 | os: [linux] 211 | 212 | '@esbuild/linux-ppc64@0.24.2': 213 | resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} 214 | engines: {node: '>=18'} 215 | cpu: [ppc64] 216 | os: [linux] 217 | 218 | '@esbuild/linux-riscv64@0.24.2': 219 | resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} 220 | engines: {node: '>=18'} 221 | cpu: [riscv64] 222 | os: [linux] 223 | 224 | '@esbuild/linux-s390x@0.24.2': 225 | resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} 226 | engines: {node: '>=18'} 227 | cpu: [s390x] 228 | os: [linux] 229 | 230 | '@esbuild/linux-x64@0.24.2': 231 | resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} 232 | engines: {node: '>=18'} 233 | cpu: [x64] 234 | os: [linux] 235 | 236 | '@esbuild/netbsd-arm64@0.24.2': 237 | resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} 238 | engines: {node: '>=18'} 239 | cpu: [arm64] 240 | os: [netbsd] 241 | 242 | '@esbuild/netbsd-x64@0.24.2': 243 | resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} 244 | engines: {node: '>=18'} 245 | cpu: [x64] 246 | os: [netbsd] 247 | 248 | '@esbuild/openbsd-arm64@0.24.2': 249 | resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} 250 | engines: {node: '>=18'} 251 | cpu: [arm64] 252 | os: [openbsd] 253 | 254 | '@esbuild/openbsd-x64@0.24.2': 255 | resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} 256 | engines: {node: '>=18'} 257 | cpu: [x64] 258 | os: [openbsd] 259 | 260 | '@esbuild/sunos-x64@0.24.2': 261 | resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} 262 | engines: {node: '>=18'} 263 | cpu: [x64] 264 | os: [sunos] 265 | 266 | '@esbuild/win32-arm64@0.24.2': 267 | resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} 268 | engines: {node: '>=18'} 269 | cpu: [arm64] 270 | os: [win32] 271 | 272 | '@esbuild/win32-ia32@0.24.2': 273 | resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} 274 | engines: {node: '>=18'} 275 | cpu: [ia32] 276 | os: [win32] 277 | 278 | '@esbuild/win32-x64@0.24.2': 279 | resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} 280 | engines: {node: '>=18'} 281 | cpu: [x64] 282 | os: [win32] 283 | 284 | '@isaacs/cliui@8.0.2': 285 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 286 | engines: {node: '>=12'} 287 | 288 | '@jridgewell/gen-mapping@0.3.8': 289 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 290 | engines: {node: '>=6.0.0'} 291 | 292 | '@jridgewell/resolve-uri@3.1.2': 293 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 294 | engines: {node: '>=6.0.0'} 295 | 296 | '@jridgewell/set-array@1.2.1': 297 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 298 | engines: {node: '>=6.0.0'} 299 | 300 | '@jridgewell/sourcemap-codec@1.5.0': 301 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 302 | 303 | '@jridgewell/trace-mapping@0.3.25': 304 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 305 | 306 | '@pkgjs/parseargs@0.11.0': 307 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 308 | engines: {node: '>=14'} 309 | 310 | '@rollup/rollup-android-arm-eabi@4.34.3': 311 | resolution: {integrity: sha512-8kq/NjMKkMTGKMPldWihncOl62kgnLYk7cW+/4NCUWfS70/wz4+gQ7rMxMMpZ3dIOP/xw7wKNzIuUnN/H2GfUg==} 312 | cpu: [arm] 313 | os: [android] 314 | 315 | '@rollup/rollup-android-arm64@4.34.3': 316 | resolution: {integrity: sha512-1PqMHiuRochQ6++SDI7SaRDWJKr/NgAlezBi5nOne6Da6IWJo3hK0TdECBDwd92IUDPG4j/bZmWuwOnomNT8wA==} 317 | cpu: [arm64] 318 | os: [android] 319 | 320 | '@rollup/rollup-darwin-arm64@4.34.3': 321 | resolution: {integrity: sha512-fqbrykX4mGV3DlCDXhF4OaMGcchd2tmLYxVt3On5oOZWVDFfdEoYAV2alzNChl8OzNaeMAGqm1f7gk7eIw/uDg==} 322 | cpu: [arm64] 323 | os: [darwin] 324 | 325 | '@rollup/rollup-darwin-x64@4.34.3': 326 | resolution: {integrity: sha512-8Wxrx/KRvMsTyLTbdrMXcVKfpW51cCNW8x7iQD72xSEbjvhCY3b+w83Bea3nQfysTMR7K28esc+ZFITThXm+1w==} 327 | cpu: [x64] 328 | os: [darwin] 329 | 330 | '@rollup/rollup-freebsd-arm64@4.34.3': 331 | resolution: {integrity: sha512-lpBmV2qSiELh+ATQPTjQczt5hvbTLsE0c43Rx4bGxN2VpnAZWy77we7OO62LyOSZNY7CzjMoceRPc+Lt4e9J6A==} 332 | cpu: [arm64] 333 | os: [freebsd] 334 | 335 | '@rollup/rollup-freebsd-x64@4.34.3': 336 | resolution: {integrity: sha512-sNPvBIXpgaYcI6mAeH13GZMXFrrw5mdZVI1M9YQPRG2LpjwL8DSxSIflZoh/B5NEuOi53kxsR/S2GKozK1vDXA==} 337 | cpu: [x64] 338 | os: [freebsd] 339 | 340 | '@rollup/rollup-linux-arm-gnueabihf@4.34.3': 341 | resolution: {integrity: sha512-MW6N3AoC61OfE1VgnN5O1OW0gt8VTbhx9s/ZEPLBM11wEdHjeilPzOxVmmsrx5YmejpGPvez8QwGGvMU+pGxpw==} 342 | cpu: [arm] 343 | os: [linux] 344 | 345 | '@rollup/rollup-linux-arm-musleabihf@4.34.3': 346 | resolution: {integrity: sha512-2SQkhr5xvatYq0/+H6qyW0zvrQz9LM4lxGkpWURLoQX5+yP8MsERh4uWmxFohOvwCP6l/+wgiHZ1qVwLDc7Qmw==} 347 | cpu: [arm] 348 | os: [linux] 349 | 350 | '@rollup/rollup-linux-arm64-gnu@4.34.3': 351 | resolution: {integrity: sha512-R3JLYt8YoRwKI5shJsovLpcR6pwIMui/MGG/MmxZ1DYI3iRSKI4qcYrvYgDf4Ss2oCR3RL3F3dYK7uAGQgMIuQ==} 352 | cpu: [arm64] 353 | os: [linux] 354 | 355 | '@rollup/rollup-linux-arm64-musl@4.34.3': 356 | resolution: {integrity: sha512-4XQhG8v/t3S7Rxs7rmFUuM6j09hVrTArzONS3fUZ6oBRSN/ps9IPQjVhp62P0W3KhqJdQADo/MRlYRMdgxr/3w==} 357 | cpu: [arm64] 358 | os: [linux] 359 | 360 | '@rollup/rollup-linux-loongarch64-gnu@4.34.3': 361 | resolution: {integrity: sha512-QlW1jCUZ1LHUIYCAK2FciVw1ptHsxzApYVi05q7bz2A8oNE8QxQ85NhM4arLxkAlcnS42t4avJbSfzSQwbIaKg==} 362 | cpu: [loong64] 363 | os: [linux] 364 | 365 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.3': 366 | resolution: {integrity: sha512-kMbLToizVeCcN69+nnm20Dh0hrRIAjgaaL+Wh0gWZcNt8e542d2FUGtsyuNsHVNNF3gqTJrpzUGIdwMGLEUM7g==} 367 | cpu: [ppc64] 368 | os: [linux] 369 | 370 | '@rollup/rollup-linux-riscv64-gnu@4.34.3': 371 | resolution: {integrity: sha512-YgD0DnZ3CHtvXRH8rzjVSxwI0kMTr0RQt3o1N92RwxGdx7YejzbBO0ELlSU48DP96u1gYYVWfUhDRyaGNqJqJg==} 372 | cpu: [riscv64] 373 | os: [linux] 374 | 375 | '@rollup/rollup-linux-s390x-gnu@4.34.3': 376 | resolution: {integrity: sha512-dIOoOz8altjp6UjAi3U9EW99s8nta4gzi52FeI45GlPyrUH4QixUoBMH9VsVjt+9A2RiZBWyjYNHlJ/HmJOBCQ==} 377 | cpu: [s390x] 378 | os: [linux] 379 | 380 | '@rollup/rollup-linux-x64-gnu@4.34.3': 381 | resolution: {integrity: sha512-lOyG3aF4FTKrhpzXfMmBXgeKUUXdAWmP2zSNf8HTAXPqZay6QYT26l64hVizBjq+hJx3pl0DTEyvPi9sTA6VGA==} 382 | cpu: [x64] 383 | os: [linux] 384 | 385 | '@rollup/rollup-linux-x64-musl@4.34.3': 386 | resolution: {integrity: sha512-usztyYLu2i+mYzzOjqHZTaRXbUOqw3P6laNUh1zcqxbPH1P2Tz/QdJJCQSnGxCtsRQeuU2bCyraGMtMumC46rw==} 387 | cpu: [x64] 388 | os: [linux] 389 | 390 | '@rollup/rollup-win32-arm64-msvc@4.34.3': 391 | resolution: {integrity: sha512-ojFOKaz/ZyalIrizdBq2vyc2f0kFbJahEznfZlxdB6pF9Do6++i1zS5Gy6QLf8D7/S57MHrmBLur6AeRYeQXSA==} 392 | cpu: [arm64] 393 | os: [win32] 394 | 395 | '@rollup/rollup-win32-ia32-msvc@4.34.3': 396 | resolution: {integrity: sha512-K/V97GMbNa+Da9mGcZqmSl+DlJmWfHXTuI9V8oB2evGsQUtszCl67+OxWjBKpeOnYwox9Jpmt/J6VhpeRCYqow==} 397 | cpu: [ia32] 398 | os: [win32] 399 | 400 | '@rollup/rollup-win32-x64-msvc@4.34.3': 401 | resolution: {integrity: sha512-CUypcYP31Q8O04myV6NKGzk9GVXslO5EJNfmARNSzLF2A+5rmZUlDJ4et6eoJaZgBT9wrC2p4JZH04Vkic8HdQ==} 402 | cpu: [x64] 403 | os: [win32] 404 | 405 | '@standard-schema/spec@1.0.0': 406 | resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} 407 | 408 | '@types/estree@1.0.6': 409 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 410 | 411 | '@types/node@22.13.1': 412 | resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} 413 | 414 | '@vitest/expect@3.0.5': 415 | resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} 416 | 417 | '@vitest/mocker@3.0.5': 418 | resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} 419 | peerDependencies: 420 | msw: ^2.4.9 421 | vite: ^5.0.0 || ^6.0.0 422 | peerDependenciesMeta: 423 | msw: 424 | optional: true 425 | vite: 426 | optional: true 427 | 428 | '@vitest/pretty-format@3.0.5': 429 | resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} 430 | 431 | '@vitest/runner@3.0.5': 432 | resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} 433 | 434 | '@vitest/snapshot@3.0.5': 435 | resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} 436 | 437 | '@vitest/spy@3.0.5': 438 | resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} 439 | 440 | '@vitest/utils@3.0.5': 441 | resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} 442 | 443 | '@zod/core@0.8.1': 444 | resolution: {integrity: sha512-djj8hPhxIHcG8ptxITaw/Bout5HJZ9NyRbKr95Eilqwt9R0kvITwUQGDU+n+MVdsBIka5KwztmZSLti22F+P0A==} 445 | 446 | agent-base@7.1.3: 447 | resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} 448 | engines: {node: '>= 14'} 449 | 450 | ansi-regex@5.0.1: 451 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 452 | engines: {node: '>=8'} 453 | 454 | ansi-regex@6.1.0: 455 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 456 | engines: {node: '>=12'} 457 | 458 | ansi-styles@4.3.0: 459 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 460 | engines: {node: '>=8'} 461 | 462 | ansi-styles@6.2.1: 463 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 464 | engines: {node: '>=12'} 465 | 466 | any-promise@1.3.0: 467 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 468 | 469 | arktype@2.0.4: 470 | resolution: {integrity: sha512-S68rWVDnJauwH7/QCm8zCUM3aTe9Xk6oRihdcc3FSUAtxCo/q1Fwq46JhcwB5Ufv1YStwdQRz+00Y/URlvbhAQ==} 471 | 472 | assertion-error@2.0.1: 473 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 474 | engines: {node: '>=12'} 475 | 476 | asynckit@0.4.0: 477 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 478 | 479 | balanced-match@1.0.2: 480 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 481 | 482 | brace-expansion@2.0.1: 483 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 484 | 485 | bundle-require@5.1.0: 486 | resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} 487 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 488 | peerDependencies: 489 | esbuild: '>=0.18' 490 | 491 | cac@6.7.14: 492 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 493 | engines: {node: '>=8'} 494 | 495 | chai@5.1.2: 496 | resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} 497 | engines: {node: '>=12'} 498 | 499 | check-error@2.1.1: 500 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 501 | engines: {node: '>= 16'} 502 | 503 | chokidar@4.0.3: 504 | resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 505 | engines: {node: '>= 14.16.0'} 506 | 507 | color-convert@2.0.1: 508 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 509 | engines: {node: '>=7.0.0'} 510 | 511 | color-name@1.1.4: 512 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 513 | 514 | combined-stream@1.0.8: 515 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 516 | engines: {node: '>= 0.8'} 517 | 518 | commander@4.1.1: 519 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 520 | engines: {node: '>= 6'} 521 | 522 | consola@3.4.0: 523 | resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} 524 | engines: {node: ^14.18.0 || >=16.10.0} 525 | 526 | cross-spawn@7.0.6: 527 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 528 | engines: {node: '>= 8'} 529 | 530 | cssstyle@4.2.1: 531 | resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} 532 | engines: {node: '>=18'} 533 | 534 | data-urls@5.0.0: 535 | resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} 536 | engines: {node: '>=18'} 537 | 538 | debug@4.4.0: 539 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 540 | engines: {node: '>=6.0'} 541 | peerDependencies: 542 | supports-color: '*' 543 | peerDependenciesMeta: 544 | supports-color: 545 | optional: true 546 | 547 | decimal.js@10.5.0: 548 | resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} 549 | 550 | deep-eql@5.0.2: 551 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 552 | engines: {node: '>=6'} 553 | 554 | delayed-stream@1.0.0: 555 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 556 | engines: {node: '>=0.4.0'} 557 | 558 | eastasianwidth@0.2.0: 559 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 560 | 561 | emoji-regex@8.0.0: 562 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 563 | 564 | emoji-regex@9.2.2: 565 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 566 | 567 | entities@4.5.0: 568 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 569 | engines: {node: '>=0.12'} 570 | 571 | es-module-lexer@1.6.0: 572 | resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 573 | 574 | esbuild@0.24.2: 575 | resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} 576 | engines: {node: '>=18'} 577 | hasBin: true 578 | 579 | estree-walker@3.0.3: 580 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 581 | 582 | expect-type@1.1.0: 583 | resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} 584 | engines: {node: '>=12.0.0'} 585 | 586 | fdir@6.4.3: 587 | resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} 588 | peerDependencies: 589 | picomatch: ^3 || ^4 590 | peerDependenciesMeta: 591 | picomatch: 592 | optional: true 593 | 594 | foreground-child@3.3.0: 595 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 596 | engines: {node: '>=14'} 597 | 598 | form-data@4.0.1: 599 | resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} 600 | engines: {node: '>= 6'} 601 | 602 | fsevents@2.3.3: 603 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 604 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 605 | os: [darwin] 606 | 607 | glob@10.4.5: 608 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 609 | hasBin: true 610 | 611 | html-encoding-sniffer@4.0.0: 612 | resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} 613 | engines: {node: '>=18'} 614 | 615 | http-proxy-agent@7.0.2: 616 | resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} 617 | engines: {node: '>= 14'} 618 | 619 | https-proxy-agent@7.0.6: 620 | resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} 621 | engines: {node: '>= 14'} 622 | 623 | iconv-lite@0.6.3: 624 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 625 | engines: {node: '>=0.10.0'} 626 | 627 | is-fullwidth-code-point@3.0.0: 628 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 629 | engines: {node: '>=8'} 630 | 631 | is-potential-custom-element-name@1.0.1: 632 | resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 633 | 634 | isexe@2.0.0: 635 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 636 | 637 | jackspeak@3.4.3: 638 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 639 | 640 | joycon@3.1.1: 641 | resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 642 | engines: {node: '>=10'} 643 | 644 | jsdom@24.1.3: 645 | resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} 646 | engines: {node: '>=18'} 647 | peerDependencies: 648 | canvas: ^2.11.2 649 | peerDependenciesMeta: 650 | canvas: 651 | optional: true 652 | 653 | lilconfig@3.1.3: 654 | resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 655 | engines: {node: '>=14'} 656 | 657 | lines-and-columns@1.2.4: 658 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 659 | 660 | load-tsconfig@0.2.5: 661 | resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 662 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 663 | 664 | lodash.sortby@4.7.0: 665 | resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} 666 | 667 | loupe@3.1.3: 668 | resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} 669 | 670 | lru-cache@10.4.3: 671 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 672 | 673 | magic-string@0.30.17: 674 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 675 | 676 | mime-db@1.52.0: 677 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 678 | engines: {node: '>= 0.6'} 679 | 680 | mime-types@2.1.35: 681 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 682 | engines: {node: '>= 0.6'} 683 | 684 | minimatch@9.0.5: 685 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 686 | engines: {node: '>=16 || 14 >=14.17'} 687 | 688 | minipass@7.1.2: 689 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 690 | engines: {node: '>=16 || 14 >=14.17'} 691 | 692 | ms@2.1.3: 693 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 694 | 695 | mz@2.7.0: 696 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 697 | 698 | nanoid@3.3.8: 699 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 700 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 701 | hasBin: true 702 | 703 | nwsapi@2.2.16: 704 | resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} 705 | 706 | object-assign@4.1.1: 707 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 708 | engines: {node: '>=0.10.0'} 709 | 710 | package-json-from-dist@1.0.1: 711 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 712 | 713 | parse5@7.2.1: 714 | resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} 715 | 716 | path-key@3.1.1: 717 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 718 | engines: {node: '>=8'} 719 | 720 | path-scurry@1.11.1: 721 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 722 | engines: {node: '>=16 || 14 >=14.18'} 723 | 724 | pathe@2.0.2: 725 | resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} 726 | 727 | pathval@2.0.0: 728 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 729 | engines: {node: '>= 14.16'} 730 | 731 | picocolors@1.1.1: 732 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 733 | 734 | picomatch@4.0.2: 735 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 736 | engines: {node: '>=12'} 737 | 738 | pirates@4.0.6: 739 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 740 | engines: {node: '>= 6'} 741 | 742 | postcss-load-config@6.0.1: 743 | resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} 744 | engines: {node: '>= 18'} 745 | peerDependencies: 746 | jiti: '>=1.21.0' 747 | postcss: '>=8.0.9' 748 | tsx: ^4.8.1 749 | yaml: ^2.4.2 750 | peerDependenciesMeta: 751 | jiti: 752 | optional: true 753 | postcss: 754 | optional: true 755 | tsx: 756 | optional: true 757 | yaml: 758 | optional: true 759 | 760 | postcss@8.5.1: 761 | resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} 762 | engines: {node: ^10 || ^12 || >=14} 763 | 764 | psl@1.15.0: 765 | resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} 766 | 767 | punycode@2.3.1: 768 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 769 | engines: {node: '>=6'} 770 | 771 | querystringify@2.2.0: 772 | resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} 773 | 774 | readdirp@4.1.1: 775 | resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} 776 | engines: {node: '>= 14.18.0'} 777 | 778 | requires-port@1.0.0: 779 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 780 | 781 | resolve-from@5.0.0: 782 | resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 783 | engines: {node: '>=8'} 784 | 785 | rollup@4.34.3: 786 | resolution: {integrity: sha512-ORCtU0UBJyiAIn9m0llUXJXAswG/68pZptCrqxHG7//Z2DDzAUeyyY5hqf4XrsGlUxscMr9GkQ2QI7KTLqeyPw==} 787 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 788 | hasBin: true 789 | 790 | rrweb-cssom@0.7.1: 791 | resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} 792 | 793 | rrweb-cssom@0.8.0: 794 | resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} 795 | 796 | safer-buffer@2.1.2: 797 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 798 | 799 | saxes@6.0.0: 800 | resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} 801 | engines: {node: '>=v12.22.7'} 802 | 803 | shebang-command@2.0.0: 804 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 805 | engines: {node: '>=8'} 806 | 807 | shebang-regex@3.0.0: 808 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 809 | engines: {node: '>=8'} 810 | 811 | siginfo@2.0.0: 812 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 813 | 814 | signal-exit@4.1.0: 815 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 816 | engines: {node: '>=14'} 817 | 818 | source-map-js@1.2.1: 819 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 820 | engines: {node: '>=0.10.0'} 821 | 822 | source-map@0.8.0-beta.0: 823 | resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} 824 | engines: {node: '>= 8'} 825 | 826 | stackback@0.0.2: 827 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 828 | 829 | std-env@3.8.0: 830 | resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} 831 | 832 | string-ts@2.2.1: 833 | resolution: {integrity: sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==} 834 | 835 | string-width@4.2.3: 836 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 837 | engines: {node: '>=8'} 838 | 839 | string-width@5.1.2: 840 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 841 | engines: {node: '>=12'} 842 | 843 | strip-ansi@6.0.1: 844 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 845 | engines: {node: '>=8'} 846 | 847 | strip-ansi@7.1.0: 848 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 849 | engines: {node: '>=12'} 850 | 851 | sucrase@3.35.0: 852 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 853 | engines: {node: '>=16 || 14 >=14.17'} 854 | hasBin: true 855 | 856 | symbol-tree@3.2.4: 857 | resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 858 | 859 | thenify-all@1.6.0: 860 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 861 | engines: {node: '>=0.8'} 862 | 863 | thenify@3.3.1: 864 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 865 | 866 | tinybench@2.9.0: 867 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 868 | 869 | tinyexec@0.3.2: 870 | resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 871 | 872 | tinyglobby@0.2.10: 873 | resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} 874 | engines: {node: '>=12.0.0'} 875 | 876 | tinypool@1.0.2: 877 | resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} 878 | engines: {node: ^18.0.0 || >=20.0.0} 879 | 880 | tinyrainbow@2.0.0: 881 | resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 882 | engines: {node: '>=14.0.0'} 883 | 884 | tinyspy@3.0.2: 885 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 886 | engines: {node: '>=14.0.0'} 887 | 888 | tough-cookie@4.1.4: 889 | resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} 890 | engines: {node: '>=6'} 891 | 892 | tr46@1.0.1: 893 | resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} 894 | 895 | tr46@5.0.0: 896 | resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} 897 | engines: {node: '>=18'} 898 | 899 | tree-kill@1.2.2: 900 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 901 | hasBin: true 902 | 903 | ts-interface-checker@0.1.13: 904 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 905 | 906 | tsup@8.3.6: 907 | resolution: {integrity: sha512-XkVtlDV/58S9Ye0JxUUTcrQk4S+EqlOHKzg6Roa62rdjL1nGWNUstG0xgI4vanHdfIpjP448J8vlN0oK6XOJ5g==} 908 | engines: {node: '>=18'} 909 | hasBin: true 910 | peerDependencies: 911 | '@microsoft/api-extractor': ^7.36.0 912 | '@swc/core': ^1 913 | postcss: ^8.4.12 914 | typescript: '>=4.5.0' 915 | peerDependenciesMeta: 916 | '@microsoft/api-extractor': 917 | optional: true 918 | '@swc/core': 919 | optional: true 920 | postcss: 921 | optional: true 922 | typescript: 923 | optional: true 924 | 925 | typescript@5.7.3: 926 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 927 | engines: {node: '>=14.17'} 928 | hasBin: true 929 | 930 | undici-types@6.20.0: 931 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 932 | 933 | universalify@0.2.0: 934 | resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} 935 | engines: {node: '>= 4.0.0'} 936 | 937 | url-parse@1.5.10: 938 | resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} 939 | 940 | vite-node@3.0.5: 941 | resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} 942 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 943 | hasBin: true 944 | 945 | vite@6.0.11: 946 | resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} 947 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 948 | hasBin: true 949 | peerDependencies: 950 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 951 | jiti: '>=1.21.0' 952 | less: '*' 953 | lightningcss: ^1.21.0 954 | sass: '*' 955 | sass-embedded: '*' 956 | stylus: '*' 957 | sugarss: '*' 958 | terser: ^5.16.0 959 | tsx: ^4.8.1 960 | yaml: ^2.4.2 961 | peerDependenciesMeta: 962 | '@types/node': 963 | optional: true 964 | jiti: 965 | optional: true 966 | less: 967 | optional: true 968 | lightningcss: 969 | optional: true 970 | sass: 971 | optional: true 972 | sass-embedded: 973 | optional: true 974 | stylus: 975 | optional: true 976 | sugarss: 977 | optional: true 978 | terser: 979 | optional: true 980 | tsx: 981 | optional: true 982 | yaml: 983 | optional: true 984 | 985 | vitest@3.0.5: 986 | resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} 987 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 988 | hasBin: true 989 | peerDependencies: 990 | '@edge-runtime/vm': '*' 991 | '@types/debug': ^4.1.12 992 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 993 | '@vitest/browser': 3.0.5 994 | '@vitest/ui': 3.0.5 995 | happy-dom: '*' 996 | jsdom: '*' 997 | peerDependenciesMeta: 998 | '@edge-runtime/vm': 999 | optional: true 1000 | '@types/debug': 1001 | optional: true 1002 | '@types/node': 1003 | optional: true 1004 | '@vitest/browser': 1005 | optional: true 1006 | '@vitest/ui': 1007 | optional: true 1008 | happy-dom: 1009 | optional: true 1010 | jsdom: 1011 | optional: true 1012 | 1013 | w3c-xmlserializer@5.0.0: 1014 | resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 1015 | engines: {node: '>=18'} 1016 | 1017 | webidl-conversions@4.0.2: 1018 | resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} 1019 | 1020 | webidl-conversions@7.0.0: 1021 | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} 1022 | engines: {node: '>=12'} 1023 | 1024 | whatwg-encoding@3.1.1: 1025 | resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} 1026 | engines: {node: '>=18'} 1027 | 1028 | whatwg-mimetype@4.0.0: 1029 | resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} 1030 | engines: {node: '>=18'} 1031 | 1032 | whatwg-url@14.1.0: 1033 | resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} 1034 | engines: {node: '>=18'} 1035 | 1036 | whatwg-url@7.1.0: 1037 | resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} 1038 | 1039 | which@2.0.2: 1040 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1041 | engines: {node: '>= 8'} 1042 | hasBin: true 1043 | 1044 | why-is-node-running@2.3.0: 1045 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1046 | engines: {node: '>=8'} 1047 | hasBin: true 1048 | 1049 | wrap-ansi@7.0.0: 1050 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1051 | engines: {node: '>=10'} 1052 | 1053 | wrap-ansi@8.1.0: 1054 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1055 | engines: {node: '>=12'} 1056 | 1057 | ws@8.18.0: 1058 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 1059 | engines: {node: '>=10.0.0'} 1060 | peerDependencies: 1061 | bufferutil: ^4.0.1 1062 | utf-8-validate: '>=5.0.2' 1063 | peerDependenciesMeta: 1064 | bufferutil: 1065 | optional: true 1066 | utf-8-validate: 1067 | optional: true 1068 | 1069 | xml-name-validator@5.0.0: 1070 | resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} 1071 | engines: {node: '>=18'} 1072 | 1073 | xmlchars@2.2.0: 1074 | resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} 1075 | 1076 | zod@4.0.0-beta.20250420T053007: 1077 | resolution: {integrity: sha512-5pp8Q0PNDaNcUptGiBE9akyioJh3RJpagIxrLtAVMR9IxwcSZiOsJD/1/98CyhItdTlI2H91MfhhLzRlU+fifA==} 1078 | 1079 | snapshots: 1080 | 1081 | '@ark/schema@0.39.0': 1082 | dependencies: 1083 | '@ark/util': 0.39.0 1084 | 1085 | '@ark/util@0.39.0': {} 1086 | 1087 | '@asamuzakjp/css-color@2.8.3': 1088 | dependencies: 1089 | '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) 1090 | '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) 1091 | '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) 1092 | '@csstools/css-tokenizer': 3.0.3 1093 | lru-cache: 10.4.3 1094 | 1095 | '@biomejs/biome@1.9.4': 1096 | optionalDependencies: 1097 | '@biomejs/cli-darwin-arm64': 1.9.4 1098 | '@biomejs/cli-darwin-x64': 1.9.4 1099 | '@biomejs/cli-linux-arm64': 1.9.4 1100 | '@biomejs/cli-linux-arm64-musl': 1.9.4 1101 | '@biomejs/cli-linux-x64': 1.9.4 1102 | '@biomejs/cli-linux-x64-musl': 1.9.4 1103 | '@biomejs/cli-win32-arm64': 1.9.4 1104 | '@biomejs/cli-win32-x64': 1.9.4 1105 | 1106 | '@biomejs/cli-darwin-arm64@1.9.4': 1107 | optional: true 1108 | 1109 | '@biomejs/cli-darwin-x64@1.9.4': 1110 | optional: true 1111 | 1112 | '@biomejs/cli-linux-arm64-musl@1.9.4': 1113 | optional: true 1114 | 1115 | '@biomejs/cli-linux-arm64@1.9.4': 1116 | optional: true 1117 | 1118 | '@biomejs/cli-linux-x64-musl@1.9.4': 1119 | optional: true 1120 | 1121 | '@biomejs/cli-linux-x64@1.9.4': 1122 | optional: true 1123 | 1124 | '@biomejs/cli-win32-arm64@1.9.4': 1125 | optional: true 1126 | 1127 | '@biomejs/cli-win32-x64@1.9.4': 1128 | optional: true 1129 | 1130 | '@csstools/color-helpers@5.0.1': {} 1131 | 1132 | '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': 1133 | dependencies: 1134 | '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) 1135 | '@csstools/css-tokenizer': 3.0.3 1136 | 1137 | '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': 1138 | dependencies: 1139 | '@csstools/color-helpers': 5.0.1 1140 | '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) 1141 | '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) 1142 | '@csstools/css-tokenizer': 3.0.3 1143 | 1144 | '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': 1145 | dependencies: 1146 | '@csstools/css-tokenizer': 3.0.3 1147 | 1148 | '@csstools/css-tokenizer@3.0.3': {} 1149 | 1150 | '@esbuild/aix-ppc64@0.24.2': 1151 | optional: true 1152 | 1153 | '@esbuild/android-arm64@0.24.2': 1154 | optional: true 1155 | 1156 | '@esbuild/android-arm@0.24.2': 1157 | optional: true 1158 | 1159 | '@esbuild/android-x64@0.24.2': 1160 | optional: true 1161 | 1162 | '@esbuild/darwin-arm64@0.24.2': 1163 | optional: true 1164 | 1165 | '@esbuild/darwin-x64@0.24.2': 1166 | optional: true 1167 | 1168 | '@esbuild/freebsd-arm64@0.24.2': 1169 | optional: true 1170 | 1171 | '@esbuild/freebsd-x64@0.24.2': 1172 | optional: true 1173 | 1174 | '@esbuild/linux-arm64@0.24.2': 1175 | optional: true 1176 | 1177 | '@esbuild/linux-arm@0.24.2': 1178 | optional: true 1179 | 1180 | '@esbuild/linux-ia32@0.24.2': 1181 | optional: true 1182 | 1183 | '@esbuild/linux-loong64@0.24.2': 1184 | optional: true 1185 | 1186 | '@esbuild/linux-mips64el@0.24.2': 1187 | optional: true 1188 | 1189 | '@esbuild/linux-ppc64@0.24.2': 1190 | optional: true 1191 | 1192 | '@esbuild/linux-riscv64@0.24.2': 1193 | optional: true 1194 | 1195 | '@esbuild/linux-s390x@0.24.2': 1196 | optional: true 1197 | 1198 | '@esbuild/linux-x64@0.24.2': 1199 | optional: true 1200 | 1201 | '@esbuild/netbsd-arm64@0.24.2': 1202 | optional: true 1203 | 1204 | '@esbuild/netbsd-x64@0.24.2': 1205 | optional: true 1206 | 1207 | '@esbuild/openbsd-arm64@0.24.2': 1208 | optional: true 1209 | 1210 | '@esbuild/openbsd-x64@0.24.2': 1211 | optional: true 1212 | 1213 | '@esbuild/sunos-x64@0.24.2': 1214 | optional: true 1215 | 1216 | '@esbuild/win32-arm64@0.24.2': 1217 | optional: true 1218 | 1219 | '@esbuild/win32-ia32@0.24.2': 1220 | optional: true 1221 | 1222 | '@esbuild/win32-x64@0.24.2': 1223 | optional: true 1224 | 1225 | '@isaacs/cliui@8.0.2': 1226 | dependencies: 1227 | string-width: 5.1.2 1228 | string-width-cjs: string-width@4.2.3 1229 | strip-ansi: 7.1.0 1230 | strip-ansi-cjs: strip-ansi@6.0.1 1231 | wrap-ansi: 8.1.0 1232 | wrap-ansi-cjs: wrap-ansi@7.0.0 1233 | 1234 | '@jridgewell/gen-mapping@0.3.8': 1235 | dependencies: 1236 | '@jridgewell/set-array': 1.2.1 1237 | '@jridgewell/sourcemap-codec': 1.5.0 1238 | '@jridgewell/trace-mapping': 0.3.25 1239 | 1240 | '@jridgewell/resolve-uri@3.1.2': {} 1241 | 1242 | '@jridgewell/set-array@1.2.1': {} 1243 | 1244 | '@jridgewell/sourcemap-codec@1.5.0': {} 1245 | 1246 | '@jridgewell/trace-mapping@0.3.25': 1247 | dependencies: 1248 | '@jridgewell/resolve-uri': 3.1.2 1249 | '@jridgewell/sourcemap-codec': 1.5.0 1250 | 1251 | '@pkgjs/parseargs@0.11.0': 1252 | optional: true 1253 | 1254 | '@rollup/rollup-android-arm-eabi@4.34.3': 1255 | optional: true 1256 | 1257 | '@rollup/rollup-android-arm64@4.34.3': 1258 | optional: true 1259 | 1260 | '@rollup/rollup-darwin-arm64@4.34.3': 1261 | optional: true 1262 | 1263 | '@rollup/rollup-darwin-x64@4.34.3': 1264 | optional: true 1265 | 1266 | '@rollup/rollup-freebsd-arm64@4.34.3': 1267 | optional: true 1268 | 1269 | '@rollup/rollup-freebsd-x64@4.34.3': 1270 | optional: true 1271 | 1272 | '@rollup/rollup-linux-arm-gnueabihf@4.34.3': 1273 | optional: true 1274 | 1275 | '@rollup/rollup-linux-arm-musleabihf@4.34.3': 1276 | optional: true 1277 | 1278 | '@rollup/rollup-linux-arm64-gnu@4.34.3': 1279 | optional: true 1280 | 1281 | '@rollup/rollup-linux-arm64-musl@4.34.3': 1282 | optional: true 1283 | 1284 | '@rollup/rollup-linux-loongarch64-gnu@4.34.3': 1285 | optional: true 1286 | 1287 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.3': 1288 | optional: true 1289 | 1290 | '@rollup/rollup-linux-riscv64-gnu@4.34.3': 1291 | optional: true 1292 | 1293 | '@rollup/rollup-linux-s390x-gnu@4.34.3': 1294 | optional: true 1295 | 1296 | '@rollup/rollup-linux-x64-gnu@4.34.3': 1297 | optional: true 1298 | 1299 | '@rollup/rollup-linux-x64-musl@4.34.3': 1300 | optional: true 1301 | 1302 | '@rollup/rollup-win32-arm64-msvc@4.34.3': 1303 | optional: true 1304 | 1305 | '@rollup/rollup-win32-ia32-msvc@4.34.3': 1306 | optional: true 1307 | 1308 | '@rollup/rollup-win32-x64-msvc@4.34.3': 1309 | optional: true 1310 | 1311 | '@standard-schema/spec@1.0.0': {} 1312 | 1313 | '@types/estree@1.0.6': {} 1314 | 1315 | '@types/node@22.13.1': 1316 | dependencies: 1317 | undici-types: 6.20.0 1318 | 1319 | '@vitest/expect@3.0.5': 1320 | dependencies: 1321 | '@vitest/spy': 3.0.5 1322 | '@vitest/utils': 3.0.5 1323 | chai: 5.1.2 1324 | tinyrainbow: 2.0.0 1325 | 1326 | '@vitest/mocker@3.0.5(vite@6.0.11(@types/node@22.13.1))': 1327 | dependencies: 1328 | '@vitest/spy': 3.0.5 1329 | estree-walker: 3.0.3 1330 | magic-string: 0.30.17 1331 | optionalDependencies: 1332 | vite: 6.0.11(@types/node@22.13.1) 1333 | 1334 | '@vitest/pretty-format@3.0.5': 1335 | dependencies: 1336 | tinyrainbow: 2.0.0 1337 | 1338 | '@vitest/runner@3.0.5': 1339 | dependencies: 1340 | '@vitest/utils': 3.0.5 1341 | pathe: 2.0.2 1342 | 1343 | '@vitest/snapshot@3.0.5': 1344 | dependencies: 1345 | '@vitest/pretty-format': 3.0.5 1346 | magic-string: 0.30.17 1347 | pathe: 2.0.2 1348 | 1349 | '@vitest/spy@3.0.5': 1350 | dependencies: 1351 | tinyspy: 3.0.2 1352 | 1353 | '@vitest/utils@3.0.5': 1354 | dependencies: 1355 | '@vitest/pretty-format': 3.0.5 1356 | loupe: 3.1.3 1357 | tinyrainbow: 2.0.0 1358 | 1359 | '@zod/core@0.8.1': {} 1360 | 1361 | agent-base@7.1.3: {} 1362 | 1363 | ansi-regex@5.0.1: {} 1364 | 1365 | ansi-regex@6.1.0: {} 1366 | 1367 | ansi-styles@4.3.0: 1368 | dependencies: 1369 | color-convert: 2.0.1 1370 | 1371 | ansi-styles@6.2.1: {} 1372 | 1373 | any-promise@1.3.0: {} 1374 | 1375 | arktype@2.0.4: 1376 | dependencies: 1377 | '@ark/schema': 0.39.0 1378 | '@ark/util': 0.39.0 1379 | 1380 | assertion-error@2.0.1: {} 1381 | 1382 | asynckit@0.4.0: {} 1383 | 1384 | balanced-match@1.0.2: {} 1385 | 1386 | brace-expansion@2.0.1: 1387 | dependencies: 1388 | balanced-match: 1.0.2 1389 | 1390 | bundle-require@5.1.0(esbuild@0.24.2): 1391 | dependencies: 1392 | esbuild: 0.24.2 1393 | load-tsconfig: 0.2.5 1394 | 1395 | cac@6.7.14: {} 1396 | 1397 | chai@5.1.2: 1398 | dependencies: 1399 | assertion-error: 2.0.1 1400 | check-error: 2.1.1 1401 | deep-eql: 5.0.2 1402 | loupe: 3.1.3 1403 | pathval: 2.0.0 1404 | 1405 | check-error@2.1.1: {} 1406 | 1407 | chokidar@4.0.3: 1408 | dependencies: 1409 | readdirp: 4.1.1 1410 | 1411 | color-convert@2.0.1: 1412 | dependencies: 1413 | color-name: 1.1.4 1414 | 1415 | color-name@1.1.4: {} 1416 | 1417 | combined-stream@1.0.8: 1418 | dependencies: 1419 | delayed-stream: 1.0.0 1420 | 1421 | commander@4.1.1: {} 1422 | 1423 | consola@3.4.0: {} 1424 | 1425 | cross-spawn@7.0.6: 1426 | dependencies: 1427 | path-key: 3.1.1 1428 | shebang-command: 2.0.0 1429 | which: 2.0.2 1430 | 1431 | cssstyle@4.2.1: 1432 | dependencies: 1433 | '@asamuzakjp/css-color': 2.8.3 1434 | rrweb-cssom: 0.8.0 1435 | 1436 | data-urls@5.0.0: 1437 | dependencies: 1438 | whatwg-mimetype: 4.0.0 1439 | whatwg-url: 14.1.0 1440 | 1441 | debug@4.4.0: 1442 | dependencies: 1443 | ms: 2.1.3 1444 | 1445 | decimal.js@10.5.0: {} 1446 | 1447 | deep-eql@5.0.2: {} 1448 | 1449 | delayed-stream@1.0.0: {} 1450 | 1451 | eastasianwidth@0.2.0: {} 1452 | 1453 | emoji-regex@8.0.0: {} 1454 | 1455 | emoji-regex@9.2.2: {} 1456 | 1457 | entities@4.5.0: {} 1458 | 1459 | es-module-lexer@1.6.0: {} 1460 | 1461 | esbuild@0.24.2: 1462 | optionalDependencies: 1463 | '@esbuild/aix-ppc64': 0.24.2 1464 | '@esbuild/android-arm': 0.24.2 1465 | '@esbuild/android-arm64': 0.24.2 1466 | '@esbuild/android-x64': 0.24.2 1467 | '@esbuild/darwin-arm64': 0.24.2 1468 | '@esbuild/darwin-x64': 0.24.2 1469 | '@esbuild/freebsd-arm64': 0.24.2 1470 | '@esbuild/freebsd-x64': 0.24.2 1471 | '@esbuild/linux-arm': 0.24.2 1472 | '@esbuild/linux-arm64': 0.24.2 1473 | '@esbuild/linux-ia32': 0.24.2 1474 | '@esbuild/linux-loong64': 0.24.2 1475 | '@esbuild/linux-mips64el': 0.24.2 1476 | '@esbuild/linux-ppc64': 0.24.2 1477 | '@esbuild/linux-riscv64': 0.24.2 1478 | '@esbuild/linux-s390x': 0.24.2 1479 | '@esbuild/linux-x64': 0.24.2 1480 | '@esbuild/netbsd-arm64': 0.24.2 1481 | '@esbuild/netbsd-x64': 0.24.2 1482 | '@esbuild/openbsd-arm64': 0.24.2 1483 | '@esbuild/openbsd-x64': 0.24.2 1484 | '@esbuild/sunos-x64': 0.24.2 1485 | '@esbuild/win32-arm64': 0.24.2 1486 | '@esbuild/win32-ia32': 0.24.2 1487 | '@esbuild/win32-x64': 0.24.2 1488 | 1489 | estree-walker@3.0.3: 1490 | dependencies: 1491 | '@types/estree': 1.0.6 1492 | 1493 | expect-type@1.1.0: {} 1494 | 1495 | fdir@6.4.3(picomatch@4.0.2): 1496 | optionalDependencies: 1497 | picomatch: 4.0.2 1498 | 1499 | foreground-child@3.3.0: 1500 | dependencies: 1501 | cross-spawn: 7.0.6 1502 | signal-exit: 4.1.0 1503 | 1504 | form-data@4.0.1: 1505 | dependencies: 1506 | asynckit: 0.4.0 1507 | combined-stream: 1.0.8 1508 | mime-types: 2.1.35 1509 | 1510 | fsevents@2.3.3: 1511 | optional: true 1512 | 1513 | glob@10.4.5: 1514 | dependencies: 1515 | foreground-child: 3.3.0 1516 | jackspeak: 3.4.3 1517 | minimatch: 9.0.5 1518 | minipass: 7.1.2 1519 | package-json-from-dist: 1.0.1 1520 | path-scurry: 1.11.1 1521 | 1522 | html-encoding-sniffer@4.0.0: 1523 | dependencies: 1524 | whatwg-encoding: 3.1.1 1525 | 1526 | http-proxy-agent@7.0.2: 1527 | dependencies: 1528 | agent-base: 7.1.3 1529 | debug: 4.4.0 1530 | transitivePeerDependencies: 1531 | - supports-color 1532 | 1533 | https-proxy-agent@7.0.6: 1534 | dependencies: 1535 | agent-base: 7.1.3 1536 | debug: 4.4.0 1537 | transitivePeerDependencies: 1538 | - supports-color 1539 | 1540 | iconv-lite@0.6.3: 1541 | dependencies: 1542 | safer-buffer: 2.1.2 1543 | 1544 | is-fullwidth-code-point@3.0.0: {} 1545 | 1546 | is-potential-custom-element-name@1.0.1: {} 1547 | 1548 | isexe@2.0.0: {} 1549 | 1550 | jackspeak@3.4.3: 1551 | dependencies: 1552 | '@isaacs/cliui': 8.0.2 1553 | optionalDependencies: 1554 | '@pkgjs/parseargs': 0.11.0 1555 | 1556 | joycon@3.1.1: {} 1557 | 1558 | jsdom@24.1.3: 1559 | dependencies: 1560 | cssstyle: 4.2.1 1561 | data-urls: 5.0.0 1562 | decimal.js: 10.5.0 1563 | form-data: 4.0.1 1564 | html-encoding-sniffer: 4.0.0 1565 | http-proxy-agent: 7.0.2 1566 | https-proxy-agent: 7.0.6 1567 | is-potential-custom-element-name: 1.0.1 1568 | nwsapi: 2.2.16 1569 | parse5: 7.2.1 1570 | rrweb-cssom: 0.7.1 1571 | saxes: 6.0.0 1572 | symbol-tree: 3.2.4 1573 | tough-cookie: 4.1.4 1574 | w3c-xmlserializer: 5.0.0 1575 | webidl-conversions: 7.0.0 1576 | whatwg-encoding: 3.1.1 1577 | whatwg-mimetype: 4.0.0 1578 | whatwg-url: 14.1.0 1579 | ws: 8.18.0 1580 | xml-name-validator: 5.0.0 1581 | transitivePeerDependencies: 1582 | - bufferutil 1583 | - supports-color 1584 | - utf-8-validate 1585 | 1586 | lilconfig@3.1.3: {} 1587 | 1588 | lines-and-columns@1.2.4: {} 1589 | 1590 | load-tsconfig@0.2.5: {} 1591 | 1592 | lodash.sortby@4.7.0: {} 1593 | 1594 | loupe@3.1.3: {} 1595 | 1596 | lru-cache@10.4.3: {} 1597 | 1598 | magic-string@0.30.17: 1599 | dependencies: 1600 | '@jridgewell/sourcemap-codec': 1.5.0 1601 | 1602 | mime-db@1.52.0: {} 1603 | 1604 | mime-types@2.1.35: 1605 | dependencies: 1606 | mime-db: 1.52.0 1607 | 1608 | minimatch@9.0.5: 1609 | dependencies: 1610 | brace-expansion: 2.0.1 1611 | 1612 | minipass@7.1.2: {} 1613 | 1614 | ms@2.1.3: {} 1615 | 1616 | mz@2.7.0: 1617 | dependencies: 1618 | any-promise: 1.3.0 1619 | object-assign: 4.1.1 1620 | thenify-all: 1.6.0 1621 | 1622 | nanoid@3.3.8: {} 1623 | 1624 | nwsapi@2.2.16: {} 1625 | 1626 | object-assign@4.1.1: {} 1627 | 1628 | package-json-from-dist@1.0.1: {} 1629 | 1630 | parse5@7.2.1: 1631 | dependencies: 1632 | entities: 4.5.0 1633 | 1634 | path-key@3.1.1: {} 1635 | 1636 | path-scurry@1.11.1: 1637 | dependencies: 1638 | lru-cache: 10.4.3 1639 | minipass: 7.1.2 1640 | 1641 | pathe@2.0.2: {} 1642 | 1643 | pathval@2.0.0: {} 1644 | 1645 | picocolors@1.1.1: {} 1646 | 1647 | picomatch@4.0.2: {} 1648 | 1649 | pirates@4.0.6: {} 1650 | 1651 | postcss-load-config@6.0.1(postcss@8.5.1): 1652 | dependencies: 1653 | lilconfig: 3.1.3 1654 | optionalDependencies: 1655 | postcss: 8.5.1 1656 | 1657 | postcss@8.5.1: 1658 | dependencies: 1659 | nanoid: 3.3.8 1660 | picocolors: 1.1.1 1661 | source-map-js: 1.2.1 1662 | 1663 | psl@1.15.0: 1664 | dependencies: 1665 | punycode: 2.3.1 1666 | 1667 | punycode@2.3.1: {} 1668 | 1669 | querystringify@2.2.0: {} 1670 | 1671 | readdirp@4.1.1: {} 1672 | 1673 | requires-port@1.0.0: {} 1674 | 1675 | resolve-from@5.0.0: {} 1676 | 1677 | rollup@4.34.3: 1678 | dependencies: 1679 | '@types/estree': 1.0.6 1680 | optionalDependencies: 1681 | '@rollup/rollup-android-arm-eabi': 4.34.3 1682 | '@rollup/rollup-android-arm64': 4.34.3 1683 | '@rollup/rollup-darwin-arm64': 4.34.3 1684 | '@rollup/rollup-darwin-x64': 4.34.3 1685 | '@rollup/rollup-freebsd-arm64': 4.34.3 1686 | '@rollup/rollup-freebsd-x64': 4.34.3 1687 | '@rollup/rollup-linux-arm-gnueabihf': 4.34.3 1688 | '@rollup/rollup-linux-arm-musleabihf': 4.34.3 1689 | '@rollup/rollup-linux-arm64-gnu': 4.34.3 1690 | '@rollup/rollup-linux-arm64-musl': 4.34.3 1691 | '@rollup/rollup-linux-loongarch64-gnu': 4.34.3 1692 | '@rollup/rollup-linux-powerpc64le-gnu': 4.34.3 1693 | '@rollup/rollup-linux-riscv64-gnu': 4.34.3 1694 | '@rollup/rollup-linux-s390x-gnu': 4.34.3 1695 | '@rollup/rollup-linux-x64-gnu': 4.34.3 1696 | '@rollup/rollup-linux-x64-musl': 4.34.3 1697 | '@rollup/rollup-win32-arm64-msvc': 4.34.3 1698 | '@rollup/rollup-win32-ia32-msvc': 4.34.3 1699 | '@rollup/rollup-win32-x64-msvc': 4.34.3 1700 | fsevents: 2.3.3 1701 | 1702 | rrweb-cssom@0.7.1: {} 1703 | 1704 | rrweb-cssom@0.8.0: {} 1705 | 1706 | safer-buffer@2.1.2: {} 1707 | 1708 | saxes@6.0.0: 1709 | dependencies: 1710 | xmlchars: 2.2.0 1711 | 1712 | shebang-command@2.0.0: 1713 | dependencies: 1714 | shebang-regex: 3.0.0 1715 | 1716 | shebang-regex@3.0.0: {} 1717 | 1718 | siginfo@2.0.0: {} 1719 | 1720 | signal-exit@4.1.0: {} 1721 | 1722 | source-map-js@1.2.1: {} 1723 | 1724 | source-map@0.8.0-beta.0: 1725 | dependencies: 1726 | whatwg-url: 7.1.0 1727 | 1728 | stackback@0.0.2: {} 1729 | 1730 | std-env@3.8.0: {} 1731 | 1732 | string-ts@2.2.1: {} 1733 | 1734 | string-width@4.2.3: 1735 | dependencies: 1736 | emoji-regex: 8.0.0 1737 | is-fullwidth-code-point: 3.0.0 1738 | strip-ansi: 6.0.1 1739 | 1740 | string-width@5.1.2: 1741 | dependencies: 1742 | eastasianwidth: 0.2.0 1743 | emoji-regex: 9.2.2 1744 | strip-ansi: 7.1.0 1745 | 1746 | strip-ansi@6.0.1: 1747 | dependencies: 1748 | ansi-regex: 5.0.1 1749 | 1750 | strip-ansi@7.1.0: 1751 | dependencies: 1752 | ansi-regex: 6.1.0 1753 | 1754 | sucrase@3.35.0: 1755 | dependencies: 1756 | '@jridgewell/gen-mapping': 0.3.8 1757 | commander: 4.1.1 1758 | glob: 10.4.5 1759 | lines-and-columns: 1.2.4 1760 | mz: 2.7.0 1761 | pirates: 4.0.6 1762 | ts-interface-checker: 0.1.13 1763 | 1764 | symbol-tree@3.2.4: {} 1765 | 1766 | thenify-all@1.6.0: 1767 | dependencies: 1768 | thenify: 3.3.1 1769 | 1770 | thenify@3.3.1: 1771 | dependencies: 1772 | any-promise: 1.3.0 1773 | 1774 | tinybench@2.9.0: {} 1775 | 1776 | tinyexec@0.3.2: {} 1777 | 1778 | tinyglobby@0.2.10: 1779 | dependencies: 1780 | fdir: 6.4.3(picomatch@4.0.2) 1781 | picomatch: 4.0.2 1782 | 1783 | tinypool@1.0.2: {} 1784 | 1785 | tinyrainbow@2.0.0: {} 1786 | 1787 | tinyspy@3.0.2: {} 1788 | 1789 | tough-cookie@4.1.4: 1790 | dependencies: 1791 | psl: 1.15.0 1792 | punycode: 2.3.1 1793 | universalify: 0.2.0 1794 | url-parse: 1.5.10 1795 | 1796 | tr46@1.0.1: 1797 | dependencies: 1798 | punycode: 2.3.1 1799 | 1800 | tr46@5.0.0: 1801 | dependencies: 1802 | punycode: 2.3.1 1803 | 1804 | tree-kill@1.2.2: {} 1805 | 1806 | ts-interface-checker@0.1.13: {} 1807 | 1808 | tsup@8.3.6(postcss@8.5.1)(typescript@5.7.3): 1809 | dependencies: 1810 | bundle-require: 5.1.0(esbuild@0.24.2) 1811 | cac: 6.7.14 1812 | chokidar: 4.0.3 1813 | consola: 3.4.0 1814 | debug: 4.4.0 1815 | esbuild: 0.24.2 1816 | joycon: 3.1.1 1817 | picocolors: 1.1.1 1818 | postcss-load-config: 6.0.1(postcss@8.5.1) 1819 | resolve-from: 5.0.0 1820 | rollup: 4.34.3 1821 | source-map: 0.8.0-beta.0 1822 | sucrase: 3.35.0 1823 | tinyexec: 0.3.2 1824 | tinyglobby: 0.2.10 1825 | tree-kill: 1.2.2 1826 | optionalDependencies: 1827 | postcss: 8.5.1 1828 | typescript: 5.7.3 1829 | transitivePeerDependencies: 1830 | - jiti 1831 | - supports-color 1832 | - tsx 1833 | - yaml 1834 | 1835 | typescript@5.7.3: {} 1836 | 1837 | undici-types@6.20.0: {} 1838 | 1839 | universalify@0.2.0: {} 1840 | 1841 | url-parse@1.5.10: 1842 | dependencies: 1843 | querystringify: 2.2.0 1844 | requires-port: 1.0.0 1845 | 1846 | vite-node@3.0.5(@types/node@22.13.1): 1847 | dependencies: 1848 | cac: 6.7.14 1849 | debug: 4.4.0 1850 | es-module-lexer: 1.6.0 1851 | pathe: 2.0.2 1852 | vite: 6.0.11(@types/node@22.13.1) 1853 | transitivePeerDependencies: 1854 | - '@types/node' 1855 | - jiti 1856 | - less 1857 | - lightningcss 1858 | - sass 1859 | - sass-embedded 1860 | - stylus 1861 | - sugarss 1862 | - supports-color 1863 | - terser 1864 | - tsx 1865 | - yaml 1866 | 1867 | vite@6.0.11(@types/node@22.13.1): 1868 | dependencies: 1869 | esbuild: 0.24.2 1870 | postcss: 8.5.1 1871 | rollup: 4.34.3 1872 | optionalDependencies: 1873 | '@types/node': 22.13.1 1874 | fsevents: 2.3.3 1875 | 1876 | vitest@3.0.5(@types/node@22.13.1)(jsdom@24.1.3): 1877 | dependencies: 1878 | '@vitest/expect': 3.0.5 1879 | '@vitest/mocker': 3.0.5(vite@6.0.11(@types/node@22.13.1)) 1880 | '@vitest/pretty-format': 3.0.5 1881 | '@vitest/runner': 3.0.5 1882 | '@vitest/snapshot': 3.0.5 1883 | '@vitest/spy': 3.0.5 1884 | '@vitest/utils': 3.0.5 1885 | chai: 5.1.2 1886 | debug: 4.4.0 1887 | expect-type: 1.1.0 1888 | magic-string: 0.30.17 1889 | pathe: 2.0.2 1890 | std-env: 3.8.0 1891 | tinybench: 2.9.0 1892 | tinyexec: 0.3.2 1893 | tinypool: 1.0.2 1894 | tinyrainbow: 2.0.0 1895 | vite: 6.0.11(@types/node@22.13.1) 1896 | vite-node: 3.0.5(@types/node@22.13.1) 1897 | why-is-node-running: 2.3.0 1898 | optionalDependencies: 1899 | '@types/node': 22.13.1 1900 | jsdom: 24.1.3 1901 | transitivePeerDependencies: 1902 | - jiti 1903 | - less 1904 | - lightningcss 1905 | - msw 1906 | - sass 1907 | - sass-embedded 1908 | - stylus 1909 | - sugarss 1910 | - supports-color 1911 | - terser 1912 | - tsx 1913 | - yaml 1914 | 1915 | w3c-xmlserializer@5.0.0: 1916 | dependencies: 1917 | xml-name-validator: 5.0.0 1918 | 1919 | webidl-conversions@4.0.2: {} 1920 | 1921 | webidl-conversions@7.0.0: {} 1922 | 1923 | whatwg-encoding@3.1.1: 1924 | dependencies: 1925 | iconv-lite: 0.6.3 1926 | 1927 | whatwg-mimetype@4.0.0: {} 1928 | 1929 | whatwg-url@14.1.0: 1930 | dependencies: 1931 | tr46: 5.0.0 1932 | webidl-conversions: 7.0.0 1933 | 1934 | whatwg-url@7.1.0: 1935 | dependencies: 1936 | lodash.sortby: 4.7.0 1937 | tr46: 1.0.1 1938 | webidl-conversions: 4.0.2 1939 | 1940 | which@2.0.2: 1941 | dependencies: 1942 | isexe: 2.0.0 1943 | 1944 | why-is-node-running@2.3.0: 1945 | dependencies: 1946 | siginfo: 2.0.0 1947 | stackback: 0.0.2 1948 | 1949 | wrap-ansi@7.0.0: 1950 | dependencies: 1951 | ansi-styles: 4.3.0 1952 | string-width: 4.2.3 1953 | strip-ansi: 6.0.1 1954 | 1955 | wrap-ansi@8.1.0: 1956 | dependencies: 1957 | ansi-styles: 6.2.1 1958 | string-width: 5.1.2 1959 | strip-ansi: 7.1.0 1960 | 1961 | ws@8.18.0: {} 1962 | 1963 | xml-name-validator@5.0.0: {} 1964 | 1965 | xmlchars@2.2.0: {} 1966 | 1967 | zod@4.0.0-beta.20250420T053007: 1968 | dependencies: 1969 | '@zod/core': 0.8.1 1970 | -------------------------------------------------------------------------------- /src/api.test.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'arktype' 2 | import { deepCamelKeys } from 'string-ts' 3 | import { z } from 'zod' 4 | import * as subject from './api' 5 | import { HTTP_METHODS } from './constants' 6 | import { ParseResponseError } from './primitives' 7 | import type { HTTPMethod } from './types' 8 | 9 | const reqMock = vi.fn() 10 | function successfulFetch(response: string | Record) { 11 | return async (input: URL | RequestInfo, init?: RequestInit | undefined) => { 12 | reqMock({ 13 | url: input, 14 | headers: init?.headers, 15 | method: init?.method, 16 | body: init?.body, 17 | }) 18 | return new Response( 19 | typeof response === 'string' ? response : JSON.stringify(response) 20 | ) 21 | } 22 | } 23 | 24 | beforeEach(() => { 25 | vi.clearAllMocks() 26 | }) 27 | 28 | describe('enhancedFetch', () => { 29 | describe('json', () => { 30 | it('should be untyped by default', async () => { 31 | vi.spyOn(global, 'fetch').mockImplementationOnce( 32 | successfulFetch({ foo: 'bar' }) 33 | ) 34 | const result = await subject 35 | .enhancedFetch('https://example.com/api/users') 36 | .then((r) => r.json()) 37 | type _R = Expect> 38 | expect(result).toEqual({ foo: 'bar' }) 39 | }) 40 | 41 | it('should accept a type', async () => { 42 | vi.spyOn(global, 'fetch').mockImplementationOnce( 43 | successfulFetch({ foo: 'bar' }) 44 | ) 45 | const result = await subject 46 | .enhancedFetch('https://example.com/api/users') 47 | .then((r) => r.json<{ foo: string }>()) 48 | type _R = Expect> 49 | expect(result).toEqual({ foo: 'bar' }) 50 | }) 51 | 52 | it('should accept a parser', async () => { 53 | vi.spyOn(global, 'fetch').mockImplementationOnce( 54 | successfulFetch({ foo: 'bar' }) 55 | ) 56 | const result = await subject 57 | .enhancedFetch('https://example.com/api/users') 58 | .then((r) => r.json(z.object({ foo: z.string() }))) 59 | type _R = Expect> 60 | expect(result).toEqual({ foo: 'bar' }) 61 | }) 62 | }) 63 | 64 | describe('text', () => { 65 | it('should be untyped by default', async () => { 66 | vi.spyOn(global, 'fetch').mockImplementationOnce( 67 | successfulFetch({ foo: 'bar' }) 68 | ) 69 | const result = await subject 70 | .enhancedFetch('https://example.com/api/users') 71 | .then((r) => r.text()) 72 | type _R = Expect> 73 | expect(result).toEqual(`{"foo":"bar"}`) 74 | }) 75 | 76 | it('should accept a type', async () => { 77 | vi.spyOn(global, 'fetch').mockImplementationOnce( 78 | successfulFetch('john@doe.com') 79 | ) 80 | const result = await subject 81 | .enhancedFetch('https://example.com/api/users') 82 | .then((r) => r.text<`${string}@${string}.${string}`>()) 83 | type _R = Expect> 84 | expect(result).toEqual('john@doe.com') 85 | }) 86 | 87 | it('should accept a parser', async () => { 88 | vi.spyOn(global, 'fetch').mockImplementationOnce( 89 | successfulFetch('john@doe.com') 90 | ) 91 | const result = await subject 92 | .enhancedFetch('https://example.com/api/users') 93 | .then((r) => r.text(z.string().email())) 94 | type _R = Expect> 95 | expect(result).toEqual('john@doe.com') 96 | }) 97 | }) 98 | 99 | it('should accept a schema that transforms the response', async () => { 100 | vi.spyOn(global, 'fetch').mockImplementationOnce( 101 | successfulFetch({ foo: { 'deep-nested': { 'kind-of-value': true } } }) 102 | ) 103 | const parsedResult = await subject 104 | .enhancedFetch('https://example.com/api/users') 105 | .then((r) => 106 | r.json( 107 | z.object({ 108 | foo: z.object({ 109 | 'deep-nested': z.object({ 'kind-of-value': z.boolean() }), 110 | }), 111 | }) 112 | ) 113 | ) 114 | const result = deepCamelKeys(parsedResult) 115 | type _R = Expect< 116 | Equal 117 | > 118 | expect(result).toEqual({ foo: { deepNested: { kindOfValue: true } } }) 119 | }) 120 | 121 | it('should replace params in the URL', async () => { 122 | vi.spyOn(global, 'fetch').mockImplementationOnce( 123 | successfulFetch({ foo: 'bar' }) 124 | ) 125 | await subject.enhancedFetch( 126 | 'https://example.com/api/users/:user/page/:page', 127 | { 128 | params: { 129 | user: '1', 130 | page: '2', 131 | // @ts-expect-error argument not infered from URL 132 | foo: 'bar', 133 | }, 134 | } 135 | ) 136 | expect(reqMock).toHaveBeenCalledWith({ 137 | url: 'https://example.com/api/users/1/page/2', 138 | }) 139 | }) 140 | 141 | it('should accept a requestInit and a query', async () => { 142 | vi.spyOn(global, 'fetch').mockImplementationOnce( 143 | successfulFetch({ foo: 'bar' }) 144 | ) 145 | await subject.enhancedFetch('https://example.com/api/users', { 146 | headers: { Authorization: 'Bearer 123' }, 147 | query: { admin: 'true' }, 148 | }) 149 | expect(reqMock).toHaveBeenCalledWith({ 150 | url: 'https://example.com/api/users?admin=true', 151 | headers: { Authorization: 'Bearer 123' }, 152 | }) 153 | }) 154 | 155 | it('should accept a stringified body', async () => { 156 | vi.spyOn(global, 'fetch').mockImplementationOnce( 157 | successfulFetch({ foo: 'bar' }) 158 | ) 159 | await subject.enhancedFetch('https://example.com/api/users', { 160 | body: JSON.stringify({ id: 1, name: { first: 'John', last: 'Doe' } }), 161 | method: 'POST', 162 | }) 163 | expect(reqMock).toHaveBeenCalledWith({ 164 | url: 'https://example.com/api/users', 165 | method: 'POST', 166 | body: `{"id":1,"name":{"first":"John","last":"Doe"}}`, 167 | }) 168 | }) 169 | 170 | it('should stringify the body', async () => { 171 | vi.spyOn(global, 'fetch').mockImplementationOnce( 172 | successfulFetch({ foo: 'bar' }) 173 | ) 174 | await subject.enhancedFetch('https://example.com/api/users', { 175 | body: { id: 1, name: { first: 'John', last: 'Doe' } }, 176 | method: 'POST', 177 | }) 178 | expect(reqMock).toHaveBeenCalledWith({ 179 | url: 'https://example.com/api/users', 180 | method: 'POST', 181 | body: `{"id":1,"name":{"first":"John","last":"Doe"}}`, 182 | }) 183 | }) 184 | 185 | it('should accept a trace function for debugging purposes', async () => { 186 | const trace = vi.fn() 187 | vi.spyOn(global, 'fetch').mockImplementationOnce( 188 | successfulFetch({ foo: 'bar' }) 189 | ) 190 | await subject.enhancedFetch('https://example.com/api/users', { 191 | body: { id: 1, name: { first: 'John', last: 'Doe' } }, 192 | query: { admin: 'true' }, 193 | trace, 194 | method: 'POST', 195 | }) 196 | expect(trace).toHaveBeenCalledWith( 197 | 'https://example.com/api/users?admin=true', 198 | { 199 | method: 'POST', 200 | body: `{"id":1,"name":{"first":"John","last":"Doe"}}`, 201 | }, 202 | expect.any(Response) 203 | ) 204 | }) 205 | 206 | it('should return some result from other Response methods', async () => { 207 | const response = await subject.enhancedFetch('data:text/plain;,foo') 208 | const blob = await response.blob() 209 | expect(await blob.text()).toEqual('foo') 210 | }) 211 | }) 212 | 213 | describe('makeFetcher', () => { 214 | it('should return an applied enhancedFetch', async () => { 215 | vi.spyOn(global, 'fetch').mockImplementationOnce( 216 | successfulFetch({ foo: 'bar' }) 217 | ) 218 | const service = subject.makeFetcher('https://example.com/api') 219 | const result = await service('/users', { method: 'post' }).then((r) => 220 | r.json(z.object({ foo: z.string() })) 221 | ) 222 | type _R = Expect> 223 | 224 | expect(result).toEqual({ foo: 'bar' }) 225 | expect(reqMock).toHaveBeenCalledWith({ 226 | url: 'https://example.com/api/users', 227 | method: 'post', 228 | headers: new Headers(), 229 | }) 230 | }) 231 | 232 | it('should add headers to the request', async () => { 233 | vi.spyOn(global, 'fetch').mockImplementationOnce( 234 | successfulFetch({ foo: 'bar' }) 235 | ) 236 | const fetcher = subject.makeFetcher('https://example.com/api', { 237 | headers: { 238 | Authorization: 'Bearer 123', 239 | }, 240 | }) 241 | await fetcher('/users', { headers: { 'Content-type': 'application/json' } }) 242 | expect(reqMock).toHaveBeenCalledWith({ 243 | url: 'https://example.com/api/users', 244 | headers: new Headers({ 245 | authorization: 'Bearer 123', 246 | 'content-type': 'application/json', 247 | }), 248 | }) 249 | }) 250 | 251 | it('should transform the request', async () => { 252 | vi.spyOn(global, 'fetch').mockImplementationOnce( 253 | successfulFetch({ foo: 'bar' }) 254 | ) 255 | const fetcher = subject.makeFetcher('https://example.com/api', { 256 | headers: { 257 | Authorization: 'Bearer 123', 258 | }, 259 | requestTransformer: (request) => ({ ...request, query: { foo: 'bar' } }), 260 | }) 261 | await fetcher('/users') 262 | expect(reqMock).toHaveBeenCalledWith({ 263 | url: 'https://example.com/api/users?foo=bar', 264 | headers: new Headers({ authorization: 'Bearer 123' }), 265 | }) 266 | }) 267 | 268 | it('should transform the response', async () => { 269 | vi.spyOn(global, 'fetch').mockImplementationOnce( 270 | successfulFetch({ foo: 'bar' }) 271 | ) 272 | const fetcher = subject.makeFetcher('https://example.com/api', { 273 | headers: { 274 | Authorization: 'Bearer 123', 275 | }, 276 | responseTransformer: (response) => ({ 277 | ...response, 278 | statusText: 'Foo Bar', 279 | }), 280 | }) 281 | const response = await fetcher('/users') 282 | expect(response.statusText).toEqual('Foo Bar') 283 | expect(reqMock).toHaveBeenCalledWith({ 284 | url: 'https://example.com/api/users', 285 | headers: new Headers({ authorization: 'Bearer 123' }), 286 | }) 287 | }) 288 | 289 | it('should accept a typed params object', async () => { 290 | vi.spyOn(global, 'fetch').mockImplementationOnce( 291 | successfulFetch({ foo: 'bar' }) 292 | ) 293 | const fetcher = subject.makeFetcher('https://example.com/api') 294 | await fetcher('/users/:id', { 295 | params: { 296 | id: '1', 297 | // @ts-expect-error argument not infered from URL 298 | foo: 'bar', 299 | }, 300 | }) 301 | expect(reqMock).toHaveBeenCalledWith({ 302 | url: 'https://example.com/api/users/1', 303 | headers: new Headers(), 304 | }) 305 | }) 306 | 307 | it('should accept a function for dynamic headers', async () => { 308 | vi.spyOn(global, 'fetch').mockImplementationOnce( 309 | successfulFetch({ foo: 'bar' }) 310 | ) 311 | const fetcher = subject.makeFetcher('https://example.com/api', { 312 | headers: () => ({ 313 | Authorization: 'Bearer 123', 314 | }), 315 | }) 316 | await fetcher('/users') 317 | expect(reqMock).toHaveBeenCalledWith({ 318 | url: 'https://example.com/api/users', 319 | headers: new Headers({ authorization: 'Bearer 123' }), 320 | }) 321 | }) 322 | 323 | it('should accept an async function for dynamic headers', async () => { 324 | vi.spyOn(global, 'fetch').mockImplementationOnce( 325 | successfulFetch({ foo: 'bar' }) 326 | ) 327 | const fetcher = subject.makeFetcher('https://example.com/api', { 328 | headers: async () => ({ 329 | Authorization: 'Bearer 123', 330 | }), 331 | }) 332 | await fetcher('/users', { 333 | headers: new Headers({ 'content-type': 'application/json' }), 334 | }) 335 | expect(reqMock).toHaveBeenCalledWith({ 336 | url: 'https://example.com/api/users', 337 | headers: new Headers({ 338 | authorization: 'Bearer 123', 339 | 'content-type': 'application/json', 340 | }), 341 | }) 342 | }) 343 | 344 | it('should accept a query, trace, and JSON-like body', async () => { 345 | const trace = vi.fn() 346 | vi.spyOn(global, 'fetch').mockImplementationOnce( 347 | successfulFetch({ foo: 'bar' }) 348 | ) 349 | const fetcher = subject.makeFetcher('https://example.com/api') 350 | await fetcher('/users', { 351 | method: 'POST', 352 | body: { id: 1, name: { first: 'John', last: 'Doe' } }, 353 | query: { admin: 'true' }, 354 | trace, 355 | }) 356 | expect(trace).toHaveBeenCalledWith( 357 | 'https://example.com/api/users?admin=true', 358 | { 359 | method: 'POST', 360 | body: `{"id":1,"name":{"first":"John","last":"Doe"}}`, 361 | headers: new Headers(), 362 | }, 363 | expect.any(Response) 364 | ) 365 | }) 366 | }) 367 | 368 | describe('makeService', () => { 369 | it('should return an object with http methods', () => { 370 | const service = subject.makeService('https://example.com/api') 371 | for (const method of HTTP_METHODS) { 372 | expect( 373 | typeof service[method.toLocaleLowerCase() as Lowercase] 374 | ).toBe('function') 375 | } 376 | }) 377 | 378 | it('should return an API with enhancedFetch', async () => { 379 | vi.spyOn(global, 'fetch').mockImplementationOnce( 380 | successfulFetch({ foo: 'bar' }) 381 | ) 382 | const service = subject.makeService('https://example.com/api') 383 | const result = await service 384 | .post('/users') 385 | .then((r) => r.json(z.object({ foo: z.string() }))) 386 | type _R = Expect> 387 | 388 | expect(result).toEqual({ foo: 'bar' }) 389 | expect(reqMock).toHaveBeenCalledWith({ 390 | url: 'https://example.com/api/users', 391 | method: 'POST', 392 | headers: new Headers(), 393 | }) 394 | }) 395 | 396 | it('should accept a typed params object', async () => { 397 | vi.spyOn(global, 'fetch').mockImplementationOnce( 398 | successfulFetch({ foo: 'bar' }) 399 | ) 400 | const service = subject.makeService('https://example.com/api') 401 | await service.get('/users/:id', { 402 | params: { 403 | id: '1', 404 | // @ts-expect-error argument not infered from URL 405 | foo: 'bar', 406 | }, 407 | }) 408 | expect(reqMock).toHaveBeenCalledWith({ 409 | url: 'https://example.com/api/users/1', 410 | method: 'GET', 411 | headers: new Headers(), 412 | }) 413 | }) 414 | }) 415 | 416 | describe('typedResponse', () => { 417 | it('should return unknown by default when turning into a JSON', async () => { 418 | const result = await subject.typedResponse(new Response('1')).json() 419 | type _R = Expect> 420 | expect(result).toEqual(1) 421 | }) 422 | 423 | it('should accept a type for the JSON method', async () => { 424 | const result = await subject 425 | .typedResponse(new Response(`{"foo":"bar"}`)) 426 | .json<{ foo: string }>() 427 | type _R = Expect> 428 | expect(result).toEqual({ foo: 'bar' }) 429 | }) 430 | 431 | it('should accept a parser for the JSON method', async () => { 432 | const result = await subject 433 | .typedResponse(new Response(`{"foo":"bar"}`)) 434 | .json(z.object({ foo: z.string() })) 435 | type _R = Expect> 436 | expect(result).toEqual({ foo: 'bar' }) 437 | }) 438 | 439 | it('should accept other parsers, such as arktype for the JSON method', async () => { 440 | const result = await subject 441 | .typedResponse(new Response(`{"foo":"bar"}`)) 442 | .json(type({ foo: 'string' })) 443 | type _R = Expect> 444 | expect(result).toEqual({ foo: 'bar' }) 445 | }) 446 | 447 | it('should throw a ParseResponseError when the JSON does not match the parser', async () => { 448 | const response = new Response(`{"foo":1}`) 449 | try { 450 | await subject.typedResponse(response).json(z.object({ foo: z.string() })) 451 | } catch (error) { 452 | if (!(error instanceof ParseResponseError)) throw error 453 | 454 | expect(error).toBeInstanceOf(ParseResponseError) 455 | expect(error.message).toContain( 456 | `"message": "Failed to parse response.json"` 457 | ) 458 | expect(error.issues).toMatchObject([ 459 | { 460 | message: 'Invalid input: expected string, received number', 461 | path: ['foo'], 462 | }, 463 | ]) 464 | } 465 | }) 466 | 467 | it('should return string by default when turning into text', async () => { 468 | const result = await subject.typedResponse(new Response('foo')).text() 469 | type _R = Expect> 470 | expect(result).toBe('foo') 471 | }) 472 | 473 | it('should accept a type for the text method', async () => { 474 | const result = await subject 475 | .typedResponse(new Response('john@doe.com')) 476 | .text<`${string}@${string}.${string}`>() 477 | type _R = Expect> 478 | expect(result).toBe('john@doe.com') 479 | }) 480 | 481 | it('should accept a parser for the text method', async () => { 482 | const result = await subject 483 | .typedResponse(new Response('john@doe.com')) 484 | .text(z.string().email()) 485 | type _R = Expect> 486 | expect(result).toBe('john@doe.com') 487 | }) 488 | 489 | it('should throw a ParseResponseError when the text does not match the parser', async () => { 490 | const response = new Response('not an email') 491 | try { 492 | await subject.typedResponse(response).text(z.string().email()) 493 | } catch (error) { 494 | if (!(error instanceof ParseResponseError)) throw error 495 | 496 | expect(error).toBeInstanceOf(ParseResponseError) 497 | expect(error.message).toContain( 498 | `"message": "Failed to parse response.text"` 499 | ) 500 | expect(error.issues.length).toBeGreaterThan(0) 501 | } 502 | }) 503 | }) 504 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_METHODS } from './constants' 2 | import { getJson, getText } from './internals' 3 | import { 4 | addQueryToURL, 5 | ensureStringBody, 6 | makeGetApiURL, 7 | mergeHeaders, 8 | replaceURLParams, 9 | } from './primitives' 10 | import type { 11 | BaseOptions, 12 | EnhancedRequestInit, 13 | GetJson, 14 | GetText, 15 | HTTPMethod, 16 | ServiceRequestInit, 17 | TypedResponse, 18 | TypedResponseJson, 19 | TypedResponseText, 20 | } from './types' 21 | 22 | const identity = (value: T) => value 23 | 24 | /** 25 | * It hacks the Response object to add typed json and text methods 26 | * @param response the Response to be proxied 27 | * @returns a Response with typed json and text methods 28 | * @example const response = await fetch("https://example.com/api/users"); 29 | * const users = await response.json(userSchema); 30 | * // ^? User[] 31 | * const untyped = await response.json(); 32 | * // ^? unknown 33 | * const text = await response.text(); 34 | * // ^? string 35 | * const typedJson = await response.json(); 36 | * // ^? User[] 37 | */ 38 | function typedResponse( 39 | response: Response, 40 | options?: { getJson?: GetJson; getText?: GetText } 41 | ): TypedResponse { 42 | const getJsonFn = options?.getJson ?? getJson 43 | const getTextFn = options?.getText ?? getText 44 | 45 | return new Proxy(response, { 46 | get(target, prop) { 47 | if (prop === 'json') return getJsonFn(target) 48 | if (prop === 'text') return getTextFn(target) 49 | 50 | const value = Reflect.get(target, prop) 51 | 52 | if (typeof value === 'function') { 53 | return value.bind(target) 54 | } 55 | 56 | return value 57 | }, 58 | }) as Omit & { 59 | json: TypedResponseJson 60 | text: TypedResponseText 61 | } 62 | } 63 | 64 | /** 65 | * 66 | * @param url a string or URL to be fetched 67 | * @param requestInit the requestInit to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter. 68 | * @param requestInit.body the body of the request. It will be automatically stringified so you can send a JSON-like object 69 | * @param requestInit.query the query parameters to be added to the URL 70 | * @param requestInit.trace a function that receives the URL, the requestInit and a clone of the response in order to log or troubleshoot the request 71 | * @returns a Response with typed json and text methods 72 | * @example const response = await fetch("https://example.com/api/users"); 73 | * const users = await response.json(userSchema); 74 | * // ^? User[] 75 | * const untyped = await response.json(); 76 | * // ^? unknown 77 | */ 78 | async function enhancedFetch( 79 | url: T, 80 | requestInit?: EnhancedRequestInit 81 | ) { 82 | const { query, trace, ...reqInit } = requestInit ?? {} 83 | const body = ensureStringBody(reqInit.body) 84 | const withParams = replaceURLParams(url, reqInit.params ?? ({} as never)) 85 | const fullURL = addQueryToURL(withParams, query) 86 | const enhancedReqInit = { ...reqInit, body } 87 | 88 | const response = await fetch(fullURL, enhancedReqInit) 89 | 90 | await trace?.(fullURL, enhancedReqInit, typedResponse(response.clone())) 91 | return typedResponse(response) 92 | } 93 | 94 | /** 95 | * 96 | * @param baseURL the base URL to be fetched in every request 97 | * @param baseOptions options that will be applied to all requests 98 | * @param baseOptions.headers any headers that should be sent with every request 99 | * @param baseOptions.requestTransformer a function that will transform the enhanced request init of every request 100 | * @param baseOptions.responseTransformer a function that will transform the typed response of every request 101 | * @returns a function that receive a path and requestInit and return a serialized json response that can be typed or not. 102 | * @example const headers = { Authorization: "Bearer 123" } 103 | * const fetcher = makeFetcher("https://example.com/api", headers); 104 | * const response = await fetcher("/users", { method: "GET" }) 105 | * const users = await response.json(userSchema); 106 | * // ^? User[] 107 | */ 108 | function makeFetcher(baseURL: string | URL, baseOptions: BaseOptions = {}) { 109 | return async ( 110 | path: T, 111 | requestInit: EnhancedRequestInit = {} 112 | ) => { 113 | const { headers } = baseOptions 114 | const requestTransformer = baseOptions.requestTransformer ?? identity 115 | const responseTransformer = baseOptions.responseTransformer ?? identity 116 | const headerTransformer = async (ri: EnhancedRequestInit) => ({ 117 | ...ri, 118 | headers: mergeHeaders( 119 | typeof headers === 'function' ? await headers() : (headers ?? {}), 120 | ri.headers ?? {}, 121 | requestInit?.headers ?? {} 122 | ), 123 | }) 124 | 125 | const url = makeGetApiURL(baseURL)(path) 126 | const response = await enhancedFetch( 127 | url, 128 | await headerTransformer(await requestTransformer(requestInit)) 129 | ) 130 | return responseTransformer(response) 131 | } 132 | } 133 | 134 | /** 135 | * 136 | * @param baseURL the base URL to the API 137 | * @param baseOptions options that will be applied to all requests 138 | * @param baseOptions.headers any headers that should be sent with every request 139 | * @param baseOptions.requestTransformer a function that will transform the enhanced request init of every request 140 | * @param baseOptions.responseTransformer a function that will transform the typed response of every request 141 | * @returns a service object with HTTP methods that are functions that receive a path and requestInit and return a serialized json response that can be typed or not. 142 | * @example const headers = { Authorization: "Bearer 123" } 143 | * const api = makeService("https://example.com/api", headers); 144 | * const response = await api.get("/users") 145 | * const users = await response.json(userSchema); 146 | * // ^? User[] 147 | */ 148 | function makeService(baseURL: string | URL, baseOptions?: BaseOptions) { 149 | const fetcher = makeFetcher(baseURL, baseOptions) 150 | 151 | function appliedService(method: HTTPMethod) { 152 | return async ( 153 | path: T, 154 | requestInit: ServiceRequestInit = {} 155 | ) => fetcher(path, { ...requestInit, method }) 156 | } 157 | 158 | const service = {} as Record< 159 | Lowercase, 160 | ReturnType 161 | > 162 | for (const method of HTTP_METHODS) { 163 | const lowerMethod = method.toLowerCase() as Lowercase 164 | service[lowerMethod] = appliedService(method) 165 | } 166 | return service 167 | } 168 | 169 | export { enhancedFetch, makeFetcher, makeService, typedResponse } 170 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const HTTP_METHODS = [ 2 | 'GET', 3 | 'POST', 4 | 'PUT', 5 | 'DELETE', 6 | 'PATCH', 7 | 'OPTIONS', 8 | 'HEAD', 9 | 'CONNECT', 10 | // 'TRACE', it has no support in most browsers yet 11 | ] as const 12 | 13 | export { HTTP_METHODS } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { enhancedFetch, makeFetcher, makeService, typedResponse } from './api' 2 | export { 3 | addQueryToURL, 4 | ensureStringBody, 5 | makeGetApiURL, 6 | mergeHeaders, 7 | replaceURLParams, 8 | typeOf, 9 | ParseResponseError, 10 | } from './primitives' 11 | export type * from './types' 12 | -------------------------------------------------------------------------------- /src/internals.ts: -------------------------------------------------------------------------------- 1 | import type { StandardSchemaV1 } from '@standard-schema/spec' 2 | import { ParseResponseError } from './primitives' 3 | import type { GetJson, GetText } from './types' 4 | 5 | /** 6 | * It returns the JSON object or throws an error if the response is not ok. 7 | * @param response the Response to be parsed 8 | * @returns the response.json method that accepts a type or Zod schema for a typed json response 9 | */ 10 | const getJson: GetJson = 11 | (response) => 12 | async (schema?: StandardSchemaV1) => { 13 | const json = await response.json() 14 | if (!schema) return json as T 15 | const result = await schema['~standard'].validate(json) 16 | if (result.issues) { 17 | throw new ParseResponseError( 18 | 'Failed to parse response.json', 19 | result.issues 20 | ) 21 | } 22 | return result.value 23 | } 24 | 25 | /** 26 | * @param response the Response to be parsed 27 | * @returns the response.text method that accepts a type or Zod schema for a typed response 28 | */ 29 | const getText: GetText = 30 | (response) => 31 | async (schema?: StandardSchemaV1) => { 32 | const text = await response.text() 33 | if (!schema) return text as T 34 | const result = await schema['~standard'].validate(text) 35 | if (result.issues) { 36 | throw new ParseResponseError( 37 | 'Failed to parse response.text', 38 | result.issues 39 | ) 40 | } 41 | return result.value 42 | } 43 | 44 | export { getJson, getText } 45 | -------------------------------------------------------------------------------- /src/primitives.test.ts: -------------------------------------------------------------------------------- 1 | import * as subject from './primitives' 2 | 3 | beforeEach(() => { 4 | vi.clearAllMocks() 5 | }) 6 | 7 | describe('addQueryToURL', () => { 8 | it('should add the query object to a string input', () => { 9 | expect(subject.addQueryToURL('https://example.com/api', { id: '1' })).toBe( 10 | 'https://example.com/api?id=1' 11 | ) 12 | expect( 13 | subject.addQueryToURL('https://example.com/api', 'page=2&foo=bar') 14 | ).toBe('https://example.com/api?page=2&foo=bar') 15 | }) 16 | 17 | it('should add the query object to a URL input', () => { 18 | expect( 19 | subject 20 | .addQueryToURL(new URL('https://example.com/api'), { 21 | id: '1', 22 | }) 23 | .toString() 24 | ).toEqual(new URL('https://example.com/api?id=1').toString()) 25 | expect( 26 | subject 27 | .addQueryToURL(new URL('https://example.com/api'), 'page=2') 28 | .toString() 29 | ).toEqual(new URL('https://example.com/api?page=2').toString()) 30 | }) 31 | 32 | it('should append the query to a URL string that already has QS', () => { 33 | expect( 34 | subject.addQueryToURL('https://example.com/api?id=1', { page: '2' }) 35 | ).toBe('https://example.com/api?id=1&page=2') 36 | expect( 37 | subject.addQueryToURL('https://example.com/api?id=1', 'page=2') 38 | ).toBe('https://example.com/api?id=1&page=2') 39 | expect( 40 | subject.addQueryToURL( 41 | 'https://example.com/api?id=1', 42 | new URLSearchParams({ page: '2' }) 43 | ) 44 | ).toBe('https://example.com/api?id=1&page=2') 45 | }) 46 | 47 | it('should append the query to a URL instance that already has QS', () => { 48 | expect( 49 | subject 50 | .addQueryToURL(new URL('https://example.com/api?id=1'), { 51 | page: '2', 52 | }) 53 | .toString() 54 | ).toEqual(new URL('https://example.com/api?id=1&page=2').toString()) 55 | expect( 56 | subject 57 | .addQueryToURL(new URL('https://example.com/api?id=1'), 'page=2') 58 | .toString() 59 | ).toEqual(new URL('https://example.com/api?id=1&page=2').toString()) 60 | expect( 61 | subject 62 | .addQueryToURL( 63 | new URL('https://example.com/api?id=1'), 64 | new URLSearchParams({ page: '2' }) 65 | ) 66 | .toString() 67 | ).toEqual(new URL('https://example.com/api?id=1&page=2').toString()) 68 | }) 69 | 70 | it("should return the input in case there's no query", () => { 71 | expect(subject.addQueryToURL('https://example.com/api')).toBe( 72 | 'https://example.com/api' 73 | ) 74 | expect(subject.addQueryToURL(new URL('https://example.com/api'))).toEqual( 75 | new URL('https://example.com/api') 76 | ) 77 | }) 78 | }) 79 | 80 | describe('ensureStringBody', () => { 81 | it('should return the same if body was string', () => { 82 | expect(subject.ensureStringBody('foo')).toBe('foo') 83 | }) 84 | 85 | it('should return the same if body was not defined', () => { 86 | expect(subject.ensureStringBody()).toBeUndefined() 87 | }) 88 | 89 | it('should stringify the body if it is a JSON-like value', () => { 90 | expect(subject.ensureStringBody({ page: 2 })).toBe(`{"page":2}`) 91 | expect(subject.ensureStringBody([1, 2])).toBe('[1,2]') 92 | expect(subject.ensureStringBody(3)).toBe('3') 93 | expect(subject.ensureStringBody(true)).toBe('true') 94 | expect(subject.ensureStringBody({})).toBe('{}') 95 | }) 96 | 97 | it('should not stringify other valid kinds of BodyInit', () => { 98 | const ab = new ArrayBuffer(0) 99 | expect(subject.ensureStringBody(ab)).toBe(ab) 100 | const rs = new ReadableStream() 101 | expect(subject.ensureStringBody(rs)).toBe(rs) 102 | const fd = new FormData() 103 | expect(subject.ensureStringBody(fd)).toBe(fd) 104 | const usp = new URLSearchParams() 105 | expect(subject.ensureStringBody(usp)).toBe(usp) 106 | const blob = new Blob() 107 | expect(subject.ensureStringBody(blob)).toBe(blob) 108 | }) 109 | }) 110 | 111 | describe('makeGetApiURL', () => { 112 | it('should return a URL which is baseURL and path joined', () => { 113 | expect(subject.makeGetApiURL('https://example.com/api')('/users')).toBe( 114 | 'https://example.com/api/users' 115 | ) 116 | }) 117 | 118 | it('should accept an object-like queryString and return it joined to the URL', () => { 119 | const getApiURL = subject.makeGetApiURL('https://example.com/api') 120 | expect(getApiURL('/users', { id: '1' })).toBe( 121 | 'https://example.com/api/users?id=1' 122 | ) 123 | expect(getApiURL('/users', { active: 'true', page: '2' })).toBe( 124 | 'https://example.com/api/users?active=true&page=2' 125 | ) 126 | }) 127 | 128 | it('should accept a URL as baseURL and remove extra slashes', () => { 129 | expect( 130 | subject.makeGetApiURL(new URL('https://example.com/api'))('/users') 131 | ).toBe('https://example.com/api/users') 132 | expect( 133 | subject.makeGetApiURL(new URL('https://example.com/api/'))('/users') 134 | ).toBe('https://example.com/api/users') 135 | expect( 136 | subject.makeGetApiURL(new URL('https://example.com/api/'))('///users') 137 | ).toBe('https://example.com/api/users') 138 | }) 139 | 140 | it('should add missing slashes', () => { 141 | expect( 142 | subject.makeGetApiURL(new URL('https://example.com/api'))('users') 143 | ).toBe('https://example.com/api/users') 144 | }) 145 | }) 146 | 147 | describe('mergeHeaders', () => { 148 | it('should merge diferent kinds of Headers', () => { 149 | expect( 150 | subject.mergeHeaders(new Headers({ a: '1' }), { b: '2' }, [['c', '3']]) 151 | ).toEqual(new Headers({ a: '1', b: '2', c: '3' })) 152 | }) 153 | 154 | it('should merge diferent kinds of Headers and override values', () => { 155 | expect( 156 | subject.mergeHeaders(new Headers({ a: '1' }), { a: '2' }, [['a', '3']]) 157 | ).toEqual(new Headers({ a: '3' })) 158 | }) 159 | 160 | it('should merge diferent kinds of Headers and delete undefined values', () => { 161 | expect( 162 | subject.mergeHeaders(new Headers({ a: '1' }), { a: undefined }) 163 | ).toEqual(new Headers({})) 164 | expect( 165 | subject.mergeHeaders(new Headers({ a: '1' }), { a: 'undefined' }) 166 | ).toEqual(new Headers({})) 167 | expect( 168 | subject.mergeHeaders(new Headers({ a: '1' }), [['a', undefined]]) 169 | ).toEqual(new Headers({})) 170 | }) 171 | 172 | it('should be case insensitive such as Headers', () => { 173 | expect( 174 | subject.mergeHeaders(new Headers({ 'Content-Type': 'text/html' }), { 175 | 'content-type': 'application/json', 176 | }) 177 | ).toEqual(new Headers({ 'Content-Type': 'application/json' })) 178 | 179 | expect( 180 | subject.mergeHeaders(new Headers({ 'Content-Type': 'text/html' }), { 181 | 'content-type': undefined, 182 | }) 183 | ).toEqual(new Headers({})) 184 | }) 185 | }) 186 | 187 | describe('replaceURLParams', () => { 188 | it('should replace the wildcards in an URL string with the given parameters', () => { 189 | expect(subject.replaceURLParams('/users/:id', { id: '1' })).toBe('/users/1') 190 | expect( 191 | subject.replaceURLParams('http://example.com/users/:id/posts/:postId', { 192 | id: '1', 193 | postId: '3', 194 | }) 195 | ).toBe('http://example.com/users/1/posts/3') 196 | }) 197 | 198 | it('should replace the wildcards in an instance of URL', () => { 199 | expect( 200 | subject.replaceURLParams(new URL('/users/:id', 'http://example.com'), { 201 | id: '1', 202 | }) 203 | ).toEqual(new URL('http://example.com/users/1')) 204 | }) 205 | 206 | it('should accept numbers as parameters', () => { 207 | expect(subject.replaceURLParams('/users/:id', { id: 1 })).toBe('/users/1') 208 | }) 209 | }) 210 | 211 | describe('typeOf', () => { 212 | it('should a string version of the type of an unknown value', () => { 213 | expect(subject.typeOf([])).toBe('array') 214 | expect(subject.typeOf(new ArrayBuffer(0))).toBe('arraybuffer') 215 | expect(subject.typeOf(BigInt(1))).toBe('bigint') 216 | expect(subject.typeOf(new Blob())).toBe('blob') 217 | expect(subject.typeOf(false)).toBe('boolean') 218 | expect(subject.typeOf(new FormData())).toBe('formdata') 219 | expect(subject.typeOf(() => {})).toBe('function') 220 | expect(subject.typeOf(null)).toBe('null') 221 | expect(subject.typeOf(1)).toBe('number') 222 | expect(subject.typeOf({})).toBe('object') 223 | expect(subject.typeOf(new ReadableStream())).toBe('readablestream') 224 | expect(subject.typeOf('')).toBe('string') 225 | expect(subject.typeOf(Symbol('a'))).toBe('symbol') 226 | expect(subject.typeOf(undefined)).toBe('undefined') 227 | expect(subject.typeOf(new URL('http://localhost'))).toBe('url') 228 | expect(subject.typeOf(new URLSearchParams())).toBe('urlsearchparams') 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /src/primitives.ts: -------------------------------------------------------------------------------- 1 | import type { StandardSchemaV1 } from '@standard-schema/spec' 2 | import type { JSONValue, PathParams, SearchParams } from './types' 3 | 4 | /** 5 | * @param url a string or URL to which the query parameters will be added 6 | * @param searchParams the query parameters 7 | * @returns the url with the query parameters added with the same type as the url 8 | */ 9 | function addQueryToURL( 10 | url: T, 11 | searchParams?: SearchParams 12 | ): T { 13 | if (!searchParams) return url 14 | 15 | if (typeof url === 'string') { 16 | const separator = url.includes('?') ? '&' : '?' 17 | return `${url}${separator}${new URLSearchParams(searchParams)}` as T 18 | } 19 | if (searchParams && url instanceof URL) { 20 | for (const [key, value] of new URLSearchParams(searchParams).entries()) { 21 | url.searchParams.set(key, value) 22 | } 23 | } 24 | return url 25 | } 26 | 27 | /** 28 | * @param body the JSON-like body of the request 29 | * @returns the body is stringified if it is not a string and it is a JSON-like object. It also accepts other types of BodyInit such as Blob, ReadableStream, etc. 30 | */ 31 | function ensureStringBody( 32 | body?: B 33 | ): B extends JSONValue ? string : B { 34 | if (typeof body === 'undefined') return body as never 35 | if (typeof body === 'string') return body as never 36 | 37 | return ( 38 | ['number', 'boolean', 'array', 'object'].includes(typeOf(body)) 39 | ? JSON.stringify(body) 40 | : body 41 | ) as never 42 | } 43 | 44 | /** 45 | * @param baseURL the base path to the API 46 | * @returns a function that receives a path and an object of query parameters and returns a URL 47 | */ 48 | function makeGetApiURL(baseURL: T) { 49 | const base = baseURL instanceof URL ? baseURL.toString() : baseURL 50 | return (path: string, searchParams?: SearchParams): T => { 51 | const url = `${base}/${path}`.replace(/([^https?:]\/)\/+/g, '$1') 52 | return addQueryToURL(url, searchParams) as T 53 | } 54 | } 55 | 56 | /** 57 | * It merges multiple HeadersInit objects into a single Headers object 58 | * @param entries Any number of HeadersInit objects 59 | * @returns a new Headers object with the merged headers 60 | */ 61 | function mergeHeaders( 62 | ...entries: ( 63 | | HeadersInit 64 | | [string, undefined][] 65 | | Record 66 | )[] 67 | ) { 68 | const result = new Headers() 69 | 70 | for (const entry of entries) { 71 | const headers = new Headers(entry as HeadersInit) 72 | 73 | for (const [key, value] of headers.entries()) { 74 | if (value === undefined || value === 'undefined') { 75 | result.delete(key) 76 | } else { 77 | result.set(key, value) 78 | } 79 | } 80 | } 81 | 82 | return result 83 | } 84 | 85 | /** 86 | * 87 | * @param url the url string or URL object to replace the params 88 | * @param params the params map to be replaced in the url 89 | * @returns the url with the params replaced and with the same type as the given url 90 | */ 91 | function replaceURLParams( 92 | url: T, 93 | params: PathParams 94 | ): T { 95 | // TODO: use the URL Pattern API as soon as it has better browser support 96 | if (!params) return url as T 97 | 98 | let urlString = String(url) 99 | for (const [key, value] of Object.entries(params)) { 100 | urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`) 101 | } 102 | return (url instanceof URL ? new URL(urlString) : urlString) as T 103 | } 104 | 105 | /** 106 | * This is an enhanced version of the typeof operator to check the type of more complex values. 107 | * @param t the value to be checked 108 | * @returns the type of the value 109 | */ 110 | function typeOf(t: unknown) { 111 | return Object.prototype.toString 112 | .call(t) 113 | .replace(/^\[object (.+)\]$/, '$1') 114 | .toLowerCase() as 115 | | 'array' 116 | | 'arraybuffer' 117 | | 'bigint' 118 | | 'blob' 119 | | 'boolean' 120 | | 'formdata' 121 | | 'function' 122 | | 'null' 123 | | 'number' 124 | | 'object' 125 | | 'readablestream' 126 | | 'string' 127 | | 'symbol' 128 | | 'undefined' 129 | | 'url' 130 | | 'urlsearchparams' 131 | } 132 | 133 | /** 134 | * Error thrown when the response cannot be parsed. 135 | */ 136 | class ParseResponseError extends Error { 137 | constructor( 138 | message: string, 139 | public issues: readonly StandardSchemaV1.Issue[] 140 | ) { 141 | super(JSON.stringify({ message, issues }, null, 2)) 142 | this.name = 'ParseResponseError' 143 | this.issues = issues 144 | } 145 | } 146 | 147 | export { 148 | addQueryToURL, 149 | ensureStringBody, 150 | makeGetApiURL, 151 | mergeHeaders, 152 | replaceURLParams, 153 | typeOf, 154 | } 155 | export { ParseResponseError } 156 | -------------------------------------------------------------------------------- /src/test.d.ts: -------------------------------------------------------------------------------- 1 | type Expect = T 2 | type Equal = 3 | // prettier-ignore 4 | (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 5 | ? true 6 | : false 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { StandardSchemaV1 } from '@standard-schema/spec' 2 | import type { HTTP_METHODS } from './constants' 3 | 4 | type JSONValue = 5 | | string 6 | | number 7 | | boolean 8 | | Date // Will be turned into a string 9 | | { [x: string]: JSONValue | undefined | null } 10 | | Array 11 | 12 | type SearchParams = ConstructorParameters[0] 13 | 14 | type TypedResponse = Omit & { 15 | json: TypedResponseJson 16 | text: TypedResponseText 17 | } 18 | 19 | type PathParams = T extends string 20 | ? ExtractPathParams extends Record 21 | ? ExtractPathParams 22 | : Record 23 | : Record 24 | 25 | type EnhancedRequestInit = Omit & { 26 | method?: HTTPMethod | Lowercase 27 | body?: JSONValue | BodyInit | null 28 | query?: SearchParams 29 | params?: PathParams 30 | trace?: ( 31 | fullUrl: string | URL, 32 | init: EnhancedRequestInit, 33 | response: TypedResponse 34 | ) => void | Promise 35 | } 36 | 37 | type ServiceRequestInit = Omit, 'method'> 38 | 39 | type RequestTransformer = ( 40 | request: EnhancedRequestInit 41 | ) => EnhancedRequestInit | Promise 42 | 43 | type ResponseTransformer = ( 44 | response: TypedResponse 45 | ) => TypedResponse | Promise 46 | 47 | type BaseOptions = { 48 | headers?: HeadersInit | (() => HeadersInit | Promise) 49 | requestTransformer?: RequestTransformer 50 | responseTransformer?: ResponseTransformer 51 | } 52 | 53 | type HTTPMethod = (typeof HTTP_METHODS)[number] 54 | 55 | type TypedResponseJson = ( 56 | schema?: StandardSchemaV1 57 | ) => Promise 58 | 59 | type TypedResponseText = ( 60 | schema?: StandardSchemaV1 61 | ) => Promise 62 | 63 | type GetJson = (response: Response) => TypedResponseJson 64 | type GetText = (response: Response) => TypedResponseText 65 | 66 | type Prettify = { 67 | [K in keyof T]: T[K] 68 | } & {} 69 | 70 | type ExtractPathParams = 71 | T extends `${infer _}:${infer Param}/${infer Rest}` 72 | ? Prettify< 73 | Omit<{ [K in Param]: string | number } & ExtractPathParams, ''> 74 | > 75 | : T extends `${infer _}:${infer Param}` 76 | ? { [K in Param]: string | number } 77 | : // biome-ignore lint/complexity/noBannedTypes: 78 | {} 79 | 80 | export type { 81 | EnhancedRequestInit, 82 | GetJson, 83 | GetText, 84 | HTTPMethod, 85 | JSONValue, 86 | PathParams, 87 | SearchParams, 88 | ServiceRequestInit, 89 | BaseOptions, 90 | TypedResponse, 91 | TypedResponseJson, 92 | TypedResponseText, 93 | RequestTransformer, 94 | ResponseTransformer, 95 | } 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "src/**/*.tsx"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "module": "commonjs", 8 | "outDir": "./tsc/", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "target": "ES2020", 12 | "types": ["vitest/globals", "node"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig(() => ({ 5 | test: { 6 | environment: 'jsdom', 7 | globals: true, 8 | maxConcurrency: 1, 9 | minThreads: 0, 10 | maxThreads: 1, 11 | exclude: ['tsc', 'node_modules'], 12 | }, 13 | })) 14 | --------------------------------------------------------------------------------