├── .github └── workflows │ ├── dispatch-test-pr.yml │ ├── publish.yml │ └── test-pr.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── DrFetch.ts ├── StatusCodes.ts ├── headers.ts ├── index.ts └── types.ts ├── tests ├── DrFetch.test.ts ├── headers.test.ts └── index.test.ts └── tsconfig.json /.github/workflows/dispatch-test-pr.yml: -------------------------------------------------------------------------------- 1 | name: On-Demand Unit Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | node-version: 7 | description: Node.js version to use. 8 | required: true 9 | type: choice 10 | options: 11 | - '18' 12 | - '20' 13 | - '22' 14 | - '24' 15 | default: '24' 16 | 17 | jobs: 18 | test: 19 | name: On-Demand Unit Testing on Node.js v${{ inputs.node-version }} 20 | uses: WJSoftware/cicd/.github/workflows/npm-test.yml@v0.4 21 | secrets: inherit 22 | with: 23 | pwsh: false 24 | build: false 25 | node-version: ${{ inputs.node-version }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | node-version: 7 | description: Node.js version to use. 8 | required: false 9 | type: choice 10 | options: 11 | - '18' 12 | - '20' 13 | - '22' 14 | - '24' 15 | default: '24' 16 | dry-run: 17 | description: Performs a dry run of the publish. 18 | required: false 19 | type: boolean 20 | default: false 21 | 22 | jobs: 23 | publish: 24 | name: Publish NPM Package 25 | uses: WJSoftware/cicd/.github/workflows/npm-publish.yml@v0.4 26 | secrets: inherit 27 | with: 28 | node-version: ${{ inputs.node-version }} 29 | npm-tag: latest 30 | dry-run: ${{ inputs.dry-run }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test-pr.yml: -------------------------------------------------------------------------------- 1 | name: Test Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | test: 10 | name: Unit Testing 11 | uses: WJSoftware/cicd/.github/workflows/npm-test.yml@v0.4 12 | secrets: inherit 13 | with: 14 | pwsh: false 15 | build: false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 WJSoftware 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 | # dr-fetch 2 | 3 | This is not just one more wrapper for `fetch()`: This package promotes the idea of using customized data-fetching 4 | functions, which is the most maintainable option, and adds features no other wrapper provides to date. 5 | 6 | This package: 7 | 8 | + Uses the modern, standardized `fetch` function. 9 | + Does **not** throw on non-OK HTTP responses. 10 | + **Can fully type all possible HTTP responses depending on the HTTP status code, even non-standard ones like 499.** 11 | + **Supports abortable HTTP requests; no boilerplate.** 12 | + **Can auto-abort HTTP requests in favor of newer request versions, with optional delaying (debouncing).** 13 | + Works in any runtime that implements `fetch()` (browsers, NodeJS, etc.). 14 | + Is probably the tiniest fetch wrapper you'll ever need: **421 LOC** including typing (`npx cloc .\src --exclude-dir=tests`). 15 | 16 | ## Does a Non-OK Status Code Warrant an Error? 17 | 18 | The short story is: 19 | 20 | 1. Wrappers like `axios` or `ky` do `if (!response.ok) throw ...`, which forces code to `try..catch`. This is a code 21 | smell: `try..catch` is being used as a branching mechanism. 22 | 2. The performance drop is huge. [See this benchmark](https://jsperf.app/dogeco). Over 40% loss. 23 | 24 | [The issue of fetch wrappers explained in more detail](https://webjose.hashnode.dev/the-ugly-truth-all-popular-fetch-wrappers-do-it-wrong) 25 | 26 | ## Quickstart 27 | 28 | 1. Install the package. 29 | 2. Create your custom fetch function, usually including logic to inject an authorization header/token. 30 | 3. Create a fetcher object. 31 | 4. Optionally add body processors. 32 | 5. Use the fetcher for every HTTP request needed. 33 | 34 | ### Installation 35 | 36 | ```bash 37 | npm i dr-fetch 38 | ``` 39 | 40 | ### Create a Custom Fetch Function 41 | 42 | This is optional and only needed if you need to do something before or after fetching. By far the most common task to 43 | do is to add the `authorization` header and the `accept` header to every call. 44 | 45 | ```typescript 46 | // myFetch.ts 47 | import { obtainToken } from "./magical-auth-stuff.js"; 48 | import { setHeaders, type FetchFnUrl, type FetchFnInit } from "dr-fetch"; 49 | 50 | export function myFetch(url: FetchFnUrl, init?: FetchFnInit) { 51 | const token = obtainToken(); 52 | // Make sure there's an object where headers can be added: 53 | init ??= {}; 54 | setHeaders(init, { Accept: 'application/json', Authorization: `Bearer ${token}`}); 55 | return fetch(url, init); 56 | } 57 | ``` 58 | 59 | Think of this custom function as the place where you do interceptions (if you are familiar with this term from `axios`). 60 | 61 | ### Create Fetcher Object 62 | 63 | ```typescript 64 | // fetcher.ts 65 | import { DrFetch } from "dr-fetch"; 66 | import { myFetch } from "./myFetch.js"; 67 | 68 | export default new DrFetch(myFetch); 69 | // If you don't need a custom fetch function, just do: 70 | export default new DrFetch(); 71 | ``` 72 | 73 | ### Adding a Custom Body Processor 74 | 75 | This step is also optional. 76 | 77 | One can say that the `DrFetch` class comes with 2 basic body processors: 78 | 79 | 1. JSON processor when the value of the `content-type` response header is `application/json` or similar 80 | (`application/problem+json`, for instance). 81 | 2. Text processor when the value of the `content-type` response header is `text/`, such as `text/plain` or 82 | `text/csv`. 83 | 84 | If your API sends a content type not covered by any of the above two cases, use `DrFetch.withProcessor()` to add a 85 | custom processor for the content type you are expecting. The class allows for fluent syntax, so you can chain calls: 86 | 87 | ```typescript 88 | // fetcher.ts 89 | ... 90 | 91 | export default new DrFetch(myFetch) 92 | .withProcessor('desired/contentType', async (response, stockParsers) => { 93 | // Do what you must with the provided response object. Whatever you return is carried in the `body` 94 | // property of the final DrFetch.fetch()'s response object. 95 | return finalBody; 96 | }); 97 | ; 98 | ``` 99 | 100 | > [!NOTE] 101 | > The matching pattern can be a string, a regular expression, or an array of either. If this is not sufficient, pass 102 | > a predicate function with signature `(response: Response, contentType: string) => boolean`. 103 | 104 | Now the fetcher object is ready for use. 105 | 106 | ### Using the Fetcher Object 107 | 108 | This is the fun part where we can enumerate the various shapes of the body depending on the HTTP status code: 109 | 110 | ```typescript 111 | import type { MyData } from "./my-types.js"; 112 | import fetcher from "./fetcher.js"; 113 | 114 | const response = await fetcher 115 | .for<200, MyData[]>() 116 | .for<401, { loginUrl: string; }>() 117 | .fetch('/api/mydata/?active=true') 118 | ; 119 | ``` 120 | 121 | The object stored in the `response` variable will contain the following properties: 122 | 123 | + `aborted`: Will be `false` (since **v0.8.0**). 124 | + `ok`: Same as `Response.ok`. 125 | + `status`: Same as `Response.status`. 126 | + `statusText`: Same as `Response.statusText`. 127 | + `headers`: Same as `Response.headers` (since **v0.11.0**). 128 | + `body`: The HTTP response body, already parsed and typed according to the specification: `MyData[]` if the status 129 | code was `200`, or `{ loginUrl: string; }` if the status code was `401`. 130 | 131 | Your editor's Intellisense should be able to properly and accurately tell you all this: 132 | 133 | ```typescript 134 | import { StatusCodes } from "dr-fetch"; // Enum available since v0.11.0. 135 | 136 | // In this example, doing response.ok in the IF narrows the type just as well. 137 | if (response.status === StatusCodes.Ok) { 138 | // Say, display the data somehow/somewhere. In Svelte, we would set a store, perhaps? 139 | myDataStore.set(response.body); 140 | } 141 | else { 142 | // TypeScript/Intellisense will tell you that the only other option is for the status code to be 401: 143 | window.location.href = response.body.loginUrl; 144 | } 145 | ``` 146 | 147 | ## The StatusCodes Enumeration 148 | 149 | > Since v0.11.0 150 | 151 | All standardized HTTP status codes documented at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status) 152 | for the 2xx, 4xx and 5xx ranges have been collected into the `StatusCodes` enumeration. Feel free to import it and use 153 | it to make your code far more readable and free of magic numbers. 154 | 155 | ```typescript 156 | import { StatusCodes } from "dr-fetch"; 157 | 158 | ... 159 | if (response.status === StatusCodes.Created) { 160 | // Get resource URL from headers. 161 | } 162 | else if (response.status === StatusCodes.BadRequest) { 163 | ... 164 | } 165 | ... 166 | ``` 167 | 168 | ## Typing For Non-Standard Status Codes 169 | 170 | > Since **v0.8.0** 171 | 172 | This library currently supports, out of the box, the OK status codes, client error status codes and server error status 173 | codes that the MDN website lists, and are therefore considered standardized. 174 | 175 | If you need to type a response based on any other status code not currently supported, just do something like this: 176 | 177 | ```typescript 178 | import { DrFetch, type StatusCode } from "dr-fetch"; 179 | 180 | type MyStatusCode = StatusCode | 499; 181 | export default new DrFetch(); 182 | ``` 183 | 184 | You will now be able to use non-standardized status code `499` to type the response body with `DrFetch.for<>()`. 185 | 186 | ## Abortable HTTP Requests 187 | 188 | > Since **v0.8.0** 189 | 190 | To create abortable HTTP requests, as per the standard, use an `AbortController`. The following is how you would have 191 | to write your code *without* `dr-fetch`: 192 | 193 | ```typescript 194 | const ac = new AbortController(); 195 | let aborted = false; 196 | let response: Response; 197 | 198 | try { 199 | response = await fetch('/url', { signal: ac.signal }); 200 | } 201 | catch (err) { 202 | if (err instanceof DOMException && err.name === 'AbortError') { 203 | aborted = true; 204 | } 205 | // Other stuff for non-aborted scenarios. 206 | } 207 | if (!aborted) { 208 | const body = await response.json(); 209 | ... 210 | } 211 | ``` 212 | 213 | In contrast, using an abortable fetcher from `dr-fetch`, you reduce your code to: 214 | 215 | ```typescript 216 | // abortable-fetcher.ts 217 | import { DrFetch } from "dr-fetch"; 218 | 219 | export const abortableFetcher = new DrFetch() 220 | .abortable(); 221 | ``` 222 | 223 | ```typescript 224 | // some-component.ts 225 | import { abortableFetcher } from "./abortable-fetcher.js"; 226 | 227 | const ac = new AbortController(); 228 | 229 | const response = await abortableFetcher 230 | .for<200, MyData[]>(), 231 | .for<400, ValidationError[]>() 232 | .get('/url', { signal: ac.signal }); 233 | if (!response.aborted) { 234 | ... 235 | } 236 | ``` 237 | 238 | In short: All boilerplate is gone. Your only job is to create the abort controller, pass the signal and after 239 | awaiting for the response, you check the value of the `aborted` property. 240 | 241 | TypeScript and Intellisense will be fully accurate: If `response.aborted` is true, then the `response.error` property 242 | is available; otherwise the usual `ok`, `status`, `statusText` and `body` properties will be the ones available. 243 | 244 | For full details and feedback on this feature, see [this discussion](https://github.com/WJSoftware/dr-fetch/discussions/25). 245 | 246 | > [!IMPORTANT] 247 | > Calling `DrFetch.abortable()` permanently changes the fetcher object's configuration. 248 | 249 | ## Smarter Uses 250 | 251 | It is smart to create just one fetcher, configure it, then use it for every fetch call. Because generally speaking, 252 | different URL's will carry a different body type, the fetcher object should be kept free of `for<>()` calls. But what 253 | if your API is standardized so all status `400` bodies look the same? Then configure that type: 254 | 255 | ```typescript 256 | // root-fetcher.ts 257 | import { DrFetch } from "dr-fetch"; 258 | import { myFetch } from "./my-fetch.js"; 259 | import type { BadRequestBody } from "my-types.js"; 260 | 261 | export default new DrFetch(myFetch) 262 | .withProcessor(...) // Optional processors 263 | .withProcessor(...) 264 | .for<400, BadRequestBody>() 265 | ; 266 | ``` 267 | 268 | You can now consume this root fetcher object and it will be pre-typed for the `400` status code. 269 | 270 | ### About Abortable Fetchers 271 | 272 | > Since **v0.8.0** 273 | 274 | If your project has a need for abortable and non-abortable fetcher objects, the smarter option would be to create and 275 | export 2 fetcher objects, instead of one root fetcher: 276 | 277 | ```typescript 278 | // root-fetchers.ts 279 | import { DrFetch } from "dr-fetch"; 280 | import { myFetch } from "./my-fetch.js"; 281 | import type { BadRequestBody } from "my-types.js"; 282 | 283 | export const rootFetcher new DrFetch(myFetch) 284 | .withProcessor(...) // Optional processors 285 | .withProcessor(...) 286 | .for<400, BadRequestBody>() 287 | ; 288 | 289 | export const abortableRootFetcher = rootFetcher.clone().abortable(); 290 | ``` 291 | 292 | We clone it because `abortable()` has permanent side effects on the object's state. Cloning can also help with other 293 | scenarios, as explained next. 294 | 295 | ### Specializing the Root Fetcher 296 | 297 | What if we needed a custom processor for just one particular URL? It makes no sense to add it to the root fetcher, and 298 | maybe it is even harmful to do so. In cases like this one, clone the fetcher. 299 | 300 | Cloning a fetcher produces a new fetcher with the same data-fetching function, the same body processors, the same 301 | support for abortable HTTP requests and the same body typings, **unless** we specify we want something different, like 302 | not cloning the body types, or specifying a new data-fetching function. 303 | 304 | ```typescript 305 | import rootFetcher from "./root-fetcher.js"; 306 | import type { FetchFnUrl, FetchFnInit } from "dr-fetch"; 307 | 308 | function specialFetch(url: FetchFnUrl, init?: FetchFnInit) { 309 | ... 310 | } 311 | 312 | // Same data-fetching function, body processors, abortable support and body typing. 313 | const specialFetcher = rootFetcher.clone(); 314 | // Same data-fetching function, abortable support and body processors; no body typing. 315 | const specialFetcher = rootFetcher.clone({ preserveTyping: false }); 316 | // Same everything; different data-fetching function. 317 | const specialFetcher = rootFetcher.clone({ fetchFn: specialFetch }); 318 | // Same everything; no custom body processors. 319 | const specialFetcher = rootFetcher.clone({ includeProcessors: false }); 320 | // Identical processors, abortable support and body typing; stock fetch(). 321 | const specialFetcher = rootFetcher.clone({ fetchFn: false }); 322 | // Identical processors, body typing and fetch function; no abortable support (the default when constructing). 323 | const specialFetcher = rootFetcher.clone({ preserveAbortable: false }); 324 | ``` 325 | 326 | > [!IMPORTANT] 327 | > `preserveTyping` is a TypeScript trick and cannot be a variable of type `boolean`. Its value doesn't matter in 328 | > runtime because types are not a runtime thing, and TypeScript depends on knowing if the value is `true` or `false`. 329 | > 330 | > On the other hand, `preserveAbortable` (since **v0.9.0**) is a hybrid: It uses the same TypeScript trick, but its 331 | > value does matter in runtime because an abortable fetcher object has different inner state than a stock fetcher 332 | > object. In this sense, supporting a variable would be ideal, but there's just no way to properly reconcile the 333 | > TypeScript side with a variable of type `boolean`. Therefore, try to always use constant values. 334 | 335 | ## Auto-Abortable HTTP Requests 336 | 337 | > Since **v0.9.0** 338 | 339 | An HTTP request can automatically abort whenever a new version of the HTTP request is executed. This is useful in 340 | cases like server-sided autocomplete components, where an HTTP request is made every time a user stops typing in the 341 | search textbox. As soon as a new HTTP request is made, the previous has no value. With `dr-fetch`, this chore is 342 | fully automated. 343 | 344 | To illustrate, this is how it would be done "by hand", as if auto-abortable wasn't a feature: 345 | 346 | ```typescript 347 | import { abortableRootFetcher } from './root-fetchers.js'; 348 | import type { SimpleItem } from './my-types.js'; 349 | 350 | let ac: AbortController; 351 | 352 | async function fetchAutocompleteList(searchTerm: string) { 353 | ac?.abort(); 354 | ac = new AbortController(); 355 | const response = await abortableRootFetcher 356 | .for<200, SimpleItem[]>() 357 | .get(`/my/data?s=${searchTerm}`, { signal: ac.signal }); 358 | if (!response.aborted) { 359 | ... 360 | } 361 | } 362 | ``` 363 | 364 | While this is not too bad, it can actually be like this: 365 | 366 | ```typescript 367 | import { abortableRootFetcher } from './root-fetchers.js'; 368 | import type { SimpleItem } from './my-types.js'; 369 | 370 | async function fetchAutocompleteList(searchTerm: string) { 371 | const response = await abortableRootFetcher 372 | .for<200, SimpleItem[]>() 373 | .get(`/my/data?s=${searchTerm}`, { autoAbort: 'my-key' }); 374 | if (!response.aborted) { 375 | ... 376 | } 377 | } 378 | ``` 379 | 380 | > [!NOTE] 381 | > The key can be a string, a number or a unique symbol. Keys are not shared between fetcher instances, and cloning 382 | > does not clone any existing keys. 383 | 384 | `DrFetch` instances create and keep track of abort controllers by key. All one must do is provide a key when starting 385 | the HTTP request. Furthermore, the abort controllers are disposed as soon as the HTTP request resolves or rejects. 386 | 387 | ### Delaying an Auto-Abortable HTTP Request 388 | 389 | Aborting the HTTP request (the call to `fetch()`) is usually not the only thing that front-end developers do in cases 390 | like the autocomplete component. Developers usually also debounce the action of executing the HTTP request for a short 391 | period of time (around 500 milliseconds). 392 | 393 | You can do this very easily as well with `dr-fetch`. There is no need to program the debouncing externally. 394 | 395 | This is the previous example, with a delay specified: 396 | 397 | ```typescript 398 | import { abortableRootFetcher } from './root-fetchers.js'; 399 | import type { SimpleItem } from './my-types.js'; 400 | 401 | async function fetchAutocompleteList(searchTerm: string) { 402 | const response = await abortableRootFetcher 403 | .for<200, SimpleItem[]>() 404 | .get(`/my/data?s=${searchTerm}`, { autoAbort: { key: 'my-key', delay: 500 }}); 405 | if (!response.aborted) { 406 | ... 407 | } 408 | } 409 | ``` 410 | 411 | By using the object form of `autoAbort`, one can specify the desired delay, in milliseconds. 412 | 413 | ## Shortcut Functions 414 | 415 | > Since **v0.3.0** 416 | 417 | `DrFetch` objects now provide the shortcut functions `get`, `head`, `post`, `patch`, `put` and `delete`. Except for 418 | `get` and `head`, all these accept a body parameter. When this body is a POJO or an array, the body is stringified 419 | and, if no explicit `Content-Type` header is set, the `Content-Type` header is given the value `application/json`. If 420 | a body of any other type is given (that the `fetch()` function accepts, such as `FormData`), no headers are explicitly 421 | added and therefore it is up to what `fetch()` (or the custom data-fetching function you provide) does in these cases. 422 | 423 | ```typescript 424 | import type { Todo } from "./myTypes.js"; 425 | 426 | const newTodo = { text: 'I am new. Insert me!' }; 427 | const response = await fetcher 428 | .for<200, { success: true; entity: Todo; }>() 429 | .for<400, { errors: string[]; }>() 430 | .post('/api/todos', newTodo); 431 | 432 | const newTodos = [{ text: 'I am new. Insert me!' }, { text: 'Me too!' }]; 433 | const response = await fetcher 434 | .for<200, { success: true; entities: Todo[]; }>() 435 | .for<400, { errors: string[]; }>() 436 | .post('/api/todos', newTodos); 437 | ``` 438 | 439 | As stated, your custom fetch can be used to further customize the request because these shortcut functions will, in the 440 | end, call it. 441 | 442 | ### Parameters 443 | 444 | > Since **v0.8.0** 445 | 446 | The `get` and `head` shortcut functions' parameters are: 447 | 448 | `(url: URL | string, init?: RequestInit)` 449 | 450 | The other shortcut functions' parameters are: 451 | 452 | `(url: URL | string, body?: BodyInit | null | Record, init?: RequestInit)` 453 | 454 | Just note that `init` won't accept the `method` or `body` properties (the above is a simplification). 455 | 456 | ## setHeaders and makeIterableHeaders 457 | 458 | > Since **v0.4.0** 459 | 460 | These are two helper functions that assist you in writing custom data-fetching functions. 461 | 462 | If you haven't realized, the `init` parameter in `fetch()` can have the headers specified in 3 different formats: 463 | 464 | + As a `Headers` object (an instance of the `Headers` class) 465 | + As a POJO object, where the property key is the header name, and the property value is the header value 466 | + As an array of tuples of type `[string, string]`, where the first element is the header name, and the second one is 467 | its value 468 | 469 | To further complicate this, the POJO object also accepts an array of strings as property values for headers that accept 470 | multiple values. 471 | 472 | So writing a formal custom fetch **without** `setHeaders()` looks like this: 473 | 474 | ```typescript 475 | import type { FetchFnUrl, FetchFnInit } from "dr-fetch"; 476 | 477 | export function myFetch(URL: FetchFnUrl, init?: FetchFnInit) { 478 | const acceptHdrKey = 'Accept'; 479 | const acceptHdrValue = 'application/json'; 480 | init ??= {}; 481 | init.headers ??= new Headers(); 482 | if (Array.isArray(init.headers)) { 483 | // Tuples, so push a tuple per desired header: 484 | init.headers.push([acceptHdrKey, acceptHdrValue]); 485 | } 486 | else if (init.headers instanceof Headers) { 487 | init.headers.set(acceptHdrKey, acceptHdrValue); 488 | } 489 | else { 490 | // POJO object, so add headers as properties of an object: 491 | init.headers[acceptHdrKey] = acceptHdrValue; 492 | } 493 | return fetch(url, init); 494 | } 495 | ``` 496 | 497 | This would also get more complex if you account for multi-value headers. The bottom line is: This is complex. 498 | 499 | Now the same thing, using `setHeaders()`: 500 | 501 | ```typescript 502 | import type { FetchFnUrl, FetchFnInit } from "dr-fetch"; 503 | 504 | export function myFetch(URL: FetchFnUrl, init?: FetchFnInit) { 505 | init ??= {}; 506 | setHeaders(init, [['Accept', 'application/json']]); 507 | // OR: 508 | setHeaders(init, new Map([['Accept', ['application/json', 'application/xml']]])); 509 | // OR: 510 | setHeaders(init, { 'Accept': ['application/json', 'application/xml'] }); 511 | // OR: 512 | setHeaders(init, new Headers([['Accept', 'application/json']])); 513 | return fetch(url, init); 514 | } 515 | ``` 516 | > [!NOTE] 517 | > With `setHeaders()`, you can add headers to 'init' with a map, an array of tuples, a `Headers` instance or a POJO 518 | > object. 519 | 520 | The difference is indeed pretty shocking: One line of code and you are done. Also note that adding arrays of values 521 | doesn't increase the complexity of the code: It's still one line. 522 | 523 | ### makeIterableHeaders 524 | 525 | This function is the magic trick that powers the `setHeaders` function, and is very handy for troubleshooting or unit 526 | testing because it can take a collection of HTTP header specifications in the form of a map, a `Headers` object, a POJO 527 | object or an array of tuples and return an iterator object that iterates through the definitions in the same way: A 528 | list of tuples. 529 | 530 | ```typescript 531 | const myHeaders1 = new Headers(); 532 | myHeaders1.set('Accept', 'application/json'); 533 | myHeaders1.set('Authorization', 'Bearer x'); 534 | 535 | const myHeaders2 = new Map(); 536 | myHeaders2.set('Accept', 'application/json'); 537 | myHeaders2.set('Authorization', 'Bearer x'); 538 | 539 | const myHeaders3 = { 540 | 'Accept': 'application/json', 541 | 'Authorization': 'Bearer x' 542 | }; 543 | 544 | const myHeaders4 = [ 545 | ['Accept', 'application/json'], 546 | ['Authorization', 'Bearer x'], 547 | ]; 548 | 549 | // The output of all these is identical. 550 | console.log([...makeIterableHeaders(myHeaders1)]); 551 | console.log([...makeIterableHeaders(myHeaders2)]); 552 | console.log([...makeIterableHeaders(myHeaders3)]); 553 | console.log([...makeIterableHeaders(myHeaders4)]); 554 | ``` 555 | 556 | This function is a **generator function**, so what returns is an iterator object. The two most helpful ways of using 557 | it are in `for..of` statements and spreading: 558 | 559 | ```typescript 560 | for (let [key, value] of makeIterableHeaders(myHeaders)) { ... } 561 | 562 | // In unit-testing, perhaps: 563 | expect([...makeIterableHeaders(myHeaders)].length).to.equal(2); 564 | ``` 565 | 566 | ## hasHeader and getHeader 567 | 568 | > Since **v0.8.0** 569 | 570 | These are two helper functions that do exactly what the names imply: `hasHeader` checks for the existence of a 571 | particular HTTP header; `getHeader` obtains the value of a particular HTTP header. 572 | 573 | These functions perform a sequential search with the help of `makeIterableHeaders`. 574 | 575 | > [!NOTE] 576 | > Try not to use `getHeader` to determine the existence of a header **without** having the following in mind: The 577 | > function returns `undefined` if the value is not found, but it could return `undefined` if the header is found *and* 578 | > its value is `undefined`. 579 | 580 | ## Usage Without TypeScript (JavaScript Projects) 581 | 582 | Why are you a weird fellow/gal? Anyway, prejudice aside, body typing will mean nothing to you, so forget about `for()` 583 | and anything else regarding types. Do your custom data-fetching function, add your custom body processors and fetch 584 | away using `.fetch()`, `.get()`, `head()`, `.post()`, `.put()`, `.patch()` or `.delete()`. 585 | 586 | ## Plug-ins? Fancy Stuff? 587 | 588 | Indeed, we can have fancy stuff. As demonstration, this section will show you how one can add download progress with 589 | a simple class, the `fetch-api-progress` NPM package and a custom body processor. 590 | 591 | [Live demo in the Svelte REPL](https://svelte.dev/playground/ddeedfb44ab74727ac40df320c552b92) 592 | 593 | > [!NOTE] 594 | > If you wanted to, `fetch-api-progress` also supports upload progress. This is achieved by calling 595 | > `trackRequestProgress` to create a specialized `RequestInit` object. See [the next subsection](#custom-fetch-options) 596 | > for details. 597 | 598 | ```ts 599 | import { trackResponseProgress } from "fetch-api-progress"; 600 | 601 | export class DownloadProgress { 602 | progress = $state(0); 603 | response; 604 | 605 | constructor(response: Response) { 606 | this.response = response; 607 | trackResponseProgress(response, (p) => { 608 | this.progress = p.lengthComputable ? p.loaded / p.total : 0; 609 | }); 610 | } 611 | } 612 | ``` 613 | 614 | The above class is a simple Svelte v5 class that exposes a reactive `progress` property. Feel free to create 615 | equivalent classes/wrappers for your favorite frameworks. 616 | 617 | The `response` property can be used to access the original response object, to, for instance, get the actual data. 618 | 619 | ### How To Use 620 | 621 | Create a custom processor for the content type that will be received, for example, `video/mp4` for MP4 video files. 622 | 623 | ```ts 624 | // downloader.ts 625 | import { DownloadProgress } from "./DownloadProgress.svelte.js"; 626 | 627 | export default new DrFetch(/* custom fetch function here, if needed */) 628 | .withProcessor('video/mp4', (r) => Promise.resolve(new DownloadProgress(r))) 629 | ; 630 | ``` 631 | 632 | The Svelte component would use this fetcher object. The response from `fetcher.fetch()` (or `fetcher.get()`) will 633 | carry the class instance in the `body` property. 634 | 635 | ```svelte 636 | 650 | 651 | 654 | 655 | ``` 656 | 657 | When the button is clicked, the download is started. The custom processor simply creates the new instance of the 658 | `DownloadProgress` class. Svelte's reactivity system takes care of the rest, effectively bringing the progress element 659 | to life as the download progresses. 660 | 661 | ### Custom Fetch Options 662 | 663 | > Since **v0.10.0** 664 | 665 | If your custom data-fetching function (your custom `fetch()`) has abilities that require extra custom options from the 666 | caller, you're in luck: You can, and TypeScript and Intellisense will fully have your back. 667 | 668 | To exemplify, let's do **upload progress** with the `fetch-api-progress` NPM package. 669 | 670 | As first and last step, create your custom data-fetching function and give it to `DrFetch`: 671 | 672 | ```typescript 673 | // uploader.ts 674 | import { DrFetch, type FetchFnUrl, type FetchFnInit } from "dr-fetch"; 675 | import { trackRequestProgress, type FetchProgressEvent } from "fetch-api-progress"; 676 | 677 | export type UploaderInit = FetchFnInit & { 678 | onProgress?: (progress: FetchProgressEvent) => void; 679 | } 680 | 681 | function uploadingFetch(url: FetchFnUrl, init?: UploaderInit) { 682 | const trackedRequest = trackRequestProgress(init, init.onProgress); 683 | return fetch(url, trackedRequest); 684 | } 685 | 686 | export default new DrFetch(uploadingFetch); // This will be fully typed to support onProgress. 687 | ``` 688 | 689 | Simple, right? I hope it is. 690 | 691 | ```typescript 692 | import uploader from './uploader.js'; 693 | 694 | const response = await uploader 695 | .for<200, undefined>() 696 | .post( 697 | '/upload/big/data', 698 | bigDataBody, 699 | { 700 | onProgress: (p) => console.log('Progress: %s', (p.lengthComputable && (p.loaded / p.total).toFixed(2)) || 0) 701 | } 702 | ); 703 | ``` 704 | 705 | ### I want fancier! 706 | 707 | If you feel you definitely need more, remember that `DrFetch` is a class. You can always extend it as per JavaScript's 708 | own rules. 709 | 710 | Are you still stuck? [Open a new issue](https://github.com/WJSoftware/dr-fetch/issues) if you have an idea for a 711 | feature that cannot be easily achieved in "user land". 712 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dr-fetch", 3 | "version": "0.11.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "dr-fetch", 9 | "version": "0.11.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/chai": "^5.0.1", 13 | "@types/mocha": "^10.0.10", 14 | "@types/node": "^24.0.3", 15 | "@types/sinon": "^17.0.3", 16 | "chai": "^5.1.2", 17 | "mocha": "^11.7.1", 18 | "publint": "^0.3.9", 19 | "sinon": "^21.0.0", 20 | "ts-mocha": "^11.1.0", 21 | "ts-node": "^10.9.2", 22 | "typescript": "^5.7.2" 23 | } 24 | }, 25 | "node_modules/@cspotcode/source-map-support": { 26 | "version": "0.8.1", 27 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 28 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 29 | "dev": true, 30 | "license": "MIT", 31 | "dependencies": { 32 | "@jridgewell/trace-mapping": "0.3.9" 33 | }, 34 | "engines": { 35 | "node": ">=12" 36 | } 37 | }, 38 | "node_modules/@isaacs/cliui": { 39 | "version": "8.0.2", 40 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 41 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 42 | "dev": true, 43 | "license": "ISC", 44 | "dependencies": { 45 | "string-width": "^5.1.2", 46 | "string-width-cjs": "npm:string-width@^4.2.0", 47 | "strip-ansi": "^7.0.1", 48 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 49 | "wrap-ansi": "^8.1.0", 50 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 51 | }, 52 | "engines": { 53 | "node": ">=12" 54 | } 55 | }, 56 | "node_modules/@jridgewell/resolve-uri": { 57 | "version": "3.1.2", 58 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 59 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 60 | "dev": true, 61 | "license": "MIT", 62 | "engines": { 63 | "node": ">=6.0.0" 64 | } 65 | }, 66 | "node_modules/@jridgewell/sourcemap-codec": { 67 | "version": "1.5.0", 68 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 69 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 70 | "dev": true, 71 | "license": "MIT" 72 | }, 73 | "node_modules/@jridgewell/trace-mapping": { 74 | "version": "0.3.9", 75 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 76 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 77 | "dev": true, 78 | "license": "MIT", 79 | "dependencies": { 80 | "@jridgewell/resolve-uri": "^3.0.3", 81 | "@jridgewell/sourcemap-codec": "^1.4.10" 82 | } 83 | }, 84 | "node_modules/@pkgjs/parseargs": { 85 | "version": "0.11.0", 86 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 87 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 88 | "dev": true, 89 | "license": "MIT", 90 | "optional": true, 91 | "engines": { 92 | "node": ">=14" 93 | } 94 | }, 95 | "node_modules/@publint/pack": { 96 | "version": "0.1.2", 97 | "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz", 98 | "integrity": "sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==", 99 | "dev": true, 100 | "license": "MIT", 101 | "engines": { 102 | "node": ">=18" 103 | }, 104 | "funding": { 105 | "url": "https://bjornlu.com/sponsor" 106 | } 107 | }, 108 | "node_modules/@sinonjs/commons": { 109 | "version": "3.0.1", 110 | "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", 111 | "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", 112 | "dev": true, 113 | "license": "BSD-3-Clause", 114 | "dependencies": { 115 | "type-detect": "4.0.8" 116 | } 117 | }, 118 | "node_modules/@sinonjs/fake-timers": { 119 | "version": "13.0.5", 120 | "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", 121 | "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", 122 | "dev": true, 123 | "license": "BSD-3-Clause", 124 | "dependencies": { 125 | "@sinonjs/commons": "^3.0.1" 126 | } 127 | }, 128 | "node_modules/@sinonjs/samsam": { 129 | "version": "8.0.2", 130 | "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", 131 | "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", 132 | "dev": true, 133 | "license": "BSD-3-Clause", 134 | "dependencies": { 135 | "@sinonjs/commons": "^3.0.1", 136 | "lodash.get": "^4.4.2", 137 | "type-detect": "^4.1.0" 138 | } 139 | }, 140 | "node_modules/@sinonjs/samsam/node_modules/type-detect": { 141 | "version": "4.1.0", 142 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", 143 | "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", 144 | "dev": true, 145 | "license": "MIT", 146 | "engines": { 147 | "node": ">=4" 148 | } 149 | }, 150 | "node_modules/@tsconfig/node10": { 151 | "version": "1.0.11", 152 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", 153 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", 154 | "dev": true, 155 | "license": "MIT" 156 | }, 157 | "node_modules/@tsconfig/node12": { 158 | "version": "1.0.11", 159 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 160 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 161 | "dev": true, 162 | "license": "MIT" 163 | }, 164 | "node_modules/@tsconfig/node14": { 165 | "version": "1.0.3", 166 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 167 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 168 | "dev": true, 169 | "license": "MIT" 170 | }, 171 | "node_modules/@tsconfig/node16": { 172 | "version": "1.0.4", 173 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 174 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 175 | "dev": true, 176 | "license": "MIT" 177 | }, 178 | "node_modules/@types/chai": { 179 | "version": "5.2.2", 180 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", 181 | "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", 182 | "dev": true, 183 | "license": "MIT", 184 | "dependencies": { 185 | "@types/deep-eql": "*" 186 | } 187 | }, 188 | "node_modules/@types/deep-eql": { 189 | "version": "4.0.2", 190 | "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 191 | "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 192 | "dev": true, 193 | "license": "MIT" 194 | }, 195 | "node_modules/@types/mocha": { 196 | "version": "10.0.10", 197 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", 198 | "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", 199 | "dev": true, 200 | "license": "MIT" 201 | }, 202 | "node_modules/@types/node": { 203 | "version": "24.0.3", 204 | "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", 205 | "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", 206 | "dev": true, 207 | "license": "MIT", 208 | "dependencies": { 209 | "undici-types": "~7.8.0" 210 | } 211 | }, 212 | "node_modules/@types/sinon": { 213 | "version": "17.0.4", 214 | "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", 215 | "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", 216 | "dev": true, 217 | "license": "MIT", 218 | "dependencies": { 219 | "@types/sinonjs__fake-timers": "*" 220 | } 221 | }, 222 | "node_modules/@types/sinonjs__fake-timers": { 223 | "version": "8.1.5", 224 | "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", 225 | "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", 226 | "dev": true, 227 | "license": "MIT" 228 | }, 229 | "node_modules/acorn": { 230 | "version": "8.15.0", 231 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 232 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 233 | "dev": true, 234 | "license": "MIT", 235 | "bin": { 236 | "acorn": "bin/acorn" 237 | }, 238 | "engines": { 239 | "node": ">=0.4.0" 240 | } 241 | }, 242 | "node_modules/acorn-walk": { 243 | "version": "8.3.4", 244 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 245 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 246 | "dev": true, 247 | "license": "MIT", 248 | "dependencies": { 249 | "acorn": "^8.11.0" 250 | }, 251 | "engines": { 252 | "node": ">=0.4.0" 253 | } 254 | }, 255 | "node_modules/ansi-regex": { 256 | "version": "6.1.0", 257 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", 258 | "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", 259 | "dev": true, 260 | "license": "MIT", 261 | "engines": { 262 | "node": ">=12" 263 | }, 264 | "funding": { 265 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 266 | } 267 | }, 268 | "node_modules/ansi-styles": { 269 | "version": "4.3.0", 270 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 271 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 272 | "dev": true, 273 | "license": "MIT", 274 | "dependencies": { 275 | "color-convert": "^2.0.1" 276 | }, 277 | "engines": { 278 | "node": ">=8" 279 | }, 280 | "funding": { 281 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 282 | } 283 | }, 284 | "node_modules/arg": { 285 | "version": "4.1.3", 286 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 287 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 288 | "dev": true, 289 | "license": "MIT" 290 | }, 291 | "node_modules/argparse": { 292 | "version": "2.0.1", 293 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 294 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 295 | "dev": true, 296 | "license": "Python-2.0" 297 | }, 298 | "node_modules/assertion-error": { 299 | "version": "2.0.1", 300 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 301 | "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 302 | "dev": true, 303 | "license": "MIT", 304 | "engines": { 305 | "node": ">=12" 306 | } 307 | }, 308 | "node_modules/balanced-match": { 309 | "version": "1.0.2", 310 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 311 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 312 | "dev": true, 313 | "license": "MIT" 314 | }, 315 | "node_modules/brace-expansion": { 316 | "version": "2.0.2", 317 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 318 | "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 319 | "dev": true, 320 | "license": "MIT", 321 | "dependencies": { 322 | "balanced-match": "^1.0.0" 323 | } 324 | }, 325 | "node_modules/browser-stdout": { 326 | "version": "1.3.1", 327 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 328 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 329 | "dev": true, 330 | "license": "ISC" 331 | }, 332 | "node_modules/camelcase": { 333 | "version": "6.3.0", 334 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 335 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 336 | "dev": true, 337 | "license": "MIT", 338 | "engines": { 339 | "node": ">=10" 340 | }, 341 | "funding": { 342 | "url": "https://github.com/sponsors/sindresorhus" 343 | } 344 | }, 345 | "node_modules/chai": { 346 | "version": "5.2.0", 347 | "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 348 | "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 349 | "dev": true, 350 | "license": "MIT", 351 | "dependencies": { 352 | "assertion-error": "^2.0.1", 353 | "check-error": "^2.1.1", 354 | "deep-eql": "^5.0.1", 355 | "loupe": "^3.1.0", 356 | "pathval": "^2.0.0" 357 | }, 358 | "engines": { 359 | "node": ">=12" 360 | } 361 | }, 362 | "node_modules/chalk": { 363 | "version": "4.1.2", 364 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 365 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 366 | "dev": true, 367 | "license": "MIT", 368 | "dependencies": { 369 | "ansi-styles": "^4.1.0", 370 | "supports-color": "^7.1.0" 371 | }, 372 | "engines": { 373 | "node": ">=10" 374 | }, 375 | "funding": { 376 | "url": "https://github.com/chalk/chalk?sponsor=1" 377 | } 378 | }, 379 | "node_modules/chalk/node_modules/supports-color": { 380 | "version": "7.2.0", 381 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 382 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 383 | "dev": true, 384 | "license": "MIT", 385 | "dependencies": { 386 | "has-flag": "^4.0.0" 387 | }, 388 | "engines": { 389 | "node": ">=8" 390 | } 391 | }, 392 | "node_modules/check-error": { 393 | "version": "2.1.1", 394 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 395 | "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 396 | "dev": true, 397 | "license": "MIT", 398 | "engines": { 399 | "node": ">= 16" 400 | } 401 | }, 402 | "node_modules/chokidar": { 403 | "version": "4.0.3", 404 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", 405 | "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 406 | "dev": true, 407 | "license": "MIT", 408 | "dependencies": { 409 | "readdirp": "^4.0.1" 410 | }, 411 | "engines": { 412 | "node": ">= 14.16.0" 413 | }, 414 | "funding": { 415 | "url": "https://paulmillr.com/funding/" 416 | } 417 | }, 418 | "node_modules/cliui": { 419 | "version": "8.0.1", 420 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 421 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 422 | "dev": true, 423 | "license": "ISC", 424 | "dependencies": { 425 | "string-width": "^4.2.0", 426 | "strip-ansi": "^6.0.1", 427 | "wrap-ansi": "^7.0.0" 428 | }, 429 | "engines": { 430 | "node": ">=12" 431 | } 432 | }, 433 | "node_modules/cliui/node_modules/ansi-regex": { 434 | "version": "5.0.1", 435 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 436 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 437 | "dev": true, 438 | "license": "MIT", 439 | "engines": { 440 | "node": ">=8" 441 | } 442 | }, 443 | "node_modules/cliui/node_modules/emoji-regex": { 444 | "version": "8.0.0", 445 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 446 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 447 | "dev": true, 448 | "license": "MIT" 449 | }, 450 | "node_modules/cliui/node_modules/string-width": { 451 | "version": "4.2.3", 452 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 453 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 454 | "dev": true, 455 | "license": "MIT", 456 | "dependencies": { 457 | "emoji-regex": "^8.0.0", 458 | "is-fullwidth-code-point": "^3.0.0", 459 | "strip-ansi": "^6.0.1" 460 | }, 461 | "engines": { 462 | "node": ">=8" 463 | } 464 | }, 465 | "node_modules/cliui/node_modules/strip-ansi": { 466 | "version": "6.0.1", 467 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 468 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 469 | "dev": true, 470 | "license": "MIT", 471 | "dependencies": { 472 | "ansi-regex": "^5.0.1" 473 | }, 474 | "engines": { 475 | "node": ">=8" 476 | } 477 | }, 478 | "node_modules/cliui/node_modules/wrap-ansi": { 479 | "version": "7.0.0", 480 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 481 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 482 | "dev": true, 483 | "license": "MIT", 484 | "dependencies": { 485 | "ansi-styles": "^4.0.0", 486 | "string-width": "^4.1.0", 487 | "strip-ansi": "^6.0.0" 488 | }, 489 | "engines": { 490 | "node": ">=10" 491 | }, 492 | "funding": { 493 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 494 | } 495 | }, 496 | "node_modules/color-convert": { 497 | "version": "2.0.1", 498 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 499 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 500 | "dev": true, 501 | "license": "MIT", 502 | "dependencies": { 503 | "color-name": "~1.1.4" 504 | }, 505 | "engines": { 506 | "node": ">=7.0.0" 507 | } 508 | }, 509 | "node_modules/color-name": { 510 | "version": "1.1.4", 511 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 512 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 513 | "dev": true, 514 | "license": "MIT" 515 | }, 516 | "node_modules/create-require": { 517 | "version": "1.1.1", 518 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 519 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 520 | "dev": true, 521 | "license": "MIT" 522 | }, 523 | "node_modules/cross-spawn": { 524 | "version": "7.0.6", 525 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 526 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 527 | "dev": true, 528 | "license": "MIT", 529 | "dependencies": { 530 | "path-key": "^3.1.0", 531 | "shebang-command": "^2.0.0", 532 | "which": "^2.0.1" 533 | }, 534 | "engines": { 535 | "node": ">= 8" 536 | } 537 | }, 538 | "node_modules/debug": { 539 | "version": "4.4.1", 540 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 541 | "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 542 | "dev": true, 543 | "license": "MIT", 544 | "dependencies": { 545 | "ms": "^2.1.3" 546 | }, 547 | "engines": { 548 | "node": ">=6.0" 549 | }, 550 | "peerDependenciesMeta": { 551 | "supports-color": { 552 | "optional": true 553 | } 554 | } 555 | }, 556 | "node_modules/decamelize": { 557 | "version": "4.0.0", 558 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 559 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 560 | "dev": true, 561 | "license": "MIT", 562 | "engines": { 563 | "node": ">=10" 564 | }, 565 | "funding": { 566 | "url": "https://github.com/sponsors/sindresorhus" 567 | } 568 | }, 569 | "node_modules/deep-eql": { 570 | "version": "5.0.2", 571 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 572 | "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 573 | "dev": true, 574 | "license": "MIT", 575 | "engines": { 576 | "node": ">=6" 577 | } 578 | }, 579 | "node_modules/diff": { 580 | "version": "7.0.0", 581 | "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", 582 | "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", 583 | "dev": true, 584 | "license": "BSD-3-Clause", 585 | "engines": { 586 | "node": ">=0.3.1" 587 | } 588 | }, 589 | "node_modules/eastasianwidth": { 590 | "version": "0.2.0", 591 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 592 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 593 | "dev": true, 594 | "license": "MIT" 595 | }, 596 | "node_modules/emoji-regex": { 597 | "version": "9.2.2", 598 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 599 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 600 | "dev": true, 601 | "license": "MIT" 602 | }, 603 | "node_modules/escalade": { 604 | "version": "3.2.0", 605 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 606 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 607 | "dev": true, 608 | "license": "MIT", 609 | "engines": { 610 | "node": ">=6" 611 | } 612 | }, 613 | "node_modules/escape-string-regexp": { 614 | "version": "4.0.0", 615 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 616 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 617 | "dev": true, 618 | "license": "MIT", 619 | "engines": { 620 | "node": ">=10" 621 | }, 622 | "funding": { 623 | "url": "https://github.com/sponsors/sindresorhus" 624 | } 625 | }, 626 | "node_modules/find-up": { 627 | "version": "5.0.0", 628 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 629 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 630 | "dev": true, 631 | "license": "MIT", 632 | "dependencies": { 633 | "locate-path": "^6.0.0", 634 | "path-exists": "^4.0.0" 635 | }, 636 | "engines": { 637 | "node": ">=10" 638 | }, 639 | "funding": { 640 | "url": "https://github.com/sponsors/sindresorhus" 641 | } 642 | }, 643 | "node_modules/flat": { 644 | "version": "5.0.2", 645 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 646 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 647 | "dev": true, 648 | "license": "BSD-3-Clause", 649 | "bin": { 650 | "flat": "cli.js" 651 | } 652 | }, 653 | "node_modules/foreground-child": { 654 | "version": "3.3.1", 655 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", 656 | "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", 657 | "dev": true, 658 | "license": "ISC", 659 | "dependencies": { 660 | "cross-spawn": "^7.0.6", 661 | "signal-exit": "^4.0.1" 662 | }, 663 | "engines": { 664 | "node": ">=14" 665 | }, 666 | "funding": { 667 | "url": "https://github.com/sponsors/isaacs" 668 | } 669 | }, 670 | "node_modules/get-caller-file": { 671 | "version": "2.0.5", 672 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 673 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 674 | "dev": true, 675 | "license": "ISC", 676 | "engines": { 677 | "node": "6.* || 8.* || >= 10.*" 678 | } 679 | }, 680 | "node_modules/glob": { 681 | "version": "10.4.5", 682 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 683 | "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 684 | "dev": true, 685 | "license": "ISC", 686 | "dependencies": { 687 | "foreground-child": "^3.1.0", 688 | "jackspeak": "^3.1.2", 689 | "minimatch": "^9.0.4", 690 | "minipass": "^7.1.2", 691 | "package-json-from-dist": "^1.0.0", 692 | "path-scurry": "^1.11.1" 693 | }, 694 | "bin": { 695 | "glob": "dist/esm/bin.mjs" 696 | }, 697 | "funding": { 698 | "url": "https://github.com/sponsors/isaacs" 699 | } 700 | }, 701 | "node_modules/has-flag": { 702 | "version": "4.0.0", 703 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 704 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 705 | "dev": true, 706 | "license": "MIT", 707 | "engines": { 708 | "node": ">=8" 709 | } 710 | }, 711 | "node_modules/he": { 712 | "version": "1.2.0", 713 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 714 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 715 | "dev": true, 716 | "license": "MIT", 717 | "bin": { 718 | "he": "bin/he" 719 | } 720 | }, 721 | "node_modules/is-fullwidth-code-point": { 722 | "version": "3.0.0", 723 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 724 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 725 | "dev": true, 726 | "license": "MIT", 727 | "engines": { 728 | "node": ">=8" 729 | } 730 | }, 731 | "node_modules/is-plain-obj": { 732 | "version": "2.1.0", 733 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 734 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 735 | "dev": true, 736 | "license": "MIT", 737 | "engines": { 738 | "node": ">=8" 739 | } 740 | }, 741 | "node_modules/is-unicode-supported": { 742 | "version": "0.1.0", 743 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 744 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 745 | "dev": true, 746 | "license": "MIT", 747 | "engines": { 748 | "node": ">=10" 749 | }, 750 | "funding": { 751 | "url": "https://github.com/sponsors/sindresorhus" 752 | } 753 | }, 754 | "node_modules/isexe": { 755 | "version": "2.0.0", 756 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 757 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 758 | "dev": true, 759 | "license": "ISC" 760 | }, 761 | "node_modules/jackspeak": { 762 | "version": "3.4.3", 763 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 764 | "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 765 | "dev": true, 766 | "license": "BlueOak-1.0.0", 767 | "dependencies": { 768 | "@isaacs/cliui": "^8.0.2" 769 | }, 770 | "funding": { 771 | "url": "https://github.com/sponsors/isaacs" 772 | }, 773 | "optionalDependencies": { 774 | "@pkgjs/parseargs": "^0.11.0" 775 | } 776 | }, 777 | "node_modules/js-yaml": { 778 | "version": "4.1.0", 779 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 780 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 781 | "dev": true, 782 | "license": "MIT", 783 | "dependencies": { 784 | "argparse": "^2.0.1" 785 | }, 786 | "bin": { 787 | "js-yaml": "bin/js-yaml.js" 788 | } 789 | }, 790 | "node_modules/locate-path": { 791 | "version": "6.0.0", 792 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 793 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 794 | "dev": true, 795 | "license": "MIT", 796 | "dependencies": { 797 | "p-locate": "^5.0.0" 798 | }, 799 | "engines": { 800 | "node": ">=10" 801 | }, 802 | "funding": { 803 | "url": "https://github.com/sponsors/sindresorhus" 804 | } 805 | }, 806 | "node_modules/lodash.get": { 807 | "version": "4.4.2", 808 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 809 | "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", 810 | "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", 811 | "dev": true, 812 | "license": "MIT" 813 | }, 814 | "node_modules/log-symbols": { 815 | "version": "4.1.0", 816 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 817 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 818 | "dev": true, 819 | "license": "MIT", 820 | "dependencies": { 821 | "chalk": "^4.1.0", 822 | "is-unicode-supported": "^0.1.0" 823 | }, 824 | "engines": { 825 | "node": ">=10" 826 | }, 827 | "funding": { 828 | "url": "https://github.com/sponsors/sindresorhus" 829 | } 830 | }, 831 | "node_modules/loupe": { 832 | "version": "3.1.4", 833 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", 834 | "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", 835 | "dev": true, 836 | "license": "MIT" 837 | }, 838 | "node_modules/lru-cache": { 839 | "version": "10.4.3", 840 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 841 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 842 | "dev": true, 843 | "license": "ISC" 844 | }, 845 | "node_modules/make-error": { 846 | "version": "1.3.6", 847 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 848 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 849 | "dev": true, 850 | "license": "ISC" 851 | }, 852 | "node_modules/minimatch": { 853 | "version": "9.0.5", 854 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 855 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 856 | "dev": true, 857 | "license": "ISC", 858 | "dependencies": { 859 | "brace-expansion": "^2.0.1" 860 | }, 861 | "engines": { 862 | "node": ">=16 || 14 >=14.17" 863 | }, 864 | "funding": { 865 | "url": "https://github.com/sponsors/isaacs" 866 | } 867 | }, 868 | "node_modules/minipass": { 869 | "version": "7.1.2", 870 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 871 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 872 | "dev": true, 873 | "license": "ISC", 874 | "engines": { 875 | "node": ">=16 || 14 >=14.17" 876 | } 877 | }, 878 | "node_modules/mocha": { 879 | "version": "11.7.1", 880 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", 881 | "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", 882 | "dev": true, 883 | "license": "MIT", 884 | "dependencies": { 885 | "browser-stdout": "^1.3.1", 886 | "chokidar": "^4.0.1", 887 | "debug": "^4.3.5", 888 | "diff": "^7.0.0", 889 | "escape-string-regexp": "^4.0.0", 890 | "find-up": "^5.0.0", 891 | "glob": "^10.4.5", 892 | "he": "^1.2.0", 893 | "js-yaml": "^4.1.0", 894 | "log-symbols": "^4.1.0", 895 | "minimatch": "^9.0.5", 896 | "ms": "^2.1.3", 897 | "picocolors": "^1.1.1", 898 | "serialize-javascript": "^6.0.2", 899 | "strip-json-comments": "^3.1.1", 900 | "supports-color": "^8.1.1", 901 | "workerpool": "^9.2.0", 902 | "yargs": "^17.7.2", 903 | "yargs-parser": "^21.1.1", 904 | "yargs-unparser": "^2.0.0" 905 | }, 906 | "bin": { 907 | "_mocha": "bin/_mocha", 908 | "mocha": "bin/mocha.js" 909 | }, 910 | "engines": { 911 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 912 | } 913 | }, 914 | "node_modules/mri": { 915 | "version": "1.2.0", 916 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 917 | "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", 918 | "dev": true, 919 | "license": "MIT", 920 | "engines": { 921 | "node": ">=4" 922 | } 923 | }, 924 | "node_modules/ms": { 925 | "version": "2.1.3", 926 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 927 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 928 | "dev": true, 929 | "license": "MIT" 930 | }, 931 | "node_modules/p-limit": { 932 | "version": "3.1.0", 933 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 934 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 935 | "dev": true, 936 | "license": "MIT", 937 | "dependencies": { 938 | "yocto-queue": "^0.1.0" 939 | }, 940 | "engines": { 941 | "node": ">=10" 942 | }, 943 | "funding": { 944 | "url": "https://github.com/sponsors/sindresorhus" 945 | } 946 | }, 947 | "node_modules/p-locate": { 948 | "version": "5.0.0", 949 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 950 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 951 | "dev": true, 952 | "license": "MIT", 953 | "dependencies": { 954 | "p-limit": "^3.0.2" 955 | }, 956 | "engines": { 957 | "node": ">=10" 958 | }, 959 | "funding": { 960 | "url": "https://github.com/sponsors/sindresorhus" 961 | } 962 | }, 963 | "node_modules/package-json-from-dist": { 964 | "version": "1.0.1", 965 | "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 966 | "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 967 | "dev": true, 968 | "license": "BlueOak-1.0.0" 969 | }, 970 | "node_modules/package-manager-detector": { 971 | "version": "1.3.0", 972 | "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", 973 | "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", 974 | "dev": true, 975 | "license": "MIT" 976 | }, 977 | "node_modules/path-exists": { 978 | "version": "4.0.0", 979 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 980 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 981 | "dev": true, 982 | "license": "MIT", 983 | "engines": { 984 | "node": ">=8" 985 | } 986 | }, 987 | "node_modules/path-key": { 988 | "version": "3.1.1", 989 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 990 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 991 | "dev": true, 992 | "license": "MIT", 993 | "engines": { 994 | "node": ">=8" 995 | } 996 | }, 997 | "node_modules/path-scurry": { 998 | "version": "1.11.1", 999 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 1000 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 1001 | "dev": true, 1002 | "license": "BlueOak-1.0.0", 1003 | "dependencies": { 1004 | "lru-cache": "^10.2.0", 1005 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 1006 | }, 1007 | "engines": { 1008 | "node": ">=16 || 14 >=14.18" 1009 | }, 1010 | "funding": { 1011 | "url": "https://github.com/sponsors/isaacs" 1012 | } 1013 | }, 1014 | "node_modules/pathval": { 1015 | "version": "2.0.0", 1016 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 1017 | "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 1018 | "dev": true, 1019 | "license": "MIT", 1020 | "engines": { 1021 | "node": ">= 14.16" 1022 | } 1023 | }, 1024 | "node_modules/picocolors": { 1025 | "version": "1.1.1", 1026 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1027 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1028 | "dev": true, 1029 | "license": "ISC" 1030 | }, 1031 | "node_modules/publint": { 1032 | "version": "0.3.12", 1033 | "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.12.tgz", 1034 | "integrity": "sha512-1w3MMtL9iotBjm1mmXtG3Nk06wnq9UhGNRpQ2j6n1Zq7YAD6gnxMMZMIxlRPAydVjVbjSm+n0lhwqsD1m4LD5w==", 1035 | "dev": true, 1036 | "license": "MIT", 1037 | "dependencies": { 1038 | "@publint/pack": "^0.1.2", 1039 | "package-manager-detector": "^1.1.0", 1040 | "picocolors": "^1.1.1", 1041 | "sade": "^1.8.1" 1042 | }, 1043 | "bin": { 1044 | "publint": "src/cli.js" 1045 | }, 1046 | "engines": { 1047 | "node": ">=18" 1048 | }, 1049 | "funding": { 1050 | "url": "https://bjornlu.com/sponsor" 1051 | } 1052 | }, 1053 | "node_modules/randombytes": { 1054 | "version": "2.1.0", 1055 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1056 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1057 | "dev": true, 1058 | "license": "MIT", 1059 | "dependencies": { 1060 | "safe-buffer": "^5.1.0" 1061 | } 1062 | }, 1063 | "node_modules/readdirp": { 1064 | "version": "4.1.2", 1065 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", 1066 | "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", 1067 | "dev": true, 1068 | "license": "MIT", 1069 | "engines": { 1070 | "node": ">= 14.18.0" 1071 | }, 1072 | "funding": { 1073 | "type": "individual", 1074 | "url": "https://paulmillr.com/funding/" 1075 | } 1076 | }, 1077 | "node_modules/require-directory": { 1078 | "version": "2.1.1", 1079 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1080 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1081 | "dev": true, 1082 | "license": "MIT", 1083 | "engines": { 1084 | "node": ">=0.10.0" 1085 | } 1086 | }, 1087 | "node_modules/sade": { 1088 | "version": "1.8.1", 1089 | "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", 1090 | "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", 1091 | "dev": true, 1092 | "license": "MIT", 1093 | "dependencies": { 1094 | "mri": "^1.1.0" 1095 | }, 1096 | "engines": { 1097 | "node": ">=6" 1098 | } 1099 | }, 1100 | "node_modules/safe-buffer": { 1101 | "version": "5.2.1", 1102 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1103 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1104 | "dev": true, 1105 | "funding": [ 1106 | { 1107 | "type": "github", 1108 | "url": "https://github.com/sponsors/feross" 1109 | }, 1110 | { 1111 | "type": "patreon", 1112 | "url": "https://www.patreon.com/feross" 1113 | }, 1114 | { 1115 | "type": "consulting", 1116 | "url": "https://feross.org/support" 1117 | } 1118 | ], 1119 | "license": "MIT" 1120 | }, 1121 | "node_modules/serialize-javascript": { 1122 | "version": "6.0.2", 1123 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 1124 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 1125 | "dev": true, 1126 | "license": "BSD-3-Clause", 1127 | "dependencies": { 1128 | "randombytes": "^2.1.0" 1129 | } 1130 | }, 1131 | "node_modules/shebang-command": { 1132 | "version": "2.0.0", 1133 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1134 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1135 | "dev": true, 1136 | "license": "MIT", 1137 | "dependencies": { 1138 | "shebang-regex": "^3.0.0" 1139 | }, 1140 | "engines": { 1141 | "node": ">=8" 1142 | } 1143 | }, 1144 | "node_modules/shebang-regex": { 1145 | "version": "3.0.0", 1146 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1147 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1148 | "dev": true, 1149 | "license": "MIT", 1150 | "engines": { 1151 | "node": ">=8" 1152 | } 1153 | }, 1154 | "node_modules/signal-exit": { 1155 | "version": "4.1.0", 1156 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 1157 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 1158 | "dev": true, 1159 | "license": "ISC", 1160 | "engines": { 1161 | "node": ">=14" 1162 | }, 1163 | "funding": { 1164 | "url": "https://github.com/sponsors/isaacs" 1165 | } 1166 | }, 1167 | "node_modules/sinon": { 1168 | "version": "21.0.0", 1169 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", 1170 | "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", 1171 | "dev": true, 1172 | "license": "BSD-3-Clause", 1173 | "dependencies": { 1174 | "@sinonjs/commons": "^3.0.1", 1175 | "@sinonjs/fake-timers": "^13.0.5", 1176 | "@sinonjs/samsam": "^8.0.1", 1177 | "diff": "^7.0.0", 1178 | "supports-color": "^7.2.0" 1179 | }, 1180 | "funding": { 1181 | "type": "opencollective", 1182 | "url": "https://opencollective.com/sinon" 1183 | } 1184 | }, 1185 | "node_modules/sinon/node_modules/supports-color": { 1186 | "version": "7.2.0", 1187 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1188 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1189 | "dev": true, 1190 | "license": "MIT", 1191 | "dependencies": { 1192 | "has-flag": "^4.0.0" 1193 | }, 1194 | "engines": { 1195 | "node": ">=8" 1196 | } 1197 | }, 1198 | "node_modules/string-width": { 1199 | "version": "5.1.2", 1200 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 1201 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 1202 | "dev": true, 1203 | "license": "MIT", 1204 | "dependencies": { 1205 | "eastasianwidth": "^0.2.0", 1206 | "emoji-regex": "^9.2.2", 1207 | "strip-ansi": "^7.0.1" 1208 | }, 1209 | "engines": { 1210 | "node": ">=12" 1211 | }, 1212 | "funding": { 1213 | "url": "https://github.com/sponsors/sindresorhus" 1214 | } 1215 | }, 1216 | "node_modules/string-width-cjs": { 1217 | "name": "string-width", 1218 | "version": "4.2.3", 1219 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1220 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1221 | "dev": true, 1222 | "license": "MIT", 1223 | "dependencies": { 1224 | "emoji-regex": "^8.0.0", 1225 | "is-fullwidth-code-point": "^3.0.0", 1226 | "strip-ansi": "^6.0.1" 1227 | }, 1228 | "engines": { 1229 | "node": ">=8" 1230 | } 1231 | }, 1232 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 1233 | "version": "5.0.1", 1234 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1235 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1236 | "dev": true, 1237 | "license": "MIT", 1238 | "engines": { 1239 | "node": ">=8" 1240 | } 1241 | }, 1242 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 1243 | "version": "8.0.0", 1244 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1245 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1246 | "dev": true, 1247 | "license": "MIT" 1248 | }, 1249 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 1250 | "version": "6.0.1", 1251 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1252 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1253 | "dev": true, 1254 | "license": "MIT", 1255 | "dependencies": { 1256 | "ansi-regex": "^5.0.1" 1257 | }, 1258 | "engines": { 1259 | "node": ">=8" 1260 | } 1261 | }, 1262 | "node_modules/strip-ansi": { 1263 | "version": "7.1.0", 1264 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 1265 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 1266 | "dev": true, 1267 | "license": "MIT", 1268 | "dependencies": { 1269 | "ansi-regex": "^6.0.1" 1270 | }, 1271 | "engines": { 1272 | "node": ">=12" 1273 | }, 1274 | "funding": { 1275 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 1276 | } 1277 | }, 1278 | "node_modules/strip-ansi-cjs": { 1279 | "name": "strip-ansi", 1280 | "version": "6.0.1", 1281 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1282 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1283 | "dev": true, 1284 | "license": "MIT", 1285 | "dependencies": { 1286 | "ansi-regex": "^5.0.1" 1287 | }, 1288 | "engines": { 1289 | "node": ">=8" 1290 | } 1291 | }, 1292 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 1293 | "version": "5.0.1", 1294 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1295 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1296 | "dev": true, 1297 | "license": "MIT", 1298 | "engines": { 1299 | "node": ">=8" 1300 | } 1301 | }, 1302 | "node_modules/strip-json-comments": { 1303 | "version": "3.1.1", 1304 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1305 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1306 | "dev": true, 1307 | "license": "MIT", 1308 | "engines": { 1309 | "node": ">=8" 1310 | }, 1311 | "funding": { 1312 | "url": "https://github.com/sponsors/sindresorhus" 1313 | } 1314 | }, 1315 | "node_modules/supports-color": { 1316 | "version": "8.1.1", 1317 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1318 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1319 | "dev": true, 1320 | "license": "MIT", 1321 | "dependencies": { 1322 | "has-flag": "^4.0.0" 1323 | }, 1324 | "engines": { 1325 | "node": ">=10" 1326 | }, 1327 | "funding": { 1328 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1329 | } 1330 | }, 1331 | "node_modules/ts-mocha": { 1332 | "version": "11.1.0", 1333 | "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-11.1.0.tgz", 1334 | "integrity": "sha512-yT7FfzNRCu8ZKkYvAOiH01xNma/vLq6Vit7yINKYFNVP8e5UyrYXSOMIipERTpzVKJQ4Qcos5bQo1tNERNZevQ==", 1335 | "dev": true, 1336 | "license": "MIT", 1337 | "bin": { 1338 | "ts-mocha": "bin/ts-mocha" 1339 | }, 1340 | "engines": { 1341 | "node": ">= 6.X.X" 1342 | }, 1343 | "peerDependencies": { 1344 | "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X || ^11.X.X", 1345 | "ts-node": "^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X", 1346 | "tsconfig-paths": "^4.X.X" 1347 | }, 1348 | "peerDependenciesMeta": { 1349 | "tsconfig-paths": { 1350 | "optional": true 1351 | } 1352 | } 1353 | }, 1354 | "node_modules/ts-node": { 1355 | "version": "10.9.2", 1356 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 1357 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1358 | "dev": true, 1359 | "license": "MIT", 1360 | "dependencies": { 1361 | "@cspotcode/source-map-support": "^0.8.0", 1362 | "@tsconfig/node10": "^1.0.7", 1363 | "@tsconfig/node12": "^1.0.7", 1364 | "@tsconfig/node14": "^1.0.0", 1365 | "@tsconfig/node16": "^1.0.2", 1366 | "acorn": "^8.4.1", 1367 | "acorn-walk": "^8.1.1", 1368 | "arg": "^4.1.0", 1369 | "create-require": "^1.1.0", 1370 | "diff": "^4.0.1", 1371 | "make-error": "^1.1.1", 1372 | "v8-compile-cache-lib": "^3.0.1", 1373 | "yn": "3.1.1" 1374 | }, 1375 | "bin": { 1376 | "ts-node": "dist/bin.js", 1377 | "ts-node-cwd": "dist/bin-cwd.js", 1378 | "ts-node-esm": "dist/bin-esm.js", 1379 | "ts-node-script": "dist/bin-script.js", 1380 | "ts-node-transpile-only": "dist/bin-transpile.js", 1381 | "ts-script": "dist/bin-script-deprecated.js" 1382 | }, 1383 | "peerDependencies": { 1384 | "@swc/core": ">=1.2.50", 1385 | "@swc/wasm": ">=1.2.50", 1386 | "@types/node": "*", 1387 | "typescript": ">=2.7" 1388 | }, 1389 | "peerDependenciesMeta": { 1390 | "@swc/core": { 1391 | "optional": true 1392 | }, 1393 | "@swc/wasm": { 1394 | "optional": true 1395 | } 1396 | } 1397 | }, 1398 | "node_modules/ts-node/node_modules/diff": { 1399 | "version": "4.0.2", 1400 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 1401 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 1402 | "dev": true, 1403 | "license": "BSD-3-Clause", 1404 | "engines": { 1405 | "node": ">=0.3.1" 1406 | } 1407 | }, 1408 | "node_modules/type-detect": { 1409 | "version": "4.0.8", 1410 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1411 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1412 | "dev": true, 1413 | "license": "MIT", 1414 | "engines": { 1415 | "node": ">=4" 1416 | } 1417 | }, 1418 | "node_modules/typescript": { 1419 | "version": "5.8.3", 1420 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1421 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1422 | "dev": true, 1423 | "license": "Apache-2.0", 1424 | "bin": { 1425 | "tsc": "bin/tsc", 1426 | "tsserver": "bin/tsserver" 1427 | }, 1428 | "engines": { 1429 | "node": ">=14.17" 1430 | } 1431 | }, 1432 | "node_modules/undici-types": { 1433 | "version": "7.8.0", 1434 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", 1435 | "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", 1436 | "dev": true, 1437 | "license": "MIT" 1438 | }, 1439 | "node_modules/v8-compile-cache-lib": { 1440 | "version": "3.0.1", 1441 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 1442 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 1443 | "dev": true, 1444 | "license": "MIT" 1445 | }, 1446 | "node_modules/which": { 1447 | "version": "2.0.2", 1448 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1449 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1450 | "dev": true, 1451 | "license": "ISC", 1452 | "dependencies": { 1453 | "isexe": "^2.0.0" 1454 | }, 1455 | "bin": { 1456 | "node-which": "bin/node-which" 1457 | }, 1458 | "engines": { 1459 | "node": ">= 8" 1460 | } 1461 | }, 1462 | "node_modules/workerpool": { 1463 | "version": "9.3.2", 1464 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", 1465 | "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", 1466 | "dev": true, 1467 | "license": "Apache-2.0" 1468 | }, 1469 | "node_modules/wrap-ansi": { 1470 | "version": "8.1.0", 1471 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1472 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1473 | "dev": true, 1474 | "license": "MIT", 1475 | "dependencies": { 1476 | "ansi-styles": "^6.1.0", 1477 | "string-width": "^5.0.1", 1478 | "strip-ansi": "^7.0.1" 1479 | }, 1480 | "engines": { 1481 | "node": ">=12" 1482 | }, 1483 | "funding": { 1484 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1485 | } 1486 | }, 1487 | "node_modules/wrap-ansi-cjs": { 1488 | "name": "wrap-ansi", 1489 | "version": "7.0.0", 1490 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1491 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1492 | "dev": true, 1493 | "license": "MIT", 1494 | "dependencies": { 1495 | "ansi-styles": "^4.0.0", 1496 | "string-width": "^4.1.0", 1497 | "strip-ansi": "^6.0.0" 1498 | }, 1499 | "engines": { 1500 | "node": ">=10" 1501 | }, 1502 | "funding": { 1503 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1504 | } 1505 | }, 1506 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 1507 | "version": "5.0.1", 1508 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1509 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1510 | "dev": true, 1511 | "license": "MIT", 1512 | "engines": { 1513 | "node": ">=8" 1514 | } 1515 | }, 1516 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 1517 | "version": "8.0.0", 1518 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1519 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1520 | "dev": true, 1521 | "license": "MIT" 1522 | }, 1523 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 1524 | "version": "4.2.3", 1525 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1526 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1527 | "dev": true, 1528 | "license": "MIT", 1529 | "dependencies": { 1530 | "emoji-regex": "^8.0.0", 1531 | "is-fullwidth-code-point": "^3.0.0", 1532 | "strip-ansi": "^6.0.1" 1533 | }, 1534 | "engines": { 1535 | "node": ">=8" 1536 | } 1537 | }, 1538 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 1539 | "version": "6.0.1", 1540 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1541 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1542 | "dev": true, 1543 | "license": "MIT", 1544 | "dependencies": { 1545 | "ansi-regex": "^5.0.1" 1546 | }, 1547 | "engines": { 1548 | "node": ">=8" 1549 | } 1550 | }, 1551 | "node_modules/wrap-ansi/node_modules/ansi-styles": { 1552 | "version": "6.2.1", 1553 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 1554 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 1555 | "dev": true, 1556 | "license": "MIT", 1557 | "engines": { 1558 | "node": ">=12" 1559 | }, 1560 | "funding": { 1561 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1562 | } 1563 | }, 1564 | "node_modules/y18n": { 1565 | "version": "5.0.8", 1566 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1567 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1568 | "dev": true, 1569 | "license": "ISC", 1570 | "engines": { 1571 | "node": ">=10" 1572 | } 1573 | }, 1574 | "node_modules/yargs": { 1575 | "version": "17.7.2", 1576 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1577 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1578 | "dev": true, 1579 | "license": "MIT", 1580 | "dependencies": { 1581 | "cliui": "^8.0.1", 1582 | "escalade": "^3.1.1", 1583 | "get-caller-file": "^2.0.5", 1584 | "require-directory": "^2.1.1", 1585 | "string-width": "^4.2.3", 1586 | "y18n": "^5.0.5", 1587 | "yargs-parser": "^21.1.1" 1588 | }, 1589 | "engines": { 1590 | "node": ">=12" 1591 | } 1592 | }, 1593 | "node_modules/yargs-parser": { 1594 | "version": "21.1.1", 1595 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1596 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1597 | "dev": true, 1598 | "license": "ISC", 1599 | "engines": { 1600 | "node": ">=12" 1601 | } 1602 | }, 1603 | "node_modules/yargs-unparser": { 1604 | "version": "2.0.0", 1605 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1606 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1607 | "dev": true, 1608 | "license": "MIT", 1609 | "dependencies": { 1610 | "camelcase": "^6.0.0", 1611 | "decamelize": "^4.0.0", 1612 | "flat": "^5.0.2", 1613 | "is-plain-obj": "^2.1.0" 1614 | }, 1615 | "engines": { 1616 | "node": ">=10" 1617 | } 1618 | }, 1619 | "node_modules/yargs/node_modules/ansi-regex": { 1620 | "version": "5.0.1", 1621 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1622 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1623 | "dev": true, 1624 | "license": "MIT", 1625 | "engines": { 1626 | "node": ">=8" 1627 | } 1628 | }, 1629 | "node_modules/yargs/node_modules/emoji-regex": { 1630 | "version": "8.0.0", 1631 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1632 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1633 | "dev": true, 1634 | "license": "MIT" 1635 | }, 1636 | "node_modules/yargs/node_modules/string-width": { 1637 | "version": "4.2.3", 1638 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1639 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1640 | "dev": true, 1641 | "license": "MIT", 1642 | "dependencies": { 1643 | "emoji-regex": "^8.0.0", 1644 | "is-fullwidth-code-point": "^3.0.0", 1645 | "strip-ansi": "^6.0.1" 1646 | }, 1647 | "engines": { 1648 | "node": ">=8" 1649 | } 1650 | }, 1651 | "node_modules/yargs/node_modules/strip-ansi": { 1652 | "version": "6.0.1", 1653 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1654 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1655 | "dev": true, 1656 | "license": "MIT", 1657 | "dependencies": { 1658 | "ansi-regex": "^5.0.1" 1659 | }, 1660 | "engines": { 1661 | "node": ">=8" 1662 | } 1663 | }, 1664 | "node_modules/yn": { 1665 | "version": "3.1.1", 1666 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 1667 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 1668 | "dev": true, 1669 | "license": "MIT", 1670 | "engines": { 1671 | "node": ">=6" 1672 | } 1673 | }, 1674 | "node_modules/yocto-queue": { 1675 | "version": "0.1.0", 1676 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1677 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1678 | "dev": true, 1679 | "license": "MIT", 1680 | "engines": { 1681 | "node": ">=10" 1682 | }, 1683 | "funding": { 1684 | "url": "https://github.com/sponsors/sindresorhus" 1685 | } 1686 | } 1687 | } 1688 | } 1689 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dr-fetch", 3 | "version": "0.11.0", 4 | "description": "Fetching done right, not just the happy path.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "scripts": { 9 | "test": "ts-mocha -n loader=ts-node/esm -p ./tsconfig.json ./tests/**/*.test.ts", 10 | "build": "tsc && publint", 11 | "prebuild": "npm run test", 12 | "prepublishOnly": "npm run build" 13 | }, 14 | "keywords": [ 15 | "fetch" 16 | ], 17 | "author": { 18 | "email": "webJose@gmail.com", 19 | "name": "José Pablo Ramírez Vargas", 20 | "url": "https://github.com/WJSoftware" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/WJSoftware/dr-fetch.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/WJSoftware/dr-fetch/issues" 28 | }, 29 | "files": [ 30 | "dist/**/*.js", 31 | "dist/**/*.d.ts" 32 | ], 33 | "exports": { 34 | ".": { 35 | "types": "./dist/index.d.ts", 36 | "import": "./dist/index.js" 37 | } 38 | }, 39 | "license": "MIT", 40 | "devDependencies": { 41 | "@types/chai": "^5.0.1", 42 | "@types/mocha": "^10.0.10", 43 | "@types/node": "^24.0.3", 44 | "@types/sinon": "^17.0.3", 45 | "chai": "^5.1.2", 46 | "mocha": "^11.7.1", 47 | "publint": "^0.3.9", 48 | "sinon": "^21.0.0", 49 | "ts-mocha": "^11.1.0", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.7.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/DrFetch.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AbortedFetchResult, 3 | AutoAbortKey, 4 | BodyParserFn, 5 | CloneOptions, 6 | FetchFn, 7 | FetchFnInit, 8 | FetchFnUrl, 9 | FetchResult, 10 | ProcessorPattern, 11 | StatusCode 12 | } from "./types.js"; 13 | import { hasHeader, setHeaders } from "./headers.js"; 14 | 15 | /** 16 | * List of patterns to match against the content-type response header. If there's a match, the response is treated as 17 | * JSON. 18 | */ 19 | const jsonTypes: ProcessorPattern[] = [ 20 | /^application\/(\w+\+?)?json/, 21 | ]; 22 | 23 | /** 24 | * List of patterns to match against the content-type response header. If there's a match, the response is treated as 25 | * text. 26 | */ 27 | const textTypes: ProcessorPattern[] = [ 28 | /^text\/.+/, 29 | ]; 30 | 31 | /** 32 | * Determines if the given object is a POJO. 33 | * @param obj Object under test. 34 | * @returns `true` if it is a POJO, or `false` otherwise. 35 | */ 36 | function isPojo(obj: unknown): obj is Record { 37 | if (obj === null || typeof obj !== 'object') { 38 | return false; 39 | } 40 | const proto = Object.getPrototypeOf(obj); 41 | if (proto == null) { 42 | return true; 43 | } 44 | return proto === Object.prototype; 45 | } 46 | 47 | function jsonParser(response: Response) { 48 | return response.json(); 49 | } 50 | 51 | function textParser(response: Response) { 52 | return response.text(); 53 | } 54 | 55 | /** 56 | * # DrFetch 57 | * 58 | * Class that wraps around the provided data-fetching function (or the standard `fetch` function) in order to provide 59 | * full body typing. 60 | * 61 | * ## How To Use 62 | * 63 | * Create a new instance of this class to simplify fetching while being able to fully type the response body. The 64 | * process of typing the body can be done per HTTP status code. 65 | * 66 | * @example 67 | * ```typescript 68 | * type ToDo = { id: number; text: string; } 69 | * type NotAuthBody = { loginUrl: string; } 70 | * 71 | * const fetcher = new DrFetch() 72 | * .for<200, ToDo[]>() 73 | * .for<401, NotAuthBody>() 74 | * ; 75 | * 76 | * const response = await fetcher.go('api/todos'); 77 | * // At this point, your editor's Intellisense will predict that response.body is either ToDo[] or NotAuthBody, and 78 | * // TypeScript narrowing will work by either testing for response.ok or response.status: 79 | * if (response.status === 200 ) { 80 | * // Do stuff with response.body, an array of ToDo objects. 81 | * } 82 | * else { 83 | * // Redirect to the login page. 84 | * window.location.href = response.body.loginUrl; 85 | * } 86 | * ``` 87 | * 88 | * ### Specifying the Same Type for Multiple Status Codes 89 | * 90 | * This is done quite simply by specifying multiple status codes. 91 | * 92 | * @example 93 | * ```typescript 94 | * const fetcher = new DrFetch() 95 | * .for<200 | 201, { data: ToDo }>() 96 | * ; 97 | * ``` 98 | * 99 | * You can also take advantage of the `OkStatusCode` and `NonOkStatusCode` types. The former is all possible 2xx 100 | * status codes; the latter is all other status codes. There's also `ClientErrorStatusCode` for 4xx status codes, and 101 | * `ServerErrorStatusCode` for 5xx errors. Yes, `StatusCode` is one that comprehends all status codes. 102 | * 103 | * @example 104 | * ```typescript 105 | * const fetcher = new DrFetch() 106 | * .for() 107 | * ; 108 | * ``` 109 | * 110 | * ## When to Create a New Fetcher 111 | * 112 | * Create a new fetcher when your current one needs a different data-fetching function, a different set of custom 113 | * processors, or you need an abortable fetcher (while keeping a non-abortable fetcher also available). 114 | * 115 | * A new fetcher object may be created by creating it from scratch using the class constructor, or by cloning an 116 | * existing one using the parent's `clone` function. When cloning, pass a new data-fetching function (if required) so 117 | * the clone uses this one instead of the one of the parent fetcher. 118 | */ 119 | export class DrFetch< 120 | TStatusCode extends number = StatusCode, 121 | TFetchInit extends FetchFnInit = FetchFnInit, 122 | T = unknown, 123 | Abortable extends boolean = false 124 | > { 125 | #fetchFn: FetchFn; 126 | #customProcessors: [ProcessorPattern, (response: Response, stockParsers: { json: BodyParserFn; text: BodyParserFn; }) => Promise][] = []; 127 | #fetchImpl: (url: FetchFnUrl, init?: TFetchInit) => Promise; 128 | #autoAbortMap: Map | undefined; 129 | 130 | async #abortableFetch(url: FetchFnUrl, init?: TFetchInit) { 131 | try { 132 | return await this.#simpleFetch(url, init); 133 | } 134 | catch (err: unknown) { 135 | if (err instanceof DOMException && err.name === 'AbortError') { 136 | return { 137 | aborted: true, 138 | error: err 139 | }; 140 | } 141 | throw err; 142 | } 143 | } 144 | 145 | async #simpleFetch(url: FetchFnUrl, init?: TFetchInit) { 146 | const response = await this.#fetchFn(url, init); 147 | const body = await this.#readBody(response); 148 | return { 149 | aborted: false, 150 | ok: response.ok, 151 | status: response.status, 152 | statusText: response.statusText, 153 | headers: response.headers, 154 | body 155 | } as T; 156 | } 157 | 158 | /** 159 | * Initializes a new instance of this class. 160 | * 161 | * If you would like to set interception before or after fetching, simply write a custom function that has the same 162 | * signature as the stock `fetch` function that does what is required before or after fetching, and pass it as the 163 | * first argument when creating the new instance. 164 | * 165 | * @example 166 | * ```typescript 167 | * import { setHeaders, type FetchFnUrl, type FetchFnInit } from "dr-fetch"; 168 | * 169 | * async function myCustomFetch(url: FetchFnUrl, init?: FetchFnInit) { 170 | * // Code before fetching is the equivalent of pre-fetch interception. 171 | * const myToken = getMyToken(); 172 | * init = init ?? { }; 173 | * setHeaders(init, { 174 | * Authorization: `Bearer ${myToken}`, 175 | * Accept: 'application/json', 176 | * }); 177 | * const response = await fetch(url, init); 178 | * // Code after obtaining the response is the equivalent of post-fetch interception. 179 | * // Post-fetch interception is usually unneeded. Use custom processors instead. 180 | * ... 181 | * return response; 182 | * } 183 | * 184 | * // Create fetcher instance now, and usually you export it. 185 | * export default new DrFetch(myCustomFetch); 186 | * ``` 187 | * 188 | * If you need to do special processing of the body, don't do post-interception and instead use the `withProcessor` 189 | * function to register a custom body processor. 190 | * @param fetchFn Optional data-fetching function to use instead of the stock `fetch` function. 191 | */ 192 | constructor(fetchFn?: FetchFn) { 193 | this.#fetchFn = fetchFn ?? fetch.bind(globalThis.window || global); 194 | this.#fetchImpl = this.#simpleFetch.bind(this); 195 | } 196 | 197 | /** 198 | * Gets a Boolean value indicating whether this fetcher object is in abortable mode or not. 199 | * 200 | * **NOTE**: Once in abortable mode, the fetcher object cannot be reverted to non-abortable mode. 201 | */ 202 | get isAbortable() { 203 | return !!this.#autoAbortMap; 204 | } 205 | 206 | /** 207 | * Clones this fetcher object by creating a new fetcher object with the same data-fetching function, custom 208 | * body processors, and data typing unless specified otherwise via the options parameter. 209 | * @param options Optional options to control which features are cloned. 210 | * @returns A new fetcher object that complies with the supplied (or if not supplied, the default) options. 211 | */ 212 | clone( 213 | options?: CloneOptions 214 | ) { 215 | const opts = { 216 | fetchFn: undefined, 217 | includeProcessors: true, 218 | preserveTyping: true, 219 | preserveAbortable: true, 220 | ...options 221 | }; 222 | const newClone = new DrFetch(opts.fetchFn === false ? undefined : opts?.fetchFn ?? this.#fetchFn); 223 | if (opts.includeProcessors) { 224 | newClone.#customProcessors = [...this.#customProcessors]; 225 | } 226 | if (opts.preserveAbortable && this.isAbortable) { 227 | newClone.abortable(); 228 | } 229 | return newClone as DrFetch; 230 | } 231 | 232 | /** 233 | * Adds a custom processor to the fetcher object. 234 | * 235 | * The custom processor will be used if the value of the `"content-type"` header satisfies the given pattern. The 236 | * pattern can be a string or regular expression, and when a string is used, the processor will qualify if the 237 | * pattern is found inside the `Content-Type` HTTP header's value. 238 | * @param pattern String, regular expression, array of strings or regular expressions or predicate function used to 239 | * test the value of the `Content-Type` HTTP response header. The predicate function receives the response object 240 | * and the content type. 241 | * @param processorFn Custom processor function that is given the HTTP response object and the stock body processors, 242 | * and is responsible to return the body. 243 | * @returns The current fetcher object to enable fluent syntax. 244 | */ 245 | withProcessor( 246 | pattern: ProcessorPattern, 247 | processorFn: (response: Response, stockParsers: { json: BodyParserFn; text: BodyParserFn; }) => Promise 248 | ) { 249 | this.#customProcessors.push([pattern, processorFn]); 250 | return this; 251 | } 252 | 253 | /** 254 | * Alters this fetcher's response type by associating the given body type to the given status code type, which can 255 | * be a single status code, or multiple status codes. 256 | * @returns This fetcher object with its response type modified to include the body specification provided. 257 | */ 258 | for() { 259 | return this as DrFetch, Abortable>; 260 | } 261 | 262 | #contentMatchesType(contentType: string, response: Response, ...types: ProcessorPattern[]) { 263 | for (let pattern of types) { 264 | if (Array.isArray(pattern)) { 265 | if (this.#contentMatchesType(contentType, response, ...pattern)) { 266 | return true; 267 | } 268 | } else if (typeof pattern === 'string') { 269 | if (contentType.includes(pattern)) { 270 | return true; 271 | } 272 | } 273 | else if (pattern instanceof RegExp) { 274 | if (pattern.test(contentType)) { 275 | return true; 276 | } 277 | } 278 | else { 279 | if (pattern(response, contentType)) { 280 | return true; 281 | } 282 | } 283 | } 284 | return false; 285 | } 286 | 287 | async #readBody(response: Response) { 288 | if (!response.body) { 289 | return null; 290 | } 291 | const contentType = response.headers.get('content-type'); 292 | if (!contentType) { 293 | throw new Error('The response carries no content type header. Cannot determine how to parse.'); 294 | } 295 | // Custom processors have the highest priority. 296 | if (this.#customProcessors.length) { 297 | for (let [pattern, processorFn] of this.#customProcessors) { 298 | if (this.#contentMatchesType(contentType, response, pattern)) { 299 | return await processorFn(response, { 300 | json: jsonParser, 301 | text: textParser, 302 | }); 303 | } 304 | } 305 | } 306 | if (this.#contentMatchesType(contentType, response, ...jsonTypes)) { 307 | return await jsonParser(response); 308 | } 309 | else if (this.#contentMatchesType(contentType, response, ...textTypes)) { 310 | return await textParser(response); 311 | } 312 | throw new Error(`Could not determine how to process body of type "${contentType}". Provide a custom processor by calling 'withProcessor()'.`); 313 | } 314 | 315 | abortable() { 316 | this.#fetchImpl = this.#abortableFetch.bind(this); 317 | this.#autoAbortMap ??= new Map(); 318 | return this as DrFetch; 319 | } 320 | 321 | /** 322 | * Fetches the specified URL using the specified options and returns information contained within the HTTP response 323 | * object. 324 | * @param url URL parameter for the data-fetching function. 325 | * @param init Options for the data-fetching function. 326 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 327 | */ 328 | async fetch(url: FetchFnUrl, init?: TFetchInit): Promise<(Abortable extends true ? AbortedFetchResult | T : T)> { 329 | if (!this.#autoAbortMap && init?.autoAbort) { 330 | throw new Error('Cannot use autoAbort if the fetcher is not in abortable mode. Call "abortable()" first.'); 331 | } 332 | const autoAbort = { 333 | key: typeof init?.autoAbort === 'object' ? init.autoAbort.key : init?.autoAbort, 334 | delay: typeof init?.autoAbort === 'object' ? init.autoAbort.delay : undefined, 335 | }; 336 | if (autoAbort.key) { 337 | this.#autoAbortMap?.get(autoAbort.key)?.abort(); 338 | const ac = new AbortController(); 339 | this.#autoAbortMap!.set(autoAbort.key, ac); 340 | init ??= {} as TFetchInit; 341 | init.signal = ac.signal; 342 | if (autoAbort.delay !== undefined) { 343 | const aborted = await new Promise((rs) => { 344 | setTimeout(() => rs(ac.signal.aborted), autoAbort.delay); 345 | }); 346 | if (aborted) { 347 | // @ts-expect-error TS2322: A runtime check is in place to ensure that the type is correct. 348 | return { 349 | aborted: true, 350 | error: new DOMException('Aborted while delayed.', 'AbortError') 351 | }; 352 | } 353 | } 354 | } 355 | return await this.#fetchImpl(url, init) 356 | .finally(() => autoAbort.key && this.#autoAbortMap?.delete(autoAbort.key)); 357 | } 358 | 359 | #createInit(body: BodyInit | null | Record | undefined, init?: FetchFnInit) { 360 | init ??= {}; 361 | let headers: [string, string] | undefined; 362 | if (isPojo(body) || Array.isArray(body)) { 363 | body = JSON.stringify(body); 364 | headers = ['content-type', 'application/json']; 365 | } 366 | if (headers && !hasHeader(init.headers ?? {}, 'content-type')) { 367 | setHeaders(init, [headers]); 368 | } 369 | init.body = body; 370 | return init; 371 | } 372 | 373 | /** 374 | * Shortcut method to emit a GET HTTP request. 375 | * @param url URL for the fetch function call. 376 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 377 | */ 378 | get(url: URL | string, init?: Omit) { 379 | return this.fetch(url, { ...init, method: 'GET' } as TFetchInit); 380 | } 381 | 382 | /** 383 | * Shortcut method to emit a HEAD HTTP request. 384 | * @param url URL for the fetch function call. 385 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 386 | */ 387 | head(url: URL | string, init?: Omit) { 388 | return this.fetch(url, { ...init, method: 'HEAD' } as TFetchInit); 389 | } 390 | 391 | /** 392 | * Shortcut method to emit a POST HTTP request. 393 | * @param url URL for the fetch function call. 394 | * @param body The data to send as body. 395 | * 396 | * If a POJO is passed, it will be stringified and the `Content-Type` header of the request will be set to 397 | * `'application/json'`. This is also true with arrays. 398 | * 399 | * > **NOTE**: You must make sure that the POJO or the array (and its elements) you pass as body are serializable. 400 | * 401 | * Any other body type will not generate a `Content-Type` header and will be reliant on what the `fetch()` function 402 | * does in those cases. 403 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 404 | */ 405 | post(url: URL | string, body?: BodyInit | null | Record, init?: Omit) { 406 | const fullInit = this.#createInit(body, init); 407 | fullInit.method = 'POST'; 408 | return this.fetch(url, fullInit as TFetchInit); 409 | } 410 | 411 | /** 412 | * Shortcut method to emit a PATCH HTTP request. 413 | * @param url URL for the fetch function call. 414 | * @param body The data to send as body. 415 | * 416 | * If a POJO is passed, it will be stringified and the `Content-Type` header of the request will be set to 417 | * `'application/json'`. This is also true with arrays. 418 | * 419 | * > **NOTE**: You must make sure that the POJO or the array (and its elements) you pass as body are serializable. 420 | * 421 | * Any other body type will not generate a `Content-Type` header and will be reliant on what the `fetch()` function 422 | * does in those cases. 423 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 424 | */ 425 | patch(url: URL | string, body?: BodyInit | null | Record, init?: Omit) { 426 | const fullInit = this.#createInit(body, init); 427 | fullInit.method = 'PATCH'; 428 | return this.fetch(url, fullInit as TFetchInit); 429 | } 430 | 431 | /** 432 | * Shortcut method to emit a DELETE HTTP request. 433 | * @param url URL for the fetch function call. 434 | * @param body The data to send as body. 435 | * 436 | * If a POJO is passed, it will be stringified and the `Content-Type` header of the request will be set to 437 | * `'application/json'`. This is also true with arrays. 438 | * 439 | * > **NOTE**: You must make sure that the POJO or the array (and its elements) you pass as body are serializable. 440 | * 441 | * Any other body type will not generate a `Content-Type` header and will be reliant on what the `fetch()` function 442 | * does in those cases. 443 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 444 | */ 445 | delete(url: URL | string, body?: BodyInit | null | Record, init?: Omit) { 446 | const fullInit = this.#createInit(body, init); 447 | fullInit.method = 'DELETE'; 448 | return this.fetch(url, fullInit as TFetchInit); 449 | } 450 | 451 | /** 452 | * Shortcut method to emit a PUT HTTP request. 453 | * @param url URL for the fetch function call. 454 | * @param body The data to send as body. 455 | * 456 | * If a POJO is passed, it will be stringified and the `Content-Type` header of the request will be set to 457 | * `'application/json'`. This is also true with arrays. 458 | * 459 | * > **NOTE**: You must make sure that the POJO or the array (and its elements) you pass as body are serializable. 460 | * 461 | * Any other body type will not generate a `Content-Type` header and will be reliant on what the `fetch()` function 462 | * does in those cases. 463 | * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties. 464 | */ 465 | put(url: URL | string, body?: BodyInit | null | Record, init?: Omit) { 466 | const fullInit = this.#createInit(body, init); 467 | fullInit.method = 'PUT'; 468 | return this.fetch(url, fullInit as TFetchInit); 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /src/StatusCodes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumerates all standardized HTTP status codes, except for the ones in the 1xx and 3xx ranges. 3 | * 4 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status) 5 | */ 6 | export const StatusCodes = Object.freeze({ 7 | /** 8 | * The request has succeeded. 9 | * 10 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200) 11 | */ 12 | Ok: 200, 13 | /** 14 | * The request has been fulfilled and resulted in a new resource being created. 15 | * 16 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201) 17 | */ 18 | Created: 201, 19 | /** 20 | * The server has accepted the request but has not yet processed it. 21 | * 22 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202) 23 | */ 24 | Accepted: 202, 25 | /** 26 | * This response code means the returned metadata is not exactly the same as is available from the origin server, 27 | * but is collected from a local or a third-party copy. This is mostly used for mirrors or backups of another 28 | * resource. Except for that specific case, the 200 OK response is preferred to this status. 29 | * 30 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203) 31 | */ 32 | NonAuthoritativeInformation: 203, 33 | /** 34 | * The server successfully processed the request, but is not returning any content. 35 | * 36 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204) 37 | */ 38 | NoContent: 204, 39 | /** 40 | * The server successfully processed the request, and is not returning any content, but requires that the requester 41 | * reset the document view. 42 | * 43 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205) 44 | */ 45 | ResetContent: 205, 46 | /** 47 | * The server is delivering only part of the resource due to a range header sent by the client. 48 | * 49 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206) 50 | */ 51 | PartialContent: 206, 52 | /** 53 | * The message body that follows is an XML message and can contain a number of separate response codes, depending on 54 | * how many sub-requests were made. 55 | * 56 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/207) 57 | */ 58 | MultiStatus: 207, 59 | /** 60 | * Used inside a `` response element to avoid repeatedly enumerating the internal members of multiple 61 | * bindings to the same collection. 62 | * 63 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/208) 64 | */ 65 | AlreadyReported: 208, 66 | /** 67 | * The server has fulfilled a GET request for the resource, and the response is a representation of the result of 68 | * one or more instance-manipulations applied to the current instance. 69 | * 70 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/226) 71 | */ 72 | IMUsed: 226, 73 | /** 74 | * The server cannot or will not process the request due to something that is perceived to be a client error 75 | * (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). 76 | * 77 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) 78 | */ 79 | BadRequest: 400, 80 | /** 81 | * The client must authenticate itself to get the requested response. 82 | * 83 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) 84 | */ 85 | Unauthorized: 401, 86 | /** 87 | * Payment is required to access the requested resource. 88 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402) 89 | */ 90 | PaymentRequired: 402, 91 | /** 92 | * The client does not have access rights to the content. 93 | * 94 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) 95 | */ 96 | Forbidden: 403, 97 | /** 98 | * The server can not find the requested resource. 99 | * 100 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404) 101 | */ 102 | NotFound: 404, 103 | /** 104 | * The request method is known by the server but has been disabled and cannot be used. 105 | * 106 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405) 107 | */ 108 | MethodNotAllowed: 405, 109 | /** 110 | * This response is sent when the web server, after performing server-driven content negotiation, doesn't find any 111 | * content that conforms to the criteria given by the user agent. 112 | * 113 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406) 114 | */ 115 | NotAcceptable: 406, 116 | /** 117 | * This is similar to 401 but authentication is needed to be done by a proxy. 118 | * 119 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407) 120 | */ 121 | ProxyAuthenticationRequired: 407, 122 | /** 123 | * This response is sent on an idle connection by some servers, even without any previous request by the client. 124 | * 125 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) 126 | */ 127 | RequestTimeout: 408, 128 | /** 129 | * The request conflicts with the current state of the server. 130 | * 131 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) 132 | */ 133 | Conflict: 409, 134 | /** 135 | * This response would be sent when the requested content has been permanently deleted from server, with no 136 | * forwarding address. 137 | * 138 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410) 139 | */ 140 | Gone: 410, 141 | /** 142 | * The server rejected the request because the Content-Length header field is not defined and the server requires it. 143 | * 144 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411) 145 | */ 146 | LengthRequired: 411, 147 | /** 148 | * The client has indicated preconditions in its headers which the server does not meet. 149 | * 150 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412) 151 | */ 152 | PreconditionFailed: 412, 153 | /** 154 | * The request is larger than the server is willing or able to process. 155 | * 156 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) 157 | */ 158 | PayloadTooLarge: 413, 159 | /** 160 | * The URI requested by the client is longer than the server is willing to interpret. 161 | * 162 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414) 163 | */ 164 | URITooLong: 414, 165 | /** 166 | * The media format of the requested data is not supported by the server. 167 | * 168 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415) 169 | */ 170 | UnsupportedMediaType: 415, 171 | /** 172 | * The range specified by the Range header field in the request can't be fulfilled. 173 | * 174 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416) 175 | */ 176 | RangeNotSatisfiable: 416, 177 | /** 178 | * This response code means the expectation indicated by the Expect request header field can't be met by the server. 179 | * 180 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417) 181 | */ 182 | ExpectationFailed: 417, 183 | /** 184 | * The server refuses the attempt to brew coffee with a teapot. 185 | * 186 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418) 187 | */ 188 | ImATeapot: 418, 189 | /** 190 | * The request was directed at a server that is not able to produce a response. 191 | * 192 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/421) 193 | */ 194 | MisdirectedRequest: 421, 195 | /** 196 | * The request was well-formed but was unable to be followed due to semantic errors. 197 | * 198 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422) 199 | */ 200 | UnprocessableEntity: 422, 201 | /** 202 | * The resource that is being accessed is locked. 203 | * 204 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/423) 205 | */ 206 | Locked: 423, 207 | /** 208 | * The request failed due to failure of a previous request. 209 | * 210 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/424) 211 | */ 212 | FailedDependency: 424, 213 | /** 214 | * Indicates that the server is unwilling to risk processing a request that might be replayed. 215 | * 216 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/425) 217 | */ 218 | TooEarly: 425, 219 | /** 220 | * The server refuses to perform the request using the current protocol but might be willing to do so after the 221 | * client upgrades to a different protocol. 222 | * 223 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426) 224 | */ 225 | UpgradeRequired: 426, 226 | /** 227 | * The origin server requires the request to be conditional. 228 | * 229 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428) 230 | */ 231 | PreconditionRequired: 428, 232 | /** 233 | * The user has sent too many requests in a given amount of time ("rate limiting"). 234 | * 235 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) 236 | */ 237 | TooManyRequests: 429, 238 | /** 239 | * The server is unwilling to process the request because its header fields are too large. 240 | * 241 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431) 242 | */ 243 | RequestHeaderFieldsTooLarge: 431, 244 | /** 245 | * The user-agent requested a resource that cannot legally be provided, such as a web page censored by a government. 246 | * 247 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451) 248 | */ 249 | UnavailableForLegalReasons: 451, 250 | /** 251 | * The server has encountered a situation it doesn't know how to handle. 252 | * 253 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) 254 | */ 255 | InternalServerError: 500, 256 | /** 257 | * The request method is not supported by the server and cannot be handled. 258 | * 259 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501) 260 | */ 261 | NotImplemented: 501, 262 | /** 263 | * The server is acting as a gateway or proxy and received an invalid response from the upstream server. 264 | * 265 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) 266 | */ 267 | BadGateway: 502, 268 | /** 269 | * The server is not ready to handle the request. 270 | * 271 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) 272 | */ 273 | ServiceUnavailable: 503, 274 | /** 275 | * This error response is given when the server is acting as a gateway and cannot get a response in time. 276 | * 277 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) 278 | */ 279 | GatewayTimeout: 504, 280 | /** 281 | * The HTTP version used in the request is not supported by the server. 282 | * 283 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505) 284 | */ 285 | HTTPVersionNotSupported: 505, 286 | /** 287 | * The server has an internal configuration error: the chosen variant resource is configured to engage in 288 | * transparent content negotiation itself, and is therefore not a proper end point in the negotiation process. 289 | * 290 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506) 291 | */ 292 | VariantAlsoNegotiates: 506, 293 | /** 294 | * The server is unable to store the representation needed to complete the request. 295 | * 296 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507) 297 | */ 298 | InsufficientStorage: 507, 299 | /** 300 | * The server detected an infinite loop while processing the request. 301 | * 302 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508) 303 | */ 304 | LoopDetected: 508, 305 | /** 306 | * Further extensions to the request are required for the server to fulfill it. 307 | * 308 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510) 309 | */ 310 | NotExtended: 510, 311 | /** 312 | * The client needs to authenticate to gain network access. 313 | * 314 | * [Online Documentation at MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511) 315 | */ 316 | NetworkAuthenticationRequired: 511, 317 | }); 318 | -------------------------------------------------------------------------------- /src/headers.ts: -------------------------------------------------------------------------------- 1 | import type { FetchFnInit } from "./types.js"; 2 | 3 | /** 4 | * Defines all the possible data constructs that can be used to set HTTP headers in an 'init' configuration object. 5 | */ 6 | export type HeaderInput = 7 | Map> | 8 | [string, string][] | 9 | Record> | 10 | Headers; 11 | 12 | function* headerTuplesGenerator(headers: [string, string][]) { 13 | yield* headers; 14 | } 15 | 16 | function headerMapGenerator(headers: Map>) { 17 | return headers.entries(); 18 | } 19 | 20 | function* headersPojoGenerator(headers: Record>) { 21 | yield* Object.entries(headers); 22 | } 23 | 24 | function headersClassGenerator(headers: Headers) { 25 | return headers.entries(); 26 | } 27 | 28 | /** 29 | * Creates an iterator object that can be used to examine the contents of the provided headers source. 30 | * 31 | * Useful for troubleshooting or unit testing, and used internally by `setHeaders` because it reduces the many possible 32 | * ways to specify headers into to one: Tuples. Because it is an iterator, it can: 33 | * 34 | * + Be used in `for..of` statements 35 | * + Be spread using the spread (`...`) operator in arrays and parameters 36 | * + Be used in other generators via `yield*` 37 | * + Be destructured (array destructuring) 38 | * @param headers The source of the headers to enumerate. 39 | * @returns An iterator object that will enumerate every header contained in the source in the form of a tuple 40 | * `[header, value]`. 41 | * @example 42 | * ```typescript 43 | * const myHeaders1 = new Headers(); 44 | * myHeaders1.set('Accept', 'application/json'); 45 | * myHeaders1.set('Authorization', 'Bearer x'); 46 | * 47 | * const myHeaders2 = new Map(); 48 | * myHeaders2.set('Accept', 'application/json'); 49 | * myHeaders2.set('Authorization', 'Bearer x'); 50 | * 51 | * const myHeaders3 = { 52 | * 'Accept': 'application/json', 53 | * 'Authorization': 'Bearer x' 54 | * }; 55 | * 56 | * // The output of these is identical. 57 | * console.log([...makeIterableHeaders(myHeaders1)]); 58 | * console.log([...makeIterableHeaders(myHeaders2)]); 59 | * console.log([...makeIterableHeaders(myHeaders3)]); 60 | * ``` 61 | */ 62 | export function makeIterableHeaders(headers: HeaderInput) { 63 | const iterator = Array.isArray(headers) ? 64 | headerTuplesGenerator(headers) : 65 | headers instanceof Map ? 66 | headerMapGenerator(headers) : 67 | headers instanceof Headers ? 68 | headersClassGenerator(headers) : 69 | headersPojoGenerator(headers) 70 | ; 71 | return { 72 | [Symbol.iterator]() { 73 | return { 74 | next() { 75 | return iterator.next() 76 | } 77 | }; 78 | } 79 | }; 80 | } 81 | 82 | function setTupleHeaders(headers: [string, string][], newHeaders: HeaderInput) { 83 | for (let [key, value] of makeIterableHeaders(newHeaders)) { 84 | headers.push([key, Array.isArray(value) ? value.join(', ') : value as string]); 85 | } 86 | } 87 | 88 | function setHeadersInHeadersInstance(headers: Headers, newHeaders: HeaderInput) { 89 | for (let [key, value] of makeIterableHeaders(newHeaders)) { 90 | if (Array.isArray(value)) { 91 | for (let v of value) { 92 | headers.append(key, v); 93 | } 94 | } 95 | else { 96 | headers.set(key, value as string); 97 | } 98 | } 99 | } 100 | 101 | function setPojoHeaders(headers: Record, newHeaders: HeaderInput) { 102 | for (let [key, value] of makeIterableHeaders(newHeaders)) { 103 | headers[key] = Array.isArray(value) ? value.join(', ') : value as string; 104 | } 105 | } 106 | 107 | /** 108 | * Sets the provided HTTP headers into the `init.headers` property of the given `init` object. 109 | * 110 | * The function sets headers, and doesn't append values to existing headers. The only exception is when the new 111 | * headers are specified with a POJO or Map object, where the value can be an array of strings. In these cases, the 112 | * array of values are combined and this combination becomes the value of the header. 113 | * @param init The `init` object that will receive the specified headers. 114 | * @param headers The collection of headers to include in the `init` object. 115 | */ 116 | export function setHeaders(init: Exclude, headers: HeaderInput) { 117 | if (!init) { 118 | throw new Error("The 'init' argument cannot be undefined."); 119 | } 120 | init.headers ??= new Headers(); 121 | if (Array.isArray(init.headers)) { 122 | setTupleHeaders(init.headers, headers); 123 | } 124 | else if (init.headers instanceof Headers) { 125 | setHeadersInHeadersInstance(init.headers, headers); 126 | } 127 | else { 128 | setPojoHeaders(init.headers, headers); 129 | } 130 | } 131 | 132 | /** 133 | * Tests the given collection of headers to see if the specified header is present. 134 | * @param headers The headers to check. 135 | * @param header The sought header. The search is case-insensitive. 136 | * @returns `true` if the header is present in the headers, or `false` otherwise. 137 | */ 138 | export function hasHeader(headers: HeaderInput, header: string) { 139 | const lcHeader = header.toLowerCase(); 140 | for (let [key] of makeIterableHeaders(headers)) { 141 | if (key.toLowerCase() === lcHeader) { 142 | return true; 143 | } 144 | } 145 | return false; 146 | } 147 | 148 | /** 149 | * Gets the value of the specified header from the given collection of headers. 150 | * @param headers The headers to check. 151 | * @param header The sought header. The search is case-insensitive. 152 | * @returns The value of the header, or `undefined` if the header is not present in the headers. 153 | */ 154 | export function getHeader(headers: HeaderInput, header: string) { 155 | const lcHeader = header.toLowerCase(); 156 | for (let [key, value] of makeIterableHeaders(headers)) { 157 | if (key.toLowerCase() === lcHeader) { 158 | return value; 159 | } 160 | } 161 | return undefined; 162 | } 163 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DrFetch.js'; 2 | export * from './headers.js'; 3 | export * from './StatusCodes.js'; 4 | export type * from './types.js'; 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List of all possible OK status codes (2xx). 3 | */ 4 | export type OkStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226; 5 | 6 | /** 7 | * List of all possible client-sided error status codes (4xx). 8 | */ 9 | export type ClientErrorStatusCode = 400 | 401 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 10 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451; 11 | 12 | /** 13 | * List of all possible server-sided error status codes (5xx). 14 | */ 15 | export type ServerErrorStatusCode = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511; 16 | 17 | /** 18 | * List of all possible status codes (2xx + 4xx + 5xx). 19 | */ 20 | export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; 21 | 22 | /** 23 | * List of all possible non-OK status codes (4xx + 5xx). 24 | */ 25 | export type NonOkStatusCode = Exclude; 26 | 27 | /** 28 | * Type that represents a fetch response's body parser function. 29 | */ 30 | export type BodyParserFn = (response: Response) => Promise; 31 | 32 | /** 33 | * Type that builds a single status code's response. 34 | */ 35 | type CoreFetchResult = { 36 | /** 37 | * Indicates whether the request was aborted. 38 | */ 39 | aborted: false; 40 | /** 41 | * Indicates whether the request was successful (2xx). 42 | */ 43 | ok: TStatus extends OkStatusCode ? true : false; 44 | /** 45 | * The status code of the response. 46 | */ 47 | status: TStatus; 48 | /** 49 | * The status text of the response. 50 | */ 51 | statusText: string; 52 | /** 53 | * The HTTP response headers. 54 | */ 55 | headers: Headers; 56 | } & (TBody extends undefined ? {} : { 57 | /** 58 | * The parsed body obtained from the response. 59 | */ 60 | body: TBody; 61 | }); 62 | 63 | /** 64 | * Type that defines the result of an aborted fetch request. 65 | */ 66 | export type AbortedFetchResult = { 67 | /** 68 | * Indicates whether the request was aborted. 69 | */ 70 | aborted: true; 71 | /** 72 | * The error that caused the request to be aborted. Useful to examine the `cause` property of the error to 73 | * determine the reason for the abortion, if your implementation needs this. 74 | */ 75 | error: DOMException; 76 | } 77 | 78 | /** 79 | * Type that builds DrFetch's final result object's type. 80 | */ 81 | export type FetchResult = 82 | ( 83 | unknown extends T ? 84 | CoreFetchResult : 85 | T | CoreFetchResult 86 | ) extends infer R ? R : never; 87 | 88 | /** 89 | * Type of the fetch function's URL parameter. 90 | */ 91 | export type FetchFnUrl = Parameters[0]; 92 | 93 | /** 94 | * Possible types of keys accepted by the `autoAbort` option. 95 | */ 96 | export type AutoAbortKey = string | symbol | number; 97 | 98 | /** 99 | * Type of the fetch function's init parameter. 100 | */ 101 | export type FetchFnInit = Parameters[1] & { 102 | /** 103 | * Specifies the options for auto-aborting the HTTP request. 104 | * 105 | * If a string is provided, it will be used as the key for the abort signal; provide an object to specify further 106 | * options. 107 | */ 108 | autoAbort?: AutoAbortKey | { 109 | /** 110 | * The key used to identify the abort signal. 111 | */ 112 | key: AutoAbortKey; 113 | /** 114 | * The amount of time (in milliseconds) to wait before emitting the request. If not specified, then no delay 115 | * is applied. 116 | */ 117 | delay?: number; 118 | }; 119 | }; 120 | 121 | /** 122 | * Type of the stock fetch function. 123 | */ 124 | export type FetchFn = (url: FetchFnUrl, init?: TInit) => Promise; 125 | 126 | /** 127 | * Fetcher cloning options. 128 | */ 129 | export type CloneOptions = { 130 | /** 131 | * Determines whether to preserve the body typing of the original fetcher. The default is `true`. 132 | */ 133 | preserveTyping?: BodyTyping; 134 | /** 135 | * Defines which data-fetching function the clone will use. 136 | * 137 | * Pass `false` if you want the clone to use the standard `fetch()` function, or leave it `undefined` to inherit 138 | * the fetch function of the parent. 139 | */ 140 | fetchFn?: FetchFn | false; 141 | /** 142 | * Determines if body processors are included in the clone. The default is `true`. 143 | */ 144 | includeProcessors?: boolean; 145 | /** 146 | * Defines whether to preserve the abortable state of the original fetcher. The default is `true`. 147 | */ 148 | preserveAbortable?: Abortable; 149 | }; 150 | 151 | /** 152 | * Defines the possible data types that can be used to install custom body processors. 153 | */ 154 | export type ProcessorPattern = string | RegExp | (string | RegExp)[] | ((response: Response, contentType: string) => boolean); 155 | -------------------------------------------------------------------------------- /tests/DrFetch.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { describe, test } from "mocha"; 3 | import { fake } from 'sinon'; 4 | import { DrFetch } from "../src/DrFetch.js"; 5 | import type { FetchFnInit, FetchFnUrl, StatusCode } from "../src/types.js"; 6 | import { getHeader } from "../src/headers.js"; 7 | 8 | const shortcutMethodsWithBody = [ 9 | 'post', 10 | 'put', 11 | 'patch', 12 | 'delete', 13 | ] as const; 14 | 15 | const allShortcutMethods = [ 16 | 'get', 17 | 'head', 18 | ...shortcutMethodsWithBody 19 | ] as const; 20 | 21 | describe('DrFetch', () => { 22 | describe('clone()', () => { 23 | [ 24 | { 25 | newFetchFn: false, 26 | includeProcessors: false, 27 | text: 'the same data-fetching function and no processors', 28 | }, 29 | { 30 | newFetchFn: true, 31 | includeProcessors: false, 32 | text: 'a new data-fetching function and no processors', 33 | }, 34 | { 35 | newFetchFn: false, 36 | includeProcessors: true, 37 | text: 'the same data-fetching function and identical processors', 38 | }, 39 | { 40 | newFetchFn: true, 41 | includeProcessors: true, 42 | text: 'a new data-fetching function and identical processors', 43 | }, 44 | ].forEach(tc => { 45 | test(`Should create a new fetcher object with ${tc.text}.`, async () => { 46 | // Arrange. 47 | const contentType = 'text/plain'; 48 | const origFetchFn = fake.resolves(new Response('Hi!', { headers: { 'content-type': contentType } })); 49 | const customProcessorFn = fake(); 50 | const origFetcher = new DrFetch(origFetchFn); 51 | origFetcher.withProcessor(contentType, customProcessorFn); 52 | const newFetchFn = fake.resolves(new Response('Hi!', { headers: { 'content-type': contentType } })); 53 | 54 | // Act. 55 | const cloned = origFetcher.clone({ 56 | fetchFn: tc.newFetchFn ? newFetchFn : undefined, 57 | includeProcessors: tc.includeProcessors 58 | }); 59 | 60 | // Assert. 61 | await cloned.fetch('x'); 62 | expect(newFetchFn.called).to.equal(tc.newFetchFn); 63 | expect(customProcessorFn.called).to.equal(tc.includeProcessors); 64 | }); 65 | }); 66 | test("Should create a clone that uses the standard fetch() function when 'options.fetchFn' is 'false'.", async () => { 67 | const fetchFn = fake.resolves(new Response(null)); 68 | const origFetch = globalThis.fetch; 69 | const fetchFake = fake.resolves(new Response(null)); 70 | globalThis.fetch = fetchFake; 71 | const fetcher = new DrFetch(fetchFn); 72 | 73 | // Act. 74 | const clone = fetcher.clone({ fetchFn: false }); 75 | 76 | // Assert. 77 | try { 78 | await clone.fetch('x'); 79 | } 80 | finally { 81 | globalThis.fetch = origFetch; 82 | } 83 | expect(fetchFn.calledOnce).to.be.false; 84 | expect(fetchFake.calledOnce).to.be.true; 85 | }); 86 | }); 87 | describe('fetch()', () => { 88 | test("Should call the stock fetch() function when the fetcher is built without a custom one.", async () => { 89 | // Arrange. 90 | const origFetch = globalThis.fetch; 91 | const fakeFetch = fake.resolves(new Response(null)); 92 | globalThis.fetch = fakeFetch; 93 | const fetcher = new DrFetch(); 94 | 95 | // Act. 96 | try { 97 | await fetcher.fetch('x'); 98 | } 99 | finally { 100 | globalThis.fetch = origFetch; 101 | } 102 | 103 | // Assert. 104 | expect(fakeFetch.called).to.be.true; 105 | }); 106 | test("Should call the provided data-fetching function.", async () => { 107 | // Arrange. 108 | const fakeFetch = fake.resolves(new Response(null)); 109 | const fetcher = new DrFetch(fakeFetch); 110 | 111 | // Act. 112 | await fetcher.fetch('x'); 113 | 114 | // Assert. 115 | expect(fakeFetch.called).to.be.true; 116 | }); 117 | [ 118 | { 119 | contentType: 'application/json', 120 | body: { 121 | a: 'hello', 122 | }, 123 | }, 124 | { 125 | contentType: 'application/ld+json', 126 | body: { 127 | a: 'hello', 128 | b: true, 129 | } 130 | }, 131 | { 132 | contentType: 'application/problem+json', 133 | body: { 134 | a: 'hello', 135 | b: true, 136 | c: { 137 | h: 123, 138 | }, 139 | } 140 | }, 141 | { 142 | contentType: 'text/plain', 143 | body: "Plain.", 144 | }, 145 | { 146 | contentType: 'text/csv', 147 | body: "a,b,c\n1,2,3", 148 | }, 149 | ].map(x => ({ 150 | contentType: x.contentType, 151 | body: typeof x.body === 'string' ? x.body : JSON.stringify(x.body), 152 | testBody: x.body, 153 | })).forEach(tc => { 154 | test(`Should parse the body using the stock body processors when content type is "${tc.contentType}".`, async () => { 155 | // Arrange. 156 | const fetchFn = fake.resolves(new Response(tc.body, { headers: { 'content-type': tc.contentType } })); 157 | const fetcher = new DrFetch(fetchFn); 158 | 159 | // Act. 160 | const result = await fetcher.for<200, string | object>().fetch('x'); 161 | 162 | // Assert. 163 | if (typeof tc.testBody === 'object') { 164 | expect(result.body).to.deep.equal(tc.testBody); 165 | } 166 | else { 167 | expect(result.body).to.equal(tc.testBody); 168 | } 169 | }); 170 | }); 171 | test("Should return 'null' as body whenever the response carries no body.", async () => { 172 | // Arrange. 173 | const fetchFn = fake.resolves(new Response()); 174 | const fetcher = new DrFetch(fetchFn); 175 | 176 | // Act. 177 | const response = await fetcher.for<200, string>().fetch('x'); 178 | 179 | // Assert. 180 | expect(response.body).to.be.null; 181 | }); 182 | test("Should throw whenever the response carries no content-type header.", async () => { 183 | // Arrange. 184 | const response = new Response('x'); 185 | response.headers.delete('content-type'); 186 | const fetchFn = fake.resolves(response); 187 | const fetcher = new DrFetch(fetchFn); 188 | let didThrow = false; 189 | 190 | // Act. 191 | try { 192 | await fetcher.for<200, string>().fetch('x'); 193 | } 194 | catch { 195 | didThrow = true; 196 | } 197 | 198 | // Assert. 199 | expect(didThrow).to.be.true; 200 | }); 201 | test("Should throw an error if the content type is unknown by the built-in and custom processors.", async () => { 202 | // Arrange. 203 | const fetchFn = fake.resolves(new Response('x', { headers: { 'content-type': 'application/xml' } })); 204 | const fetcher = new DrFetch(fetchFn); 205 | let didThrow = false; 206 | 207 | // Act. 208 | try { 209 | await fetcher.fetch('x'); 210 | } 211 | catch { 212 | didThrow = true; 213 | } 214 | 215 | // Assert. 216 | expect(didThrow).to.be.true; 217 | }); 218 | [ 219 | { 220 | pattern: 'xml', 221 | patternType: 'string', 222 | contentTypes: [ 223 | 'application/xml', 224 | 'application/custom+xml', 225 | 'text/xml', 226 | 'xml', 227 | 'text/xml; encoding: utf-8' 228 | ], 229 | }, 230 | { 231 | pattern: /^application\/([\w+-]*)xml/, 232 | patternType: 'regular expression', 233 | contentTypes: [ 234 | 'application/xml', 235 | 'application/custom+xml', 236 | 'application/custom+xml; encoding: utf-8', 237 | 'application/hyper-xml', 238 | ], 239 | }, 240 | ].flatMap(x => { 241 | const expanded: (Omit<(typeof x), 'contentTypes'> & { contentType: string; })[] = []; 242 | for (let ct of x.contentTypes) { 243 | expanded.push({ 244 | pattern: x.pattern, 245 | patternType: x.patternType, 246 | contentType: ct 247 | }); 248 | } 249 | return expanded; 250 | }).forEach(tc => { 251 | test(`Should use the provided custom processor with ${tc.patternType} pattern "${tc.pattern.toString()}" for content type "${tc.contentType}".`, async () => { 252 | // Arrange. 253 | const processorFn = fake(); 254 | const fetchFn = fake.resolves(new Response('x', { headers: { 'content-type': tc.contentType } })); 255 | const fetcher = new DrFetch(fetchFn); 256 | fetcher.withProcessor(tc.pattern, processorFn); 257 | 258 | // Act. 259 | await fetcher.fetch('x'); 260 | 261 | // Assert. 262 | expect(processorFn.calledOnce).to.be.true; 263 | }); 264 | }); 265 | test("Should throw and error if 'autoAbort' is used without calling 'abortable()'.", async () => { 266 | // Arrange. 267 | const fetchFn = fake.resolves(new Response(null)); 268 | const fetcher = new DrFetch(fetchFn); 269 | let didThrow = false; 270 | 271 | // Act. 272 | try { 273 | await fetcher.fetch('x', { autoAbort: 'abc' }); 274 | } 275 | catch { 276 | didThrow = true; 277 | } 278 | 279 | // Assert. 280 | expect(didThrow).to.be.true; 281 | }); 282 | test("Should not throw an error if 'autoAbort' is used with 'abortable()'.", async () => { 283 | // Arrange. 284 | const fetchFn = fake.resolves(new Response(null)); 285 | const fetcher = new DrFetch(fetchFn).abortable(); 286 | let didThrow = false; 287 | 288 | // Act. 289 | try { 290 | await fetcher.fetch('x', { autoAbort: 'abc' }); 291 | } 292 | catch { 293 | didThrow = true; 294 | } 295 | 296 | // Assert. 297 | expect(didThrow).to.be.false; 298 | }); 299 | [ 300 | { 301 | autoAbort: 'abc', 302 | text: 'a string', 303 | }, 304 | { 305 | autoAbort: { key: 'abc' }, 306 | text: 'an object', 307 | }, 308 | ].forEach(tc => { 309 | test(`Should abort the previous HTTP request whenever 'autoAbort' is used as ${tc.text}.`, async () => { 310 | // Arrange. 311 | const fetchFn = fake((url: FetchFnUrl, init?: FetchFnInit) => new Promise((rs, rj) => setTimeout(() => { 312 | if (init?.signal?.aborted) { 313 | rj(new DOMException('Test: Aborted.', 'AbortError')); 314 | } 315 | rs(new Response(null)); 316 | }, 0))); 317 | const fetcher = new DrFetch(fetchFn).abortable().for(); 318 | const request1 = fetcher.fetch('x', { autoAbort: tc.autoAbort }); 319 | const request2 = fetcher.fetch('y', { autoAbort: tc.autoAbort }); 320 | 321 | // Act. 322 | const response = await request1; 323 | 324 | // Assert. 325 | expect(fetchFn.calledTwice).to.be.true; 326 | expect(response.aborted).to.be.true; 327 | expect(response.aborted && response.error).to.be.instanceOf(DOMException); 328 | expect(response.aborted && response.error.name).to.equal('AbortError'); 329 | 330 | // Clean up. 331 | await request2; 332 | }); 333 | }); 334 | test("Should delay the HTTP request whenever a delay is specified.", async () => { 335 | // Arrange. 336 | const fetchFn = fake((url: FetchFnUrl, init?: FetchFnInit) => new Promise((rs, rj) => setTimeout(() => { 337 | if (init?.signal?.aborted) { 338 | rj(new DOMException('Test: Aborted.', 'AbortError')); 339 | } 340 | rs(new Response(null)); 341 | }, 0))); 342 | const fetcher = new DrFetch(fetchFn).abortable().for(); 343 | const request1 = fetcher.fetch('x', { autoAbort: { key: 'abc', delay: 0 } }); 344 | const request2 = fetcher.fetch('y', { autoAbort: { key: 'abc', delay: 0 } }); 345 | // Act. 346 | const response = await request1; 347 | 348 | // Assert. 349 | expect(fetchFn.called, 'Fetch was called.').to.be.false; 350 | expect(response.aborted, 'Response is not aborted.').to.be.true; 351 | expect(response.aborted && response.error).to.be.instanceOf(DOMException); 352 | expect(response.aborted && response.error.name).to.equal('AbortError'); 353 | 354 | // Clean up. 355 | await request2; 356 | }); 357 | }); 358 | describe('Shortcut Functions', () => { 359 | allShortcutMethods.map(x => ({ 360 | shortcutFn: x, 361 | expectedMethod: x.toUpperCase() 362 | })).forEach(tc => { 363 | test(`${tc.shortcutFn}(): Should perform a fetch() call with the '${tc.expectedMethod}' method.`, async () => { 364 | // Arrange. 365 | const fetchFn = fake.resolves(new Response()); 366 | const fetcher = new DrFetch(fetchFn); 367 | 368 | // Act. 369 | await fetcher[tc.shortcutFn]('x'); 370 | 371 | // Assert. 372 | expect(fetchFn.calledOnce).to.be.true; 373 | expect(fetchFn.args[0][1]['method']).to.equal(tc.expectedMethod); 374 | }); 375 | }); 376 | shortcutMethodsWithBody.forEach(method => { 377 | test(`${method}(): Should stringify the body argument when said argument is a POJO object.`, async () => { 378 | // Arrange. 379 | const body = { a: 'hi' }; 380 | const fetchFn = fake.resolves(new Response()); 381 | const fetcher = new DrFetch(fetchFn); 382 | 383 | // Act. 384 | await fetcher[method]('x', body); 385 | 386 | // Assert. 387 | expect(fetchFn.calledOnce).to.be.true; 388 | expect(fetchFn.args[0][1]['body']).to.equal(JSON.stringify(body)); 389 | expect((fetchFn.args[0][1]['headers'] as Headers).get('content-type')).to.equal('application/json'); 390 | }); 391 | }); 392 | shortcutMethodsWithBody.forEach(method => { 393 | test(`${method}(): Should stringify the body argument when said argument is an array.`, async () => { 394 | // Arrange. 395 | const body = [{ a: 'hi' }]; 396 | const fetchFn = fake.resolves(new Response()); 397 | const fetcher = new DrFetch(fetchFn); 398 | 399 | // Act. 400 | await fetcher[method]('x', body); 401 | 402 | // Assert. 403 | expect(fetchFn.calledOnce).to.be.true; 404 | expect(fetchFn.args[0][1]['body']).to.equal(JSON.stringify(body)); 405 | expect((fetchFn.args[0][1]['headers'] as Headers).get('content-type')).to.equal('application/json'); 406 | }); 407 | }); 408 | shortcutMethodsWithBody.flatMap(method => [ 409 | { 410 | body: new ReadableStream(), 411 | text: 'a readable stream', 412 | }, 413 | { 414 | body: new Blob(), 415 | text: 'a blob', 416 | }, 417 | { 418 | body: new ArrayBuffer(8), 419 | text: 'an array buffer', 420 | }, 421 | { 422 | body: new FormData(), 423 | text: 'a form data object', 424 | }, 425 | { 426 | body: new URLSearchParams(), 427 | text: 'a URL search params object', 428 | }, 429 | { 430 | body: 'abc', 431 | text: 'a string' 432 | } 433 | ].map(body => ({ 434 | method, 435 | body 436 | }))).forEach(tc => { 437 | test(`${tc.method}(): Should not stringify the body when said argument is ${tc.body.text}.`, async () => { 438 | // Arrange. 439 | const fetchFn = fake.resolves(new Response()); 440 | const fetcher = new DrFetch(fetchFn); 441 | 442 | // Act. 443 | await fetcher[tc.method]('x', tc.body.body); 444 | 445 | // Assert. 446 | expect(fetchFn.calledOnce).to.be.true; 447 | expect(fetchFn.args[0][1]['body']).to.equal(tc.body.body); 448 | }); 449 | }); 450 | shortcutMethodsWithBody.forEach(method => { 451 | test(`${method}(): Should not add or change the content-type header when a content type is pre-specified.`, async () => { 452 | // Arrange. 453 | const fetchFn = fake.resolves(new Response()); 454 | const fetcher = new DrFetch(fetchFn); 455 | 456 | // Act. 457 | await fetcher[method]('x', { a: 1 }, { headers: { 'content-type': 'text/plain' } }); 458 | 459 | // Assert. 460 | expect(fetchFn.calledOnce).to.be.true; 461 | expect(getHeader(fetchFn.args[0][1]['headers'], 'content-type')).to.equal('text/plain'); 462 | }); 463 | }); 464 | shortcutMethodsWithBody.forEach(method => { 465 | test(`${method}(): Should pass the init object to the fetch() function.`, async () => { 466 | // Arrange. 467 | const fetchFn = fake.resolves(new Response()); 468 | const fetcher = new DrFetch(fetchFn); 469 | const init = { 470 | headers: { 'x-test': 'abc' }, 471 | signal: new AbortController().signal, 472 | mode: 'cors' as const, 473 | credentials: 'include' as const, 474 | redirect: 'follow' as const, 475 | referrer: 'test', 476 | referrerPolicy: 'no-referrer' as const, 477 | integrity: 'sha256-abc' 478 | }; 479 | 480 | // Act. 481 | await fetcher[method]('x', { a: 1 }, init); 482 | 483 | // Assert. 484 | expect(fetchFn.calledOnce).to.be.true; 485 | Object.entries(init).forEach(([key, value]) => { 486 | expect(fetchFn.args[0][1][key]).to.equal(value); 487 | }); 488 | }); 489 | }); 490 | }); 491 | describe('withProcessor()', () => { 492 | [ 493 | { 494 | text1: 'select', 495 | text2: 'a string', 496 | pattern: 'text/plain', 497 | }, 498 | { 499 | text1: 'select', 500 | text2: 'a regular expression', 501 | pattern: /text\/plain/i, 502 | }, 503 | { 504 | text1: 'select', 505 | text2: 'an array of strings', 506 | pattern: ["application/json", "text/plain"], 507 | }, 508 | { 509 | text1: 'select', 510 | text2: 'an array of regular expressions', 511 | pattern: [/application\/json/i, /text\/plain/i], 512 | }, 513 | { 514 | text1: 'select', 515 | text2: 'an array with strings and regular expressions', 516 | pattern: ["application/json", /text\/plain/i], 517 | }, 518 | { 519 | text1: 'select', 520 | text2: 'a predicate function', 521 | pattern: (_: Response, c: string) => c === 'text/plain', 522 | }, 523 | { 524 | text1: 'skip', 525 | text2: 'a string', 526 | pattern: 'text/csv', 527 | }, 528 | { 529 | text1: 'skip', 530 | text2: 'a regular expression', 531 | pattern: /text\/csv/i, 532 | }, 533 | { 534 | text1: 'skip', 535 | text2: 'an array of strings', 536 | pattern: ["application/json", "text/csv"], 537 | }, 538 | { 539 | text1: 'skip', 540 | text2: 'an array of regular expressions', 541 | pattern: [/application\/json/i, /text\/csv/i], 542 | }, 543 | { 544 | text1: 'skip', 545 | text2: 'an array with strings and regular expressions', 546 | pattern: ["application/json", /text\/csv/i], 547 | }, 548 | { 549 | text1: 'skip', 550 | text2: 'a predicate function', 551 | pattern: (_: Response, c: string) => c === 'text/csv', 552 | }, 553 | ].forEach(tc => { 554 | test(`Should appropriately ${tc.text1} the custom processor when the specified pattern is ${tc.text2}.`, async () => { 555 | // Arrange. 556 | const fetchFn = fake.resolves(new Response('x', { headers: { 'content-type': 'text/plain' } })); 557 | const fetcher = new DrFetch(fetchFn); 558 | const processorFn = fake(); 559 | 560 | // Act. 561 | fetcher.withProcessor(tc.pattern, processorFn); 562 | await fetcher.fetch('x'); 563 | 564 | // Assert. 565 | expect(processorFn.calledOnce).to.equal(tc.text1 === 'select'); 566 | }); 567 | }); 568 | }); 569 | describe('abortable()', () => { 570 | test("Should modify the fetcher object so it supports abortable HTTP requests.", async () => { 571 | // Arrange. 572 | const abortController = new AbortController(); 573 | const fetchFn = fake(() => { 574 | if (abortController.signal.aborted) { 575 | throw new DOMException('Test: Aborted.', 'AbortError'); 576 | } 577 | return Promise.resolve(new Response()); 578 | }); 579 | const fetcher = new DrFetch(fetchFn).abortable().for(); 580 | abortController.abort(); 581 | const responsePromise = fetcher.fetch('x', { signal: abortController.signal }); 582 | let didThrow = false; 583 | let response: Awaited>; 584 | 585 | // Act. 586 | try { 587 | response = await responsePromise; 588 | } 589 | catch { 590 | didThrow = true; 591 | } 592 | 593 | // Assert. 594 | expect(didThrow, "Exception thrown.").to.be.false; 595 | expect(response!.aborted, "Aborted is not properly set.").to.be.true; 596 | }); 597 | test("Should make clone() return a fetcher that is also abortable.", async () => { 598 | // Arrange. 599 | const abortController = new AbortController(); 600 | const fetchFn = fake(() => { 601 | if (abortController.signal.aborted) { 602 | throw new DOMException('Test: Aborted.', 'AbortError'); 603 | } 604 | return Promise.resolve(new Response()); 605 | }); 606 | const fetcher = new DrFetch(fetchFn).abortable().for().clone(); 607 | abortController.abort(); 608 | const responsePromise = fetcher.fetch('x', { signal: abortController.signal }); 609 | let didThrow = false; 610 | let response: Awaited>; 611 | 612 | // Act. 613 | try { 614 | response = await responsePromise; 615 | } 616 | catch { 617 | didThrow = true; 618 | } 619 | 620 | // Assert. 621 | expect(didThrow).to.be.false; 622 | expect(response!.aborted).to.be.true; 623 | }); 624 | }); 625 | }); 626 | -------------------------------------------------------------------------------- /tests/headers.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, test } from 'mocha'; 3 | import { getHeader, hasHeader, makeIterableHeaders, setHeaders } from '../src/headers.js'; 4 | 5 | describe('setHeaders', () => { 6 | test("Should throw an error whenever the 'init' argument is undefined.", () => { 7 | // Act. 8 | // @ts-expect-error Undefined is an invalid argument for 'init'. 9 | const act = () => setHeaders(undefined, {}); 10 | 11 | // Assert. 12 | expect(act).to.throw; 13 | }); 14 | [ 15 | { 16 | init: { headers: new Headers() }, 17 | destinationType: 'a Headers instance', 18 | }, 19 | { 20 | init: { headers: {} }, 21 | destinationType: 'POJO', 22 | }, 23 | { 24 | init: { headers: [] }, 25 | destinationType: 'an array of tuples', 26 | }, 27 | ].flatMap(d => { 28 | const headersMap1 = new Map(); 29 | headersMap1.set('Accept', 'application/json'); 30 | const headersMap2 = new Map(); 31 | headersMap2.set('Accept', 'application/json'); 32 | headersMap2.set('Authorization', 'Bearer let-me-in'); 33 | const headers1 = new Headers(); 34 | headers1.set('Accept', 'application/json'); 35 | const headers2 = new Headers(); 36 | headers2.set('Accept', 'application/json'); 37 | headers2.set('Authorization', 'Bearer let-me-in'); 38 | return [ 39 | { 40 | headers: [ 41 | ['Accept', 'application/json'], 42 | ] satisfies [string, string][], 43 | headersType: 'an array of tuples', 44 | text: '1 header', 45 | headerCount: 1, 46 | }, 47 | { 48 | headers: [ 49 | ['Accept', 'application/json'], 50 | ['Authorization', 'Bearer let-me-in'], 51 | ] as [string, string][], 52 | headersType: 'an array of tuples', 53 | text: '2 headers', 54 | headerCount: 2, 55 | }, 56 | { 57 | headers: { 58 | 'Accept': 'application/json' 59 | } as Record, 60 | headersType: 'a POJO', 61 | text: '1 header', 62 | headerCount: 1, 63 | }, 64 | { 65 | headers: { 66 | 'Accept': 'application/json', 67 | 'Authorization': 'Bearer let-me-in', 68 | } satisfies Record, 69 | headersType: 'a POJO', 70 | text: '2 headers', 71 | headerCount: 2, 72 | }, 73 | { 74 | headers: headers1, 75 | headersType: 'a Headers object', 76 | text: '1 header', 77 | headerCount: 1, 78 | }, 79 | { 80 | headers: headers2, 81 | headersType: 'a Headers object', 82 | text: '2 headers', 83 | headerCount: 2, 84 | }, 85 | { 86 | headers: headersMap1, 87 | headersType: 'a Map object', 88 | text: '1 header', 89 | headerCount: 1, 90 | }, 91 | { 92 | headers: headersMap2, 93 | headersType: 'a Map object', 94 | text: '2 headers', 95 | headerCount: 2, 96 | }, 97 | ].map(s => ({ 98 | ...d, 99 | ...s, 100 | })); 101 | }).forEach(tc => { 102 | test(`Should add the specified ${tc.text} to the 'init' object when specified as ${tc.headersType} and stored in ${tc.destinationType}.`, () => { 103 | // Arrange. 104 | // Clean up the destination. Needed because of how the test cases are set up with flatMap(). 105 | if (Array.isArray(tc.init.headers)) { 106 | tc.init.headers.length = 0; 107 | } 108 | else if (tc.init.headers instanceof Headers) { 109 | for (let key of [...tc.init.headers.keys()]) { 110 | tc.init.headers.delete(key); 111 | } 112 | } 113 | else { 114 | tc.init.headers = {}; 115 | } 116 | 117 | // Act. 118 | setHeaders(tc.init, tc.headers); 119 | 120 | // Assert. 121 | const resultingHeaders = [...makeIterableHeaders(tc.init.headers)]; 122 | expect(resultingHeaders.length).to.equal(tc.headerCount); 123 | }); 124 | }); 125 | const headerKey = 'Accept'; 126 | const headerValues = [ 127 | 'application/json', 128 | 'application/xml', 129 | ]; 130 | [ 131 | { 132 | source: new Map>([[headerKey, headerValues]]), 133 | sourceText: 'a Map object', 134 | }, 135 | { 136 | source: { 137 | [headerKey]: headerValues 138 | }, 139 | sourceText: 'a POJO object', 140 | }, 141 | ].flatMap(s => [ 142 | { 143 | destination: new Headers(), 144 | text: 'a Headers object', 145 | }, 146 | { 147 | destination: {} satisfies Record>, 148 | text: 'a POJO object', 149 | }, 150 | { 151 | destination: [] satisfies [string, string][], 152 | text: 'an array of tuples', 153 | }, 154 | ].map(d => ({ ...s, ...d }))).forEach(tc => { 155 | test(`Should combine multiple values of the same header from ${tc.sourceText} when the destination is ${tc.text}.`, () => { 156 | // Arrange. 157 | const init = { 158 | headers: tc.destination, 159 | }; 160 | 161 | // Act. 162 | setHeaders(init, tc.source); 163 | 164 | // Assert. 165 | const resultingHeaders = [...makeIterableHeaders(init.headers)]; 166 | expect(resultingHeaders.length).to.equal(1); 167 | expect(resultingHeaders[0][1]).to.equal(headerValues.join(', ')); 168 | }); 169 | }); 170 | }); 171 | 172 | describe('hasHeader', () => { 173 | [ 174 | { 175 | headers: new Map([['Accept', 'application/json']]), 176 | text: 'a Map object', 177 | header: 'Accept', 178 | exists: true, 179 | }, 180 | { 181 | headers: new Map([['Accept', 'application/json']]), 182 | text: 'a Map object', 183 | header: 'Authorization', 184 | exists: false, 185 | }, 186 | { 187 | headers: new Headers([['Accept', 'application/json']]), 188 | text: 'a Headers object', 189 | header: 'Accept', 190 | exists: true, 191 | }, 192 | { 193 | headers: new Headers([['Accept', 'application/json']]), 194 | text: 'a Headers object', 195 | header: 'Authorization', 196 | exists: false, 197 | }, 198 | { 199 | headers: { Accept: 'application/json' }, 200 | text: 'a POJO object', 201 | header: 'Accept', 202 | exists: true, 203 | }, 204 | { 205 | headers: { Accept: 'application/json' }, 206 | text: 'a POJO object', 207 | header: 'Authorization', 208 | exists: false, 209 | }, 210 | { 211 | headers: [['Accept', 'application/json'] as [string, string]], 212 | text: 'an array of tuples', 213 | header: 'Accept', 214 | exists: true, 215 | }, 216 | { 217 | headers: [['Accept', 'application/json'] as [string, string]], 218 | text: 'an array of tuples', 219 | header: 'Authorization', 220 | exists: false, 221 | }, 222 | ].forEach(tc => { 223 | test(`Should return ${tc.exists} when checking for the header ${tc.header} in ${tc.text}.`, () => { 224 | // Act. 225 | const result = hasHeader(tc.headers, tc.header); 226 | 227 | // Assert. 228 | expect(result).to.equal(tc.exists); 229 | }); 230 | }); 231 | ["accept", "ACCEPT", "Accept"].forEach(header => { 232 | test(`Should be case-insensitive when searching for the header '${header}'.`, () => { 233 | // Arrange. 234 | const headers = new Headers([['Accept', 'application/json']]); 235 | 236 | // Act. 237 | const result = hasHeader(headers, header); 238 | 239 | // Assert. 240 | expect(result).to.be.true; 241 | }); 242 | }); 243 | }); 244 | 245 | describe('getHeader', () => { 246 | [ 247 | { 248 | headers: new Map([['Accept', 'application/json']]), 249 | text: 'a Map object', 250 | header: 'Accept', 251 | expected: 'application/json', 252 | }, 253 | { 254 | headers: new Map([['Accept', 'application/json']]), 255 | text: 'a Map object', 256 | header: 'Authorization', 257 | expected: undefined, 258 | }, 259 | { 260 | headers: new Headers([['Accept', 'application/json']]), 261 | text: 'a Headers object', 262 | header: 'Accept', 263 | expected: 'application/json', 264 | }, 265 | { 266 | headers: new Headers([['Accept', 'application/json']]), 267 | text: 'a Headers object', 268 | header: 'Authorization', 269 | expected: undefined, 270 | }, 271 | { 272 | headers: { Accept: 'application/json' }, 273 | text: 'a POJO object', 274 | header: 'Accept', 275 | expected: 'application/json', 276 | }, 277 | { 278 | headers: { Accept: 'application/json' }, 279 | text: 'a POJO object', 280 | header: 'Authorization', 281 | expected: undefined, 282 | }, 283 | { 284 | headers: [['Accept', 'application/json'] as [string, string]], 285 | text: 'an array of tuples', 286 | header: 'Accept', 287 | expected: 'application/json', 288 | }, 289 | { 290 | headers: [['Accept', 'application/json'] as [string, string]], 291 | text: 'an array of tuples', 292 | header: 'Authorization', 293 | expected: undefined, 294 | }, 295 | ].forEach(tc => { 296 | test(`Should return '${tc.expected}' when getting header ${tc.header} from ${tc.text}.`, () => { 297 | // Act. 298 | const result = getHeader(tc.headers, tc.header); 299 | 300 | // Assert. 301 | expect(result).to.equal(tc.expected); 302 | }); 303 | }); 304 | ["accept", "ACCEPT", "Accept"].forEach(header => { 305 | test(`Should be case-insensitive when getting the header '${header}'.`, () => { 306 | // Arrange. 307 | const headers = new Headers([['Accept', 'application/json']]); 308 | 309 | // Act. 310 | const result = getHeader(headers, header); 311 | 312 | // Assert. 313 | expect(result).to.equal('application/json'); 314 | }); 315 | }); 316 | }); 317 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, test } from 'mocha'; 3 | 4 | describe('index', () => { 5 | test("Should only export the exact expected list of objects.", async () => { 6 | // Arrange. 7 | const expectedExports = [ 8 | 'DrFetch', 9 | 'makeIterableHeaders', 10 | 'setHeaders', 11 | 'getHeader', 12 | 'hasHeader', 13 | 'StatusCodes', 14 | ]; 15 | 16 | // Act. 17 | const module = await import('../src/index.js'); 18 | for (const name of expectedExports) { 19 | expect(module, `Expected object '${name}' is not exported.`).to.have.property(name); 20 | } 21 | for (const name of Object.keys(module)) { 22 | expect(expectedExports, `Unexpected object '${name}' is exported.`).to.include(name); 23 | } 24 | }); 25 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "noEmit": true, /* Disable emitting files from a compilation. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 62 | // "removeComments": true, /* Disable emitting comments. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 65 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 80 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 81 | "esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 83 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 84 | 85 | /* Type Checking */ 86 | "strict": true, /* Enable all strict type-checking options. */ 87 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 88 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 90 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 91 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 92 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | }, 111 | "exclude": [ 112 | "**/*.test.ts", 113 | "**/*.test.js", 114 | "dist/**/*.*" 115 | ], 116 | "include": [ 117 | "**/*.ts" 118 | ] 119 | } 120 | --------------------------------------------------------------------------------