create-swagger-client 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/index.mjs +315 -0
  2. package/package.json +6 -10
  3. package/dist/index.js +0 -631
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] || "client-api.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.0",
3
+ "version": "0.1.2",
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 --outdir dist --external openapi-typescript --external ts-morph",
16
- "typecheck": "tsc --noEmit",
17
- "prepublishOnly": "bun run build"
18
- },
19
14
  "keywords": [
20
15
  "openapi",
21
16
  "swagger",
@@ -48,6 +43,7 @@
48
43
  },
49
44
  "dependencies": {
50
45
  "openapi-typescript": "^7.10.1",
46
+ "ora": "^9.1.0",
51
47
  "ts-morph": "^27.0.2"
52
48
  }
53
49
  }
package/dist/index.js DELETED
@@ -1,631 +0,0 @@
1
- // index.ts
2
- import openapiTS, { astToString } from "openapi-typescript";
3
-
4
- // node:path
5
- function assertPath(path) {
6
- if (typeof path !== "string")
7
- throw TypeError("Path must be a string. Received " + JSON.stringify(path));
8
- }
9
- function normalizeStringPosix(path, allowAboveRoot) {
10
- var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
11
- for (var i = 0;i <= path.length; ++i) {
12
- if (i < path.length)
13
- code = path.charCodeAt(i);
14
- else if (code === 47)
15
- break;
16
- else
17
- code = 47;
18
- if (code === 47) {
19
- if (lastSlash === i - 1 || dots === 1)
20
- ;
21
- else if (lastSlash !== i - 1 && dots === 2) {
22
- if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
23
- if (res.length > 2) {
24
- var lastSlashIndex = res.lastIndexOf("/");
25
- if (lastSlashIndex !== res.length - 1) {
26
- if (lastSlashIndex === -1)
27
- res = "", lastSegmentLength = 0;
28
- else
29
- res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
30
- lastSlash = i, dots = 0;
31
- continue;
32
- }
33
- } else if (res.length === 2 || res.length === 1) {
34
- res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
35
- continue;
36
- }
37
- }
38
- if (allowAboveRoot) {
39
- if (res.length > 0)
40
- res += "/..";
41
- else
42
- res = "..";
43
- lastSegmentLength = 2;
44
- }
45
- } else {
46
- if (res.length > 0)
47
- res += "/" + path.slice(lastSlash + 1, i);
48
- else
49
- res = path.slice(lastSlash + 1, i);
50
- lastSegmentLength = i - lastSlash - 1;
51
- }
52
- lastSlash = i, dots = 0;
53
- } else if (code === 46 && dots !== -1)
54
- ++dots;
55
- else
56
- dots = -1;
57
- }
58
- return res;
59
- }
60
- function _format(sep, pathObject) {
61
- var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
62
- if (!dir)
63
- return base;
64
- if (dir === pathObject.root)
65
- return dir + base;
66
- return dir + sep + base;
67
- }
68
- function resolve() {
69
- var resolvedPath = "", resolvedAbsolute = false, cwd;
70
- for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
71
- var path;
72
- if (i >= 0)
73
- path = arguments[i];
74
- else {
75
- if (cwd === undefined)
76
- cwd = process.cwd();
77
- path = cwd;
78
- }
79
- if (assertPath(path), path.length === 0)
80
- continue;
81
- resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
82
- }
83
- if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
84
- if (resolvedPath.length > 0)
85
- return "/" + resolvedPath;
86
- else
87
- return "/";
88
- else if (resolvedPath.length > 0)
89
- return resolvedPath;
90
- else
91
- return ".";
92
- }
93
- function normalize(path) {
94
- if (assertPath(path), path.length === 0)
95
- return ".";
96
- var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
97
- if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
98
- path = ".";
99
- if (path.length > 0 && trailingSeparator)
100
- path += "/";
101
- if (isAbsolute)
102
- return "/" + path;
103
- return path;
104
- }
105
- function isAbsolute(path) {
106
- return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
107
- }
108
- function join() {
109
- if (arguments.length === 0)
110
- return ".";
111
- var joined;
112
- for (var i = 0;i < arguments.length; ++i) {
113
- var arg = arguments[i];
114
- if (assertPath(arg), arg.length > 0)
115
- if (joined === undefined)
116
- joined = arg;
117
- else
118
- joined += "/" + arg;
119
- }
120
- if (joined === undefined)
121
- return ".";
122
- return normalize(joined);
123
- }
124
- function relative(from, to) {
125
- if (assertPath(from), assertPath(to), from === to)
126
- return "";
127
- if (from = resolve(from), to = resolve(to), from === to)
128
- return "";
129
- var fromStart = 1;
130
- for (;fromStart < from.length; ++fromStart)
131
- if (from.charCodeAt(fromStart) !== 47)
132
- break;
133
- var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
134
- for (;toStart < to.length; ++toStart)
135
- if (to.charCodeAt(toStart) !== 47)
136
- break;
137
- var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
138
- for (;i <= length; ++i) {
139
- if (i === length) {
140
- if (toLen > length) {
141
- if (to.charCodeAt(toStart + i) === 47)
142
- return to.slice(toStart + i + 1);
143
- else if (i === 0)
144
- return to.slice(toStart + i);
145
- } else if (fromLen > length) {
146
- if (from.charCodeAt(fromStart + i) === 47)
147
- lastCommonSep = i;
148
- else if (i === 0)
149
- lastCommonSep = 0;
150
- }
151
- break;
152
- }
153
- var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
154
- if (fromCode !== toCode)
155
- break;
156
- else if (fromCode === 47)
157
- lastCommonSep = i;
158
- }
159
- var out = "";
160
- for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
161
- if (i === fromEnd || from.charCodeAt(i) === 47)
162
- if (out.length === 0)
163
- out += "..";
164
- else
165
- out += "/..";
166
- if (out.length > 0)
167
- return out + to.slice(toStart + lastCommonSep);
168
- else {
169
- if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
170
- ++toStart;
171
- return to.slice(toStart);
172
- }
173
- }
174
- function _makeLong(path) {
175
- return path;
176
- }
177
- function dirname(path) {
178
- if (assertPath(path), path.length === 0)
179
- return ".";
180
- var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
181
- for (var i = path.length - 1;i >= 1; --i)
182
- if (code = path.charCodeAt(i), code === 47) {
183
- if (!matchedSlash) {
184
- end = i;
185
- break;
186
- }
187
- } else
188
- matchedSlash = false;
189
- if (end === -1)
190
- return hasRoot ? "/" : ".";
191
- if (hasRoot && end === 1)
192
- return "//";
193
- return path.slice(0, end);
194
- }
195
- function basename(path, ext) {
196
- if (ext !== undefined && typeof ext !== "string")
197
- throw TypeError('"ext" argument must be a string');
198
- assertPath(path);
199
- var start = 0, end = -1, matchedSlash = true, i;
200
- if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
201
- if (ext.length === path.length && ext === path)
202
- return "";
203
- var extIdx = ext.length - 1, firstNonSlashEnd = -1;
204
- for (i = path.length - 1;i >= 0; --i) {
205
- var code = path.charCodeAt(i);
206
- if (code === 47) {
207
- if (!matchedSlash) {
208
- start = i + 1;
209
- break;
210
- }
211
- } else {
212
- if (firstNonSlashEnd === -1)
213
- matchedSlash = false, firstNonSlashEnd = i + 1;
214
- if (extIdx >= 0)
215
- if (code === ext.charCodeAt(extIdx)) {
216
- if (--extIdx === -1)
217
- end = i;
218
- } else
219
- extIdx = -1, end = firstNonSlashEnd;
220
- }
221
- }
222
- if (start === end)
223
- end = firstNonSlashEnd;
224
- else if (end === -1)
225
- end = path.length;
226
- return path.slice(start, end);
227
- } else {
228
- for (i = path.length - 1;i >= 0; --i)
229
- if (path.charCodeAt(i) === 47) {
230
- if (!matchedSlash) {
231
- start = i + 1;
232
- break;
233
- }
234
- } else if (end === -1)
235
- matchedSlash = false, end = i + 1;
236
- if (end === -1)
237
- return "";
238
- return path.slice(start, end);
239
- }
240
- }
241
- function extname(path) {
242
- assertPath(path);
243
- var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
244
- for (var i = path.length - 1;i >= 0; --i) {
245
- var code = path.charCodeAt(i);
246
- if (code === 47) {
247
- if (!matchedSlash) {
248
- startPart = i + 1;
249
- break;
250
- }
251
- continue;
252
- }
253
- if (end === -1)
254
- matchedSlash = false, end = i + 1;
255
- if (code === 46) {
256
- if (startDot === -1)
257
- startDot = i;
258
- else if (preDotState !== 1)
259
- preDotState = 1;
260
- } else if (startDot !== -1)
261
- preDotState = -1;
262
- }
263
- if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
264
- return "";
265
- return path.slice(startDot, end);
266
- }
267
- function format(pathObject) {
268
- if (pathObject === null || typeof pathObject !== "object")
269
- throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
270
- return _format("/", pathObject);
271
- }
272
- function parse(path) {
273
- assertPath(path);
274
- var ret = { root: "", dir: "", base: "", ext: "", name: "" };
275
- if (path.length === 0)
276
- return ret;
277
- var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
278
- if (isAbsolute2)
279
- ret.root = "/", start = 1;
280
- else
281
- start = 0;
282
- var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
283
- for (;i >= start; --i) {
284
- if (code = path.charCodeAt(i), code === 47) {
285
- if (!matchedSlash) {
286
- startPart = i + 1;
287
- break;
288
- }
289
- continue;
290
- }
291
- if (end === -1)
292
- matchedSlash = false, end = i + 1;
293
- if (code === 46) {
294
- if (startDot === -1)
295
- startDot = i;
296
- else if (preDotState !== 1)
297
- preDotState = 1;
298
- } else if (startDot !== -1)
299
- preDotState = -1;
300
- }
301
- if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
302
- if (end !== -1)
303
- if (startPart === 0 && isAbsolute2)
304
- ret.base = ret.name = path.slice(1, end);
305
- else
306
- ret.base = ret.name = path.slice(startPart, end);
307
- } else {
308
- if (startPart === 0 && isAbsolute2)
309
- ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
310
- else
311
- ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
312
- ret.ext = path.slice(startDot, end);
313
- }
314
- if (startPart > 0)
315
- ret.dir = path.slice(0, startPart - 1);
316
- else if (isAbsolute2)
317
- ret.dir = "/";
318
- return ret;
319
- }
320
- var sep = "/";
321
- var delimiter = ":";
322
- var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
323
-
324
- // index.ts
325
- import * as tsMorph from "ts-morph";
326
- var args = process.argv.slice(2);
327
- var source = args[0];
328
- var outPut = args[1] || "client-api.ts";
329
- if (!source) {
330
- console.error("Please provide a source URL or file path.");
331
- process.exit(1);
332
- }
333
- var isUrl = (str) => {
334
- try {
335
- new URL(str);
336
- return true;
337
- } catch {
338
- return false;
339
- }
340
- };
341
- async function generate() {
342
- if (!source)
343
- return;
344
- if (isUrl(source) === false) {
345
- source = resolve(process.cwd(), source);
346
- }
347
- console.log(`Generating API client from ${source}...`);
348
- const ast = await openapiTS(source);
349
- const contents = astToString(ast);
350
- const project = new tsMorph.Project;
351
- const sourceFile = project.createSourceFile(resolve(process.cwd(), outPut), contents, {
352
- overwrite: true
353
- });
354
- sourceFile.addTypeAlias({
355
- name: "RestMethod",
356
- isExported: true,
357
- type: '"get" | "post" | "put" | "delete" | "patch"'
358
- });
359
- sourceFile.addTypeAlias({
360
- name: "KeyPaths",
361
- isExported: true,
362
- type: "keyof paths"
363
- });
364
- sourceFile.addTypeAlias({
365
- name: "ExtractPathParams",
366
- isExported: true,
367
- typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
368
- type: "paths[T][K] extends { parameters: { path?: infer P } } ? P : never"
369
- });
370
- sourceFile.addTypeAlias({
371
- name: "ExtractQueryParams",
372
- isExported: true,
373
- typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
374
- type: "paths[T][K] extends { parameters: { query?: infer Q } } ? Q : never"
375
- });
376
- sourceFile.addTypeAlias({
377
- name: "ExtractHeaderParams",
378
- isExported: true,
379
- typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
380
- type: "paths[T][K] extends { parameters: { header?: infer H } } ? H : never"
381
- });
382
- sourceFile.addTypeAlias({
383
- name: "ExtractBody",
384
- isExported: true,
385
- typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
386
- type: `paths[T][K] extends {
387
- requestBody: { content: { "application/json": infer B } };
388
- }
389
- ? B
390
- : never`
391
- });
392
- sourceFile.addTypeAlias({
393
- name: "APIResponse",
394
- isExported: true,
395
- typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
396
- type: `paths[T][K] extends {
397
- responses:
398
- | { content: { "application/json": infer R } }
399
- | { [code: number]: { content: { "application/json": infer R } } };
400
- }
401
- ? R
402
- : unknown`
403
- });
404
- sourceFile.addTypeAlias({
405
- name: "ApiPayload",
406
- isExported: true,
407
- typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
408
- type: `{
409
- path?: ExtractPathParams<T, K>;
410
- query?: ExtractQueryParams<T, K>;
411
- body?: K extends "post" | "put" | "patch" ? ExtractBody<T, K> : never;
412
- headers?: ExtractHeaderParams<T, K>;
413
- }`
414
- });
415
- sourceFile.addTypeAlias({
416
- name: "ApiClientType",
417
- isExported: true,
418
- type: `{
419
- [K in RestMethod]: <T extends KeyPaths>(
420
- path: T,
421
- payload?: ApiPayload<T, K>,
422
- ) => Promise<APIResponse<T, K>>;
423
- }`
424
- });
425
- sourceFile.addTypeAlias({
426
- name: "TypePaths",
427
- typeParameters: ["T extends RestMethod"],
428
- type: `{
429
- [K in KeyPaths]: paths[K] extends { [M in T]: unknown } ? K : never;
430
- }[KeyPaths]`
431
- });
432
- sourceFile.addClass({
433
- name: "RestApiClient",
434
- isExported: true,
435
- ctors: [
436
- {
437
- parameters: [
438
- { name: "basePath", type: "string", scope: tsMorph.Scope.Private },
439
- {
440
- name: "option",
441
- type: "RequestInit",
442
- hasQuestionToken: true,
443
- scope: tsMorph.Scope.Private
444
- }
445
- ]
446
- }
447
- ],
448
- methods: [
449
- {
450
- name: "fetcher",
451
- scope: tsMorph.Scope.Public,
452
- isAsync: true,
453
- parameters: [
454
- { name: "input", type: "RequestInfo" },
455
- { name: "init", type: "RequestInit", hasQuestionToken: true }
456
- ],
457
- statements: `const headers = {
458
- "Content-Type": "application/json",
459
- ...init?.headers,
460
- };
461
-
462
- const response = await fetch(input, { ...init, headers });
463
- if (!response.ok) {
464
- const errorBody = await response.text();
465
- throw new Error(
466
- \`API request failed: \${response.status} \${response.statusText} - \${errorBody}\`,
467
- );
468
- }
469
- return response.json();`
470
- },
471
- {
472
- name: "request",
473
- typeParameters: ["M extends RestMethod", "P extends TypePaths<M>"],
474
- parameters: [
475
- { name: "method", type: "M" },
476
- { name: "path", type: "P" },
477
- {
478
- name: "init",
479
- type: "ApiPayload<P, M>",
480
- initializer: "{} as ApiPayload<P, M>"
481
- }
482
- ],
483
- returnType: "Promise<APIResponse<P, M>>",
484
- statements: `const url = new URL(this.basePath + String(path));
485
-
486
- url.pathname = this.buildPathUrl(url.pathname, init.path);
487
- this.appendQueryParams(url, init.query);
488
-
489
- const requestInit: RequestInit = {
490
- method: method.toUpperCase(),
491
- ...this.option,
492
- headers: {
493
- ...(this.option?.headers ?? {}),
494
- ...(init.headers ?? {}),
495
- },
496
- body: this.prepareBody(method, init.body),
497
- };
498
-
499
- return this.fetcher(url.toString(), requestInit) as Promise<
500
- APIResponse<P, M>
501
- >;`
502
- },
503
- {
504
- name: "get",
505
- scope: tsMorph.Scope.Public,
506
- typeParameters: ['T extends TypePaths<"get">'],
507
- parameters: [
508
- { name: "path", type: "T" },
509
- {
510
- name: "payload",
511
- type: 'ApiPayload<T, "get">',
512
- hasQuestionToken: true
513
- }
514
- ],
515
- returnType: 'Promise<APIResponse<T, "get">>',
516
- statements: 'return this.request("get", path, payload);'
517
- },
518
- {
519
- name: "post",
520
- scope: tsMorph.Scope.Public,
521
- typeParameters: ['T extends TypePaths<"post">'],
522
- parameters: [
523
- { name: "path", type: "T" },
524
- {
525
- name: "payload",
526
- type: 'ApiPayload<T, "post">',
527
- hasQuestionToken: true
528
- }
529
- ],
530
- returnType: 'Promise<APIResponse<T, "post">>',
531
- statements: 'return this.request("post", path, payload);'
532
- },
533
- {
534
- name: "put",
535
- scope: tsMorph.Scope.Public,
536
- typeParameters: ['T extends TypePaths<"put">'],
537
- parameters: [
538
- { name: "path", type: "T" },
539
- {
540
- name: "payload",
541
- type: 'ApiPayload<T, "put">',
542
- hasQuestionToken: true
543
- }
544
- ],
545
- returnType: 'Promise<APIResponse<T, "put">>',
546
- statements: 'return this.request("put", path, payload);'
547
- },
548
- {
549
- name: "delete",
550
- scope: tsMorph.Scope.Public,
551
- typeParameters: ['T extends TypePaths<"delete">'],
552
- parameters: [
553
- { name: "path", type: "T" },
554
- {
555
- name: "payload",
556
- type: 'ApiPayload<T, "delete">',
557
- hasQuestionToken: true
558
- }
559
- ],
560
- returnType: 'Promise<APIResponse<T, "delete">>',
561
- statements: 'return this.request("delete", path, payload);'
562
- },
563
- {
564
- name: "patch",
565
- scope: tsMorph.Scope.Public,
566
- typeParameters: ['T extends TypePaths<"patch">'],
567
- parameters: [
568
- { name: "path", type: "T" },
569
- {
570
- name: "payload",
571
- type: 'ApiPayload<T, "patch">',
572
- hasQuestionToken: true
573
- }
574
- ],
575
- returnType: 'Promise<APIResponse<T, "patch">>',
576
- statements: 'return this.request("patch", path, payload);'
577
- },
578
- {
579
- name: "buildPathUrl",
580
- scope: tsMorph.Scope.Private,
581
- parameters: [
582
- { name: "basePath", type: "string" },
583
- { name: "pathParams", type: "unknown", hasQuestionToken: true }
584
- ],
585
- returnType: "string",
586
- statements: `let pathname = basePath;
587
- if (pathParams != null) {
588
- const params = pathParams as Record<string, unknown>;
589
- pathname = decodeURIComponent(pathname).replace(/{(w+)}/g, (_, key) =>
590
- encodeURIComponent(String(params[key])),
591
- );
592
- }
593
- return pathname;`
594
- },
595
- {
596
- name: "prepareBody",
597
- scope: tsMorph.Scope.Private,
598
- parameters: [
599
- { name: "method", type: "RestMethod" },
600
- { name: "body", type: "unknown", hasQuestionToken: true }
601
- ],
602
- returnType: "string | undefined",
603
- statements: `if (body && ["post", "put", "patch"].includes(method)) {
604
- return JSON.stringify(body);
605
- }
606
- return undefined;`
607
- },
608
- {
609
- name: "appendQueryParams",
610
- scope: tsMorph.Scope.Private,
611
- parameters: [
612
- { name: "url", type: "URL" },
613
- { name: "queryParams", type: "unknown", hasQuestionToken: true }
614
- ],
615
- returnType: "void",
616
- statements: `if (queryParams != null) {
617
- const params = queryParams as Record<string, unknown>;
618
- for (const [key, value] of Object.entries(params)) {
619
- if (value !== undefined && value !== null) {
620
- url.searchParams.append(key, String(value));
621
- }
622
- }
623
- }`
624
- }
625
- ]
626
- });
627
- await sourceFile.formatText();
628
- await project.save();
629
- console.log(`API client generated at ${outPut}`);
630
- }
631
- generate();