ty-fetch 0.0.2-beta.7 → 0.0.2-beta.9
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/README.md +75 -61
- package/dist/core/spec-cache.js +93 -31
- package/dist/core/types.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# ty-fetch
|
|
2
2
|
|
|
3
|
-
TypeScript
|
|
3
|
+
Automatic TypeScript types for any REST API. No codegen step, no manual types — just install, write fetch calls, and get full type safety from OpenAPI specs.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import tf from "ty-fetch";
|
|
7
7
|
|
|
8
|
+
// Fully typed response — no codegen, no manual types
|
|
8
9
|
const customers = await tf.get("https://api.stripe.com/v1/customers").json();
|
|
9
|
-
|
|
10
|
+
customers.data // Customer[] — autocomplete works
|
|
10
11
|
|
|
11
12
|
tf.get("https://api.stripe.com/v1/cutsomers");
|
|
12
13
|
// ~~~~~~~~~~
|
|
@@ -14,21 +15,34 @@ tf.get("https://api.stripe.com/v1/cutsomers");
|
|
|
14
15
|
// Did you mean '/v1/customers'?
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## How is this different?
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
Most OpenAPI tools require a **codegen step** — you run a command, it generates a client, you import from the generated code. When the spec changes, you regenerate.
|
|
21
|
+
|
|
22
|
+
ty-fetch has **no codegen**. The TypeScript language service plugin reads OpenAPI specs and generates typed overloads on-the-fly as you write code. Types appear instantly in your editor. You write plain fetch calls with real URLs.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Zero codegen** — types are generated on-the-fly by a TS plugin, not a build step
|
|
27
|
+
- **Typed responses** — `.json()` returns the actual response type from the spec
|
|
21
28
|
- **Typed request bodies** — body params validated against the spec
|
|
22
|
-
- **
|
|
29
|
+
- **Typed path & query params** — `params.path` and `params.query` based on the endpoint
|
|
30
|
+
- **Typed headers** — required headers (API keys, auth) from security schemes
|
|
31
|
+
- **Path validation** — red squiggles for typos, with "did you mean?" suggestions
|
|
23
32
|
- **Autocomplete** — URL path completions inside string literals, filtered by HTTP method
|
|
24
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
|
|
25
39
|
|
|
26
|
-
Works as both a **TS language service plugin** (editor
|
|
40
|
+
Works as both a **TS language service plugin** (editor) and a **CLI** (CI).
|
|
27
41
|
|
|
28
42
|
## Setup
|
|
29
43
|
|
|
30
44
|
```bash
|
|
31
|
-
npm install
|
|
45
|
+
npm install ty-fetch
|
|
32
46
|
```
|
|
33
47
|
|
|
34
48
|
Add the plugin to your `tsconfig.json`:
|
|
@@ -41,13 +55,11 @@ Add the plugin to your `tsconfig.json`:
|
|
|
41
55
|
}
|
|
42
56
|
```
|
|
43
57
|
|
|
44
|
-
In VS Code,
|
|
45
|
-
|
|
46
|
-
## Usage
|
|
58
|
+
In VS Code, use the workspace TypeScript version: command palette > **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
That's it. Start writing `tf.get("https://...")` and types appear automatically.
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
## Usage
|
|
51
63
|
|
|
52
64
|
```ts
|
|
53
65
|
import tf from "ty-fetch";
|
|
@@ -69,6 +81,12 @@ const repo = await tf.get("https://api.github.com/repos/{owner}/{repo}", {
|
|
|
69
81
|
const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
|
|
70
82
|
params: { query: { status: "available" } },
|
|
71
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();
|
|
72
90
|
```
|
|
73
91
|
|
|
74
92
|
Response methods:
|
|
@@ -81,26 +99,34 @@ Response methods:
|
|
|
81
99
|
| `.arrayBuffer()` | `Promise<ArrayBuffer>` |
|
|
82
100
|
| `await` directly | `T` (same as `.json()`) |
|
|
83
101
|
|
|
84
|
-
|
|
102
|
+
## Spec configuration
|
|
85
103
|
|
|
86
|
-
|
|
104
|
+
### Built-in specs (no config needed)
|
|
87
105
|
|
|
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
|
-
```
|
|
106
|
+
These APIs are typed out of the box:
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
| Domain | API |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `api.stripe.com` | Stripe API |
|
|
111
|
+
| `api.github.com` | GitHub REST API |
|
|
112
|
+
| `petstore3.swagger.io` | Swagger Petstore |
|
|
97
113
|
|
|
98
|
-
|
|
99
|
-
```
|
|
114
|
+
### Auto-discovery
|
|
100
115
|
|
|
101
|
-
|
|
116
|
+
For unknown domains, ty-fetch probes well-known paths automatically:
|
|
102
117
|
|
|
103
|
-
|
|
118
|
+
- `/.well-known/openapi.json`, `.yaml`, `.yml`
|
|
119
|
+
- `/openapi.json`, `.yaml`, `.yml`
|
|
120
|
+
- `/api/openapi.json`, `.yaml`
|
|
121
|
+
- `/docs/openapi.json`, `.yaml`
|
|
122
|
+
- `/swagger.json`
|
|
123
|
+
- `/api-docs/openapi.json`
|
|
124
|
+
|
|
125
|
+
If any of these return a valid OpenAPI spec, types are generated automatically.
|
|
126
|
+
|
|
127
|
+
### Custom specs
|
|
128
|
+
|
|
129
|
+
Map domains to local files or remote URLs in your tsconfig:
|
|
104
130
|
|
|
105
131
|
```jsonc
|
|
106
132
|
{
|
|
@@ -109,7 +135,7 @@ Map domains to local files or URLs in your tsconfig plugin config:
|
|
|
109
135
|
{
|
|
110
136
|
"name": "ty-fetch/plugin",
|
|
111
137
|
"specs": {
|
|
112
|
-
"api.internal.company.com": "./specs/internal-api.
|
|
138
|
+
"api.internal.company.com": "./specs/internal-api.yaml",
|
|
113
139
|
"api.partner.com": "https://partner.com/openapi.json"
|
|
114
140
|
}
|
|
115
141
|
}
|
|
@@ -118,55 +144,43 @@ Map domains to local files or URLs in your tsconfig plugin config:
|
|
|
118
144
|
}
|
|
119
145
|
```
|
|
120
146
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
147
|
+
- File paths are resolved relative to the tsconfig directory
|
|
148
|
+
- Supports JSON and YAML specs
|
|
123
149
|
- Custom specs override built-in defaults for the same domain
|
|
150
|
+
- Works in both the editor plugin and the CLI
|
|
151
|
+
|
|
152
|
+
## CLI
|
|
124
153
|
|
|
125
|
-
|
|
154
|
+
Validate API calls in CI:
|
|
126
155
|
|
|
127
|
-
|
|
156
|
+
```bash
|
|
157
|
+
npx ty-fetch # uses ./tsconfig.json
|
|
158
|
+
npx ty-fetch tsconfig.json # explicit path
|
|
159
|
+
npx ty-fetch --verbose # show spec fetching details
|
|
160
|
+
```
|
|
128
161
|
|
|
129
|
-
|
|
162
|
+
```
|
|
163
|
+
src/api.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API. Did you mean '/v1/customers'?
|
|
130
164
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| `api.stripe.com` | Stripe API | 414 |
|
|
134
|
-
| `petstore3.swagger.io` | Swagger Petstore | 13 |
|
|
135
|
-
| `api.github.com` | GitHub REST API | 551 |
|
|
165
|
+
1 error(s) found.
|
|
166
|
+
```
|
|
136
167
|
|
|
137
168
|
## How it works
|
|
138
169
|
|
|
139
|
-
1.
|
|
140
|
-
2. Finds `
|
|
170
|
+
1. The TS plugin intercepts the language service (`getSemanticDiagnostics`, `getCompletionsAtPosition`, `getQuickInfoAtPosition`)
|
|
171
|
+
2. Finds `tf.get()` / `tf.post()` / `fetch()` calls with string literal URLs
|
|
141
172
|
3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
|
|
142
173
|
4. Validates paths against the spec, suggests corrections via Levenshtein distance
|
|
143
|
-
5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts`
|
|
144
|
-
|
|
145
|
-
Spec fetching is async. On first encounter of a domain, the plugin fires a background fetch and returns no extra diagnostics. When the spec arrives, `refreshDiagnostics()` triggers the editor to re-check. This follows the same pattern as [graphqlsp](https://github.com/0no-co/graphqlsp).
|
|
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
|
|
146
175
|
|
|
147
|
-
|
|
176
|
+
Spec fetching is async. On first encounter, the plugin fires a background fetch and returns no extra diagnostics. When the spec arrives, `refreshDiagnostics()` re-checks the file. Same pattern as [graphqlsp](https://github.com/0no-co/graphqlsp).
|
|
148
177
|
|
|
149
|
-
|
|
150
|
-
src/
|
|
151
|
-
plugin/index.ts TS language service plugin (diagnostics, completions, hover)
|
|
152
|
-
cli/index.ts CLI entry point for CI validation
|
|
153
|
-
core/ Shared logic (URL parsing, spec cache, path matching, body validation)
|
|
154
|
-
generate-types.ts OpenAPI schema -> TypeScript type declarations
|
|
155
|
-
test-project/ Example project using the plugin
|
|
156
|
-
test/ Unit tests
|
|
157
|
-
```
|
|
178
|
+
Types are generated **only for endpoints you actually use** — not the entire spec. This keeps overload counts low and TypeScript fast.
|
|
158
179
|
|
|
159
180
|
## Development
|
|
160
181
|
|
|
161
182
|
```bash
|
|
162
183
|
npm run build # compile TypeScript
|
|
163
184
|
npm run watch # compile in watch mode
|
|
164
|
-
npm test # run unit tests
|
|
185
|
+
npm test # run unit tests (74 tests)
|
|
165
186
|
```
|
|
166
|
-
|
|
167
|
-
To test the editor experience:
|
|
168
|
-
|
|
169
|
-
1. Open `test-project/` in VS Code
|
|
170
|
-
2. Select the workspace TypeScript version
|
|
171
|
-
3. Restart the TS server (`TypeScript: Restart TS Server`)
|
|
172
|
-
4. Edit `test-project/example.ts` and observe diagnostics/completions
|
package/dist/core/spec-cache.js
CHANGED
|
@@ -77,26 +77,43 @@ function ensureSpec(domain, log, onLoaded) {
|
|
|
77
77
|
if (existing)
|
|
78
78
|
return existing;
|
|
79
79
|
const specUrl = exports.KNOWN_SPECS[domain];
|
|
80
|
-
if (!specUrl) {
|
|
81
|
-
const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
|
|
82
|
-
exports.specCache.set(domain, entry);
|
|
83
|
-
return entry;
|
|
84
|
-
}
|
|
85
80
|
const entry = { status: "loading", spec: null, fetchedAt: Date.now() };
|
|
86
81
|
exports.specCache.set(domain, entry);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
if (specUrl) {
|
|
83
|
+
log(`Fetching spec for ${domain} from ${specUrl}`);
|
|
84
|
+
fetchSpec(specUrl)
|
|
85
|
+
.then((spec) => {
|
|
86
|
+
entry.status = "loaded";
|
|
87
|
+
entry.spec = spec;
|
|
88
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
89
|
+
log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
90
|
+
onLoaded?.(domain, spec);
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
entry.status = "not-found";
|
|
94
|
+
log(`Failed to fetch spec for ${domain}: ${err}`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
log(`No spec configured for ${domain}, probing well-known URLs...`);
|
|
99
|
+
probeWellKnownSpecs(domain, log)
|
|
100
|
+
.then((spec) => {
|
|
101
|
+
if (spec) {
|
|
102
|
+
entry.status = "loaded";
|
|
103
|
+
entry.spec = spec;
|
|
104
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
105
|
+
log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
106
|
+
onLoaded?.(domain, spec);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
entry.status = "not-found";
|
|
110
|
+
log(`No spec found for ${domain} at well-known URLs`);
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {
|
|
114
|
+
entry.status = "not-found";
|
|
115
|
+
});
|
|
116
|
+
}
|
|
100
117
|
return entry;
|
|
101
118
|
}
|
|
102
119
|
/**
|
|
@@ -114,26 +131,71 @@ async function fetchSpecForDomain(domain, log) {
|
|
|
114
131
|
if (existing && existing.status === "loaded")
|
|
115
132
|
return existing;
|
|
116
133
|
const specUrl = exports.KNOWN_SPECS[domain];
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
if (specUrl) {
|
|
135
|
+
log(`Fetching spec for ${domain} from ${specUrl}`);
|
|
136
|
+
try {
|
|
137
|
+
const spec = await fetchSpec(specUrl);
|
|
138
|
+
const entry = { status: "loaded", spec, fetchedAt: Date.now() };
|
|
139
|
+
exports.specCache.set(domain, entry);
|
|
140
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
141
|
+
log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
142
|
+
return entry;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
|
|
146
|
+
exports.specCache.set(domain, entry);
|
|
147
|
+
log(`Failed to fetch spec for ${domain}: ${err}`);
|
|
148
|
+
return entry;
|
|
149
|
+
}
|
|
121
150
|
}
|
|
122
|
-
log(`
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
log(`No spec configured for ${domain}, probing well-known URLs...`);
|
|
152
|
+
const spec = await probeWellKnownSpecs(domain, log);
|
|
153
|
+
if (spec) {
|
|
125
154
|
const entry = { status: "loaded", spec, fetchedAt: Date.now() };
|
|
126
155
|
exports.specCache.set(domain, entry);
|
|
127
156
|
const pathCount = Object.keys(spec.paths || {}).length;
|
|
128
|
-
log(`Spec
|
|
157
|
+
log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
|
|
129
158
|
return entry;
|
|
130
159
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
|
|
161
|
+
exports.specCache.set(domain, entry);
|
|
162
|
+
log(`No spec found for ${domain} at well-known URLs`);
|
|
163
|
+
return entry;
|
|
164
|
+
}
|
|
165
|
+
const WELL_KNOWN_PATHS = [
|
|
166
|
+
"/.well-known/openapi.json",
|
|
167
|
+
"/.well-known/openapi.yaml",
|
|
168
|
+
"/.well-known/openapi.yml",
|
|
169
|
+
"/openapi.json",
|
|
170
|
+
"/openapi.yaml",
|
|
171
|
+
"/openapi.yml",
|
|
172
|
+
"/api/openapi.json",
|
|
173
|
+
"/api/openapi.yaml",
|
|
174
|
+
"/docs/openapi.json",
|
|
175
|
+
"/docs/openapi.yaml",
|
|
176
|
+
"/swagger.json",
|
|
177
|
+
"/api-docs/openapi.json",
|
|
178
|
+
];
|
|
179
|
+
async function probeWellKnownSpecs(domain, log) {
|
|
180
|
+
// Try HTTPS first, then HTTP for local/dev servers
|
|
181
|
+
const protocols = domain.startsWith("127.0.0.1") || domain.startsWith("localhost")
|
|
182
|
+
? ["http"] : ["https", "http"];
|
|
183
|
+
for (const proto of protocols) {
|
|
184
|
+
for (const path of WELL_KNOWN_PATHS) {
|
|
185
|
+
const url = `${proto}://${domain}${path}`;
|
|
186
|
+
try {
|
|
187
|
+
const spec = await fetchSpec(url);
|
|
188
|
+
if (spec?.openapi || spec?.swagger) {
|
|
189
|
+
log(`Found spec at ${url}`);
|
|
190
|
+
return spec;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// try next
|
|
195
|
+
}
|
|
196
|
+
}
|
|
136
197
|
}
|
|
198
|
+
return null;
|
|
137
199
|
}
|
|
138
200
|
function parseSpec(data, source) {
|
|
139
201
|
const isYaml = /\.ya?ml$/i.test(source) ||
|
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";
|