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 CHANGED
@@ -1,12 +1,13 @@
1
1
  # ty-fetch
2
2
 
3
- TypeScript tooling that validates API calls against OpenAPI specs. Get autocomplete, diagnostics, and fully typed responses with zero manual types.
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
- // customers is fully typed data, has_more, object, url all autocomplete
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
- ## What it does
18
+ ## How is this different?
18
19
 
19
- - **Path validation** — red squiggles for typos in API URLs, with "did you mean?" suggestions
20
- - **Typed responses** — response types generated from OpenAPI schemas, no manual `as` casts
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
- - **Path & query params** — typed `params.path` and `params.query` based on the endpoint
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 DX) and a **CLI** (CI validation).
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 github:alnorris/ty-fetch
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, make sure you're using the workspace TypeScript version (not the built-in one). Open the command palette and run **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
45
-
46
- ## Usage
58
+ In VS Code, use the workspace TypeScript version: command palette > **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
47
59
 
48
- ### The `ty-fetch` client
60
+ That's it. Start writing `tf.get("https://...")` and types appear automatically.
49
61
 
50
- A lightweight HTTP client (similar to [ky](https://github.com/sindresorhus/ky)) with typed methods:
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
- ### CLI
102
+ ## Spec configuration
85
103
 
86
- Run validation in CI or from the terminal:
104
+ ### Built-in specs (no config needed)
87
105
 
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
- ```
106
+ These APIs are typed out of the box:
93
107
 
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'?
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
- 2 error(s) found.
99
- ```
114
+ ### Auto-discovery
100
115
 
101
- ## Custom specs
116
+ For unknown domains, ty-fetch probes well-known paths automatically:
102
117
 
103
- Map domains to local files or URLs in your tsconfig plugin config:
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.json",
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
- - **File paths** are resolved relative to the tsconfig directory
122
- - **URLs** are fetched over HTTPS
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
- This works in both the editor plugin and the CLI.
154
+ Validate API calls in CI:
126
155
 
127
- ### Built-in specs
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
- These APIs are supported out of the box (no config needed):
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
- | Domain | API | Paths |
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. Plugin intercepts the TS language service (`getSemanticDiagnostics`, `getCompletionsAtPosition`, `getQuickInfoAtPosition`)
140
- 2. Finds `fetch()` / `tf.get()` / `tf.post()` etc. calls with string literal URLs
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` using interface declaration merging — only for URLs actually used in your code
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
- ## Architecture
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
@@ -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
- log(`Fetching spec for ${domain} from ${specUrl}`);
88
- fetchSpec(specUrl)
89
- .then((spec) => {
90
- entry.status = "loaded";
91
- entry.spec = spec;
92
- const pathCount = Object.keys(spec.paths || {}).length;
93
- log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
94
- onLoaded?.(domain, spec);
95
- })
96
- .catch((err) => {
97
- entry.status = "not-found";
98
- log(`Failed to fetch spec for ${domain}: ${err}`);
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 (!specUrl) {
118
- const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
119
- exports.specCache.set(domain, entry);
120
- return entry;
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(`Fetching spec for ${domain} from ${specUrl}`);
123
- try {
124
- const spec = await fetchSpec(specUrl);
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 loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
157
+ log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
129
158
  return entry;
130
159
  }
131
- catch (err) {
132
- const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
133
- exports.specCache.set(domain, entry);
134
- log(`Failed to fetch spec for ${domain}: ${err}`);
135
- return entry;
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) ||
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ty-fetch",
3
- "version": "0.0.2-beta.7",
3
+ "version": "0.0.2-beta.9",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "exports": {