ty-fetch 0.0.2-beta.9 → 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 +21 -0
- package/README.md +399 -105
- package/base.d.ts +22 -15
- package/dist/cli/index.js +21 -7
- package/dist/core/ast-helpers.js +13 -7
- package/dist/core/body-validator.d.ts +1 -1
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +4 -4
- package/dist/core/spec-cache.d.ts +1 -1
- package/dist/core/spec-cache.js +15 -15
- package/dist/core/url-parser.js +6 -0
- package/dist/generate-types.d.ts +4 -0
- package/dist/generate-types.js +87 -38
- package/dist/plugin/index.js +38 -15
- package/index.d.ts +22 -15
- package/index.js +153 -30
- package/package.json +37 -5
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,51 +1,91 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.png" alt="ty-fetch" width="350" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">ty-fetch</h1>
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/ty-fetch)
|
|
8
|
+
[](https://github.com/alnorris/ty-fetch/blob/main/LICENSE)
|
|
9
|
+
[](https://github.com/alnorris/ty-fetch/actions/workflows/ci.yml)
|
|
10
|
+
|
|
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.**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install ty-fetch
|
|
15
|
+
```
|
|
16
|
+
|
|
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
|
+
```
|
|
7
32
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
});
|
|
11
45
|
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
// Error: Path '/v1/cutsomers' does not exist in Stripe API.
|
|
15
|
-
// Did you mean '/v1/customers'?
|
|
46
|
+
if (error) return console.error(error);
|
|
47
|
+
console.log(data.user.id); // fully typed, autocomplete works
|
|
16
48
|
```
|
|
17
49
|
|
|
18
|
-
|
|
50
|
+
If your API serves an OpenAPI spec at `/openapi.json` (or any well-known path), ty-fetch finds it automatically. No config needed.
|
|
19
51
|
|
|
20
|
-
|
|
52
|
+
---
|
|
21
53
|
|
|
22
|
-
|
|
54
|
+
## 🤔 How does it work?
|
|
23
55
|
|
|
24
|
-
|
|
56
|
+
ty-fetch is a **TypeScript language service plugin**. When you write a `ty.get("https://...")` call:
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- **Typed headers** — required headers (API keys, auth) from security schemes
|
|
31
|
-
- **Path validation** — red squiggles for typos, with "did you mean?" suggestions
|
|
32
|
-
- **Autocomplete** — URL path completions inside string literals, filtered by HTTP method
|
|
33
|
-
- **Hover info** — hover over a URL to see available methods and descriptions
|
|
34
|
-
- **JSDoc descriptions** — property descriptions from the spec show up in hover tooltips
|
|
35
|
-
- **Auto-discovery** — probes well-known paths (`/openapi.json`, `/.well-known/openapi.yaml`, etc.) when no spec is configured
|
|
36
|
-
- **YAML support** — specs can be JSON or YAML, local files or remote URLs
|
|
37
|
-
- **Example inference** — generates types from response `example` when `schema` is missing
|
|
38
|
-
- **On-demand** — only fetches specs and generates types for APIs you actually call
|
|
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
|
|
39
62
|
|
|
40
|
-
|
|
63
|
+
Types appear in your editor instantly. When the spec changes, types update automatically. No build step ever.
|
|
41
64
|
|
|
42
|
-
|
|
65
|
+
### Compared to other tools
|
|
66
|
+
|
|
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
|
|
43
83
|
|
|
44
84
|
```bash
|
|
45
85
|
npm install ty-fetch
|
|
46
86
|
```
|
|
47
87
|
|
|
48
|
-
Add the plugin to
|
|
88
|
+
### 2. Add the plugin to tsconfig.json
|
|
49
89
|
|
|
50
90
|
```jsonc
|
|
51
91
|
{
|
|
@@ -55,78 +95,43 @@ Add the plugin to your `tsconfig.json`:
|
|
|
55
95
|
}
|
|
56
96
|
```
|
|
57
97
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
That's it. Start writing `tf.get("https://...")` and types appear automatically.
|
|
61
|
-
|
|
62
|
-
## Usage
|
|
98
|
+
### 3. Start fetching
|
|
63
99
|
|
|
64
100
|
```ts
|
|
65
|
-
import
|
|
66
|
-
|
|
67
|
-
// GET with typed response
|
|
68
|
-
const customers = await tf.get("https://api.stripe.com/v1/customers").json();
|
|
69
|
-
|
|
70
|
-
// POST with typed body
|
|
71
|
-
const customer = await tf.post("https://api.stripe.com/v1/customers", {
|
|
72
|
-
body: { name: "Jane Doe", email: "jane@example.com" },
|
|
73
|
-
}).json();
|
|
101
|
+
import ty from "ty-fetch";
|
|
74
102
|
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
params: { path: { owner: "anthropics", repo: "claude-code" } },
|
|
78
|
-
}).json();
|
|
79
|
-
|
|
80
|
-
// Query params
|
|
81
|
-
const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
|
|
82
|
-
params: { query: { status: "available" } },
|
|
83
|
-
}).json();
|
|
84
|
-
|
|
85
|
-
// Headers (typed from security schemes)
|
|
86
|
-
const data = await tf.get("https://api.scrapecreators.com/v1/reddit/search", {
|
|
87
|
-
params: { query: { query: "typescript" } },
|
|
88
|
-
headers: { "x-api-key": process.env.API_KEY },
|
|
89
|
-
}).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");
|
|
90
105
|
```
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
That's it. ✨
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
|---|---|
|
|
96
|
-
| `.json()` | `Promise<T>` (typed from spec) |
|
|
97
|
-
| `.text()` | `Promise<string>` |
|
|
98
|
-
| `.blob()` | `Promise<Blob>` |
|
|
99
|
-
| `.arrayBuffer()` | `Promise<ArrayBuffer>` |
|
|
100
|
-
| `await` directly | `T` (same as `.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**
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
Want to try it without setting up a project? Check out the [playground](./playground/).
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
---
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
## 🔍 Spec discovery
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|---|---|
|
|
110
|
-
| `api.stripe.com` | Stripe API |
|
|
111
|
-
| `api.github.com` | GitHub REST API |
|
|
112
|
-
| `petstore3.swagger.io` | Swagger Petstore |
|
|
117
|
+
### Auto-discovery (zero config)
|
|
113
118
|
|
|
114
|
-
|
|
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:
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
```
|
|
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
|
|
126
|
+
```
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
- `/openapi.json`, `.yaml`, `.yml`
|
|
120
|
-
- `/api/openapi.json`, `.yaml`
|
|
121
|
-
- `/docs/openapi.json`, `.yaml`
|
|
122
|
-
- `/swagger.json`
|
|
123
|
-
- `/api-docs/openapi.json`
|
|
128
|
+
If any path returns a valid OpenAPI spec, types are generated automatically.
|
|
124
129
|
|
|
125
|
-
|
|
130
|
+
**This means if your internal API serves a spec, ty-fetch will find it with zero configuration.**
|
|
126
131
|
|
|
127
|
-
###
|
|
132
|
+
### Point to specific specs
|
|
128
133
|
|
|
129
|
-
|
|
134
|
+
For APIs that don't serve specs at standard paths, or for local spec files:
|
|
130
135
|
|
|
131
136
|
```jsonc
|
|
132
137
|
{
|
|
@@ -135,7 +140,13 @@ Map domains to local files or remote URLs in your tsconfig:
|
|
|
135
140
|
{
|
|
136
141
|
"name": "ty-fetch/plugin",
|
|
137
142
|
"specs": {
|
|
138
|
-
|
|
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
|
|
139
150
|
"api.partner.com": "https://partner.com/openapi.json"
|
|
140
151
|
}
|
|
141
152
|
}
|
|
@@ -144,14 +155,218 @@ Map domains to local files or remote URLs in your tsconfig:
|
|
|
144
155
|
}
|
|
145
156
|
```
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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)
|
|
214
|
+
|
|
215
|
+
No `.json()` call needed — responses are parsed automatically.
|
|
216
|
+
|
|
217
|
+
### Options
|
|
218
|
+
|
|
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
|
+
```
|
|
242
|
+
|
|
243
|
+
| Option | Type | Description |
|
|
244
|
+
|---|---|---|
|
|
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`) |
|
|
250
|
+
|
|
251
|
+
Plus all standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options (`signal`, `cache`, `credentials`, `mode`, etc.)
|
|
252
|
+
|
|
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
|
+
```
|
|
273
|
+
|
|
274
|
+
### Middleware
|
|
275
|
+
|
|
276
|
+
Add middleware to intercept requests and responses:
|
|
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
|
+
});
|
|
307
|
+
```
|
|
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)
|
|
151
341
|
|
|
152
|
-
|
|
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 |
|
|
153
356
|
|
|
154
|
-
|
|
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:
|
|
155
370
|
|
|
156
371
|
```bash
|
|
157
372
|
npx ty-fetch # uses ./tsconfig.json
|
|
@@ -160,27 +375,106 @@ npx ty-fetch --verbose # show spec fetching details
|
|
|
160
375
|
```
|
|
161
376
|
|
|
162
377
|
```
|
|
163
|
-
src/api.ts:21:11 - error TF99001: Path '/v1/
|
|
378
|
+
src/api.ts:21:11 - error TF99001: Path '/v1/uusers' does not exist.
|
|
379
|
+
Did you mean '/v1/users'?
|
|
164
380
|
|
|
165
381
|
1 error(s) found.
|
|
166
382
|
```
|
|
167
383
|
|
|
168
|
-
|
|
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** | ✅ |
|
|
169
423
|
|
|
170
|
-
|
|
171
|
-
2. Finds `tf.get()` / `tf.post()` / `fetch()` calls with string literal URLs
|
|
172
|
-
3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
|
|
173
|
-
4. Validates paths against the spec, suggests corrections via Levenshtein distance
|
|
174
|
-
5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts` via interface declaration merging — only for URLs actually used in your code
|
|
424
|
+
The TS plugin (type generation) runs in your editor's TypeScript server — it doesn't affect runtime behavior.
|
|
175
425
|
|
|
176
|
-
|
|
426
|
+
---
|
|
177
427
|
|
|
178
|
-
|
|
428
|
+
## ❓ FAQ
|
|
179
429
|
|
|
180
|
-
|
|
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" }
|
|
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
|
+
---
|
|
468
|
+
|
|
469
|
+
## 🧪 Development
|
|
181
470
|
|
|
182
471
|
```bash
|
|
183
472
|
npm run build # compile TypeScript
|
|
184
473
|
npm run watch # compile in watch mode
|
|
185
|
-
npm test # run unit tests (
|
|
474
|
+
npm test # run unit tests (132 tests)
|
|
475
|
+
npm run check # lint with biome + eslint
|
|
186
476
|
```
|
|
477
|
+
|
|
478
|
+
## License
|
|
479
|
+
|
|
480
|
+
MIT
|
package/base.d.ts
CHANGED
|
@@ -17,29 +17,36 @@ export interface Options<
|
|
|
17
17
|
prefixUrl?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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):
|
|
33
|
-
get(url: string, options?: BaseOptions):
|
|
34
|
-
post(url: string, options?: BaseOptions):
|
|
35
|
-
put(url: string, options?: BaseOptions):
|
|
36
|
-
patch(url: string, options?: BaseOptions):
|
|
37
|
-
delete(url: string, options?: BaseOptions):
|
|
38
|
-
head(url: string, options?: BaseOptions):
|
|
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
|
|
45
|
-
export default
|
|
51
|
+
declare const ty: TyFetch;
|
|
52
|
+
export default ty;
|