ty-fetch 0.0.2-beta.1 → 0.0.2-beta.11
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 +161 -97
- package/base.d.ts +15 -10
- package/dist/cli/index.js +10 -0
- package/dist/core/spec-cache.js +115 -41
- package/dist/core/types.d.ts +3 -0
- package/dist/generate-types.d.ts +8 -0
- package/dist/generate-types.js +78 -10
- package/index.d.ts +15 -10
- package/package.json +28 -3
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,70 @@
|
|
|
1
|
-
# ty-fetch
|
|
1
|
+
# ⚡ ty-fetch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ty-fetch)
|
|
4
|
+
[](https://github.com/alnorris/ty-fetch/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/alnorris/ty-fetch/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
**Type-safe fetch from OpenAPI specs. No codegen, no build step.**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install ty-fetch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
// tsconfig.json — that's the entire setup
|
|
15
|
+
{
|
|
16
|
+
"compilerOptions": {
|
|
17
|
+
"plugins": [{ "name": "ty-fetch/plugin" }]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
4
21
|
|
|
5
22
|
```ts
|
|
6
23
|
import tf from "ty-fetch";
|
|
7
24
|
|
|
8
|
-
const
|
|
9
|
-
//
|
|
10
|
-
|
|
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'?
|
|
25
|
+
const data = await tf.get("https://api.mycompany.com/v1/users").json();
|
|
26
|
+
// ^ fully typed from your OpenAPI spec — autocomplete, hover docs, everything
|
|
15
27
|
```
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
If your API serves an OpenAPI spec at `/openapi.json` (or any well-known path), ty-fetch finds it automatically. No config, no codegen, no generated files. Just types.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🤔 How does it work?
|
|
34
|
+
|
|
35
|
+
ty-fetch is a **TypeScript language service plugin**. When you write a `tf.get("https://...")` call:
|
|
36
|
+
|
|
37
|
+
1. 🔍 It extracts the domain from the URL
|
|
38
|
+
2. 📡 Fetches the OpenAPI spec (checks `/openapi.json`, `/.well-known/openapi.yaml`, etc.)
|
|
39
|
+
3. 🏗️ Generates typed overloads on-the-fly — response types, query params, headers, everything
|
|
40
|
+
4. ✅ Validates your API paths and suggests corrections for typos
|
|
41
|
+
|
|
42
|
+
Types appear in your editor instantly. When the spec changes, types update automatically. No build step ever.
|
|
43
|
+
|
|
44
|
+
### Compared to other tools
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
| | 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) |
|
|
47
|
+
|---|---|---|---|---|
|
|
48
|
+
| **Codegen step** | None | `npx openapi-typescript ...` | Needs openapi-typescript first | `npx orval` |
|
|
49
|
+
| **Build step** | None | Required | Required | Required |
|
|
50
|
+
| **Generated files** | None | `.d.ts` files | `.d.ts` files | Full client |
|
|
51
|
+
| **Spec changes** | Auto-updates | Re-run codegen | Re-run codegen | Re-run codegen |
|
|
52
|
+
| **Editor integration** | TS plugin (autocomplete, hover, diagnostics) | Types only | Types only | Types only |
|
|
53
|
+
| **Path validation** | Typo detection with suggestions | None | None | None |
|
|
54
|
+
| **Auto-discovery** | Probes well-known paths | Manual | Manual | Manual |
|
|
55
|
+
| **Runtime** | Lightweight fetch wrapper | None (types only) | Fetch wrapper | Axios/fetch client |
|
|
25
56
|
|
|
26
|
-
|
|
57
|
+
---
|
|
27
58
|
|
|
28
|
-
##
|
|
59
|
+
## 📦 Quick start
|
|
60
|
+
|
|
61
|
+
### 1. Install
|
|
29
62
|
|
|
30
63
|
```bash
|
|
31
|
-
npm install
|
|
64
|
+
npm install ty-fetch
|
|
32
65
|
```
|
|
33
66
|
|
|
34
|
-
Add the plugin to
|
|
67
|
+
### 2. Add the plugin to tsconfig.json
|
|
35
68
|
|
|
36
69
|
```jsonc
|
|
37
70
|
{
|
|
@@ -41,66 +74,43 @@ Add the plugin to your `tsconfig.json`:
|
|
|
41
74
|
}
|
|
42
75
|
```
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## Usage
|
|
77
|
+
### 3. Use workspace TypeScript in VS Code
|
|
47
78
|
|
|
48
|
-
|
|
79
|
+
**Command Palette** → **TypeScript: Select TypeScript Version** → **Use Workspace Version**
|
|
49
80
|
|
|
50
|
-
|
|
81
|
+
### 4. Start fetching
|
|
51
82
|
|
|
52
83
|
```ts
|
|
53
84
|
import tf from "ty-fetch";
|
|
54
85
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
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();
|
|
62
|
-
|
|
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();
|
|
67
|
-
|
|
68
|
-
// Query params
|
|
69
|
-
const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
|
|
70
|
-
params: { query: { status: "available" } },
|
|
71
|
-
}).json();
|
|
86
|
+
// If your API has a spec at /openapi.json — types just work
|
|
87
|
+
const users = await tf.get("https://api.mycompany.com/v1/users").json();
|
|
72
88
|
```
|
|
73
89
|
|
|
74
|
-
|
|
90
|
+
That's it. ✨
|
|
75
91
|
|
|
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()`) |
|
|
92
|
+
---
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
## 🔍 Spec discovery
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
### Auto-discovery (zero config)
|
|
87
97
|
|
|
88
|
-
|
|
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
|
-
```
|
|
98
|
+
When you call `tf.get("https://api.example.com/...")`, ty-fetch automatically probes the domain for an OpenAPI spec at these well-known paths:
|
|
93
99
|
|
|
94
100
|
```
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
/.well-known/openapi.json /.well-known/openapi.yaml
|
|
102
|
+
/openapi.json /openapi.yaml
|
|
103
|
+
/api/openapi.json /docs/openapi.json
|
|
104
|
+
/swagger.json /api-docs/openapi.json
|
|
99
105
|
```
|
|
100
106
|
|
|
101
|
-
|
|
107
|
+
If any path returns a valid OpenAPI spec, types are generated automatically.
|
|
108
|
+
|
|
109
|
+
**This means if your internal API serves a spec, ty-fetch will find it with zero configuration.**
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
### Point to specific specs
|
|
112
|
+
|
|
113
|
+
For APIs that don't serve specs at standard paths, or for local spec files:
|
|
104
114
|
|
|
105
115
|
```jsonc
|
|
106
116
|
{
|
|
@@ -109,7 +119,13 @@ Map domains to local files or URLs in your tsconfig plugin config:
|
|
|
109
119
|
{
|
|
110
120
|
"name": "ty-fetch/plugin",
|
|
111
121
|
"specs": {
|
|
112
|
-
|
|
122
|
+
// Remote spec URL
|
|
123
|
+
"api.mycompany.com": "https://api.mycompany.com/docs/v2/openapi.json",
|
|
124
|
+
|
|
125
|
+
// Local file (resolved relative to tsconfig)
|
|
126
|
+
"payments.internal.com": "./specs/payments.yaml",
|
|
127
|
+
|
|
128
|
+
// Third-party API
|
|
113
129
|
"api.partner.com": "https://partner.com/openapi.json"
|
|
114
130
|
}
|
|
115
131
|
}
|
|
@@ -118,55 +134,103 @@ Map domains to local files or URLs in your tsconfig plugin config:
|
|
|
118
134
|
}
|
|
119
135
|
```
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
- **URLs** are fetched over HTTPS
|
|
123
|
-
- Custom specs override built-in defaults for the same domain
|
|
137
|
+
JSON and YAML specs both supported. Custom specs override auto-discovery for the same domain.
|
|
124
138
|
|
|
125
|
-
|
|
139
|
+
---
|
|
126
140
|
|
|
127
|
-
|
|
141
|
+
## 🔧 Usage
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
```ts
|
|
144
|
+
import tf from "ty-fetch";
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| `api.stripe.com` | Stripe API | 414 |
|
|
134
|
-
| `petstore3.swagger.io` | Swagger Petstore | 13 |
|
|
135
|
-
| `api.github.com` | GitHub REST API | 551 |
|
|
146
|
+
// 📥 GET — response is fully typed
|
|
147
|
+
const users = await tf.get("https://api.mycompany.com/v1/users").json();
|
|
136
148
|
|
|
137
|
-
|
|
149
|
+
// 📤 POST — body is validated against the spec
|
|
150
|
+
const user = await tf.post("https://api.mycompany.com/v1/users", {
|
|
151
|
+
body: { name: "Jane Doe", email: "jane@example.com" },
|
|
152
|
+
}).json();
|
|
138
153
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
// 🔗 Path params — typed from the spec's {param} placeholders
|
|
155
|
+
const user = await tf.get("https://api.mycompany.com/v1/users/{id}", {
|
|
156
|
+
params: { path: { id: "123" } },
|
|
157
|
+
}).json();
|
|
158
|
+
|
|
159
|
+
// 🔍 Query params — typed from the spec's parameter definitions
|
|
160
|
+
const results = await tf.get("https://api.mycompany.com/v1/users", {
|
|
161
|
+
params: { query: { role: "admin", limit: 10 } },
|
|
162
|
+
}).json();
|
|
144
163
|
|
|
145
|
-
|
|
164
|
+
// 🔑 Headers — required API keys typed from security schemes
|
|
165
|
+
const data = await tf.get("https://api.mycompany.com/v1/data", {
|
|
166
|
+
headers: { "x-api-key": process.env.API_KEY },
|
|
167
|
+
}).json();
|
|
168
|
+
```
|
|
146
169
|
|
|
147
|
-
|
|
170
|
+
### Response methods
|
|
148
171
|
|
|
172
|
+
| Method | Returns |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `.json()` | `Promise<T>` — typed from spec |
|
|
175
|
+
| `.text()` | `Promise<string>` |
|
|
176
|
+
| `.blob()` | `Promise<Blob>` |
|
|
177
|
+
| `.arrayBuffer()` | `Promise<ArrayBuffer>` |
|
|
178
|
+
| `await` directly | `T` — same as `.json()` |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 🚀 Features
|
|
183
|
+
|
|
184
|
+
- 🔮 **Zero codegen** — types generated on-the-fly by a TS plugin, not a build step
|
|
185
|
+
- 🔍 **Auto-discovery** — finds OpenAPI specs at well-known paths automatically
|
|
186
|
+
- 📦 **Typed responses** — `.json()` returns the actual response type
|
|
187
|
+
- ✏️ **Typed request bodies** — body params validated against the schema
|
|
188
|
+
- 🔗 **Typed path & query params** — based on the endpoint definition
|
|
189
|
+
- 🔑 **Typed headers** — required API keys from security schemes
|
|
190
|
+
- 🚨 **Path validation** — red squiggles for typos, with "did you mean?" suggestions
|
|
191
|
+
- 💡 **Autocomplete** — URL path completions inside string literals
|
|
192
|
+
- 📖 **JSDoc descriptions** — property descriptions from the spec in hover tooltips
|
|
193
|
+
- 📄 **YAML + JSON** — specs can be either format, local files or remote URLs
|
|
194
|
+
- 🧠 **Example inference** — generates types from response `example` when `schema` is missing
|
|
195
|
+
- ⚡ **On-demand** — only fetches specs for APIs you actually call in your code
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 🖥️ CLI
|
|
200
|
+
|
|
201
|
+
Validate API calls in CI — catches typos and bad paths without running the app:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npx ty-fetch # uses ./tsconfig.json
|
|
205
|
+
npx ty-fetch tsconfig.json # explicit path
|
|
206
|
+
npx ty-fetch --verbose # show spec fetching details
|
|
149
207
|
```
|
|
150
|
-
|
|
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
|
|
208
|
+
|
|
157
209
|
```
|
|
210
|
+
src/api.ts:21:11 - error TF99001: Path '/v1/uusers' does not exist.
|
|
211
|
+
Did you mean '/v1/users'?
|
|
158
212
|
|
|
159
|
-
|
|
213
|
+
1 error(s) found.
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 🧪 Development
|
|
160
219
|
|
|
161
220
|
```bash
|
|
162
221
|
npm run build # compile TypeScript
|
|
163
222
|
npm run watch # compile in watch mode
|
|
164
|
-
npm test # run unit tests
|
|
223
|
+
npm test # run unit tests (74 tests)
|
|
165
224
|
```
|
|
166
225
|
|
|
167
|
-
|
|
226
|
+
## Roadmap
|
|
227
|
+
|
|
228
|
+
- [ ] Record demo GIF showing autocomplete + typo detection in VS Code
|
|
229
|
+
- [ ] Spec caching to disk (avoid re-fetching on TS server restart)
|
|
230
|
+
- [ ] `ty-fetch generate` CLI command to generate `.d.ts` for `tsc` compatibility
|
|
231
|
+
- [ ] Support OpenAPI 2.0 (Swagger) specs
|
|
232
|
+
- [ ] Request/response interceptors and middleware
|
|
233
|
+
|
|
234
|
+
## License
|
|
168
235
|
|
|
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
|
|
236
|
+
MIT
|
package/base.d.ts
CHANGED
|
@@ -6,12 +6,14 @@ export interface Options<
|
|
|
6
6
|
TBody = never,
|
|
7
7
|
TPathParams = never,
|
|
8
8
|
TQueryParams = never,
|
|
9
|
-
|
|
9
|
+
THeaders extends Record<string, string> = Record<string, string>,
|
|
10
|
+
> extends Omit<RequestInit, 'body' | 'headers'> {
|
|
10
11
|
body?: TBody;
|
|
11
12
|
params?: {
|
|
12
13
|
path?: TPathParams;
|
|
13
14
|
query?: TQueryParams;
|
|
14
15
|
};
|
|
16
|
+
headers?: THeaders & Record<string, string>;
|
|
15
17
|
prefixUrl?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -23,16 +25,19 @@ export interface ResponsePromise<T = unknown> extends PromiseLike<T> {
|
|
|
23
25
|
formData(): Promise<FormData>;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
type BaseOptions = Options<any, Record<string, any>, Record<string, any>>;
|
|
30
|
+
|
|
26
31
|
export interface TyFetch {
|
|
27
|
-
(url: string, options?:
|
|
28
|
-
get(url: string, options?:
|
|
29
|
-
post(url: string, options?:
|
|
30
|
-
put(url: string, options?:
|
|
31
|
-
patch(url: string, options?:
|
|
32
|
-
delete(url: string, options?:
|
|
33
|
-
head(url: string, options?:
|
|
34
|
-
create(defaults?:
|
|
35
|
-
extend(defaults?:
|
|
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>;
|
|
39
|
+
create(defaults?: BaseOptions): TyFetch;
|
|
40
|
+
extend(defaults?: BaseOptions): TyFetch;
|
|
36
41
|
HTTPError: typeof HTTPError;
|
|
37
42
|
}
|
|
38
43
|
|
package/dist/cli/index.js
CHANGED
|
@@ -39,6 +39,16 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const core_1 = require("../core");
|
|
40
40
|
async function main() {
|
|
41
41
|
const args = process.argv.slice(2);
|
|
42
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
43
|
+
console.log(`Usage: ty-fetch [tsconfig.json] [--verbose]
|
|
44
|
+
|
|
45
|
+
Validate API calls against OpenAPI specs.
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--verbose Show spec fetching details
|
|
49
|
+
--help, -h Show this help message`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
42
52
|
const tsconfigPath = args[0] ?? "tsconfig.json";
|
|
43
53
|
const configFile = ts.readConfigFile(path.resolve(tsconfigPath), ts.sys.readFile);
|
|
44
54
|
if (configFile.error) {
|
package/dist/core/spec-cache.js
CHANGED
|
@@ -32,17 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.specCache = exports.KNOWN_SPECS = void 0;
|
|
37
40
|
exports.registerSpecs = registerSpecs;
|
|
38
41
|
exports.ensureSpec = ensureSpec;
|
|
39
42
|
exports.ensureSpecSync = ensureSpecSync;
|
|
40
43
|
exports.fetchSpecForDomain = fetchSpecForDomain;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"petstore3.swagger.io": "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
44
|
-
"api.github.com": "https://api.apis.guru/v2/specs/github.com/api.github.com/1.1.4/openapi.json",
|
|
45
|
-
};
|
|
44
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
45
|
+
exports.KNOWN_SPECS = {};
|
|
46
46
|
exports.specCache = new Map();
|
|
47
47
|
/**
|
|
48
48
|
* Register user-provided spec mappings (from tsconfig plugin config).
|
|
@@ -73,26 +73,43 @@ function ensureSpec(domain, log, onLoaded) {
|
|
|
73
73
|
if (existing)
|
|
74
74
|
return existing;
|
|
75
75
|
const specUrl = exports.KNOWN_SPECS[domain];
|
|
76
|
-
if (!specUrl) {
|
|
77
|
-
const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
|
|
78
|
-
exports.specCache.set(domain, entry);
|
|
79
|
-
return entry;
|
|
80
|
-
}
|
|
81
76
|
const entry = { status: "loading", spec: null, fetchedAt: Date.now() };
|
|
82
77
|
exports.specCache.set(domain, entry);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
if (specUrl) {
|
|
79
|
+
log(`Fetching spec for ${domain} from ${specUrl}`);
|
|
80
|
+
fetchSpec(specUrl)
|
|
81
|
+
.then((spec) => {
|
|
82
|
+
entry.status = "loaded";
|
|
83
|
+
entry.spec = spec;
|
|
84
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
85
|
+
log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
86
|
+
onLoaded?.(domain, spec);
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
entry.status = "not-found";
|
|
90
|
+
log(`Failed to fetch spec for ${domain}: ${err}`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
log(`No spec configured for ${domain}, probing well-known URLs...`);
|
|
95
|
+
probeWellKnownSpecs(domain, log)
|
|
96
|
+
.then((spec) => {
|
|
97
|
+
if (spec) {
|
|
98
|
+
entry.status = "loaded";
|
|
99
|
+
entry.spec = spec;
|
|
100
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
101
|
+
log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
102
|
+
onLoaded?.(domain, spec);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
entry.status = "not-found";
|
|
106
|
+
log(`No spec found for ${domain} at well-known URLs`);
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.catch(() => {
|
|
110
|
+
entry.status = "not-found";
|
|
111
|
+
});
|
|
112
|
+
}
|
|
96
113
|
return entry;
|
|
97
114
|
}
|
|
98
115
|
/**
|
|
@@ -110,33 +127,86 @@ async function fetchSpecForDomain(domain, log) {
|
|
|
110
127
|
if (existing && existing.status === "loaded")
|
|
111
128
|
return existing;
|
|
112
129
|
const specUrl = exports.KNOWN_SPECS[domain];
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
if (specUrl) {
|
|
131
|
+
log(`Fetching spec for ${domain} from ${specUrl}`);
|
|
132
|
+
try {
|
|
133
|
+
const spec = await fetchSpec(specUrl);
|
|
134
|
+
const entry = { status: "loaded", spec, fetchedAt: Date.now() };
|
|
135
|
+
exports.specCache.set(domain, entry);
|
|
136
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
137
|
+
log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
138
|
+
return entry;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
|
|
142
|
+
exports.specCache.set(domain, entry);
|
|
143
|
+
log(`Failed to fetch spec for ${domain}: ${err}`);
|
|
144
|
+
return entry;
|
|
145
|
+
}
|
|
117
146
|
}
|
|
118
|
-
log(`
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
log(`No spec configured for ${domain}, probing well-known URLs...`);
|
|
148
|
+
const spec = await probeWellKnownSpecs(domain, log);
|
|
149
|
+
if (spec) {
|
|
121
150
|
const entry = { status: "loaded", spec, fetchedAt: Date.now() };
|
|
122
151
|
exports.specCache.set(domain, entry);
|
|
123
152
|
const pathCount = Object.keys(spec.paths || {}).length;
|
|
124
|
-
log(`Spec
|
|
153
|
+
log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
125
154
|
return entry;
|
|
126
155
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
|
|
157
|
+
exports.specCache.set(domain, entry);
|
|
158
|
+
log(`No spec found for ${domain} at well-known URLs`);
|
|
159
|
+
return entry;
|
|
160
|
+
}
|
|
161
|
+
const WELL_KNOWN_PATHS = [
|
|
162
|
+
"/.well-known/openapi.json",
|
|
163
|
+
"/.well-known/openapi.yaml",
|
|
164
|
+
"/.well-known/openapi.yml",
|
|
165
|
+
"/openapi.json",
|
|
166
|
+
"/openapi.yaml",
|
|
167
|
+
"/openapi.yml",
|
|
168
|
+
"/api/openapi.json",
|
|
169
|
+
"/api/openapi.yaml",
|
|
170
|
+
"/docs/openapi.json",
|
|
171
|
+
"/docs/openapi.yaml",
|
|
172
|
+
"/swagger.json",
|
|
173
|
+
"/api-docs/openapi.json",
|
|
174
|
+
];
|
|
175
|
+
async function probeWellKnownSpecs(domain, log) {
|
|
176
|
+
// Try HTTPS first, then HTTP for local/dev servers
|
|
177
|
+
const protocols = domain.startsWith("127.0.0.1") || domain.startsWith("localhost")
|
|
178
|
+
? ["http"] : ["https", "http"];
|
|
179
|
+
for (const proto of protocols) {
|
|
180
|
+
for (const path of WELL_KNOWN_PATHS) {
|
|
181
|
+
const url = `${proto}://${domain}${path}`;
|
|
182
|
+
try {
|
|
183
|
+
const spec = await fetchSpec(url);
|
|
184
|
+
if (spec?.openapi || spec?.swagger) {
|
|
185
|
+
log(`Found spec at ${url}`);
|
|
186
|
+
return spec;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// try next
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function parseSpec(data, source) {
|
|
197
|
+
const isYaml = /\.ya?ml$/i.test(source) ||
|
|
198
|
+
(!source.endsWith(".json") && !data.trimStart().startsWith("{"));
|
|
199
|
+
if (isYaml) {
|
|
200
|
+
return js_yaml_1.default.load(data);
|
|
132
201
|
}
|
|
202
|
+
return JSON.parse(data);
|
|
133
203
|
}
|
|
134
204
|
async function fetchSpec(urlOrPath) {
|
|
135
205
|
// Local file path — read from disk
|
|
136
206
|
if (!urlOrPath.startsWith("http://") && !urlOrPath.startsWith("https://")) {
|
|
137
207
|
const fs = require("fs");
|
|
138
208
|
const data = fs.readFileSync(urlOrPath, "utf-8");
|
|
139
|
-
return
|
|
209
|
+
return parseSpec(data, urlOrPath);
|
|
140
210
|
}
|
|
141
211
|
// Remote URL — fetch via HTTPS/HTTP
|
|
142
212
|
const mod = urlOrPath.startsWith("https://") ? await Promise.resolve().then(() => __importStar(require("https"))) : await Promise.resolve().then(() => __importStar(require("http")));
|
|
@@ -146,17 +216,21 @@ async function fetchSpec(urlOrPath) {
|
|
|
146
216
|
fetchSpec(res.headers.location).then(resolve, reject);
|
|
147
217
|
return;
|
|
148
218
|
}
|
|
219
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
220
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
149
223
|
let data = "";
|
|
150
224
|
res.on("data", (chunk) => (data += chunk));
|
|
151
225
|
res.on("end", () => {
|
|
152
226
|
try {
|
|
153
|
-
resolve(
|
|
227
|
+
resolve(parseSpec(data, urlOrPath));
|
|
154
228
|
}
|
|
155
|
-
catch {
|
|
156
|
-
reject(new Error(
|
|
229
|
+
catch (err) {
|
|
230
|
+
reject(new Error(`Failed to parse spec: ${err}`));
|
|
157
231
|
}
|
|
158
232
|
});
|
|
159
233
|
res.on("error", reject);
|
|
160
|
-
});
|
|
234
|
+
}).on("error", reject);
|
|
161
235
|
});
|
|
162
236
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export interface OpenAPISpec {
|
|
2
|
+
openapi?: string;
|
|
3
|
+
swagger?: string;
|
|
2
4
|
paths: Record<string, Record<string, any>>;
|
|
3
5
|
info?: {
|
|
4
6
|
title?: string;
|
|
@@ -10,6 +12,7 @@ export interface OpenAPISpec {
|
|
|
10
12
|
components?: {
|
|
11
13
|
schemas?: Record<string, any>;
|
|
12
14
|
};
|
|
15
|
+
security?: Array<Record<string, string[]>>;
|
|
13
16
|
}
|
|
14
17
|
export interface SpecEntry {
|
|
15
18
|
status: "loading" | "loaded" | "not-found";
|
package/dist/generate-types.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ interface OpenAPIOperation {
|
|
|
20
20
|
responses?: Record<string, {
|
|
21
21
|
content?: Record<string, {
|
|
22
22
|
schema?: OpenAPISchema;
|
|
23
|
+
example?: unknown;
|
|
23
24
|
}>;
|
|
24
25
|
description?: string;
|
|
25
26
|
}>;
|
|
@@ -42,7 +43,14 @@ interface FullOpenAPISpec {
|
|
|
42
43
|
paths: Record<string, Record<string, OpenAPIOperation>>;
|
|
43
44
|
components?: {
|
|
44
45
|
schemas?: Record<string, OpenAPISchema>;
|
|
46
|
+
securitySchemes?: Record<string, {
|
|
47
|
+
type?: string;
|
|
48
|
+
in?: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}>;
|
|
45
52
|
};
|
|
53
|
+
security?: Array<Record<string, string[]>>;
|
|
46
54
|
servers?: Array<{
|
|
47
55
|
url?: string;
|
|
48
56
|
}>;
|
package/dist/generate-types.js
CHANGED
|
@@ -81,15 +81,20 @@ function generateDtsContent(domainSpecs) {
|
|
|
81
81
|
if (!operation?.responses)
|
|
82
82
|
continue;
|
|
83
83
|
const successResp = operation.responses["200"] ?? operation.responses["201"];
|
|
84
|
-
|
|
84
|
+
const jsonContent = successResp?.content?.["application/json"];
|
|
85
|
+
if (!jsonContent?.schema && !jsonContent?.example)
|
|
85
86
|
continue;
|
|
86
|
-
const schema =
|
|
87
|
+
const schema = jsonContent.schema ?? inferSchemaFromExample(jsonContent.example);
|
|
87
88
|
const fullUrl = `${baseUrl}${basePath}${path}`;
|
|
88
89
|
const baseName = sanitizeTypeName(domain, path, method);
|
|
89
90
|
const count = typeNameCounter.get(baseName) ?? 0;
|
|
90
91
|
typeNameCounter.set(baseName, count + 1);
|
|
91
92
|
const typeName = count > 0 ? `${baseName}_${count}` : baseName;
|
|
92
93
|
const typeBody = schemaToType(schema, resolver, 1);
|
|
94
|
+
const opDesc = operation.summary ?? operation.description;
|
|
95
|
+
if (opDesc) {
|
|
96
|
+
typeDefinitions.push(` /** ${method.toUpperCase()} ${path} — ${opDesc.replace(/\*\//g, "* /")} */`);
|
|
97
|
+
}
|
|
93
98
|
typeDefinitions.push(` type ${typeName} = ${typeBody};`);
|
|
94
99
|
// ── Body type ──
|
|
95
100
|
const reqBodySchema = operation.requestBody?.content?.["application/json"]?.schema ??
|
|
@@ -124,26 +129,64 @@ function generateDtsContent(domainSpecs) {
|
|
|
124
129
|
: paramSchema?.type === "number" ? "number"
|
|
125
130
|
: paramSchema?.type === "boolean" ? "boolean"
|
|
126
131
|
: "string";
|
|
127
|
-
queryParams.push({ name: resolved.name, type: tsType, required: !!resolved.required });
|
|
132
|
+
queryParams.push({ name: resolved.name, type: tsType, required: !!resolved.required, description: resolved.description });
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
let queryParamsArg = "never";
|
|
131
136
|
if (queryParams.length > 0) {
|
|
132
137
|
const queryParamsTypeName = `${typeName}_QueryParams`;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
138
|
+
const queryPropLines = [];
|
|
139
|
+
for (const q of queryParams) {
|
|
140
|
+
if (q.description)
|
|
141
|
+
queryPropLines.push(`/** ${q.description.replace(/\*\//g, "* /")} */`);
|
|
142
|
+
queryPropLines.push(`${safePropName(q.name)}${q.required ? "" : "?"}: ${q.type};`);
|
|
143
|
+
}
|
|
144
|
+
typeDefinitions.push(` type ${queryParamsTypeName} = { ${queryPropLines.join(" ")} };`);
|
|
137
145
|
queryParamsArg = queryParamsTypeName;
|
|
138
146
|
}
|
|
139
|
-
|
|
147
|
+
// ── Headers type (from security schemes + header params) ──
|
|
148
|
+
const headerProps = [];
|
|
149
|
+
// Collect from header parameters
|
|
150
|
+
for (const param of allParams) {
|
|
151
|
+
const resolved = param.$ref ? resolveRef(spec, param.$ref) : param;
|
|
152
|
+
if (resolved?.in === "header") {
|
|
153
|
+
headerProps.push({ name: resolved.name, description: resolved.description, required: !!resolved.required });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Collect from security schemes
|
|
157
|
+
const opSecurity = operation.security ?? spec.security;
|
|
158
|
+
if (opSecurity && spec.components?.securitySchemes) {
|
|
159
|
+
for (const req of opSecurity) {
|
|
160
|
+
for (const schemeName of Object.keys(req)) {
|
|
161
|
+
const scheme = spec.components.securitySchemes[schemeName];
|
|
162
|
+
if (scheme?.type === "apiKey" && scheme.in === "header" && scheme.name) {
|
|
163
|
+
if (!headerProps.some((h) => h.name === scheme.name)) {
|
|
164
|
+
headerProps.push({ name: scheme.name, description: scheme.description, required: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let headersArg = "Record<string, string>";
|
|
171
|
+
if (headerProps.length > 0) {
|
|
172
|
+
const headersTypeName = `${typeName}_Headers`;
|
|
173
|
+
const headerPropLines = [];
|
|
174
|
+
for (const h of headerProps) {
|
|
175
|
+
if (h.description)
|
|
176
|
+
headerPropLines.push(`/** ${h.description.replace(/\*\//g, "* /")} */`);
|
|
177
|
+
headerPropLines.push(`${safePropName(h.name)}${h.required ? "" : "?"}: string;`);
|
|
178
|
+
}
|
|
179
|
+
typeDefinitions.push(` type ${headersTypeName} = { ${headerPropLines.join(" ")} };`);
|
|
180
|
+
headersArg = headersTypeName;
|
|
181
|
+
}
|
|
182
|
+
const optionsType = `Options<${bodyTypeArg}, ${pathParamsArg}, ${queryParamsArg}, ${headersArg}>`;
|
|
140
183
|
overloads.push(` ${method}(url: \`${escapeTemplateUrl(fullUrl)}\`, options?: ${optionsType}): ResponsePromise<${typeName}>;`);
|
|
141
184
|
// Don't break — emit an overload per HTTP method
|
|
142
185
|
}
|
|
143
186
|
}
|
|
144
187
|
}
|
|
145
188
|
lines.push("// Response types");
|
|
146
|
-
lines.push(...typeDefinitions.map((l) => l.replace(/^ /, "export ")));
|
|
189
|
+
lines.push(...typeDefinitions.map((l) => l.startsWith(" type ") ? l.replace(/^ /, "export ") : l.replace(/^ /, "")));
|
|
147
190
|
lines.push("");
|
|
148
191
|
lines.push("export interface TyFetch {");
|
|
149
192
|
lines.push(...overloads.map((l) => l.replace(/^ /, " ")));
|
|
@@ -157,9 +200,31 @@ function resolveRef(spec, ref) {
|
|
|
157
200
|
return undefined;
|
|
158
201
|
return spec.components?.schemas?.[match[1]];
|
|
159
202
|
}
|
|
203
|
+
/** Infer an OpenAPI schema from a JSON example value. */
|
|
204
|
+
function inferSchemaFromExample(example) {
|
|
205
|
+
if (example === null || example === undefined)
|
|
206
|
+
return { type: "string" };
|
|
207
|
+
if (typeof example === "string")
|
|
208
|
+
return { type: "string" };
|
|
209
|
+
if (typeof example === "number")
|
|
210
|
+
return { type: "number" };
|
|
211
|
+
if (typeof example === "boolean")
|
|
212
|
+
return { type: "boolean" };
|
|
213
|
+
if (Array.isArray(example)) {
|
|
214
|
+
return { type: "array", items: example.length > 0 ? inferSchemaFromExample(example[0]) : { type: "string" } };
|
|
215
|
+
}
|
|
216
|
+
if (typeof example === "object") {
|
|
217
|
+
const properties = {};
|
|
218
|
+
for (const [key, value] of Object.entries(example)) {
|
|
219
|
+
properties[key] = inferSchemaFromExample(value);
|
|
220
|
+
}
|
|
221
|
+
return { type: "object", properties };
|
|
222
|
+
}
|
|
223
|
+
return { type: "string" };
|
|
224
|
+
}
|
|
160
225
|
function schemaToType(schema, resolver, depth) {
|
|
161
226
|
// Depth limit to avoid huge nested types
|
|
162
|
-
if (depth >
|
|
227
|
+
if (depth > 4)
|
|
163
228
|
return "any";
|
|
164
229
|
const indent = " ".repeat(depth + 1);
|
|
165
230
|
const outerIndent = " ".repeat(depth);
|
|
@@ -195,6 +260,9 @@ function schemaToType(schema, resolver, depth) {
|
|
|
195
260
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
196
261
|
const optional = required.has(key) ? "" : "?";
|
|
197
262
|
const propType = schemaToType(propSchema, resolver, depth + 1);
|
|
263
|
+
if (propSchema.description) {
|
|
264
|
+
propLines.push(`${indent}/** ${propSchema.description.replace(/\*\//g, "* /")} */`);
|
|
265
|
+
}
|
|
198
266
|
propLines.push(`${indent}${safePropName(key)}${optional}: ${propType};`);
|
|
199
267
|
}
|
|
200
268
|
if (schema.additionalProperties) {
|
package/index.d.ts
CHANGED
|
@@ -6,12 +6,14 @@ export interface Options<
|
|
|
6
6
|
TBody = never,
|
|
7
7
|
TPathParams = never,
|
|
8
8
|
TQueryParams = never,
|
|
9
|
-
|
|
9
|
+
THeaders extends Record<string, string> = Record<string, string>,
|
|
10
|
+
> extends Omit<RequestInit, 'body' | 'headers'> {
|
|
10
11
|
body?: TBody;
|
|
11
12
|
params?: {
|
|
12
13
|
path?: TPathParams;
|
|
13
14
|
query?: TQueryParams;
|
|
14
15
|
};
|
|
16
|
+
headers?: THeaders & Record<string, string>;
|
|
15
17
|
prefixUrl?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -23,16 +25,19 @@ export interface ResponsePromise<T = unknown> extends PromiseLike<T> {
|
|
|
23
25
|
formData(): Promise<FormData>;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
type BaseOptions = Options<any, Record<string, any>, Record<string, any>>;
|
|
30
|
+
|
|
26
31
|
export interface TyFetch {
|
|
27
|
-
(url: string, options?:
|
|
28
|
-
get(url: string, options?:
|
|
29
|
-
post(url: string, options?:
|
|
30
|
-
put(url: string, options?:
|
|
31
|
-
patch(url: string, options?:
|
|
32
|
-
delete(url: string, options?:
|
|
33
|
-
head(url: string, options?:
|
|
34
|
-
create(defaults?:
|
|
35
|
-
extend(defaults?:
|
|
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>;
|
|
39
|
+
create(defaults?: BaseOptions): TyFetch;
|
|
40
|
+
extend(defaults?: BaseOptions): TyFetch;
|
|
36
41
|
HTTPError: typeof HTTPError;
|
|
37
42
|
}
|
|
38
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ty-fetch",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
3
|
+
"version": "0.0.2-beta.11",
|
|
4
|
+
"description": "Automatic TypeScript types for any REST API. No codegen, no manual types — just fetch.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"typescript",
|
|
8
|
+
"fetch",
|
|
9
|
+
"openapi",
|
|
10
|
+
"types",
|
|
11
|
+
"api",
|
|
12
|
+
"swagger",
|
|
13
|
+
"codegen-free"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/alnorris/ty-fetch.git"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
4
22
|
"main": "index.js",
|
|
5
23
|
"types": "index.d.ts",
|
|
6
24
|
"exports": {
|
|
@@ -22,16 +40,23 @@
|
|
|
22
40
|
"base.d.ts",
|
|
23
41
|
"index.js",
|
|
24
42
|
"index.d.ts",
|
|
25
|
-
"plugin.js"
|
|
43
|
+
"plugin.js",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
26
46
|
],
|
|
27
47
|
"scripts": {
|
|
28
|
-
"test": "
|
|
48
|
+
"test": "tsx --test test/*.test.ts",
|
|
29
49
|
"build": "tsc -p tsconfig.build.json",
|
|
30
50
|
"watch": "tsc -p tsconfig.build.json --watch",
|
|
31
51
|
"prepublishOnly": "cp base.d.ts index.d.ts && npm run build"
|
|
32
52
|
},
|
|
33
53
|
"devDependencies": {
|
|
54
|
+
"@types/js-yaml": "^4.0.9",
|
|
34
55
|
"@types/node": "^25.5.2",
|
|
56
|
+
"tsx": "^4.21.0",
|
|
35
57
|
"typescript": "^5.5.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"js-yaml": "^4.1.1"
|
|
36
61
|
}
|
|
37
62
|
}
|