diff --git a/README.md b/README.md index 9f35134..aee052d 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,384 @@ -Modern, lightweight HTTP client for JavaScript, with handy tools for working with requests and responses. +# **Httio** -## 🚀 Features +[![Bundle size](https://img.shields.io/bundlephobia/minzip/httio)](https://bundlephobia.com/package/httio) +[![License](https://img.shields.io/npm/l/httio)](https://github.com/vladstsk/httio/blob/main/LICENSE) +[![Typed with TypeScript](https://badgen.net/npm/types/httio)](https://github.com/vladstsk/httio) -- **TypeScript First**: Full type safety with TypeScript generics -- **Simple API**: Intuitive interface for making HTTP requests -- **Lazy Evaluation**: Control when to parse response data -- **Promise-based**: Modern async/await pattern for request handling -- **Middleware Support**: Extend functionality with custom middleware -- **Lightweight**: Minimal footprint with no external dependencies -- **Customizable**: Flexible configuration for various use cases +> Lightweight, type-safe HTTP client for browsers and Node.js. +> Built on top of the native `fetch` but provides a better DX with strong typing, middleware and a minimalistic API. -## 📚 Documentation +--- -- [HTTP Client](https://github.com/vladstsk/httio/tree/main/packages/httio) +## Table of Contents -## 🗺️ Roadmap +1. [Why Httio?](#why-httio) +2. [Installation](#installation) +3. [Quick Start](#quick-start) +4. [Type-Safe Requests](#type-safe-requests) +5. [Client Configuration](#client-configuration) +6. [Handling Responses](#handling-responses) +7. [Middleware](#middleware) +8. [Error Handling](#error-handling) +9. [Recipes](#recipes) +10. [API Reference](#api-reference) +11. [FAQ](#faq) +12. [License](#license) -We're planning to expand the httio ecosystem with these packages: +--- -- **@httio/rest**: REST routing solutions -- **@httio/cache**: Response caching solutions +## Why Httio? -If you have any suggestions for us, we would be happy to hear them. +| Feature | Description | +|---------------------------|-------------------------------------------------------------------------------------------------------| +| **TypeScript-first** | All public types are exported; strict compile-time checks. | +| **Tiny footprint** | Ships as ESM + CJS, zero runtime dependencies. | +| **Lazy parsing** | Body is not parsed automatically—_you_ decide when and how. | +| **One interface everywhere** | Works in browsers, Node 18+, edge functions—no polyfills required. | +| **Extensible** | Middleware chain for logging, auth, caching, etc. | +| **Convenient cloning** | `extends()` lets you reuse and override base options elegantly. | +| **Full control** | Everything from `fetch` is exposed plus syntactic sugar (`params`, `json`, `timeout`). | + +--- + +## Installation + +```bash +# npm +npm i httio + +# yarn +yarn add httio + +# pnpm +pnpm add httio +``` + +Node 18+ already includes `fetch`. +For earlier versions add any fetch polyfill. + +--- + +## Quick Start + +```ts +import httio from 'httio'; + +// GET +const users = await httio.get('https://api.example.com/users').json(); + +// POST +await httio.post( + 'https://api.example.com/users', + { name: 'Alice', email: 'alice@example.com' } +).json(); + +// PUT with query params +await httio.put( + 'https://api.example.com/users/42', + { name: 'Bob' }, + { params: { notify: true } } +); +``` + +--- + +## Type-Safe Requests + +```ts +import httio from 'httio'; + +interface User { + id: number; + name: string; + email: string; +} + +interface CreateUserDto { + name: string; + email: string; +} + +// Strictly typed array +const list = async (): Promise => { + return httio.get('https://api.example.com/users').json(); +} + +// Typed payload + response +const create = async (payload: CreateUserDto): Promise => { + return httio.post('https://api.example.com/users', payload).json(); +}; +``` + +`json()` guarantees the shape of data at **compile-time**. + +--- + +## Client Configuration + +### Base client + +```ts +import { client } from 'httio'; + +const api = client({ + url: 'https://api.example.com', + headers: { 'Accept': 'application/json' }, +}); +``` + +### Extending an existing client + +`extends()` returns a _new_ instance with inherited and/or overridden options: + +```ts +const v2 = api.extends({ url: '/v2' }); +await v2.get('/status'); // → https://api.example.com/v2/status +``` + +### Supported options + +| Option | Type | Default | Description | +|-----------|----------------------------------------|------------------------------------|-------------------------------------------------| +| `url` | `string \| URL` | – | Base URL for relative paths. | +| `headers` | `HeadersInit` | – | Global headers. | +| `params` | `Record` | – | Query params (auto-encoded). | +| `timeout` | `number` | – | Abort request after N ms via `AbortController`. | +| `url` | `string \| URL` (only in `extends`) | – | Alias for `base` when only host changes. | +| `fetch` | `(input, init) => Promise` | `globalThis.fetch \| window.fetch` | Custom fetch implementation (handy in tests). | + +--- + +## Handling Responses + +Httio returns a **wrapper** around `Response` with extra parsing helpers. +They are _lazy_—HTTP call is sent immediately, but body reading starts only when a parser is invoked. + +| Method | Return type | +|------------------|------------------------| +| `json()` | `Promise` | +| `text()` | `Promise` | +| `blob()` | `Promise` | +| `bytes()` | `Promise` | +| `buffer()` | `Promise` | +| `stream()` | `Promise` | + +Example of deferred parsing: + +```ts +// Fire the request +const response = httio.get('/slow-endpoint'); + +// Do something else in parallel +await doSomething(); + +// Now read the body +const data = await response.json(); +``` + +--- + +## Middleware + +Middleware are async functions `(req, next) => Response` executed in a chain: + +```ts +import httio, { type Middleware } from 'httio'; + +const auth: Middleware = async (req, next) => { + req.headers.set('Authorization', `Bearer ${getToken()}`); + + return next(req); +}; + +const logger: Middleware = async (req, next) => { + const started = performance.now(); + + const res = await next(req); + console.log(`${req.method} ${req.url} → ${res.status} (${Date.now() - started} ms)`); + + return res; +}; + +httio.use(auth, logger); +``` + +Flow: `auth → logger → fetch → logger → auth`. + +--- + +## Error Handling + +HTTP status `4xx/5xx` triggers a `HttpError`: + +```ts +import httio, { HttpError } from 'httio'; + +try { + await httio.get('/admin').json(); +} catch (err) { + if (err instanceof HttpError) { + console.error(`Error ${err.status}: ${err.message}`); + } else { + throw err; // non-HTTP error + } +} +``` + +`HttpError` exposes: + +| Property | Description | +|-------------|----------------------------------| +| `status` | HTTP status code | +| `statusText`| Human-readable text (if any) | +| `response` | Original `Response` object | +| `url` | Final URL (after redirects) | + +--- + +## Error Handling + +HTTP status codes in the `4xx/5xx` range throw an instance of `HttpError`: +```ts +import httio, { HttpError } from 'httio'; + +try { + await httio.get('/admin').json(); +} catch (err) { + if (err instanceof HttpError) { + // Access the original request and response objects + const { request, response } = err; + + console.error( + `Request to ${request.url} failed with status ${response.status} ${response.statusText}`, + ); + } else { + throw err; // non-HTTP error + } +} +``` + +`HttpError` exposes only two properties: + +| Property | Type | Description | +|-----------|-----------------|------------------------------------------| +| `request` | `HttioRequest` | Object representing the original request | +| `response`| `HttioResponse` | Object representing the server response | + +You can read any additional details (status code, headers, body, etc.) from the `response` object itself. + +--- + +## Recipes + +### Passing query params + +```ts +httio.get('/search', { params: { q: 'httio', page: 2 } }); +``` + +Produces `/search?q=httio&page=2`. + +### Uploading files + +```ts +const form = new FormData(); +form.append('avatar', file); + +await httio.post('/me/avatar', form); +``` + +`Content-Type: multipart/form-data` is set automatically. + +### Working with streams + +```ts +const stream = await httio.get('/logs').stream(); +const reader = stream.getReader(); + +while (true) { + const { value, done } = await reader.read(); + + if (done) { + break; + } + + console.log(new TextDecoder().decode(value)); +} +``` + +### Reusing an `AbortController` + +```ts +const controller = new AbortController(); + +setTimeout(() => controller.abort(), 5000); // 5 s timeout + +httio.get('/long', { signal: controller.signal }); +``` + +--- + +## API Reference + +### `client(options?)` + +Creates a new client. +See [configuration](#supported-options) for the list of parameters. + +### `HttioClient` instance + +| Method | Description | +|-------------------------------------|------------------------------------------------| +| `get(url, opts?)` | `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` — same signature | +| `extends(opts)` | Returns a new client inheriting options | +| `use(...middleware)` | Adds middleware | + +### `RequestOptions` + +Extends the native `RequestInit`: + +| Extra field | Type | Description | +|-------------|---------------------------------|--------------------------------| +| `params` | `Record` | Adds query parameters | +| `timeout` | `number` | Aborts the request after N ms | + +### Types + +All core interfaces are exported from the package root: + +```ts +import type { HttioClient, Middleware, HttpError } from 'httio'; +``` + +--- + +## FAQ + +### Is it a full Axios replacement? + +Httio focuses on **type safety** and minimalism. +If you need request/response transformers, cancellation, works-everywhere support and do not want heavy dependencies—yes, Httio can be a solid alternative. + +### Are legacy browsers supported? + +Any environment with `fetch` (or a polyfill) and `ReadableStream` works. +IE 11 would require polyfills for both Promise and fetch, but the package is not officially tested there. + +### How do I test code that uses Httio? + +Pass your own `fetch` to the client: + +```ts +import { client } from 'httio'; +import { createFetchMock } from '@mswjs/interceptors'; + +const fetchMock = createFetchMock(); +const api = client({ fetch: fetchMock }); +``` + +--- + +## License + +Distributed under the MIT License. +See [LICENSE](LICENSE) for more details. diff --git a/package-lock.json b/package-lock.json index 4323a6c..8b1c7f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", + "@types/express": "^5.0.1", + "express": "^5.1.0", "husky": "^9.1.7", "lerna": "^8.2.2", "rimfar": "^3.0.2-omega", @@ -563,6 +565,37 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, "node_modules/@commitlint/cli": { "version": "19.8.0", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.0.tgz", @@ -1818,6 +1851,132 @@ "node": ">=6.9.0" } }, + "node_modules/@inquirer/confirm": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", + "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2551,6 +2710,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz", @@ -3509,6 +3686,31 @@ "@octokit/openapi-types": "^24.2.0" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4045,6 +4247,27 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", @@ -4055,6 +4278,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/css-tree": { "resolved": "node_modules/@eslint/css/typings/css-tree", "link": true @@ -4065,6 +4295,31 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -4074,6 +4329,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4120,6 +4382,13 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -4150,12 +4419,63 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4661,6 +4981,53 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -5288,6 +5655,40 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5420,6 +5821,16 @@ "node": ">=12.17" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5923,12 +6334,35 @@ "dev": true, "license": "ISC" }, - "node_modules/conventional-changelog-angular": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", - "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", "dependencies": { "compare-func": "^2.0.0" }, @@ -6101,6 +6535,26 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6441,6 +6895,16 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -6580,6 +7044,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6619,6 +7090,16 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -6922,6 +7403,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7484,6 +7972,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -7545,6 +8043,72 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -7740,6 +8304,24 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7867,6 +8449,26 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/front-matter": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", @@ -8427,6 +9029,16 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "license": "MIT" }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -8553,6 +9165,13 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -8590,6 +9209,23 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -8910,6 +9546,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9182,6 +9828,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9240,6 +9893,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -10950,6 +11610,16 @@ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -11188,6 +11858,19 @@ "node": ">=10" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -11497,6 +12180,64 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.5.tgz", + "integrity": "sha512-00MyTlY3TJutBa5kiU+jWiz2z5pNJDYHn2TgPkGkh92kMmNH43RqvMXd8y/7HxNn8RjzUbvZWYZjcS36fdb6sw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", + "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/multimatch": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", @@ -12190,6 +12931,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12283,6 +13037,13 @@ "node": ">=0.10.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12558,6 +13319,16 @@ "parse-path": "^7.0.0" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12615,6 +13386,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -12975,6 +13753,20 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12982,6 +13774,19 @@ "dev": true, "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -13007,6 +13812,29 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -13037,6 +13865,45 @@ "node": ">=8" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -13399,6 +14266,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -13631,6 +14505,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -13766,6 +14667,68 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -13819,6 +14782,13 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -14151,6 +15121,23 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -14775,6 +15762,42 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -15152,6 +16175,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -15362,6 +16423,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrs-resolver": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.3.3.tgz", @@ -15438,6 +16509,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -15494,6 +16576,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walk-up-path": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", @@ -15886,6 +16978,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/httio": { "version": "0.6.0", "license": "MIT", @@ -15893,6 +16998,7 @@ "@repo/eslint": "*", "@repo/jest": "*", "@repo/typescript": "*", + "msw": "^2.7.5", "tsup": "^8.4.0" }, "engines": { diff --git a/package.json b/package.json index d9cb93b..ec6c339 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "devDependencies": { "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", + "@types/express": "^5.0.1", + "express": "^5.1.0", "husky": "^9.1.7", "lerna": "^8.2.2", "rimfar": "^3.0.2-omega", diff --git a/packages/httio/README.md b/packages/httio/README.md index 322f157..fc21c87 100644 --- a/packages/httio/README.md +++ b/packages/httio/README.md @@ -1,227 +1,384 @@ +# **Httio** + [![Bundle size](https://img.shields.io/bundlephobia/minzip/httio)](https://bundlephobia.com/package/httio) [![License](https://img.shields.io/npm/l/httio)](https://github.com/vladstsk/httio/blob/main/LICENSE) [![Typed with TypeScript](https://badgen.net/npm/types/httio)](https://github.com/vladstsk/httio) -A lightweight, type-safe HTTP client for browser and Node.js environments. +> Lightweight, type-safe HTTP client for browsers and Node.js. +> Built on top of the native `fetch` but provides a better DX with strong typing, middleware and a minimalistic API. + +--- + +## Table of Contents + +1. [Why Httio?](#why-httio) +2. [Installation](#installation) +3. [Quick Start](#quick-start) +4. [Type-Safe Requests](#type-safe-requests) +5. [Client Configuration](#client-configuration) +6. [Handling Responses](#handling-responses) +7. [Middleware](#middleware) +8. [Error Handling](#error-handling) +9. [Recipes](#recipes) +10. [API Reference](#api-reference) +11. [FAQ](#faq) +12. [License](#license) + +--- + +## Why Httio? -## Features +| Feature | Description | +|---------------------------|-------------------------------------------------------------------------------------------------------| +| **TypeScript-first** | All public types are exported; strict compile-time checks. | +| **Tiny footprint** | Ships as ESM + CJS, zero runtime dependencies. | +| **Lazy parsing** | Body is not parsed automatically—_you_ decide when and how. | +| **One interface everywhere** | Works in browsers, Node 18+, edge functions—no polyfills required. | +| **Extensible** | Middleware chain for logging, auth, caching, etc. | +| **Convenient cloning** | `extends()` lets you reuse and override base options elegantly. | +| **Full control** | Everything from `fetch` is exposed plus syntactic sugar (`params`, `json`, `timeout`). | -- **TypeScript first** - Full type safety with TypeScript generics -- **Simple API** - Intuitive interface for making HTTP requests -- **Lazy evaluation** - Control when to parse response data -- **Promise-based** - Modern async/await pattern for request handling -- **Middleware support** - Extend functionality with custom middleware -- **Lightweight** - Minimal footprint -- **Customizable** - Flexible configuration for various use cases +--- ## Installation ```bash -npm install httio -``` +# npm +npm i httio -```bash +# yarn yarn add httio -``` -```bash +# pnpm pnpm add httio ``` -## Basic Usage +Node 18+ already includes `fetch`. +For earlier versions add any fetch polyfill. -```typescript -import httio from 'httio'; +--- -// Make a GET request -const getUsers = async () => { - const response = httio.get('https://api.example.com/users'); - - return response.json(); -}; +## Quick Start -// Make a POST request -const createUser = async (payload) => { - const response = httio.post('https://api.example.com/users', payload); - - return response.json(); -}; +```ts +import httio from 'httio'; + +// GET +const users = await httio.get('https://api.example.com/users').json(); + +// POST +await httio.post( + 'https://api.example.com/users', + { name: 'Alice', email: 'alice@example.com' } +).json(); + +// PUT with query params +await httio.put( + 'https://api.example.com/users/42', + { name: 'Bob' }, + { params: { notify: true } } +); ``` +--- + ## Type-Safe Requests -```typescript +```ts import httio from 'httio'; -// Define types for requests and responses interface User { id: number; name: string; email: string; } -interface CreateUserRequest { +interface CreateUserDto { name: string; email: string; } -// Typed requests -const getUsers = async (): Promise => { - const response = httio.get('https://api.example.com/users'); - - return response.json(); -}; - -const createUser = async (payload: CreateUserRequest): Promise => { - const response = httio.post('https://api.example.com/users', payload); +// Strictly typed array +const list = async (): Promise => { + return httio.get('https://api.example.com/users').json(); +} - return response.json(); +// Typed payload + response +const create = async (payload: CreateUserDto): Promise => { + return httio.post('https://api.example.com/users', payload).json(); }; ``` -## Lazy Evaluation +`json()` guarantees the shape of data at **compile-time**. -One of the key features of httio is that responses need explicit parsing, giving you control over when and how to process the response data: +--- -```typescript -import httio from 'httio'; +## Client Configuration + +### Base client -// Start a request but don't parse the result immediately -const pendingRequest = httio.post('https://api.example.com/users/create', { - name: "John Doe", - email: "john@example.com", +```ts +import { client } from 'httio'; + +const api = client({ + url: 'https://api.example.com', + headers: { 'Accept': 'application/json' }, }); +``` -// Do some other operations -await someOtherOperation(); +### Extending an existing client -// Now wait for the request to complete and get JSON result -const result = await pendingRequest.json(); +`extends()` returns a _new_ instance with inherited and/or overridden options: + +```ts +const v2 = api.extends({ url: '/v2' }); +await v2.get('/status'); // → https://api.example.com/v2/status ``` -## Using Middleware +### Supported options -```typescript -import httio from 'httio'; -import type { Middleware } from 'httio'; +| Option | Type | Default | Description | +|-----------|----------------------------------------|------------------------------------|-------------------------------------------------| +| `url` | `string \| URL` | – | Base URL for relative paths. | +| `headers` | `HeadersInit` | – | Global headers. | +| `params` | `Record` | – | Query params (auto-encoded). | +| `timeout` | `number` | – | Abort request after N ms via `AbortController`. | +| `url` | `string \| URL` (only in `extends`) | – | Alias for `base` when only host changes. | +| `fetch` | `(input, init) => Promise` | `globalThis.fetch \| window.fetch` | Custom fetch implementation (handy in tests). | + +--- + +## Handling Responses + +Httio returns a **wrapper** around `Response` with extra parsing helpers. +They are _lazy_—HTTP call is sent immediately, but body reading starts only when a parser is invoked. + +| Method | Return type | +|------------------|------------------------| +| `json()` | `Promise` | +| `text()` | `Promise` | +| `blob()` | `Promise` | +| `bytes()` | `Promise` | +| `buffer()` | `Promise` | +| `stream()` | `Promise` | + +Example of deferred parsing: + +```ts +// Fire the request +const response = httio.get('/slow-endpoint'); + +// Do something else in parallel +await doSomething(); -// Create middleware -const auth: Middleware = async (request, next) => { - request.headers.Authorization = `Bearer ${getToken()}`; +// Now read the body +const data = await response.json(); +``` + +--- + +## Middleware + +Middleware are async functions `(req, next) => Response` executed in a chain: + +```ts +import httio, { type Middleware } from 'httio'; + +const auth: Middleware = async (req, next) => { + req.headers.set('Authorization', `Bearer ${getToken()}`); - return next(request); + return next(req); }; -const logging: Middleware = async (request, next) => { - console.log(`[${request.method}] ${request.url}`); +const logger: Middleware = async (req, next) => { + const started = performance.now(); - const response = await next(request); + const res = await next(req); + console.log(`${req.method} ${req.url} → ${res.status} (${Date.now() - started} ms)`); - console.log(`Response status: ${response.status}`); - - return response; + return res; }; -httio.use(auth, logging); +httio.use(auth, logger); +``` + +Flow: `auth → logger → fetch → logger → auth`. -// Apply middleware -const response = await httio.get('https://api.example.com/users').json(); +--- + +## Error Handling + +HTTP status `4xx/5xx` triggers a `HttpError`: + +```ts +import httio, { HttpError } from 'httio'; + +try { + await httio.get('/admin').json(); +} catch (err) { + if (err instanceof HttpError) { + console.error(`Error ${err.status}: ${err.message}`); + } else { + throw err; // non-HTTP error + } +} ``` +`HttpError` exposes: + +| Property | Description | +|-------------|----------------------------------| +| `status` | HTTP status code | +| `statusText`| Human-readable text (if any) | +| `response` | Original `Response` object | +| `url` | Final URL (after redirects) | + +--- + ## Error Handling -```typescript -import httio from 'httio'; +HTTP status codes in the `4xx/5xx` range throw an instance of `HttpError`: +```ts +import httio, { HttpError } from 'httio'; try { - const users = await httio.get('https://api.example.com/users').json(); - - console.log(users); -} catch (error) { - if (error.status === 404) { - console.error('Resource not found'); - } else if (error.status === 401) { - console.error('Unauthorized'); + await httio.get('/admin').json(); +} catch (err) { + if (err instanceof HttpError) { + // Access the original request and response objects + const { request, response } = err; + + console.error( + `Request to ${request.url} failed with status ${response.status} ${response.statusText}`, + ); } else { - console.error('An error occurred:', error.message); + throw err; // non-HTTP error } } ``` -## Response Methods +`HttpError` exposes only two properties: -The client provides several methods to handle different response types: +| Property | Type | Description | +|-----------|-----------------|------------------------------------------| +| `request` | `HttioRequest` | Object representing the original request | +| `response`| `HttioResponse` | Object representing the server response | -```typescript -// Get JSON response -const json = await httio.get('https://api.example.com/users').json(); +You can read any additional details (status code, headers, body, etc.) from the `response` object itself. -// Get text response -const text = await httio.get('https://api.example.com/content').text(); +--- -// Get Blob response -const blob = await httio.get('https://api.example.com/image').blob(); +## Recipes -// Get ArrayBuffer response -const buffer = await httio.get('https://api.example.com/binary').buffer(); +### Passing query params -// Get ReadableStream response -const stream = await httio.get('https://api.example.com/form').stream(); +```ts +httio.get('/search', { params: { q: 'httio', page: 2 } }); ``` -## Advanced Configuration +Produces `/search?q=httio&page=2`. -```typescript -import httio from 'httio'; +### Uploading files + +```ts +const form = new FormData(); +form.append('avatar', file); + +await httio.post('/me/avatar', form); +``` + +`Content-Type: multipart/form-data` is set automatically. + +### Working with streams + +```ts +const stream = await httio.get('/logs').stream(); +const reader = stream.getReader(); + +while (true) { + const { value, done } = await reader.read(); + + if (done) { + break; + } + + console.log(new TextDecoder().decode(value)); +} +``` + +### Reusing an `AbortController` -const response = await httio - .get('https://api.example.com/users', { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - }) - .json(); +```ts +const controller = new AbortController(); + +setTimeout(() => controller.abort(), 5000); // 5 s timeout + +httio.get('/long', { signal: controller.signal }); ``` +--- + ## API Reference -### New Client Instance +### `client(options?)` -Creates a new HTTP client instance targeting the specified URL. +Creates a new client. +See [configuration](#supported-options) for the list of parameters. -```typescript -import { client } from 'httio'; +### `HttioClient` instance -const httio = client({ - base: 'https://api.example.com', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, -}); +| Method | Description | +|-------------------------------------|------------------------------------------------| +| `get(url, opts?)` | `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` — same signature | +| `extends(opts)` | Returns a new client inheriting options | +| `use(...middleware)` | Adds middleware | + +### `RequestOptions` + +Extends the native `RequestInit`: + +| Extra field | Type | Description | +|-------------|---------------------------------|--------------------------------| +| `params` | `Record` | Adds query parameters | +| `timeout` | `number` | Aborts the request after N ms | + +### Types + +All core interfaces are exported from the package root: + +```ts +import type { HttioClient, Middleware, HttpError } from 'httio'; ``` -### Request Methods +--- + +## FAQ + +### Is it a full Axios replacement? + +Httio focuses on **type safety** and minimalism. +If you need request/response transformers, cancellation, works-everywhere support and do not want heavy dependencies—yes, Httio can be a solid alternative. + +### Are legacy browsers supported? -- `client.get(url, options?)` - Make a GET request -- `client.post(url, payload?, options?)` - Make a POST request -- `client.put(url, payload?, options?)` - Make a PUT request -- `client.patch(url, payload?, options?)` - Make a PATCH request -- `client.delete(url, options?)` - Make a DELETE request -- `client.head(url, options?)` - Make a HEAD request -- `client.options(url, options?)` - Make an OPTIONS request +Any environment with `fetch` (or a polyfill) and `ReadableStream` works. +IE 11 would require polyfills for both Promise and fetch, but the package is not officially tested there. -### Configuration Methods +### How do I test code that uses Httio? + +Pass your own `fetch` to the client: + +```ts +import { client } from 'httio'; +import { createFetchMock } from '@mswjs/interceptors'; + +const fetchMock = createFetchMock(); +const api = client({ fetch: fetchMock }); +``` -- `client.extends(options?)` - Set request timeout in milliseconds -- `client.use(middleware)` - Add middleware to the request +--- -### Response Methods +## License -- `response.json()` - Parse response as JSON -- `response.text()` - Parse response as text -- `response.blob()` - Parse response as Blob -- `response.bytes()` - Parse response as Uint8Array -- `response.buffer()` - Parse response as ArrayBuffer -- `response.stream()` - Parse response as ReadableStream +Distributed under the MIT License. +See [LICENSE](../../LICENSE) for more details. diff --git a/packages/httio/package.json b/packages/httio/package.json index 00a0b0b..b4757ca 100644 --- a/packages/httio/package.json +++ b/packages/httio/package.json @@ -55,6 +55,7 @@ "@repo/eslint": "*", "@repo/jest": "*", "@repo/typescript": "*", + "msw": "^2.7.5", "tsup": "^8.4.0" } } diff --git a/packages/httio/src/client.ts b/packages/httio/src/client.ts index d018c2c..4f6cbef 100644 --- a/packages/httio/src/client.ts +++ b/packages/httio/src/client.ts @@ -1,64 +1,80 @@ -import pipeline from "~/http/pipeline"; -import type { HttioClient, HttioClientMethods, HttioClientOptions, HttioMethodOptions } from "~/types/client"; +import { pipeline } from "~/http/pipeline"; +import type { HttioClient, HttioClientOptions, HttioRequestOptions } from "~/types/client"; +import type { Payload } from "~/types/data"; +import type { Fetcher } from "~/types/fetch"; import type { Middleware } from "~/types/pipeline"; -import assign from "~/utils/assign"; -import merge from "~/utils/merge"; -import url from "~/utils/url"; -import { isPlaneObject } from "~/utils/validate"; - -const METHODS_WITH_BODY: (keyof HttioClientMethods)[] = ["delete", "options", "patch", "post", "put"]; - -const METHODS: (keyof HttioClientMethods)[] = ["get", "head", ...METHODS_WITH_BODY]; - -export default function client(options?: HttioClientOptions): HttioClient { - const { fetch: $fetch = fetch, url: base, ...init } = options || {}; - - const middleware = pipeline($fetch); - - const methods = {} as HttioClientMethods; - - for (const method of METHODS) { - // @ts-expect-error --- - methods[method] = (path, body, options) => { - if (!METHODS_WITH_BODY.includes(method)) { - if (isPlaneObject(body)) { - options = body as HttioMethodOptions; - } - - body = void 0; - } - - const { query, ...$options } = merge(init, options || {}); - - return middleware.handle( - url(base || path, path, query), - assign($options, { - body, - method: method.toUpperCase(), - }) - ); - }; - } - - return assign(methods, { - extends(_options: HttioClientOptions): HttioClient { - if (!_options.fetch) { - _options.fetch = $fetch; - } - - if (!_options.url) { - _options.url = base; - } else if (base) { - _options.url = url(base, _options.url instanceof URL ? _options.url.toString() : _options.url); - } - - return client(merge(init, _options)).use(...middleware.pipes); - }, +import { assign, merge } from "~/utils/object"; +import { join, search } from "~/utils/url"; - use(this: HttioClient, ...middlewares: Middleware[]): HttioClient { - middleware.use(...middlewares); +type RequestOptions = Omit & { + fetch: Fetcher; +}; - return this; - }, +function mergeOptions(oldOptions?: HttioClientOptions, newOptions?: HttioClientOptions): RequestOptions { + return assign({ fetch }, oldOptions, newOptions, { + params: merge(oldOptions?.params, newOptions?.params), + url: join(oldOptions?.url || "", newOptions?.url || ""), }); } + +function request(method: string, payload: Payload, middlewares: Middleware[], options: RequestOptions) { + const { fetch, headers, params, retry, timeout, url, ...$init } = options; + + const handle = pipeline(middlewares, fetch, { retry, timeout }); + + assign($init, { + body: payload, + headers: headers, + method: method.toUpperCase(), + }); + + return handle(url + search(params), $init); +} + +export function client(options?: HttioClientOptions): HttioClient { + const middlewares: Middleware[] = []; + + return assign( + { + extends(init: HttioClientOptions): HttioClient { + return client(mergeOptions(options, init)).use(...middlewares); + }, + + use(this: HttioClient, ...middlewares: Middleware[]): HttioClient { + middlewares.push(...middlewares); + + return this; + }, + }, + + { + delete(path: string, init?: HttioRequestOptions) { + return request("delete", undefined, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + + get(path: string, init?: HttioRequestOptions) { + return request("get", undefined, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + + head(path: string, init?: HttioRequestOptions) { + return request("head", undefined, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + + options(path: string, init?: HttioRequestOptions) { + return request("options", undefined, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + + patch(path: string, payload?: Payload, init?: HttioRequestOptions) { + return request("patch", payload, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + + post(path: string, payload?: Payload, init?: HttioRequestOptions) { + return request("post", payload, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + + put(path: string, payload?: Payload, init?: HttioRequestOptions) { + return request("put", payload, middlewares, mergeOptions(options, assign({}, init, { url: path }))); + }, + } + ); +} diff --git a/packages/httio/src/constants/http.ts b/packages/httio/src/constants/http.ts new file mode 100644 index 0000000..d24a303 --- /dev/null +++ b/packages/httio/src/constants/http.ts @@ -0,0 +1,3 @@ +export const RequestSymbol = Symbol("HttioRequest"); + +export const ResponseSymbol = Symbol("HttioResponse"); diff --git a/packages/httio/src/error/http.ts b/packages/httio/src/error/http.ts new file mode 100644 index 0000000..93bf559 --- /dev/null +++ b/packages/httio/src/error/http.ts @@ -0,0 +1,14 @@ +import type { HttioRequest, HttioResponse } from "~/types/http"; + +export class HttpError extends Error { + public request: HttioRequest; + public response: HttioResponse; + + constructor(request: HttioRequest, response: HttioResponse) { + super(`[${request.method}] ${request.url}: status code ${response.status} ${response.statusText}`); + + this.name = "HttpError"; + this.request = request; + this.response = response; + } +} diff --git a/packages/httio/src/errors/http.ts b/packages/httio/src/errors/http.ts deleted file mode 100644 index e4818cc..0000000 --- a/packages/httio/src/errors/http.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { HttioRequest } from "~/types/request"; -import type { HttioResponse } from "~/types/response"; - -export default class HttpError extends Error { - public request: HttioRequest; - public response: HttioResponse; - - constructor(message: string | null | undefined, request: HttioRequest, response: HttioResponse) { - super(message || response.toString()); - - this.request = request; - this.response = response; - } -} diff --git a/packages/httio/src/errors/index.ts b/packages/httio/src/errors/index.ts deleted file mode 100644 index 0950347..0000000 --- a/packages/httio/src/errors/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import HttpError from "./http"; - -export { HttpError }; diff --git a/packages/httio/src/http/body.ts b/packages/httio/src/http/body.ts index 1236ab5..cc83b80 100644 --- a/packages/httio/src/http/body.ts +++ b/packages/httio/src/http/body.ts @@ -1,27 +1,61 @@ -import type { HttioBody } from "~/types/response"; -import { isFunction } from "~/utils/validate"; +import type { HttioBody } from "~/types/body"; +import type { Payload } from "~/types/data"; +import { isPlaneObject } from "~/utils/validate"; -type Failure = readonly [never, unknown]; -type Success = readonly [Response, never?]; - -function bind(promise: Promise, key: keyof Response) { - return async function handle(): Promise { - const [data, error] = await promise; +export function getBodyInit(payload?: Payload): BodyInit | null | undefined { + if (isPlaneObject(payload)) { + return JSON.stringify(payload); + } - if (error) { - throw error; - } + return payload; +} - return isFunction(data[key]) ? (data[key]() as T) : (data[key] as T); - }; +export function getBodyPayload(body?: BodyInit | Payload): Payload { + try { + return JSON.parse(body as string); + } catch { + return body; + } } -export default function body(promise: Promise): HttioBody { - const data = { stream: bind(promise, "body") } as HttioBody; +export function getResponseBody(response: Body | Promise): HttioBody { + const promise = Promise.resolve(response); - for (const property of ["arrayBuffer", "blob", "bytes", "json", "text"] satisfies (keyof HttioBody)[]) { - data[property] = bind(promise, property) as never; - } + return { + async blob() { + const res = await promise; + + return res.blob(); + }, + + async buffer() { + const res = await promise; + + return res.arrayBuffer(); + }, - return data; + async bytes() { + const res = await promise; + + return res.bytes(); + }, + + async json() { + const res = await promise; + + return res.json(); + }, + + async stream() { + const res = await promise; + + return res.body || new ReadableStream(); + }, + + async text() { + const res = await promise; + + return res.text(); + }, + }; } diff --git a/packages/httio/src/http/pipeline.ts b/packages/httio/src/http/pipeline.ts index ea1fb12..448d55b 100644 --- a/packages/httio/src/http/pipeline.ts +++ b/packages/httio/src/http/pipeline.ts @@ -1,55 +1,110 @@ -import request from "~/http/request"; -import response from "~/http/response"; -import normalize from "~/middleware/normalize"; +import { HttpError } from "~/error/http"; +import { request } from "~/http/request"; +import { response } from "~/http/response"; +import type { HttioBody } from "~/types/body"; +import type { RetryOptions } from "~/types/client"; +import type { Payload } from "~/types/data"; import type { Fetcher } from "~/types/fetch"; -import type { Middleware, NextMiddleware, Pipeline } from "~/types/pipeline"; -import type { HttioRequest, HttioRequestInit } from "~/types/request"; -import type { ResponseInstance } from "~/types/response"; -import { isHttioResponse } from "~/utils/validate"; +import type { HttioRequest, HttioResponse } from "~/types/http"; +import type { Middleware, NextMiddleware } from "~/types/pipeline"; +import { assign, merge } from "~/utils/object"; +import { delay } from "~/utils/timer"; +import { isNumber } from "~/utils/validate"; -export default function pipeline(fetch: Fetcher): Pipeline { - const pipes: Middleware[] = []; +type PipelineOptions = { + retry?: RetryOptions | number; + timeout?: number; +}; - const open: NextMiddleware = (request) => { - const { url, ...init } = request; +function attach(promise: Promise, type: K): HttioBody[K] { + // todo: fix test + /* istanbul ignore next */ + return () => promise.then((response) => response[type].call(response)) as never; +} - return response(url, request.method, async () => fetch(new Request(url, init as RequestInit))); - }; +function handle(source: Promise): HttioBody & Promise { + const promise = source.then(response, (error) => { + // todo: fix test + /* istanbul ignore next */ + return >{ + [Symbol.toStringTag]: Promise.prototype[Symbol.toStringTag], - const reducer = (next: NextMiddleware, middleware: Middleware) => { - return function handle(req: HttioRequest): ResponseInstance { - return response(req.url, req.method, async () => { - const data = await middleware(req, next); + catch(rejected) { + return Promise.reject(error).catch(rejected); + }, - if (isHttioResponse(data)) { - return new Response(await data.stream(), { - headers: data.headers, - status: data.status, - }); - } + finally(callback) { + return Promise.reject(error).finally(callback); + }, - return data instanceof Response ? data : new Response(data); - }); + then(fulfilled, rejected) { + return Promise.reject(error).then(fulfilled, rejected); + }, }; + }); + + return assign(promise, { + blob: attach(promise, "blob"), + buffer: attach(promise, "buffer"), + bytes: attach(promise, "bytes"), + json: attach(promise, "json"), + stream: attach(promise, "stream"), + text: attach(promise, "text"), + }); +} + +// eslint-disable-next-line prettier/prettier +async function process(fetch: Fetcher, request: HttioRequest, options?: PipelineOptions): Promise { + let retry: Required = { + delay: 1000, + limit: 3, }; - return { - get handle() { - const next = pipes.reduceRight(reducer, open); + if (isNumber(options?.retry)) { + retry.limit = options.retry; + } else if (options?.retry) { + retry = merge(retry, options.retry as Required); + } + + let res: HttioResponse; + const req = request.toRequest(); - return function handle(url: URL | string, options: HttioRequestInit): ResponseInstance { - return normalize(request(url, options), next) as never; - }; - }, + for (let i = 0; i < retry.limit; i++) { + res = await Promise.race([ + fetch(req).then(response), + delay(options?.timeout || 1000).then(() => { + return response(null, { status: 408 }); + }), + ]); - get pipes() { - return pipes; - }, + if ([429, 500, 502, 503].includes(res.status)) { + await delay(retry.delay); - use(...middleware: Middleware[]) { - pipes.push(...middleware); + continue; + } - return this; - }, + if (!res.ok) { + break; + } + + return res; + } + + throw new HttpError(request, res!); +} + +function reduce(next: NextMiddleware, middleware: Middleware): NextMiddleware { + return (input, init?) => { + try { + return handle(Promise.resolve(middleware(request(input as never, init), next))); + } catch (error) { + return handle(Promise.reject(error)); + } }; } + +export function pipeline(middlewares: Middleware[], fetch: Fetcher, options?: PipelineOptions): NextMiddleware { + return middlewares.reduceRight(reduce, (input, init?) => { + return handle(process(fetch, request(input as never, init), options)); + }); +} diff --git a/packages/httio/src/http/request.ts b/packages/httio/src/http/request.ts index d94785e..6c3710e 100644 --- a/packages/httio/src/http/request.ts +++ b/packages/httio/src/http/request.ts @@ -1,22 +1,118 @@ -import type { HttioRequest, HttioRequestInit } from "~/types/request"; -import assign from "~/utils/assign"; -import { REQUEST } from "~/utils/consts"; -import { isString } from "~/utils/validate"; +import { RequestSymbol } from "~/constants/http"; +import { getBodyPayload } from "~/http/body"; +import type { Payload } from "~/types/data"; +import type { HttioRequest, HttioRequestInit } from "~/types/http"; +import { assign, clone, references } from "~/utils/object"; +import { instanceOf, isHttioRequest, isPlaneObject, isString } from "~/utils/validate"; -export default function request(url: URL | string, options: HttioRequestInit): HttioRequest { - const { headers, method, ...init } = options; +type RequestOptions = Omit & { + body?: Payload; + headers: Headers; + method: string; +}; - if (isString(url)) { - url = new URL(url); +function extract(source: HttioRequestInit | Request): RequestOptions { + return assign( + { + body: getBodyPayload(source.body), + headers: new Headers(source.headers), + method: source.method || "GET", + }, + clone( + source, + "cache", + "credentials", + "integrity", + "keepalive", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "signal" + ) + ); +} + +export function request(origin: Request): HttioRequest; +export function request(instance: HttioRequest): HttioRequest; +export function request(url: URL | string, init?: HttioRequestInit): HttioRequest; +export function request(input: HttioRequest | Request | URL | string, init?: HttioRequestInit): HttioRequest { + if (isHttioRequest(input)) { + return input; + } + + if (instanceOf(input, Request)) { + return request(input.url, extract(input)); } - return assign({ [REQUEST]: REQUEST }, init, { - headers: new Headers(headers), - method: method.toUpperCase(), - url, + let url = new URL(input); + const options = extract(init || {}); - toString() { - return `[${options.method.toUpperCase()}] ${url.toString()}`; + return assign( + { [RequestSymbol]: RequestSymbol }, + + references(options, "body", "headers", "method"), + { + get url() { + return url; + }, + + // todo: fix test + /* istanbul ignore next */ + set url(value) { + url = value; + }, }, - }); + + { + clone(): HttioRequest { + return request(url, options); + }, + + toRequest(): Request { + let body = options.body; + let type = "application/json"; + const headers = new Headers(options.headers); + + if (isString(body)) { + type = "text/plain; charset=utf-8"; + } + + if (isPlaneObject(body)) { + body = JSON.stringify(body); + } + + if (body instanceof URLSearchParams) { + headers.set("Content-Type", ""); + } + + if (body instanceof URLSearchParams) { + type = "application/x-www-form-urlencoded"; + } + + if (body instanceof FormData) { + type = "multipart/form-data"; + } + + if (body instanceof Blob || body instanceof ReadableStream || body instanceof ArrayBuffer) { + type = "application/octet-stream"; + } + + if (body instanceof ReadableStream) { + // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + // @ts-expect-error - experimental + options.duplex = "half"; + } + + headers.set("Accept", "text/*,image/*,application/json,application/octet-stream"); + headers.set("Content-Type", type); + + if (isString(body)) { + headers.set("Content-length", body.length.toString()); + } + + return new Request(url, assign(options, { body, headers })); + }, + } + ); } diff --git a/packages/httio/src/http/response.ts b/packages/httio/src/http/response.ts index b320ee6..716d3a1 100644 --- a/packages/httio/src/http/response.ts +++ b/packages/httio/src/http/response.ts @@ -1,39 +1,33 @@ -import body from "~/http/body"; -import type { HttioBody, HttioResponse, ResponseInstance } from "~/types/response"; -import assign from "~/utils/assign"; -import { RESPONSE } from "~/utils/consts"; -import pick from "~/utils/pick"; - -export function wrap(method: string, response: Response, body: HttioBody, urlFallback: URL): HttioResponse { - const url = response.url ? new URL(response.url) : urlFallback; - const status = response.url ? response.statusText : "OK"; - - return assign(pick(response, "headers", "status"), body, { - [RESPONSE]: RESPONSE, - - url, - - toString() { - return `[${method.toUpperCase()}] ${url}: ${response.status} ${status}`; - }, - }); -} - -export default function response(url: URL, method: string, factory: () => Promise): ResponseInstance { - const promise = factory().then( - (response) => [response] as const, - (error) => [null as never, error] as const - ); - - const data = body(promise); - - const instance = promise.then(([res, error]) => { - if (error) { - throw error; +import { ResponseSymbol } from "~/constants/http"; +import { getBodyInit, getResponseBody } from "~/http/body"; +import type { Payload } from "~/types/data"; +import type { HttioResponse } from "~/types/http"; +import { assign, references } from "~/utils/object"; +import { instanceOf, isHttioResponse } from "~/utils/validate"; + +export function response(origin: Response): HttioResponse; +export function response(instance: HttioResponse): HttioResponse; +export function response(payload?: Payload, init?: ResponseInit): HttioResponse; +export function response(origin: HttioResponse | Payload | Response, init?: ResponseInit): HttioResponse { + if (isHttioResponse(origin)) { + return origin; + } + + if (!instanceOf(origin, Response)) { + return response(new Response(getBodyInit(origin), init)); + } + + return assign( + { [ResponseSymbol]: ResponseSymbol }, + + getResponseBody(origin), + + references(origin, "headers", "ok", "redirected", "status", "statusText", "type", "url"), + + { + clone(): HttioResponse { + return response(origin.clone()); + }, } - - return wrap(method, res, data, url); - }); - - return assign(instance, data); + ); } diff --git a/packages/httio/src/index.ts b/packages/httio/src/index.ts index 355dd43..48e8960 100644 --- a/packages/httio/src/index.ts +++ b/packages/httio/src/index.ts @@ -1,14 +1,13 @@ -export * from "~/errors"; - +export type * from "~/types/body"; export type * from "~/types/client"; export type * from "~/types/data"; export type * from "~/types/fetch"; +export type * from "~/types/http"; export type * from "~/types/pipeline"; -export type * from "~/types/request"; -export type * from "~/types/response"; -import client from "~/client"; +import { client } from "~/client"; +import { HttpError } from "~/error/http"; const httio = client(); -export { client, httio as default }; +export { client, httio as default, HttpError }; diff --git a/packages/httio/src/middleware/normalize.ts b/packages/httio/src/middleware/normalize.ts deleted file mode 100644 index 45fa6a0..0000000 --- a/packages/httio/src/middleware/normalize.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { MiddlewareResult, NextMiddleware } from "~/types/pipeline"; -import type { HttioRequest } from "~/types/request"; -import { isString } from "~/utils/validate"; - -export default function normalize(request: HttioRequest, next: NextMiddleware): MiddlewareResult { - if (!request.headers.has("Accept")) { - request.headers.set("Accept", "text/*,image/*,application/json,application/octet-stream"); - } - - if (!request.headers.has("Content-Type")) { - let type = "application/json"; - - if (isString(request.body)) { - type = "text/plain; charset=utf-8"; - - // - } else if (request.body instanceof URLSearchParams) { - type = "application/x-www-form-urlencoded"; - - // - } else if (request.body instanceof FormData) { - type = "multipart/form-data"; - - // - } else if ( - request.body instanceof Blob || - request.body instanceof ReadableStream || - request.body instanceof ArrayBuffer - ) { - type = "application/octet-stream"; - - // - } else { - request.body = JSON.stringify(request.body); - } - - request.headers.set("Content-Type", type); - } - - if (isString(request.body) && !request.headers.has("Content-length")) { - request.headers.set("Content-length", request.body.length.toString()); - } - - return next(request); -} diff --git a/packages/httio/src/utils/assign.ts b/packages/httio/src/utils/assign.ts deleted file mode 100644 index 556abb0..0000000 --- a/packages/httio/src/utils/assign.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default function assign(target: T, source: U): T & U; -export default function assign(target: T, source1: U, source2: V): T & U & V; -export default function assign(target: T, source1: U, source2: V, source3: W): T & U & V & W; -export default function assign(target: object, ...source: object[]): object { - return Object.assign(target, ...source); -} diff --git a/packages/httio/src/utils/consts.ts b/packages/httio/src/utils/consts.ts deleted file mode 100644 index 3fcc0b0..0000000 --- a/packages/httio/src/utils/consts.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const REQUEST = Symbol("HttioRequest"); - -export const RESPONSE = Symbol("HttioResponse"); diff --git a/packages/httio/src/utils/merge.ts b/packages/httio/src/utils/merge.ts deleted file mode 100644 index 7f394f5..0000000 --- a/packages/httio/src/utils/merge.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { isPlaneObject } from "~/utils/validate"; - -type MergeResult = T extends PlaneObject - ? U extends PlaneObject - ? { - [K in keyof (T & U)]: K extends keyof T & keyof U - ? MergeResult - : K extends keyof U - ? U[K] - : K extends keyof T - ? T[K] - : never; - } - : U - : U; - -type PlaneObject = Record; - -export default function merge(target: T, source: U): MergeResult { - const references = new Set(); - - function combine(a: V, b: W): MergeResult { - if (references.has(b)) { - return b as MergeResult; - } - - if (isPlaneObject(a) && isPlaneObject(b)) { - references.add(b); - - const result = { ...a } as PlaneObject; - - for (const key of Object.getOwnPropertyNames(b)) { - result[key] = combine(a[key], b[key]); - } - - for (const key of Object.getOwnPropertySymbols(b)) { - result[key as never] = combine(a[key as never], b[key as never]); - } - - return result as MergeResult; - } - - return b as MergeResult; - } - - return combine(target, source); -} diff --git a/packages/httio/src/utils/object.ts b/packages/httio/src/utils/object.ts new file mode 100644 index 0000000..4a41eb4 --- /dev/null +++ b/packages/httio/src/utils/object.ts @@ -0,0 +1,85 @@ +import type { PlaneObject } from "~/types/data"; +import { isPlaneObject } from "~/utils/validate"; + +type DeepMergeResult = T extends PlaneObject ? (U extends PlaneObject ? MergeObjects : U) : U; + +type MergeObjects = { + [K in keyof T & keyof U]: K extends keyof U ? (K extends keyof T ? DeepMergeResult : U[K]) : T[K]; +}; + +export function assign(target: T, source: U): T & U; +export function assign(target: T, source1: U1, source2: U2): T & U1 & U2; +export function assign(target: T, source1: U1, source2: U2, source3: U3): T & U1 & U2 & U3; +// eslint-disable-next-line prettier/prettier +export function assign(target: T, source1: U1, source2: U2, source3: U3, source4: U4): T & U1 & U2 & U3 & U4; +export function assign(target: object, ...source: object[]): object { + return Object.assign(target, ...source); +} + +export function clone(source: T, ...keys: K[]): Pick { + if (keys.length === 0) { + return { ...source } as Pick; + } + + const result = {} as Pick; + + for (const key of keys) { + result[key] = source[key]; + } + + return result; +} + +// eslint-disable-next-line prettier/prettier +export function define(target: T, key: K, get: () => U, set?: (value: U) => void): T & { [P in K]: U } { + return Object.defineProperty(target as never, key, { + configurable: true, + enumerable: true, + + get, + set, + }); +} + +export function merge(target: T, source: U): DeepMergeResult { + const references = new Set(); + + function combine(a: V, b: W): DeepMergeResult { + if (references.has(b) || !isPlaneObject(a) || !isPlaneObject(b)) { + return b as DeepMergeResult; + } + + references.add(b); + + const result = clone(a) as PlaneObject; + + for (const key of Object.getOwnPropertyNames(b)) { + result[key] = combine(a[key], b[key]); + } + + for (const key of Object.getOwnPropertySymbols(b)) { + result[key as never] = combine(a[key as never], b[key as never]); + } + + return result as DeepMergeResult; + } + + return combine(target, source); +} + +export function references(target: T, ...properties: K[]): Pick { + const result = {} as Pick; + + for (const property of properties) { + define( + result, + property, + () => target[property], + (value) => { + target[property] = value; + } + ); + } + + return result; +} diff --git a/packages/httio/src/utils/pick.ts b/packages/httio/src/utils/pick.ts deleted file mode 100644 index f815020..0000000 --- a/packages/httio/src/utils/pick.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function pick(obj: T, ...keys: K[]): Pick { - return keys.reduce( - (acc, key) => { - acc[key] = obj[key]; - return acc; - }, - {} as Pick - ); -} diff --git a/packages/httio/src/utils/sleep.ts b/packages/httio/src/utils/sleep.ts deleted file mode 100644 index cb01e85..0000000 --- a/packages/httio/src/utils/sleep.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/packages/httio/src/utils/timer.ts b/packages/httio/src/utils/timer.ts new file mode 100644 index 0000000..e6f7f69 --- /dev/null +++ b/packages/httio/src/utils/timer.ts @@ -0,0 +1,5 @@ +export function delay(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/packages/httio/src/utils/url.ts b/packages/httio/src/utils/url.ts index 815c504..29f766f 100644 --- a/packages/httio/src/utils/url.ts +++ b/packages/httio/src/utils/url.ts @@ -1,58 +1,86 @@ import type { QueryParams } from "~/types/fetch"; -import { isArray, isPlaneObject, type } from "~/utils/validate"; +import { isArray, isPlaneObject, isPrimitive } from "~/utils/validate"; function inject(search: URLSearchParams, parent?: string, params?: QueryParams): void { - for (const name in params) { + if (!params) { + return; + } + + for (const [name, value] of Object.entries(params)) { const key = parent ? `${parent}[${name}]` : name; - if (isArray(params[name])) { - for (let i = 0; i < params[name].length; i++) { - const index = `${key}[${i + 1}]`; + if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const index = `${key}[${i}]`; + const item = value[i]; - if (["Boolean", "Number", "String"].includes(type(params[name][i]))) { - search.append(index, String(params[name][i])); + if (isPrimitive(item)) { + search.append(index, encodeURI(String(item))); } else { - inject(search, index, params[name][i] as QueryParams); + inject(search, index, item); } } } - if (["Boolean", "Number", "String"].includes(type(params[name]))) { - search.append(key, String(params[name])); - } - - if (isPlaneObject(params[name])) { - inject(search, key, params[name] as QueryParams); + if (isPrimitive(value)) { + search.append(key, String(value)); + } else if (isPlaneObject(value)) { + inject(search, key, value); } } } -export default function url(base: URL | string, path: string, params?: QueryParams): URL { - if (!(base instanceof URL)) { - base = new URL(base); - } +export function join(...paths: string[]): string { + let domain: string | undefined; + const segments: string[] = []; - let pathname = base.pathname; + for (const path of paths) { + const offset = path.indexOf("://"); + let start: number = 0; + let end: number = path.length; - base.pathname = ""; + if (offset > 0) { + start = path.indexOf("/", offset + 3); - if (path.includes("://")) { - pathname = ""; - } + if (start < 0) { + start = path.length; + } + } - const url = new URL(path, base); + if (offset > 0) { + domain = path.slice(0, start); + } - url.pathname = (pathname + url.pathname).replace("//", "/"); + for (; start < path.length; start++) { + if (path[start] !== "/") { + break; + } + } - if (url.pathname.length > 1 && url.pathname.endsWith("/")) { - url.pathname = url.pathname.slice(0, -1); + for (; end > start; end--) { + if (path[end - 1] !== "/") { + break; + } + } + + if (start < end) { + segments.push(path.slice(start, end)); + } } - if (url.search !== base.search) { - url.search = base.search + "&" + url.search.slice(1); + if (domain) { + segments.unshift(domain); } - inject(url.searchParams, void 0, params); + return segments.join("/"); +} + +export function search(params?: QueryParams) { + const target = new URLSearchParams(); + + inject(target, void 0, params); + + const value = target.toString(); - return url; + return value ? "?" + value : value; } diff --git a/packages/httio/src/utils/validate.ts b/packages/httio/src/utils/validate.ts index fa836bb..1d0b5ef 100644 --- a/packages/httio/src/utils/validate.ts +++ b/packages/httio/src/utils/validate.ts @@ -1,29 +1,45 @@ -import type { HttioRequest } from "~/types/request"; -import type { HttioResponse } from "~/types/response"; -import { REQUEST, RESPONSE } from "~/utils/consts"; +import { RequestSymbol, ResponseSymbol } from "~/constants/http"; +import type { PlaneObject } from "~/types/data"; +import type { HttioRequest, HttioResponse } from "~/types/http"; const OBJECT_PROTOTYPE = Object.prototype; -export function isArray(value: unknown): value is unknown[] { - return type(value) === "Array"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +interface InstanceConstructor { + new (...args: A): R; + + prototype: R; } -export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { - return type(value) === "Function"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function instanceOf(value: unknown, constructor: InstanceConstructor): value is R { + return value instanceof constructor; +} + +export function isArray(value: unknown): value is unknown[] { + return type(value) === "Array"; } export function isHttioRequest(value: unknown): value is HttioRequest { - return type(value) === "Object" && REQUEST in (value as object); + return isPlaneObject(value) && RequestSymbol in value; } export function isHttioResponse(value: unknown): value is HttioResponse { - return type(value) === "Object" && RESPONSE in (value as object); + return isPlaneObject(value) && ResponseSymbol in value; +} + +export function isNumber(value: unknown): value is number { + return type(value) === "Number"; } -export function isPlaneObject(value: unknown): value is Record { +export function isPlaneObject(value: unknown): value is PlaneObject { return type(value) === "Object" && Object.getPrototypeOf(value) === OBJECT_PROTOTYPE; } +export function isPrimitive(value: unknown): value is boolean | number | string | null | undefined { + return value === null || value === undefined || typeof value !== "object"; +} + export function isString(value: unknown): value is string { return type(value) === "String"; } diff --git a/packages/httio/tests/client.test.ts b/packages/httio/tests/client.test.ts new file mode 100644 index 0000000..c124346 --- /dev/null +++ b/packages/httio/tests/client.test.ts @@ -0,0 +1,87 @@ +import { client } from "~/client"; +import type { HttioClient } from "~/types/client"; + +import { start, stop } from "./server"; + +describe("Client", () => { + let host: string; + let base: HttioClient; + let extended: HttioClient; + + beforeAll(() => { + base = client(); + extended = base.extends({ + url: (host = start()), + }); + }); + + afterAll(stop); + + describe("HTTP methods", () => { + test.each(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const)( + "should make %s request", + async (method) => { + const $method = method.toLowerCase() as Lowercase; + + expect(base[$method]).toBeInstanceOf(Function); + expect(extended[$method]).toBeInstanceOf(Function); + + const response = { url: new URL(`${host}/${$method}`) }; + + await expect(base[$method](`${host}/${$method}`)).resolves.toMatchObject(response); + await expect(extended[$method](`/${$method}`)).resolves.toMatchObject(response); + } + ); + }); + + describe("Response types", () => { + const instances = [ + ["buffer", ArrayBuffer], + ["blob", Blob], + ["bytes", Uint8Array], + ["stream", ReadableStream], + ] as const; + + test.each(instances)("should handle %s response", async (method, instance) => { + const response = extended.get("/get"); + + expect(response[method]).toBeInstanceOf(Function); + await expect(response[method]()).resolves.toBeInstanceOf(instance); + }); + + test("should return response as JSON", async () => { + const response = extended.get("/get"); + + expect(response.json).toBeInstanceOf(Function); + await expect(response.json()).resolves.toMatchObject({ url: new URL(`${host}/get`) }); + }); + + test("should return response as text", async () => { + const response = extended.get("/get"); + + expect(response.text).toBeInstanceOf(Function); + expect(JSON.parse(await response.text())).toMatchObject({ url: new URL(`${host}/get`) }); + }); + }); + + describe("Request options", () => { + test("should handle redirects", async () => { + const response = extended.get(`/redirect`, { + params: { url: `${host}/get` }, + }); + + await expect(response.json()).resolves.toMatchObject({ url: new URL(`${host}/get`) }); + }); + }); + + describe("Client extensions", () => { + test("should extend extended with new URL", async () => { + const status = 201; + const response = await extended.extends({ url: "/status" }).get(`/${status}`); + + expect(response.status).toBe(status); + expect(response.url).toBe(`${host}/status/${status}`); + await expect(response.json()).resolves.toMatchObject({ status }); + }); + }); +}); diff --git a/packages/httio/tests/http/body.test.ts b/packages/httio/tests/http/body.test.ts new file mode 100644 index 0000000..9f015a6 --- /dev/null +++ b/packages/httio/tests/http/body.test.ts @@ -0,0 +1,101 @@ +import { getBodyInit, getBodyPayload, getResponseBody } from "~/http/body"; + +describe("Http Body", () => { + describe("getBodyInit() function", () => { + test("should return undefined when payload is undefined", () => { + expect(getBodyInit(undefined)).toBeUndefined(); + }); + + test("should return a JSON string when payload is a plain object", () => { + const payload = { key: "value" }; + const result = getBodyInit(payload); + expect(result).toBe(JSON.stringify(payload)); + }); + + test("should return input as-is when payload is of BodyInit type", () => { + const payload = "plain string"; + const result = getBodyInit(payload); + expect(result).toBe(payload); + }); + + test("should return null when payload is null", () => { + const result = getBodyInit(null); + expect(result).toBeNull(); + }); + + test("should return input as-is when payload is not a plain object", () => { + const payload = new Date(); + const result = getBodyInit(payload); + expect(result).toBe(payload); + }); + }); + + describe("getBodyPayload() function", () => { + test("should parse and return an object when body is a valid JSON string", () => { + const body = '{"key":"value"}'; + const result = getBodyPayload(body); + expect(result).toEqual({ key: "value" }); + }); + + test("should return input as-is when body is of BodyInit type", () => { + const body = "plain string"; + const result = getBodyPayload(body); + expect(result).toBe(body); + }); + + test("should return null when body is null", () => { + const result = getBodyPayload(null); + expect(result).toBeNull(); + }); + + test("should return undefined when body is undefined", () => { + const result = getBodyPayload(undefined); + expect(result).toBeUndefined(); + }); + + test("should return input as-is when body is an invalid JSON string", () => { + const invalidJson = '{"key": "value"'; // Missing closing brace + const result = getBodyPayload(invalidJson); + expect(result).toBe(invalidJson); + }); + }); + + describe("getResponseBody() function", () => { + test("should return a blob() function returning a Blob", async () => { + const body = getResponseBody(new Response()); + + await expect(body.blob()).resolves.toBeInstanceOf(Blob); + }); + + test("should return a buffer() function returning an ArrayBuffer", async () => { + const body = getResponseBody(new Response()); + + await expect(body.buffer()).resolves.toBeInstanceOf(ArrayBuffer); + }); + + test("should return bytes() function returning a Uint8Array", async () => { + const body = getResponseBody(new Response()); + + await expect(body.bytes()).resolves.toBeInstanceOf(Uint8Array); + }); + + test("should return a json() function returning parsed JSON", async () => { + const payload = { key: "value" }; + const body = getResponseBody(new Response(JSON.stringify(payload))); + + await expect(body.json()).resolves.toEqual(payload); + }); + + test("should return a stream() function returning a ReadableStream", async () => { + const body = getResponseBody(new Response()); + + await expect(body.stream()).resolves.toBeInstanceOf(ReadableStream); + }); + + test("should return a text() function returning a string", async () => { + const body = getResponseBody(new Response("some text")); + + await expect(body.text()).resolves.toEqual("some text"); + }); + }); +}); diff --git a/packages/httio/tests/http/pipeline.test.ts b/packages/httio/tests/http/pipeline.test.ts new file mode 100644 index 0000000..02a0a26 --- /dev/null +++ b/packages/httio/tests/http/pipeline.test.ts @@ -0,0 +1,87 @@ +import { pipeline } from "~/http/pipeline"; +import type { Middleware } from "~/types/pipeline"; +import { delay } from "~/utils/timer"; + +describe("Http Pipeline", () => { + const url = "https://example.com/"; + const request = new Request(url); + const response = new Response("OK"); + const fetcher = jest.fn(async () => response); + + afterEach(() => { + fetcher.mockClear(); + }); + + test("should work without middlewares", async () => { + const handle = pipeline([], fetcher); + + await handle(request); + + expect(fetcher).toHaveBeenCalledTimes(1); + expect(fetcher).toHaveBeenCalledWith(expect.objectContaining({ url })); + }); + + test("should work with middlewares", async () => { + const middleware: Middleware = jest.fn((req, next) => next(req)); + + const handle = pipeline([middleware], fetcher); + + await handle(request); + + expect(middleware).toHaveBeenCalledTimes(1); + }); + + test("should handle http errors", async () => { + fetcher.mockResolvedValue(new Response("Not Found", { status: 404 })); + + const handle = pipeline([], fetcher); + + await expect(handle(request)).rejects.toThrow(); + }); + + test("should handle errors in middleware", async () => { + const middleware: Middleware = jest.fn(() => { + throw new Error("Middleware error"); + }); + + const handle = pipeline([middleware], fetcher); + + await expect(handle(request)).rejects.toThrow("Middleware error"); + expect(fetcher).not.toHaveBeenCalled(); + }); + + test("should handle timeout", async () => { + fetcher.mockResolvedValue(delay(1000).then(() => new Response("OK", { status: 200 }))); + + const handle = pipeline([], fetcher, { timeout: 50 }); + + await expect(handle(request)).rejects.toThrow(); + expect(fetcher).toHaveBeenCalledTimes(1); + }); + + test("should handle retries", async () => { + const retry = 1; + + fetcher.mockResolvedValue(new Response("Too Many Requests", { status: 429 })); + + const handle = pipeline([], fetcher, { + retry: { delay: 50, limit: retry }, + }); + + await expect(handle(request)).rejects.toThrow(); + expect(fetcher).toHaveBeenCalledTimes(retry); + }); + + test("should handle retries", async () => { + const retry = 1; + + fetcher.mockResolvedValue(new Response("Too Many Requests", { status: 429 })); + + const handle = pipeline([], fetcher, { + retry, + }); + + await expect(handle(request)).rejects.toThrow(); + expect(fetcher).toHaveBeenCalledTimes(retry); + }); +}); diff --git a/packages/httio/tests/http/request.test.ts b/packages/httio/tests/http/request.test.ts new file mode 100644 index 0000000..55b1196 --- /dev/null +++ b/packages/httio/tests/http/request.test.ts @@ -0,0 +1,96 @@ +import { request } from "~/http/request"; +import { isHttioRequest } from "~/utils/validate"; + +describe("Http Request", () => { + describe("creating HttioRequest instances", () => { + test("should create an HttioRequest from a Request object", () => { + const origin = new Request("https://example.com"); + const req = request(origin); + + expect(isHttioRequest(req)).toBe(true); + }); + + test("should create an HttioRequest from a URL object", () => { + const origin = new URL("https://example.com"); + const req = request(origin); + + expect(isHttioRequest(req)).toBe(true); + }); + + test("should create an HttioRequest from a string URL", () => { + const url = "https://example.com/"; + const req = request(url); + + expect(isHttioRequest(req)).toBe(true); + }); + + test("should return the same HttioRequest when passed as input", () => { + const instance = request("https://example.com"); + const req = request(instance); + + expect(req).toBe(instance); + }); + }); + + describe("clone() method", () => { + test("should create a new HttioRequest instance with identical properties", () => { + const origin = request("https://example.com"); + const cloned = origin.clone(); + + expect(cloned).not.toBe(origin); + expect(cloned.url.toString()).toBe(origin.url.toString()); + }); + + test("should not modify the original instance", () => { + const origin = request("https://example.com"); + const cloned = origin.clone(); + + cloned.headers.set("X-Test", "value"); + + expect(origin.headers.has("X-Test")).toBe(false); + expect(cloned.headers.has("X-Test")).toBe(true); + }); + }); + + describe("toRequest() method", () => { + test("should create a standard Request object with the same URL", () => { + const origin = request("https://example.com"); + const res = origin.toRequest(); + + expect(res).toBeInstanceOf(Request); + expect(res.url).toBe(origin.url.toString()); + }); + + test("should correctly set body and headers", () => { + const origin = request("https://example.com", { + body: JSON.stringify({ test: "value" }), + headers: { "Content-Type": "application/json" }, + method: "POST", + }); + const res = origin.toRequest(); + + expect(res.body).toBeDefined(); + expect(res.headers.get("Content-Type")).toBe("application/json"); + }); + }); + + describe("Headers", () => { + const types = [ + ["application/json", null], + ["application/json", undefined], + ["application/json", { key: "value" }], + ["text/plain", "test body"], + ["application/x-www-form-urlencoded", new URLSearchParams()], + ["multipart/form-data", new FormData()], + ["application/octet-stream", new Blob()], + ["application/octet-stream", new ReadableStream()], + ["application/octet-stream", new ArrayBuffer()], + ] as const; + + test.each(types)("should set Content-Type header to %s for given body type", (type, body) => { + const req = request("https://example.com", { body, method: "POST" }).toRequest(); + + expect(req.headers.get("Content-Type")).toContain(type); + }); + }); +}); diff --git a/packages/httio/tests/http/response.test.ts b/packages/httio/tests/http/response.test.ts new file mode 100644 index 0000000..fb02547 --- /dev/null +++ b/packages/httio/tests/http/response.test.ts @@ -0,0 +1,40 @@ +import { response } from "~/http/response"; +import { isHttioResponse } from "~/utils/validate"; + +describe("Http Response", () => { + describe("creating HttioResponse instances", () => { + test("should create an HttioResponse from a Response object", () => { + const origin = new Response("test payload"); + const res = response(origin); + + expect(isHttioResponse(res)).toBe(true); + }); + + test("should return the same HttioResponse when passed as input", () => { + const instance = response("https://example.com"); + const res = response(instance); + + expect(res).toBe(instance); + }); + }); + + describe("clone() method", () => { + test("should create a new HttioResponse instance with identical properties", () => { + const origin = response(); + const cloned = origin.clone(); + + expect(cloned).not.toBe(origin); + expect(isHttioResponse(cloned)).toBe(true); + }); + + test("should not modify the original instance", () => { + const origin = response(); + const cloned = origin.clone(); + + cloned.headers.set("X-Test", "value"); + + expect(origin.headers.has("X-Test")).toBe(false); + expect(cloned.headers.has("X-Test")).toBe(true); + }); + }); +}); diff --git a/packages/httio/tests/server.ts b/packages/httio/tests/server.ts new file mode 100644 index 0000000..27ce05d --- /dev/null +++ b/packages/httio/tests/server.ts @@ -0,0 +1,100 @@ +/* istanbul ignore file */ + +import express from "express"; +import type http from "http"; + +let server: ReturnType; +const app = express(); + +function payload(request: http.IncomingMessage) { + if (!request.url) { + throw new Error("Request url is not defined"); + } + + const url = new URL(request.url, hostname()); + + return { + args: Object.fromEntries(url.searchParams.entries()), + headers: request.headers, + url: `${url.protocol}//${url.host}${url.pathname}`, + }; +} + +// ==== METHODS ==== +app.delete("/delete", (req, res) => { + res.json(payload(req)); +}); + +app.get("/get", (req, res) => { + res.json(payload(req)); +}); + +app.head("/head", (_req, res) => { + res.end(); +}); + +app.options("/options", (req, res) => { + res.json(payload(req)); +}); + +app.patch("/patch", (req, res) => { + res.json(payload(req)); +}); + +app.post("/post", (req, res) => { + res.json(payload(req)); +}); + +app.put("/put", (req, res) => { + res.json(payload(req)); +}); + +// ==== HEADERS ==== +app.all("/headers", (req, res) => { + const data = payload(req); + + res.json({ headers: data.args }); +}); + +// ==== UTILITIES ==== +app.all("/status/:status", (req, res) => { + const status = Number(req.params.status); + + res.status(status).json({ status }); +}); + +app.all("/delay/:delay", (req, res) => { + const s = Number(req.params.delay); + + setTimeout(() => res.json({ delay: s }), s * 1000); +}); + +app.all("/redirect", (req, res) => { + const url = payload(req).args.url; + + if (!url) { + throw new Error("Redirect url is not defined"); + } + + res.redirect(url); +}); + +function hostname(): string { + const address = server?.address() || null; + + if (address === null || typeof address === "string") { + throw new Error("Server is not listening"); + } + + return `http://127.0.0.1:${address.port}`; +} + +export function start() { + server = app.listen(); + + return hostname(); +} + +export function stop() { + return server?.close(); +} diff --git a/packages/httio/tests/unit/client.test.ts b/packages/httio/tests/unit/client.test.ts deleted file mode 100644 index f58ca3e..0000000 --- a/packages/httio/tests/unit/client.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import client from "~/client"; -import type { HttioClientOptions } from "~/types/client"; -import type { Json } from "~/types/data"; -import type { Fetcher } from "~/types/fetch"; - -describe("HttioClient", () => { - const mockUrl = "https://example.com"; - let mockFetch: jest.MockedFunction; - let mockOptions: HttioClientOptions; - - beforeEach(() => { - mockFetch = jest.fn(async (req: Request) => { - return new Response(req.body, { - status: 200, - }); - }); - - mockOptions = { - fetch: mockFetch, - url: mockUrl, - }; - }); - - test("should initialize client with default options", () => { - const instance = client(); - - expect(instance).toBeDefined(); - expect(instance).toHaveProperty("delete"); - expect(instance).toHaveProperty("extends"); - expect(instance).toHaveProperty("get"); - expect(instance).toHaveProperty("head"); - expect(instance).toHaveProperty("options"); - expect(instance).toHaveProperty("patch"); - expect(instance).toHaveProperty("post"); - expect(instance).toHaveProperty("put"); - expect(instance).toHaveProperty("use"); - }); - - const methods: [method: string, body: Json][] = [ - ["GET", null], - ["POST", { key: "value" }], - ["PUT", { key: "value" }], - ["PATCH", { key: "value" }], - ["DELETE", null], - ["HEAD", null], - ["OPTIONS", null], - ]; - - test.each(methods)("should send a %s request with the correct URL and options", async (method, body) => { - // @ts-expect-error --- - const res = client(mockOptions)[method.toLowerCase()]("/test", body || {}); - - expect(mockFetch).toHaveBeenCalled(); - - const req = mockFetch.mock.calls[0]![0]; - expect(req.method).toBe(method); - expect(req.url).toBe(`${mockUrl}/test`); - - if (body !== null) { - expect(await res.json()).toEqual(body); - } - }); - - test("should extend the client with new options", async () => { - client(mockOptions).extends({ url: "/extended" }).get("/test"); - - expect(mockFetch).toHaveBeenCalled(); - - const req = mockFetch.mock.calls[0]![0]; - expect(req.method).toBe("GET"); - expect(req.url).toBe(`${mockUrl}/extended/test`); - }); - - test("should correctly perform request when full URL is passed to the method and client has no base URL", async () => { - delete mockOptions.url; - - client(mockOptions).get(mockUrl); - - expect(mockFetch).toHaveBeenCalled(); - - const req = mockFetch.mock.calls[0]![0]; - - expect(req.method).toBe("GET"); - expect(req.url).toBe(new URL(mockUrl).toString()); - }); - - // test("should throw an error for non-ok responses", async () => { - // const resp = new Response("Not Found", { - // status: 404, - // }); - // - // mockFetch.mockResolvedValue(resp); - // - // await expect(client(mockOptions).get("/fail").json()).rejects.toThrow(HttpError); - // }); - - test("should allow using middlewares via use()", async () => { - const middleware = jest.fn((req, next) => next(req)); - - client(mockOptions).use(middleware).get("/test"); - - expect(mockFetch).toHaveBeenCalled(); - expect(middleware).toHaveBeenCalled(); - }); -}); diff --git a/packages/httio/tests/unit/errors/http.test.ts b/packages/httio/tests/unit/errors/http.test.ts deleted file mode 100644 index 0aaad69..0000000 --- a/packages/httio/tests/unit/errors/http.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import HttpError from "~/errors/http"; -import type { HttioRequest } from "~/types/request"; -import type { HttioResponse } from "~/types/response"; - -describe("HttpError", () => { - const mockRequest = { - toString: () => "MockRequest", - }; - - const mockResponse = { - toString: () => "MockResponse", - }; - - test("should default the message to the response.toString() if no message is provided", () => { - const error = new HttpError(undefined, mockRequest, mockResponse); - - expect(error.message).toBe("MockResponse"); - }); -}); diff --git a/packages/httio/tests/unit/http/body.test.ts b/packages/httio/tests/unit/http/body.test.ts deleted file mode 100644 index 64c48d5..0000000 --- a/packages/httio/tests/unit/http/body.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import body from "~/http/body"; - -describe("body function", () => { - const mockMethods = [ - ["arrayBuffer", new ArrayBuffer(10)], - ["blob", new Blob(["test"])], - ["bytes", new Uint8Array([1, 2, 3])], - ["json", { key: "value" }], - ["text", "test string"], - ] as const; - - test.each(mockMethods)("should include the %s method", async (property, payload) => { - const res = mockMethods.reduce((response, [method, value]) => { - response[method as never] = jest.fn(() => Promise.resolve(value)) as never; - - return response; - }, {} as Response); - - const methods = body(Promise.resolve([res])); - - expect(methods).toHaveProperty(property); - - const method = methods[property]; - - await expect(method()).resolves.toBe(payload); - }); - - test("should throw an error", async () => { - const error = new Error("Test error"); - const methods = body(Promise.resolve([null as never, error])); - - await expect(methods.json()).rejects.toThrow(error); - }); -}); diff --git a/packages/httio/tests/unit/http/pipeline.test.ts b/packages/httio/tests/unit/http/pipeline.test.ts deleted file mode 100644 index ed72b31..0000000 --- a/packages/httio/tests/unit/http/pipeline.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import pipeline from "~/http/pipeline"; -import type { Middleware } from "~/types/pipeline"; - -describe("pipeline", () => { - const mockFetcher = jest.fn(); - - beforeEach(() => { - mockFetcher.mockReset(); - }); - - it("should add middleware via use()", () => { - const mockMiddleware = jest.fn(); - const pipe = pipeline(mockFetcher); - - pipe.use(mockMiddleware); - - expect(pipe.pipes).toContain(mockMiddleware); - }); - - it("should execute fetch when handle is called", async () => { - mockFetcher.mockResolvedValueOnce(new Response("test response")); - - pipeline(mockFetcher).handle("https://example.com", { - method: "GET", - }); - - expect(mockFetcher).toHaveBeenCalledTimes(1); - }); - - it("should execute middleware in order", async () => { - const middleware1 = jest.fn((req, next) => next(req)); - const middleware2 = jest.fn((req, next) => next(req)); - - mockFetcher.mockResolvedValueOnce(new Response("test response")); - - const pipe = pipeline(mockFetcher).use(middleware1, middleware2); - - pipe.handle("https://example.com", { method: "GET" }); - - expect(middleware1).toHaveBeenCalledTimes(1); - expect(middleware2).toHaveBeenCalledTimes(1); - }); - - it("should transform request in middleware", async () => { - let headers = new Headers({ - "Content-Type": "application/json", - }); - - const update: Middleware = jest.fn((req, next) => { - req.headers.set("X-Test", "test"); - - return next(req); - }); - - const replace: Middleware = (req, next) => { - headers = req.headers; - - return next(req); - }; - - mockFetcher.mockResolvedValueOnce(new Response("test response")); - - expect(headers.get("X-Test")).toBeNull(); - - const pipe = pipeline(mockFetcher).use(update, replace); - - await pipe.handle("https://example.com", { - headers, - method: "GET", - }); - - expect(update).toHaveBeenCalledTimes(1); - expect(headers.get("X-Test")).toBe("test"); - }); - - it("should transform response in middleware", async () => { - const middleware: Middleware = jest.fn(async () => { - return "transformed response"; - }); - - mockFetcher.mockResolvedValueOnce(new Response("test response")); - - const pipe = pipeline(mockFetcher).use(middleware); - - const response = await pipe.handle("https://example.com", { - method: "GET", - }); - - expect(middleware).toHaveBeenCalledTimes(1); - - expect(await response.text()).toBe("transformed response"); - }); -}); diff --git a/packages/httio/tests/unit/http/request.test.ts b/packages/httio/tests/unit/http/request.test.ts deleted file mode 100644 index ecc29f2..0000000 --- a/packages/httio/tests/unit/http/request.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import request from "~/http/request"; -import type { HttioRequestInit } from "~/types/request"; - -describe("request function", () => { - const mockUrl = "https://example.com/api"; - let mockOptions: HttioRequestInit; - - beforeEach(() => { - mockOptions = { - body: JSON.stringify({ key: "value" }), - credentials: "include", - headers: { - Authorization: "Bearer token", - "Content-Type": "application/json", - }, - method: "GET", - }; - }); - - test("should create an HttioRequest with proper fields", () => { - const result = request(mockUrl, mockOptions); - - expect(result.url).toBeInstanceOf(URL); - expect(result.url.toString()).toBe(mockUrl); - expect(result.headers).toBeInstanceOf(Headers); - expect(result.headers.get("Content-Type")).toBe("application/json"); - expect(result.headers.get("Authorization")).toBe("Bearer token"); - expect(result.method).toBe("GET"); - expect(result.body).toBe(mockOptions.body); - expect(result.credentials).toBe(mockOptions.credentials); - }); - - test("should accept a URL object as the 'url' parameter", () => { - const urlObj = new URL(mockUrl); - const result = request(urlObj, mockOptions); - - expect(result.url).toBeInstanceOf(URL); - expect(result.url.toString()).toBe(mockUrl); - }); - - test("should correctly implement the 'toString' method", () => { - const result = request(mockUrl, mockOptions); - expect(result.toString()).toBe("[GET] https://example.com/api"); - }); -}); diff --git a/packages/httio/tests/unit/http/response.test.ts b/packages/httio/tests/unit/http/response.test.ts deleted file mode 100644 index 9e9cf5b..0000000 --- a/packages/httio/tests/unit/http/response.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import response from "~/http/response"; -import type { ResponseInstance } from "~/types/response"; - -describe("response function", () => { - const mockUrl = new URL("https://example.com"); - const mockMethod = "GET"; - let mockFactory: jest.Mock>; - let mockResponse: ResponseInstance; - - beforeEach(() => { - const resp = new Response(JSON.stringify({ test: "value" }), { - headers: new Headers({ "Content-Type": "application/json" }), - }); - - mockFactory = jest.fn().mockResolvedValue(resp); - mockResponse = response(mockUrl, mockMethod, mockFactory); - }); - - test("should throw an error when factory rejects", async () => { - mockFactory = jest.fn().mockRejectedValue(new Error("Network Error")); - const rejectedResponse = response(mockUrl, mockMethod, mockFactory); - - await expect(rejectedResponse).rejects.toThrow("Network Error"); - }); - - test("should use the correct URL from the response object", async () => { - const resp = new Response(JSON.stringify({ test: "value" }), { - headers: new Headers({ "Content-Type": "application/json" }), - }); - Object.defineProperty(resp, "url", { value: "https://example.com/resource" }); - - mockFactory = jest.fn().mockResolvedValue(resp); - mockResponse = response(mockUrl, mockMethod, mockFactory); - - const result = await mockResponse; - - expect(result.url.toString()).toBe("https://example.com/resource"); - }); - - test("should return correct toString() representation", async () => { - const result = await mockResponse; - - expect(result.toString()).toBe("[GET] https://example.com/: 200 OK"); - }); -}); diff --git a/packages/httio/tests/unit/middleware/normalize.test.ts b/packages/httio/tests/unit/middleware/normalize.test.ts deleted file mode 100644 index ad256ea..0000000 --- a/packages/httio/tests/unit/middleware/normalize.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import normalize from "~/middleware/normalize"; -import type { HttioRequest } from "~/types/request"; - -describe("normalize middleware", () => { - let mockRequest: HttioRequest; - const mockNextMiddleware = jest.fn(); - - beforeEach(() => { - mockRequest = { - body: null, - headers: new Headers(), - method: "GET", - url: new URL("https://example.com"), - }; - - mockNextMiddleware.mockClear(); - }); - - it("should set default Accept header if not present", () => { - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Accept")).toBe("text/*,image/*,application/json,application/octet-stream"); - }); - - it("should not override existing Accept header", () => { - mockRequest.headers.set("Accept", "application/xml"); - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Accept")).toBe("application/xml"); - }); - - it("should set default Content-Type header based on body type if not present", () => { - mockRequest.body = { key: "value" }; - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Content-Type")).toBe("application/json"); - expect(mockRequest.body).toBe(JSON.stringify({ key: "value" })); - }); - - it("should set Content-Type to text/plain; charset=utf-8 for string body", () => { - mockRequest.body = "string body"; - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Content-Type")).toBe("text/plain; charset=utf-8"); - }); - - it("should set Content-Type to application/x-www-form-urlencoded for URLSearchParams body", () => { - mockRequest.body = new URLSearchParams("key=value"); - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Content-Type")).toBe("application/x-www-form-urlencoded"); - }); - - it("should set Content-Type to multipart/form-data for FormData body", () => { - mockRequest.body = new FormData(); - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Content-Type")).toBe("multipart/form-data"); - }); - - it("should set Content-Type to application/octet-stream for Blob body", () => { - mockRequest.body = new Blob(["test"]); - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Content-Type")).toBe("application/octet-stream"); - }); - - it("should add Content-length header for string body if not present", () => { - mockRequest.body = "string body"; - - normalize(mockRequest, mockNextMiddleware); - - expect(mockRequest.headers.get("Content-length")).toBe("11"); - }); - - it("should call next middleware with the modified request", () => { - normalize(mockRequest, mockNextMiddleware); - - expect(mockNextMiddleware).toHaveBeenCalledWith(mockRequest); - expect(mockNextMiddleware).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/httio/tests/unit/utils/merge.test.ts b/packages/httio/tests/unit/utils/merge.test.ts deleted file mode 100644 index 8883500..0000000 --- a/packages/httio/tests/unit/utils/merge.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import merge from "~/utils/merge"; - -describe("merge", () => { - test("should merge two plain objects with primitive properties", () => { - const target = { a: 1, b: 2 }; - const source = { b: 3, c: 4 }; - - const result = merge(target, source); - - expect(result).toEqual({ a: 1, b: 3, c: 4 }); - }); - - test("should handle objects with symbol keys", () => { - const target = { [Symbol("key")]: "value1" }; - const source = { [Symbol("key")]: "value2" }; - - const result = merge(target, source); - - expect(Object.getOwnPropertySymbols(result).length).toBe(2); - }); - - test("should handle objects with circular references in source", () => { - const target = {}; - const source: any = { key: "value" }; - source.self = source; // Create circular reference - - const result = merge(target, source); - - expect(result.key).toBe("value"); - expect(result.self).toStrictEqual(result); // Result should also have the circular reference - }); -}); diff --git a/packages/httio/tests/unit/utils/pick.test.ts b/packages/httio/tests/unit/utils/pick.test.ts deleted file mode 100644 index 63d9d33..0000000 --- a/packages/httio/tests/unit/utils/pick.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import pick from "~/utils/pick"; - -describe("pick", () => { - test("should select specified properties from an object", () => { - const obj = { - age: 30, - city: "New York", - country: "USA", - name: "John", - }; - - const result = pick(obj, "name", "age"); - - expect(result).toEqual({ - age: 30, - name: "John", - }); - }); - - test("should return an empty object when no keys are specified", () => { - const obj = { - age: 30, - name: "John", - }; - - const result = pick(obj); - - expect(result).toEqual({}); - }); - - test("should work correctly with objects containing undefined property values", () => { - const obj = { - age: undefined, - city: "New York", - name: "John", - }; - - const result = pick(obj, "name", "age", "city"); - - expect(result).toEqual({ - age: undefined, - city: "New York", - name: "John", - }); - }); - - test("should work with objects having nested properties", () => { - const obj = { - address: { - city: "New York", - country: "USA", - }, - user: { - age: 30, - name: "John", - }, - }; - - const result = pick(obj, "user", "address"); - - expect(result).toEqual({ - address: { - city: "New York", - country: "USA", - }, - user: { - age: 30, - name: "John", - }, - }); - }); - - test("should preserve references to nested objects", () => { - const nestedObj = { name: "John" }; - const obj = { - age: 30, - user: nestedObj, - }; - - const result = pick(obj, "user"); - - expect(result.user).toBe(nestedObj); - }); -}); diff --git a/packages/httio/tests/unit/utils/sleep.test.ts b/packages/httio/tests/unit/utils/sleep.test.ts deleted file mode 100644 index 47536f1..0000000 --- a/packages/httio/tests/unit/utils/sleep.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import sleep from "~/utils/sleep"; - -describe("sleep", () => { - test("should create a delay for the specified number of milliseconds", async () => { - const startTime = Date.now(); - const delay = 100; - - await sleep(delay); - - const endTime = Date.now(); - const elapsed = endTime - startTime; - - expect(elapsed).toBeGreaterThanOrEqual(delay); - expect(elapsed).toBeLessThan(delay + 50); - }); - - test("should allow creating chains of delays", async () => { - const startTime = Date.now(); - const delay = 50; - - await sleep(delay); - await sleep(delay); - await sleep(delay); - - const endTime = Date.now(); - const elapsed = endTime - startTime; - - const totalDelay = delay * 3; - expect(elapsed).toBeGreaterThanOrEqual(totalDelay); - expect(elapsed).toBeLessThan(totalDelay + 75); - }); - - test("should return a Promise that resolves to undefined", async () => { - const result = await sleep(10); - expect(result).toBeUndefined(); - }); - - test("should work correctly with zero delay", async () => { - const startTime = Date.now(); - - await sleep(0); - - const endTime = Date.now(); - const elapsed = endTime - startTime; - - expect(elapsed).toBeGreaterThanOrEqual(0); - expect(elapsed).toBeLessThan(50); - }); - - test("should handle negative values correctly", async () => { - const startTime = Date.now(); - - await sleep(-100); - - const endTime = Date.now(); - const elapsed = endTime - startTime; - - expect(elapsed).toBeGreaterThanOrEqual(0); - expect(elapsed).toBeLessThan(50); - }); -}); diff --git a/packages/httio/tests/unit/utils/url.test.ts b/packages/httio/tests/unit/utils/url.test.ts deleted file mode 100644 index 0615f67..0000000 --- a/packages/httio/tests/unit/utils/url.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { QueryParams } from "~/types/fetch"; -import url from "~/utils/url"; - -describe("url", () => { - test("should create a URL from string", () => { - const result = url("https://example.com", ""); - - expect(result.toString()).toBe("https://example.com/"); - }); - - test("should create a URL from URL instance", () => { - const baseUrl = new URL("https://example.com"); - const result = url(baseUrl, ""); - - expect(result.toString()).toBe("https://example.com/"); - }); - - test("should handle path parameter correctly", () => { - const result = url("https://example.com", "/api/users"); - - expect(result.toString()).toBe("https://example.com/api/users"); - }); - - test("should handle relative path", () => { - const result = url("https://example.com/base", "users"); - - expect(result.toString()).toBe("https://example.com/base/users"); - }); - - test("should handle path with double slashes", () => { - const result = url("https://example.com/base/", "/users"); - - expect(result.toString()).toBe("https://example.com/base/users"); - }); - - test("should remove trailing slash from path", () => { - const result = url("https://example.com", "/api/users/"); - - expect(result.toString()).toBe("https://example.com/api/users"); - }); - - test("should override base URL if path contains protocol", () => { - const result = url("https://example.com/base", "https://api.example.com/users"); - - expect(result.toString()).toBe("https://api.example.com/users"); - }); - - test("should merge search parameters from base and path", () => { - const result = url("https://example.com?sort=asc", "/api/users?page=1"); - expect(result.toString()).toBe("https://example.com/api/users?sort=asc&page=1"); - }); - - test("should add simple query parameters", () => { - const params: QueryParams = { - active: true, - limit: 10, - page: 1, - search: "test", - }; - - const result = url("https://example.com", "/api/users", params); - - expect(result.searchParams.get("page")).toBe("1"); - expect(result.searchParams.get("limit")).toBe("10"); - expect(result.searchParams.get("search")).toBe("test"); - expect(result.searchParams.get("active")).toBe("true"); - }); - - test("should handle nested object query parameters", () => { - const params: QueryParams = { - filter: { - role: "admin", - status: "active", - }, - }; - - const result = url("https://example.com", "/api/users", params); - - expect(result.searchParams.get("filter[status]")).toBe("active"); - expect(result.searchParams.get("filter[role]")).toBe("admin"); - }); - - test("should handle array query parameters", () => { - const params: QueryParams = { - ids: [1, 2, 3], - }; - - const result = url("https://example.com", "/api/users", params); - - expect(result.searchParams.get("ids[1]")).toBe("1"); - expect(result.searchParams.get("ids[2]")).toBe("2"); - expect(result.searchParams.get("ids[3]")).toBe("3"); - }); - - test("should handle complex nested array and object query parameters", () => { - const params: QueryParams = { - filters: [ - { name: "status", value: "active" }, - { name: "role", value: "admin" }, - ], - }; - - const result = url("https://example.com", "/api/users", params); - - expect(result.searchParams.get("filters[1][name]")).toBe("status"); - expect(result.searchParams.get("filters[1][value]")).toBe("active"); - expect(result.searchParams.get("filters[2][name]")).toBe("role"); - expect(result.searchParams.get("filters[2][value]")).toBe("admin"); - }); - - test("should handle null and undefined values", () => { - const params: QueryParams = { - filter: null, - page: 1, - sort: undefined, - }; - - const result = url("https://example.com", "/api/users", params); - - expect(result.searchParams.get("page")).toBe("1"); - expect(result.searchParams.has("sort")).toBe(false); - expect(result.searchParams.has("filter")).toBe(false); - }); - - test("should handle arrays with mixed types", () => { - const params: QueryParams = { - items: ["string", 123, true, { name: "object" }], - }; - - const result = url("https://example.com", "/api/users", params); - - expect(result.searchParams.get("items[1]")).toBe("string"); - expect(result.searchParams.get("items[2]")).toBe("123"); - expect(result.searchParams.get("items[3]")).toBe("true"); - expect(result.searchParams.get("items[4][name]")).toBe("object"); - }); -}); diff --git a/packages/httio/tests/unit/utils/validate.test.ts b/packages/httio/tests/unit/utils/validate.test.ts deleted file mode 100644 index 449cc91..0000000 --- a/packages/httio/tests/unit/utils/validate.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { REQUEST, RESPONSE } from "~/utils/consts"; -import { isArray, isHttioRequest, isHttioResponse, isPlaneObject, isString, type } from "~/utils/validate"; - -describe("validate utils", () => { - test("type function returns correct type for different values", () => { - expect(type("")).toBe("String"); - expect(type("hello")).toBe("String"); - expect(type(0)).toBe("Number"); - expect(type(123)).toBe("Number"); - expect(type(123.45)).toBe("Number"); - expect(type(true)).toBe("Boolean"); - expect(type(false)).toBe("Boolean"); - expect(type(null)).toBe("Null"); - expect(type(undefined)).toBe("Undefined"); - expect(type({})).toBe("Object"); - expect(type({ name: "test" })).toBe("Object"); - expect(type([])).toBe("Array"); - expect(type([1, 2, 3])).toBe("Array"); - expect(type(new Date())).toBe("Date"); - expect(type(new RegExp(""))).toBe("RegExp"); - expect(type(() => {})).toBe("Function"); - expect(type(new Map())).toBe("Map"); - expect(type(new Set())).toBe("Set"); - }); - - test("isArray function correctly identifies arrays", () => { - expect(isArray([])).toBe(true); - expect(isArray([1, 2, 3])).toBe(true); - expect(isArray([])).toBe(true); - expect(isArray(Array.from("abc"))).toBe(true); - - expect(isArray(null)).toBe(false); - expect(isArray(undefined)).toBe(false); - expect(isArray("")).toBe(false); - expect(isArray("string")).toBe(false); - expect(isArray(123)).toBe(false); - expect(isArray({})).toBe(false); - expect(isArray({ length: 1 })).toBe(false); - expect(isArray(true)).toBe(false); - expect(isArray(false)).toBe(false); - expect(isArray(new Set())).toBe(false); - expect(isArray(new Map())).toBe(false); - }); - - test("isHttioRequest function correctly identifies HttioRequest", () => { - expect(isHttioRequest({ [REQUEST]: REQUEST })).toBe(true); - - expect(isHttioRequest({})).toBe(false); - expect(isHttioRequest(null)).toBe(false); - expect(isHttioRequest({ [RESPONSE]: RESPONSE })).toBe(false); - expect(isHttioRequest([])).toBe(false); - }); - - test("isHttioResponse function correctly identifies HttioResponse", () => { - expect(isHttioResponse({ [RESPONSE]: RESPONSE })).toBe(true); - - expect(isHttioResponse({})).toBe(false); - expect(isHttioResponse(null)).toBe(false); - expect(isHttioResponse({ [REQUEST]: REQUEST })).toBe(false); - expect(isHttioResponse([])).toBe(false); - }); - - test("isPlaneObject function correctly identifies plain objects", () => { - expect(isPlaneObject({})).toBe(true); - expect(isPlaneObject({ key: "value" })).toBe(true); - - expect(isPlaneObject(null)).toBe(false); - expect(isPlaneObject(Object.create(null))).toBe(false); - expect(isPlaneObject(undefined)).toBe(false); - expect(isPlaneObject([])).toBe(false); - expect(isPlaneObject(new Date())).toBe(false); - expect(isPlaneObject(new Map())).toBe(false); - expect(isPlaneObject(new Set())).toBe(false); - expect(isPlaneObject(() => {})).toBe(false); - expect(isPlaneObject(new RegExp(""))).toBe(false); - }); - - test("isString function correctly identifies strings", () => { - expect(isString("")).toBe(true); - expect(isString("hello")).toBe(true); - expect(isString(String("test"))).toBe(true); - expect(isString(new String("test"))).toBe(true); - expect(isString(`template literal`)).toBe(true); - - expect(isString(null)).toBe(false); - expect(isString(undefined)).toBe(false); - expect(isString(123)).toBe(false); - expect(isString({})).toBe(false); - expect(isString([])).toBe(false); - expect(isString(true)).toBe(false); - expect(isString(false)).toBe(false); - expect(isString(new Date())).toBe(false); - expect(isString(new Set())).toBe(false); - expect(isString(new Map())).toBe(false); - }); -}); diff --git a/packages/httio/tests/utils/object.test.ts b/packages/httio/tests/utils/object.test.ts new file mode 100644 index 0000000..696447d --- /dev/null +++ b/packages/httio/tests/utils/object.test.ts @@ -0,0 +1,88 @@ +import { clone, merge } from "~/utils/object"; + +describe("Object utilities", () => { + describe("merge() function", () => { + test("should merge two plain objects deeply", () => { + const target = { a: 1, b: { c: 2 } }; + const source = { b: { d: 3 }, e: 4 }; + + const result = merge(target, source); + + expect(result).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4 }); + }); + + test("should merge objects containing symbols as keys", () => { + const sym = Symbol("sym"); + const target = { a: 1, [sym]: { x: 1 } }; + const source = { b: 2, [sym]: { y: 2 } }; + + const result = merge(target, source); + + expect(result).toEqual({ a: 1, b: 2, [sym]: { x: 1, y: 2 } }); + }); + + test("should replace arrays instead of merging them", () => { + const target = { a: [1, 2] }; + const source = { a: [3, 4] }; + + const result = merge(target, source); + + expect(result).toEqual({ a: [3, 4] }); + }); + + test("should return the source if target is not a plain object", () => { + const target = null; + const source = { a: 1 }; + + const result = merge(target, source); + + expect(result).toEqual({ a: 1 }); + }); + + test("should merge objects with overlapping but non-conflicting keys", () => { + const target = { a: 1, b: 2 }; + const source = { c: 3, d: 4 }; + + const result = merge(target, source); + + expect(result).toEqual({ a: 1, b: 2, c: 3, d: 4 }); + }); + }); + + describe("clone() function", () => { + test("should clone object with specified keys", () => { + const source = { a: 1, b: 2, c: 3 }; + const result = clone(source, "a", "c"); + + expect(result).toEqual({ a: 1, c: 3 }); + }); + + // test("should return an empty object if no keys are specified", () => { + // const source = { a: 1, b: 2, c: 3 }; + // const result = clone(source); + // + // expect(result).toEqual({}); + // }); + + test("should handle cloning all keys if no keys are specified", () => { + const source = { a: 1, b: 2, c: 3 }; + const result = clone(source); + + expect(result).toEqual({ a: 1, b: 2, c: 3 }); + }); + + test("should return an empty object for invalid keys", () => { + const source = { a: 1, b: 2, c: 3 }; + const result = clone(source, "x" as keyof typeof source); + + expect(result).toEqual({}); + }); + + test("should support cloning complex objects with nested arrays", () => { + const source = { a: { b: [1, 2] }, c: 3 }; + const result = clone(source, "a"); + + expect(result).toEqual({ a: { b: [1, 2] } }); + }); + }); +}); diff --git a/packages/httio/tests/utils/url.test.ts b/packages/httio/tests/utils/url.test.ts new file mode 100644 index 0000000..5487cf6 --- /dev/null +++ b/packages/httio/tests/utils/url.test.ts @@ -0,0 +1,54 @@ +import type { QueryParams } from "~/types/fetch"; +import { join, search } from "~/utils/url"; + +describe("URL utilities", () => { + describe("join() function", () => { + test("should join paths with a domain and handle slashes correctly", () => { + expect(join("http://example.com", "path/to/resource")).toBe("http://example.com/path/to/resource"); + expect(join("http://example.com/path", "/to/resource")).toBe("http://example.com/path/to/resource"); + }); + + test("should join multiple paths", () => { + expect(join("path/", "/to/", "/resource")).toBe("path/to/resource"); + expect(join("path", "to", "resource")).toBe("path/to/resource"); + }); + + test("should handle empty paths gracefully", () => { + expect(join("path", "", "to/resource")).toBe("path/to/resource"); + }); + + test("should handle paths with multiple consecutive slashes", () => { + expect(join("path//", "//to/", "///resource")).toBe("path/to/resource"); + }); + }); + + describe("search() function", () => { + test("should return an empty string if no parameters are provided", () => { + expect(search()).toBe(""); + }); + + test("should return a query string for simple key-value pairs", () => { + const params: QueryParams = { anotherKey: "anotherValue", key: "value" }; + + expect(search(params)).toBe("?anotherKey=anotherValue&key=value"); + }); + + test("should handle nested object parameters", () => { + const params: QueryParams = { + key: { + nestedKey: { + deepNestedKey: "deepNestedValue", + }, + }, + }; + + expect(search(params)).toBe(`?${encodeURI("key[nestedKey][deepNestedKey]")}=deepNestedValue`); + }); + + test("should handle array parameters", () => { + const params: QueryParams = { key: ["value1", "value2"] }; + + expect(search(params)).toBe(`?${encodeURI("key[0]")}=value1&${encodeURI("key[1]")}=value2`); + }); + }); +}); diff --git a/packages/httio/tsup.config.ts b/packages/httio/tsup.config.ts index 6c550d2..deea80b 100644 --- a/packages/httio/tsup.config.ts +++ b/packages/httio/tsup.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from "tsup"; -const entries = ["src/index.ts", "src/errors/index.ts"]; +const entries = ["src/index.ts"]; export default defineConfig((options) => [ { diff --git a/packages/httio/types/response.d.ts b/packages/httio/types/body.d.ts similarity index 51% rename from packages/httio/types/response.d.ts rename to packages/httio/types/body.d.ts index 91492c1..43a24a1 100644 --- a/packages/httio/types/response.d.ts +++ b/packages/httio/types/body.d.ts @@ -1,12 +1,10 @@ import type { Json } from "~/types/data"; -export type ResponseInstance = HttioBody & Promise; - export declare interface HttioBody { - arrayBuffer(): Promise; - blob(): Promise; + buffer(): Promise; + bytes(): Promise; json(): Promise; @@ -15,11 +13,3 @@ export declare interface HttioBody { text(): Promise; } - -export declare interface HttioResponse extends HttioBody { - headers: Headers; - status: number; - url: URL; - - toString(): string; -} diff --git a/packages/httio/types/client.d.ts b/packages/httio/types/client.d.ts index bd141d0..0bebb04 100644 --- a/packages/httio/types/client.d.ts +++ b/packages/httio/types/client.d.ts @@ -1,9 +1,8 @@ -import type { Json } from "~/types/data"; +import type { HttioBody } from "~/types/body"; +import type { Payload } from "~/types/data"; import type { Fetcher, QueryParams } from "~/types/fetch"; +import type { HttioResponse } from "~/types/http"; import type { Middleware } from "~/types/pipeline"; -import type { HttioRequestInit } from "~/types/request"; - -export type HttioMethodOptions = Omit; export declare interface HttioClient extends HttioClientMethods { extends(options: HttioClientOptions): HttioClient; @@ -12,23 +11,34 @@ export declare interface HttioClient extends HttioClientMethods { } export declare interface HttioClientMethods { - delete(url: string, options?: HttioMethodOptions): ResponseInstance; + delete(url: string, options?: HttioRequestOptions): HttioBody & Promise; - get(url: string, options?: HttioMethodOptions): ResponseInstance; + get(url: string, options?: HttioRequestOptions): HttioBody & Promise; - head(url: string, options?: HttioMethodOptions): ResponseInstance; + head(url: string, options?: HttioRequestOptions): HttioBody & Promise; - options(url: string, options?: HttioMethodOptions): ResponseInstance; + options(url: string, options?: HttioRequestOptions): HttioBody & Promise; - patch(url: string, payload?: BodyInit | Json, options?: HttioMethodOptions): ResponseInstance; + patch(url: string, payload?: Payload, options?: HttioRequestOptions): HttioBody & Promise; - post(url: string, payload?: BodyInit | Json, options?: HttioMethodOptions): ResponseInstance; + post(url: string, payload?: Payload, options?: HttioRequestOptions): HttioBody & Promise; - put(url: string, payload?: BodyInit | Json, options?: HttioMethodOptions): ResponseInstance; + put(url: string, payload?: Payload, options?: HttioRequestOptions): HttioBody & Promise; } -export declare interface HttioClientOptions extends Omit { +export declare interface HttioClientOptions extends HttioRequestOptions { fetch?: Fetcher; - query?: QueryParams; - url?: URL | string; + url?: string; +} + +export declare interface HttioRequestOptions extends Omit { + headers?: Record; + params?: QueryParams; + retry?: RetryOptions | number; + timeout?: number; +} + +export declare interface RetryOptions { + delay?: number; + limit?: number; } diff --git a/packages/httio/types/data.d.ts b/packages/httio/types/data.d.ts index 4a590f3..fcfd1c6 100644 --- a/packages/httio/types/data.d.ts +++ b/packages/httio/types/data.d.ts @@ -1,2 +1,5 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type Json = Json[] | Record | boolean | number | string | null; +export type Json = Json[] | Record | boolean | number | string | null; + +export type Payload = BodyInit | Record | null | undefined; + +export type PlaneObject = Record; diff --git a/packages/httio/types/http.d.ts b/packages/httio/types/http.d.ts new file mode 100644 index 0000000..6eb0b30 --- /dev/null +++ b/packages/httio/types/http.d.ts @@ -0,0 +1,28 @@ +import type { HttioBody } from "~/types/body"; +import type { Payload } from "~/types/data"; + +export declare interface HttioRequest { + body?: Payload; + headers: Headers; + method: string; + url: URL; + + clone(): HttioRequest; + toRequest(): Request; +} + +export declare interface HttioRequestInit extends Omit { + body?: Payload; +} + +export declare interface HttioResponse extends HttioBody { + readonly headers: Headers; + readonly ok: boolean; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly type: ResponseType; + readonly url: string; + + clone(): HttioResponse; +} diff --git a/packages/httio/types/pipeline.d.ts b/packages/httio/types/pipeline.d.ts index ef3b55a..2a100de 100644 --- a/packages/httio/types/pipeline.d.ts +++ b/packages/httio/types/pipeline.d.ts @@ -1,20 +1,15 @@ -import type { HttioRequest, HttioRequestInit } from "~/types/request"; -import type { HttioResponse, ResponseInstance } from "~/types/response"; +import type { HttioBody } from "~/types/body"; +import type { Payload } from "~/types/data"; +import type { HttioRequest, HttioResponse } from "~/types/http"; -export type MiddlewareResult = BodyInit | HttioResponse | Response | ResponseInstance | null; +type MaybePromise = Promise | T; export declare interface Middleware { - (request: HttioRequest, next: NextMiddleware): MiddlewareResult | Promise; + (request: HttioRequest, next: NextMiddleware): MaybePromise; } export declare interface NextMiddleware { - (request: HttioRequest): ResponseInstance; -} - -export declare interface Pipeline { - readonly pipes: Middleware[]; - - handle(url: URL | string, options: HttioRequestInit): ResponseInstance; + (request: HttioRequest | Request): HttioBody & Promise; - use(...middleware: Middleware[]): this; + (url: URL | string, init?: HttioRequestInit): HttioBody & Promise; } diff --git a/packages/httio/types/request.d.ts b/packages/httio/types/request.d.ts deleted file mode 100644 index 1609bd2..0000000 --- a/packages/httio/types/request.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Json } from "~/types/data"; - -export declare interface HttioRequest extends Omit { - headers: Headers; - url: URL; - - toString(): string; -} - -export declare interface HttioRequestInit extends Omit { - body?: BodyInit | Json; - headers?: Headers | Record; - method: string; -}