create-swagger-client 0.1.0
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 +208 -0
- package/dist/index.js +631 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# create-swagger-client
|
|
2
|
+
|
|
3
|
+
A TypeScript tool that generates a fully type-safe REST API client from OpenAPI/Swagger specifications. Built with `openapi-typescript` and `ts-morph`, it creates a strongly-typed client class with autocomplete and compile-time type checking for all API endpoints.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Full Type Safety**: All endpoints, parameters, request bodies, and responses are type-checked
|
|
8
|
+
- 🚀 **Auto-completion**: IDE autocomplete for paths, methods, and payloads
|
|
9
|
+
- 🔄 **Multiple HTTP Methods**: Support for GET, POST, PUT, DELETE, and PATCH
|
|
10
|
+
- 📝 **OpenAPI Spec Support**: Works with OpenAPI 3.x specifications (JSON or YAML)
|
|
11
|
+
- 🌐 **URL or File Input**: Generate from remote URLs or local files
|
|
12
|
+
- 🎯 **Type Inference**: Automatic extraction of path params, query params, headers, and request/response types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Generate API Client
|
|
18
|
+
|
|
19
|
+
Run the generator with your OpenAPI specification:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# From a URL
|
|
23
|
+
npx create-swagger-client https://api.example.com/openapi.json
|
|
24
|
+
|
|
25
|
+
# From a local file
|
|
26
|
+
npx create-swagger-client ./swagger.json
|
|
27
|
+
|
|
28
|
+
# Specify custom output file
|
|
29
|
+
npx create-swagger-client https://api.example.com/openapi.json my-api-client.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Arguments:**
|
|
33
|
+
- `source` (required): URL or file path to your OpenAPI/Swagger specification
|
|
34
|
+
- `output` (optional): Output file name (default: `client-api.ts`)
|
|
35
|
+
|
|
36
|
+
This will generate a file with:
|
|
37
|
+
- All TypeScript types from your OpenAPI spec
|
|
38
|
+
- A `RestApiClient` class with type-safe methods
|
|
39
|
+
- Helper types for extracting parameters and responses
|
|
40
|
+
|
|
41
|
+
### Using the Generated Client
|
|
42
|
+
|
|
43
|
+
After generation, import and use the client:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { RestApiClient } from './client-api';
|
|
47
|
+
|
|
48
|
+
// Initialize the client
|
|
49
|
+
const api = new RestApiClient('https://api.example.com', {
|
|
50
|
+
headers: {
|
|
51
|
+
'Authorization': 'Bearer YOUR_TOKEN'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Make type-safe requests
|
|
56
|
+
// GET request with query parameters
|
|
57
|
+
const users = await api.get('/users', {
|
|
58
|
+
query: { page: 1, limit: 10 }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// GET request with path parameters
|
|
62
|
+
const user = await api.get('/users/{id}', {
|
|
63
|
+
path: { id: '123' }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// POST request with body
|
|
67
|
+
const newUser = await api.post('/users', {
|
|
68
|
+
body: {
|
|
69
|
+
name: 'John Doe',
|
|
70
|
+
email: 'john@example.com'
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// PUT request
|
|
75
|
+
const updatedUser = await api.put('/users/{id}', {
|
|
76
|
+
path: { id: '123' },
|
|
77
|
+
body: {
|
|
78
|
+
name: 'Jane Doe',
|
|
79
|
+
email: 'jane@example.com'
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// DELETE request
|
|
84
|
+
await api.delete('/users/{id}', {
|
|
85
|
+
path: { id: '123' }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// PATCH request
|
|
89
|
+
const patchedUser = await api.patch('/users/{id}', {
|
|
90
|
+
path: { id: '123' },
|
|
91
|
+
body: {
|
|
92
|
+
email: 'newemail@example.com'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Advanced Usage
|
|
98
|
+
|
|
99
|
+
#### Custom Headers per Request
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const data = await api.get('/protected-endpoint', {
|
|
103
|
+
headers: {
|
|
104
|
+
'X-Custom-Header': 'value'
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Request with Multiple Parameter Types
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const result = await api.post('/projects/{projectId}/tasks', {
|
|
113
|
+
path: { projectId: 'proj-123' },
|
|
114
|
+
query: { notify: true },
|
|
115
|
+
headers: { 'X-Request-ID': 'req-456' },
|
|
116
|
+
body: {
|
|
117
|
+
title: 'New Task',
|
|
118
|
+
description: 'Task description'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Custom Fetch Options
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const api = new RestApiClient('https://api.example.com', {
|
|
127
|
+
headers: {
|
|
128
|
+
'Authorization': 'Bearer TOKEN'
|
|
129
|
+
},
|
|
130
|
+
mode: 'cors',
|
|
131
|
+
credentials: 'include',
|
|
132
|
+
// Any other RequestInit options
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Generated Types
|
|
137
|
+
|
|
138
|
+
The generator creates several useful type utilities:
|
|
139
|
+
|
|
140
|
+
- `RestMethod`: Union of HTTP methods (`"get" | "post" | "put" | "delete" | "patch"`)
|
|
141
|
+
- `KeyPaths`: All available API paths
|
|
142
|
+
- `ExtractPathParams<Path, Method>`: Extract path parameters for an endpoint
|
|
143
|
+
- `ExtractQueryParams<Path, Method>`: Extract query parameters for an endpoint
|
|
144
|
+
- `ExtractHeaderParams<Path, Method>`: Extract header parameters for an endpoint
|
|
145
|
+
- `ExtractBody<Path, Method>`: Extract request body type for an endpoint
|
|
146
|
+
- `APIResponse<Path, Method>`: Extract response type for an endpoint
|
|
147
|
+
- `ApiPayload<Path, Method>`: Combined payload type for a request
|
|
148
|
+
- `ApiClientType`: Type definition for the entire client
|
|
149
|
+
|
|
150
|
+
## Example: Using Generated Types
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import {
|
|
154
|
+
RestApiClient,
|
|
155
|
+
ExtractBody,
|
|
156
|
+
APIResponse
|
|
157
|
+
} from './client-api';
|
|
158
|
+
|
|
159
|
+
// Use types in your code
|
|
160
|
+
type CreateUserBody = ExtractBody<'/users', 'post'>;
|
|
161
|
+
type UserResponse = APIResponse<'/users/{id}', 'get'>;
|
|
162
|
+
|
|
163
|
+
const createUser = (data: CreateUserBody): Promise<UserResponse> => {
|
|
164
|
+
return api.post('/users', { body: data });
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Error Handling
|
|
169
|
+
|
|
170
|
+
The client throws errors for failed requests:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
try {
|
|
174
|
+
const user = await api.get('/users/{id}', {
|
|
175
|
+
path: { id: '123' }
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('API request failed:', error.message);
|
|
179
|
+
// Error message includes status code, status text, and response body
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
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
|
+
## Requirements
|
|
198
|
+
|
|
199
|
+
- TypeScript 5.x
|
|
200
|
+
- Node.js 16+ or Bun
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
|
|
208
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
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();
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-swagger-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate fully type-safe REST API clients from OpenAPI/Swagger specifications",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-swagger-client": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
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
|
+
"keywords": [
|
|
20
|
+
"openapi",
|
|
21
|
+
"swagger",
|
|
22
|
+
"typescript",
|
|
23
|
+
"api-client",
|
|
24
|
+
"type-safe",
|
|
25
|
+
"rest-api",
|
|
26
|
+
"code-generator",
|
|
27
|
+
"openapi-generator",
|
|
28
|
+
"swagger-codegen"
|
|
29
|
+
],
|
|
30
|
+
"author": "cuongboi",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/cuongboi/create-swagger-client.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/cuongboi/create-swagger-client/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/cuongboi/create-swagger-client#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=16.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/bun": "latest"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"typescript": "^5"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"openapi-typescript": "^7.10.1",
|
|
51
|
+
"ts-morph": "^27.0.2"
|
|
52
|
+
}
|
|
53
|
+
}
|