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 +21 -0
- package/README.md +406 -98
- 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 +106 -44
- package/dist/core/types.d.ts +3 -0
- 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,37 +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)
|
|
7
10
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
### Compared to other tools
|
|
27
66
|
|
|
28
|
-
|
|
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
|
|
85
|
+
npm install ty-fetch
|
|
32
86
|
```
|
|
33
87
|
|
|
34
|
-
Add the plugin to
|
|
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
|
-
|
|
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
|
|
101
|
+
import ty from "ty-fetch";
|
|
54
102
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
---
|
|
75
114
|
|
|
76
|
-
|
|
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
|
-
###
|
|
117
|
+
### Auto-discovery (zero config)
|
|
85
118
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
+
### Point to specific specs
|
|
102
133
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
215
|
+
No `.json()` call needed — responses are parsed automatically.
|
|
126
216
|
|
|
127
|
-
###
|
|
217
|
+
### Options
|
|
128
218
|
|
|
129
|
-
|
|
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
|
-
|
|
|
243
|
+
| Option | Type | Description |
|
|
132
244
|
|---|---|---|
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
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
|
-
|
|
251
|
+
Plus all standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options (`signal`, `cache`, `credentials`, `mode`, etc.)
|
|
138
252
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
274
|
+
### Middleware
|
|
146
275
|
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
478
|
+
## License
|
|
168
479
|
|
|
169
|
-
|
|
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
|
|
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;
|