dexix-request 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2026-07-13
9
+
10
+ ### Added
11
+
12
+ - **Interceptors.** `dx.interceptors.request.use()` and `dx.interceptors.response.use()`, matching Axios's chaining model — mutate outgoing config, transform resolved data, or recover from a rejected request. Every interceptor returns an id that can be passed to `.eject()`.
13
+ - **Built-in retries.** `retries`, `retryDelay` (fixed or a `(attempt, error) => ms` function), and `retryOn` (custom predicate) on any request or on `dx.create()` defaults. Defaults to exponential backoff on `5xx`, `429`, network failures, and timeouts — no extra package required.
14
+ - **`dx` / `dexix` CLI.** A colorful command-line client bundled with the package (`npx dx get <url>`), supporting headers, query params, JSON bodies (inline or `@file.json`), timeouts, retries, `-i`/`-s` output modes, and colorized JSON output.
15
+ - A friendly, colorized welcome banner on `npm install` (skipped automatically in CI and non-interactive shells).
16
+
17
+ ### Changed
18
+
19
+ - Internal source is now comment-free; behavior is documented in the README and via descriptive naming instead.
20
+
8
21
  ## [1.0.0] - 2026-07-12
9
22
 
10
23
  ### Added
@@ -21,3 +34,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
34
  - Zero runtime dependencies. ESM and CommonJS builds, tree-shakeable, under 5KB gzipped.
22
35
  - `onRequest` / `onResponse` hooks for logging, auth injection, and observability.
23
36
  - Full test suite covering GET, POST, PUT, DELETE, error handling, timeouts, and JSON parsing.
37
+
package/README.md CHANGED
@@ -1,22 +1,22 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://raw.githubusercontent.com/dexix-dev/dexix-request/main/.github/logo-placeholder.svg" width="88" height="88" alt="DexiX Request logo" />
3
+ <img src="https://raw.githubusercontent.com/AkbariCodes/DexiX-Request/main/.github/logo-placeholder.svg" width="88" height="88" alt="DexiX Request logo" />
4
4
 
5
5
  # DexiX Request
6
6
 
7
7
  **If Axios were redesigned from scratch in 2026.**
8
8
 
9
- A tiny, modern, zero-dependency HTTP client for JavaScript and TypeScript — for the browser, Node.js, and React.
9
+ A tiny, zero-dependency HTTP client for JavaScript and TypeScript — with interceptors, built-in retries, and a CLI you'll actually enjoy typing into. For the browser, Node.js, and React.
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/dexix-request.svg?color=blue)](https://www.npmjs.com/package/dexix-request)
12
12
  [![npm downloads](https://img.shields.io/npm/dm/dexix-request.svg)](https://www.npmjs.com/package/dexix-request)
13
13
  [![bundle size](https://img.shields.io/bundlephobia/minzip/dexix-request)](https://bundlephobia.com/package/dexix-request)
14
- [![CI](https://github.com/dexix-dev/dexix-request/actions/workflows/ci.yml/badge.svg)](https://github.com/dexix-dev/dexix-request/actions/workflows/ci.yml)
14
+ [![CI](https://github.com/AkbariCodes/DexiX-Request/actions/workflows/ci.yml/badge.svg)](https://github.com/AkbariCodes/DexiX-Request/actions/workflows/ci.yml)
15
15
  [![types](https://img.shields.io/npm/types/dexix-request.svg)](https://www.npmjs.com/package/dexix-request)
16
16
  [![license](https://img.shields.io/npm/l/dexix-request.svg)](./LICENSE)
17
17
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](./package.json)
18
18
 
19
- [Quick Start](#quick-start) · [Features](#features) · [API Reference](#api-reference) · [Why DexiX](#why-dexix-request) · [Comparison](#comparison-with-axios) · [FAQ](#faq)
19
+ [Quick Start](#quick-start) · [CLI](#cli) · [Interceptors](#interceptors) · [Retries](#retries) · [API Reference](#api-reference) · [Comparison](#comparison-with-axios) · [FAQ](#faq)
20
20
 
21
21
  </div>
22
22
 
@@ -36,24 +36,21 @@ pnpm add dexix-request
36
36
  yarn add dexix-request
37
37
  ```
38
38
 
39
- No peer dependencies. No polyfills required in any environment that has `fetch` (all evergreen browsers, and Node.js 20+).
39
+ No peer dependencies. No polyfills required in any environment that has `fetch` (all evergreen browsers, and Node.js 20+). Installing it also gives you a `dx` command in your terminal — see [CLI](#cli).
40
40
 
41
41
  ## Quick Start
42
42
 
43
43
  ```ts
44
44
  import dx from "dexix-request";
45
45
 
46
- // Calling the instance directly performs a GET request.
47
46
  const users = await dx("/users");
48
47
 
49
- // Method helpers for everything else.
50
48
  const user = await dx.get<User>("/users/1");
51
49
 
52
50
  await dx.post("/posts", {
53
51
  title: "Hello DexiX",
54
52
  });
55
53
 
56
- // Scoped instances for talking to a specific API.
57
54
  const api = dx.create({
58
55
  baseURL: "https://api.example.com",
59
56
  headers: { Authorization: "Bearer token" },
@@ -67,15 +64,56 @@ That's it. No `response.json()`. No `axios.create()` boilerplate. No config obje
67
64
  ## Features
68
65
 
69
66
  - ✅ **Extremely simple API** — call the instance directly, or use `.get` / `.post` / `.put` / `.patch` / `.delete` / `.head` / `.options`
67
+ - ✅ **Interceptor chains** — `dx.interceptors.request.use()` and `dx.interceptors.response.use()`, with recovery on rejection
68
+ - ✅ **Built-in retries** — exponential backoff on `5xx`/`429`/network failures, fully configurable, no extra package
69
+ - ✅ **A real CLI** — `dx get https://api.example.com` from your terminal, colorized, zero setup
70
70
  - ✅ **Automatic JSON** — request bodies are serialized and response bodies are parsed for you
71
71
  - ✅ **Zero dependencies** — nothing to audit, nothing to update, nothing to break
72
- - ✅ **Tiny** — ~2KB gzipped, tree-shakeable, side-effect free
72
+ - ✅ **Tiny** — ~2.6KB gzipped, tree-shakeable, side-effect free
73
73
  - ✅ **Modern TypeScript** — full generics, strict types, no `any` in the public API
74
74
  - ✅ **Built on the platform** — `fetch`, `AbortController`, `URLSearchParams`, `ReadableStream`, `FormData` — nothing reinvented
75
75
  - ✅ **Composable timeouts** — `timeout` and a caller-supplied `AbortSignal` work together, not against each other
76
76
  - ✅ **Rich errors** — `NetworkError`, `TimeoutError`, `HTTPError`, and `ParseError`, each carrying the request and response
77
77
  - ✅ **Works everywhere** — browser, Node.js 20+, React, Next.js, Vite, ESM, and CommonJS
78
78
 
79
+ ## CLI
80
+
81
+ Every install of `dexix-request` ships a command-line client — no separate install, no `curl` flag-soup to remember.
82
+
83
+ ```bash
84
+ dx get https://api.example.com/users
85
+ dx post https://api.example.com/users -d '{"name":"Ada"}'
86
+ dx get https://api.example.com/users -H "authorization: Bearer xyz" -i
87
+ npx dx get https://api.example.com/users # without installing globally
88
+ ```
89
+
90
+ ```
91
+ $ dx get api.example.com/users -q limit=2 -i
92
+ 200 OK · 84ms
93
+ content-type: application/json
94
+ ...
95
+
96
+ {
97
+ "id": 1,
98
+ "name": "Ada Lovelace"
99
+ }
100
+ ```
101
+
102
+ | Flag | Description |
103
+ | --- | --- |
104
+ | `-H, --header <k:v>` | Add a request header (repeatable) |
105
+ | `-d, --data <json>` | Request body; use `@file.json` to read from a file |
106
+ | `-q, --query <k=v>` | Add a query parameter (repeatable) |
107
+ | `-t, --timeout <ms>` | Abort the request after N milliseconds |
108
+ | `-i, --include` | Print response status and headers |
109
+ | `-o, --output <file>` | Write the response body to a file |
110
+ | `-s, --silent` | Print only the response body |
111
+ | `--retries <n>` | Retry failed requests up to `n` times |
112
+ | `--no-color` | Disable colored output |
113
+ | `-v, --version` / `-h, --help` | Print version / usage |
114
+
115
+ The CLI is a thin, dependency-free script (`bin/dexix.mjs`) — it doesn't pull in `chalk`, `commander`, or anything else. Same philosophy as the library: built on what the platform already gives you. It's also why installing the package with `npm install` greets you with a small welcome banner instead of silence.
116
+
79
117
  ## Examples
80
118
 
81
119
  ### JavaScript
@@ -141,6 +179,49 @@ function useUser(id: number) {
141
179
 
142
180
  More complete examples live in [`examples/`](./examples): [Node.js](./examples/node.mjs), [browser](./examples/browser.js), [React](./examples/react-example.tsx), and [TypeScript](./examples/typescript-example.ts).
143
181
 
182
+ ## Interceptors
183
+
184
+ Attach behavior to every request or response an instance makes — auth injection, logging, response reshaping, or error recovery.
185
+
186
+ ```ts
187
+ import dx, { HTTPError } from "dexix-request";
188
+
189
+ dx.interceptors.request.use((config) => ({
190
+ ...config,
191
+ headers: { ...config.headers, Authorization: `Bearer ${getToken()}` },
192
+ }));
193
+
194
+ dx.interceptors.response.use(
195
+ (data) => data,
196
+ async (error) => {
197
+ if (error instanceof HTTPError && error.status === 401) {
198
+ await refreshToken();
199
+ return dx(error.request.url);
200
+ }
201
+ throw error;
202
+ },
203
+ );
204
+
205
+ const id = dx.interceptors.request.use((config) => config);
206
+ dx.interceptors.request.eject(id);
207
+ ```
208
+
209
+ Request interceptors receive and return a `ResolvedRequestConfig`. Response interceptors receive and return the resolved value (or, in the rejection handler, the thrown error — return a value to recover, or re-throw to propagate). Interceptors are per-instance, so `dx.create()` gives you an isolated chain.
210
+
211
+ ## Retries
212
+
213
+ Flaky networks and cold-starting APIs are normal. DexiX Request retries them for you, with sane defaults and full control when you need it.
214
+
215
+ ```ts
216
+ await dx.get("/orders", {
217
+ retries: 3,
218
+ retryDelay: (attempt) => attempt * 500,
219
+ retryOn: (error) => error instanceof HTTPError && error.status >= 500,
220
+ });
221
+ ```
222
+
223
+ By default, `retries` is `0` (no surprises), and when you do set it, failures are retried with exponential backoff (`250ms, 500ms, 1000ms, ...` capped at 8s) on `5xx` responses, `429`, `NetworkError`, and `TimeoutError`. Set `retryOn` to fully replace that logic.
224
+
144
225
  ## API Reference
145
226
 
146
227
  ### `dx(url, config?)`
@@ -165,30 +246,38 @@ Every method returns a `Promise<T>` that resolves with the already-parsed respon
165
246
 
166
247
  ### `dx.create(config?)`
167
248
 
168
- Creates a new, independent instance with its own defaults. Config passed to individual requests is merged on top of (and overrides) the instance's defaults.
249
+ Creates a new, independent instance with its own defaults and its own interceptor chains. Config passed to individual requests is merged on top of (and overrides) the instance's defaults.
169
250
 
170
251
  ```ts
171
252
  const api = dx.create({
172
253
  baseURL: "https://api.example.com",
173
254
  headers: { "X-App": "my-app" },
174
255
  timeout: 10_000,
256
+ retries: 2,
175
257
  });
176
258
  ```
177
259
 
260
+ ### `dx.interceptors.request` / `dx.interceptors.response`
261
+
262
+ `DexiXInterceptorManager` with `.use(onFulfilled, onRejected?)`, `.eject(id)`, and `.clear()`. See [Interceptors](#interceptors).
263
+
178
264
  ### `RequestConfig`
179
265
 
180
- | Option | Type | Description |
181
- | --------------- | ---------------------------------------- | ----------------------------------------------------------------|
182
- | `baseURL` | `string` | Prepended to relative URLs. |
183
- | `headers` | `HeadersInit \| Record<string, string>` | Merged with instance defaults; per-request wins. |
184
- | `query` | `Record<string, ...>` | Serialized into the URL's query string. Arrays become repeated keys. |
185
- | `body` | `BodyInit \| object \| unknown[]` | Plain objects/arrays are JSON-serialized automatically. |
186
- | `timeout` | `number` | Milliseconds before the request is aborted with `TimeoutError`. |
187
- | `signal` | `AbortSignal` | Composed with the internal timeout signal. |
188
- | `responseType` | `"json" \| "text" \| "blob" \| "arrayBuffer" \| "stream"` | Overrides automatic response parsing. |
189
- | `credentials`, `cache`, `redirect`, `referrerPolicy`, `mode` | | Forwarded straight to `fetch`. |
190
- | `onRequest` | `(request: Request) => void` | Called with the final `Request` before it's sent. |
191
- | `onResponse` | `(response: Response) => void` | Called with the raw `Response` before the body is parsed. |
266
+ | Option | Type | Description |
267
+ | --- | --- | --- |
268
+ | `baseURL` | `string` | Prepended to relative URLs. |
269
+ | `headers` | `HeadersInit \| Record<string, string>` | Merged with instance defaults; per-request wins. |
270
+ | `query` | `Record<string, ...>` | Serialized into the URL's query string. Arrays become repeated keys. |
271
+ | `body` | `BodyInit \| object \| unknown[]` | Plain objects/arrays are JSON-serialized automatically. |
272
+ | `timeout` | `number` | Milliseconds before the request is aborted with `TimeoutError`. |
273
+ | `signal` | `AbortSignal` | Composed with the internal timeout signal. |
274
+ | `retries` | `number` | Number of retry attempts after the first failure. Default `0`. |
275
+ | `retryDelay` | `number \| (attempt, error) => number` | Delay before each retry. Default exponential backoff. |
276
+ | `retryOn` | `(error, attempt) => boolean` | Overrides which errors are retried. |
277
+ | `responseType` | `"json" \| "text" \| "blob" \| "arrayBuffer" \| "stream"` | Overrides automatic response parsing. |
278
+ | `credentials`, `cache`, `redirect`, `referrerPolicy`, `mode` | — | Forwarded straight to `fetch`. |
279
+ | `onRequest` | `(request: Request) => void` | Called with the final `Request` before it's sent. |
280
+ | `onResponse` | `(response: Response) => void` | Called with the raw `Response` before the body is parsed. |
192
281
 
193
282
  ## Error Handling
194
283
 
@@ -201,16 +290,12 @@ try {
201
290
  await dx.get("/users/1");
202
291
  } catch (error) {
203
292
  if (error instanceof HTTPError) {
204
- // Server responded with a non-2xx status.
205
293
  console.error(error.status, error.response?.data);
206
294
  } else if (error instanceof TimeoutError) {
207
- // Request exceeded `timeout`.
208
295
  console.error(`Timed out after ${error.timeout}ms`);
209
296
  } else if (error instanceof NetworkError) {
210
- // The request never reached a server (offline, DNS, CORS, etc).
211
297
  console.error(error.message);
212
298
  } else if (error instanceof ParseError) {
213
- // A response body couldn't be parsed as JSON/text/etc.
214
299
  console.error(error.message);
215
300
  }
216
301
  }
@@ -229,7 +314,7 @@ Every DexiX error carries:
229
314
 
230
315
  Most HTTP clients fall into one of two camps: a thin `fetch` wrapper that still makes you write `response.json()` everywhere, or a heavyweight library carrying years of legacy API surface and a dependency tree of its own.
231
316
 
232
- DexiX Request is neither. It's built directly on the platform primitives that already ship in every modern runtime — `fetch`, `AbortController`, `Headers`, `URLSearchParams`, `FormData`, `ReadableStream` — and adds exactly one thing on top: an API that gets out of your way.
317
+ DexiX Request is neither. It's built directly on the platform primitives that already ship in every modern runtime — `fetch`, `AbortController`, `Headers`, `URLSearchParams`, `FormData`, `ReadableStream` — and adds exactly what a modern client needs on top: interceptors, retries, and a CLI, without a single dependency.
233
318
 
234
319
  - No dependencies to audit or update.
235
320
  - No custom body-parsing logic to trust — you get real `Headers`, real `Request`, real `Response` under the hood.
@@ -240,10 +325,12 @@ DexiX Request is neither. It's built directly on the platform primitives that al
240
325
  | | DexiX Request | Axios |
241
326
  | --- | --- | --- |
242
327
  | Dependencies | 0 | 0 (as of v1, but larger internal surface) |
243
- | Bundle size (gzip) | ~2KB | ~13KB |
328
+ | Bundle size (gzip) | ~2.6KB | ~13KB |
244
329
  | Transport | `fetch` | `XMLHttpRequest` (browser) / `http` (Node) |
245
330
  | TypeScript | Written in TS, strict, no `any` | Typed via bundled `.d.ts`, some `any` |
246
- | Interceptors | `onRequest` / `onResponse` hooks | Interceptor chain |
331
+ | Interceptors | Request/response chains with rejection recovery | Interceptor chain |
332
+ | Built-in retries | Yes, configurable backoff | No (requires `axios-retry`) |
333
+ | CLI included | Yes (`dx`) | No |
247
334
  | Cancellation | Native `AbortSignal`, composable with `timeout` | Native `AbortSignal` (v1+) or legacy `CancelToken` |
248
335
  | Default body parsing | Automatic JSON | Automatic JSON |
249
336
  | Node.js support | 20+ (uses global `fetch`) | Broad, including legacy Node |
@@ -253,13 +340,12 @@ DexiX Request intentionally supports fewer legacy environments in exchange for a
253
340
 
254
341
  ## Roadmap
255
342
 
256
- - [ ] Request/response interceptor chains (beyond `onRequest`/`onResponse`)
257
- - [ ] Built-in retry with configurable backoff
258
343
  - [ ] Upload/download progress events
259
344
  - [ ] Automatic request deduplication
260
345
  - [ ] Optional plugin for schema validation (Zod/Valibot) on responses
346
+ - [ ] CLI: `--jq`-style response filtering
261
347
 
262
- Have an idea? [Open an issue](https://github.com/dexix-dev/dexix-request/issues).
348
+ Have an idea? [Open an issue](https://github.com/AkbariCodes/DexiX-Request/issues).
263
349
 
264
350
  ## Browser Support
265
351
 
@@ -267,24 +353,25 @@ All browsers with `fetch`, `AbortController`, and `URLSearchParams` support —
267
353
 
268
354
  ## Performance
269
355
 
270
- - **Bundle size:** ~2KB gzipped, well under the 5KB target
356
+ - **Bundle size:** ~2.6KB gzipped
271
357
  - **Zero dependencies:** nothing extra ships in your `node_modules` or your bundle
272
358
  - **Tree-shakeable:** unused exports (individual error classes, `createInstance`) are dropped by any modern bundler
273
- - **Minimal allocations:** a single `Headers` merge and a single `Request` construction per call — no intermediate wrapper objects
359
+ - **Minimal allocations:** a single `Headers` merge and a single `Request` construction per attempt — no intermediate wrapper objects
274
360
 
275
361
  ## Developer Experience
276
362
 
277
- - Every public function is documented with JSDoc, so your editor shows real descriptions, not just type signatures.
363
+ - Fully typed public API every method, config option, and error class is a real TypeScript type your editor can autocomplete against.
278
364
  - Errors are specific and typed, so `catch` blocks can branch with `instanceof` instead of parsing error strings.
279
- - `dx.create()` returns a fully independent instance — no shared mutable state, no surprises across modules.
365
+ - `dx.create()` returns a fully independent instance — its own defaults, its own interceptor chains, no shared mutable state.
366
+ - The source itself is intentionally comment-free: behavior lives in small, clearly named functions rather than prose next to the code, and this README is the source of truth for *why*.
280
367
 
281
368
  ## Contributing
282
369
 
283
370
  Contributions are welcome. To get started:
284
371
 
285
372
  ```bash
286
- git clone https://github.com/dexix-dev/dexix-request.git
287
- cd dexix-request/packages/request
373
+ git clone https://github.com/AkbariCodes/DexiX-Request.git
374
+ cd DexiX-Request
288
375
  npm install
289
376
  npm run test
290
377
  ```
@@ -321,7 +408,10 @@ controller.abort();
321
408
  ```
322
409
 
323
410
  **Does DexiX Request retry failed requests automatically?**
324
- Not yet automatic retry with backoff is on the [roadmap](#roadmap). Today, wrap a call in your own retry logic if needed.
411
+ Only if you ask it to set `retries` on a request or on `dx.create()` defaults. See [Retries](#retries).
412
+
413
+ **Does installing this package run arbitrary code on my machine?**
414
+ The `postinstall` script only prints a colored welcome message to your terminal — no network calls, no file writes outside your terminal's stdout. It skips itself entirely in CI or any non-interactive shell.
325
415
 
326
416
  ## License
327
417
 
package/bin/dexix.mjs ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
4
+
5
+ const c = {
6
+ reset: (s) => (COLOR ? `\x1b[0m${s}\x1b[0m` : s),
7
+ bold: (s) => (COLOR ? `\x1b[1m${s}\x1b[0m` : s),
8
+ dim: (s) => (COLOR ? `\x1b[2m${s}\x1b[0m` : s),
9
+ red: (s) => (COLOR ? `\x1b[31m${s}\x1b[0m` : s),
10
+ green: (s) => (COLOR ? `\x1b[32m${s}\x1b[0m` : s),
11
+ yellow: (s) => (COLOR ? `\x1b[33m${s}\x1b[0m` : s),
12
+ blue: (s) => (COLOR ? `\x1b[34m${s}\x1b[0m` : s),
13
+ magenta: (s) => (COLOR ? `\x1b[35m${s}\x1b[0m` : s),
14
+ cyan: (s) => (COLOR ? `\x1b[36m${s}\x1b[0m` : s),
15
+ gray: (s) => (COLOR ? `\x1b[90m${s}\x1b[0m` : s),
16
+ };
17
+
18
+ const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
19
+ const VERSION = "1.1.0";
20
+
21
+ function printHelp() {
22
+ console.log(`
23
+ ${c.bold(c.cyan("DexiX"))} ${c.dim(`v${VERSION}`)} — a tiny, fast HTTP client for your terminal
24
+
25
+ ${c.bold("Usage")}
26
+ ${c.green("dx")} ${c.yellow("<method>")} ${c.magenta("<url>")} [options]
27
+ ${c.green("dx")} ${c.magenta("<url>")} ${c.dim("(defaults to GET)")}
28
+
29
+ ${c.bold("Methods")}
30
+ get, post, put, patch, delete, head, options
31
+
32
+ ${c.bold("Options")}
33
+ -H, --header <k:v> Add a request header ${c.dim("(repeatable)")}
34
+ -d, --data <json> Request body. Use ${c.cyan("@file.json")} to read from a file
35
+ -q, --query <k=v> Add a query parameter ${c.dim("(repeatable)")}
36
+ -t, --timeout <ms> Abort the request after N milliseconds
37
+ -i, --include Print response status and headers
38
+ -o, --output <file> Write the response body to a file
39
+ -s, --silent Print only the response body
40
+ --retries <n> Retry failed requests up to n times
41
+ --no-color Disable colored output
42
+ -v, --version Print the CLI version
43
+ -h, --help Show this help message
44
+
45
+ ${c.bold("Examples")}
46
+ ${c.gray("$")} dx get https://api.example.com/users
47
+ ${c.gray("$")} dx post https://api.example.com/users -d '{"name":"Ada"}'
48
+ ${c.gray("$")} dx https://api.example.com/users -H "authorization: Bearer xyz" -i
49
+ `);
50
+ }
51
+
52
+ function parseArgs(argv) {
53
+ const args = [...argv];
54
+ let method = "GET";
55
+ let url;
56
+
57
+ if (args.length && METHODS.has(String(args[0]).toUpperCase())) {
58
+ method = args.shift().toUpperCase();
59
+ }
60
+
61
+ const options = {
62
+ headers: {},
63
+ query: {},
64
+ data: undefined,
65
+ timeout: undefined,
66
+ include: false,
67
+ silent: false,
68
+ output: undefined,
69
+ retries: 0,
70
+ };
71
+
72
+ while (args.length) {
73
+ const token = args.shift();
74
+
75
+ if (!url && !token.startsWith("-")) {
76
+ url = token;
77
+ continue;
78
+ }
79
+
80
+ switch (token) {
81
+ case "-H":
82
+ case "--header": {
83
+ const value = args.shift() ?? "";
84
+ const index = value.indexOf(":");
85
+ if (index !== -1) {
86
+ options.headers[value.slice(0, index).trim()] = value.slice(index + 1).trim();
87
+ }
88
+ break;
89
+ }
90
+ case "-d":
91
+ case "--data":
92
+ options.data = args.shift();
93
+ break;
94
+ case "-q":
95
+ case "--query": {
96
+ const value = args.shift() ?? "";
97
+ const index = value.indexOf("=");
98
+ if (index !== -1) {
99
+ options.query[value.slice(0, index)] = value.slice(index + 1);
100
+ }
101
+ break;
102
+ }
103
+ case "-t":
104
+ case "--timeout":
105
+ options.timeout = Number(args.shift());
106
+ break;
107
+ case "-i":
108
+ case "--include":
109
+ options.include = true;
110
+ break;
111
+ case "-s":
112
+ case "--silent":
113
+ options.silent = true;
114
+ break;
115
+ case "-o":
116
+ case "--output":
117
+ options.output = args.shift();
118
+ break;
119
+ case "--retries":
120
+ options.retries = Number(args.shift());
121
+ break;
122
+ case "--no-color":
123
+ break;
124
+ case "-h":
125
+ case "--help":
126
+ printHelp();
127
+ process.exit(0);
128
+ break;
129
+ case "-v":
130
+ case "--version":
131
+ console.log(VERSION);
132
+ process.exit(0);
133
+ break;
134
+ default:
135
+ break;
136
+ }
137
+ }
138
+
139
+ return { method, url, options };
140
+ }
141
+
142
+ async function readBody(data) {
143
+ if (!data) return undefined;
144
+
145
+ if (data.startsWith("@")) {
146
+ const { readFile } = await import("node:fs/promises");
147
+ const raw = await readFile(data.slice(1), "utf8");
148
+ try {
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ return raw;
152
+ }
153
+ }
154
+
155
+ try {
156
+ return JSON.parse(data);
157
+ } catch {
158
+ return data;
159
+ }
160
+ }
161
+
162
+ function buildURL(rawURL, query) {
163
+ const url = new URL(rawURL);
164
+ for (const [key, value] of Object.entries(query)) {
165
+ url.searchParams.append(key, value);
166
+ }
167
+ return url.toString();
168
+ }
169
+
170
+ function statusColor(status) {
171
+ if (status >= 500) return c.red;
172
+ if (status >= 400) return c.yellow;
173
+ if (status >= 300) return c.cyan;
174
+ if (status >= 200) return c.green;
175
+ return c.gray;
176
+ }
177
+
178
+ function colorizeJSON(json) {
179
+ if (!COLOR) return json;
180
+ return json
181
+ .replace(/"([^"]+)":/g, (_, key) => `${c.cyan(`"${key}"`)}:`)
182
+ .replace(/: "([^"]*)"/g, (_, value) => `: ${c.green(`"${value}"`)}`)
183
+ .replace(/: (-?\d+\.?\d*)/g, (_, value) => `: ${c.yellow(value)}`)
184
+ .replace(/: (true|false)/g, (_, value) => `: ${c.magenta(value)}`)
185
+ .replace(/: (null)/g, (_, value) => `: ${c.gray(value)}`);
186
+ }
187
+
188
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
189
+
190
+ function startSpinner(label) {
191
+ if (!COLOR) return () => {};
192
+ let frame = 0;
193
+ const timer = setInterval(() => {
194
+ process.stderr.write(`\r${c.cyan(spinnerFrames[frame % spinnerFrames.length])} ${label}`);
195
+ frame += 1;
196
+ }, 80);
197
+ return () => {
198
+ clearInterval(timer);
199
+ process.stderr.write(`\r${" ".repeat(label.length + 4)}\r`);
200
+ };
201
+ }
202
+
203
+ async function main() {
204
+ const { method, url, options } = parseArgs(process.argv.slice(2));
205
+
206
+ if (!url) {
207
+ printHelp();
208
+ process.exit(1);
209
+ }
210
+
211
+ const requestURL = buildURL(url, options.query);
212
+ const body = await readBody(options.data);
213
+ const stopSpinner = options.silent ? () => {} : startSpinner(`${method} ${requestURL}`);
214
+ const startedAt = Date.now();
215
+
216
+ const init = {
217
+ method,
218
+ headers: { "content-type": "application/json", ...options.headers },
219
+ signal: options.timeout ? AbortSignal.timeout(options.timeout) : undefined,
220
+ };
221
+
222
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
223
+ init.body = typeof body === "string" ? body : JSON.stringify(body);
224
+ }
225
+
226
+ let response;
227
+ let attempts = 0;
228
+
229
+ while (true) {
230
+ attempts += 1;
231
+ try {
232
+ response = await fetch(requestURL, init);
233
+ if (!response.ok && attempts <= options.retries && response.status >= 500) {
234
+ continue;
235
+ }
236
+ break;
237
+ } catch (error) {
238
+ if (attempts <= options.retries) continue;
239
+ stopSpinner();
240
+ console.error(c.red(`✖ ${error instanceof Error ? error.message : String(error)}`));
241
+ process.exit(1);
242
+ }
243
+ }
244
+
245
+ stopSpinner();
246
+
247
+ const elapsed = Date.now() - startedAt;
248
+ const contentType = response.headers.get("content-type") ?? "";
249
+ const isJSON = contentType.includes("json");
250
+ const text = await response.text();
251
+
252
+ if (!options.silent) {
253
+ const color = statusColor(response.status);
254
+ console.error(
255
+ `${color(c.bold(String(response.status)))} ${c.dim(response.statusText)} ${c.gray(`· ${elapsed}ms`)}`,
256
+ );
257
+ }
258
+
259
+ if (options.include && !options.silent) {
260
+ response.headers.forEach((value, key) => {
261
+ console.error(`${c.gray(key)}: ${value}`);
262
+ });
263
+ console.error("");
264
+ }
265
+
266
+ const output = isJSON && text ? JSON.stringify(JSON.parse(text), null, 2) : text;
267
+
268
+ if (options.output) {
269
+ const { writeFile } = await import("node:fs/promises");
270
+ await writeFile(options.output, output);
271
+ if (!options.silent) console.error(c.dim(`saved to ${options.output}`));
272
+ } else {
273
+ console.log(isJSON ? colorizeJSON(output) : output);
274
+ }
275
+
276
+ process.exit(response.ok ? 0 : 1);
277
+ }
278
+
279
+ main();
package/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var i=class extends Error{request;response;status;headers;url;constructor(t,r,n){super(t),this.name="DexiXError",this.request=r,this.response=n,this.status=n?.status,this.headers=n?.headers,this.url=r.url,Object.setPrototypeOf(this,new.target.prototype);}};var c=class extends i{constructor(t,r){super(`Request failed with status ${r.status} (${r.statusText}) for ${t.url}`,t,r),this.name="HTTPError";}};var d=class extends i{constructor(t,r,n){super(t||"Network request failed",r),this.name="NetworkError",n!==void 0&&(this.cause=n);}};var m=class extends i{timeout;constructor(t,r){super(`Request to ${t.url} timed out after ${r}ms`,t),this.name="TimeoutError",this.timeout=r;}};function C(e){return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function k(e,t){if(!e||C(t))return t;let r=e.endsWith("/")?e.slice(0,-1):e,n=t.startsWith("/")?t:`/${t}`;return `${r}${n}`}function H(e,t){if(!t||Object.keys(t).length===0)return e;let[r,n=""]=e.split("?"),o=new URLSearchParams(n);for(let[f,u]of Object.entries(t))if(u!=null)if(Array.isArray(u))for(let b of u)o.append(f,String(b));else o.append(f,String(u));let s=o.toString();return s?`${r}?${s}`:r}function q(e,t,r){let n=k(e??"",t);return H(n,r)}function l(...e){let t=new Headers;for(let r of e)if(r){if(r instanceof Headers){r.forEach((n,o)=>{t.set(o,n);});continue}if(Array.isArray(r)){for(let[n,o]of r)t.set(n,o);continue}for(let[n,o]of Object.entries(r))o===void 0?t.delete(n):t.set(n,o);}return t}function R(e,t){return {...e,...t,headers:l(e.headers,t.headers),query:{...e.query,...t.query}}}function E(e){if(typeof e!="object"||e===null)return false;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function S(e){return typeof e=="string"||e instanceof FormData||e instanceof Blob||e instanceof URLSearchParams||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof ReadableStream}function w(e){return e==null?{body:void 0}:S(e)?e instanceof FormData?{body:e}:e instanceof URLSearchParams?{body:e,contentType:"application/x-www-form-urlencoded;charset=UTF-8"}:typeof e=="string"?{body:e,contentType:"text/plain;charset=UTF-8"}:{body:e}:E(e)||Array.isArray(e)?{body:JSON.stringify(e),contentType:"application/json;charset=UTF-8"}:{body:JSON.stringify(e),contentType:"application/json;charset=UTF-8"}}var x=class extends i{constructor(t,r,n){super(`Failed to parse response body from ${t.url}`,t,r),this.name="ParseError",n!==void 0&&(this.cause=n);}};var O=new Set([204,205,304]);function A(e){let t=e.headers.get("content-type")??"";return t.includes("application/json")||t.includes("+json")?"json":t.startsWith("text/")?"text":"json"}async function h(e,t,r){if(t.method==="HEAD"||O.has(e.status))return;let n=r??A(e);try{switch(n){case "json":{let o=await e.text();return o.length===0?void 0:JSON.parse(o)}case "text":return await e.text();case "blob":return await e.blob();case "arrayBuffer":return await e.arrayBuffer();case "stream":return e.body;default:return await e.text()}}catch(o){let s={data:void 0,status:e.status,statusText:e.statusText,headers:e.headers,url:e.url||t.url};throw new x(t,s,o)}}function D(e,t){if(!t||t<=0)return {signal:e,cancel:()=>{},didTimeout:()=>false};let r=new AbortController,n=false,o=setTimeout(()=>{n=true,r.abort();},t),s=()=>r.abort();e?.addEventListener("abort",s,{once:true});let f=()=>{clearTimeout(o),e?.removeEventListener("abort",s);};return {signal:r.signal,cancel:f,didTimeout:()=>n}}async function j(e){let{body:t,contentType:r}=w(e.body),n=l(r?{"content-type":r}:void 0,e.headers),o=q(e.baseURL,e.url,e.query),s={url:o,method:e.method,headers:n,body:e.body},{signal:f,cancel:u,didTimeout:b}=D(e.signal,e.timeout),I={method:e.method,headers:n,body:e.method==="GET"||e.method==="HEAD"?void 0:t,signal:f,credentials:e.credentials,cache:e.cache,redirect:e.redirect,referrerPolicy:e.referrerPolicy,mode:e.mode},X=new Request(o,I);e.onRequest&&await e.onRequest(X);let a;try{a=await fetch(X);}catch(p){throw u(),p instanceof DOMException&&p.name==="AbortError"?b()?new m(s,e.timeout??0):p:new d(p instanceof Error?p.message:"Network request failed",s,p)}if(u(),e.onResponse&&await e.onResponse(a),!a.ok){let P={data:await h(a,s,e.responseType).catch(()=>{}),status:a.status,statusText:a.statusText,headers:a.headers,url:a.url||o};throw new c(s,P)}return h(a,s,e.responseType)}function y(e,t,r,n){let o={...R(e,n??{}),method:t,url:r};return j(o)}function g(e,t,r,n,o){return y(e,t,r,{...o,body:n??o?.body})}function T(e={}){let t=((r,n)=>y(e,"GET",r,n));return t.get=(r,n)=>y(e,"GET",r,n),t.post=(r,n,o)=>g(e,"POST",r,n,o),t.put=(r,n,o)=>g(e,"PUT",r,n,o),t.patch=(r,n,o)=>g(e,"PATCH",r,n,o),t.delete=(r,n)=>y(e,"DELETE",r,n),t.head=(r,n)=>y(e,"HEAD",r,n),t.options=(r,n)=>y(e,"OPTIONS",r,n),Object.defineProperty(t,"defaults",{value:e,writable:false,enumerable:true}),t.create=r=>T(R(e,r??{})),t}var B=T();var Ee=B;
2
- exports.DexiXError=i;exports.HTTPError=c;exports.NetworkError=d;exports.ParseError=x;exports.TimeoutError=m;exports.createInstance=T;exports.default=Ee;//# sourceMappingURL=index.cjs.map
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var u=class extends Error{request;response;status;headers;url;constructor(t,r,n){super(t),this.name="DexiXError",this.request=r,this.response=n,this.status=n?.status,this.headers=n?.headers,this.url=r.url,Object.setPrototypeOf(this,new.target.prototype);}};var d=class extends u{constructor(t,r){super(`Request failed with status ${r.status} (${r.statusText}) for ${t.url}`,t,r),this.name="HTTPError";}};var l=class extends u{constructor(t,r,n){super(t||"Network request failed",r),this.name="NetworkError",n!==void 0&&(this.cause=n);}};var m=class extends u{timeout;constructor(t,r){super(`Request to ${t.url} timed out after ${r}ms`,t),this.name="TimeoutError",this.timeout=r;}};function k(e){return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function H(e,t){if(!e||k(t))return t;let r=e.endsWith("/")?e.slice(0,-1):e,n=t.startsWith("/")?t:`/${t}`;return `${r}${n}`}function O(e,t){if(!t||Object.keys(t).length===0)return e;let[r,n=""]=e.split("?"),s=new URLSearchParams(n);for(let[o,i]of Object.entries(t))if(i!=null)if(Array.isArray(i))for(let c of i)s.append(o,String(c));else s.append(o,String(i));let a=s.toString();return a?`${r}?${a}`:r}function q(e,t,r){let n=H(e??"",t);return O(n,r)}function R(...e){let t=new Headers;for(let r of e)if(r){if(r instanceof Headers){r.forEach((n,s)=>{t.set(s,n);});continue}if(Array.isArray(r)){for(let[n,s]of r)t.set(n,s);continue}for(let[n,s]of Object.entries(r))s===void 0?t.delete(n):t.set(n,s);}return t}function T(e,t){return {...e,...t,headers:R(e.headers,t.headers),query:{...e.query,...t.query}}}function X(e){if(typeof e!="object"||e===null)return false;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function A(e){return typeof e=="string"||e instanceof FormData||e instanceof Blob||e instanceof URLSearchParams||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof ReadableStream}function I(e){return e==null?{body:void 0}:A(e)?e instanceof FormData?{body:e}:e instanceof URLSearchParams?{body:e,contentType:"application/x-www-form-urlencoded;charset=UTF-8"}:typeof e=="string"?{body:e,contentType:"text/plain;charset=UTF-8"}:{body:e}:X(e)||Array.isArray(e)?{body:JSON.stringify(e),contentType:"application/json;charset=UTF-8"}:{body:JSON.stringify(e),contentType:"application/json;charset=UTF-8"}}var y=class extends u{constructor(t,r,n){super(`Failed to parse response body from ${t.url}`,t,r),this.name="ParseError",n!==void 0&&(this.cause=n);}};var S=new Set([204,205,304]);function v(e){let t=e.headers.get("content-type")??"";return t.includes("application/json")||t.includes("+json")?"json":t.startsWith("text/")?"text":"json"}async function w(e,t,r){if(t.method==="HEAD"||S.has(e.status))return;let n=r??v(e);try{switch(n){case "json":{let s=await e.text();return s.length===0?void 0:JSON.parse(s)}case "text":return await e.text();case "blob":return await e.blob();case "arrayBuffer":return await e.arrayBuffer();case "stream":return e.body;default:return await e.text()}}catch(s){let a={data:void 0,status:e.status,statusText:e.statusText,headers:e.headers,url:e.url||t.url};throw new y(t,a,s)}}function g(e,t){if(!t||t<=0)return {signal:e,cancel:()=>{},didTimeout:()=>false};let r=new AbortController,n=false,s=setTimeout(()=>{n=true,r.abort();},t),a=()=>r.abort();e?.addEventListener("abort",a,{once:true});let o=()=>{clearTimeout(s),e?.removeEventListener("abort",a);};return {signal:r.signal,cancel:o,didTimeout:()=>n}}async function B(e){let{body:t,contentType:r}=I(e.body),n=R(r?{"content-type":r}:void 0,e.headers),s=q(e.baseURL,e.url,e.query),a={url:s,method:e.method,headers:n,body:e.body},{signal:o,cancel:i,didTimeout:c}=g(e.signal,e.timeout),E={method:e.method,headers:n,body:e.method==="GET"||e.method==="HEAD"?void 0:t,signal:o,credentials:e.credentials,cache:e.cache,redirect:e.redirect,referrerPolicy:e.referrerPolicy,mode:e.mode},j=new Request(s,E);e.onRequest&&await e.onRequest(j);let p;try{p=await fetch(j);}catch(f){throw i(),f instanceof DOMException&&f.name==="AbortError"?c()?new m(a,e.timeout??0):f:new l(f instanceof Error?f.message:"Network request failed",a,f)}if(i(),e.onResponse&&await e.onResponse(p),!p.ok){let C={data:await w(p,a,e.responseType).catch(()=>{}),status:p.status,statusText:p.statusText,headers:p.headers,url:p.url||s};throw new d(a,C)}return w(p,a,e.responseType)}function U(e){return new Promise(t=>setTimeout(t,e))}function M(e){return Math.min(250*2**e,8e3)}function $(e){return e instanceof d?e.status!==void 0&&(e.status>=500||e.status===429):e instanceof l||e instanceof m}async function P(e){let t=e.retries??0,r=e.retryDelay??M,n=e.retryOn??$;for(let s=0;;s++)try{return await B(e)}catch(a){if(!(s<t&&n(a,s)))throw a;let i=typeof r=="function"?r(s,a):r;i>0&&await U(i);}}var h=class{handlers=[];use(t,r){return this.handlers.push({fulfilled:t,rejected:r}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null);}clear(){this.handlers=[];}list(){return this.handlers.filter(t=>t!==null)}};async function F(e,t){let r=t;for(let n of e.list())try{r=await n.fulfilled(r);}catch(s){if(!n.rejected)throw s;r=await n.rejected(s);}return r}async function N(e,t){let r,n,s=false;try{r=await t;}catch(a){s=true,n=a;}for(let a of e.list()){if(s){if(!a.rejected)continue;try{r=await a.rejected(n),s=!1;}catch(o){n=o;}continue}r=await a.fulfilled(r);}if(s)throw n;return r}function x(e,t,r,n,s){let a={...T(e,s??{}),method:r,url:n},o=(async()=>{let i=await F(t.request,a);return P(i)})();return N(t.response,o)}function D(e,t,r,n,s,a){return x(e,t,r,n,{...a,body:s??a?.body})}function b(e={}){let t=new h,r=new h,n={request:t,response:r},s=((o,i)=>x(e,n,"GET",o,i));return s.get=(o,i)=>x(e,n,"GET",o,i),s.post=(o,i,c)=>D(e,n,"POST",o,i,c),s.put=(o,i,c)=>D(e,n,"PUT",o,i,c),s.patch=(o,i,c)=>D(e,n,"PATCH",o,i,c),s.delete=(o,i)=>x(e,n,"DELETE",o,i),s.head=(o,i)=>x(e,n,"HEAD",o,i),s.options=(o,i)=>x(e,n,"OPTIONS",o,i),Object.defineProperty(s,"defaults",{value:e,writable:false,enumerable:true}),Object.defineProperty(s,"interceptors",{value:{request:{use:(o,i)=>t.use(o,i),eject:o=>t.eject(o),clear:()=>t.clear()},response:{use:(o,i)=>r.use(o,i),eject:o=>r.eject(o),clear:()=>r.clear()}},writable:false,enumerable:true}),s.create=o=>b(T(e,o??{})),s}var L=b();var Se=L;
2
+ exports.DexiXError=u;exports.HTTPError=d;exports.NetworkError=l;exports.ParseError=y;exports.TimeoutError=m;exports.createInstance=b;exports.default=Se;//# sourceMappingURL=index.cjs.map
3
3
  //# sourceMappingURL=index.cjs.map