ty-fetch 0.0.2-beta.7 → 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alister Norris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,37 +1,91 @@
1
- # ty-fetch
1
+ <p align="center">
2
+ <img src="logo.png" alt="ty-fetch" width="350" />
3
+ </p>
2
4
 
3
- TypeScript tooling that validates API calls against OpenAPI specs. Get autocomplete, diagnostics, and fully typed responses with zero manual types.
5
+ <h1 align="center">ty-fetch</h1>
4
6
 
5
- ```ts
6
- import tf from "ty-fetch";
7
+ [![npm version](https://img.shields.io/npm/v/ty-fetch.svg)](https://www.npmjs.com/package/ty-fetch)
8
+ [![license](https://img.shields.io/npm/l/ty-fetch.svg)](https://github.com/alnorris/ty-fetch/blob/main/LICENSE)
9
+ [![CI](https://github.com/alnorris/ty-fetch/actions/workflows/ci.yml/badge.svg)](https://github.com/alnorris/ty-fetch/actions/workflows/ci.yml)
7
10
 
8
- const customers = await tf.get("https://api.stripe.com/v1/customers").json();
9
- // customers is fully typed — data, has_more, object, url all autocomplete
11
+ **A tiny, zero-dependency HTTP client that auto-discovers your OpenAPI specs and types your API calls on-the-fly. No codegen, no build step — types generated by a TS plugin.**
10
12
 
11
- tf.get("https://api.stripe.com/v1/cutsomers");
12
- // ~~~~~~~~~~
13
- // Error: Path '/v1/cutsomers' does not exist in Stripe API.
14
- // Did you mean '/v1/customers'?
13
+ ```bash
14
+ npm install ty-fetch
15
15
  ```
16
16
 
17
- ## What it does
17
+ ```jsonc
18
+ // tsconfig.json
19
+ {
20
+ "compilerOptions": {
21
+ "plugins": [{
22
+ "name": "ty-fetch/plugin",
23
+ // Optional — point to specs manually. Without this, ty-fetch
24
+ // auto-discovers specs at /openapi.json, /.well-known/openapi.yaml, etc.
25
+ "specs": {
26
+ "api.mycompany.com": "https://api.mycompany.com/docs/openapi.json"
27
+ }
28
+ }]
29
+ }
30
+ }
31
+ ```
32
+
33
+ ```ts
34
+ import ty from "ty-fetch";
35
+
36
+ // Fully typed — response, body, path params, query params, headers
37
+ const { data, error } = await ty.post("https://api.mycompany.com/v1/users/{team}/invite", {
38
+ params: {
39
+ path: { team: "engineering" },
40
+ query: { notify: true },
41
+ },
42
+ body: { email: "jane@example.com", role: "admin" },
43
+ headers: { "x-api-key": process.env.API_KEY },
44
+ });
45
+
46
+ if (error) return console.error(error);
47
+ console.log(data.user.id); // fully typed, autocomplete works
48
+ ```
49
+
50
+ If your API serves an OpenAPI spec at `/openapi.json` (or any well-known path), ty-fetch finds it automatically. No config needed.
51
+
52
+ ---
53
+
54
+ ## 🤔 How does it work?
55
+
56
+ ty-fetch is a **TypeScript language service plugin**. When you write a `ty.get("https://...")` call:
57
+
58
+ 1. 🔍 It extracts the domain from the URL
59
+ 2. 📡 Auto-discovers the OpenAPI spec (checks `/openapi.json`, `/.well-known/openapi.yaml`, etc.)
60
+ 3. 🏗️ Generates typed overloads on-the-fly — response types, query params, headers, everything
61
+ 4. ✅ Validates your API paths and suggests corrections for typos
18
62
 
19
- - **Path validation** red squiggles for typos in API URLs, with "did you mean?" suggestions
20
- - **Typed responses** — response types generated from OpenAPI schemas, no manual `as` casts
21
- - **Typed request bodies** — body params validated against the spec
22
- - **Path & query params** — typed `params.path` and `params.query` based on the endpoint
23
- - **Autocomplete** — URL path completions inside string literals, filtered by HTTP method
24
- - **Hover info** — hover over a URL to see available methods and descriptions
63
+ Types appear in your editor instantly. When the spec changes, types update automatically. No build step ever.
25
64
 
26
- Works as both a **TS language service plugin** (editor DX) and a **CLI** (CI validation).
65
+ ### Compared to other tools
27
66
 
28
- ## Setup
67
+ | | ty-fetch | [openapi-typescript](https://github.com/openapi-ts/openapi-typescript) | [openapi-fetch](https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch) | [orval](https://github.com/orval-labs/orval) |
68
+ |---|---|---|---|---|
69
+ | **Codegen step** | None | `npx openapi-typescript ...` | Needs openapi-typescript first | `npx orval` |
70
+ | **Build step** | None | Required | Required | Required |
71
+ | **Generated files** | None | `.d.ts` files | `.d.ts` files | Full client |
72
+ | **Spec changes** | Auto-updates | Re-run codegen | Re-run codegen | Re-run codegen |
73
+ | **Auto-discovery** | Probes well-known paths | Manual | Manual | Manual |
74
+ | **Editor integration** | TS plugin (autocomplete, hover, diagnostics) | Types only | Types only | Types only |
75
+ | **Path validation** | Typo detection with suggestions | None | None | None |
76
+ | **Runtime** | Thin fetch wrapper (~100 LOC) | None (types only) | Fetch wrapper | Axios/fetch client |
77
+
78
+ ---
79
+
80
+ ## 📦 Quick start
81
+
82
+ ### 1. Install
29
83
 
30
84
  ```bash
31
- npm install github:alnorris/ty-fetch
85
+ npm install ty-fetch
32
86
  ```
33
87
 
34
- Add the plugin to your `tsconfig.json`:
88
+ ### 2. Add the plugin to tsconfig.json
35
89
 
36
90
  ```jsonc
37
91
  {
@@ -41,66 +95,43 @@ Add the plugin to your `tsconfig.json`:
41
95
  }
42
96
  ```
43
97
 
44
- In VS Code, make sure you're using the workspace TypeScript version (not the built-in one). Open the command palette and run **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
45
-
46
- ## Usage
47
-
48
- ### The `ty-fetch` client
49
-
50
- A lightweight HTTP client (similar to [ky](https://github.com/sindresorhus/ky)) with typed methods:
98
+ ### 3. Start fetching
51
99
 
52
100
  ```ts
53
- import tf from "ty-fetch";
101
+ import ty from "ty-fetch";
54
102
 
55
- // GET with typed response
56
- const customers = await tf.get("https://api.stripe.com/v1/customers").json();
103
+ // If your API has a spec at /openapi.json — types just work
104
+ const { data, error } = await ty.get("https://api.mycompany.com/v1/users");
105
+ ```
57
106
 
58
- // POST with typed body
59
- const customer = await tf.post("https://api.stripe.com/v1/customers", {
60
- body: { name: "Jane Doe", email: "jane@example.com" },
61
- }).json();
107
+ That's it.
62
108
 
63
- // Path params
64
- const repo = await tf.get("https://api.github.com/repos/{owner}/{repo}", {
65
- params: { path: { owner: "anthropics", repo: "claude-code" } },
66
- }).json();
109
+ > **VS Code users:** Make sure you're using the workspace TypeScript version, not the built-in one. Command Palette → **TypeScript: Select TypeScript Version** → **Use Workspace Version**
67
110
 
68
- // Query params
69
- const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
70
- params: { query: { status: "available" } },
71
- }).json();
72
- ```
111
+ Want to try it without setting up a project? Check out the [playground](./playground/).
73
112
 
74
- Response methods:
113
+ ---
75
114
 
76
- | Method | Returns |
77
- |---|---|
78
- | `.json()` | `Promise<T>` (typed from spec) |
79
- | `.text()` | `Promise<string>` |
80
- | `.blob()` | `Promise<Blob>` |
81
- | `.arrayBuffer()` | `Promise<ArrayBuffer>` |
82
- | `await` directly | `T` (same as `.json()`) |
115
+ ## 🔍 Spec discovery
83
116
 
84
- ### CLI
117
+ ### Auto-discovery (zero config)
85
118
 
86
- Run validation in CI or from the terminal:
119
+ When you call `ty.get("https://api.example.com/...")`, ty-fetch automatically probes the domain for an OpenAPI spec at these well-known paths:
87
120
 
88
- ```bash
89
- npx ty-fetch # uses ./tsconfig.json
90
- npx ty-fetch tsconfig.json # explicit path
91
- npx ty-fetch --verbose # show spec fetching details
92
121
  ```
93
-
122
+ /.well-known/openapi.json /.well-known/openapi.yaml
123
+ /openapi.json /openapi.yaml
124
+ /api/openapi.json /docs/openapi.json
125
+ /swagger.json /api-docs/openapi.json
94
126
  ```
95
- example.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API. Did you mean '/v1/customers'?
96
- example.ts:57:11 - error TF99001: Path '/pets' does not exist in Swagger Petstore. Did you mean '/pet'?
97
127
 
98
- 2 error(s) found.
99
- ```
128
+ If any path returns a valid OpenAPI spec, types are generated automatically.
129
+
130
+ **This means if your internal API serves a spec, ty-fetch will find it with zero configuration.**
100
131
 
101
- ## Custom specs
132
+ ### Point to specific specs
102
133
 
103
- Map domains to local files or URLs in your tsconfig plugin config:
134
+ For APIs that don't serve specs at standard paths, or for local spec files:
104
135
 
105
136
  ```jsonc
106
137
  {
@@ -109,7 +140,13 @@ Map domains to local files or URLs in your tsconfig plugin config:
109
140
  {
110
141
  "name": "ty-fetch/plugin",
111
142
  "specs": {
112
- "api.internal.company.com": "./specs/internal-api.json",
143
+ // Remote spec URL
144
+ "api.mycompany.com": "https://api.mycompany.com/docs/v2/openapi.json",
145
+
146
+ // Local file (resolved relative to tsconfig)
147
+ "payments.internal.com": "./specs/payments.yaml",
148
+
149
+ // Third-party API
113
150
  "api.partner.com": "https://partner.com/openapi.json"
114
151
  }
115
152
  }
@@ -118,55 +155,326 @@ Map domains to local files or URLs in your tsconfig plugin config:
118
155
  }
119
156
  ```
120
157
 
121
- - **File paths** are resolved relative to the tsconfig directory
122
- - **URLs** are fetched over HTTPS
123
- - Custom specs override built-in defaults for the same domain
158
+ Supported spec formats:
159
+
160
+ | Format | Versions |
161
+ |---|---|
162
+ | **OpenAPI** | 3.0, 3.1 |
163
+ | **Swagger** | 2.0 |
164
+ | **File types** | JSON, YAML |
165
+ | **Sources** | Local files, remote URLs, auto-discovered |
166
+
167
+ Custom specs override auto-discovery for the same domain.
168
+
169
+ ---
170
+
171
+ ## 📖 API Reference
172
+
173
+ ### `import ty from "ty-fetch"`
174
+
175
+ The default export is a pre-configured `TyFetch` instance.
176
+
177
+ ### HTTP Methods
178
+
179
+ ```ts
180
+ ty.get(url, options?) // GET
181
+ ty.post(url, options?) // POST
182
+ ty.put(url, options?) // PUT
183
+ ty.patch(url, options?) // PATCH
184
+ ty.delete(url, options?) // DELETE
185
+ ty.head(url, options?) // HEAD
186
+ ty(url, options?) // Custom method (set options.method)
187
+ ```
188
+
189
+ All methods return `Promise<{ data, error, response }>`.
190
+
191
+ When the plugin is active, the `url` parameter and all options are **typed from the OpenAPI spec**. Without the plugin, everything still works — just untyped.
192
+
193
+ ### Response Shape
194
+
195
+ Every method returns `{ data, error, response }`:
196
+
197
+ ```ts
198
+ const { data, error, response } = await ty.get("https://api.example.com/v1/users");
199
+
200
+ if (error) {
201
+ // error is the parsed error body (typed if spec defines error responses)
202
+ console.error(error.message);
203
+ console.log(response.status); // raw Response always available
204
+ return;
205
+ }
206
+
207
+ // data is the parsed response body — fully typed from the spec
208
+ console.log(data.users);
209
+ ```
210
+
211
+ - **`data`** — parsed response body (`undefined` if error). JSON is auto-parsed, otherwise text.
212
+ - **`error`** — parsed error body on non-2xx responses (`undefined` if success)
213
+ - **`response`** — the raw `Response` object (always present)
124
214
 
125
- This works in both the editor plugin and the CLI.
215
+ No `.json()` call needed responses are parsed automatically.
126
216
 
127
- ### Built-in specs
217
+ ### Options
128
218
 
129
- These APIs are supported out of the box (no config needed):
219
+ ```ts
220
+ ty.post("https://api.example.com/v1/users/{team}/invite", {
221
+ // Path params — replaces {placeholders} in the URL
222
+ params: {
223
+ path: { team: "engineering" },
224
+ query: { notify: true, role: "admin" },
225
+ },
226
+
227
+ // JSON request body (auto-serialized, Content-Type set automatically)
228
+ body: { email: "jane@example.com", name: "Jane Doe" },
229
+
230
+ // Headers (typed from security schemes when plugin is active)
231
+ headers: { "x-api-key": "sk_live_..." },
232
+
233
+ // Prefix URL — prepended to the url argument
234
+ prefixUrl: "https://api.example.com",
235
+
236
+ // All standard fetch options are supported
237
+ signal: AbortSignal.timeout(5000),
238
+ cache: "no-store",
239
+ credentials: "include",
240
+ });
241
+ ```
130
242
 
131
- | Domain | API | Paths |
243
+ | Option | Type | Description |
132
244
  |---|---|---|
133
- | `api.stripe.com` | Stripe API | 414 |
134
- | `petstore3.swagger.io` | Swagger Petstore | 13 |
135
- | `api.github.com` | GitHub REST API | 551 |
245
+ | `body` | `object` | JSON body — auto-serialized, `Content-Type: application/json` set |
246
+ | `params.path` | `object` | Replaces `{placeholder}` segments in the URL |
247
+ | `params.query` | `object` | Appended as `?key=value` query string |
248
+ | `headers` | `object` | HTTP headers (typed from spec security schemes) |
249
+ | `prefixUrl` | `string` | Prepended to the URL (useful with `create`/`extend`) |
136
250
 
137
- ## How it works
251
+ Plus all standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options (`signal`, `cache`, `credentials`, `mode`, etc.)
138
252
 
139
- 1. Plugin intercepts the TS language service (`getSemanticDiagnostics`, `getCompletionsAtPosition`, `getQuickInfoAtPosition`)
140
- 2. Finds `fetch()` / `tf.get()` / `tf.post()` etc. calls with string literal URLs
141
- 3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
142
- 4. Validates paths against the spec, suggests corrections via Levenshtein distance
143
- 5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts` using interface declaration merging — only for URLs actually used in your code
253
+ ### Creating Instances
254
+
255
+ ```ts
256
+ // Create a pre-configured instance
257
+ const api = ty.create({
258
+ prefixUrl: "https://api.mycompany.com",
259
+ headers: { "x-api-key": process.env.API_KEY },
260
+ });
261
+
262
+ // Now use short paths
263
+ const { data } = await api.get("/v1/users");
264
+ const { data: user } = await api.post("/v1/users", {
265
+ body: { name: "Jane" },
266
+ });
267
+
268
+ // Extend an existing instance (merges options)
269
+ const adminApi = api.extend({
270
+ headers: { "x-admin-token": process.env.ADMIN_TOKEN },
271
+ });
272
+ ```
144
273
 
145
- Spec fetching is async. On first encounter of a domain, the plugin fires a background fetch and returns no extra diagnostics. When the spec arrives, `refreshDiagnostics()` triggers the editor to re-check. This follows the same pattern as [graphqlsp](https://github.com/0no-co/graphqlsp).
274
+ ### Middleware
146
275
 
147
- ## Architecture
276
+ Add middleware to intercept requests and responses:
148
277
 
278
+ ```ts
279
+ import ty from "ty-fetch";
280
+
281
+ // Add auth header to every request
282
+ ty.use({
283
+ onRequest(request) {
284
+ request.headers.set("Authorization", `Bearer ${getToken()}`);
285
+ return request;
286
+ },
287
+ });
288
+
289
+ // Log all responses
290
+ ty.use({
291
+ onResponse(response) {
292
+ console.log(`${response.status} ${response.url}`);
293
+ return response;
294
+ },
295
+ });
296
+
297
+ // Retry on 401
298
+ ty.use({
299
+ async onResponse(response) {
300
+ if (response.status === 401) {
301
+ await refreshToken();
302
+ return fetch(response.url, { headers: { Authorization: `Bearer ${getToken()}` } });
303
+ }
304
+ return response;
305
+ },
306
+ });
149
307
  ```
150
- src/
151
- plugin/index.ts TS language service plugin (diagnostics, completions, hover)
152
- cli/index.ts CLI entry point for CI validation
153
- core/ Shared logic (URL parsing, spec cache, path matching, body validation)
154
- generate-types.ts OpenAPI schema -> TypeScript type declarations
155
- test-project/ Example project using the plugin
156
- test/ Unit tests
308
+
309
+ | Hook | Signature | Description |
310
+ |---|---|---|
311
+ | `onRequest` | `(request: Request) => Request \| RequestInit \| void` | Modify the request before it's sent |
312
+ | `onResponse` | `(response: Response) => Response \| void` | Modify or replace the response |
313
+
314
+ Both hooks can be async. Middleware runs in the order it's added.
315
+
316
+ ### Streaming
317
+
318
+ Stream SSE (Server-Sent Events), NDJSON, or raw text responses:
319
+
320
+ ```ts
321
+ // Server-Sent Events (e.g. OpenAI, Anthropic streaming APIs)
322
+ for await (const event of ty.stream("https://api.example.com/v1/chat", {
323
+ method: "POST",
324
+ body: { prompt: "Hello", stream: true },
325
+ })) {
326
+ console.log(event); // each parsed SSE event
327
+ }
328
+
329
+ // NDJSON streaming
330
+ for await (const line of ty.stream("https://api.example.com/v1/logs")) {
331
+ console.log(line); // each parsed JSON line
332
+ }
333
+ ```
334
+
335
+ Auto-detects the format from `Content-Type`:
336
+ - `text/event-stream` → SSE (parses `data:` lines, stops at `[DONE]`)
337
+ - `application/x-ndjson` / `application/jsonl` → NDJSON (parses each line as JSON)
338
+ - Anything else → raw text chunks
339
+
340
+ ### Plugin Features (editor only)
341
+
342
+ When the TS plugin is active, you get these extras on top of the runtime API:
343
+
344
+ | Feature | What it does |
345
+ |---|---|
346
+ | **Typed responses** | `data` is the actual response type from the spec, not `any` |
347
+ | **Typed body** | `body` option is validated against the spec's request body schema |
348
+ | **Typed query params** | `params.query` keys and types from the spec's parameter definitions |
349
+ | **Typed path params** | `params.path` keys from `{placeholder}` segments |
350
+ | **Typed headers** | Required headers from the spec's security schemes |
351
+ | **Path validation** | Red squiggles on invalid API paths with "did you mean?" |
352
+ | **Autocomplete** | URL completions inside string literals, filtered by HTTP method |
353
+ | **Hover docs** | Hover over a URL to see available methods and descriptions |
354
+ | **JSDoc** | Property descriptions from the spec appear in hover tooltips |
355
+ | **Example inference** | Types inferred from response `example` when `schema` is missing |
356
+
357
+ ---
358
+
359
+ ## 🖥️ CI / Type checking
360
+
361
+ ### Why not just `tsc`?
362
+
363
+ `tsc` doesn't run TypeScript language service plugins — it only sees the base `any` types. Your code will compile, but you won't get type errors for wrong API paths or mismatched params.
364
+
365
+ For CI, you have two options:
366
+
367
+ ### Option 1: ty-fetch CLI (recommended)
368
+
369
+ Validates API paths against OpenAPI specs — catches typos and invalid endpoints:
370
+
371
+ ```bash
372
+ npx ty-fetch # uses ./tsconfig.json
373
+ npx ty-fetch tsconfig.json # explicit path
374
+ npx ty-fetch --verbose # show spec fetching details
375
+ ```
376
+
377
+ ```
378
+ src/api.ts:21:11 - error TF99001: Path '/v1/uusers' does not exist.
379
+ Did you mean '/v1/users'?
380
+
381
+ 1 error(s) found.
382
+ ```
383
+
384
+ Add to your CI pipeline:
385
+
386
+ ```yaml
387
+ # GitHub Actions
388
+ - run: npx ty-fetch tsconfig.json
389
+ ```
390
+
391
+ ### Option 2: ESLint plugin (coming soon)
392
+
393
+ If you prefer eslint over a separate CLI, you can use the ty-fetch eslint rule:
394
+
395
+ ```bash
396
+ npm install -D eslint-plugin-ty-fetch
397
+ ```
398
+
399
+ ```js
400
+ // eslint.config.mjs
401
+ import tyFetch from "eslint-plugin-ty-fetch";
402
+
403
+ export default [tyFetch.configs.recommended];
404
+ ```
405
+
406
+ This runs the same validation as the CLI but inside your existing eslint pipeline.
407
+
408
+ > **Note:** The eslint plugin is not yet published. For now, use the CLI.
409
+
410
+ ---
411
+
412
+ ## 🌍 Runtime compatibility
413
+
414
+ ty-fetch is a thin wrapper around the standard Fetch API. It works anywhere `fetch` is available:
415
+
416
+ | Runtime | Supported |
417
+ |---|---|
418
+ | **Node.js** | 18+ (native fetch) |
419
+ | **Bun** | ✅ |
420
+ | **Deno** | ✅ |
421
+ | **Browsers** | ✅ (all modern browsers) |
422
+ | **Cloudflare Workers** | ✅ |
423
+
424
+ The TS plugin (type generation) runs in your editor's TypeScript server — it doesn't affect runtime behavior.
425
+
426
+ ---
427
+
428
+ ## ❓ FAQ
429
+
430
+ <details>
431
+ <summary><strong>Why do I see <code>any</code> types instead of typed responses?</strong></summary>
432
+
433
+ The TS plugin needs to be active. Check:
434
+ 1. Plugin is in `tsconfig.json` under `compilerOptions.plugins`
435
+ 2. In VS Code, you're using the **workspace** TypeScript version (not the built-in one)
436
+ 3. Restart the TS server after config changes (Command Palette → "TypeScript: Restart TS Server")
437
+ 4. The API's OpenAPI spec is reachable (try `curl https://your-api.com/openapi.json`)
438
+ </details>
439
+
440
+ <details>
441
+ <summary><strong>Why doesn't <code>tsc</code> catch type errors?</strong></summary>
442
+
443
+ `tsc` doesn't run language service plugins — it only sees the base `any` types. Use the ty-fetch CLI for CI validation: `npx ty-fetch tsconfig.json`
444
+ </details>
445
+
446
+ <details>
447
+ <summary><strong>Does ty-fetch work without the plugin?</strong></summary>
448
+
449
+ Yes. The runtime client works independently — you get `{ data, error, response }` back from every call. You just won't get typed responses or path validation. It's a perfectly usable HTTP client on its own.
450
+ </details>
451
+
452
+ <details>
453
+ <summary><strong>What if my API doesn't serve an OpenAPI spec?</strong></summary>
454
+
455
+ You can point to a local spec file in your tsconfig:
456
+ ```jsonc
457
+ "specs": { "api.mycompany.com": "./specs/my-api.yaml" }
157
458
  ```
459
+ </details>
460
+
461
+ <details>
462
+ <summary><strong>How big is this package?</strong></summary>
463
+
464
+ The runtime client (`index.js`) is ~100 lines. The plugin code ships in `dist/` but only runs inside the TS server, not in your bundle.
465
+ </details>
466
+
467
+ ---
158
468
 
159
- ## Development
469
+ ## 🧪 Development
160
470
 
161
471
  ```bash
162
472
  npm run build # compile TypeScript
163
473
  npm run watch # compile in watch mode
164
- npm test # run unit tests
474
+ npm test # run unit tests (132 tests)
475
+ npm run check # lint with biome + eslint
165
476
  ```
166
477
 
167
- To test the editor experience:
478
+ ## License
168
479
 
169
- 1. Open `test-project/` in VS Code
170
- 2. Select the workspace TypeScript version
171
- 3. Restart the TS server (`TypeScript: Restart TS Server`)
172
- 4. Edit `test-project/example.ts` and observe diagnostics/completions
480
+ MIT
package/base.d.ts CHANGED
@@ -17,29 +17,36 @@ export interface Options<
17
17
  prefixUrl?: string;
18
18
  }
19
19
 
20
- export interface ResponsePromise<T = unknown> extends PromiseLike<T> {
21
- json(): Promise<T>;
22
- text(): Promise<string>;
23
- blob(): Promise<Blob>;
24
- arrayBuffer(): Promise<ArrayBuffer>;
25
- formData(): Promise<FormData>;
20
+ export type FetchResult<TData = unknown, TError = unknown> =
21
+ | { data: TData; error: undefined; response: Response }
22
+ | { data: undefined; error: TError; response: Response };
23
+
24
+ export interface Middleware {
25
+ onRequest?: (request: Request) => Request | RequestInit | void | Promise<Request | RequestInit | void>;
26
+ onResponse?: (response: Response) => Response | void | Promise<Response | void>;
27
+ }
28
+
29
+ export interface StreamResult<T = unknown> extends AsyncIterable<T> {
30
+ response: Promise<Response>;
26
31
  }
27
32
 
28
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
34
  type BaseOptions = Options<any, Record<string, any>, Record<string, any>>;
30
35
 
31
36
  export interface TyFetch {
32
- (url: string, options?: BaseOptions): ResponsePromise<any>;
33
- get(url: string, options?: BaseOptions): ResponsePromise<any>;
34
- post(url: string, options?: BaseOptions): ResponsePromise<any>;
35
- put(url: string, options?: BaseOptions): ResponsePromise<any>;
36
- patch(url: string, options?: BaseOptions): ResponsePromise<any>;
37
- delete(url: string, options?: BaseOptions): ResponsePromise<any>;
38
- head(url: string, options?: BaseOptions): ResponsePromise<any>;
37
+ (url: string, options?: BaseOptions): Promise<FetchResult<any>>;
38
+ get(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
39
+ post(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
40
+ put(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
41
+ patch(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
42
+ delete(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
43
+ head(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
44
+ stream(url: string, options?: BaseOptions): StreamResult;
39
45
  create(defaults?: BaseOptions): TyFetch;
40
46
  extend(defaults?: BaseOptions): TyFetch;
47
+ use(middleware: Middleware): TyFetch;
41
48
  HTTPError: typeof HTTPError;
42
49
  }
43
50
 
44
- declare const tf: TyFetch;
45
- export default tf;
51
+ declare const ty: TyFetch;
52
+ export default ty;