@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/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 {};