ty-fetch 0.0.2-beta.0 → 0.0.2-beta.10
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 +115 -76
- 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 -1291
- package/package.json +27 -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,34 +1,66 @@
|
|
|
1
|
-
# ty-fetch
|
|
1
|
+
# ⚡ ty-fetch
|
|
2
2
|
|
|
3
|
-
TypeScript
|
|
3
|
+
**Automatic TypeScript types for any REST API. No codegen. No manual types. Just fetch.**
|
|
4
|
+
|
|
5
|
+
ty-fetch is a TypeScript language service plugin that reads OpenAPI specs and gives you fully typed API calls — response types, request bodies, query params, headers, path validation, and autocomplete — all without a single line of generated code.
|
|
4
6
|
|
|
5
7
|
```ts
|
|
6
8
|
import tf from "ty-fetch";
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
// ✅ Fully typed — response, query params, headers all inferred from the spec
|
|
11
|
+
const data = await tf.get("https://api.stripe.com/v1/customers", {
|
|
12
|
+
params: { query: { limit: 10 } },
|
|
13
|
+
}).json();
|
|
14
|
+
data.data // Customer[] — autocomplete just works
|
|
10
15
|
|
|
16
|
+
// ❌ Typo? Caught instantly with a suggestion
|
|
11
17
|
tf.get("https://api.stripe.com/v1/cutsomers");
|
|
12
18
|
// ~~~~~~~~~~
|
|
13
19
|
// Error: Path '/v1/cutsomers' does not exist in Stripe API.
|
|
14
20
|
// Did you mean '/v1/customers'?
|
|
15
21
|
```
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🤔 Why ty-fetch?
|
|
26
|
+
|
|
27
|
+
Every other OpenAPI tool makes you **run a codegen step**. You generate a client, import from generated files, and re-run the generator when the spec changes. It works, but it's friction.
|
|
28
|
+
|
|
29
|
+
ty-fetch takes a completely different approach:
|
|
30
|
+
|
|
31
|
+
| | Traditional codegen | ty-fetch |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| **Setup** | Install generator, run codegen, import client | `npm install ty-fetch` and go |
|
|
34
|
+
| **When spec changes** | Re-run generator, fix imports | Types update automatically |
|
|
35
|
+
| **What you write** | `client.customers.list()` | `tf.get("https://api.stripe.com/v1/customers")` |
|
|
36
|
+
| **Build step** | Required | None |
|
|
37
|
+
| **Generated files** | Committed to repo or `.gitignore`'d | None — types live in node_modules |
|
|
38
|
+
|
|
39
|
+
**You write real URLs. The types just appear.**
|
|
18
40
|
|
|
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
|
|
41
|
+
---
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
## 🚀 Features
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
- 🔮 **Zero codegen** — types generated on-the-fly by a TS plugin, not a build step
|
|
46
|
+
- 📦 **Typed responses** — `.json()` returns the actual response type from the spec
|
|
47
|
+
- ✏️ **Typed request bodies** — body params validated against the schema
|
|
48
|
+
- 🔗 **Typed path & query params** — `params.path` and `params.query` based on the endpoint
|
|
49
|
+
- 🔑 **Typed headers** — required headers (API keys, auth) from security schemes
|
|
50
|
+
- 🚨 **Path validation** — red squiggles for typos, with "did you mean?" suggestions
|
|
51
|
+
- 💡 **Autocomplete** — URL path completions inside string literals
|
|
52
|
+
- 📖 **JSDoc descriptions** — property descriptions from the spec in hover tooltips
|
|
53
|
+
- 🔍 **Auto-discovery** — probes well-known paths (`/openapi.json`, `/.well-known/openapi.yaml`) when no spec is configured
|
|
54
|
+
- 📄 **YAML + JSON** — specs can be either format, local files or remote URLs
|
|
55
|
+
- 🧠 **Example inference** — generates types from response `example` when `schema` is missing
|
|
56
|
+
- ⚡ **On-demand** — only fetches specs and generates types for APIs you actually call
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 📦 Setup
|
|
29
61
|
|
|
30
62
|
```bash
|
|
31
|
-
npm install
|
|
63
|
+
npm install ty-fetch
|
|
32
64
|
```
|
|
33
65
|
|
|
34
66
|
Add the plugin to your `tsconfig.json`:
|
|
@@ -41,66 +73,75 @@ Add the plugin to your `tsconfig.json`:
|
|
|
41
73
|
}
|
|
42
74
|
```
|
|
43
75
|
|
|
44
|
-
In VS Code,
|
|
76
|
+
In VS Code, use the workspace TypeScript version:
|
|
77
|
+
**Command Palette** → **TypeScript: Select TypeScript Version** → **Use Workspace Version**
|
|
45
78
|
|
|
46
|
-
|
|
79
|
+
That's it. Start writing `tf.get("https://...")` and types appear automatically. ✨
|
|
47
80
|
|
|
48
|
-
|
|
81
|
+
---
|
|
49
82
|
|
|
50
|
-
|
|
83
|
+
## 🔧 Usage
|
|
51
84
|
|
|
52
85
|
```ts
|
|
53
86
|
import tf from "ty-fetch";
|
|
54
87
|
|
|
55
|
-
// GET with typed response
|
|
88
|
+
// 📥 GET with typed response
|
|
56
89
|
const customers = await tf.get("https://api.stripe.com/v1/customers").json();
|
|
57
90
|
|
|
58
|
-
// POST with typed body
|
|
91
|
+
// 📤 POST with typed body
|
|
59
92
|
const customer = await tf.post("https://api.stripe.com/v1/customers", {
|
|
60
93
|
body: { name: "Jane Doe", email: "jane@example.com" },
|
|
61
94
|
}).json();
|
|
62
95
|
|
|
63
|
-
// Path params
|
|
96
|
+
// 🔗 Path params
|
|
64
97
|
const repo = await tf.get("https://api.github.com/repos/{owner}/{repo}", {
|
|
65
98
|
params: { path: { owner: "anthropics", repo: "claude-code" } },
|
|
66
99
|
}).json();
|
|
67
100
|
|
|
68
|
-
// Query params
|
|
69
|
-
const
|
|
70
|
-
params: { query: {
|
|
101
|
+
// 🔍 Query params
|
|
102
|
+
const results = await tf.get("https://hn.algolia.com/api/v1/search_by_date", {
|
|
103
|
+
params: { query: { query: "typescript", hitsPerPage: 10 } },
|
|
104
|
+
}).json();
|
|
105
|
+
|
|
106
|
+
// 🔑 Headers (typed from security schemes)
|
|
107
|
+
const data = await tf.get("https://api.example.com/v1/data", {
|
|
108
|
+
headers: { "x-api-key": process.env.API_KEY },
|
|
71
109
|
}).json();
|
|
72
110
|
```
|
|
73
111
|
|
|
74
|
-
Response methods
|
|
112
|
+
### Response methods
|
|
75
113
|
|
|
76
114
|
| Method | Returns |
|
|
77
115
|
|---|---|
|
|
78
|
-
| `.json()` | `Promise<T>`
|
|
116
|
+
| `.json()` | `Promise<T>` — typed from spec |
|
|
79
117
|
| `.text()` | `Promise<string>` |
|
|
80
118
|
| `.blob()` | `Promise<Blob>` |
|
|
81
119
|
| `.arrayBuffer()` | `Promise<ArrayBuffer>` |
|
|
82
|
-
| `await` directly | `T`
|
|
120
|
+
| `await` directly | `T` — same as `.json()` |
|
|
83
121
|
|
|
84
|
-
|
|
122
|
+
---
|
|
85
123
|
|
|
86
|
-
|
|
124
|
+
## 🔍 Spec discovery
|
|
87
125
|
|
|
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
|
-
```
|
|
126
|
+
### Auto-discovery (zero config)
|
|
93
127
|
|
|
94
|
-
|
|
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'?
|
|
128
|
+
When ty-fetch encounters an API domain it hasn't seen before, it automatically probes these well-known paths:
|
|
97
129
|
|
|
98
|
-
2 error(s) found.
|
|
99
130
|
```
|
|
131
|
+
/.well-known/openapi.json
|
|
132
|
+
/.well-known/openapi.yaml
|
|
133
|
+
/openapi.json
|
|
134
|
+
/openapi.yaml
|
|
135
|
+
/api/openapi.json
|
|
136
|
+
/docs/openapi.json
|
|
137
|
+
/swagger.json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If any return a valid OpenAPI spec → types are generated automatically. No config needed.
|
|
100
141
|
|
|
101
|
-
|
|
142
|
+
### Custom specs
|
|
102
143
|
|
|
103
|
-
Map domains to local files or URLs in your tsconfig
|
|
144
|
+
Map domains to local files or remote URLs in your tsconfig:
|
|
104
145
|
|
|
105
146
|
```jsonc
|
|
106
147
|
{
|
|
@@ -109,7 +150,7 @@ Map domains to local files or URLs in your tsconfig plugin config:
|
|
|
109
150
|
{
|
|
110
151
|
"name": "ty-fetch/plugin",
|
|
111
152
|
"specs": {
|
|
112
|
-
"api.internal.company.com": "./specs/internal-api.
|
|
153
|
+
"api.internal.company.com": "./specs/internal-api.yaml",
|
|
113
154
|
"api.partner.com": "https://partner.com/openapi.json"
|
|
114
155
|
}
|
|
115
156
|
}
|
|
@@ -118,55 +159,53 @@ Map domains to local files or URLs in your tsconfig plugin config:
|
|
|
118
159
|
}
|
|
119
160
|
```
|
|
120
161
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
162
|
+
- 📁 File paths resolved relative to tsconfig directory
|
|
163
|
+
- 🌐 URLs fetched over HTTPS
|
|
164
|
+
- 📄 JSON and YAML both supported
|
|
165
|
+
- Custom specs override auto-discovery for the same domain
|
|
166
|
+
- Works in both the editor plugin and the CLI
|
|
124
167
|
|
|
125
|
-
|
|
168
|
+
---
|
|
126
169
|
|
|
127
|
-
|
|
170
|
+
## 🖥️ CLI
|
|
128
171
|
|
|
129
|
-
|
|
172
|
+
Validate API calls in CI — no editor required:
|
|
130
173
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
```bash
|
|
175
|
+
npx ty-fetch # uses ./tsconfig.json
|
|
176
|
+
npx ty-fetch tsconfig.json # explicit path
|
|
177
|
+
npx ty-fetch --verbose # show spec fetching details
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
src/api.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API.
|
|
182
|
+
Did you mean '/v1/customers'?
|
|
136
183
|
|
|
137
|
-
|
|
184
|
+
1 error(s) found.
|
|
185
|
+
```
|
|
138
186
|
|
|
139
|
-
|
|
140
|
-
2. Finds `fetch()` / `tf.get()` / `tf.post()` etc. calls with string literal URLs
|
|
141
|
-
3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
|
|
142
|
-
4. Validates paths against the spec, suggests corrections via Levenshtein distance
|
|
143
|
-
5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts` using interface declaration merging — only for URLs actually used in your code
|
|
187
|
+
---
|
|
144
188
|
|
|
145
|
-
|
|
189
|
+
## ⚙️ How it works
|
|
146
190
|
|
|
147
|
-
|
|
191
|
+
1. 🔌 Plugin intercepts the TS language service
|
|
192
|
+
2. 🔎 Finds `tf.get()` / `tf.post()` / `fetch()` calls with string literal URLs
|
|
193
|
+
3. 📡 Extracts the domain, fetches the OpenAPI spec on-demand (cached after first fetch)
|
|
194
|
+
4. ✅ Validates paths, suggests corrections via Levenshtein distance
|
|
195
|
+
5. 🏗️ Generates typed overloads into `node_modules/ty-fetch/index.d.ts` via declaration merging — **only for URLs you actually use**
|
|
148
196
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
157
|
-
```
|
|
197
|
+
Types are generated **only for endpoints you call** — not the entire spec. A 500-path API might produce just 5 overloads if that's all you use. This keeps TypeScript fast.
|
|
198
|
+
|
|
199
|
+
---
|
|
158
200
|
|
|
159
|
-
## Development
|
|
201
|
+
## 🧪 Development
|
|
160
202
|
|
|
161
203
|
```bash
|
|
162
204
|
npm run build # compile TypeScript
|
|
163
205
|
npm run watch # compile in watch mode
|
|
164
|
-
npm test # run unit tests
|
|
206
|
+
npm test # run unit tests (74 tests)
|
|
165
207
|
```
|
|
166
208
|
|
|
167
|
-
|
|
209
|
+
## License
|
|
168
210
|
|
|
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
|
|
211
|
+
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
|
}>;
|