create-swagger-client 0.1.1 → 0.1.3

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
@@ -11,6 +11,13 @@ A TypeScript tool that generates a fully type-safe REST API client from OpenAPI/
11
11
  - 🌐 **URL or File Input**: Generate from remote URLs or local files
12
12
  - 🎯 **Type Inference**: Automatic extraction of path params, query params, headers, and request/response types
13
13
 
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -g create-swagger-client
18
+ # or
19
+ npx create-swagger-client
20
+ ```
14
21
 
15
22
  ## Usage
16
23
 
@@ -180,24 +187,10 @@ try {
180
187
  }
181
188
  ```
182
189
 
183
- ## Development
184
-
185
- ### Build
186
-
187
- ```bash
188
- bun run build
189
- ```
190
-
191
- ### Type Check
192
-
193
- ```bash
194
- bun run typecheck
195
- ```
196
-
197
190
  ## Requirements
198
191
 
199
- - TypeScript 5.x
200
- - Node.js 16+ or Bun
192
+ - Node.js 16+ (for running the CLI)
193
+ - TypeScript 5.x (peer dependency for generated types)
201
194
 
202
195
  ## License
203
196
 
package/index.mjs ADDED
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ import openapiTS, { astToString } from "openapi-typescript";
3
+ import ora from "ora";
4
+ import { resolve } from "path";
5
+ import * as tsMorph from "ts-morph";
6
+
7
+ var args = process.argv.slice(2);
8
+ var source = args[0];
9
+ var outPut = args[1] || "swagger-client.ts";
10
+ if (!source) {
11
+ console.error("Please provide a source URL or file path.");
12
+ process.exit(1);
13
+ }
14
+ var isUrl = (str) => {
15
+ try {
16
+ new URL(str);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ };
22
+ async function generate() {
23
+ if (!source)
24
+ return;
25
+ if (isUrl(source) === false) {
26
+ source = resolve(process.cwd(), source);
27
+ }
28
+ const spinner = ora(`Generating API client from ${source}...`).start();
29
+ const ast = await openapiTS(source);
30
+ const contents = astToString(ast);
31
+ const project = new tsMorph.Project;
32
+ const sourceFile = project.createSourceFile(resolve(process.cwd(), outPut), contents, {
33
+ overwrite: true
34
+ });
35
+ sourceFile.addTypeAlias({
36
+ name: "RestMethod",
37
+ isExported: true,
38
+ type: '"get" | "post" | "put" | "delete" | "patch"'
39
+ });
40
+ sourceFile.addTypeAlias({
41
+ name: "KeyPaths",
42
+ isExported: true,
43
+ type: "keyof paths"
44
+ });
45
+ sourceFile.addTypeAlias({
46
+ name: "ExtractPathParams",
47
+ isExported: true,
48
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
49
+ type: "paths[T][K] extends { parameters: { path?: infer P } } ? P : never"
50
+ });
51
+ sourceFile.addTypeAlias({
52
+ name: "ExtractQueryParams",
53
+ isExported: true,
54
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
55
+ type: "paths[T][K] extends { parameters: { query?: infer Q } } ? Q : never"
56
+ });
57
+ sourceFile.addTypeAlias({
58
+ name: "ExtractHeaderParams",
59
+ isExported: true,
60
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
61
+ type: "paths[T][K] extends { parameters: { header?: infer H } } ? H : never"
62
+ });
63
+ sourceFile.addTypeAlias({
64
+ name: "ExtractBody",
65
+ isExported: true,
66
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
67
+ type: `paths[T][K] extends {
68
+ requestBody: { content: { "application/json": infer B } };
69
+ }
70
+ ? B
71
+ : never`
72
+ });
73
+ sourceFile.addTypeAlias({
74
+ name: "APIResponse",
75
+ isExported: true,
76
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
77
+ type: `paths[T][K] extends {
78
+ responses:
79
+ | { content: { "application/json": infer R } }
80
+ | { [code: number]: { content: { "application/json": infer R } } };
81
+ }
82
+ ? R
83
+ : unknown`
84
+ });
85
+ sourceFile.addTypeAlias({
86
+ name: "ApiPayload",
87
+ isExported: true,
88
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
89
+ type: `{
90
+ path?: ExtractPathParams<T, K>;
91
+ query?: ExtractQueryParams<T, K>;
92
+ body?: K extends "post" | "put" | "patch" ? ExtractBody<T, K> : never;
93
+ headers?: ExtractHeaderParams<T, K>;
94
+ }`
95
+ });
96
+ sourceFile.addTypeAlias({
97
+ name: "ApiClientType",
98
+ isExported: true,
99
+ type: `{
100
+ [K in RestMethod]: <T extends KeyPaths>(
101
+ path: T,
102
+ payload?: ApiPayload<T, K>,
103
+ ) => Promise<APIResponse<T, K>>;
104
+ }`
105
+ });
106
+ sourceFile.addTypeAlias({
107
+ name: "TypePaths",
108
+ typeParameters: ["T extends RestMethod"],
109
+ type: `{
110
+ [K in KeyPaths]: paths[K] extends { [M in T]: unknown } ? K : never;
111
+ }[KeyPaths]`
112
+ });
113
+ sourceFile.addClass({
114
+ name: "RestApiClient",
115
+ isExported: true,
116
+ ctors: [
117
+ {
118
+ parameters: [
119
+ { name: "basePath", type: "string", scope: tsMorph.Scope.Private },
120
+ {
121
+ name: "option",
122
+ type: "RequestInit",
123
+ hasQuestionToken: true,
124
+ scope: tsMorph.Scope.Private
125
+ }
126
+ ]
127
+ }
128
+ ],
129
+ methods: [
130
+ {
131
+ name: "fetcher",
132
+ scope: tsMorph.Scope.Public,
133
+ isAsync: true,
134
+ parameters: [
135
+ { name: "input", type: "RequestInfo" },
136
+ { name: "init", type: "RequestInit", hasQuestionToken: true }
137
+ ],
138
+ statements: `const headers = {
139
+ "Content-Type": "application/json",
140
+ ...init?.headers,
141
+ };
142
+
143
+ const response = await fetch(input, { ...init, headers });
144
+ if (!response.ok) {
145
+ const errorBody = await response.text();
146
+ throw new Error(
147
+ \`API request failed: \${response.status} \${response.statusText} - \${errorBody}\`,
148
+ );
149
+ }
150
+ return response.json();`
151
+ },
152
+ {
153
+ name: "request",
154
+ typeParameters: ["M extends RestMethod", "P extends TypePaths<M>"],
155
+ parameters: [
156
+ { name: "method", type: "M" },
157
+ { name: "path", type: "P" },
158
+ {
159
+ name: "init",
160
+ type: "ApiPayload<P, M>",
161
+ initializer: "{} as ApiPayload<P, M>"
162
+ }
163
+ ],
164
+ returnType: "Promise<APIResponse<P, M>>",
165
+ statements: `const url = new URL(this.basePath + String(path));
166
+
167
+ url.pathname = this.buildPathUrl(url.pathname, init.path);
168
+ this.appendQueryParams(url, init.query);
169
+
170
+ const requestInit: RequestInit = {
171
+ method: method.toUpperCase(),
172
+ ...this.option,
173
+ headers: {
174
+ ...(this.option?.headers ?? {}),
175
+ ...(init.headers ?? {}),
176
+ },
177
+ body: this.prepareBody(method, init.body),
178
+ };
179
+
180
+ return this.fetcher(url.toString(), requestInit) as Promise<
181
+ APIResponse<P, M>
182
+ >;`
183
+ },
184
+ {
185
+ name: "get",
186
+ scope: tsMorph.Scope.Public,
187
+ typeParameters: ['T extends TypePaths<"get">'],
188
+ parameters: [
189
+ { name: "path", type: "T" },
190
+ {
191
+ name: "payload",
192
+ type: 'ApiPayload<T, "get">',
193
+ hasQuestionToken: true
194
+ }
195
+ ],
196
+ returnType: 'Promise<APIResponse<T, "get">>',
197
+ statements: 'return this.request("get", path, payload);'
198
+ },
199
+ {
200
+ name: "post",
201
+ scope: tsMorph.Scope.Public,
202
+ typeParameters: ['T extends TypePaths<"post">'],
203
+ parameters: [
204
+ { name: "path", type: "T" },
205
+ {
206
+ name: "payload",
207
+ type: 'ApiPayload<T, "post">',
208
+ hasQuestionToken: true
209
+ }
210
+ ],
211
+ returnType: 'Promise<APIResponse<T, "post">>',
212
+ statements: 'return this.request("post", path, payload);'
213
+ },
214
+ {
215
+ name: "put",
216
+ scope: tsMorph.Scope.Public,
217
+ typeParameters: ['T extends TypePaths<"put">'],
218
+ parameters: [
219
+ { name: "path", type: "T" },
220
+ {
221
+ name: "payload",
222
+ type: 'ApiPayload<T, "put">',
223
+ hasQuestionToken: true
224
+ }
225
+ ],
226
+ returnType: 'Promise<APIResponse<T, "put">>',
227
+ statements: 'return this.request("put", path, payload);'
228
+ },
229
+ {
230
+ name: "delete",
231
+ scope: tsMorph.Scope.Public,
232
+ typeParameters: ['T extends TypePaths<"delete">'],
233
+ parameters: [
234
+ { name: "path", type: "T" },
235
+ {
236
+ name: "payload",
237
+ type: 'ApiPayload<T, "delete">',
238
+ hasQuestionToken: true
239
+ }
240
+ ],
241
+ returnType: 'Promise<APIResponse<T, "delete">>',
242
+ statements: 'return this.request("delete", path, payload);'
243
+ },
244
+ {
245
+ name: "patch",
246
+ scope: tsMorph.Scope.Public,
247
+ typeParameters: ['T extends TypePaths<"patch">'],
248
+ parameters: [
249
+ { name: "path", type: "T" },
250
+ {
251
+ name: "payload",
252
+ type: 'ApiPayload<T, "patch">',
253
+ hasQuestionToken: true
254
+ }
255
+ ],
256
+ returnType: 'Promise<APIResponse<T, "patch">>',
257
+ statements: 'return this.request("patch", path, payload);'
258
+ },
259
+ {
260
+ name: "buildPathUrl",
261
+ scope: tsMorph.Scope.Private,
262
+ parameters: [
263
+ { name: "basePath", type: "string" },
264
+ { name: "pathParams", type: "unknown", hasQuestionToken: true }
265
+ ],
266
+ returnType: "string",
267
+ statements: `let pathname = basePath;
268
+ if (pathParams != null) {
269
+ const params = pathParams as Record<string, unknown>;
270
+ pathname = decodeURIComponent(pathname).replace(/{(w+)}/g, (_, key) =>
271
+ encodeURIComponent(String(params[key])),
272
+ );
273
+ }
274
+ return pathname;`
275
+ },
276
+ {
277
+ name: "prepareBody",
278
+ scope: tsMorph.Scope.Private,
279
+ parameters: [
280
+ { name: "method", type: "RestMethod" },
281
+ { name: "body", type: "unknown", hasQuestionToken: true }
282
+ ],
283
+ returnType: "string | undefined",
284
+ statements: `if (body && ["post", "put", "patch"].includes(method)) {
285
+ return JSON.stringify(body);
286
+ }
287
+ return undefined;`
288
+ },
289
+ {
290
+ name: "appendQueryParams",
291
+ scope: tsMorph.Scope.Private,
292
+ parameters: [
293
+ { name: "url", type: "URL" },
294
+ { name: "queryParams", type: "unknown", hasQuestionToken: true }
295
+ ],
296
+ returnType: "void",
297
+ statements: `if (queryParams != null) {
298
+ const params = queryParams as Record<string, unknown>;
299
+ for (const [key, value] of Object.entries(params)) {
300
+ if (value !== undefined && value !== null) {
301
+ url.searchParams.append(key, String(value));
302
+ }
303
+ }
304
+ }`
305
+ }
306
+ ]
307
+ });
308
+ await sourceFile.formatText();
309
+ await project.save();
310
+ spinner.stopAndPersist({
311
+ symbol: "✔",
312
+ text: `API client generated at ${resolve(process.cwd(), outPut)}`
313
+ });
314
+ }
315
+ generate();
package/package.json CHANGED
@@ -1,21 +1,16 @@
1
1
  {
2
2
  "name": "create-swagger-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Generate fully type-safe REST API clients from OpenAPI/Swagger specifications",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.js",
5
+ "main": "./index.mjs",
6
+ "module": "./index.mjs",
7
7
  "type": "module",
8
8
  "bin": {
9
- "create-swagger-client": "./dist/index.js"
9
+ "create-swagger-client": "./index.mjs"
10
10
  },
11
11
  "files": [
12
- "dist"
12
+ "index.mjs"
13
13
  ],
14
- "scripts": {
15
- "build": "bun build index.ts --minify --outdir dist --target=node --external openapi-typescript --external ts-morph --external ora",
16
- "typecheck": "tsc --noEmit",
17
- "prepublishOnly": "bun run build"
18
- },
19
14
  "keywords": [
20
15
  "openapi",
21
16
  "swagger",
package/dist/index.js DELETED
@@ -1,69 +0,0 @@
1
- import y,{astToString as m}from"openapi-typescript";import h from"ora";import{resolve as r}from"path";import*as e from"ts-morph";var o=process.argv.slice(2),a=o[0],p=o[1]||"client-api.ts";if(!a)console.error("Please provide a source URL or file path."),process.exit(1);var u=(s)=>{try{return new URL(s),!0}catch{return!1}};async function c(){if(!a)return;if(u(a)===!1)a=r(process.cwd(),a);let s=h(`Generating API client from ${a}...`).start(),i=await y(a),d=m(i),n=new e.Project,t=n.createSourceFile(r(process.cwd(),p),d,{overwrite:!0});t.addTypeAlias({name:"RestMethod",isExported:!0,type:'"get" | "post" | "put" | "delete" | "patch"'}),t.addTypeAlias({name:"KeyPaths",isExported:!0,type:"keyof paths"}),t.addTypeAlias({name:"ExtractPathParams",isExported:!0,typeParameters:["T extends KeyPaths","K extends RestMethod"],type:"paths[T][K] extends { parameters: { path?: infer P } } ? P : never"}),t.addTypeAlias({name:"ExtractQueryParams",isExported:!0,typeParameters:["T extends KeyPaths","K extends RestMethod"],type:"paths[T][K] extends { parameters: { query?: infer Q } } ? Q : never"}),t.addTypeAlias({name:"ExtractHeaderParams",isExported:!0,typeParameters:["T extends KeyPaths","K extends RestMethod"],type:"paths[T][K] extends { parameters: { header?: infer H } } ? H : never"}),t.addTypeAlias({name:"ExtractBody",isExported:!0,typeParameters:["T extends KeyPaths","K extends RestMethod"],type:`paths[T][K] extends {
2
- requestBody: { content: { "application/json": infer B } };
3
- }
4
- ? B
5
- : never`}),t.addTypeAlias({name:"APIResponse",isExported:!0,typeParameters:["T extends KeyPaths","K extends RestMethod"],type:`paths[T][K] extends {
6
- responses:
7
- | { content: { "application/json": infer R } }
8
- | { [code: number]: { content: { "application/json": infer R } } };
9
- }
10
- ? R
11
- : unknown`}),t.addTypeAlias({name:"ApiPayload",isExported:!0,typeParameters:["T extends KeyPaths","K extends RestMethod"],type:`{
12
- path?: ExtractPathParams<T, K>;
13
- query?: ExtractQueryParams<T, K>;
14
- body?: K extends "post" | "put" | "patch" ? ExtractBody<T, K> : never;
15
- headers?: ExtractHeaderParams<T, K>;
16
- }`}),t.addTypeAlias({name:"ApiClientType",isExported:!0,type:`{
17
- [K in RestMethod]: <T extends KeyPaths>(
18
- path: T,
19
- payload?: ApiPayload<T, K>,
20
- ) => Promise<APIResponse<T, K>>;
21
- }`}),t.addTypeAlias({name:"TypePaths",typeParameters:["T extends RestMethod"],type:`{
22
- [K in KeyPaths]: paths[K] extends { [M in T]: unknown } ? K : never;
23
- }[KeyPaths]`}),t.addClass({name:"RestApiClient",isExported:!0,ctors:[{parameters:[{name:"basePath",type:"string",scope:e.Scope.Private},{name:"option",type:"RequestInit",hasQuestionToken:!0,scope:e.Scope.Private}]}],methods:[{name:"fetcher",scope:e.Scope.Public,isAsync:!0,parameters:[{name:"input",type:"RequestInfo"},{name:"init",type:"RequestInit",hasQuestionToken:!0}],statements:`const headers = {
24
- "Content-Type": "application/json",
25
- ...init?.headers,
26
- };
27
-
28
- const response = await fetch(input, { ...init, headers });
29
- if (!response.ok) {
30
- const errorBody = await response.text();
31
- throw new Error(
32
- \`API request failed: \${response.status} \${response.statusText} - \${errorBody}\`,
33
- );
34
- }
35
- return response.json();`},{name:"request",typeParameters:["M extends RestMethod","P extends TypePaths<M>"],parameters:[{name:"method",type:"M"},{name:"path",type:"P"},{name:"init",type:"ApiPayload<P, M>",initializer:"{} as ApiPayload<P, M>"}],returnType:"Promise<APIResponse<P, M>>",statements:`const url = new URL(this.basePath + String(path));
36
-
37
- url.pathname = this.buildPathUrl(url.pathname, init.path);
38
- this.appendQueryParams(url, init.query);
39
-
40
- const requestInit: RequestInit = {
41
- method: method.toUpperCase(),
42
- ...this.option,
43
- headers: {
44
- ...(this.option?.headers ?? {}),
45
- ...(init.headers ?? {}),
46
- },
47
- body: this.prepareBody(method, init.body),
48
- };
49
-
50
- return this.fetcher(url.toString(), requestInit) as Promise<
51
- APIResponse<P, M>
52
- >;`},{name:"get",scope:e.Scope.Public,typeParameters:['T extends TypePaths<"get">'],parameters:[{name:"path",type:"T"},{name:"payload",type:'ApiPayload<T, "get">',hasQuestionToken:!0}],returnType:'Promise<APIResponse<T, "get">>',statements:'return this.request("get", path, payload);'},{name:"post",scope:e.Scope.Public,typeParameters:['T extends TypePaths<"post">'],parameters:[{name:"path",type:"T"},{name:"payload",type:'ApiPayload<T, "post">',hasQuestionToken:!0}],returnType:'Promise<APIResponse<T, "post">>',statements:'return this.request("post", path, payload);'},{name:"put",scope:e.Scope.Public,typeParameters:['T extends TypePaths<"put">'],parameters:[{name:"path",type:"T"},{name:"payload",type:'ApiPayload<T, "put">',hasQuestionToken:!0}],returnType:'Promise<APIResponse<T, "put">>',statements:'return this.request("put", path, payload);'},{name:"delete",scope:e.Scope.Public,typeParameters:['T extends TypePaths<"delete">'],parameters:[{name:"path",type:"T"},{name:"payload",type:'ApiPayload<T, "delete">',hasQuestionToken:!0}],returnType:'Promise<APIResponse<T, "delete">>',statements:'return this.request("delete", path, payload);'},{name:"patch",scope:e.Scope.Public,typeParameters:['T extends TypePaths<"patch">'],parameters:[{name:"path",type:"T"},{name:"payload",type:'ApiPayload<T, "patch">',hasQuestionToken:!0}],returnType:'Promise<APIResponse<T, "patch">>',statements:'return this.request("patch", path, payload);'},{name:"buildPathUrl",scope:e.Scope.Private,parameters:[{name:"basePath",type:"string"},{name:"pathParams",type:"unknown",hasQuestionToken:!0}],returnType:"string",statements:`let pathname = basePath;
53
- if (pathParams != null) {
54
- const params = pathParams as Record<string, unknown>;
55
- pathname = decodeURIComponent(pathname).replace(/{(w+)}/g, (_, key) =>
56
- encodeURIComponent(String(params[key])),
57
- );
58
- }
59
- return pathname;`},{name:"prepareBody",scope:e.Scope.Private,parameters:[{name:"method",type:"RestMethod"},{name:"body",type:"unknown",hasQuestionToken:!0}],returnType:"string | undefined",statements:`if (body && ["post", "put", "patch"].includes(method)) {
60
- return JSON.stringify(body);
61
- }
62
- return undefined;`},{name:"appendQueryParams",scope:e.Scope.Private,parameters:[{name:"url",type:"URL"},{name:"queryParams",type:"unknown",hasQuestionToken:!0}],returnType:"void",statements:`if (queryParams != null) {
63
- const params = queryParams as Record<string, unknown>;
64
- for (const [key, value] of Object.entries(params)) {
65
- if (value !== undefined && value !== null) {
66
- url.searchParams.append(key, String(value));
67
- }
68
- }
69
- }`}]}),await t.formatText(),await n.save(),s.stopAndPersist({symbol:"✔",text:`API client generated at ${r(process.cwd(),p)}`})}c();