@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/cli.mjs
ADDED
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { C as LISTEN_PATTERN, D as NEVER_SCHEMAS, E as NAME_FLAG_HELP, O as PATTERNS, S as INTERFACE_MODEL_PATTERN, T as MODEL_TYPE_PATTERN, _ as isPresent, a as setLoggerNoop, b as EXE_NAME, c as quote, d as toPascalCase, f as assert, g as isObject, h as isEmpty, i as logger, l as toCamelCase, m as isAbsent, o as StringBuilder, r as logFatal, s as resolveCwdPath, t as getConfig, u as toKebabCase, v as isSomeArray, w as MODEL_PATTERN, x as GEN_FUNC, y as APP_NAME } from "./getConfig-lU3I2Ejy.mjs";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { parseSync } from "oxc-parser";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { parseArgs } from "util";
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
9
|
+
//#region src/internal/StringReader.ts
|
|
10
|
+
var StringReader = class StringReader {
|
|
11
|
+
source;
|
|
12
|
+
constructor(source) {
|
|
13
|
+
this.source = source;
|
|
14
|
+
this.state = source;
|
|
15
|
+
}
|
|
16
|
+
state;
|
|
17
|
+
toString() {
|
|
18
|
+
return this.state;
|
|
19
|
+
}
|
|
20
|
+
resolveIndex(searchOrIndex) {
|
|
21
|
+
if (typeof searchOrIndex === "number") return searchOrIndex;
|
|
22
|
+
if (searchOrIndex instanceof RegExp) return this.state.search(searchOrIndex);
|
|
23
|
+
return this.state.indexOf(searchOrIndex);
|
|
24
|
+
}
|
|
25
|
+
resolveLineNumber(searchOrLine) {
|
|
26
|
+
if (typeof searchOrLine === "number") return searchOrLine;
|
|
27
|
+
const index = isObject(searchOrLine) ? searchOrLine.charIndex : this.resolveIndex(searchOrLine);
|
|
28
|
+
if (index === -1) return -1;
|
|
29
|
+
return this.state.slice(0, index).split("\n").length - 1;
|
|
30
|
+
}
|
|
31
|
+
static contains(text, search) {
|
|
32
|
+
return search instanceof RegExp ? search.test(text) : text.includes(search);
|
|
33
|
+
}
|
|
34
|
+
contains(search) {
|
|
35
|
+
return StringReader.contains(this.state, search);
|
|
36
|
+
}
|
|
37
|
+
containsAnyOf(...searches) {
|
|
38
|
+
return searches.some((search) => StringReader.contains(this.state, search));
|
|
39
|
+
}
|
|
40
|
+
containsAllOf(...searches) {
|
|
41
|
+
return searches.every((search) => StringReader.contains(this.state, search));
|
|
42
|
+
}
|
|
43
|
+
getSplitLines() {
|
|
44
|
+
return this.state.split("\n");
|
|
45
|
+
}
|
|
46
|
+
getLineNumberOfCharIndex(charIndex) {
|
|
47
|
+
return this.resolveLineNumber({ charIndex });
|
|
48
|
+
}
|
|
49
|
+
getLineNumber(search) {
|
|
50
|
+
return this.getSplitLines().findIndex((line) => StringReader.contains(line, search));
|
|
51
|
+
}
|
|
52
|
+
getLineNumberAfter(afterSearchOrLine, search) {
|
|
53
|
+
const afterLine = this.resolveLineNumber(afterSearchOrLine);
|
|
54
|
+
return this.getSplitLines().findIndex((line, index) => index > afterLine && StringReader.contains(line, search));
|
|
55
|
+
}
|
|
56
|
+
getLine(searchOrLine) {
|
|
57
|
+
const lineNumber = this.resolveLineNumber(searchOrLine);
|
|
58
|
+
return this.getSplitLines()[lineNumber] ?? "";
|
|
59
|
+
}
|
|
60
|
+
getLineOfCharIndex(charIndex) {
|
|
61
|
+
return this.getLine(this.getLineNumberOfCharIndex(charIndex));
|
|
62
|
+
}
|
|
63
|
+
getCharAt(charIndex) {
|
|
64
|
+
return this.state[charIndex] ?? null;
|
|
65
|
+
}
|
|
66
|
+
getBetween(start, end) {
|
|
67
|
+
const fromIndex = this.resolveIndex(start);
|
|
68
|
+
if (fromIndex === -1) return "";
|
|
69
|
+
const from = typeof start === "string" ? fromIndex + start.length : fromIndex;
|
|
70
|
+
const toIndex = this.state.indexOf(typeof end === "string" ? end : "", from);
|
|
71
|
+
if (toIndex === -1) return "";
|
|
72
|
+
return this.state.slice(from, typeof end === "number" ? end : toIndex).trim();
|
|
73
|
+
}
|
|
74
|
+
getFrom(startOrIndex) {
|
|
75
|
+
const index = this.resolveIndex(startOrIndex);
|
|
76
|
+
if (index === -1) return "";
|
|
77
|
+
const from = typeof startOrIndex === "string" ? index + startOrIndex.length : index;
|
|
78
|
+
return this.state.slice(from);
|
|
79
|
+
}
|
|
80
|
+
getUntil(endOrIndex) {
|
|
81
|
+
const index = this.resolveIndex(endOrIndex);
|
|
82
|
+
if (index === -1) return this.state;
|
|
83
|
+
return this.state.slice(0, index);
|
|
84
|
+
}
|
|
85
|
+
getLinesBetween(startSearchOrLine, endSearchOrLine) {
|
|
86
|
+
const fromLine = this.resolveLineNumber(startSearchOrLine);
|
|
87
|
+
const lines = this.getSplitLines();
|
|
88
|
+
const startLine = lines[fromLine] ?? "";
|
|
89
|
+
if (typeof endSearchOrLine === "string" && startLine.includes(endSearchOrLine)) return [];
|
|
90
|
+
const toLine = typeof endSearchOrLine === "number" ? endSearchOrLine : lines.findIndex((line, index) => index > fromLine && StringReader.contains(line, endSearchOrLine));
|
|
91
|
+
return lines.slice(fromLine + 1, toLine);
|
|
92
|
+
}
|
|
93
|
+
getLinesFrom(startSearchOrLine) {
|
|
94
|
+
const fromLine = this.resolveLineNumber(startSearchOrLine);
|
|
95
|
+
return this.getSplitLines().slice(fromLine);
|
|
96
|
+
}
|
|
97
|
+
getLinesUntil(endSearchOrLine) {
|
|
98
|
+
const toLine = this.resolveLineNumber(endSearchOrLine);
|
|
99
|
+
return this.getSplitLines().slice(0, toLine);
|
|
100
|
+
}
|
|
101
|
+
useBetween(start, end) {
|
|
102
|
+
return new StringReader(this.getBetween(start, end));
|
|
103
|
+
}
|
|
104
|
+
useFrom(startOrIndex) {
|
|
105
|
+
return new StringReader(this.getFrom(startOrIndex));
|
|
106
|
+
}
|
|
107
|
+
useUntil(endOrIndex) {
|
|
108
|
+
return new StringReader(this.getUntil(endOrIndex));
|
|
109
|
+
}
|
|
110
|
+
useLineOfCharIndex(charIndex) {
|
|
111
|
+
return new StringReader(this.getLine(this.getLineNumberOfCharIndex(charIndex)));
|
|
112
|
+
}
|
|
113
|
+
useLinesBetween(startSearchOrLine, endSearchOrLine) {
|
|
114
|
+
return new StringReader(this.getLinesBetween(startSearchOrLine, endSearchOrLine).join("\n"));
|
|
115
|
+
}
|
|
116
|
+
useLinesFrom(startSearchOrLine) {
|
|
117
|
+
return new StringReader(this.getLinesFrom(startSearchOrLine).join("\n"));
|
|
118
|
+
}
|
|
119
|
+
useLinesUntil(endSearchOrLine) {
|
|
120
|
+
return new StringReader(this.getLinesUntil(endSearchOrLine).join("\n"));
|
|
121
|
+
}
|
|
122
|
+
useLine(searchOrLine) {
|
|
123
|
+
return new StringReader(this.getLine(searchOrLine));
|
|
124
|
+
}
|
|
125
|
+
collapse() {
|
|
126
|
+
this.state = this.state.trim().replace(/\s+/g, " ");
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
replace(search, replacement) {
|
|
130
|
+
this.state = this.state.replace(search, replacement);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
replaceAll(search, replacement) {
|
|
134
|
+
this.state = this.state.replaceAll(search, replacement);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
replaceLine(searchOrLine, text) {
|
|
138
|
+
const lineNumber = this.resolveLineNumber(searchOrLine);
|
|
139
|
+
const lines = this.getSplitLines();
|
|
140
|
+
lines[lineNumber] = text;
|
|
141
|
+
this.state = lines.join("\n");
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
modifyLine(searchOrLine, modifier) {
|
|
145
|
+
const lineNumber = this.resolveLineNumber(searchOrLine);
|
|
146
|
+
const lines = this.getSplitLines();
|
|
147
|
+
lines[lineNumber] = modifier(lines[lineNumber] ?? "");
|
|
148
|
+
this.state = lines.join("\n");
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
addLine(text) {
|
|
152
|
+
this.state = this.state + "\n" + text;
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
addToLine(searchOrLine, text) {
|
|
156
|
+
const lineNumber = this.resolveLineNumber(searchOrLine);
|
|
157
|
+
const lines = this.getSplitLines();
|
|
158
|
+
lines[lineNumber] = lines[lineNumber] + text;
|
|
159
|
+
this.state = lines.join("\n");
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
getIndentation(line) {
|
|
163
|
+
const match = line.match(/^[\t ]*/);
|
|
164
|
+
return match ? match[0] : "";
|
|
165
|
+
}
|
|
166
|
+
indentText(text, indent) {
|
|
167
|
+
return text.split("\n").map((line) => line.length > 0 ? indent + line : line).join("\n");
|
|
168
|
+
}
|
|
169
|
+
addBelowLine(searchOrLine, text) {
|
|
170
|
+
const lineNumber = this.resolveLineNumber(searchOrLine);
|
|
171
|
+
const lines = this.getSplitLines();
|
|
172
|
+
const indent = this.getIndentation(lines[lineNumber] ?? "");
|
|
173
|
+
lines.splice(lineNumber + 1, 0, this.indentText(text, indent));
|
|
174
|
+
this.state = lines.join("\n");
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
addAboveLine(searchOrLine, text) {
|
|
178
|
+
const lineNumber = this.resolveLineNumber(searchOrLine);
|
|
179
|
+
const lines = this.getSplitLines();
|
|
180
|
+
const indent = this.getIndentation(lines[lineNumber] ?? "");
|
|
181
|
+
lines.splice(lineNumber, 0, this.indentText(text, indent));
|
|
182
|
+
this.state = lines.join("\n");
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/FileParser/index.ts
|
|
188
|
+
var FileParser = class {
|
|
189
|
+
filePath;
|
|
190
|
+
constructor(filePath) {
|
|
191
|
+
this.filePath = filePath;
|
|
192
|
+
this.reload();
|
|
193
|
+
}
|
|
194
|
+
reload() {
|
|
195
|
+
this._contents = this.initFileContent();
|
|
196
|
+
this._reader = this.initReader();
|
|
197
|
+
this._program = this.initProgram();
|
|
198
|
+
}
|
|
199
|
+
initFileContent() {
|
|
200
|
+
return fs.readFileSync(this.filePath, "utf8");
|
|
201
|
+
}
|
|
202
|
+
initReader() {
|
|
203
|
+
return new StringReader(this.contents);
|
|
204
|
+
}
|
|
205
|
+
initProgram() {
|
|
206
|
+
const { program, errors } = parseSync(this.filePath, this.contents, { sourceType: "module" });
|
|
207
|
+
if (errors.length > 0) {
|
|
208
|
+
logger.error("Parse errors:");
|
|
209
|
+
for (const err of errors) logger.error(err);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
return program;
|
|
213
|
+
}
|
|
214
|
+
_reader;
|
|
215
|
+
get reader() {
|
|
216
|
+
if (isAbsent(this._reader)) this._reader = this.initReader();
|
|
217
|
+
return this._reader;
|
|
218
|
+
}
|
|
219
|
+
_contents;
|
|
220
|
+
get contents() {
|
|
221
|
+
if (isAbsent(this._contents)) this._contents = this.initFileContent();
|
|
222
|
+
return this._contents;
|
|
223
|
+
}
|
|
224
|
+
_program;
|
|
225
|
+
get program() {
|
|
226
|
+
if (isAbsent(this._program)) this._program = this.initProgram();
|
|
227
|
+
return this._program;
|
|
228
|
+
}
|
|
229
|
+
flushReader() {
|
|
230
|
+
fs.writeFileSync(this.filePath, this.reader.toString());
|
|
231
|
+
this.reload();
|
|
232
|
+
}
|
|
233
|
+
getNodeTextContent(node) {
|
|
234
|
+
return this.reader.getBetween(node.start, node.end);
|
|
235
|
+
}
|
|
236
|
+
runCallbackOn(node, cb) {
|
|
237
|
+
this.handleNode(node, cb);
|
|
238
|
+
}
|
|
239
|
+
runCallback(cb) {
|
|
240
|
+
this.handleNode(this.program, cb);
|
|
241
|
+
}
|
|
242
|
+
handleNode(input, cb) {
|
|
243
|
+
if (typeof input !== "object") return;
|
|
244
|
+
if (input === null) return;
|
|
245
|
+
if (!("start" in input)) return;
|
|
246
|
+
if (typeof input.start !== "number") return;
|
|
247
|
+
if (!("end" in input)) return;
|
|
248
|
+
if (typeof input.end !== "number") return;
|
|
249
|
+
if (!("type" in input)) return;
|
|
250
|
+
if (typeof input.type !== "string") return;
|
|
251
|
+
const node = input;
|
|
252
|
+
const result = cb(node, this.reader);
|
|
253
|
+
const nextCb = typeof result === "function" ? result : cb;
|
|
254
|
+
if (node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration") {
|
|
255
|
+
if (node.declaration?.type) node.type = node.declaration?.type;
|
|
256
|
+
}
|
|
257
|
+
for (const inner of Object.values(node)) if (Array.isArray(inner)) for (const ii of inner) this.handleNode(ii, nextCb);
|
|
258
|
+
else this.handleNode(inner, nextCb);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/FileParser/MainFileUpdater.ts
|
|
263
|
+
const above = (kind) => ({
|
|
264
|
+
at: "above",
|
|
265
|
+
kind
|
|
266
|
+
});
|
|
267
|
+
const below = (kind) => ({
|
|
268
|
+
at: "below",
|
|
269
|
+
kind
|
|
270
|
+
});
|
|
271
|
+
const PLACES_BY_KIND = {
|
|
272
|
+
import: [above("import")],
|
|
273
|
+
route: [
|
|
274
|
+
below("route"),
|
|
275
|
+
above("controller"),
|
|
276
|
+
above("middleware"),
|
|
277
|
+
below("service")
|
|
278
|
+
],
|
|
279
|
+
middleware: [
|
|
280
|
+
below("middleware"),
|
|
281
|
+
below("controller"),
|
|
282
|
+
below("route"),
|
|
283
|
+
below("service")
|
|
284
|
+
],
|
|
285
|
+
controller: [
|
|
286
|
+
below("controller"),
|
|
287
|
+
below("route"),
|
|
288
|
+
above("middleware"),
|
|
289
|
+
below("service")
|
|
290
|
+
],
|
|
291
|
+
service: [
|
|
292
|
+
below("service"),
|
|
293
|
+
above("controller"),
|
|
294
|
+
above("route"),
|
|
295
|
+
above("middleware")
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
var MainFileUpdater = class {
|
|
299
|
+
constructor() {
|
|
300
|
+
this.filePath = getConfig().main;
|
|
301
|
+
}
|
|
302
|
+
filePath;
|
|
303
|
+
set = /* @__PURE__ */ new Set();
|
|
304
|
+
reader;
|
|
305
|
+
reload() {
|
|
306
|
+
this.set.clear();
|
|
307
|
+
new FileParser(this.filePath).runCallback((node, reader) => {
|
|
308
|
+
this.reader = reader;
|
|
309
|
+
this.processNode(node);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
flush() {
|
|
313
|
+
fs.writeFileSync(this.filePath, this.reader.toString());
|
|
314
|
+
this.reload();
|
|
315
|
+
}
|
|
316
|
+
addLines(kind, ...lines) {
|
|
317
|
+
this.reload();
|
|
318
|
+
lines = lines.filter((line) => {
|
|
319
|
+
const match = this.reader.getLine(line.trim());
|
|
320
|
+
const pass = isEmpty(match);
|
|
321
|
+
if (!pass) logger.warn(`Skipping existing line: ${line}`);
|
|
322
|
+
return pass;
|
|
323
|
+
});
|
|
324
|
+
if (!isSomeArray(lines)) return;
|
|
325
|
+
const place = this.getPlace(kind);
|
|
326
|
+
if (place.at === "below") this.reader.addBelowLine(place.line, lines.join("\n"));
|
|
327
|
+
else this.reader.addAboveLine(place.line, lines.join("\n"));
|
|
328
|
+
this.flush();
|
|
329
|
+
}
|
|
330
|
+
getKind(line) {
|
|
331
|
+
for (const kind of Object.keys(PATTERNS)) if (PATTERNS[kind].test(line)) return kind;
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
getPlace(kind) {
|
|
335
|
+
const places = PLACES_BY_KIND[kind];
|
|
336
|
+
let index = null;
|
|
337
|
+
let at = "below";
|
|
338
|
+
let i = 0;
|
|
339
|
+
while (isAbsent(index) && i < places.length) {
|
|
340
|
+
const place = places[i];
|
|
341
|
+
if (place) {
|
|
342
|
+
at = place.at;
|
|
343
|
+
if (place.at === "below") index = this.getChunkEnd(place.kind);
|
|
344
|
+
else index = this.getChunkStart(place.kind);
|
|
345
|
+
}
|
|
346
|
+
i++;
|
|
347
|
+
}
|
|
348
|
+
if (isPresent(index)) return {
|
|
349
|
+
at,
|
|
350
|
+
line: this.reader.getLineNumberOfCharIndex(index)
|
|
351
|
+
};
|
|
352
|
+
if (kind === "import") return {
|
|
353
|
+
at: "above",
|
|
354
|
+
line: 0
|
|
355
|
+
};
|
|
356
|
+
return {
|
|
357
|
+
at: "above",
|
|
358
|
+
line: this.reader.getLineNumber(LISTEN_PATTERN)
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
getChunkStart(kind) {
|
|
362
|
+
const vals = Array.from(this.set).filter((it) => it.kind === kind).map((it) => it.start);
|
|
363
|
+
if (!isSomeArray(vals)) return null;
|
|
364
|
+
return Math.min(...vals);
|
|
365
|
+
}
|
|
366
|
+
getChunkEnd(kind) {
|
|
367
|
+
const vals = Array.from(this.set).filter((it) => it.kind === kind).map((it) => it.end);
|
|
368
|
+
if (!isSomeArray(vals)) return null;
|
|
369
|
+
return Math.max(...vals);
|
|
370
|
+
}
|
|
371
|
+
processNode(node) {
|
|
372
|
+
switch (node.type) {
|
|
373
|
+
case "ImportDeclaration":
|
|
374
|
+
this.set.add({
|
|
375
|
+
kind: "import",
|
|
376
|
+
text: this.reader.getBetween(node.start, node.end),
|
|
377
|
+
start: node.start,
|
|
378
|
+
end: node.end
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
case "ExpressionStatement":
|
|
382
|
+
case "VariableDeclaration": for (const [kind, pattern] of Object.entries(PATTERNS)) {
|
|
383
|
+
if (kind === "import") continue;
|
|
384
|
+
const content = this.reader.useBetween(node.start, node.end);
|
|
385
|
+
if (content.useLine(0).contains(pattern)) this.set.add({
|
|
386
|
+
kind,
|
|
387
|
+
text: content.toString(),
|
|
388
|
+
start: node.start,
|
|
389
|
+
end: node.end
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/Config/getTsConfig.ts
|
|
397
|
+
function getTsConfig() {
|
|
398
|
+
const tsconfigPath = resolveCwdPath("tsconfig.json");
|
|
399
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
400
|
+
logger.log(`No tsconfig.json found.`);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/Importable/index.ts
|
|
407
|
+
var Importable = class {
|
|
408
|
+
resourceName;
|
|
409
|
+
kind;
|
|
410
|
+
constructor(resourceName, kind) {
|
|
411
|
+
this.resourceName = resourceName;
|
|
412
|
+
this.kind = kind;
|
|
413
|
+
this.config = getConfig();
|
|
414
|
+
this.targetDirPath = this.resolveTargetDir();
|
|
415
|
+
}
|
|
416
|
+
config;
|
|
417
|
+
targetDirPath;
|
|
418
|
+
get name() {
|
|
419
|
+
return `${this.resourceName}-${this.kind}`;
|
|
420
|
+
}
|
|
421
|
+
get pascalName() {
|
|
422
|
+
return toPascalCase(this.name);
|
|
423
|
+
}
|
|
424
|
+
get camelName() {
|
|
425
|
+
return toCamelCase(this.name);
|
|
426
|
+
}
|
|
427
|
+
get filePath() {
|
|
428
|
+
const relPath = (this.config.folderStructure?.[this.kind] ?? "{resource}/{resource}-{kind}.ts").replaceAll("{resource}", this.resourceName).replaceAll("{kind}", this.kind).split("/").map((segment) => {
|
|
429
|
+
const ext = path.extname(segment);
|
|
430
|
+
const base = ext ? segment.slice(0, -ext.length) : segment;
|
|
431
|
+
return this.convertCase(base) + ext;
|
|
432
|
+
}).join("/");
|
|
433
|
+
return path.join(path.relative(process.cwd(), this.targetDirPath), relPath);
|
|
434
|
+
}
|
|
435
|
+
parseFile(cb) {
|
|
436
|
+
return new FileParser(this.filePath).runCallback(cb);
|
|
437
|
+
}
|
|
438
|
+
importFrom(inFile) {
|
|
439
|
+
const alias = this.resolveAliasFor(this.filePath);
|
|
440
|
+
if (alias) return alias;
|
|
441
|
+
const rel = path.relative(path.dirname(inFile), this.filePath).replace(/\.ts$/, "");
|
|
442
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
443
|
+
}
|
|
444
|
+
resolveAliasFor(filePath) {
|
|
445
|
+
const tsconfig = getTsConfig();
|
|
446
|
+
if (!tsconfig) return null;
|
|
447
|
+
const paths = tsconfig.compilerOptions?.paths ?? {};
|
|
448
|
+
const fileNoExt = filePath.replace(/\.ts$/, "").replace(/^\.\//, "");
|
|
449
|
+
let bestAlias = null;
|
|
450
|
+
let bestTargetLen = -1;
|
|
451
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
452
|
+
const targetDir = (targets[0] ?? "").replace(/\/\*$/, "").replace(/^\.\//, "");
|
|
453
|
+
if (!fileNoExt.startsWith(targetDir)) continue;
|
|
454
|
+
if (targetDir.length <= bestTargetLen) continue;
|
|
455
|
+
bestTargetLen = targetDir.length;
|
|
456
|
+
const aliasPrefix = alias.replace(/\/\*$/, "");
|
|
457
|
+
const rest = fileNoExt.slice(targetDir.length).replace(/^\//, "");
|
|
458
|
+
bestAlias = rest ? `${aliasPrefix}/${rest}` : aliasPrefix;
|
|
459
|
+
}
|
|
460
|
+
return bestAlias;
|
|
461
|
+
}
|
|
462
|
+
resolveTargetDir() {
|
|
463
|
+
const mainPath = resolveCwdPath(this.config.main);
|
|
464
|
+
if (!fs.existsSync(mainPath)) logFatal(`Could not find main file at ${mainPath}.`);
|
|
465
|
+
return path.dirname(mainPath);
|
|
466
|
+
}
|
|
467
|
+
convertCase(s) {
|
|
468
|
+
switch (this.config.casing) {
|
|
469
|
+
case "pascal":
|
|
470
|
+
default: return toPascalCase(s);
|
|
471
|
+
case "camel": return toCamelCase(s);
|
|
472
|
+
case "kebab": return toKebabCase(s);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/internal/parseModelDefinition.ts
|
|
478
|
+
function parseModelDefinition(model) {
|
|
479
|
+
let modelName = "";
|
|
480
|
+
let modelTypeName = "";
|
|
481
|
+
const modelDef = {};
|
|
482
|
+
model.parseFile((node, reader) => {
|
|
483
|
+
const firstLn = reader.useBetween(node.start, node.end).useUntil("\n");
|
|
484
|
+
const isModel = firstLn.contains(MODEL_PATTERN);
|
|
485
|
+
function str(node) {
|
|
486
|
+
return reader.getBetween(node.start, node.end);
|
|
487
|
+
}
|
|
488
|
+
if (isModel) {
|
|
489
|
+
if (node.type === "ClassDeclaration") {
|
|
490
|
+
modelName = node.id?.name ?? "";
|
|
491
|
+
for (const member of node.body.body) if (member.type === "MethodDefinition" && member.static && member.kind === "get") {
|
|
492
|
+
const returns = member.value.body?.body.find((b) => b.type === "ReturnStatement");
|
|
493
|
+
if (returns?.argument?.type !== "ObjectExpression") continue;
|
|
494
|
+
modelDef[str(member.key)] = {};
|
|
495
|
+
for (const prop of returns.argument.properties) {
|
|
496
|
+
if (prop.type === "SpreadElement") continue;
|
|
497
|
+
if (prop.value) modelDef[str(member.key)][str(prop.key)] = str(prop.value);
|
|
498
|
+
}
|
|
499
|
+
} else if (member.type === "PropertyDefinition" && member.static) {
|
|
500
|
+
if (member.value?.type !== "ObjectExpression") continue;
|
|
501
|
+
modelDef[str(member.key)] = {};
|
|
502
|
+
for (const prop of member.value.properties) {
|
|
503
|
+
if (prop.type === "SpreadElement") continue;
|
|
504
|
+
if (prop.value) modelDef[str(member.key)][str(prop.key)] = str(prop.value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} else if (node.type === "VariableDeclaration") {
|
|
508
|
+
const decl = node.declarations.find((decl) => decl.init?.type === "ObjectExpression");
|
|
509
|
+
if (decl?.id.type === "Identifier") modelName = decl.id.name;
|
|
510
|
+
const obj = decl?.init;
|
|
511
|
+
if (!obj || obj.type !== "ObjectExpression") return;
|
|
512
|
+
for (const prop of obj.properties) {
|
|
513
|
+
if (prop.type === "SpreadElement") continue;
|
|
514
|
+
if (prop.value?.type !== "ObjectExpression") continue;
|
|
515
|
+
modelDef[str(prop.key)] = {};
|
|
516
|
+
for (const prop2 of prop.value.properties) {
|
|
517
|
+
if (prop2.type === "SpreadElement") continue;
|
|
518
|
+
if (prop2.value) modelDef[str(prop.key)][str(prop2.key)] = str(prop2.value);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (firstLn.contains(MODEL_TYPE_PATTERN)) modelTypeName = firstLn.toString().match(MODEL_TYPE_PATTERN)?.[1] ?? "";
|
|
524
|
+
if (node.type === "TSInterfaceDeclaration") {
|
|
525
|
+
const match = firstLn.toString().match(INTERFACE_MODEL_PATTERN);
|
|
526
|
+
if (match?.[1]) {
|
|
527
|
+
modelTypeName = match[1];
|
|
528
|
+
for (const member of node.body.body) {
|
|
529
|
+
if (member.type !== "TSPropertySignature") continue;
|
|
530
|
+
if (!member.typeAnnotation) continue;
|
|
531
|
+
const key = str(member.key);
|
|
532
|
+
const typeNode = member.typeAnnotation.typeAnnotation;
|
|
533
|
+
if (typeNode.type !== "TSTypeLiteral") continue;
|
|
534
|
+
modelDef[key] = {};
|
|
535
|
+
for (const inner of typeNode.members) {
|
|
536
|
+
if (inner.type !== "TSPropertySignature") continue;
|
|
537
|
+
if (!inner.typeAnnotation) continue;
|
|
538
|
+
const innerKey = str(inner.key);
|
|
539
|
+
modelDef[key][innerKey] = str(inner.typeAnnotation.typeAnnotation);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
return {
|
|
546
|
+
modelName,
|
|
547
|
+
modelTypeName,
|
|
548
|
+
modelDef
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/Modules/ModuleAbstract.ts
|
|
553
|
+
var ModuleAbstract = class {
|
|
554
|
+
constructor() {
|
|
555
|
+
this.config = getConfig();
|
|
556
|
+
}
|
|
557
|
+
config;
|
|
558
|
+
flags = {
|
|
559
|
+
name: null,
|
|
560
|
+
empty: false
|
|
561
|
+
};
|
|
562
|
+
passedKey = "";
|
|
563
|
+
messages = [];
|
|
564
|
+
addMessage(msg) {
|
|
565
|
+
this.messages.push(msg);
|
|
566
|
+
}
|
|
567
|
+
async run() {
|
|
568
|
+
const shutdown = async (code, err) => {
|
|
569
|
+
await this.stop();
|
|
570
|
+
if (err) logger.error(String(err));
|
|
571
|
+
process.exit(code);
|
|
572
|
+
};
|
|
573
|
+
process.once("SIGINT", () => void shutdown(130));
|
|
574
|
+
process.once("SIGTERM", () => void shutdown(143));
|
|
575
|
+
process.once("uncaughtException", (err) => void shutdown(1, err));
|
|
576
|
+
process.once("unhandledRejection", (err) => void shutdown(1, err));
|
|
577
|
+
this.parseFlags();
|
|
578
|
+
logger.info(`Running: ${this.passedKey}`);
|
|
579
|
+
if (this.config.silent) setLoggerNoop();
|
|
580
|
+
await this.main();
|
|
581
|
+
}
|
|
582
|
+
stopped = false;
|
|
583
|
+
async stop() {
|
|
584
|
+
logger.info(`Stopping: ${this.passedKey}`);
|
|
585
|
+
if (this.stopped) return;
|
|
586
|
+
this.stopped = true;
|
|
587
|
+
for (const msg of this.messages) logger.warn(msg);
|
|
588
|
+
}
|
|
589
|
+
printHelp(exitCode) {
|
|
590
|
+
process.stdout.write(this.help.join("\n"));
|
|
591
|
+
process.exit(exitCode);
|
|
592
|
+
}
|
|
593
|
+
parseFlags() {
|
|
594
|
+
this.passedKey = process.argv.slice(2)[0] ?? "";
|
|
595
|
+
const { values, positionals } = parseArgs({
|
|
596
|
+
args: process.argv.slice(3),
|
|
597
|
+
options: {
|
|
598
|
+
help: {
|
|
599
|
+
type: "boolean",
|
|
600
|
+
short: "h"
|
|
601
|
+
},
|
|
602
|
+
main: {
|
|
603
|
+
type: "string",
|
|
604
|
+
short: "m"
|
|
605
|
+
},
|
|
606
|
+
silent: {
|
|
607
|
+
type: "boolean",
|
|
608
|
+
short: "s",
|
|
609
|
+
default: false
|
|
610
|
+
},
|
|
611
|
+
name: {
|
|
612
|
+
type: "string",
|
|
613
|
+
short: "n"
|
|
614
|
+
},
|
|
615
|
+
output: {
|
|
616
|
+
type: "string",
|
|
617
|
+
short: "o"
|
|
618
|
+
},
|
|
619
|
+
empty: {
|
|
620
|
+
type: "boolean",
|
|
621
|
+
short: "e",
|
|
622
|
+
default: false
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
allowPositionals: true
|
|
626
|
+
});
|
|
627
|
+
if (values.help) this.printHelp(0);
|
|
628
|
+
this.flags.name = values.name ?? positionals[0] ?? null;
|
|
629
|
+
this.flags.empty = values.empty;
|
|
630
|
+
if (isPresent(values.silent)) this.config.silent = values.silent;
|
|
631
|
+
if (isPresent(values.main)) this.config.main = values.main;
|
|
632
|
+
if (isPresent(values.output)) this.config.output = values.output;
|
|
633
|
+
}
|
|
634
|
+
async promptConfirm(question) {
|
|
635
|
+
const rl = createInterface({
|
|
636
|
+
input: process.stdin,
|
|
637
|
+
output: process.stdout
|
|
638
|
+
});
|
|
639
|
+
const answer = await rl.question(question + " (y/n) ");
|
|
640
|
+
rl.close();
|
|
641
|
+
const confirmed = /^y(es)?$/i.test(answer.trim());
|
|
642
|
+
if (!confirmed) process.exit(0);
|
|
643
|
+
return confirmed;
|
|
644
|
+
}
|
|
645
|
+
readFile(segments) {
|
|
646
|
+
try {
|
|
647
|
+
return fs.readFileSync(resolveCwdPath(...segments), "utf8");
|
|
648
|
+
} catch {
|
|
649
|
+
return "";
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
checkFileExists(segments) {
|
|
653
|
+
return fs.existsSync(resolveCwdPath(...segments));
|
|
654
|
+
}
|
|
655
|
+
writeFile(content, segments) {
|
|
656
|
+
return this.writeToAbsolutePath(content, resolveCwdPath(...segments));
|
|
657
|
+
}
|
|
658
|
+
writeToAbsolutePath(content, absPath) {
|
|
659
|
+
const exists = fs.existsSync(absPath);
|
|
660
|
+
const cleanPath = absPath.replace(process.cwd(), "");
|
|
661
|
+
if (exists) {
|
|
662
|
+
logger.warn(`NOT WRITTEN: File exists at ${cleanPath}.`);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
666
|
+
fs.writeFileSync(absPath, content);
|
|
667
|
+
logger.info(`Writing file: ${cleanPath}`);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/Modules/AddControllerModule.ts
|
|
672
|
+
var AddControllerModule = class extends ModuleAbstract {
|
|
673
|
+
mainFileUpdater;
|
|
674
|
+
constructor(mainFileUpdater) {
|
|
675
|
+
super();
|
|
676
|
+
this.mainFileUpdater = mainFileUpdater;
|
|
677
|
+
}
|
|
678
|
+
keys = ["controller", "ctrl"];
|
|
679
|
+
get help() {
|
|
680
|
+
return [
|
|
681
|
+
"Scaffold a standalone controller with stubbed CRUD routes.",
|
|
682
|
+
"",
|
|
683
|
+
`Usage: ${EXE_NAME} ${this.keys.join("|")} ${NAME_FLAG_HELP}`,
|
|
684
|
+
"",
|
|
685
|
+
"Options:",
|
|
686
|
+
` ${NAME_FLAG_HELP} Name of the controller to generate.`,
|
|
687
|
+
"",
|
|
688
|
+
"Note: this only generates the controller file. Without a matching",
|
|
689
|
+
"model and service, the stubbed routes will be untyped and just throw."
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
main() {
|
|
693
|
+
const name = this.flags.name;
|
|
694
|
+
assert(name, `name is required.\n\t${EXE_NAME} ${this.passedKey} ${NAME_FLAG_HELP}`);
|
|
695
|
+
const controller = new Importable(name, "controller");
|
|
696
|
+
const model = new Importable(name, "model");
|
|
697
|
+
const service = new Importable(name, "service");
|
|
698
|
+
this.writeFile(this.buildControllerFile(controller, model, service), [controller.filePath]);
|
|
699
|
+
this.mainFileUpdater.addLines("import", `import { ${controller.pascalName} } from "${controller.importFrom(this.config.main)}";`);
|
|
700
|
+
this.mainFileUpdater.addLines("controller", `new ${controller.pascalName}(${service.camelName});`);
|
|
701
|
+
}
|
|
702
|
+
buildControllerFile(controller, model, service) {
|
|
703
|
+
const modelExists = fs.existsSync(model.filePath);
|
|
704
|
+
const serviceExists = fs.existsSync(service.filePath);
|
|
705
|
+
if (modelExists && serviceExists) return this.buildControllerFileWithModel(controller, model, service);
|
|
706
|
+
if (this.flags.empty) return this.buildEmptyControllerFile(controller);
|
|
707
|
+
return this.buildControllerFileWithDefaults(controller);
|
|
708
|
+
}
|
|
709
|
+
buildEmptyControllerFile(controller) {
|
|
710
|
+
const b = new StringBuilder();
|
|
711
|
+
b.line(`import { C } from "${this.config.pkgPath}";`);
|
|
712
|
+
b.line("");
|
|
713
|
+
b.line(`export class ${controller.pascalName} extends C.Controller {`);
|
|
714
|
+
b.line(1)(`constructor(private readonly service: unknown) {`);
|
|
715
|
+
b.line(2)(`super();`);
|
|
716
|
+
b.line(1)(`}`);
|
|
717
|
+
b.line("");
|
|
718
|
+
b.line(1)(`override prefix = "/${controller.resourceName}";`);
|
|
719
|
+
b.line(`}`);
|
|
720
|
+
return b.toString();
|
|
721
|
+
}
|
|
722
|
+
buildControllerFileWithDefaults(controller) {
|
|
723
|
+
const b = new StringBuilder();
|
|
724
|
+
const methods = this.config.defaultMethods;
|
|
725
|
+
b.line(`import { C } from "${this.config.pkgPath}";`);
|
|
726
|
+
b.line("");
|
|
727
|
+
b.line(`export class ${controller.pascalName} extends C.Controller {`);
|
|
728
|
+
b.line(1)(`constructor(private readonly service: unknown) {`);
|
|
729
|
+
b.line(2)(`super();`);
|
|
730
|
+
b.line(1)(`}`);
|
|
731
|
+
b.line("");
|
|
732
|
+
b.line(1)(`override prefix = "/${controller.resourceName}";`);
|
|
733
|
+
for (const { propertyKey, address } of Object.values(methods)) {
|
|
734
|
+
b.line("");
|
|
735
|
+
b.line(1)(`${propertyKey} = this.route(${quote(address)}, (c) => { throw new Error("Method not implemented."); });`);
|
|
736
|
+
}
|
|
737
|
+
b.line(`}`);
|
|
738
|
+
return b.toString();
|
|
739
|
+
}
|
|
740
|
+
buildControllerFileWithModel(controller, model, service) {
|
|
741
|
+
const b = new StringBuilder();
|
|
742
|
+
const { modelName, modelTypeName, modelDef } = parseModelDefinition(model);
|
|
743
|
+
const noValLib = isAbsent(this.config.validationLibrary);
|
|
744
|
+
b.line(`import { C } from "${this.config.pkgPath}";`);
|
|
745
|
+
b.line(`import ${noValLib ? "type " : ""}{ ${noValLib ? modelTypeName : modelName} } from "${model.importFrom(controller.filePath)}";`);
|
|
746
|
+
b.line(`import { ${service.pascalName} } from "${service.importFrom(controller.filePath)}";`);
|
|
747
|
+
b.line("");
|
|
748
|
+
b.line(`export class ${controller.pascalName} extends C.Controller {`);
|
|
749
|
+
b.line(1)(`constructor(private readonly service: ${service.pascalName}) {`);
|
|
750
|
+
b.line(2)(`super();`);
|
|
751
|
+
b.line(1)(`}`);
|
|
752
|
+
b.line("");
|
|
753
|
+
b.line(1)(`override prefix = "/${controller.resourceName}";`);
|
|
754
|
+
for (const [key, val] of Object.entries(modelDef)) {
|
|
755
|
+
const ORDER = [
|
|
756
|
+
"body",
|
|
757
|
+
"search",
|
|
758
|
+
"params",
|
|
759
|
+
"response"
|
|
760
|
+
];
|
|
761
|
+
const callArgs = [
|
|
762
|
+
"search",
|
|
763
|
+
"params",
|
|
764
|
+
"body"
|
|
765
|
+
].filter((k) => k in val && !this.isNeverSchema(val[k])).map((k) => `c.${k}`).join(", ");
|
|
766
|
+
const generics = noValLib ? `<${ORDER.map((acc) => `\n\t\t${modelTypeName}["${key}"]["${acc}"]`).join(",")}\n\t>` : "";
|
|
767
|
+
const address = this.resolveAddress(val);
|
|
768
|
+
const baseArgs = `${quote(address)}, (c) => this.service.${key}(${callArgs})`;
|
|
769
|
+
const validatorArg = noValLib ? "" : `, ${modelName}.${key}`;
|
|
770
|
+
b.line("");
|
|
771
|
+
b.line(1)(`${key} = this.route${generics}(${baseArgs}${validatorArg});`);
|
|
772
|
+
}
|
|
773
|
+
b.line(`}`);
|
|
774
|
+
return b.toString();
|
|
775
|
+
}
|
|
776
|
+
isNeverSchema = (schema) => NEVER_SCHEMAS.has(schema.trim());
|
|
777
|
+
hasSchema(model, key) {
|
|
778
|
+
return key in model && typeof model[key] === "string" && !NEVER_SCHEMAS.has(model[key].trim());
|
|
779
|
+
}
|
|
780
|
+
resolveAddress(model) {
|
|
781
|
+
let method = "GET";
|
|
782
|
+
let endpoint = "/";
|
|
783
|
+
const hasBody = this.hasSchema(model, "body");
|
|
784
|
+
const hasParams = this.hasSchema(model, "params");
|
|
785
|
+
if (hasBody && hasParams) method = "PUT";
|
|
786
|
+
else if (hasBody && !hasParams) method = "POST";
|
|
787
|
+
else if (!hasBody && hasParams) method = "DELETE";
|
|
788
|
+
if (hasParams && typeof model.params === "string") {
|
|
789
|
+
const keys = this.extractParamKeys(model.params);
|
|
790
|
+
if (keys.length > 0) endpoint = "/" + keys.map((k) => `:${k}`).join("/");
|
|
791
|
+
}
|
|
792
|
+
return `${method} ${endpoint}`;
|
|
793
|
+
}
|
|
794
|
+
extractParamKeys(paramsStr) {
|
|
795
|
+
const keyRegex = /["']?([A-Za-z_$][A-Za-z0-9_$]*)["']?\s*:/g;
|
|
796
|
+
const keys = [];
|
|
797
|
+
let match;
|
|
798
|
+
while ((match = keyRegex.exec(paramsStr)) !== null) if (!isAbsent(match[1])) keys.push(match[1]);
|
|
799
|
+
return keys;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/Modules/AddExceptionModule.ts
|
|
804
|
+
var AddExceptionModule = class extends ModuleAbstract {
|
|
805
|
+
keys = ["exception", "exc"];
|
|
806
|
+
get help() {
|
|
807
|
+
return [
|
|
808
|
+
"Scaffold a standalone exception class with a default NotImplemented exception.",
|
|
809
|
+
"",
|
|
810
|
+
`Usage: ${EXE_NAME} ${this.keys.join("|")} ${NAME_FLAG_HELP}`,
|
|
811
|
+
"",
|
|
812
|
+
"Options:",
|
|
813
|
+
` ${NAME_FLAG_HELP} Name of the exception class to generate.`,
|
|
814
|
+
" --empty Generate a bare exception class with no default exceptions.",
|
|
815
|
+
"",
|
|
816
|
+
"Note: this only generates the exception file and does not touch any other files."
|
|
817
|
+
];
|
|
818
|
+
}
|
|
819
|
+
main() {
|
|
820
|
+
const name = this.flags.name;
|
|
821
|
+
assert(name, `name is required.\n\t${EXE_NAME} ${this.passedKey} ${NAME_FLAG_HELP}`);
|
|
822
|
+
const exception = new Importable(name, "exception");
|
|
823
|
+
this.writeFile(this.buildExceptionFile(exception), [exception.filePath]);
|
|
824
|
+
}
|
|
825
|
+
buildExceptionFile(exception) {
|
|
826
|
+
if (this.flags.empty) return this.buildEmptyExceptionFile(exception);
|
|
827
|
+
return this.buildExceptionFileWithDefaults(exception);
|
|
828
|
+
}
|
|
829
|
+
buildEmptyExceptionFile(exception) {
|
|
830
|
+
const b = new StringBuilder();
|
|
831
|
+
b.line(`export class ${exception.pascalName} {`);
|
|
832
|
+
b.line(`}`);
|
|
833
|
+
return b.toString();
|
|
834
|
+
}
|
|
835
|
+
buildExceptionFileWithDefaults(exception) {
|
|
836
|
+
const b = new StringBuilder();
|
|
837
|
+
b.line(`import { C } from ${quote(this.config.pkgPath)};`);
|
|
838
|
+
b.line("");
|
|
839
|
+
b.line(`export class ${exception.pascalName} {`);
|
|
840
|
+
b.line(1)(`static NotImplemented = new C.Exception("NotImplemented", C.Status.INTERNAL_SERVER_ERROR);`);
|
|
841
|
+
b.line(`}`);
|
|
842
|
+
return b.toString();
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region src/Modules/AddModelModule.ts
|
|
847
|
+
var AddModelModule = class extends ModuleAbstract {
|
|
848
|
+
keys = ["model", "mdl"];
|
|
849
|
+
get help() {
|
|
850
|
+
return [
|
|
851
|
+
"Scaffold a standalone model with a default CRUD-shaped interface or schema.",
|
|
852
|
+
"",
|
|
853
|
+
`Usage: ${EXE_NAME} ${this.keys.join("|")} ${NAME_FLAG_HELP}`,
|
|
854
|
+
"",
|
|
855
|
+
"Options:",
|
|
856
|
+
` ${NAME_FLAG_HELP} Name of the model to generate.`,
|
|
857
|
+
" --empty Generate a bare model with no default CRUD shape.",
|
|
858
|
+
"",
|
|
859
|
+
"Note: this only generates the model file and does not touch any other files."
|
|
860
|
+
];
|
|
861
|
+
}
|
|
862
|
+
main() {
|
|
863
|
+
const name = this.flags.name;
|
|
864
|
+
assert(name, `name is required.\n\t${EXE_NAME} ${this.passedKey} ${NAME_FLAG_HELP}`);
|
|
865
|
+
const model = new Importable(name, "model");
|
|
866
|
+
const modelTypeName = `${model.pascalName}Type`;
|
|
867
|
+
this.writeFile(this.buildModelFile(model, modelTypeName), [model.filePath]);
|
|
868
|
+
}
|
|
869
|
+
buildModelFile(model, modelTypeName) {
|
|
870
|
+
if (this.flags.empty) return this.buildEmptyModelFile(model);
|
|
871
|
+
return this.buildModelFileWithDefaults(model, modelTypeName);
|
|
872
|
+
}
|
|
873
|
+
buildEmptyModelFile(model) {
|
|
874
|
+
const b = new StringBuilder();
|
|
875
|
+
b.line(`export interface ${model.pascalName}Type {`);
|
|
876
|
+
b.line(1)(`entity: {`);
|
|
877
|
+
b.line(2)(`id: string;`);
|
|
878
|
+
b.line(1)(`};`);
|
|
879
|
+
b.line(`}`);
|
|
880
|
+
return b.toString();
|
|
881
|
+
}
|
|
882
|
+
buildModelFileWithDefaults(model, modelTypeName) {
|
|
883
|
+
const ms = this.config.defaultMethods;
|
|
884
|
+
const validationLibrary = this.config.validationLibrary;
|
|
885
|
+
const type = (methodKey, ...accessors) => {
|
|
886
|
+
const key = methodKey === "entity" ? "entity" : ms[methodKey].propertyKey;
|
|
887
|
+
return `${modelTypeName}[${quote(key)}]${accessors.map((accessor) => `[${quote(accessor)}]`).join("")}`;
|
|
888
|
+
};
|
|
889
|
+
if (isAbsent(validationLibrary)) return `export interface ${modelTypeName} {
|
|
890
|
+
entity: {
|
|
891
|
+
id: string;
|
|
892
|
+
name: string;
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
${ms.get.propertyKey}: {
|
|
896
|
+
search: {
|
|
897
|
+
page?: number;
|
|
898
|
+
limit?: number;
|
|
899
|
+
};
|
|
900
|
+
response: {
|
|
901
|
+
data: Array<${type("entity")}>;
|
|
902
|
+
page: number;
|
|
903
|
+
limit: number;
|
|
904
|
+
count: number;
|
|
905
|
+
totalCount: number;
|
|
906
|
+
pageCount: number;
|
|
907
|
+
};
|
|
908
|
+
params: never;
|
|
909
|
+
body: never;
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
${ms.getByParams.propertyKey}: {
|
|
913
|
+
params: {
|
|
914
|
+
id: string;
|
|
915
|
+
};
|
|
916
|
+
response: ${type("entity")};
|
|
917
|
+
search: never;
|
|
918
|
+
body: never;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
${ms.create.propertyKey}: {
|
|
922
|
+
body: {
|
|
923
|
+
name: string;
|
|
924
|
+
};
|
|
925
|
+
response: ${type("entity")};
|
|
926
|
+
search: never;
|
|
927
|
+
params: never;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
${ms.update.propertyKey}: {
|
|
931
|
+
body: Partial<${type("create", "body")}>;
|
|
932
|
+
response: ${type("entity")};
|
|
933
|
+
params: ${type("getByParams", "params")};
|
|
934
|
+
search: never;
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
${ms.remove.propertyKey}: {
|
|
938
|
+
params: ${type("getByParams", "params")};
|
|
939
|
+
response: void;
|
|
940
|
+
body: never;
|
|
941
|
+
search: never;
|
|
942
|
+
};
|
|
943
|
+
};`;
|
|
944
|
+
const schemas = this.getSchemas(validationLibrary);
|
|
945
|
+
return `${schemas.import}
|
|
946
|
+
import type { C } from ${quote(this.config.pkgPath)};
|
|
947
|
+
|
|
948
|
+
export type ${modelTypeName} = C.InferModel<typeof ${model.pascalName}>
|
|
949
|
+
|
|
950
|
+
export abstract class ${model.pascalName} {${validationLibrary === "yup" ? `\n\tstatic readonly never = y.mixed().oneOf([undefined] as const);\n` : ``}
|
|
951
|
+
static readonly entity = ${schemas.entity};
|
|
952
|
+
|
|
953
|
+
static readonly ${ms.get.propertyKey} = ${schemas.get};
|
|
954
|
+
|
|
955
|
+
static readonly ${ms.getByParams.propertyKey} = ${schemas.getByParams};
|
|
956
|
+
|
|
957
|
+
static readonly ${ms.create.propertyKey} = ${schemas.create};
|
|
958
|
+
|
|
959
|
+
static readonly ${ms.update.propertyKey} = ${schemas.update};
|
|
960
|
+
|
|
961
|
+
static readonly ${ms.remove.propertyKey} = ${schemas.remove};
|
|
962
|
+
}`;
|
|
963
|
+
}
|
|
964
|
+
getSchemas(validationLibrary) {
|
|
965
|
+
switch (validationLibrary) {
|
|
966
|
+
case "zod": return {
|
|
967
|
+
import: `import * as z from "zod";`,
|
|
968
|
+
entity: `z.object({
|
|
969
|
+
id: z.string(),
|
|
970
|
+
name: z.string(),
|
|
971
|
+
})`,
|
|
972
|
+
get: `{
|
|
973
|
+
body: z.never(),
|
|
974
|
+
search: z.object({
|
|
975
|
+
page: z.number().optional(),
|
|
976
|
+
limit: z.number().optional(),
|
|
977
|
+
}),
|
|
978
|
+
params: z.never(),
|
|
979
|
+
response: z.object({
|
|
980
|
+
data: z.array(this.entity),
|
|
981
|
+
page: z.number(),
|
|
982
|
+
limit: z.number(),
|
|
983
|
+
count: z.number(),
|
|
984
|
+
totalCount: z.number(),
|
|
985
|
+
pageCount: z.number(),
|
|
986
|
+
}),
|
|
987
|
+
}`,
|
|
988
|
+
getByParams: `{
|
|
989
|
+
body: z.never(),
|
|
990
|
+
search: z.never(),
|
|
991
|
+
params: z.object({
|
|
992
|
+
id: z.string(),
|
|
993
|
+
}),
|
|
994
|
+
response: this.entity,
|
|
995
|
+
}`,
|
|
996
|
+
create: `{
|
|
997
|
+
body: z.object({
|
|
998
|
+
name: z.string(),
|
|
999
|
+
}),
|
|
1000
|
+
search: z.never(),
|
|
1001
|
+
params: z.never(),
|
|
1002
|
+
response: this.entity,
|
|
1003
|
+
}`,
|
|
1004
|
+
update: `{
|
|
1005
|
+
body: this.create.body.partial(),
|
|
1006
|
+
search: z.never(),
|
|
1007
|
+
params: this.getByParams.params,
|
|
1008
|
+
response: this.entity,
|
|
1009
|
+
}`,
|
|
1010
|
+
remove: `{
|
|
1011
|
+
body: z.never(),
|
|
1012
|
+
search: z.never(),
|
|
1013
|
+
params: this.getByParams.params,
|
|
1014
|
+
response: z.void(),
|
|
1015
|
+
}`
|
|
1016
|
+
};
|
|
1017
|
+
case "yup": return {
|
|
1018
|
+
import: `import * as y from "yup";`,
|
|
1019
|
+
entity: `y.object({
|
|
1020
|
+
id: y.string().required(),
|
|
1021
|
+
name: y.string().required(),
|
|
1022
|
+
})`,
|
|
1023
|
+
get: `{
|
|
1024
|
+
body: this.never,
|
|
1025
|
+
search: y.object({
|
|
1026
|
+
page: y.number().optional(),
|
|
1027
|
+
limit: y.number().optional(),
|
|
1028
|
+
}),
|
|
1029
|
+
params: this.never,
|
|
1030
|
+
response: y.object({
|
|
1031
|
+
data: y.array(this.entity).required(),
|
|
1032
|
+
page: y.number().required(),
|
|
1033
|
+
limit: y.number().required(),
|
|
1034
|
+
count: y.number().required(),
|
|
1035
|
+
totalCount: y.number().required(),
|
|
1036
|
+
pageCount: y.number().required(),
|
|
1037
|
+
}),
|
|
1038
|
+
}`,
|
|
1039
|
+
getByParams: `{
|
|
1040
|
+
body: this.never,
|
|
1041
|
+
search: this.never,
|
|
1042
|
+
params: y.object({
|
|
1043
|
+
id: y.string().required(),
|
|
1044
|
+
}),
|
|
1045
|
+
response: this.entity,
|
|
1046
|
+
}`,
|
|
1047
|
+
create: `{
|
|
1048
|
+
body: y.object({
|
|
1049
|
+
name: y.string().required(),
|
|
1050
|
+
}),
|
|
1051
|
+
search: this.never,
|
|
1052
|
+
params: this.never,
|
|
1053
|
+
response: this.entity,
|
|
1054
|
+
}`,
|
|
1055
|
+
update: `{
|
|
1056
|
+
body: this.create.body.partial(),
|
|
1057
|
+
search: this.never,
|
|
1058
|
+
params: this.getByParams.params,
|
|
1059
|
+
response: this.entity,
|
|
1060
|
+
}`,
|
|
1061
|
+
remove: `{
|
|
1062
|
+
body: this.never,
|
|
1063
|
+
search: this.never,
|
|
1064
|
+
params: this.getByParams.params,
|
|
1065
|
+
response: undefined,
|
|
1066
|
+
}`
|
|
1067
|
+
};
|
|
1068
|
+
case "arktype": return {
|
|
1069
|
+
import: `import { type } from "arktype";`,
|
|
1070
|
+
entity: `type({
|
|
1071
|
+
id: "string",
|
|
1072
|
+
name: "string",
|
|
1073
|
+
})`,
|
|
1074
|
+
get: `{
|
|
1075
|
+
body: type("never"),
|
|
1076
|
+
search: type({
|
|
1077
|
+
"page?": "number",
|
|
1078
|
+
"limit?": "number",
|
|
1079
|
+
}),
|
|
1080
|
+
params: type("never"),
|
|
1081
|
+
response: type({
|
|
1082
|
+
data: this.entity.array(),
|
|
1083
|
+
page: type("number"),
|
|
1084
|
+
limit: type("number"),
|
|
1085
|
+
count: type("number"),
|
|
1086
|
+
totalCount: type("number"),
|
|
1087
|
+
pageCount: type("number"),
|
|
1088
|
+
}),
|
|
1089
|
+
}`,
|
|
1090
|
+
getByParams: `{
|
|
1091
|
+
body: type("never"),
|
|
1092
|
+
search: type("never"),
|
|
1093
|
+
params: type({
|
|
1094
|
+
id: type("string"),
|
|
1095
|
+
}),
|
|
1096
|
+
response: this.entity,
|
|
1097
|
+
}`,
|
|
1098
|
+
create: `{
|
|
1099
|
+
body: type({
|
|
1100
|
+
name: type("string"),
|
|
1101
|
+
}),
|
|
1102
|
+
search: type("never"),
|
|
1103
|
+
params: type("never"),
|
|
1104
|
+
response: this.entity,
|
|
1105
|
+
}`,
|
|
1106
|
+
update: `{
|
|
1107
|
+
body: this.create.body.partial(),
|
|
1108
|
+
search: type("never"),
|
|
1109
|
+
params: this.getByParams.params,
|
|
1110
|
+
response: this.entity,
|
|
1111
|
+
}`,
|
|
1112
|
+
remove: `{
|
|
1113
|
+
body: type("never"),
|
|
1114
|
+
search: type("never"),
|
|
1115
|
+
params: this.getByParams.params,
|
|
1116
|
+
}`
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/Resource/index.ts
|
|
1123
|
+
var Resource = class {
|
|
1124
|
+
constructor(key) {
|
|
1125
|
+
this.config = getConfig();
|
|
1126
|
+
this.pascalName = toPascalCase(key);
|
|
1127
|
+
this.camelName = toCamelCase(key);
|
|
1128
|
+
this.kebabName = toKebabCase(key);
|
|
1129
|
+
this.model = new Importable(key, "model");
|
|
1130
|
+
this.modelTypeName = `${this.model.pascalName}Type`;
|
|
1131
|
+
this.service = new Importable(key, "service");
|
|
1132
|
+
this.controller = new Importable(key, "controller");
|
|
1133
|
+
this.exception = new Importable(key, "exception");
|
|
1134
|
+
}
|
|
1135
|
+
config;
|
|
1136
|
+
pascalName;
|
|
1137
|
+
camelName;
|
|
1138
|
+
kebabName;
|
|
1139
|
+
model;
|
|
1140
|
+
modelTypeName;
|
|
1141
|
+
service;
|
|
1142
|
+
controller;
|
|
1143
|
+
exception;
|
|
1144
|
+
type(methodKey, ...accessors) {
|
|
1145
|
+
const ms = this.config.defaultMethods;
|
|
1146
|
+
return `${this.modelTypeName}[${quote(methodKey === "entity" ? "entity" : ms[methodKey].propertyKey)}]${accessors.map((accessor) => `[${quote(accessor)}]`)}`;
|
|
1147
|
+
}
|
|
1148
|
+
route(methodKey, body) {
|
|
1149
|
+
const ms = this.config.defaultMethods;
|
|
1150
|
+
const noValLib = isAbsent(this.config.validationLibrary);
|
|
1151
|
+
const generics = noValLib ? `<${[
|
|
1152
|
+
"body",
|
|
1153
|
+
"search",
|
|
1154
|
+
"params",
|
|
1155
|
+
"response"
|
|
1156
|
+
].map((acc) => `\n\t\t${this.type(methodKey, acc)}`).join(",")}\n\t>` : ``;
|
|
1157
|
+
const baseArgs = `${quote(ms[methodKey].address)}, ${body}`;
|
|
1158
|
+
return `${ms[methodKey].propertyKey} = this.route${generics}(${baseArgs}${noValLib ? `` : `, ${this.model.pascalName}.${ms[methodKey].propertyKey}`})`;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/Modules/AddResourceModule.ts
|
|
1163
|
+
var AddResourceModule = class extends ModuleAbstract {
|
|
1164
|
+
mainFileUpdater;
|
|
1165
|
+
addModelModule;
|
|
1166
|
+
addExceptionModule;
|
|
1167
|
+
addServiceModule;
|
|
1168
|
+
addControllerModule;
|
|
1169
|
+
constructor(mainFileUpdater, addModelModule, addExceptionModule, addServiceModule, addControllerModule) {
|
|
1170
|
+
super();
|
|
1171
|
+
this.mainFileUpdater = mainFileUpdater;
|
|
1172
|
+
this.addModelModule = addModelModule;
|
|
1173
|
+
this.addExceptionModule = addExceptionModule;
|
|
1174
|
+
this.addServiceModule = addServiceModule;
|
|
1175
|
+
this.addControllerModule = addControllerModule;
|
|
1176
|
+
}
|
|
1177
|
+
keys = ["resource", "res"];
|
|
1178
|
+
get help() {
|
|
1179
|
+
return [
|
|
1180
|
+
"Scaffold a new resource (model, service, controller, exception).",
|
|
1181
|
+
"",
|
|
1182
|
+
`Usage: ${EXE_NAME} ${this.keys.join("|")} ${NAME_FLAG_HELP}`,
|
|
1183
|
+
"",
|
|
1184
|
+
"Options:",
|
|
1185
|
+
` ${NAME_FLAG_HELP} Name of the resource to generate.`,
|
|
1186
|
+
" --empty Generate a bare model with no default CRUD shape."
|
|
1187
|
+
];
|
|
1188
|
+
}
|
|
1189
|
+
main() {
|
|
1190
|
+
const name = this.flags.name;
|
|
1191
|
+
assert(name, `name is required.\n\t${EXE_NAME} ${this.passedKey} ${NAME_FLAG_HELP}`);
|
|
1192
|
+
const r = new Resource(name);
|
|
1193
|
+
this.writeFile(this.addModelModule.buildModelFile(r.model, r.modelTypeName), [r.model.filePath]);
|
|
1194
|
+
this.writeFile(this.addExceptionModule.buildExceptionFile(r.exception), [r.exception.filePath]);
|
|
1195
|
+
this.writeFile(this.addServiceModule.buildServiceFile(r.service, r.model, r.exception), [r.service.filePath]);
|
|
1196
|
+
this.writeFile(this.addControllerModule.buildControllerFile(r.controller, r.model, r.service), [r.controller.filePath]);
|
|
1197
|
+
this.mainFileUpdater.addLines("import", `import { ${r.service.pascalName} } from ${quote(r.service.importFrom(this.config.main))};`, `import { ${r.controller.pascalName} } from ${quote(r.controller.importFrom(this.config.main))};`);
|
|
1198
|
+
this.mainFileUpdater.addLines("service", `const ${r.service.camelName} = new ${r.service.pascalName}();`);
|
|
1199
|
+
this.mainFileUpdater.addLines("controller", `new ${r.controller.pascalName}(${r.service.camelName});`);
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
//#endregion
|
|
1203
|
+
//#region src/internal/checkNotImplementedExceptionExists.ts
|
|
1204
|
+
function checkNotImplementedExceptionExists(exception) {
|
|
1205
|
+
let notImplementedExceptionExists = false;
|
|
1206
|
+
if (fs.existsSync(exception.filePath)) exception.parseFile((node) => {
|
|
1207
|
+
if (node.type === "ClassDeclaration") {
|
|
1208
|
+
for (const member of node.body.body) if ((member.type === "PropertyDefinition" || member.type === "MethodDefinition") && member.key.type === "Identifier" && member.key.name === "NotImplemented") notImplementedExceptionExists = true;
|
|
1209
|
+
} else if (node.type === "VariableDeclaration") for (const decl of node.declarations) {
|
|
1210
|
+
if (decl.init?.type !== "ObjectExpression") continue;
|
|
1211
|
+
for (const prop of decl.init.properties) if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "NotImplemented") notImplementedExceptionExists = true;
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
return notImplementedExceptionExists;
|
|
1215
|
+
}
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/Modules/AddServiceModule.ts
|
|
1218
|
+
var AddServiceModule = class extends ModuleAbstract {
|
|
1219
|
+
mainFileUpdater;
|
|
1220
|
+
constructor(mainFileUpdater) {
|
|
1221
|
+
super();
|
|
1222
|
+
this.mainFileUpdater = mainFileUpdater;
|
|
1223
|
+
}
|
|
1224
|
+
keys = ["service", "svc"];
|
|
1225
|
+
get help() {
|
|
1226
|
+
return [
|
|
1227
|
+
"Scaffold a standalone service with stubbed CRUD methods.",
|
|
1228
|
+
"",
|
|
1229
|
+
`Usage: ${EXE_NAME} ${this.keys.join("|")} ${NAME_FLAG_HELP}`,
|
|
1230
|
+
"",
|
|
1231
|
+
"Options:",
|
|
1232
|
+
` ${NAME_FLAG_HELP} Name of the service to generate.`,
|
|
1233
|
+
" --empty Generate a bare model with no default CRUD shape.",
|
|
1234
|
+
"",
|
|
1235
|
+
"Note: this only generates the service file. Without a matching model,",
|
|
1236
|
+
"the stubbed methods will be untyped."
|
|
1237
|
+
];
|
|
1238
|
+
}
|
|
1239
|
+
main() {
|
|
1240
|
+
const name = this.flags.name;
|
|
1241
|
+
assert(name, `name is required.\n\t${EXE_NAME} ${this.passedKey} ${NAME_FLAG_HELP}`);
|
|
1242
|
+
const service = new Importable(name, "service");
|
|
1243
|
+
const model = new Importable(name, "model");
|
|
1244
|
+
const exception = new Importable(service.resourceName, "exception");
|
|
1245
|
+
this.writeFile(this.buildServiceFile(service, model, exception), [service.filePath]);
|
|
1246
|
+
this.mainFileUpdater.addLines("import", `import { ${service.pascalName} } from "${service.importFrom(this.config.main)}";`);
|
|
1247
|
+
this.mainFileUpdater.addLines("service", `const ${service.camelName} = new ${service.pascalName}();`);
|
|
1248
|
+
}
|
|
1249
|
+
buildServiceFile(service, model, exception) {
|
|
1250
|
+
if (fs.existsSync(model.filePath)) return this.buildServiceFileWithModel(service, model, exception);
|
|
1251
|
+
if (this.flags.empty) return this.buildEmptyServiceFile(service);
|
|
1252
|
+
return this.buildServiceFileWithDefaults(service, exception);
|
|
1253
|
+
}
|
|
1254
|
+
buildEmptyServiceFile(service) {
|
|
1255
|
+
const b = new StringBuilder();
|
|
1256
|
+
b.line(`export class ${service.pascalName} {`);
|
|
1257
|
+
b.line(1)(`constructor() {}`);
|
|
1258
|
+
b.line(`}`);
|
|
1259
|
+
return b.toString();
|
|
1260
|
+
}
|
|
1261
|
+
buildServiceFileWithDefaults(service, exception) {
|
|
1262
|
+
const b = new StringBuilder();
|
|
1263
|
+
const methods = this.config.defaultMethods;
|
|
1264
|
+
const notImplementedExceptionExists = checkNotImplementedExceptionExists(exception);
|
|
1265
|
+
if (notImplementedExceptionExists) b.line(`import { ${exception.pascalName} } from "${exception.importFrom(service.filePath)}";`).line(``);
|
|
1266
|
+
b.line(`export class ${service.pascalName} {`);
|
|
1267
|
+
b.line(1)(`constructor() {}`);
|
|
1268
|
+
for (const { propertyKey } of Object.values(methods)) {
|
|
1269
|
+
b.line(``);
|
|
1270
|
+
b.line(1)(`async ${propertyKey}(): Promise<void> {`);
|
|
1271
|
+
if (notImplementedExceptionExists) b.line(2)(`throw ${exception.pascalName}.NotImplemented;`);
|
|
1272
|
+
else b.line(2)(`throw new Error("Method not implemented.");`);
|
|
1273
|
+
b.line(1)(`}`);
|
|
1274
|
+
}
|
|
1275
|
+
b.line(`}`);
|
|
1276
|
+
return b.toString();
|
|
1277
|
+
}
|
|
1278
|
+
buildServiceFileWithModel(service, model, exception) {
|
|
1279
|
+
const b = new StringBuilder();
|
|
1280
|
+
const { modelTypeName, modelDef } = parseModelDefinition(model);
|
|
1281
|
+
const notImplementedExceptionExists = checkNotImplementedExceptionExists(exception);
|
|
1282
|
+
if (notImplementedExceptionExists) b.line(`import { ${exception.pascalName} } from "${exception.importFrom(service.filePath)}";`);
|
|
1283
|
+
b.line(`import type { ${modelTypeName} } from "${model.importFrom(service.filePath)}";`);
|
|
1284
|
+
b.line("");
|
|
1285
|
+
b.line(`export class ${service.pascalName} {`);
|
|
1286
|
+
b.line(1)(`constructor() {}`);
|
|
1287
|
+
const isNeverSchema = (schema) => NEVER_SCHEMAS.has(schema.trim());
|
|
1288
|
+
for (const [key, val] of Object.entries(modelDef)) {
|
|
1289
|
+
const usedParams = [
|
|
1290
|
+
"params",
|
|
1291
|
+
"search",
|
|
1292
|
+
"body"
|
|
1293
|
+
].filter((k) => k in val && !isNeverSchema(val[k]));
|
|
1294
|
+
const funcParams = usedParams.map((k) => `${k}: ${modelTypeName}["${key}"]["${k}"]`).join(", ");
|
|
1295
|
+
const returnType = "response" in val && !isNeverSchema(val.response) ? `${modelTypeName}["${key}"]["response"]` : "void";
|
|
1296
|
+
b.line(``);
|
|
1297
|
+
b.line(1)(`async ${key}(${funcParams}): Promise<${returnType}> {`);
|
|
1298
|
+
for (const param of usedParams) b.line(2)(`void ${param};`);
|
|
1299
|
+
if (notImplementedExceptionExists) b.line(2)(`throw ${exception.pascalName}.NotImplemented;`);
|
|
1300
|
+
else b.line(2)(`throw new Error("Method not implemented.");`);
|
|
1301
|
+
b.line(1)(`}`);
|
|
1302
|
+
}
|
|
1303
|
+
b.line(`}`);
|
|
1304
|
+
return b.toString();
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/internal/findEnclosingFunctionName.ts
|
|
1309
|
+
function findEnclosingFunctionName(source, matchIndex) {
|
|
1310
|
+
let depth = 0;
|
|
1311
|
+
let openIndex = -1;
|
|
1312
|
+
for (let i = matchIndex; i >= 0; i--) {
|
|
1313
|
+
const ch = source[i];
|
|
1314
|
+
if (ch === "}") depth++;
|
|
1315
|
+
else if (ch === "{") {
|
|
1316
|
+
if (depth === 0) {
|
|
1317
|
+
openIndex = i;
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
depth--;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (openIndex === -1) return null;
|
|
1324
|
+
const before = source.slice(0, openIndex);
|
|
1325
|
+
return (before.match(/(?:async\s+)?function\s*\*?\s*([A-Za-z0-9_$]+)\s*\([\s\S]*?\)\s*$/) ?? before.match(/(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?(?:function\s*)?\([\s\S]*?\)\s*(?:=>)?\s*$/))?.[1] ?? null;
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/Modules/ApiClientModule.ts
|
|
1329
|
+
var ApiClientModule = class extends ModuleAbstract {
|
|
1330
|
+
keys = ["api"];
|
|
1331
|
+
get help() {
|
|
1332
|
+
return [
|
|
1333
|
+
"Codegen for all routes",
|
|
1334
|
+
"Generates types and model interfaces for all routes.",
|
|
1335
|
+
"Generates an api client with methods for all routes. (unless disabled in config)",
|
|
1336
|
+
"",
|
|
1337
|
+
`Usage: ${EXE_NAME} ${this.keys.join("|")}`,
|
|
1338
|
+
"",
|
|
1339
|
+
"Note: Your entry file must call `.listen()` either at the top level",
|
|
1340
|
+
"or inside a single function."
|
|
1341
|
+
];
|
|
1342
|
+
}
|
|
1343
|
+
async main() {
|
|
1344
|
+
const mainPath = path.resolve(this.config.main);
|
|
1345
|
+
const outPath = mainPath.replace(/\.ts$/, ".temp.mjs");
|
|
1346
|
+
const generatorImport = process.env.NODE_ENV === "development" ? "@/index" : process.env.NODE_ENV === "test" ? resolveCwdPath("dist") : "@ozanarslan/corpus-cli";
|
|
1347
|
+
try {
|
|
1348
|
+
const b = new StringBuilder();
|
|
1349
|
+
b.line(`import { getNearestApp } from "${this.config.pkgPath}";`);
|
|
1350
|
+
b.line(`import { ${GEN_FUNC} } from "${generatorImport}";`);
|
|
1351
|
+
logger.step(`Reading main file at ${mainPath}`);
|
|
1352
|
+
let mainFileContents = fs.readFileSync(mainPath, "utf-8");
|
|
1353
|
+
const match = LISTEN_PATTERN.exec(mainFileContents);
|
|
1354
|
+
if (!match) logFatal(`
|
|
1355
|
+
Could not find a .listen() call in: ${mainPath}.
|
|
1356
|
+
Make sure your entry file calls .listen() either at the top level or inside a top level function.
|
|
1357
|
+
`.trim());
|
|
1358
|
+
const funcName = findEnclosingFunctionName(mainFileContents, match.index);
|
|
1359
|
+
if (funcName) {
|
|
1360
|
+
logger.step(`Making sure ${funcName} is called.`);
|
|
1361
|
+
const callSite = new RegExp(`^\\s*(?:void|await)?\\s*${funcName}\\s*\\(\\s*\\);?.*$`, "gm");
|
|
1362
|
+
mainFileContents = mainFileContents.replace(callSite, "");
|
|
1363
|
+
mainFileContents += `\n\nawait ${funcName}();\n`;
|
|
1364
|
+
}
|
|
1365
|
+
mainFileContents = mainFileContents.replace(LISTEN_PATTERN, `
|
|
1366
|
+
try {
|
|
1367
|
+
const nearestApp = getNearestApp();
|
|
1368
|
+
const config = ${JSON.stringify(this.config)};
|
|
1369
|
+
${GEN_FUNC}(nearestApp.prefix, nearestApp.routes, config);
|
|
1370
|
+
process.exit(0);
|
|
1371
|
+
} catch (err) {
|
|
1372
|
+
console.log(String(err));
|
|
1373
|
+
process.exit(1);
|
|
1374
|
+
}
|
|
1375
|
+
`.trim());
|
|
1376
|
+
b.line(mainFileContents);
|
|
1377
|
+
logger.step(`Transpiling to JS...`);
|
|
1378
|
+
const js = new Bun.Transpiler({ loader: "ts" }).transformSync(b.toString());
|
|
1379
|
+
logger.step(`Writing temp file at ${outPath}`);
|
|
1380
|
+
fs.writeFileSync(outPath, js, "utf-8");
|
|
1381
|
+
logger.step(`Running generator...`);
|
|
1382
|
+
const result = spawnSync(process.execPath, [outPath], {
|
|
1383
|
+
stdio: "inherit",
|
|
1384
|
+
env: process.env
|
|
1385
|
+
});
|
|
1386
|
+
if (result.status !== 0) logFatal(`exited with status ${result.status}`);
|
|
1387
|
+
logger.success(`Generator completed successfully`);
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
logger.error(String(err));
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
} finally {
|
|
1392
|
+
logger.step(`Deleting temp file at ${outPath}`);
|
|
1393
|
+
fs.rmSync(outPath, { force: true });
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/cli.ts
|
|
1399
|
+
const mainFileUpdater = new MainFileUpdater();
|
|
1400
|
+
const apiClientModule = new ApiClientModule();
|
|
1401
|
+
const addServiceModule = new AddServiceModule(mainFileUpdater);
|
|
1402
|
+
const addControllerModule = new AddControllerModule(mainFileUpdater);
|
|
1403
|
+
const addModelModule = new AddModelModule();
|
|
1404
|
+
const addExceptionModule = new AddExceptionModule();
|
|
1405
|
+
const mods = [
|
|
1406
|
+
apiClientModule,
|
|
1407
|
+
addServiceModule,
|
|
1408
|
+
addControllerModule,
|
|
1409
|
+
addModelModule,
|
|
1410
|
+
addExceptionModule,
|
|
1411
|
+
new AddResourceModule(mainFileUpdater, addModelModule, addExceptionModule, addServiceModule, addControllerModule)
|
|
1412
|
+
];
|
|
1413
|
+
function printHelp() {
|
|
1414
|
+
const pad = Math.max(...mods.map((m) => m.keys.join(", ").length)) + 2;
|
|
1415
|
+
process.stdout.write([
|
|
1416
|
+
`${EXE_NAME} — ${APP_NAME} Corpus codegen tool.`,
|
|
1417
|
+
"",
|
|
1418
|
+
"Usage:",
|
|
1419
|
+
` ${EXE_NAME} <module> [args]`,
|
|
1420
|
+
"",
|
|
1421
|
+
"modules:",
|
|
1422
|
+
...mods.map((m) => ` ${m.keys.join(", ").padEnd(pad)}${m.help[0]}`),
|
|
1423
|
+
"",
|
|
1424
|
+
`Run \`${EXE_NAME} <module> --help\` for module-specific flags.`,
|
|
1425
|
+
""
|
|
1426
|
+
].join("\n"));
|
|
1427
|
+
}
|
|
1428
|
+
const first = process.argv.slice(2)[0];
|
|
1429
|
+
if (!first || first === "-h" || first === "--help") {
|
|
1430
|
+
printHelp();
|
|
1431
|
+
process.exit(first ? 0 : 1);
|
|
1432
|
+
}
|
|
1433
|
+
const mod = mods.find((m) => m.keys.includes(first));
|
|
1434
|
+
if (!mod) {
|
|
1435
|
+
printHelp();
|
|
1436
|
+
process.exit(1);
|
|
1437
|
+
}
|
|
1438
|
+
try {
|
|
1439
|
+
await mod.run();
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
await mod.stop();
|
|
1442
|
+
logFatal(err);
|
|
1443
|
+
} finally {
|
|
1444
|
+
await mod.stop();
|
|
1445
|
+
process.exit(0);
|
|
1446
|
+
}
|
|
1447
|
+
//#endregion
|
|
1448
|
+
export {};
|