apigen-ts 1.2.1 → 1.3.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/dist/_template.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/{main-BZrWxHPM.mjs → main-B8jUVaQq.js} +86 -14
- package/dist/main.cjs +570 -11
- package/dist/main.d.cts +4 -0
- package/dist/main.d.mts +4 -0
- package/dist/main.js +1 -1
- package/dist/main.mjs +572 -10
- package/package.json +16 -18
- package/readme.md +98 -78
- package/dist/cli.cjs +0 -18
- package/dist/cli.mjs +0 -16
- package/dist/main-BLaUzCCM.cjs +0 -585
- package/dist/main-BZrWxHPM.js +0 -581
package/dist/main.mjs
CHANGED
|
@@ -1,11 +1,573 @@
|
|
|
1
|
-
import 'fs/promises';
|
|
2
|
-
import 'path';
|
|
3
|
-
import 'url';
|
|
4
|
-
export { a as apigen } from './main-BZrWxHPM.mjs';
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
5
4
|
import 'cleye';
|
|
6
|
-
import '@redocly/openapi-core';
|
|
7
|
-
import 'array-utils-ts';
|
|
8
|
-
import 'lodash-es';
|
|
9
|
-
import 'swagger2openapi';
|
|
10
|
-
import 'typescript';
|
|
11
|
-
import 'node:path';
|
|
5
|
+
import { bundle, BaseResolver, createConfig } from '@redocly/openapi-core';
|
|
6
|
+
import { filterEmpty, filterNullable } from 'array-utils-ts';
|
|
7
|
+
import { get, uniq, upperFirst, isArray, isBoolean, sortBy, isObject, lowerFirst, uniqBy } from 'lodash-es';
|
|
8
|
+
import { convertObj } from 'swagger2openapi';
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const initCtx = (config) => {
|
|
13
|
+
return {
|
|
14
|
+
source: "",
|
|
15
|
+
output: "",
|
|
16
|
+
name: "ApiClient",
|
|
17
|
+
doc: { openapi: "3.1.0" },
|
|
18
|
+
parseDates: false,
|
|
19
|
+
inlineEnums: false,
|
|
20
|
+
fetchOptions: false,
|
|
21
|
+
headers: {},
|
|
22
|
+
...config,
|
|
23
|
+
logTag: "",
|
|
24
|
+
usedNames: /* @__PURE__ */ new Set()
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const unref = (ctx, s) => {
|
|
29
|
+
if (!s) return void 0;
|
|
30
|
+
if ("$ref" in s && s.$ref) {
|
|
31
|
+
const parts = s.$ref.replace("#/", "").split("/");
|
|
32
|
+
const obj = parts.reduce(
|
|
33
|
+
// openapi encodes "/" in key as "~1"
|
|
34
|
+
(acc, x) => get(acc, x, get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
|
|
35
|
+
ctx.doc
|
|
36
|
+
);
|
|
37
|
+
if (obj) return obj;
|
|
38
|
+
console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
return s;
|
|
42
|
+
};
|
|
43
|
+
const getReqSchema = (ctx, config) => {
|
|
44
|
+
const req = unref(ctx, config.requestBody);
|
|
45
|
+
if (!req) return void 0;
|
|
46
|
+
const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
|
|
47
|
+
if (cts.length === 0) return void 0;
|
|
48
|
+
const pretenders = [
|
|
49
|
+
"application/json",
|
|
50
|
+
"text/",
|
|
51
|
+
"multipart/form-data",
|
|
52
|
+
"application/x-www-form-urlencoded"
|
|
53
|
+
];
|
|
54
|
+
for (const p of pretenders) {
|
|
55
|
+
const ct = cts.find((x) => x[0].startsWith(p));
|
|
56
|
+
if (ct) return ct;
|
|
57
|
+
}
|
|
58
|
+
cts.map((x) => x[0]);
|
|
59
|
+
return void 0;
|
|
60
|
+
};
|
|
61
|
+
const getRepSchema = (ctx, config) => {
|
|
62
|
+
const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => get(config, ["responses", x]));
|
|
63
|
+
const firstSuccess = unref(ctx, get(config, ["responses", successCodes[0]], {}));
|
|
64
|
+
if (!firstSuccess || !("content" in firstSuccess)) return void 0;
|
|
65
|
+
const cts = Object.entries(firstSuccess.content ?? {}).filter((x) => x[1].schema);
|
|
66
|
+
if (cts.length === 0) return void 0;
|
|
67
|
+
const ctJson = cts.find((x) => x[0].startsWith("application/json"));
|
|
68
|
+
if (ctJson) return ctJson[1].schema;
|
|
69
|
+
const ctText = cts.find((x) => x[0].startsWith("text/"));
|
|
70
|
+
if (ctText) return { type: "string" };
|
|
71
|
+
cts.map((x) => x[0]).join(", ");
|
|
72
|
+
return void 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const f$2 = ts.factory;
|
|
76
|
+
const Keywords = /* @__PURE__ */ new Set([
|
|
77
|
+
"break",
|
|
78
|
+
"case",
|
|
79
|
+
"catch",
|
|
80
|
+
"class",
|
|
81
|
+
"const",
|
|
82
|
+
"continue",
|
|
83
|
+
"debugger",
|
|
84
|
+
"default",
|
|
85
|
+
"delete",
|
|
86
|
+
"do",
|
|
87
|
+
"else",
|
|
88
|
+
"enum",
|
|
89
|
+
"export",
|
|
90
|
+
"extends",
|
|
91
|
+
"false",
|
|
92
|
+
"finally",
|
|
93
|
+
"for",
|
|
94
|
+
"function",
|
|
95
|
+
"if",
|
|
96
|
+
"import",
|
|
97
|
+
"in",
|
|
98
|
+
"instanceof",
|
|
99
|
+
"new",
|
|
100
|
+
"null",
|
|
101
|
+
"return",
|
|
102
|
+
"super",
|
|
103
|
+
"switch",
|
|
104
|
+
"this",
|
|
105
|
+
"throw",
|
|
106
|
+
"true",
|
|
107
|
+
"try",
|
|
108
|
+
"typeof",
|
|
109
|
+
"var",
|
|
110
|
+
"void",
|
|
111
|
+
"while",
|
|
112
|
+
"with",
|
|
113
|
+
"implements",
|
|
114
|
+
"interface",
|
|
115
|
+
"let",
|
|
116
|
+
"package",
|
|
117
|
+
"private",
|
|
118
|
+
"protected",
|
|
119
|
+
"public",
|
|
120
|
+
"static",
|
|
121
|
+
"yield",
|
|
122
|
+
"any",
|
|
123
|
+
"boolean",
|
|
124
|
+
"number",
|
|
125
|
+
"string",
|
|
126
|
+
"symbol",
|
|
127
|
+
// "abstract", "as", "async", "await", "constructor", "declare", "from", "get", "is", "module",
|
|
128
|
+
// "namespace", "of", "require", "set", "type",
|
|
129
|
+
"Record",
|
|
130
|
+
"Partial",
|
|
131
|
+
"Pick",
|
|
132
|
+
"Omit",
|
|
133
|
+
"Exclude",
|
|
134
|
+
"Extract",
|
|
135
|
+
// ts keywords
|
|
136
|
+
"Date",
|
|
137
|
+
"object",
|
|
138
|
+
"Response"
|
|
139
|
+
// ts type names
|
|
140
|
+
]);
|
|
141
|
+
const normalizeIdentifier = (val, asVar = false) => {
|
|
142
|
+
let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
143
|
+
if (name.match(/^\d/)) name = `$${name}`;
|
|
144
|
+
if (asVar && Keywords.has(name)) name = `$${name}`;
|
|
145
|
+
return name;
|
|
146
|
+
};
|
|
147
|
+
const makeInlineEnum = (s) => {
|
|
148
|
+
if (!s.enum) return void 0;
|
|
149
|
+
const values = filterEmpty(s.enum);
|
|
150
|
+
if (!values.length) return void 0;
|
|
151
|
+
if (!s.type) {
|
|
152
|
+
if (values.every((x) => typeof x === "string")) s.type = "string";
|
|
153
|
+
if (values.every((x) => typeof x === "number")) s.type = "number";
|
|
154
|
+
if (values.every((x) => typeof x === "boolean")) s.type = "boolean";
|
|
155
|
+
}
|
|
156
|
+
if (s.type === "string") {
|
|
157
|
+
const tokens = uniq(values).map((x) => f$2.createStringLiteral(x.toString()));
|
|
158
|
+
return f$2.createUnionTypeNode(tokens.map((x) => f$2.createLiteralTypeNode(x)));
|
|
159
|
+
}
|
|
160
|
+
if (s.type === "number") {
|
|
161
|
+
const tokens = uniq(values).map((x) => {
|
|
162
|
+
const n = x;
|
|
163
|
+
if (n < 0) {
|
|
164
|
+
return f$2.createLiteralTypeNode(
|
|
165
|
+
f$2.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f$2.createNumericLiteral(-n))
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return f$2.createLiteralTypeNode(f$2.createNumericLiteral(n));
|
|
169
|
+
});
|
|
170
|
+
return f$2.createUnionTypeNode(tokens);
|
|
171
|
+
}
|
|
172
|
+
if (s.type === "boolean") {
|
|
173
|
+
const tokens = [];
|
|
174
|
+
if (values.includes(true)) tokens.push(f$2.createToken(ts.SyntaxKind.TrueKeyword));
|
|
175
|
+
if (values.includes(false)) tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
176
|
+
return f$2.createUnionTypeNode(tokens.map((x) => f$2.createLiteralTypeNode(x)));
|
|
177
|
+
}
|
|
178
|
+
console.warn(`enum with unknown type "${s.type}" in`, s);
|
|
179
|
+
return void 0;
|
|
180
|
+
};
|
|
181
|
+
const makeObject = (ctx, s) => {
|
|
182
|
+
if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
|
|
183
|
+
if (s.additionalProperties && !isBoolean(s.additionalProperties)) {
|
|
184
|
+
return f$2.createTypeReferenceNode("Record", [
|
|
185
|
+
f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
186
|
+
makeType(ctx, s.additionalProperties)
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
return f$2.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
|
|
190
|
+
};
|
|
191
|
+
const makeLiteralUnion = (ctx, types) => {
|
|
192
|
+
const tokens = types.map((x) => makeType(ctx, { type: x }));
|
|
193
|
+
const hasUnknown = tokens.some((x) => x.kind === ts.SyntaxKind.UnknownKeyword);
|
|
194
|
+
if (hasUnknown) return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
195
|
+
return f$2.createUnionTypeNode(tokens);
|
|
196
|
+
};
|
|
197
|
+
const makeType = (ctx, s) => {
|
|
198
|
+
const mk = makeType.bind(null, ctx);
|
|
199
|
+
if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
|
|
200
|
+
if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
|
|
201
|
+
if ("$ref" in s && s.$ref) {
|
|
202
|
+
const parts = s.$ref.replace("#/", "").split("/");
|
|
203
|
+
if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
|
|
204
|
+
return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
|
|
205
|
+
}
|
|
206
|
+
const t = unref(ctx, s);
|
|
207
|
+
if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
208
|
+
return makeType(ctx, t);
|
|
209
|
+
}
|
|
210
|
+
if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
|
|
211
|
+
if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
|
|
212
|
+
if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
213
|
+
if ("type" in s && s.type === "integer") s.type = "number";
|
|
214
|
+
if ("enum" in s && s.enum && !Array.isArray(s.type)) {
|
|
215
|
+
const isArray2 = s.type === "array";
|
|
216
|
+
const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
|
|
217
|
+
if (t) return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
218
|
+
}
|
|
219
|
+
if ("properties" in s && s.properties) {
|
|
220
|
+
let t = f$2.createTypeLiteralNode(
|
|
221
|
+
Object.entries(s.properties).map(([k, v]) => {
|
|
222
|
+
const r = s.required ?? [];
|
|
223
|
+
const q = r.includes(k) ? void 0 : f$2.createToken(ts.SyntaxKind.QuestionToken);
|
|
224
|
+
return f$2.createPropertySignature(void 0, f$2.createStringLiteral(k), q, mk(v));
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
if ("nullable" in s && s.nullable) {
|
|
228
|
+
return f$2.createUnionTypeNode([t, f$2.createLiteralTypeNode(f$2.createNull())]);
|
|
229
|
+
}
|
|
230
|
+
return t;
|
|
231
|
+
}
|
|
232
|
+
if ("type" in s) {
|
|
233
|
+
let t;
|
|
234
|
+
if (s.type === "object") t = makeObject(ctx, s);
|
|
235
|
+
else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
236
|
+
else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
237
|
+
else if (isConstantString(s)) t = f$2.createLiteralTypeNode(f$2.createStringLiteral(s.const));
|
|
238
|
+
else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
239
|
+
else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
240
|
+
else if (isArray(s.type)) t = makeLiteralUnion(ctx, s.type);
|
|
241
|
+
else if (s.type === "array" && isPrefixItems(s) && s.prefixItems && !s.items)
|
|
242
|
+
t = f$2.createTupleTypeNode(s.prefixItems.map(mk));
|
|
243
|
+
else if (s.type === "array" && !isBoolean(s.items)) t = f$2.createArrayTypeNode(mk(s.items));
|
|
244
|
+
else {
|
|
245
|
+
console.warn(`makeType: unknown type "${s.type}"`);
|
|
246
|
+
return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
247
|
+
}
|
|
248
|
+
if (s.type === "string") {
|
|
249
|
+
if (s.format === "binary") t = f$2.createTypeReferenceNode("File");
|
|
250
|
+
if (s.format === "date-time" && ctx.parseDates) t = f$2.createTypeReferenceNode("Date");
|
|
251
|
+
}
|
|
252
|
+
if ("nullable" in s && s.nullable) {
|
|
253
|
+
return f$2.createUnionTypeNode([t, f$2.createLiteralTypeNode(f$2.createNull())]);
|
|
254
|
+
}
|
|
255
|
+
return t;
|
|
256
|
+
}
|
|
257
|
+
return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
258
|
+
};
|
|
259
|
+
const isStringEnum = (s) => {
|
|
260
|
+
if ("enum" in s && s.enum) {
|
|
261
|
+
return s.enum.every((x) => typeof x === "string");
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
};
|
|
265
|
+
const isPrefixItems = (s) => {
|
|
266
|
+
return s.prefixItems !== void 0;
|
|
267
|
+
};
|
|
268
|
+
const isConstantString = (s) => {
|
|
269
|
+
return s.type === "string" && s.const !== void 0;
|
|
270
|
+
};
|
|
271
|
+
const makeTypeAlias = (ctx, name, s) => {
|
|
272
|
+
if (isStringEnum(s) && !ctx.inlineEnums) {
|
|
273
|
+
const tokens1 = uniq(s.enum);
|
|
274
|
+
const tokens2 = filterEmpty(tokens1);
|
|
275
|
+
if (tokens1.length !== tokens2.length) {
|
|
276
|
+
console.warn(`enum ${name} has empty values`, s);
|
|
277
|
+
}
|
|
278
|
+
return f$2.createEnumDeclaration(
|
|
279
|
+
[f$2.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
280
|
+
normalizeIdentifier(name, true),
|
|
281
|
+
tokens2.map(
|
|
282
|
+
(x) => f$2.createEnumMember(upperFirst(normalizeIdentifier(x)), f$2.createStringLiteral(x))
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return f$2.createTypeAliasDeclaration(
|
|
287
|
+
[f$2.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
288
|
+
f$2.createIdentifier(normalizeIdentifier(name, true)),
|
|
289
|
+
void 0,
|
|
290
|
+
makeType(ctx, s)
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const f$1 = ts.factory;
|
|
295
|
+
const HttpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
296
|
+
const normalizeOpName = (val) => {
|
|
297
|
+
const articles = /* @__PURE__ */ new Set(["a", "an", "the"]);
|
|
298
|
+
const tmp = val.replace(/'/, "").replace(/[^a-zA-Z0-9]/g, "_").split("_").filter((x) => x !== "" && !articles.has(x)).map((x) => upperFirst(x));
|
|
299
|
+
tmp[0] = tmp[0].toUpperCase() === tmp[0] ? tmp[0].toLowerCase() : lowerFirst(tmp[0]);
|
|
300
|
+
return tmp.join("");
|
|
301
|
+
};
|
|
302
|
+
const getOpName = (ctx, op) => {
|
|
303
|
+
let ns = normalizeOpName(filterEmpty(op.tags ?? [])[0] ?? "general");
|
|
304
|
+
let fn = op.operationId ?? null;
|
|
305
|
+
if (!fn) {
|
|
306
|
+
fn = op.path.replace(/^(\/api)?(\/v?\d\.?\d?)?\/(.+)$/, "$3");
|
|
307
|
+
fn = `${op.method}/${fn}`.replace(/\/+/, "/");
|
|
308
|
+
}
|
|
309
|
+
fn = normalizeOpName(fn);
|
|
310
|
+
let nsr = ns.split("").map((x) => `[${x.toUpperCase()}${x.toLowerCase()}]`).join("");
|
|
311
|
+
if (nsr.endsWith("[Ss]")) nsr += "?";
|
|
312
|
+
fn = fn.replace(new RegExp(`^${nsr}([Cc]ontroller|[Ss]ervice)?([A-Z].*)$`), "$2");
|
|
313
|
+
fn = lowerFirst(fn);
|
|
314
|
+
const proposal = [ns, fn];
|
|
315
|
+
if (ctx.resolveName) {
|
|
316
|
+
const res = ctx.resolveName(ctx, op, proposal);
|
|
317
|
+
if (Array.isArray(res) && res.length === 2) return res;
|
|
318
|
+
if (res !== void 0) {
|
|
319
|
+
console.warn(`${ctx.logTag} resolveName should return [ns, fn] or undefined (skipping)`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return proposal;
|
|
323
|
+
};
|
|
324
|
+
const prepareUrl = (url, rename) => {
|
|
325
|
+
for (const [k, v] of Object.entries(rename)) url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
326
|
+
const parts = url.split("${");
|
|
327
|
+
if (parts.length === 1) return f$1.createStringLiteral(url);
|
|
328
|
+
return f$1.createTemplateExpression(
|
|
329
|
+
f$1.createTemplateHead(parts[0]),
|
|
330
|
+
parts.slice(1).map((x, i) => {
|
|
331
|
+
const [name, ...rest] = x.split("}");
|
|
332
|
+
const right = rest.join("}");
|
|
333
|
+
return f$1.createTemplateSpan(
|
|
334
|
+
f$1.createIdentifier(name),
|
|
335
|
+
// no normalization required
|
|
336
|
+
i === parts.length - 2 ? f$1.createTemplateTail(right) : f$1.createTemplateMiddle(right)
|
|
337
|
+
);
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
const prepareOp = (ctx, cfg, opName) => {
|
|
342
|
+
cfg.parameters = cfg.parameters ?? [];
|
|
343
|
+
const reqSchema = getReqSchema(ctx, cfg);
|
|
344
|
+
const repSchema = getRepSchema(ctx, cfg);
|
|
345
|
+
const allParams = filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
|
|
346
|
+
const params = uniqBy(allParams.filter((x) => x.in === "path"), "name");
|
|
347
|
+
if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
|
|
348
|
+
const search = allParams.filter((x) => x.in === "query");
|
|
349
|
+
allParams.filter((x) => x.in === "header");
|
|
350
|
+
for (const [name, v] of Object.entries({ search })) {
|
|
351
|
+
if (!v.length) continue;
|
|
352
|
+
const properties = v.reduce((acc, x) => ({ ...acc, [x.name]: x.schema }), {});
|
|
353
|
+
params.push({ name, schema: { type: "object", properties } });
|
|
354
|
+
}
|
|
355
|
+
const urlReplacements = {};
|
|
356
|
+
const fnArgs = params.map((x) => {
|
|
357
|
+
const name = normalizeIdentifier(x.name, true);
|
|
358
|
+
const type = makeType(ctx, x.schema);
|
|
359
|
+
urlReplacements[x.name] = name;
|
|
360
|
+
return f$1.createParameterDeclaration(void 0, void 0, name, void 0, type);
|
|
361
|
+
});
|
|
362
|
+
if (ctx.fetchOptions) {
|
|
363
|
+
fnArgs.push(
|
|
364
|
+
f$1.createParameterDeclaration(
|
|
365
|
+
void 0,
|
|
366
|
+
void 0,
|
|
367
|
+
"opts",
|
|
368
|
+
f$1.createToken(ts.SyntaxKind.QuestionToken),
|
|
369
|
+
f$1.createTypeReferenceNode(f$1.createIdentifier("ApigenRequest"), void 0)
|
|
370
|
+
)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const cbArgs = filterNullable([
|
|
374
|
+
ctx.fetchOptions ? f$1.createSpreadAssignment(f$1.createIdentifier("opts")) : void 0,
|
|
375
|
+
search.length ? f$1.createShorthandPropertyAssignment("search") : void 0,
|
|
376
|
+
reqSchema && f$1.createShorthandPropertyAssignment("body"),
|
|
377
|
+
reqSchema && reqSchema[0] !== "application/json" ? f$1.createPropertyAssignment(
|
|
378
|
+
"headers",
|
|
379
|
+
f$1.createObjectLiteralExpression([
|
|
380
|
+
f$1.createPropertyAssignment(
|
|
381
|
+
f$1.createStringLiteral("content-type"),
|
|
382
|
+
f$1.createStringLiteral(reqSchema[0])
|
|
383
|
+
)
|
|
384
|
+
])
|
|
385
|
+
) : void 0
|
|
386
|
+
]);
|
|
387
|
+
return f$1.createPropertyAssignment(
|
|
388
|
+
f$1.createIdentifier(normalizeIdentifier(opName)),
|
|
389
|
+
f$1.createArrowFunction(
|
|
390
|
+
void 0,
|
|
391
|
+
void 0,
|
|
392
|
+
fnArgs,
|
|
393
|
+
void 0,
|
|
394
|
+
void 0,
|
|
395
|
+
f$1.createBlock([
|
|
396
|
+
f$1.createReturnStatement(
|
|
397
|
+
f$1.createCallExpression(
|
|
398
|
+
f$1.createIdentifier("this.Fetch"),
|
|
399
|
+
[makeType(ctx, repSchema)],
|
|
400
|
+
[
|
|
401
|
+
f$1.createStringLiteral(cfg.method),
|
|
402
|
+
// method
|
|
403
|
+
prepareUrl(cfg.path, urlReplacements),
|
|
404
|
+
// path
|
|
405
|
+
f$1.createObjectLiteralExpression(cbArgs)
|
|
406
|
+
// { query, body, headers }
|
|
407
|
+
]
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
])
|
|
411
|
+
)
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
const prepareNs = (ctx, name, handlers) => {
|
|
415
|
+
return f$1.createPropertyDeclaration(
|
|
416
|
+
void 0,
|
|
417
|
+
normalizeIdentifier(name),
|
|
418
|
+
void 0,
|
|
419
|
+
void 0,
|
|
420
|
+
f$1.createObjectLiteralExpression(handlers)
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
const prepareRoutes = async (ctx) => {
|
|
424
|
+
const routes = {};
|
|
425
|
+
for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
|
|
426
|
+
ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
|
|
427
|
+
if (!isObject(pathConfig)) continue;
|
|
428
|
+
if (ctx.filterPaths) {
|
|
429
|
+
const match = ctx.filterPaths instanceof RegExp ? ctx.filterPaths.test(path) : ctx.filterPaths(path);
|
|
430
|
+
if (!match) continue;
|
|
431
|
+
}
|
|
432
|
+
if ("$ref" in pathConfig) {
|
|
433
|
+
console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
for (const method of HttpMethods) {
|
|
437
|
+
ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
|
|
438
|
+
const config = pathConfig[method];
|
|
439
|
+
if (!config) continue;
|
|
440
|
+
if (pathConfig.parameters) {
|
|
441
|
+
config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
|
|
442
|
+
}
|
|
443
|
+
const opTags = config.tags ?? [];
|
|
444
|
+
if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) continue;
|
|
445
|
+
if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) continue;
|
|
446
|
+
const [ns, op] = getOpName(ctx, { ...config, method, path });
|
|
447
|
+
if (!routes[ns]) routes[ns] = [];
|
|
448
|
+
const joined = [ns, op].join(".");
|
|
449
|
+
if (ctx.usedNames.has(joined)) {
|
|
450
|
+
continue;
|
|
451
|
+
} else {
|
|
452
|
+
ctx.usedNames.add(joined);
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
routes[ns].push(prepareOp(ctx, { ...config, method, path }, op));
|
|
456
|
+
} catch (e) {
|
|
457
|
+
console.error(`${ctx.logTag} - ${e}`, config);
|
|
458
|
+
throw e;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return routes;
|
|
463
|
+
};
|
|
464
|
+
const prepareTypes = async (ctx) => {
|
|
465
|
+
const types = [];
|
|
466
|
+
const typesConfig = sortBy(Object.entries(ctx.doc.components?.schemas ?? {}), ([k]) => k);
|
|
467
|
+
for (const [name, config] of typesConfig) {
|
|
468
|
+
try {
|
|
469
|
+
types.push(makeTypeAlias(ctx, name, config));
|
|
470
|
+
} catch (e) {
|
|
471
|
+
console.error(`${ctx.logTag} - ${e}`, name, config);
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return types;
|
|
476
|
+
};
|
|
477
|
+
const generateAst = async (ctx) => {
|
|
478
|
+
const types = await prepareTypes(ctx);
|
|
479
|
+
const routes = await prepareRoutes(ctx);
|
|
480
|
+
const modules = [];
|
|
481
|
+
for (const [k, v] of Object.entries(routes)) {
|
|
482
|
+
modules.push(prepareNs(ctx, k, v));
|
|
483
|
+
}
|
|
484
|
+
return { modules, types };
|
|
485
|
+
};
|
|
486
|
+
const loadSchema = async ({
|
|
487
|
+
url,
|
|
488
|
+
upgrade = true,
|
|
489
|
+
headers = {}
|
|
490
|
+
}) => {
|
|
491
|
+
if (url.startsWith("file://")) url = url.substring(7);
|
|
492
|
+
const { bundle: bundle$1 } = await bundle({
|
|
493
|
+
ref: url,
|
|
494
|
+
config: await createConfig({}),
|
|
495
|
+
removeUnusedComponents: false,
|
|
496
|
+
externalRefResolver: new BaseResolver({
|
|
497
|
+
http: {
|
|
498
|
+
headers: Object.entries(headers).map(([name, value]) => {
|
|
499
|
+
return { name, value, matches: "**" };
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
});
|
|
504
|
+
const parsed = bundle$1.parsed;
|
|
505
|
+
if ("swagger" in parsed && upgrade) {
|
|
506
|
+
const { openapi } = await convertObj(parsed, { patch: true });
|
|
507
|
+
return openapi;
|
|
508
|
+
}
|
|
509
|
+
return parsed;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const f = ts.factory;
|
|
513
|
+
const addNewLines = (nodes) => {
|
|
514
|
+
const result = [];
|
|
515
|
+
for (const node of nodes) {
|
|
516
|
+
result.push(node);
|
|
517
|
+
result.push(f.createIdentifier("\n"));
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
};
|
|
521
|
+
const printCode = (nodes) => {
|
|
522
|
+
return ts.createPrinter().printFile(
|
|
523
|
+
f.createSourceFile(
|
|
524
|
+
addNewLines(nodes),
|
|
525
|
+
f.createToken(ts.SyntaxKind.EndOfFileToken),
|
|
526
|
+
ts.NodeFlags.None
|
|
527
|
+
)
|
|
528
|
+
).replaceAll("}, ", "},\n\n");
|
|
529
|
+
};
|
|
530
|
+
const formatCode = async (code) => {
|
|
531
|
+
try {
|
|
532
|
+
const prettier = await import('prettier');
|
|
533
|
+
const options = await prettier.resolveConfig(path.join(process.cwd(), "file"));
|
|
534
|
+
return prettier.format(code, { ...options, parser: "typescript" });
|
|
535
|
+
} catch (e) {
|
|
536
|
+
return code;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const apigen = async (config) => {
|
|
541
|
+
const doc = await loadSchema({
|
|
542
|
+
url: config.source,
|
|
543
|
+
...config.headers ? { headers: config.headers } : {}
|
|
544
|
+
});
|
|
545
|
+
const ctx = initCtx({ ...config, doc });
|
|
546
|
+
const { modules, types } = await generateAst(ctx);
|
|
547
|
+
const filepath = join(dirname(fileURLToPath(import.meta.url)), "_template.ts");
|
|
548
|
+
const file = await fs.readFile(filepath, "utf-8");
|
|
549
|
+
let code = [
|
|
550
|
+
`// Auto-generated by https://github.com/vladkens/apigen-ts`,
|
|
551
|
+
`// Source: ${config.source}
|
|
552
|
+
`,
|
|
553
|
+
// remove all comments expect which starts with "// apigen:"
|
|
554
|
+
...file.split("\n").filter((x) => !/\s*\/\/\s(?!apigen:)/.test(x))
|
|
555
|
+
].join("\n");
|
|
556
|
+
if (!ctx.parseDates) {
|
|
557
|
+
code = code.replace(/\s*ISO_FORMAT\s=.+$/gm, "");
|
|
558
|
+
code = code.replace(/PopulateDates.+?\n\s{2}\}/s, "");
|
|
559
|
+
code = code.replace(/this.PopulateDates\((.+)\)/, "$1");
|
|
560
|
+
}
|
|
561
|
+
code = code.replace("// apigen:modules", printCode(modules));
|
|
562
|
+
code = code.replace("// apigen:types", printCode(types));
|
|
563
|
+
code = code.replace("class ApiClient", `class ${ctx.name}`);
|
|
564
|
+
const result = await formatCode(code);
|
|
565
|
+
if (config.output === null) {
|
|
566
|
+
process.stdout.write(result);
|
|
567
|
+
} else {
|
|
568
|
+
await fs.mkdir(dirname(config.output), { recursive: true });
|
|
569
|
+
await fs.writeFile(config.output, result);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
export { apigen };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "apigen-ts",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vladkens <v.pronsky@gmail.com>",
|
|
7
7
|
"repository": "vladkens/apigen-ts",
|
|
@@ -16,30 +16,28 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "rm -rf dist && pkgroll && cp ./src/_template.ts ./dist && ls -lah dist",
|
|
19
|
-
"test": "
|
|
20
|
-
"test-cov": "
|
|
21
|
-
"format": "prettier --write .",
|
|
22
|
-
"ci": "tsc --noEmit &&
|
|
19
|
+
"test": "tsx --test --test-reporter=dot test/*.test.ts",
|
|
20
|
+
"test-cov": "tsx --experimental-test-coverage --test test/*.test.ts",
|
|
21
|
+
"format": "prettier --write --log-level warn .",
|
|
22
|
+
"ci": "tsc --noEmit && npm run test-cov && npm run build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@redocly/openapi-core": "
|
|
25
|
+
"@redocly/openapi-core": "2.21.1",
|
|
26
26
|
"@types/lodash-es": "4.17.12",
|
|
27
27
|
"@types/swagger2openapi": "7.0.4",
|
|
28
|
-
"array-utils-ts": "1.0
|
|
29
|
-
"cleye": "
|
|
30
|
-
"lodash-es": "4.17.
|
|
28
|
+
"array-utils-ts": "1.1.0",
|
|
29
|
+
"cleye": "2.3.0",
|
|
30
|
+
"lodash-es": "4.17.23",
|
|
31
31
|
"swagger2openapi": "7.0.8"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"pkgroll": "2.
|
|
38
|
-
"prettier": "3.
|
|
39
|
-
"prettier-plugin-organize-imports": "4.
|
|
40
|
-
"
|
|
41
|
-
"typescript": "5.8.3",
|
|
42
|
-
"uvu": "0.5.6"
|
|
34
|
+
"@types/node": "25.5.0",
|
|
35
|
+
"fetch-mock": "12.6.0",
|
|
36
|
+
"tsx": "4.21.0",
|
|
37
|
+
"pkgroll": "2.27.0",
|
|
38
|
+
"prettier": "3.8.1",
|
|
39
|
+
"prettier-plugin-organize-imports": "4.3.0",
|
|
40
|
+
"typescript": "5.9.3"
|
|
43
41
|
},
|
|
44
42
|
"peerDependencies": {
|
|
45
43
|
"prettier": "^3.0.0",
|