@ozanarslan/corpus-cli 0.0.1
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/LICENSE.txt +20 -0
- package/README.md +196 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +1448 -0
- package/dist/getConfig-lU3I2Ejy.mjs +324 -0
- package/dist/index.d.mts +1178 -0
- package/dist/index.mjs +595 -0
- package/package.json +48 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { c as quote, d as toPascalCase, i as logger, m as isAbsent, n as getDefaultConfig, o as StringBuilder, p as cache, v as isSomeArray } from "./getConfig-lU3I2Ejy.mjs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
//#region src/internal/objMerge.ts
|
|
5
|
+
function objMerge(base, override) {
|
|
6
|
+
if (override === void 0 || override === null) return base;
|
|
7
|
+
const result = { ...base };
|
|
8
|
+
for (const key of Object.keys(override)) {
|
|
9
|
+
const overrideVal = override[key];
|
|
10
|
+
const baseVal = base[key];
|
|
11
|
+
if (overrideVal === void 0 || overrideVal === null) continue;
|
|
12
|
+
if (typeof overrideVal === "object" && !Array.isArray(overrideVal) && typeof baseVal === "object" && !Array.isArray(baseVal) && baseVal !== null) result[key] = objMerge(baseVal, overrideVal);
|
|
13
|
+
else result[key] = overrideVal;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/Config/defineConfig.ts
|
|
19
|
+
function defineConfig(config) {
|
|
20
|
+
return objMerge(getDefaultConfig(), config);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/SchemaPrinter/SchemaPrinterAbstract.ts
|
|
24
|
+
var SchemaPrinterAbstract = class {
|
|
25
|
+
/**
|
|
26
|
+
* Split on a depth-0 separator, respecting brackets and quotes.
|
|
27
|
+
* `<`/`>` are NOT treated as brackets — arktype constraints use them as
|
|
28
|
+
* comparison operators (`string <= 20`), which would desync the depth counter.
|
|
29
|
+
*/
|
|
30
|
+
split(expr, sep) {
|
|
31
|
+
const parts = [];
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let quote = null;
|
|
34
|
+
let buf = "";
|
|
35
|
+
for (let i = 0; i < expr.length; i++) {
|
|
36
|
+
const c = expr[i];
|
|
37
|
+
if (quote) {
|
|
38
|
+
buf += c;
|
|
39
|
+
if (c === quote && expr[i - 1] !== "\\") quote = null;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c === "\"" || c === "'") {
|
|
43
|
+
quote = c;
|
|
44
|
+
buf += c;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (c === "{" || c === "(" || c === "[") depth++;
|
|
48
|
+
else if (c === "}" || c === ")" || c === "]") depth--;
|
|
49
|
+
if (depth === 0 && c === sep && (sep === "," || sep === ";" || expr[i - 1] === " " && expr[i + 1] === " ")) {
|
|
50
|
+
parts.push(buf.trim());
|
|
51
|
+
buf = "";
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
buf += c;
|
|
55
|
+
}
|
|
56
|
+
parts.push(buf.trim());
|
|
57
|
+
return parts.filter((p) => p.length > 0);
|
|
58
|
+
}
|
|
59
|
+
/** Parenthesize a union/intersection so a trailing `[]` binds to the whole thing. */
|
|
60
|
+
wrap(s) {
|
|
61
|
+
return this.split(s, "|").length > 1 || this.split(s, "&").length > 1 ? `(${s})` : s;
|
|
62
|
+
}
|
|
63
|
+
key(k) {
|
|
64
|
+
return /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
|
|
65
|
+
}
|
|
66
|
+
/** JSON.stringify returns the *value* undefined for undefined — normalize to TS text. */
|
|
67
|
+
literal(v) {
|
|
68
|
+
if (v === void 0) return "undefined";
|
|
69
|
+
if (typeof v === "bigint") return `${v}n`;
|
|
70
|
+
return JSON.stringify(v) ?? "unknown";
|
|
71
|
+
}
|
|
72
|
+
topLevelColon(s) {
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let quote = null;
|
|
75
|
+
for (let i = 0; i < s.length; i++) {
|
|
76
|
+
const c = s[i];
|
|
77
|
+
if (quote) {
|
|
78
|
+
if (c === quote && s[i - 1] !== "\\") quote = null;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (c === "\"" || c === "'") {
|
|
82
|
+
quote = c;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c === "{" || c === "(" || c === "[") depth++;
|
|
86
|
+
else if (c === "}" || c === ")" || c === "]") depth--;
|
|
87
|
+
else if (c === ":" && depth === 0) return i;
|
|
88
|
+
}
|
|
89
|
+
return -1;
|
|
90
|
+
}
|
|
91
|
+
/** Sort union members: structural types first (alphabetically), then null/undefined last. */
|
|
92
|
+
sortUnion(members) {
|
|
93
|
+
const rank = (s) => s === "undefined" ? 2 : s === "null" ? 1 : 0;
|
|
94
|
+
return [...members].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/SchemaPrinter/ArkSchemaPrinter.ts
|
|
99
|
+
/** Bare identifiers that are already valid TS. */
|
|
100
|
+
const KEEP = /* @__PURE__ */ new Set([
|
|
101
|
+
"string",
|
|
102
|
+
"number",
|
|
103
|
+
"boolean",
|
|
104
|
+
"bigint",
|
|
105
|
+
"symbol",
|
|
106
|
+
"Date",
|
|
107
|
+
"File",
|
|
108
|
+
"Blob",
|
|
109
|
+
"FormData",
|
|
110
|
+
"null",
|
|
111
|
+
"undefined",
|
|
112
|
+
"unknown",
|
|
113
|
+
"never",
|
|
114
|
+
"object",
|
|
115
|
+
"Array",
|
|
116
|
+
"Function"
|
|
117
|
+
]);
|
|
118
|
+
var ArkSchemaPrinter = class extends SchemaPrinterAbstract {
|
|
119
|
+
print(schema, io) {
|
|
120
|
+
const t = schema;
|
|
121
|
+
const side = io === "in" ? t.in : t.out;
|
|
122
|
+
return this.strip(side.expression);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Rewrite an arktype expression into valid TS: recurse through unions,
|
|
126
|
+
* intersections and bracketed groups, keeping only TS-valid constituents
|
|
127
|
+
* and dropping runtime constraints (`string <= 20`, `number % 2`, regexes).
|
|
128
|
+
*/
|
|
129
|
+
strip(expr) {
|
|
130
|
+
const unions = this.split(expr, "|");
|
|
131
|
+
if (unions.length > 1) return this.sortUnion(unions.map((u) => this.strip(u))).join(" | ");
|
|
132
|
+
const parts = this.split(expr, "&").map((p) => this.rewriteGroup(p));
|
|
133
|
+
if (parts.length === 1) return parts[0];
|
|
134
|
+
const kept = parts.filter((s) => this.isTsToken(s));
|
|
135
|
+
return kept.length > 0 ? kept.join(" & ") : "unknown";
|
|
136
|
+
}
|
|
137
|
+
isTsToken(s) {
|
|
138
|
+
return KEEP.has(s) || /^".*"$/.test(s) || /^-?[\d.]+n?$/.test(s) || /^(?:true|false)$/.test(s) || /^[({[]/.test(s) || /^Array<[\s\S]*>$/.test(s) || /^[A-Z][\w$]*</.test(s);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find the index of the bracket that closes the opening bracket at index 0
|
|
142
|
+
* of `s`, honoring quoted strings and nested brackets of the same kind.
|
|
143
|
+
* Returns -1 if `s` doesn't start with a bracket or has no matching close.
|
|
144
|
+
*/
|
|
145
|
+
findMatchingClose(s) {
|
|
146
|
+
const open = s[0];
|
|
147
|
+
if (open !== "{" && open !== "(" && open !== "[") return -1;
|
|
148
|
+
const close = open === "{" ? "}" : open === "(" ? ")" : "]";
|
|
149
|
+
let depth = 0;
|
|
150
|
+
let quote = null;
|
|
151
|
+
for (let i = 0; i < s.length; i++) {
|
|
152
|
+
const c = s[i];
|
|
153
|
+
if (quote) {
|
|
154
|
+
if (c === quote && s[i - 1] !== "\\") quote = null;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (c === "\"" || c === "'") {
|
|
158
|
+
quote = c;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (c === open) depth++;
|
|
162
|
+
else if (c === close) {
|
|
163
|
+
depth--;
|
|
164
|
+
if (depth === 0) return i;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
/** Rewrite an arktype index-signature key like "[string]" into "[key: string]". */
|
|
170
|
+
rewriteIndexKey(rawKey) {
|
|
171
|
+
const m = /^\[(.+)\]$/.exec(rawKey);
|
|
172
|
+
if (!m) return rawKey;
|
|
173
|
+
return `[key: ${this.strip(m[1])}]`;
|
|
174
|
+
}
|
|
175
|
+
/** If `p` is a bracketed group, strip each of its comma-separated members. */
|
|
176
|
+
rewriteGroup(p) {
|
|
177
|
+
const closeIdx = this.findMatchingClose(p);
|
|
178
|
+
if (closeIdx === -1) return p;
|
|
179
|
+
const open = p[0];
|
|
180
|
+
const close = p[closeIdx];
|
|
181
|
+
const inner = p.slice(1, closeIdx);
|
|
182
|
+
const suffix = p.slice(closeIdx + 1);
|
|
183
|
+
if (!/^(?:\[\])*$/.test(suffix)) return p;
|
|
184
|
+
const members = this.split(inner, ",").sort().map((member) => {
|
|
185
|
+
const idx = this.topLevelColon(member);
|
|
186
|
+
if (idx === -1) return this.strip(member);
|
|
187
|
+
const rawKey = member.slice(0, idx).trim();
|
|
188
|
+
const isIndexKey = /^\[.+\]$/.test(rawKey);
|
|
189
|
+
return `${isIndexKey ? this.rewriteIndexKey(rawKey) : member.slice(0, idx + 1)}${isIndexKey ? ":" : ""} ${this.strip(member.slice(idx + 1).trim())}`;
|
|
190
|
+
});
|
|
191
|
+
const arrayDepth = suffix.length / 2;
|
|
192
|
+
if (members.length === 0) {
|
|
193
|
+
const base = `${open}${close}`;
|
|
194
|
+
return arrayDepth > 0 ? this.wrapArray(base, arrayDepth) : base;
|
|
195
|
+
}
|
|
196
|
+
const body = members.join("; ");
|
|
197
|
+
const base = open === "{" ? `{ ${body} }` : `${open}${body}${close}`;
|
|
198
|
+
return arrayDepth > 0 ? this.wrapArray(base, arrayDepth) : base;
|
|
199
|
+
}
|
|
200
|
+
/** Wrap `base` in `Array<...>` `depth` times, instead of appending `[]` suffixes. */
|
|
201
|
+
wrapArray(base, depth) {
|
|
202
|
+
let result = base;
|
|
203
|
+
for (let i = 0; i < depth; i++) result = `Array<${result}>`;
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/SchemaPrinter/YupSchemaPrinter.ts
|
|
209
|
+
var YupSchemaPrinter = class extends SchemaPrinterAbstract {
|
|
210
|
+
print(schema, io) {
|
|
211
|
+
return this.main(schema.describe(), io);
|
|
212
|
+
}
|
|
213
|
+
main(d, io, asKey = false) {
|
|
214
|
+
let base = "";
|
|
215
|
+
if (d.oneOf?.length) base = d.oneOf.filter((v) => v === null || [
|
|
216
|
+
"string",
|
|
217
|
+
"number",
|
|
218
|
+
"boolean"
|
|
219
|
+
].includes(typeof v)).sort().map((v) => this.literal(v)).join(" | ");
|
|
220
|
+
if (base === "") switch (d.type) {
|
|
221
|
+
case "string":
|
|
222
|
+
case "number":
|
|
223
|
+
case "boolean":
|
|
224
|
+
base = d.type;
|
|
225
|
+
break;
|
|
226
|
+
case "date":
|
|
227
|
+
base = "Date";
|
|
228
|
+
break;
|
|
229
|
+
case "array":
|
|
230
|
+
base = d.innerType ? `Array<${this.main(d.innerType, io)}>` : "Array<unknown>";
|
|
231
|
+
break;
|
|
232
|
+
case "tuple":
|
|
233
|
+
base = d.innerType ? `[${d.innerType.map((i) => this.main(i, io)).join(", ")}]` : "Array<unknown>";
|
|
234
|
+
break;
|
|
235
|
+
case "object": {
|
|
236
|
+
const entries = Object.entries(d.fields).sort(([a], [b]) => a.localeCompare(b)).map(([k, f]) => `${this.key(k)}${f.optional ? "?" : ""}: ${this.main(f, io, true)}`);
|
|
237
|
+
base = entries.length === 0 ? "{}" : `{ ${entries.join("; ")} }`;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
default: base = "unknown";
|
|
241
|
+
}
|
|
242
|
+
if (d.optional && !asKey) base += ` | undefined`;
|
|
243
|
+
if (d.nullable) base += ` | null`;
|
|
244
|
+
return base;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/SchemaPrinter/ZodSchemaPrinter.ts
|
|
249
|
+
/** Field types that make an object key optional on the given side. */
|
|
250
|
+
const OPTIONAL_IN = /* @__PURE__ */ new Set([
|
|
251
|
+
"optional",
|
|
252
|
+
"default",
|
|
253
|
+
"prefault"
|
|
254
|
+
]);
|
|
255
|
+
var ZodSchemaPrinter = class extends SchemaPrinterAbstract {
|
|
256
|
+
print(schema, io) {
|
|
257
|
+
return this.main(schema._zod.def, io);
|
|
258
|
+
}
|
|
259
|
+
main(def, io, asKey = false) {
|
|
260
|
+
const inner = (d = def) => this.main(d.innerType._zod.def, io);
|
|
261
|
+
switch (def.type) {
|
|
262
|
+
case "string":
|
|
263
|
+
case "number":
|
|
264
|
+
case "boolean":
|
|
265
|
+
case "bigint":
|
|
266
|
+
case "symbol":
|
|
267
|
+
case "null":
|
|
268
|
+
case "undefined":
|
|
269
|
+
case "void":
|
|
270
|
+
case "any":
|
|
271
|
+
case "unknown":
|
|
272
|
+
case "never": return def.type;
|
|
273
|
+
case "int":
|
|
274
|
+
case "nan": return "number";
|
|
275
|
+
case "date": return "Date";
|
|
276
|
+
case "file": return "File";
|
|
277
|
+
case "literal": return def.values.sort().map((v) => this.literal(v)).join(" | ");
|
|
278
|
+
case "enum": return [...new Set(Object.values(def.entries))].sort().map((v) => this.literal(v)).join(" | ");
|
|
279
|
+
case "optional": return asKey ? inner() : `${inner()} | undefined`;
|
|
280
|
+
case "nullable": return `${inner()} | null`;
|
|
281
|
+
case "nonoptional":
|
|
282
|
+
case "readonly":
|
|
283
|
+
case "catch": return inner();
|
|
284
|
+
case "default":
|
|
285
|
+
case "prefault": return io === "in" && !asKey ? `${inner()} | undefined` : inner();
|
|
286
|
+
case "success": return io === "in" ? inner() : "boolean";
|
|
287
|
+
case "promise": return `Promise<${inner()}>`;
|
|
288
|
+
case "lazy": return this.main(def.getter()._zod.def, io);
|
|
289
|
+
case "array": return `Array<${this.main(def.element._zod.def, io)}>`;
|
|
290
|
+
case "set": return `Set<${this.main(def.valueType._zod.def, io)}>`;
|
|
291
|
+
case "map": return `Map<${this.main(def.keyType._zod.def, io)}, ${this.main(def.valueType._zod.def, io)}>`;
|
|
292
|
+
case "record": return `Record<${this.main(def.keyType._zod.def, io)}, ${this.main(def.valueType._zod.def, io)}>`;
|
|
293
|
+
case "tuple": {
|
|
294
|
+
const items = def.items.map((i) => this.main(i._zod.def, io));
|
|
295
|
+
if (def.rest) items.push(`...Array<${this.main(def.rest._zod.def, io)}>`);
|
|
296
|
+
return `[${items.join(", ")}]`;
|
|
297
|
+
}
|
|
298
|
+
case "union": return def.options.map((o) => this.main(o._zod.def, io)).join(" | ");
|
|
299
|
+
case "intersection": {
|
|
300
|
+
const left = this.main(def.left._zod.def, io);
|
|
301
|
+
const right = this.main(def.right._zod.def, io);
|
|
302
|
+
return this.mergeObjects(left, right) ?? `${this.wrap(left)} & ${this.wrap(right)}`;
|
|
303
|
+
}
|
|
304
|
+
case "pipe": return this.main((io === "in" ? def.in : def.out)._zod.def, io);
|
|
305
|
+
case "object": {
|
|
306
|
+
const entries = Object.entries(def.shape).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => {
|
|
307
|
+
const vd = v._zod.def;
|
|
308
|
+
const optional = vd.type === "optional" || io === "in" && OPTIONAL_IN.has(vd.type);
|
|
309
|
+
return `${this.key(k)}${optional ? "?" : ""}: ${this.main(vd, io, optional)}`;
|
|
310
|
+
});
|
|
311
|
+
if (def.catchall) {
|
|
312
|
+
const ct = this.main(def.catchall._zod.def, io);
|
|
313
|
+
entries.push(`[k: string]: ${ct}`);
|
|
314
|
+
}
|
|
315
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join("; ")} }`;
|
|
316
|
+
}
|
|
317
|
+
case "template_literal": return `\`${def.parts.sort().map((p) => p !== null && typeof p === "object" && "_zod" in p ? `\${${this.main(p._zod.def, io)}}` : String(p)).join("")}\``;
|
|
318
|
+
case "function": return `(...args: ${def.input ? this.main(def.input._zod.def, io) : "[...args: unknown[]]"}) => ${def.output ? this.main(def.output._zod.def, io) : "unknown"}`;
|
|
319
|
+
default: return "unknown";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Flatten `{ a } & { b }` into a single sorted object literal; null if either side isn't a plain object. */
|
|
323
|
+
mergeObjects(a, b) {
|
|
324
|
+
const members = (s) => {
|
|
325
|
+
const m = /^\{\s*([\s\S]*?)\s*\}$/.exec(s.trim());
|
|
326
|
+
if (!m) return null;
|
|
327
|
+
return m[1].length === 0 ? [] : this.split(m[1], ";");
|
|
328
|
+
};
|
|
329
|
+
const ma = members(a);
|
|
330
|
+
const mb = members(b);
|
|
331
|
+
if (!ma || !mb) return null;
|
|
332
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const entry of [...ma, ...mb]) {
|
|
334
|
+
const colon = this.topLevelColon(entry);
|
|
335
|
+
const key = colon === -1 ? entry : entry.slice(0, colon).replace(/\?$/, "").trim();
|
|
336
|
+
byKey.set(key, entry);
|
|
337
|
+
}
|
|
338
|
+
const sorted = [...byKey.entries()].sort(([x], [y]) => x.localeCompare(y)).map(([, v]) => v);
|
|
339
|
+
return sorted.length === 0 ? "{}" : `{ ${sorted.join("; ")} }`;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/SchemaPrinter/index.ts
|
|
344
|
+
var SchemaPrinter = class {
|
|
345
|
+
ark = new ArkSchemaPrinter();
|
|
346
|
+
zod = new ZodSchemaPrinter();
|
|
347
|
+
yup = new YupSchemaPrinter();
|
|
348
|
+
print(schema, io) {
|
|
349
|
+
if (!("~standard" in schema)) return /* @__PURE__ */ new Error(`Schema doesn't have "~standard" property.`);
|
|
350
|
+
if (!("vendor" in schema["~standard"])) return /* @__PURE__ */ new Error(`Schema doesn't have ["~standard"].vendor property.`);
|
|
351
|
+
try {
|
|
352
|
+
switch (schema["~standard"].vendor) {
|
|
353
|
+
case "zod": return this.zod.print(schema, io);
|
|
354
|
+
case "yup": return this.yup.print(schema, io);
|
|
355
|
+
default: return this.ark.print(schema, io);
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
return err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/exports/generateApiClient.ts
|
|
364
|
+
const MODEL_KEYS = [
|
|
365
|
+
"body",
|
|
366
|
+
"search",
|
|
367
|
+
"params",
|
|
368
|
+
"response"
|
|
369
|
+
];
|
|
370
|
+
const CT_GENERIC = `CT extends "json" | "formData" = "json"`;
|
|
371
|
+
const schemaPrinter = new SchemaPrinter();
|
|
372
|
+
const typeToNameMap = /* @__PURE__ */ new Map();
|
|
373
|
+
const toCamelCaseKey = cache("toCamelCaseKey", (endpoint, method, globalPrefix, ignoreGlobalPrefix) => {
|
|
374
|
+
let path = endpoint;
|
|
375
|
+
if (ignoreGlobalPrefix && globalPrefix) {
|
|
376
|
+
const prefixWithSlash = globalPrefix.startsWith("/") ? globalPrefix : `/${globalPrefix}`;
|
|
377
|
+
if (path.startsWith(prefixWithSlash)) path = path.slice(prefixWithSlash.length);
|
|
378
|
+
}
|
|
379
|
+
let result = path.split("/").filter((part) => part.length > 0).map((part, index) => {
|
|
380
|
+
let cleanPart = part.startsWith(":") ? part.substring(1) : part;
|
|
381
|
+
cleanPart = cleanPart.replace(/-([a-zA-Z0-9])/g, (_, char) => {
|
|
382
|
+
return char.toUpperCase();
|
|
383
|
+
});
|
|
384
|
+
cleanPart = cleanPart.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
385
|
+
if (index === 0) return cleanPart;
|
|
386
|
+
return cleanPart.charAt(0).toUpperCase() + cleanPart.slice(1);
|
|
387
|
+
}).join("");
|
|
388
|
+
if (/^\d/.test(result)) result = "_" + result;
|
|
389
|
+
return result + method.slice(0, 1).toUpperCase() + method.slice(1).toLowerCase();
|
|
390
|
+
});
|
|
391
|
+
const toPascalCaseKey = cache("toPascalCaseKey", (endpoint, method, globalPrefix, ignoreGlobalPrefix) => {
|
|
392
|
+
const camel = toCamelCaseKey(endpoint, method, globalPrefix, ignoreGlobalPrefix);
|
|
393
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
394
|
+
});
|
|
395
|
+
const extractParams = cache("extractParams", (endpoint) => {
|
|
396
|
+
const named = endpoint.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g)?.map((p) => p.substring(1)) ?? [];
|
|
397
|
+
if (endpoint.includes("*")) named.push("*");
|
|
398
|
+
return named;
|
|
399
|
+
});
|
|
400
|
+
const getTypeName = cache("getTypeName", (pascal, ns) => {
|
|
401
|
+
if (!ns) return pascal;
|
|
402
|
+
return `${pascal}${toPascalCase(ns)}`;
|
|
403
|
+
});
|
|
404
|
+
const getTypeBody = cache("getTypeBody", (endpoint, params, modelKey, schema) => {
|
|
405
|
+
if (isAbsent(schema)) {
|
|
406
|
+
if (modelKey === "params") {
|
|
407
|
+
if (!isSomeArray(params)) return null;
|
|
408
|
+
return `{ ${params.map((p) => `${p === "*" ? "\"*\"" : p}: primitive`).join("; ")} }`;
|
|
409
|
+
}
|
|
410
|
+
if (modelKey === "search") return `UnknownRecord | undefined`;
|
|
411
|
+
if (modelKey === "response") return `void`;
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const res = schemaPrinter.print(schema, modelKey === "response" ? "out" : "in");
|
|
415
|
+
if (res instanceof Error) {
|
|
416
|
+
logger.error(`ERROR ${endpoint} ${modelKey}`, res);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
if (modelKey === "body") return `CT extends "formData" ? FormData : ${res}`;
|
|
420
|
+
return res;
|
|
421
|
+
});
|
|
422
|
+
const getTypeLine = cache("getTypeLine", (modelKey, name, type, inner = false) => {
|
|
423
|
+
const existing = typeToNameMap.get(type);
|
|
424
|
+
const ib = new StringBuilder();
|
|
425
|
+
if (!existing) typeToNameMap.set(type, name);
|
|
426
|
+
if (!inner) ib.line(`export type ${name}`);
|
|
427
|
+
if (modelKey === "body" && !inner) ib.inline(`<${CT_GENERIC}>`);
|
|
428
|
+
const optional = type.endsWith(`| undefined`) ? `?` : ``;
|
|
429
|
+
ib.inline(inner ? `${modelKey}${optional}: ` : ` = `);
|
|
430
|
+
ib.inline(existing ?? type);
|
|
431
|
+
if (existing && modelKey === "body") ib.inline(`<CT>`);
|
|
432
|
+
return ib.toString();
|
|
433
|
+
});
|
|
434
|
+
function generateApiClient(prefix, routesArr, config) {
|
|
435
|
+
const b = new StringBuilder();
|
|
436
|
+
const routes = routesArr.map((route) => {
|
|
437
|
+
const camelKey = toCamelCaseKey(route.endpoint, route.method, prefix, config.ignoreGlobalPrefix);
|
|
438
|
+
const pascalKey = toPascalCaseKey(route.endpoint, route.method, prefix, config.ignoreGlobalPrefix);
|
|
439
|
+
const params = extractParams(route.endpoint);
|
|
440
|
+
return {
|
|
441
|
+
...route,
|
|
442
|
+
id: route.id,
|
|
443
|
+
camelKey,
|
|
444
|
+
pascalKey,
|
|
445
|
+
params
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
writeBase(b, routes);
|
|
449
|
+
for (const route of routes) writeRouteTypes(b, route);
|
|
450
|
+
for (const route of routes) writeRouteModel(b, route);
|
|
451
|
+
b.line(``);
|
|
452
|
+
b.line(`export const endpoints = {`);
|
|
453
|
+
for (const route of routes) writeEndpoint(b, route);
|
|
454
|
+
b.line(`};`);
|
|
455
|
+
if (!config.apiClient.disabled) {
|
|
456
|
+
b.line(``);
|
|
457
|
+
b.line(`export class ${config.apiClient.exportAs} {`);
|
|
458
|
+
writeApiClientBoilerplate(b, config.apiClient.useStaticClass);
|
|
459
|
+
for (const route of routes) writeApiClientMethod(b, route, config.apiClient.useStaticClass);
|
|
460
|
+
b.line(`}`);
|
|
461
|
+
}
|
|
462
|
+
const content = b.toString();
|
|
463
|
+
const segments = config.output.split("/");
|
|
464
|
+
const dirName = segments.slice(0, -1);
|
|
465
|
+
const fileName = segments[segments.length - 1] ?? "corpus.gen.ts";
|
|
466
|
+
const fpath = path.join(process.cwd(), ...dirName, fileName);
|
|
467
|
+
fs.mkdirSync(path.dirname(fpath), { recursive: true });
|
|
468
|
+
fs.writeFileSync(fpath, content);
|
|
469
|
+
logger.info(`Api Client written to: ${fpath}`);
|
|
470
|
+
}
|
|
471
|
+
function writeBase(b, routes) {
|
|
472
|
+
const wildcardExists = routes.some((r) => r.endpoint.includes("*"));
|
|
473
|
+
b.line(`// #region base`);
|
|
474
|
+
b.line(`type UnknownRecord = Record<string, unknown>;`);
|
|
475
|
+
typeToNameMap.set(`Record<string, unknown>`, `UnknownRecord`);
|
|
476
|
+
b.line(`type primitive = string | number | boolean;`);
|
|
477
|
+
typeToNameMap.set(`string | number | boolean`, `primitive`);
|
|
478
|
+
if (wildcardExists) {
|
|
479
|
+
b.line(`type wildcard = { "*": primitive };`);
|
|
480
|
+
typeToNameMap.set(`{ "*": primitive }`, `wildcard`);
|
|
481
|
+
}
|
|
482
|
+
b.line(`type args<T> = Omit<T, "response"> & { init?: RequestInit; };`);
|
|
483
|
+
b.line(``);
|
|
484
|
+
b.line(`export interface RequestDescriptor {`);
|
|
485
|
+
b.line(1)(`endpoint: string;`);
|
|
486
|
+
b.line(1)(`method: string;`);
|
|
487
|
+
b.line(1)(`body?: unknown;`);
|
|
488
|
+
b.line(1)(`search?: UnknownRecord;`);
|
|
489
|
+
b.line(1)(`init?: RequestInit;`);
|
|
490
|
+
b.line(`}`);
|
|
491
|
+
b.line(`// #endregion`);
|
|
492
|
+
b.line(``);
|
|
493
|
+
}
|
|
494
|
+
function writeRouteTypes(b, route) {
|
|
495
|
+
b.line(``);
|
|
496
|
+
b.line(`// #region ${route.id} types`);
|
|
497
|
+
for (const modelKey of MODEL_KEYS) {
|
|
498
|
+
const schema = route.config?.[modelKey];
|
|
499
|
+
const name = getTypeName(route.pascalKey, modelKey);
|
|
500
|
+
const type = getTypeBody(route.endpoint, route.params, modelKey, schema);
|
|
501
|
+
if (!type) continue;
|
|
502
|
+
b.line(getTypeLine(modelKey, name, type));
|
|
503
|
+
}
|
|
504
|
+
b.line(`// #endregion`);
|
|
505
|
+
}
|
|
506
|
+
function writeRouteModel(b, route) {
|
|
507
|
+
b.line(``);
|
|
508
|
+
b.line(`// #region ${route.id} model`);
|
|
509
|
+
b.line(`export interface ${getTypeName(route.pascalKey, "Model")}`);
|
|
510
|
+
if (!isAbsent(route.config?.body)) b.inline(`<${CT_GENERIC}>`);
|
|
511
|
+
b.inline(` {`);
|
|
512
|
+
for (const modelKey of MODEL_KEYS) {
|
|
513
|
+
const schema = route.config?.[modelKey];
|
|
514
|
+
const name = getTypeName(route.pascalKey, modelKey);
|
|
515
|
+
const type = getTypeBody(route.endpoint, route.params, modelKey, schema);
|
|
516
|
+
if (!type) continue;
|
|
517
|
+
b.line(1)(getTypeLine(modelKey, name, type, true));
|
|
518
|
+
}
|
|
519
|
+
b.line(`}`);
|
|
520
|
+
b.line(`// #endregion`);
|
|
521
|
+
}
|
|
522
|
+
function writeEndpoint(b, route) {
|
|
523
|
+
const modelInterfaceKey = getTypeName(route.pascalKey, "Model");
|
|
524
|
+
const endpoint = isSomeArray(route.params) ? `(p: ${modelInterfaceKey}["params"]) => \`${route.endpoint.split(/:([a-zA-Z_][a-zA-Z0-9_]*)/).map((part, i) => i % 2 === 1 ? `\${String(p.${part})}` : part.replace("*", `\${String(p["*"])}`)).join("")}\`` : `"${route.endpoint}"`;
|
|
525
|
+
b.line(1)(`${route.camelKey}: ${endpoint},`);
|
|
526
|
+
}
|
|
527
|
+
function writeApiClientBoilerplate(b, useStaticClass) {
|
|
528
|
+
const pfx = useStaticClass ? "static" : "public";
|
|
529
|
+
b.line(1)(`constructor(public readonly baseUrl: string) {}`);
|
|
530
|
+
b.line(``);
|
|
531
|
+
b.line(1)(`${pfx} fetchFn: <R>(args: RequestDescriptor) => Promise<R> = async (args) => {`);
|
|
532
|
+
b.line(2)(`const url = new URL(args.endpoint, this.baseUrl);`);
|
|
533
|
+
b.line(2)(`const headers = new Headers(args.init?.headers);`);
|
|
534
|
+
b.line(2)(`const method: RequestInit["method"] = args.method;`);
|
|
535
|
+
b.line(2)(`let body: RequestInit["body"];`);
|
|
536
|
+
b.line(2)(`if (args.search) {`);
|
|
537
|
+
b.line(3)(`for (const [key, val] of Object.entries(args.search)) {`);
|
|
538
|
+
b.line(4)(`if (val == null) continue;`);
|
|
539
|
+
b.line(4)(`url.searchParams.append(key, typeof val === "object"`);
|
|
540
|
+
b.line(5)(`? JSON.stringify(val)`);
|
|
541
|
+
b.line(5)(`: String(val as primitive));`);
|
|
542
|
+
b.line(3)(`}`);
|
|
543
|
+
b.line(2)(`}`);
|
|
544
|
+
b.line(2)(`if (args.body) {`);
|
|
545
|
+
b.line(3)(`if (!headers.has("content-type") && !(args.body instanceof FormData)) {`);
|
|
546
|
+
b.line(4)(`headers.set("content-type", "application/json");`);
|
|
547
|
+
b.line(3)(`}`);
|
|
548
|
+
b.line(3)(`body = args.body instanceof FormData ? args.body : JSON.stringify(args.body);`);
|
|
549
|
+
b.line(2)(`}`);
|
|
550
|
+
b.line(2)(`const req = new Request(url, { method, headers, body, ...args.init });`);
|
|
551
|
+
b.line(2)(`const res = await fetch(req);`);
|
|
552
|
+
b.line(2)(`const contentType = res.headers.get("content-type");`);
|
|
553
|
+
b.line(2)(`const isJson = contentType?.includes("application/json");`);
|
|
554
|
+
b.line(2)(`const isText = contentType?.includes("text/");`);
|
|
555
|
+
b.line(2)(`let data: any;`);
|
|
556
|
+
b.line(2)(`let err: string;`);
|
|
557
|
+
b.line(2)(`if (isJson) {`);
|
|
558
|
+
b.line(3)(`data = await res.json();`);
|
|
559
|
+
b.line(3)(`err = data.message ?? res.statusText;`);
|
|
560
|
+
b.line(3)(`body = args.body instanceof FormData ? args.body : JSON.stringify(args.body);`);
|
|
561
|
+
b.line(2)(`} else if (isText) {`);
|
|
562
|
+
b.line(3)(`data = await res.text();`);
|
|
563
|
+
b.line(3)(`err = data !== "" ? data : res.statusText;`);
|
|
564
|
+
b.line(2)(`} else {`);
|
|
565
|
+
b.line(3)(`data = await res.blob();`);
|
|
566
|
+
b.line(3)(`err = res.statusText;`);
|
|
567
|
+
b.line(2)(`}`);
|
|
568
|
+
b.line(2)(`if (!res.ok) throw new Error(err, { cause: data })`);
|
|
569
|
+
b.line(2)(`return data;`);
|
|
570
|
+
b.line(1)(`}`);
|
|
571
|
+
b.line(``);
|
|
572
|
+
b.line(1)(`${pfx} setFetchFn(cb: <R>(args: RequestDescriptor) => Promise<R>): void {`);
|
|
573
|
+
b.line(2)(`this.fetchFn = cb;`);
|
|
574
|
+
b.line(1)(`}`);
|
|
575
|
+
b.line(``);
|
|
576
|
+
b.line(1)(`${pfx} readonly endpoints = endpoints;`);
|
|
577
|
+
b.line(``);
|
|
578
|
+
}
|
|
579
|
+
function writeApiClientMethod(b, route, useStaticClass) {
|
|
580
|
+
const pfx = useStaticClass ? "static" : "public";
|
|
581
|
+
const endpoint = `this.endpoints.${route.camelKey}${isSomeArray(route.params) ? `(args.params)` : ``}`;
|
|
582
|
+
const generic = !isAbsent(route.config?.body) ? `<${CT_GENERIC}>` : ``;
|
|
583
|
+
const args = `args: args<${getTypeName(route.pascalKey, "Model")}${!isAbsent(route.config?.body) ? `<CT>` : ``}>`;
|
|
584
|
+
b.line(``);
|
|
585
|
+
b.line(1)(`/** ${route.id} */`);
|
|
586
|
+
b.line(1)(`${pfx} ${route.camelKey}${generic}(${args}) {`);
|
|
587
|
+
b.line(2)(`return this.fetchFn<${getTypeName(route.pascalKey, "response")}>({`);
|
|
588
|
+
b.line(3)(`endpoint: ${endpoint},`);
|
|
589
|
+
b.line(3)(`method: ${quote(route.method)},`);
|
|
590
|
+
b.line(3)(`...args,`);
|
|
591
|
+
b.line(2)(`});`);
|
|
592
|
+
b.line(1)(`}`);
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
export { defineConfig, generateApiClient };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://www.schemastore.org/package.json",
|
|
3
|
+
"name": "@ozanarslan/corpus-cli",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "CLI for @ozanarslan/corpus",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ozan Arslan <ozanarslanwork@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ozanArslan2424/corpus.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"corpus": "./dist/cli.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE.txt"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.mjs",
|
|
23
|
+
"./cli": "./dist/cli.mjs",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@standard-schema/spec": "^1.1.0",
|
|
31
|
+
"oxc-parser": "^0.147.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@ozanarslan/corpus": "0.0.1",
|
|
35
|
+
"@types/bun": "^1.4.0",
|
|
36
|
+
"arktype": "^2.2.3",
|
|
37
|
+
"tsdown": "^0.22.14",
|
|
38
|
+
"yup": "^1.7.1",
|
|
39
|
+
"zod": "^4.4.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"dev": "NODE_ENV=development bun run ./src/cli.ts",
|
|
44
|
+
"api": "bun run ./dist/cli.mjs api -m ./test/other/apigen/startServer.ts -o ./test/other/apigen/generated.ts",
|
|
45
|
+
"test:api": "bun api -s && bun run ./test/other/integration.ts -s",
|
|
46
|
+
"test:all": "VALI=arktype bun test:api && VALI=zod bun test:api && VALI=yup bun test:api"
|
|
47
|
+
}
|
|
48
|
+
}
|