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 +14 -0
- package/README.md +129 -39
- package/bin/dexix.mjs +279 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -107
- package/dist/index.d.ts +22 -107
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +17 -7
- package/scripts/postinstall.mjs +46 -0
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/
|
|
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,
|
|
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
|
[](https://www.npmjs.com/package/dexix-request)
|
|
12
12
|
[](https://www.npmjs.com/package/dexix-request)
|
|
13
13
|
[](https://bundlephobia.com/package/dexix-request)
|
|
14
|
-
[](https://github.com/AkbariCodes/DexiX-Request/actions/workflows/ci.yml)
|
|
15
15
|
[](https://www.npmjs.com/package/dexix-request)
|
|
16
16
|
[](./LICENSE)
|
|
17
17
|
[](./package.json)
|
|
18
18
|
|
|
19
|
-
[Quick Start](#quick-start) · [
|
|
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** — ~
|
|
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
|
|
181
|
-
|
|
|
182
|
-
| `baseURL`
|
|
183
|
-
| `headers`
|
|
184
|
-
| `query`
|
|
185
|
-
| `body`
|
|
186
|
-
| `timeout`
|
|
187
|
-
| `signal`
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
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
|
|
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) | ~
|
|
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 |
|
|
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/
|
|
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:** ~
|
|
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
|
|
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
|
-
-
|
|
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 —
|
|
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/
|
|
287
|
-
cd
|
|
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
|
-
|
|
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
|
|
2
|
-
exports.DexiXError=
|
|
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
|