@dudousxd/nestjs-codegen 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +161 -0
- package/dist/cli/main.cjs +1214 -1613
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1188 -1587
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.cjs +12 -2
- package/dist/extension/index.cjs.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +10 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-BwIRjOQA.d.cts → index-oH5t7x4G.d.cts} +56 -41
- package/dist/{index-BwIRjOQA.d.ts → index-oH5t7x4G.d.ts} +56 -41
- package/dist/index.cjs +1003 -1457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -16
- package/dist/index.d.ts +32 -16
- package/dist/index.js +977 -1430
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +908 -1355
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +9 -2
- package/dist/nest/index.d.ts +9 -2
- package/dist/nest/index.js +893 -1340
- package/dist/nest/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli/main.js
CHANGED
|
@@ -20,112 +20,9 @@ var CodegenError = class extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
// src/adapters/zod.ts
|
|
24
|
-
function toObjectKey(name) {
|
|
25
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
26
|
-
}
|
|
27
|
-
function messageSuffix(messageRaw2) {
|
|
28
|
-
return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
|
|
29
|
-
}
|
|
30
|
-
function renderStringChecks(checks) {
|
|
31
|
-
let out = "";
|
|
32
|
-
for (const c of checks) {
|
|
33
|
-
switch (c.check) {
|
|
34
|
-
case "email":
|
|
35
|
-
out += `.email(${messageSuffix(c.messageRaw)})`;
|
|
36
|
-
break;
|
|
37
|
-
case "url":
|
|
38
|
-
out += `.url(${messageSuffix(c.messageRaw)})`;
|
|
39
|
-
break;
|
|
40
|
-
case "uuid":
|
|
41
|
-
out += `.uuid(${messageSuffix(c.messageRaw)})`;
|
|
42
|
-
break;
|
|
43
|
-
case "regex":
|
|
44
|
-
out += `.regex(${c.pattern})`;
|
|
45
|
-
break;
|
|
46
|
-
case "min":
|
|
47
|
-
out += `.min(${c.value})`;
|
|
48
|
-
break;
|
|
49
|
-
case "max":
|
|
50
|
-
out += `.max(${c.value})`;
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
function render(node, ctx) {
|
|
57
|
-
switch (node.kind) {
|
|
58
|
-
case "string":
|
|
59
|
-
return `z.string()${renderStringChecks(node.checks)}`;
|
|
60
|
-
case "number": {
|
|
61
|
-
let out = "z.number()";
|
|
62
|
-
for (const c of node.checks) {
|
|
63
|
-
if (c.check === "int") out += ".int()";
|
|
64
|
-
else if (c.check === "positive") out += ".positive()";
|
|
65
|
-
else if (c.check === "negative") out += ".negative()";
|
|
66
|
-
else if (c.check === "min") out += `.min(${c.value})`;
|
|
67
|
-
else if (c.check === "max") out += `.max(${c.value})`;
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
case "boolean":
|
|
72
|
-
return "z.boolean()";
|
|
73
|
-
case "date":
|
|
74
|
-
return "z.coerce.date()";
|
|
75
|
-
case "unknown":
|
|
76
|
-
return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
|
|
77
|
-
case "instanceof":
|
|
78
|
-
return `z.instanceof(${node.ctor})`;
|
|
79
|
-
case "enum":
|
|
80
|
-
return `z.enum([${node.literals.join(", ")}])`;
|
|
81
|
-
case "literal":
|
|
82
|
-
return `z.literal(${node.raw})`;
|
|
83
|
-
case "union":
|
|
84
|
-
return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
|
|
85
|
-
case "object": {
|
|
86
|
-
if (node.fields.length === 0) {
|
|
87
|
-
return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
|
|
88
|
-
}
|
|
89
|
-
const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
|
|
90
|
-
return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
|
|
91
|
-
}
|
|
92
|
-
case "array":
|
|
93
|
-
return `z.array(${render(node.element, ctx)})`;
|
|
94
|
-
case "optional":
|
|
95
|
-
return `${render(node.inner, ctx)}.optional()`;
|
|
96
|
-
case "ref":
|
|
97
|
-
return node.name;
|
|
98
|
-
case "lazyRef":
|
|
99
|
-
return `z.lazy(() => ${node.name})`;
|
|
100
|
-
case "annotated": {
|
|
101
|
-
const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
|
|
102
|
-
return `${render(node.inner, ctx)} ${comments}`;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
var zodAdapter = {
|
|
107
|
-
name: "zod",
|
|
108
|
-
importStatements(usage) {
|
|
109
|
-
return usage.used ? ["import { z } from 'zod';"] : [];
|
|
110
|
-
},
|
|
111
|
-
render,
|
|
112
|
-
inferType(schemaConst) {
|
|
113
|
-
return `z.infer<typeof ${schemaConst}>`;
|
|
114
|
-
},
|
|
115
|
-
renderModule(mod) {
|
|
116
|
-
const ctx = { named: mod.named };
|
|
117
|
-
const namedNestedSchemas = /* @__PURE__ */ new Map();
|
|
118
|
-
for (const [name, node] of mod.named) {
|
|
119
|
-
namedNestedSchemas.set(name, render(node, ctx));
|
|
120
|
-
}
|
|
121
|
-
return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
23
|
// src/adapters/registry.ts
|
|
126
24
|
function resolveAdapter(option) {
|
|
127
25
|
if (typeof option !== "string") return option;
|
|
128
|
-
if (option === "zod") return zodAdapter;
|
|
129
26
|
const pkg = `@dudousxd/nestjs-codegen-${option}`;
|
|
130
27
|
const named = `${option}Adapter`;
|
|
131
28
|
throw new ConfigError(
|
|
@@ -175,8 +72,21 @@ If this is intentional, move the file inside your project directory.`
|
|
|
175
72
|
);
|
|
176
73
|
}
|
|
177
74
|
}
|
|
75
|
+
function validateUserConfig(userConfig) {
|
|
76
|
+
if (userConfig.validation == null) {
|
|
77
|
+
throw new ConfigError(
|
|
78
|
+
"validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
82
|
+
throw new ConfigError(
|
|
83
|
+
"Config validation failed: `pages.glob` must be a string when `pages` is set"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
178
87
|
function applyDefaults(userConfig, cwd) {
|
|
179
|
-
|
|
88
|
+
validateUserConfig(userConfig);
|
|
89
|
+
const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
|
|
180
90
|
const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
|
|
181
91
|
let app = null;
|
|
182
92
|
if (userConfig.app) {
|
|
@@ -194,7 +104,8 @@ function applyDefaults(userConfig, cwd) {
|
|
|
194
104
|
}
|
|
195
105
|
return {
|
|
196
106
|
extensions: userConfig.extensions ?? [],
|
|
197
|
-
|
|
107
|
+
// Non-null: validateUserConfig() above throws when `validation` is absent.
|
|
108
|
+
validation: resolveAdapter(userConfig.validation),
|
|
198
109
|
pages: userConfig.pages ? {
|
|
199
110
|
glob: userConfig.pages.glob,
|
|
200
111
|
propsExport: userConfig.pages.propsExport ?? "ComponentProps",
|
|
@@ -248,18 +159,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
248
159
|
`Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
|
|
249
160
|
);
|
|
250
161
|
}
|
|
251
|
-
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
252
|
-
throw new ConfigError(
|
|
253
|
-
`Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
162
|
return applyDefaults(userConfig, resolvedCwd);
|
|
257
163
|
}
|
|
258
164
|
|
|
259
165
|
// src/generate.ts
|
|
260
166
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
261
|
-
import { dirname as dirname2, join as
|
|
262
|
-
import { Project as Project2 } from "ts-morph";
|
|
167
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
263
168
|
|
|
264
169
|
// src/discovery/pages.ts
|
|
265
170
|
import { readFile } from "fs/promises";
|
|
@@ -314,7 +219,8 @@ function extractPropsSource(source, exportName) {
|
|
|
314
219
|
}
|
|
315
220
|
|
|
316
221
|
// src/discovery/shared-props.ts
|
|
317
|
-
import {
|
|
222
|
+
import { join as join3 } from "path";
|
|
223
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
318
224
|
function discoverSharedProps(project, moduleEntry) {
|
|
319
225
|
try {
|
|
320
226
|
let sourceFile = project.getSourceFile(moduleEntry);
|
|
@@ -334,6 +240,31 @@ function discoverSharedProps(project, moduleEntry) {
|
|
|
334
240
|
return null;
|
|
335
241
|
}
|
|
336
242
|
}
|
|
243
|
+
function discoverSharedPropsFromConfig(config) {
|
|
244
|
+
if (!config.app?.moduleEntry) return null;
|
|
245
|
+
try {
|
|
246
|
+
const tsconfigPath = config.app.tsconfig ?? join3(config.codegen.cwd, "tsconfig.json");
|
|
247
|
+
let project;
|
|
248
|
+
try {
|
|
249
|
+
project = new Project({
|
|
250
|
+
tsConfigFilePath: tsconfigPath,
|
|
251
|
+
skipAddingFilesFromTsConfig: true,
|
|
252
|
+
skipLoadingLibFiles: true,
|
|
253
|
+
skipFileDependencyResolution: true
|
|
254
|
+
});
|
|
255
|
+
} catch {
|
|
256
|
+
project = new Project({
|
|
257
|
+
skipAddingFilesFromTsConfig: true,
|
|
258
|
+
skipLoadingLibFiles: true,
|
|
259
|
+
skipFileDependencyResolution: true,
|
|
260
|
+
compilerOptions: { allowJs: true, strict: false }
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return discoverSharedProps(project, config.app.moduleEntry);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
337
268
|
function findForRootCall(sourceFile) {
|
|
338
269
|
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
339
270
|
for (const call of callExpressions) {
|
|
@@ -353,9 +284,9 @@ function findForRootCall(sourceFile) {
|
|
|
353
284
|
function findShareInitializer(forRootCall) {
|
|
354
285
|
if (!Node.isCallExpression(forRootCall)) return null;
|
|
355
286
|
const args = forRootCall.getArguments();
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
358
|
-
for (const prop of
|
|
287
|
+
const firstArg2 = args[0];
|
|
288
|
+
if (!firstArg2 || !Node.isObjectLiteralExpression(firstArg2)) return null;
|
|
289
|
+
for (const prop of firstArg2.getProperties()) {
|
|
359
290
|
if (Node.isPropertyAssignment(prop) && prop.getName() === "share") {
|
|
360
291
|
return prop.getInitializer() ?? null;
|
|
361
292
|
}
|
|
@@ -454,9 +385,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
|
|
|
454
385
|
const typeName = typeNode.getTypeName();
|
|
455
386
|
if (Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
|
|
456
387
|
const typeArgs = typeNode.getTypeArguments();
|
|
457
|
-
const
|
|
458
|
-
if (
|
|
459
|
-
return extractFromReturnTypeAnnotation(
|
|
388
|
+
const firstArg2 = typeArgs[0];
|
|
389
|
+
if (firstArg2) {
|
|
390
|
+
return extractFromReturnTypeAnnotation(firstArg2);
|
|
460
391
|
}
|
|
461
392
|
return null;
|
|
462
393
|
}
|
|
@@ -562,25 +493,14 @@ function inferExpressionType(node) {
|
|
|
562
493
|
|
|
563
494
|
// src/emit/emit-api.ts
|
|
564
495
|
import { mkdir, writeFile } from "fs/promises";
|
|
565
|
-
import { isAbsolute as isAbsolute2, join as
|
|
496
|
+
import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
|
|
566
497
|
|
|
567
498
|
// src/extension/registry.ts
|
|
568
|
-
import { Project } from "ts-morph";
|
|
499
|
+
import { Project as Project2 } from "ts-morph";
|
|
569
500
|
function resolveApiSlots(extensions) {
|
|
570
|
-
let transport;
|
|
571
|
-
let transportOwner;
|
|
572
501
|
let layer;
|
|
573
502
|
let layerOwner;
|
|
574
503
|
for (const ext of extensions) {
|
|
575
|
-
if (ext.apiTransport) {
|
|
576
|
-
if (transport) {
|
|
577
|
-
throw new CodegenError(
|
|
578
|
-
`api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
transport = ext.apiTransport;
|
|
582
|
-
transportOwner = ext.name;
|
|
583
|
-
}
|
|
584
504
|
if (ext.apiClientLayer) {
|
|
585
505
|
if (layer) {
|
|
586
506
|
throw new CodegenError(
|
|
@@ -592,11 +512,22 @@ function resolveApiSlots(extensions) {
|
|
|
592
512
|
}
|
|
593
513
|
}
|
|
594
514
|
return {
|
|
595
|
-
...transport ? { transport } : {},
|
|
596
515
|
...layer ? { layer } : {}
|
|
597
516
|
};
|
|
598
517
|
}
|
|
599
518
|
var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
|
|
519
|
+
function mergeExclusive(target, incoming, {
|
|
520
|
+
owner,
|
|
521
|
+
describe
|
|
522
|
+
}) {
|
|
523
|
+
for (const [key, value] of incoming) {
|
|
524
|
+
const prev = target.get(key);
|
|
525
|
+
if (prev !== void 0) {
|
|
526
|
+
throw new CodegenError(describe(key, prev.owner, owner));
|
|
527
|
+
}
|
|
528
|
+
target.set(key, { value, owner });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
600
531
|
function createExtensionContext(config, getRoutes) {
|
|
601
532
|
let project;
|
|
602
533
|
return {
|
|
@@ -608,7 +539,7 @@ function createExtensionContext(config, getRoutes) {
|
|
|
608
539
|
},
|
|
609
540
|
project() {
|
|
610
541
|
if (!project) {
|
|
611
|
-
project = new
|
|
542
|
+
project = new Project2({
|
|
612
543
|
skipAddingFilesFromTsConfig: true,
|
|
613
544
|
skipLoadingLibFiles: true,
|
|
614
545
|
skipFileDependencyResolution: true,
|
|
@@ -641,29 +572,36 @@ async function collectEmittedFiles(extensions, ctx) {
|
|
|
641
572
|
`Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
|
|
642
573
|
);
|
|
643
574
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
owners.set(key, ext.name);
|
|
575
|
+
mergeExclusive(owners, [[key, file]], {
|
|
576
|
+
owner: ext.name,
|
|
577
|
+
describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
|
|
578
|
+
});
|
|
651
579
|
files.push(file);
|
|
652
580
|
}
|
|
653
581
|
}
|
|
654
582
|
return files;
|
|
655
583
|
}
|
|
656
584
|
|
|
585
|
+
// src/extension/types.ts
|
|
586
|
+
function requestShape(route) {
|
|
587
|
+
const cs = route.contract?.contractSource;
|
|
588
|
+
const isGet = route.method.toUpperCase() === "GET";
|
|
589
|
+
const isQuery = isGet || !!cs?.filterFields?.length;
|
|
590
|
+
const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
|
|
591
|
+
const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
|
|
592
|
+
return { isGet, isQuery, hasBody, hasQuery };
|
|
593
|
+
}
|
|
594
|
+
|
|
657
595
|
// src/emit/emit-api.ts
|
|
658
596
|
async function emitApi(routes, outDir, opts = {}) {
|
|
659
597
|
await mkdir(outDir, { recursive: true });
|
|
660
598
|
const content = buildApiFile(routes, outDir, opts);
|
|
661
|
-
await writeFile(
|
|
599
|
+
await writeFile(join4(outDir, "api.ts"), content, "utf8");
|
|
662
600
|
}
|
|
663
601
|
function splitName(name) {
|
|
664
602
|
return name.split(".");
|
|
665
603
|
}
|
|
666
|
-
function
|
|
604
|
+
function toObjectKey(segment) {
|
|
667
605
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
668
606
|
return segment;
|
|
669
607
|
}
|
|
@@ -770,7 +708,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
770
708
|
const pad = " ".repeat(indent);
|
|
771
709
|
const lines = [];
|
|
772
710
|
for (const [key, node] of tree) {
|
|
773
|
-
const objKey =
|
|
711
|
+
const objKey = toObjectKey(key);
|
|
774
712
|
if (node.kind === "leaf") {
|
|
775
713
|
const c = node;
|
|
776
714
|
const method = c.method.toUpperCase();
|
|
@@ -796,15 +734,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
796
734
|
return lines;
|
|
797
735
|
}
|
|
798
736
|
function buildRequestModel(c) {
|
|
799
|
-
const isGet = c.method.toUpperCase() === "GET";
|
|
800
737
|
const m = c.method.toLowerCase();
|
|
801
738
|
const flat = JSON.stringify(c.name);
|
|
802
739
|
const path = JSON.stringify(c.path);
|
|
803
740
|
const TA = buildRouterTypeAccess(c.name);
|
|
804
741
|
const withParams = hasPathParams(c.params);
|
|
805
|
-
const
|
|
806
|
-
const isQuery = isGet || !!c.contractSource.filterFields?.length;
|
|
807
|
-
const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
|
|
742
|
+
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
808
743
|
const fields = [];
|
|
809
744
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
810
745
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -826,7 +761,6 @@ function buildRequestModel(c) {
|
|
|
826
761
|
urlExpr,
|
|
827
762
|
optsExpr,
|
|
828
763
|
responseType: `${TA}['response']`,
|
|
829
|
-
bodyType: `${TA}['body']`,
|
|
830
764
|
queryKeyExpr: `[${flat}, input] as const`
|
|
831
765
|
};
|
|
832
766
|
}
|
|
@@ -874,7 +808,7 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
874
808
|
const pad = " ".repeat(indent);
|
|
875
809
|
const lines = [];
|
|
876
810
|
for (const [key, node] of tree) {
|
|
877
|
-
const objKey =
|
|
811
|
+
const objKey = toObjectKey(key);
|
|
878
812
|
if (node.kind === "branch") {
|
|
879
813
|
lines.push(`${pad}${objKey}: {`);
|
|
880
814
|
lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
|
|
@@ -882,30 +816,28 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
882
816
|
continue;
|
|
883
817
|
}
|
|
884
818
|
const req = buildRequestModel(node);
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
params: node.params,
|
|
890
|
-
contract: { contractSource: node.contractSource },
|
|
891
|
-
...node.controllerRef ? { controllerRef: node.controllerRef } : {}
|
|
819
|
+
const leaf = {
|
|
820
|
+
route: node.route,
|
|
821
|
+
request: req,
|
|
822
|
+
requestExpr: renderFetcherRequest(req)
|
|
892
823
|
};
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
824
|
+
const owned = /* @__PURE__ */ new Map();
|
|
825
|
+
if (p.layer) {
|
|
826
|
+
mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
|
|
827
|
+
owner: p.layer.name,
|
|
828
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
829
|
+
});
|
|
830
|
+
}
|
|
897
831
|
for (const ext of p.memberExts) {
|
|
898
832
|
const extra = ext.apiMembers?.(leaf, p.ctx);
|
|
899
833
|
if (!extra) continue;
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
members[name] = value;
|
|
907
|
-
}
|
|
834
|
+
mergeExclusive(owned, Object.entries(extra), {
|
|
835
|
+
owner: ext.name,
|
|
836
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
837
|
+
});
|
|
908
838
|
}
|
|
839
|
+
const members = {};
|
|
840
|
+
for (const [name, { value }] of owned) members[name] = value;
|
|
909
841
|
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
|
|
910
842
|
}
|
|
911
843
|
return lines;
|
|
@@ -914,10 +846,82 @@ function buildRouterTypeAccess(name) {
|
|
|
914
846
|
const segments = splitName(name);
|
|
915
847
|
return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
|
|
916
848
|
}
|
|
849
|
+
var RESOLVER_HELPERS = [
|
|
850
|
+
// --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
|
|
851
|
+
"type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
|
|
852
|
+
" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
|
|
853
|
+
" : P extends keyof R ? R[P] : never;",
|
|
854
|
+
"",
|
|
855
|
+
// --- ResolveByName: resolve a field from a dot-path name ---
|
|
856
|
+
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
|
|
857
|
+
"",
|
|
858
|
+
// --- ResolveByPath: scan all leaves for matching method + url ---
|
|
859
|
+
// Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
|
|
860
|
+
"type _LeafValues<T> = T extends { method: string; url: string }",
|
|
861
|
+
" ? T",
|
|
862
|
+
" : T extends object ? _LeafValues<T[keyof T]> : never;",
|
|
863
|
+
"",
|
|
864
|
+
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
|
|
865
|
+
" ? L extends { method: M; url: U }",
|
|
866
|
+
" ? Field extends keyof L ? L[Field] : never",
|
|
867
|
+
" : never",
|
|
868
|
+
" : never;",
|
|
869
|
+
""
|
|
870
|
+
];
|
|
871
|
+
var ROUTE_NAMESPACE = [
|
|
872
|
+
"export namespace Route {",
|
|
873
|
+
' export type Response<K extends string> = ResolveByName<K, "response">;',
|
|
874
|
+
' export type Body<K extends string> = ResolveByName<K, "body">;',
|
|
875
|
+
' export type Query<K extends string> = ResolveByName<K, "query">;',
|
|
876
|
+
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
877
|
+
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
878
|
+
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
879
|
+
" export type Request<K extends string> = {",
|
|
880
|
+
" body: Body<K>;",
|
|
881
|
+
" query: Query<K>;",
|
|
882
|
+
" params: Params<K>;",
|
|
883
|
+
" };",
|
|
884
|
+
"}",
|
|
885
|
+
""
|
|
886
|
+
];
|
|
887
|
+
var PATH_NAMESPACE = [
|
|
888
|
+
"export namespace Path {",
|
|
889
|
+
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
890
|
+
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
|
|
891
|
+
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
|
|
892
|
+
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
893
|
+
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
894
|
+
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
895
|
+
"}",
|
|
896
|
+
""
|
|
897
|
+
];
|
|
898
|
+
var EMPTY_ROUTE_NAMESPACE = [
|
|
899
|
+
"export namespace Route {",
|
|
900
|
+
" export type Response<K extends string> = never;",
|
|
901
|
+
" export type Body<K extends string> = never;",
|
|
902
|
+
" export type Query<K extends string> = never;",
|
|
903
|
+
" export type Params<K extends string> = never;",
|
|
904
|
+
" export type Error<K extends string> = never;",
|
|
905
|
+
" export type FilterFields<K extends string> = never;",
|
|
906
|
+
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
907
|
+
"}",
|
|
908
|
+
""
|
|
909
|
+
];
|
|
910
|
+
var EMPTY_PATH_NAMESPACE = [
|
|
911
|
+
"export namespace Path {",
|
|
912
|
+
" export type Response<M extends string, U extends string> = never;",
|
|
913
|
+
" export type Body<M extends string, U extends string> = never;",
|
|
914
|
+
" export type Query<M extends string, U extends string> = never;",
|
|
915
|
+
" export type Params<M extends string, U extends string> = never;",
|
|
916
|
+
" export type Error<M extends string, U extends string> = never;",
|
|
917
|
+
" export type FilterFields<M extends string, U extends string> = never;",
|
|
918
|
+
"}",
|
|
919
|
+
""
|
|
920
|
+
];
|
|
917
921
|
function buildApiFile(routes, outDir, opts = {}) {
|
|
918
922
|
const fetcherImportPath = opts.fetcherImportPath;
|
|
919
923
|
const extensions = opts.extensions ?? [];
|
|
920
|
-
const {
|
|
924
|
+
const { layer } = resolveApiSlots(extensions);
|
|
921
925
|
const memberExts = extensions.filter((e) => e.apiMembers);
|
|
922
926
|
const headerExts = extensions.filter((e) => e.apiHeader);
|
|
923
927
|
const contracted = routes.filter((r) => r.contract);
|
|
@@ -962,7 +966,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
962
966
|
seenImports.add(imp);
|
|
963
967
|
extImports.push(imp);
|
|
964
968
|
};
|
|
965
|
-
for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
|
|
966
969
|
for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
|
|
967
970
|
for (const ext of headerExts) {
|
|
968
971
|
for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
|
|
@@ -1007,27 +1010,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1007
1010
|
lines.push("}");
|
|
1008
1011
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
1009
1012
|
lines.push("");
|
|
1010
|
-
lines.push(
|
|
1011
|
-
lines.push(
|
|
1012
|
-
lines.push(" export type Body<K extends string> = never;");
|
|
1013
|
-
lines.push(" export type Query<K extends string> = never;");
|
|
1014
|
-
lines.push(" export type Params<K extends string> = never;");
|
|
1015
|
-
lines.push(" export type Error<K extends string> = never;");
|
|
1016
|
-
lines.push(" export type FilterFields<K extends string> = never;");
|
|
1017
|
-
lines.push(
|
|
1018
|
-
" export type Request<K extends string> = { body: never; query: never; params: never };"
|
|
1019
|
-
);
|
|
1020
|
-
lines.push("}");
|
|
1021
|
-
lines.push("");
|
|
1022
|
-
lines.push("export namespace Path {");
|
|
1023
|
-
lines.push(" export type Response<M extends string, U extends string> = never;");
|
|
1024
|
-
lines.push(" export type Body<M extends string, U extends string> = never;");
|
|
1025
|
-
lines.push(" export type Query<M extends string, U extends string> = never;");
|
|
1026
|
-
lines.push(" export type Params<M extends string, U extends string> = never;");
|
|
1027
|
-
lines.push(" export type Error<M extends string, U extends string> = never;");
|
|
1028
|
-
lines.push(" export type FilterFields<M extends string, U extends string> = never;");
|
|
1029
|
-
lines.push("}");
|
|
1030
|
-
lines.push("");
|
|
1013
|
+
lines.push(...EMPTY_ROUTE_NAMESPACE);
|
|
1014
|
+
lines.push(...EMPTY_PATH_NAMESPACE);
|
|
1031
1015
|
return lines.join("\n");
|
|
1032
1016
|
}
|
|
1033
1017
|
const tree = /* @__PURE__ */ new Map();
|
|
@@ -1045,7 +1029,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1045
1029
|
path: r.path,
|
|
1046
1030
|
params: r.params,
|
|
1047
1031
|
controllerRef: r.controllerRef,
|
|
1048
|
-
contractSource: c.contractSource
|
|
1032
|
+
contractSource: c.contractSource,
|
|
1033
|
+
route: r
|
|
1049
1034
|
};
|
|
1050
1035
|
insertIntoTree(tree, segments, leaf, name);
|
|
1051
1036
|
}
|
|
@@ -1058,7 +1043,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1058
1043
|
lines.push(" return {");
|
|
1059
1044
|
lines.push(
|
|
1060
1045
|
...emitApiObjectBlock(tree, 4, {
|
|
1061
|
-
...transport ? { transport } : {},
|
|
1062
1046
|
...layer ? { layer } : {},
|
|
1063
1047
|
memberExts,
|
|
1064
1048
|
ctx
|
|
@@ -1069,61 +1053,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1069
1053
|
lines.push("");
|
|
1070
1054
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
1071
1055
|
lines.push("");
|
|
1072
|
-
lines.push(
|
|
1073
|
-
lines.push(
|
|
1074
|
-
lines.push(
|
|
1075
|
-
lines.push("");
|
|
1076
|
-
lines.push(
|
|
1077
|
-
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
|
|
1078
|
-
);
|
|
1079
|
-
lines.push("");
|
|
1080
|
-
lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
|
|
1081
|
-
lines.push(" ? T");
|
|
1082
|
-
lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
|
|
1083
|
-
lines.push("");
|
|
1084
|
-
lines.push(
|
|
1085
|
-
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
|
|
1086
|
-
);
|
|
1087
|
-
lines.push(" ? L extends { method: M; url: U }");
|
|
1088
|
-
lines.push(" ? Field extends keyof L ? L[Field] : never");
|
|
1089
|
-
lines.push(" : never");
|
|
1090
|
-
lines.push(" : never;");
|
|
1091
|
-
lines.push("");
|
|
1092
|
-
lines.push("export namespace Route {");
|
|
1093
|
-
lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
|
|
1094
|
-
lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
|
|
1095
|
-
lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
|
|
1096
|
-
lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
|
|
1097
|
-
lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
|
|
1098
|
-
lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
|
|
1099
|
-
lines.push(" export type Request<K extends string> = {");
|
|
1100
|
-
lines.push(" body: Body<K>;");
|
|
1101
|
-
lines.push(" query: Query<K>;");
|
|
1102
|
-
lines.push(" params: Params<K>;");
|
|
1103
|
-
lines.push(" };");
|
|
1104
|
-
lines.push("}");
|
|
1105
|
-
lines.push("");
|
|
1106
|
-
lines.push("export namespace Path {");
|
|
1107
|
-
lines.push(
|
|
1108
|
-
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
|
|
1109
|
-
);
|
|
1110
|
-
lines.push(
|
|
1111
|
-
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
|
|
1112
|
-
);
|
|
1113
|
-
lines.push(
|
|
1114
|
-
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
|
|
1115
|
-
);
|
|
1116
|
-
lines.push(
|
|
1117
|
-
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
|
|
1118
|
-
);
|
|
1119
|
-
lines.push(
|
|
1120
|
-
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
|
|
1121
|
-
);
|
|
1122
|
-
lines.push(
|
|
1123
|
-
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
|
|
1124
|
-
);
|
|
1125
|
-
lines.push("}");
|
|
1126
|
-
lines.push("");
|
|
1056
|
+
lines.push(...RESOLVER_HELPERS);
|
|
1057
|
+
lines.push(...ROUTE_NAMESPACE);
|
|
1058
|
+
lines.push(...PATH_NAMESPACE);
|
|
1127
1059
|
for (const ext of headerExts) {
|
|
1128
1060
|
const statements = ext.apiHeader?.(ctx)?.statements;
|
|
1129
1061
|
if (statements?.length) {
|
|
@@ -1135,7 +1067,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1135
1067
|
|
|
1136
1068
|
// src/emit/emit-cache.ts
|
|
1137
1069
|
import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
1138
|
-
import { join as
|
|
1070
|
+
import { join as join5 } from "path";
|
|
1139
1071
|
async function emitCache(pages, outDir) {
|
|
1140
1072
|
await mkdir2(outDir, { recursive: true });
|
|
1141
1073
|
const entries = await Promise.all(
|
|
@@ -1149,95 +1081,21 @@ async function emitCache(pages, outDir) {
|
|
|
1149
1081
|
})
|
|
1150
1082
|
);
|
|
1151
1083
|
const cache = { pages: entries };
|
|
1152
|
-
await writeFile2(
|
|
1084
|
+
await writeFile2(join5(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
|
|
1153
1085
|
`, "utf8");
|
|
1154
1086
|
}
|
|
1155
1087
|
|
|
1156
1088
|
// src/emit/emit-forms.ts
|
|
1157
1089
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1158
|
-
import { join as
|
|
1090
|
+
import { join as join6, relative as relative4 } from "path";
|
|
1159
1091
|
async function emitForms(routes, outDir, config, adapter) {
|
|
1160
1092
|
if (config && config.enabled === false) return false;
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if (content2 === null) return false;
|
|
1164
|
-
await mkdir3(outDir, { recursive: true });
|
|
1165
|
-
await writeFile3(join5(outDir, "forms.ts"), content2, "utf8");
|
|
1166
|
-
return true;
|
|
1167
|
-
}
|
|
1168
|
-
const entries = collectFormEntries(routes);
|
|
1169
|
-
if (entries.length === 0) return false;
|
|
1093
|
+
const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
|
|
1094
|
+
if (content === null) return false;
|
|
1170
1095
|
await mkdir3(outDir, { recursive: true });
|
|
1171
|
-
|
|
1172
|
-
await writeFile3(join5(outDir, "forms.ts"), content, "utf8");
|
|
1096
|
+
await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
|
|
1173
1097
|
return true;
|
|
1174
1098
|
}
|
|
1175
|
-
function buildFormsFileWithAdapter(routes, adapter) {
|
|
1176
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1177
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1178
|
-
for (const route of sorted) {
|
|
1179
|
-
const cs = route.contract.contractSource;
|
|
1180
|
-
if (!cs.bodySchema && !cs.querySchema) continue;
|
|
1181
|
-
methodNameCounts.set(
|
|
1182
|
-
deriveBaseName(route.name).method,
|
|
1183
|
-
(methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
const named = /* @__PURE__ */ new Map();
|
|
1187
|
-
const decls = [];
|
|
1188
|
-
const mapEntries = [];
|
|
1189
|
-
let used = false;
|
|
1190
|
-
for (const route of sorted) {
|
|
1191
|
-
const cs = route.contract.contractSource;
|
|
1192
|
-
const { method, full } = deriveBaseName(route.name);
|
|
1193
|
-
const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
|
|
1194
|
-
const block = [];
|
|
1195
|
-
if (cs.bodyZodText && !cs.bodySchema) {
|
|
1196
|
-
block.push(
|
|
1197
|
-
`// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
let bodyConst;
|
|
1201
|
-
if (cs.bodySchema) {
|
|
1202
|
-
used = true;
|
|
1203
|
-
const r = adapter.renderModule(cs.bodySchema);
|
|
1204
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
1205
|
-
bodyConst = `${base}BodySchema`;
|
|
1206
|
-
block.push(`export const ${bodyConst} = ${r.schemaText};`);
|
|
1207
|
-
block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
|
|
1208
|
-
}
|
|
1209
|
-
if (cs.querySchema) {
|
|
1210
|
-
used = true;
|
|
1211
|
-
const r = adapter.renderModule(cs.querySchema);
|
|
1212
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
1213
|
-
const queryConst = `${base}QuerySchema`;
|
|
1214
|
-
block.push(`export const ${queryConst} = ${r.schemaText};`);
|
|
1215
|
-
block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
|
|
1216
|
-
}
|
|
1217
|
-
if (block.length === 0) continue;
|
|
1218
|
-
decls.push(`// ${route.name}`, ...block, "");
|
|
1219
|
-
if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
|
|
1220
|
-
}
|
|
1221
|
-
if (!used) return null;
|
|
1222
|
-
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1223
|
-
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1224
|
-
lines.push("");
|
|
1225
|
-
if (named.size > 0) {
|
|
1226
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1227
|
-
for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
|
|
1228
|
-
lines.push("");
|
|
1229
|
-
}
|
|
1230
|
-
lines.push(...decls);
|
|
1231
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
1232
|
-
lines.push("export const formSchemas = {");
|
|
1233
|
-
lines.push(...mapEntries);
|
|
1234
|
-
lines.push("} as const;");
|
|
1235
|
-
lines.push("");
|
|
1236
|
-
return lines.join("\n");
|
|
1237
|
-
}
|
|
1238
|
-
function hasSchema(src) {
|
|
1239
|
-
return !!src && (src.ref !== null || src.text !== null);
|
|
1240
|
-
}
|
|
1241
1099
|
function pascal(segment) {
|
|
1242
1100
|
return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1243
1101
|
}
|
|
@@ -1247,37 +1105,6 @@ function deriveBaseName(routeName) {
|
|
|
1247
1105
|
const full = segments.map(pascal).join("");
|
|
1248
1106
|
return { method, full };
|
|
1249
1107
|
}
|
|
1250
|
-
function collectFormEntries(routes) {
|
|
1251
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1252
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1253
|
-
const candidates = [];
|
|
1254
|
-
for (const route of sorted) {
|
|
1255
|
-
const cs = route.contract.contractSource;
|
|
1256
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
1257
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
1258
|
-
if (!hasSchema(body) && !hasSchema(query)) continue;
|
|
1259
|
-
const { method, full } = deriveBaseName(route.name);
|
|
1260
|
-
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
1261
|
-
candidates.push({ route, method, full });
|
|
1262
|
-
}
|
|
1263
|
-
const entries = [];
|
|
1264
|
-
for (const { route, method, full } of candidates) {
|
|
1265
|
-
const cs = route.contract.contractSource;
|
|
1266
|
-
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
1267
|
-
const baseName = collision ? full : method;
|
|
1268
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
1269
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
1270
|
-
entries.push({
|
|
1271
|
-
routeName: route.name,
|
|
1272
|
-
baseName,
|
|
1273
|
-
body: hasSchema(body) ? body : void 0,
|
|
1274
|
-
query: hasSchema(query) ? query : void 0,
|
|
1275
|
-
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
1276
|
-
warnings: cs.formWarnings ?? []
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
return entries;
|
|
1280
|
-
}
|
|
1281
1108
|
function relImport(outDir, filePath) {
|
|
1282
1109
|
let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
|
|
1283
1110
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
@@ -1286,85 +1113,8 @@ function relImport(outDir, filePath) {
|
|
|
1286
1113
|
function refRootIdentifier(refName) {
|
|
1287
1114
|
return refName.split(".")[0] ?? refName;
|
|
1288
1115
|
}
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
const lines = [
|
|
1292
|
-
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1293
|
-
`import { z } from '${zodImport}';`
|
|
1294
|
-
];
|
|
1295
|
-
const importsByFile = /* @__PURE__ */ new Map();
|
|
1296
|
-
const refAlias = /* @__PURE__ */ new Map();
|
|
1297
|
-
for (const entry of entries) {
|
|
1298
|
-
for (const src of [entry.body, entry.query]) {
|
|
1299
|
-
if (src?.ref && !src.text) {
|
|
1300
|
-
const root = refRootIdentifier(src.ref.name);
|
|
1301
|
-
const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
|
|
1302
|
-
set.add(root);
|
|
1303
|
-
importsByFile.set(src.ref.filePath, set);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
if (importsByFile.size > 0) {
|
|
1308
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
1309
|
-
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
1310
|
-
const relPath = relImport(outDir, filePath);
|
|
1311
|
-
const specifiers = [];
|
|
1312
|
-
for (const root of [...roots].sort()) {
|
|
1313
|
-
if (emitted.has(root)) {
|
|
1314
|
-
const alias = `${root}_${emitted.size}`;
|
|
1315
|
-
specifiers.push(`${root} as ${alias}`);
|
|
1316
|
-
emitted.add(alias);
|
|
1317
|
-
refAlias.set(`${filePath}\0${root}`, alias);
|
|
1318
|
-
} else {
|
|
1319
|
-
specifiers.push(root);
|
|
1320
|
-
emitted.add(root);
|
|
1321
|
-
refAlias.set(`${filePath}\0${root}`, root);
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
lines.push("");
|
|
1328
|
-
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
1329
|
-
if (globalSchemas.size > 0) {
|
|
1330
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1331
|
-
for (const [name, text] of globalSchemas) {
|
|
1332
|
-
lines.push(`const ${name} = ${text};`);
|
|
1333
|
-
}
|
|
1334
|
-
lines.push("");
|
|
1335
|
-
}
|
|
1336
|
-
const mapEntries = [];
|
|
1337
|
-
for (const entry of entries) {
|
|
1338
|
-
lines.push(`// ${entry.routeName}`);
|
|
1339
|
-
if (entry.warnings && entry.warnings.length > 0) {
|
|
1340
|
-
for (const w of entry.warnings) {
|
|
1341
|
-
lines.push(`// warning: ${w}`);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
const rename = renamesByEntry.get(entry) ?? null;
|
|
1345
|
-
if (entry.body) {
|
|
1346
|
-
const schemaName = `${entry.baseName}BodySchema`;
|
|
1347
|
-
const typeName = `${entry.baseName}Body`;
|
|
1348
|
-
const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
|
|
1349
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
1350
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
1351
|
-
mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
|
|
1352
|
-
}
|
|
1353
|
-
if (entry.query) {
|
|
1354
|
-
const schemaName = `${entry.baseName}QuerySchema`;
|
|
1355
|
-
const typeName = `${entry.baseName}Query`;
|
|
1356
|
-
const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
|
|
1357
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
1358
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
1359
|
-
}
|
|
1360
|
-
lines.push("");
|
|
1361
|
-
}
|
|
1362
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
1363
|
-
lines.push("export const formSchemas = {");
|
|
1364
|
-
lines.push(...mapEntries);
|
|
1365
|
-
lines.push("} as const;");
|
|
1366
|
-
lines.push("");
|
|
1367
|
-
return lines.join("\n");
|
|
1116
|
+
function hasSource(src) {
|
|
1117
|
+
return !!(src.schema || src.zodText || src.zodRef);
|
|
1368
1118
|
}
|
|
1369
1119
|
function applyRenames(text, renames) {
|
|
1370
1120
|
if (!renames || renames.size === 0) return text;
|
|
@@ -1430,38 +1180,190 @@ function planNestedSchemas(entries) {
|
|
|
1430
1180
|
}
|
|
1431
1181
|
return { globalSchemas, renamesByEntry };
|
|
1432
1182
|
}
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1183
|
+
function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
|
|
1184
|
+
const acceptsRawZod = adapter.acceptsRawZodSource === true;
|
|
1185
|
+
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1186
|
+
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1187
|
+
const candidates = [];
|
|
1188
|
+
for (const route of sorted) {
|
|
1189
|
+
const cs = route.contract.contractSource;
|
|
1190
|
+
const body = {
|
|
1191
|
+
schema: cs.bodySchema ?? null,
|
|
1192
|
+
zodText: cs.bodyZodText ?? null,
|
|
1193
|
+
zodRef: cs.bodyZodRef ?? null
|
|
1194
|
+
};
|
|
1195
|
+
const query = {
|
|
1196
|
+
schema: cs.querySchema ?? null,
|
|
1197
|
+
zodText: cs.queryZodText ?? null,
|
|
1198
|
+
zodRef: cs.queryZodRef ?? null
|
|
1199
|
+
};
|
|
1200
|
+
if (!hasSource(body) && !hasSource(query)) continue;
|
|
1201
|
+
const { method, full } = deriveBaseName(route.name);
|
|
1202
|
+
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
1203
|
+
candidates.push({
|
|
1204
|
+
routeName: route.name,
|
|
1205
|
+
baseName: full,
|
|
1206
|
+
// resolved below
|
|
1207
|
+
body: hasSource(body) ? body : void 0,
|
|
1208
|
+
query: hasSource(query) ? query : void 0,
|
|
1209
|
+
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
1210
|
+
warnings: cs.formWarnings ?? []
|
|
1211
|
+
});
|
|
1440
1212
|
}
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1213
|
+
const entries = candidates.map((c) => {
|
|
1214
|
+
const { method, full } = deriveBaseName(c.routeName);
|
|
1215
|
+
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
1216
|
+
return { ...c, baseName: collision ? full : method };
|
|
1217
|
+
});
|
|
1218
|
+
if (entries.length === 0) return null;
|
|
1219
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
1220
|
+
const refAlias = /* @__PURE__ */ new Map();
|
|
1221
|
+
for (const entry of entries) {
|
|
1222
|
+
for (const src of [entry.body, entry.query]) {
|
|
1223
|
+
if (src?.zodRef && !src.zodText && !src.schema) {
|
|
1224
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1225
|
+
const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
|
|
1226
|
+
set.add(root);
|
|
1227
|
+
importsByFile.set(src.zodRef.filePath, set);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1452
1230
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1231
|
+
const importLines = [];
|
|
1232
|
+
if (importsByFile.size > 0) {
|
|
1233
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1234
|
+
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
1235
|
+
const relPath = relImport(outDir, filePath);
|
|
1236
|
+
const specifiers = [];
|
|
1237
|
+
for (const root of [...roots].sort()) {
|
|
1238
|
+
if (emitted.has(root)) {
|
|
1239
|
+
const alias = `${root}_${emitted.size}`;
|
|
1240
|
+
specifiers.push(`${root} as ${alias}`);
|
|
1241
|
+
emitted.add(alias);
|
|
1242
|
+
refAlias.set(`${filePath}\0${root}`, alias);
|
|
1243
|
+
} else {
|
|
1244
|
+
specifiers.push(root);
|
|
1245
|
+
emitted.add(root);
|
|
1246
|
+
refAlias.set(`${filePath}\0${root}`, root);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
1250
|
+
}
|
|
1455
1251
|
}
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1252
|
+
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
1253
|
+
const irNamed = /* @__PURE__ */ new Map();
|
|
1254
|
+
const decls = [];
|
|
1255
|
+
const mapEntries = [];
|
|
1256
|
+
let used = false;
|
|
1257
|
+
const renderSource = (src, rename) => {
|
|
1258
|
+
if (src.schema) {
|
|
1259
|
+
const r = adapter.renderModule(src.schema);
|
|
1260
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
1261
|
+
return { text: r.schemaText };
|
|
1262
|
+
}
|
|
1263
|
+
if (src.zodText) {
|
|
1264
|
+
if (!acceptsRawZod) {
|
|
1265
|
+
return {
|
|
1266
|
+
text: "",
|
|
1267
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
1271
|
+
}
|
|
1272
|
+
if (src.zodRef) {
|
|
1273
|
+
if (!acceptsRawZod) {
|
|
1274
|
+
return {
|
|
1275
|
+
text: "",
|
|
1276
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1280
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
1281
|
+
const member = src.zodRef.name.slice(root.length);
|
|
1282
|
+
return { text: `${alias}${member}` };
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
};
|
|
1286
|
+
for (const entry of entries) {
|
|
1287
|
+
const block = [];
|
|
1288
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
1289
|
+
let bodyConst;
|
|
1290
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
1291
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
1292
|
+
}
|
|
1293
|
+
if (entry.body) {
|
|
1294
|
+
const rendered = renderSource(entry.body, rename);
|
|
1295
|
+
if (rendered?.warn) {
|
|
1296
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
1297
|
+
} else if (rendered) {
|
|
1298
|
+
used = true;
|
|
1299
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
1300
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
1301
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (entry.query) {
|
|
1305
|
+
const rendered = renderSource(entry.query, rename);
|
|
1306
|
+
if (rendered?.warn) {
|
|
1307
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
1308
|
+
} else if (rendered) {
|
|
1309
|
+
used = true;
|
|
1310
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
1311
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
1312
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (block.length === 0) continue;
|
|
1316
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
1317
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
1318
|
+
}
|
|
1319
|
+
if (!used) return null;
|
|
1320
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1321
|
+
if (acceptsRawZod) {
|
|
1322
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
1323
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
1324
|
+
} else {
|
|
1325
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1326
|
+
}
|
|
1327
|
+
lines.push(...importLines);
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
1330
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
1331
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
1332
|
+
if (allNested.size > 0) {
|
|
1333
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1334
|
+
for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
|
|
1335
|
+
lines.push("");
|
|
1336
|
+
}
|
|
1337
|
+
lines.push(...decls);
|
|
1338
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
1339
|
+
lines.push("export const formSchemas = {");
|
|
1340
|
+
lines.push(...mapEntries);
|
|
1341
|
+
lines.push("} as const;");
|
|
1342
|
+
lines.push("");
|
|
1343
|
+
return lines.join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// src/emit/emit-index.ts
|
|
1347
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1348
|
+
import { join as join7 } from "path";
|
|
1349
|
+
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
1350
|
+
await mkdir4(outDir, { recursive: true });
|
|
1351
|
+
const exports = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
1352
|
+
if (hasContracts) {
|
|
1353
|
+
exports.push("export * from './api.js';");
|
|
1354
|
+
}
|
|
1355
|
+
if (hasForms) {
|
|
1356
|
+
exports.push("export * from './forms.js';");
|
|
1357
|
+
}
|
|
1358
|
+
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
|
|
1359
|
+
"\n"
|
|
1360
|
+
);
|
|
1361
|
+
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/emit/emit-pages.ts
|
|
1463
1365
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1464
|
-
import { join as
|
|
1366
|
+
import { join as join8, relative as relative5 } from "path";
|
|
1465
1367
|
async function emitPages(pages, outDir, _options = {}) {
|
|
1466
1368
|
await mkdir5(outDir, { recursive: true });
|
|
1467
1369
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
@@ -1482,7 +1384,7 @@ ${augBody}
|
|
|
1482
1384
|
}
|
|
1483
1385
|
${sharedPropsBlock}}
|
|
1484
1386
|
`;
|
|
1485
|
-
await writeFile5(
|
|
1387
|
+
await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
|
|
1486
1388
|
}
|
|
1487
1389
|
function buildSharedPropsBlock(sharedProps) {
|
|
1488
1390
|
if (!sharedProps) return "";
|
|
@@ -1513,11 +1415,11 @@ function needsQuotes(name) {
|
|
|
1513
1415
|
|
|
1514
1416
|
// src/emit/emit-routes.ts
|
|
1515
1417
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1516
|
-
import { join as
|
|
1418
|
+
import { join as join9 } from "path";
|
|
1517
1419
|
async function emitRoutes(routes, outDir) {
|
|
1518
1420
|
await mkdir6(outDir, { recursive: true });
|
|
1519
1421
|
const content = buildRoutesFile(routes);
|
|
1520
|
-
await writeFile6(
|
|
1422
|
+
await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
|
|
1521
1423
|
}
|
|
1522
1424
|
function buildRoutesFile(routes) {
|
|
1523
1425
|
if (routes.length === 0) {
|
|
@@ -1645,30 +1547,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1645
1547
|
propsExport: pagesConfig.propsExport,
|
|
1646
1548
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
1647
1549
|
});
|
|
1648
|
-
|
|
1649
|
-
if (config.app?.moduleEntry) {
|
|
1650
|
-
try {
|
|
1651
|
-
const tsconfigPath = config.app.tsconfig ?? join9(config.codegen.cwd, "tsconfig.json");
|
|
1652
|
-
let project;
|
|
1653
|
-
try {
|
|
1654
|
-
project = new Project2({
|
|
1655
|
-
tsConfigFilePath: tsconfigPath,
|
|
1656
|
-
skipAddingFilesFromTsConfig: true,
|
|
1657
|
-
skipLoadingLibFiles: true,
|
|
1658
|
-
skipFileDependencyResolution: true
|
|
1659
|
-
});
|
|
1660
|
-
} catch {
|
|
1661
|
-
project = new Project2({
|
|
1662
|
-
skipAddingFilesFromTsConfig: true,
|
|
1663
|
-
skipLoadingLibFiles: true,
|
|
1664
|
-
skipFileDependencyResolution: true,
|
|
1665
|
-
compilerOptions: { allowJs: true, strict: false }
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
sharedProps = discoverSharedProps(project, config.app.moduleEntry);
|
|
1669
|
-
} catch {
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1550
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
1672
1551
|
await emitPages(pages, config.codegen.outDir, {
|
|
1673
1552
|
propsExport: pagesConfig.propsExport,
|
|
1674
1553
|
sharedProps
|
|
@@ -1692,7 +1571,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1692
1571
|
if (extensions.length > 0) {
|
|
1693
1572
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1694
1573
|
for (const file of extraFiles) {
|
|
1695
|
-
const dest =
|
|
1574
|
+
const dest = join10(config.codegen.outDir, file.path);
|
|
1696
1575
|
await mkdir7(dirname2(dest), { recursive: true });
|
|
1697
1576
|
await writeFile7(dest, file.contents, "utf8");
|
|
1698
1577
|
}
|
|
@@ -1701,15 +1580,20 @@ async function generate(config, inputRoutes = []) {
|
|
|
1701
1580
|
|
|
1702
1581
|
// src/watch/watcher.ts
|
|
1703
1582
|
import { readFile as readFile3 } from "fs/promises";
|
|
1704
|
-
import { join as
|
|
1583
|
+
import { join as join13 } from "path";
|
|
1705
1584
|
import chokidar from "chokidar";
|
|
1706
1585
|
|
|
1707
1586
|
// src/discovery/contracts-fast.ts
|
|
1708
|
-
import { join as
|
|
1587
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
1709
1588
|
import fg2 from "fast-glob";
|
|
1710
1589
|
import {
|
|
1711
|
-
Node as
|
|
1712
|
-
Project as Project3
|
|
1590
|
+
Node as Node8,
|
|
1591
|
+
Project as Project3
|
|
1592
|
+
} from "ts-morph";
|
|
1593
|
+
|
|
1594
|
+
// src/discovery/dto-type-resolver.ts
|
|
1595
|
+
import {
|
|
1596
|
+
Node as Node6,
|
|
1713
1597
|
SyntaxKind as SyntaxKind3
|
|
1714
1598
|
} from "ts-morph";
|
|
1715
1599
|
|
|
@@ -1724,20 +1608,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
|
1724
1608
|
import {
|
|
1725
1609
|
Node as Node2
|
|
1726
1610
|
} from "ts-morph";
|
|
1727
|
-
var
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
return prev;
|
|
1732
|
-
}
|
|
1733
|
-
function restoreDiscoveryContext(ctx) {
|
|
1734
|
-
_ctx = ctx;
|
|
1611
|
+
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
1612
|
+
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
1613
|
+
function setDiscoveryContext(project, ctx) {
|
|
1614
|
+
_ctxByProject.set(project, ctx);
|
|
1735
1615
|
}
|
|
1736
|
-
function
|
|
1737
|
-
return
|
|
1738
|
-
}
|
|
1739
|
-
function _tsconfigPaths() {
|
|
1740
|
-
return _ctx.tsconfigPaths;
|
|
1616
|
+
function _ctxFor(project) {
|
|
1617
|
+
return _ctxByProject.get(project) ?? _EMPTY_CTX;
|
|
1741
1618
|
}
|
|
1742
1619
|
var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
|
|
1743
1620
|
function dbg(...args) {
|
|
@@ -1779,7 +1656,7 @@ function findTypeInFile(name, file) {
|
|
|
1779
1656
|
}
|
|
1780
1657
|
return null;
|
|
1781
1658
|
}
|
|
1782
|
-
function resolveModuleSpecifier(moduleSpecifier, sourceFile,
|
|
1659
|
+
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
1783
1660
|
if (moduleSpecifier.startsWith(".")) {
|
|
1784
1661
|
const dir = dirname3(sourceFile.getFilePath());
|
|
1785
1662
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
@@ -1789,8 +1666,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
|
|
|
1789
1666
|
resolve2(dir, moduleSpecifier, "index.ts")
|
|
1790
1667
|
];
|
|
1791
1668
|
}
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
1669
|
+
const ctx = _ctxFor(project);
|
|
1670
|
+
const baseUrl = ctx.projectRoot;
|
|
1671
|
+
const tsconfigPaths = ctx.tsconfigPaths;
|
|
1794
1672
|
dbg(
|
|
1795
1673
|
"resolveModuleSpecifier",
|
|
1796
1674
|
moduleSpecifier,
|
|
@@ -1936,7 +1814,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
1936
1814
|
if (!namedImport) continue;
|
|
1937
1815
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1938
1816
|
if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
1939
|
-
const tsconfigPaths =
|
|
1817
|
+
const tsconfigPaths = _ctxFor(project).tsconfigPaths;
|
|
1940
1818
|
const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
|
|
1941
1819
|
const prefix = p.replace("*", "");
|
|
1942
1820
|
return moduleSpecifier.startsWith(prefix);
|
|
@@ -2289,465 +2167,135 @@ function inSchemaFromDecorator(decorator) {
|
|
|
2289
2167
|
return null;
|
|
2290
2168
|
}
|
|
2291
2169
|
|
|
2292
|
-
// src/discovery/
|
|
2170
|
+
// src/discovery/filter-for.ts
|
|
2293
2171
|
import {
|
|
2294
|
-
Node as
|
|
2172
|
+
Node as Node5
|
|
2295
2173
|
} from "ts-morph";
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
"
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
"IsOptional",
|
|
2316
|
-
"IsNotEmpty",
|
|
2317
|
-
"IsArray",
|
|
2318
|
-
"ValidateNested",
|
|
2319
|
-
"Type",
|
|
2320
|
-
"IsObject",
|
|
2321
|
-
"Allow",
|
|
2322
|
-
"IsDefined"
|
|
2323
|
-
]);
|
|
2324
|
-
function extractZodFromDto(classDecl, sourceFile, project) {
|
|
2325
|
-
const ctx = {
|
|
2326
|
-
sourceFile,
|
|
2327
|
-
project,
|
|
2328
|
-
namedNestedSchemas: /* @__PURE__ */ new Map(),
|
|
2329
|
-
warnings: [],
|
|
2330
|
-
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
2331
|
-
emittedClasses: /* @__PURE__ */ new Map(),
|
|
2332
|
-
visiting: /* @__PURE__ */ new Set(),
|
|
2333
|
-
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
2334
|
-
depth: 0
|
|
2335
|
-
};
|
|
2336
|
-
const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
|
|
2337
|
-
for (const schemaName of ctx.recursiveSchemas) {
|
|
2338
|
-
ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
|
|
2339
|
-
}
|
|
2340
|
-
return {
|
|
2341
|
-
schemaText,
|
|
2342
|
-
namedNestedSchemas: ctx.namedNestedSchemas,
|
|
2343
|
-
warnings: ctx.warnings
|
|
2344
|
-
};
|
|
2174
|
+
|
|
2175
|
+
// src/discovery/filter-field-types.ts
|
|
2176
|
+
import {
|
|
2177
|
+
Node as Node4,
|
|
2178
|
+
SyntaxKind as SyntaxKind2
|
|
2179
|
+
} from "ts-morph";
|
|
2180
|
+
|
|
2181
|
+
// src/discovery/enum-resolution.ts
|
|
2182
|
+
function resolveEnumValues(name, sourceFile, project) {
|
|
2183
|
+
const resolved = findType(name, sourceFile, project);
|
|
2184
|
+
if (!resolved || resolved.kind !== "enum") return null;
|
|
2185
|
+
let numeric = true;
|
|
2186
|
+
const values = resolved.members.map((m) => {
|
|
2187
|
+
const parsed = JSON.parse(m);
|
|
2188
|
+
if (typeof parsed === "string") numeric = false;
|
|
2189
|
+
return String(parsed);
|
|
2190
|
+
});
|
|
2191
|
+
if (values.length === 0) return null;
|
|
2192
|
+
return { values, numeric };
|
|
2345
2193
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2194
|
+
|
|
2195
|
+
// src/discovery/filter-field-types.ts
|
|
2196
|
+
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2197
|
+
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2198
|
+
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2199
|
+
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2200
|
+
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2201
|
+
function classifyTypeKeyword(raw) {
|
|
2202
|
+
const t = raw.toLowerCase();
|
|
2203
|
+
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2204
|
+
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2205
|
+
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2206
|
+
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2207
|
+
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2208
|
+
return null;
|
|
2358
2209
|
}
|
|
2359
|
-
function
|
|
2360
|
-
return
|
|
2210
|
+
function markNullable(r, nullable) {
|
|
2211
|
+
return nullable ? { ...r, nullable: true } : r;
|
|
2361
2212
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
if (has("IsInt")) base = "z.number().int()";
|
|
2389
|
-
if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
|
|
2390
|
-
if (has("Allow")) base = "z.unknown()";
|
|
2391
|
-
if (has("IsEmail")) {
|
|
2392
|
-
base = ensureStringBase(base);
|
|
2393
|
-
refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
|
|
2394
|
-
}
|
|
2395
|
-
if (has("IsUrl")) {
|
|
2396
|
-
base = ensureStringBase(base);
|
|
2397
|
-
refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
|
|
2398
|
-
}
|
|
2399
|
-
if (has("IsUUID")) {
|
|
2400
|
-
base = ensureStringBase(base);
|
|
2401
|
-
refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
|
|
2402
|
-
}
|
|
2403
|
-
if (has("Matches")) {
|
|
2404
|
-
const re = firstArgText2(dec("Matches"));
|
|
2405
|
-
if (re) {
|
|
2406
|
-
base = ensureStringBase(base);
|
|
2407
|
-
refinements.push(`.regex(${re})`);
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
if (has("MinLength")) {
|
|
2411
|
-
const n = numericArg2(dec("MinLength"));
|
|
2412
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
2413
|
-
}
|
|
2414
|
-
if (has("MaxLength")) {
|
|
2415
|
-
const n = numericArg2(dec("MaxLength"));
|
|
2416
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
2417
|
-
}
|
|
2418
|
-
if (has("Length")) {
|
|
2419
|
-
const [min, max] = numericArgs2(dec("Length"));
|
|
2420
|
-
if (min !== null) refinements.push(`.min(${min})`);
|
|
2421
|
-
if (max !== null) refinements.push(`.max(${max})`);
|
|
2422
|
-
}
|
|
2423
|
-
if (has("Min")) {
|
|
2424
|
-
const n = numericArg2(dec("Min"));
|
|
2425
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
2426
|
-
}
|
|
2427
|
-
if (has("Max")) {
|
|
2428
|
-
const n = numericArg2(dec("Max"));
|
|
2429
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
2430
|
-
}
|
|
2431
|
-
if (has("IsPositive")) refinements.push(".positive()");
|
|
2432
|
-
if (has("IsNegative")) refinements.push(".negative()");
|
|
2433
|
-
if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
|
|
2434
|
-
if (has("IsEnum")) {
|
|
2435
|
-
const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
|
|
2436
|
-
if (enumExpr) base = enumExpr;
|
|
2437
|
-
}
|
|
2438
|
-
if (has("IsIn")) {
|
|
2439
|
-
const inExpr = inSchemaFromDecorator2(dec("IsIn"));
|
|
2440
|
-
if (inExpr) base = inExpr;
|
|
2441
|
-
}
|
|
2442
|
-
for (const name of decorators.keys()) {
|
|
2443
|
-
if (!KNOWN_DECORATORS2.has(name)) {
|
|
2444
|
-
comments.push(`/* @${name}: not translatable to zod (server-only) */`);
|
|
2445
|
-
if (!ctx.warnedDecorators.has(name)) {
|
|
2446
|
-
ctx.warnedDecorators.add(name);
|
|
2447
|
-
const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
|
|
2448
|
-
ctx.warnings.push(msg);
|
|
2449
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2213
|
+
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2214
|
+
if (Node4.isUnionTypeNode(typeNode)) {
|
|
2215
|
+
let nullable = false;
|
|
2216
|
+
const stringLits = [];
|
|
2217
|
+
const numberLits = [];
|
|
2218
|
+
const others = [];
|
|
2219
|
+
for (const member of typeNode.getTypeNodes()) {
|
|
2220
|
+
const kind = member.getKind();
|
|
2221
|
+
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2222
|
+
nullable = true;
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (Node4.isLiteralTypeNode(member)) {
|
|
2226
|
+
const lit = member.getLiteral();
|
|
2227
|
+
if (Node4.isStringLiteral(lit)) {
|
|
2228
|
+
stringLits.push(lit.getLiteralValue());
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
if (Node4.isNumericLiteral(lit)) {
|
|
2232
|
+
numberLits.push(lit.getText());
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2236
|
+
nullable = true;
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2450
2239
|
}
|
|
2240
|
+
others.push(member);
|
|
2241
|
+
}
|
|
2242
|
+
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2243
|
+
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2244
|
+
}
|
|
2245
|
+
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2246
|
+
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2247
|
+
}
|
|
2248
|
+
if (others.length === 1) {
|
|
2249
|
+
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2250
|
+
return markNullable(inner, nullable || inner.nullable === true);
|
|
2451
2251
|
}
|
|
2252
|
+
return markNullable({ kind: "unknown" }, nullable);
|
|
2452
2253
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2254
|
+
switch (typeNode.getKind()) {
|
|
2255
|
+
case SyntaxKind2.StringKeyword:
|
|
2256
|
+
return { kind: "string" };
|
|
2257
|
+
case SyntaxKind2.NumberKeyword:
|
|
2258
|
+
return { kind: "number" };
|
|
2259
|
+
case SyntaxKind2.BooleanKeyword:
|
|
2260
|
+
return { kind: "boolean" };
|
|
2261
|
+
case SyntaxKind2.AnyKeyword:
|
|
2262
|
+
case SyntaxKind2.UnknownKeyword:
|
|
2263
|
+
return { kind: "unknown" };
|
|
2264
|
+
default:
|
|
2265
|
+
break;
|
|
2456
2266
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2267
|
+
if (Node4.isTypeReference(typeNode)) {
|
|
2268
|
+
const refName = typeNode.getTypeName().getText();
|
|
2269
|
+
if (refName === "Date") return { kind: "date" };
|
|
2270
|
+
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
2271
|
+
const typeRef = opts?.resolveRef?.(refName) ?? null;
|
|
2272
|
+
const en = resolveEnumValues(refName, sourceFile, project);
|
|
2273
|
+
if (en) {
|
|
2274
|
+
const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
2275
|
+
return typeRef ? { ...base, typeRef } : base;
|
|
2276
|
+
}
|
|
2277
|
+
if (typeRef) return { kind: "unknown", typeRef };
|
|
2278
|
+
return { kind: "unknown" };
|
|
2460
2279
|
}
|
|
2461
|
-
return
|
|
2462
|
-
}
|
|
2463
|
-
function applyPresence2(expr, decorators) {
|
|
2464
|
-
if (decorators.has("IsDefined")) return expr;
|
|
2465
|
-
if (decorators.has("IsOptional")) return `${expr}.optional()`;
|
|
2466
|
-
return expr;
|
|
2467
|
-
}
|
|
2468
|
-
function baseFromType2(typeText, isArrayType, ctx, classFile) {
|
|
2469
|
-
const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
|
|
2470
|
-
switch (inner) {
|
|
2471
|
-
case "string":
|
|
2472
|
-
return "z.string()";
|
|
2473
|
-
case "number":
|
|
2474
|
-
return "z.number()";
|
|
2475
|
-
case "boolean":
|
|
2476
|
-
return "z.boolean()";
|
|
2477
|
-
case "Date":
|
|
2478
|
-
return "z.coerce.date()";
|
|
2479
|
-
case "File":
|
|
2480
|
-
case "Express.Multer.File":
|
|
2481
|
-
return "z.instanceof(File)";
|
|
2482
|
-
default:
|
|
2483
|
-
return "z.unknown()";
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
function ensureStringBase(base) {
|
|
2487
|
-
return isStringBase(base) ? base : "z.string()";
|
|
2488
|
-
}
|
|
2489
|
-
function isStringBase(base) {
|
|
2490
|
-
return base.startsWith("z.string(");
|
|
2491
|
-
}
|
|
2492
|
-
function buildNestedReference2(className, fromFile, ctx) {
|
|
2493
|
-
if (ctx.visiting.has(className) || ctx.depth >= 8) {
|
|
2494
|
-
const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
|
|
2495
|
-
ctx.emittedClasses.set(className, reserved);
|
|
2496
|
-
ctx.recursiveSchemas.add(reserved);
|
|
2497
|
-
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2498
|
-
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
2499
|
-
const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
|
|
2500
|
-
ctx.warnings.push(msg);
|
|
2501
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2502
|
-
}
|
|
2503
|
-
return `z.lazy(() => ${reserved})`;
|
|
2504
|
-
}
|
|
2505
|
-
const existing = ctx.emittedClasses.get(className);
|
|
2506
|
-
if (existing) return existing;
|
|
2507
|
-
const schemaName = aliasFor2(className, ctx);
|
|
2508
|
-
const resolved = findType(className, fromFile, ctx.project);
|
|
2509
|
-
if (!resolved || resolved.kind !== "class") {
|
|
2510
|
-
return "z.object({}).passthrough()";
|
|
2511
|
-
}
|
|
2512
|
-
ctx.emittedClasses.set(className, schemaName);
|
|
2513
|
-
ctx.visiting.add(className);
|
|
2514
|
-
ctx.depth += 1;
|
|
2515
|
-
const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
|
|
2516
|
-
ctx.depth -= 1;
|
|
2517
|
-
ctx.visiting.delete(className);
|
|
2518
|
-
ctx.namedNestedSchemas.set(schemaName, childText);
|
|
2519
|
-
return schemaName;
|
|
2520
|
-
}
|
|
2521
|
-
function aliasFor2(className, ctx) {
|
|
2522
|
-
const baseName = `${className}Schema`;
|
|
2523
|
-
let candidate = baseName;
|
|
2524
|
-
let i = 1;
|
|
2525
|
-
const used = new Set(ctx.namedNestedSchemas.keys());
|
|
2526
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
2527
|
-
while (used.has(candidate)) {
|
|
2528
|
-
candidate = `${baseName}_${i}`;
|
|
2529
|
-
i += 1;
|
|
2530
|
-
}
|
|
2531
|
-
return candidate;
|
|
2532
|
-
}
|
|
2533
|
-
function firstArg2(decorator) {
|
|
2534
|
-
return decorator?.getArguments()[0];
|
|
2535
|
-
}
|
|
2536
|
-
function firstArgText2(decorator) {
|
|
2537
|
-
const arg = firstArg2(decorator);
|
|
2538
|
-
return arg ? arg.getText() : null;
|
|
2539
|
-
}
|
|
2540
|
-
function numericArg2(decorator) {
|
|
2541
|
-
const arg = firstArg2(decorator);
|
|
2542
|
-
if (arg && Node4.isNumericLiteral(arg)) return arg.getText();
|
|
2543
|
-
return null;
|
|
2544
|
-
}
|
|
2545
|
-
function numericArgs2(decorator) {
|
|
2546
|
-
const args = decorator?.getArguments() ?? [];
|
|
2547
|
-
const num = (n) => n && Node4.isNumericLiteral(n) ? n.getText() : null;
|
|
2548
|
-
return [num(args[0]), num(args[1])];
|
|
2549
|
-
}
|
|
2550
|
-
function messageArg(decorator) {
|
|
2551
|
-
const args = decorator?.getArguments() ?? [];
|
|
2552
|
-
for (const arg of args) {
|
|
2553
|
-
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2554
|
-
for (const prop of arg.getProperties()) {
|
|
2555
|
-
if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
2556
|
-
const init = prop.getInitializer();
|
|
2557
|
-
if (init && Node4.isStringLiteral(init)) {
|
|
2558
|
-
return `{ message: ${init.getText()} }`;
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
return "";
|
|
2565
|
-
}
|
|
2566
|
-
function resolveTypeFactoryName2(decorator) {
|
|
2567
|
-
const arg = firstArg2(decorator);
|
|
2568
|
-
if (!arg) return null;
|
|
2569
|
-
if (Node4.isArrowFunction(arg)) {
|
|
2570
|
-
const body = arg.getBody();
|
|
2571
|
-
if (Node4.isIdentifier(body)) return body.getText();
|
|
2572
|
-
}
|
|
2573
|
-
return null;
|
|
2574
|
-
}
|
|
2575
|
-
function singularClassName2(typeText) {
|
|
2576
|
-
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2577
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2578
|
-
}
|
|
2579
|
-
function enumSchemaFromDecorator2(decorator, classFile, ctx) {
|
|
2580
|
-
const arg = firstArg2(decorator);
|
|
2581
|
-
if (!arg) return null;
|
|
2582
|
-
if (Node4.isIdentifier(arg)) {
|
|
2583
|
-
const name = arg.getText();
|
|
2584
|
-
const resolved = findType(name, classFile, ctx.project);
|
|
2585
|
-
if (resolved && resolved.kind === "enum") {
|
|
2586
|
-
return `z.enum([${resolved.members.join(", ")}])`;
|
|
2587
|
-
}
|
|
2588
|
-
const msg = `@IsEnum(${name}): enum could not be resolved to literal members and is not importable into the generated form schema; falling back to z.unknown().`;
|
|
2589
|
-
if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
|
|
2590
|
-
ctx.warnedDecorators.add(`IsEnum:${name}`);
|
|
2591
|
-
ctx.warnings.push(msg);
|
|
2592
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2593
|
-
}
|
|
2594
|
-
return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
|
|
2595
|
-
}
|
|
2596
|
-
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2597
|
-
const values = [];
|
|
2598
|
-
for (const p of arg.getProperties()) {
|
|
2599
|
-
if (!Node4.isPropertyAssignment(p)) continue;
|
|
2600
|
-
const init = p.getInitializer();
|
|
2601
|
-
if (init && Node4.isStringLiteral(init)) values.push(init.getText());
|
|
2602
|
-
}
|
|
2603
|
-
if (values.length > 0) return `z.enum([${values.join(", ")}])`;
|
|
2604
|
-
}
|
|
2605
|
-
return null;
|
|
2606
|
-
}
|
|
2607
|
-
function inSchemaFromDecorator2(decorator) {
|
|
2608
|
-
const arg = firstArg2(decorator);
|
|
2609
|
-
if (arg && Node4.isArrayLiteralExpression(arg)) {
|
|
2610
|
-
const elements = arg.getElements();
|
|
2611
|
-
const allStrings = elements.every((e) => Node4.isStringLiteral(e));
|
|
2612
|
-
if (allStrings && elements.length > 0) {
|
|
2613
|
-
return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
|
|
2614
|
-
}
|
|
2615
|
-
if (elements.length > 0) {
|
|
2616
|
-
return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
return null;
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
// src/discovery/filter-for.ts
|
|
2623
|
-
import {
|
|
2624
|
-
Node as Node6
|
|
2625
|
-
} from "ts-morph";
|
|
2626
|
-
|
|
2627
|
-
// src/discovery/filter-field-types.ts
|
|
2628
|
-
import {
|
|
2629
|
-
Node as Node5,
|
|
2630
|
-
SyntaxKind as SyntaxKind2
|
|
2631
|
-
} from "ts-morph";
|
|
2632
|
-
|
|
2633
|
-
// src/discovery/enum-resolution.ts
|
|
2634
|
-
function resolveEnumValues(name, sourceFile, project) {
|
|
2635
|
-
const resolved = findType(name, sourceFile, project);
|
|
2636
|
-
if (!resolved || resolved.kind !== "enum") return null;
|
|
2637
|
-
let numeric = true;
|
|
2638
|
-
const values = resolved.members.map((m) => {
|
|
2639
|
-
const parsed = JSON.parse(m);
|
|
2640
|
-
if (typeof parsed === "string") numeric = false;
|
|
2641
|
-
return String(parsed);
|
|
2642
|
-
});
|
|
2643
|
-
if (values.length === 0) return null;
|
|
2644
|
-
return { values, numeric };
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
// src/discovery/filter-field-types.ts
|
|
2648
|
-
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2649
|
-
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2650
|
-
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2651
|
-
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2652
|
-
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2653
|
-
function classifyTypeKeyword(raw) {
|
|
2654
|
-
const t = raw.toLowerCase();
|
|
2655
|
-
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2656
|
-
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2657
|
-
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2658
|
-
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2659
|
-
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2660
|
-
return null;
|
|
2661
|
-
}
|
|
2662
|
-
function markNullable(r, nullable) {
|
|
2663
|
-
return nullable ? { ...r, nullable: true } : r;
|
|
2664
|
-
}
|
|
2665
|
-
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2666
|
-
if (Node5.isUnionTypeNode(typeNode)) {
|
|
2667
|
-
let nullable = false;
|
|
2668
|
-
const stringLits = [];
|
|
2669
|
-
const numberLits = [];
|
|
2670
|
-
const others = [];
|
|
2671
|
-
for (const member of typeNode.getTypeNodes()) {
|
|
2672
|
-
const kind = member.getKind();
|
|
2673
|
-
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2674
|
-
nullable = true;
|
|
2675
|
-
continue;
|
|
2676
|
-
}
|
|
2677
|
-
if (Node5.isLiteralTypeNode(member)) {
|
|
2678
|
-
const lit = member.getLiteral();
|
|
2679
|
-
if (Node5.isStringLiteral(lit)) {
|
|
2680
|
-
stringLits.push(lit.getLiteralValue());
|
|
2681
|
-
continue;
|
|
2682
|
-
}
|
|
2683
|
-
if (Node5.isNumericLiteral(lit)) {
|
|
2684
|
-
numberLits.push(lit.getText());
|
|
2685
|
-
continue;
|
|
2686
|
-
}
|
|
2687
|
-
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2688
|
-
nullable = true;
|
|
2689
|
-
continue;
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
others.push(member);
|
|
2693
|
-
}
|
|
2694
|
-
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2695
|
-
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2696
|
-
}
|
|
2697
|
-
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2698
|
-
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2699
|
-
}
|
|
2700
|
-
if (others.length === 1) {
|
|
2701
|
-
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2702
|
-
return markNullable(inner, nullable || inner.nullable === true);
|
|
2703
|
-
}
|
|
2704
|
-
return markNullable({ kind: "unknown" }, nullable);
|
|
2705
|
-
}
|
|
2706
|
-
switch (typeNode.getKind()) {
|
|
2707
|
-
case SyntaxKind2.StringKeyword:
|
|
2708
|
-
return { kind: "string" };
|
|
2709
|
-
case SyntaxKind2.NumberKeyword:
|
|
2710
|
-
return { kind: "number" };
|
|
2711
|
-
case SyntaxKind2.BooleanKeyword:
|
|
2712
|
-
return { kind: "boolean" };
|
|
2713
|
-
case SyntaxKind2.AnyKeyword:
|
|
2714
|
-
case SyntaxKind2.UnknownKeyword:
|
|
2715
|
-
return { kind: "unknown" };
|
|
2716
|
-
default:
|
|
2717
|
-
break;
|
|
2718
|
-
}
|
|
2719
|
-
if (Node5.isTypeReference(typeNode)) {
|
|
2720
|
-
const refName = typeNode.getTypeName().getText();
|
|
2721
|
-
if (refName === "Date") return { kind: "date" };
|
|
2722
|
-
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
2723
|
-
const typeRef = opts?.resolveRef?.(refName) ?? null;
|
|
2724
|
-
const en = resolveEnumValues(refName, sourceFile, project);
|
|
2725
|
-
if (en) {
|
|
2726
|
-
const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
2727
|
-
return typeRef ? { ...base, typeRef } : base;
|
|
2728
|
-
}
|
|
2729
|
-
if (typeRef) return { kind: "unknown", typeRef };
|
|
2730
|
-
return { kind: "unknown" };
|
|
2731
|
-
}
|
|
2732
|
-
if (Node5.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
2733
|
-
return { kind: "unknown" };
|
|
2280
|
+
if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
2281
|
+
return { kind: "unknown" };
|
|
2734
2282
|
}
|
|
2735
2283
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
2736
2284
|
for (const arg of args) {
|
|
2737
|
-
if (
|
|
2285
|
+
if (Node4.isArrowFunction(arg)) {
|
|
2738
2286
|
const body = arg.getBody();
|
|
2739
|
-
if (
|
|
2287
|
+
if (Node4.isIdentifier(body)) {
|
|
2740
2288
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2741
2289
|
if (en) return en;
|
|
2742
2290
|
}
|
|
2743
2291
|
}
|
|
2744
|
-
if (
|
|
2292
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2745
2293
|
const itemsProp = arg.getProperty("items");
|
|
2746
|
-
if (itemsProp &&
|
|
2294
|
+
if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
|
|
2747
2295
|
const init = itemsProp.getInitializer();
|
|
2748
|
-
if (init &&
|
|
2296
|
+
if (init && Node4.isArrowFunction(init)) {
|
|
2749
2297
|
const body = init.getBody();
|
|
2750
|
-
if (
|
|
2298
|
+
if (Node4.isIdentifier(body)) {
|
|
2751
2299
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2752
2300
|
if (en) return en;
|
|
2753
2301
|
}
|
|
@@ -2770,7 +2318,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2770
2318
|
return { kind: "string" };
|
|
2771
2319
|
}
|
|
2772
2320
|
for (const arg of args) {
|
|
2773
|
-
if (
|
|
2321
|
+
if (Node4.isStringLiteral(arg)) {
|
|
2774
2322
|
const raw = arg.getLiteralValue();
|
|
2775
2323
|
const kind = classifyTypeKeyword(raw);
|
|
2776
2324
|
if (kind) {
|
|
@@ -2778,11 +2326,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2778
2326
|
return { kind };
|
|
2779
2327
|
}
|
|
2780
2328
|
}
|
|
2781
|
-
if (
|
|
2329
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2782
2330
|
const enumProp = arg.getProperty("enum");
|
|
2783
|
-
if (enumProp &&
|
|
2331
|
+
if (enumProp && Node4.isPropertyAssignment(enumProp)) {
|
|
2784
2332
|
const init = enumProp.getInitializer();
|
|
2785
|
-
if (init &&
|
|
2333
|
+
if (init && Node4.isIdentifier(init)) {
|
|
2786
2334
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
2787
2335
|
if (en) {
|
|
2788
2336
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -2791,9 +2339,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2791
2339
|
}
|
|
2792
2340
|
}
|
|
2793
2341
|
const typeProp = arg.getProperty("type");
|
|
2794
|
-
if (typeProp &&
|
|
2342
|
+
if (typeProp && Node4.isPropertyAssignment(typeProp)) {
|
|
2795
2343
|
const init = typeProp.getInitializer();
|
|
2796
|
-
if (init &&
|
|
2344
|
+
if (init && Node4.isStringLiteral(init)) {
|
|
2797
2345
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
2798
2346
|
if (kind) return { kind };
|
|
2799
2347
|
}
|
|
@@ -2829,7 +2377,7 @@ function toFilterFieldType(name, r) {
|
|
|
2829
2377
|
|
|
2830
2378
|
// src/discovery/filter-for.ts
|
|
2831
2379
|
function classifyFilterForHint(typeInit) {
|
|
2832
|
-
if (
|
|
2380
|
+
if (Node5.isStringLiteral(typeInit)) {
|
|
2833
2381
|
switch (typeInit.getLiteralValue()) {
|
|
2834
2382
|
case "string":
|
|
2835
2383
|
return { kind: "string" };
|
|
@@ -2843,10 +2391,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
2843
2391
|
return null;
|
|
2844
2392
|
}
|
|
2845
2393
|
}
|
|
2846
|
-
if (
|
|
2394
|
+
if (Node5.isArrayLiteralExpression(typeInit)) {
|
|
2847
2395
|
const values = [];
|
|
2848
2396
|
for (const el of typeInit.getElements()) {
|
|
2849
|
-
if (!
|
|
2397
|
+
if (!Node5.isStringLiteral(el)) return null;
|
|
2850
2398
|
values.push(el.getLiteralValue());
|
|
2851
2399
|
}
|
|
2852
2400
|
if (values.length === 0) return null;
|
|
@@ -2881,11 +2429,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
2881
2429
|
if (!filterForDec) continue;
|
|
2882
2430
|
const args = filterForDec.getArguments();
|
|
2883
2431
|
const keyArg = args[0];
|
|
2884
|
-
const inputKey = keyArg &&
|
|
2432
|
+
const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
2885
2433
|
const optsArg = args[1];
|
|
2886
|
-
if (optsArg &&
|
|
2434
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
2887
2435
|
const typeProp = optsArg.getProperty("type");
|
|
2888
|
-
if (typeProp &&
|
|
2436
|
+
if (typeProp && Node5.isPropertyAssignment(typeProp)) {
|
|
2889
2437
|
const typeInit = typeProp.getInitializer();
|
|
2890
2438
|
if (typeInit) {
|
|
2891
2439
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -2908,14 +2456,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
2908
2456
|
const args = filterDecorator.getArguments();
|
|
2909
2457
|
if (args.length === 0) continue;
|
|
2910
2458
|
const filterClassArg = args[0];
|
|
2911
|
-
if (!filterClassArg || !
|
|
2459
|
+
if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
|
|
2912
2460
|
let source = "query";
|
|
2913
2461
|
const optionsArg = args[1];
|
|
2914
|
-
if (optionsArg &&
|
|
2462
|
+
if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
|
|
2915
2463
|
const sourceProp = optionsArg.getProperty("source");
|
|
2916
|
-
if (sourceProp &&
|
|
2464
|
+
if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
|
|
2917
2465
|
const init = sourceProp.getInitializer();
|
|
2918
|
-
if (init &&
|
|
2466
|
+
if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
2919
2467
|
source = "body";
|
|
2920
2468
|
}
|
|
2921
2469
|
}
|
|
@@ -2978,22 +2526,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
2978
2526
|
const args = dec.getArguments();
|
|
2979
2527
|
if (args.length === 0) continue;
|
|
2980
2528
|
const arg = args[0];
|
|
2981
|
-
if (
|
|
2529
|
+
if (Node5.isObjectLiteralExpression(arg)) {
|
|
2982
2530
|
const entityProp = arg.getProperty("entity");
|
|
2983
|
-
if (entityProp &&
|
|
2531
|
+
if (entityProp && Node5.isPropertyAssignment(entityProp)) {
|
|
2984
2532
|
const init = entityProp.getInitializer();
|
|
2985
|
-
if (init &&
|
|
2533
|
+
if (init && Node5.isArrowFunction(init)) {
|
|
2986
2534
|
const body = init.getBody();
|
|
2987
|
-
if (
|
|
2535
|
+
if (Node5.isIdentifier(body)) {
|
|
2988
2536
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
2989
2537
|
if (resolved?.kind === "class") return resolved.decl;
|
|
2990
2538
|
}
|
|
2991
2539
|
}
|
|
2992
2540
|
}
|
|
2993
2541
|
}
|
|
2994
|
-
if (
|
|
2542
|
+
if (Node5.isArrowFunction(arg)) {
|
|
2995
2543
|
const body = arg.getBody();
|
|
2996
|
-
if (
|
|
2544
|
+
if (Node5.isIdentifier(body)) {
|
|
2997
2545
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
2998
2546
|
if (resolved?.kind === "class") return resolved.decl;
|
|
2999
2547
|
}
|
|
@@ -3017,11 +2565,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3017
2565
|
const args = filterableDecorator.getArguments();
|
|
3018
2566
|
if (args.length === 0) return [];
|
|
3019
2567
|
const optionsArg = args[0];
|
|
3020
|
-
if (!
|
|
2568
|
+
if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
|
|
3021
2569
|
const entityProp = optionsArg.getProperty("entity");
|
|
3022
|
-
if (!entityProp || !
|
|
2570
|
+
if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
|
|
3023
2571
|
const entityInit = entityProp.getInitializer();
|
|
3024
|
-
if (!entityInit || !
|
|
2572
|
+
if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
|
|
3025
2573
|
const entityName = entityInit.getText();
|
|
3026
2574
|
const filterSourceFile = filterClass.getSourceFile();
|
|
3027
2575
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -3037,17 +2585,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3037
2585
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
3038
2586
|
if (relationsDecorator) {
|
|
3039
2587
|
const relArgs = relationsDecorator.getArguments();
|
|
3040
|
-
if (relArgs.length > 0 &&
|
|
2588
|
+
if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
|
|
3041
2589
|
for (const relProp of relArgs[0].getProperties()) {
|
|
3042
|
-
if (!
|
|
2590
|
+
if (!Node5.isPropertyAssignment(relProp)) continue;
|
|
3043
2591
|
const relInit = relProp.getInitializer();
|
|
3044
|
-
if (!relInit || !
|
|
2592
|
+
if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
|
|
3045
2593
|
const keysProp = relInit.getProperty("keys");
|
|
3046
|
-
if (!keysProp || !
|
|
2594
|
+
if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
|
|
3047
2595
|
const keysInit = keysProp.getInitializer();
|
|
3048
|
-
if (!keysInit || !
|
|
2596
|
+
if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
|
|
3049
2597
|
for (const el of keysInit.getElements()) {
|
|
3050
|
-
if (
|
|
2598
|
+
if (Node5.isStringLiteral(el)) {
|
|
3051
2599
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
3052
2600
|
}
|
|
3053
2601
|
}
|
|
@@ -3057,267 +2605,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3057
2605
|
return fields;
|
|
3058
2606
|
}
|
|
3059
2607
|
|
|
3060
|
-
// src/discovery/
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
2608
|
+
// src/discovery/dto-type-resolver.ts
|
|
2609
|
+
var WRAPPER_TYPES = {
|
|
2610
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
2611
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
2612
|
+
// entity. Unwrap to the type argument.
|
|
2613
|
+
Ref: "unwrap",
|
|
2614
|
+
Reference: "unwrap",
|
|
2615
|
+
LoadedReference: "unwrap",
|
|
2616
|
+
IdentifiedReference: "unwrap",
|
|
2617
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
2618
|
+
Opt: "unwrap",
|
|
2619
|
+
Loaded: "unwrap",
|
|
2620
|
+
// Promise<T> — unwrap
|
|
2621
|
+
Promise: "unwrap",
|
|
2622
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
2623
|
+
Collection: "arrayOf",
|
|
2624
|
+
// Array<T> generic form
|
|
2625
|
+
Array: "arrayOf"
|
|
2626
|
+
};
|
|
2627
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
2628
|
+
"Record",
|
|
2629
|
+
"Omit",
|
|
2630
|
+
"Pick",
|
|
2631
|
+
"Partial",
|
|
2632
|
+
"Required",
|
|
2633
|
+
"Readonly",
|
|
2634
|
+
"Map",
|
|
2635
|
+
"Set"
|
|
2636
|
+
]);
|
|
2637
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
2638
|
+
if (depth <= 0) return "unknown";
|
|
2639
|
+
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2640
|
+
const elementType = typeNode.getElementTypeNode();
|
|
2641
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3083
2642
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
project.addSourceFileAtPath(f);
|
|
2643
|
+
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2644
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3087
2645
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
projectRoot: cwd,
|
|
3091
|
-
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3092
|
-
});
|
|
3093
|
-
try {
|
|
3094
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
3095
|
-
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3096
|
-
}
|
|
3097
|
-
} finally {
|
|
3098
|
-
restoreDiscoveryContext(prevCtx);
|
|
2646
|
+
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2647
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3099
2648
|
}
|
|
3100
|
-
|
|
3101
|
-
}
|
|
3102
|
-
function zodAstToTs(node) {
|
|
3103
|
-
if (!Node7.isCallExpression(node)) return "unknown";
|
|
3104
|
-
const expr = node.getExpression();
|
|
3105
|
-
if (Node7.isPropertyAccessExpression(expr)) {
|
|
3106
|
-
const methodName = expr.getName();
|
|
3107
|
-
const receiver = expr.getExpression();
|
|
3108
|
-
if (methodName === "optional") {
|
|
3109
|
-
return `${zodAstToTs(receiver)} | undefined`;
|
|
3110
|
-
}
|
|
3111
|
-
if (methodName === "nullable") {
|
|
3112
|
-
return `${zodAstToTs(receiver)} | null`;
|
|
3113
|
-
}
|
|
3114
|
-
const args = node.getArguments();
|
|
3115
|
-
switch (methodName) {
|
|
3116
|
-
case "string":
|
|
3117
|
-
return "string";
|
|
3118
|
-
case "number":
|
|
3119
|
-
return "number";
|
|
3120
|
-
case "boolean":
|
|
3121
|
-
return "boolean";
|
|
3122
|
-
case "unknown":
|
|
3123
|
-
return "unknown";
|
|
3124
|
-
case "any":
|
|
3125
|
-
return "unknown";
|
|
3126
|
-
case "literal": {
|
|
3127
|
-
const lit = args[0];
|
|
3128
|
-
if (!lit) return "unknown";
|
|
3129
|
-
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
3130
|
-
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
3131
|
-
if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
|
|
3132
|
-
if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
|
|
3133
|
-
return "unknown";
|
|
3134
|
-
}
|
|
3135
|
-
case "enum": {
|
|
3136
|
-
const arrArg = args[0];
|
|
3137
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3138
|
-
const members = arrArg.getElements().map(
|
|
3139
|
-
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
3140
|
-
);
|
|
3141
|
-
return members.join(" | ");
|
|
3142
|
-
}
|
|
3143
|
-
case "array": {
|
|
3144
|
-
const inner = args[0];
|
|
3145
|
-
if (!inner) return "unknown";
|
|
3146
|
-
return `Array<${zodAstToTs(inner)}>`;
|
|
3147
|
-
}
|
|
3148
|
-
case "object": {
|
|
3149
|
-
const objArg = args[0];
|
|
3150
|
-
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
3151
|
-
const lines = [];
|
|
3152
|
-
for (const prop of objArg.getProperties()) {
|
|
3153
|
-
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3154
|
-
const key = prop.getName();
|
|
3155
|
-
const valNode = prop.getInitializer();
|
|
3156
|
-
if (!valNode) continue;
|
|
3157
|
-
const tsType = zodAstToTs(valNode);
|
|
3158
|
-
const isOpt = isOptionalChain(valNode);
|
|
3159
|
-
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
3160
|
-
}
|
|
3161
|
-
return `{ ${lines.join("; ")} }`;
|
|
3162
|
-
}
|
|
3163
|
-
case "union": {
|
|
3164
|
-
const arrArg = args[0];
|
|
3165
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3166
|
-
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
3167
|
-
}
|
|
3168
|
-
case "record": {
|
|
3169
|
-
const valArg = args.length === 1 ? args[0] : args[1];
|
|
3170
|
-
if (!valArg) return "unknown";
|
|
3171
|
-
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
3172
|
-
}
|
|
3173
|
-
case "tuple": {
|
|
3174
|
-
const arrArg = args[0];
|
|
3175
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3176
|
-
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
3177
|
-
}
|
|
3178
|
-
default:
|
|
3179
|
-
return "unknown";
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
return "unknown";
|
|
3183
|
-
}
|
|
3184
|
-
function isOptionalChain(node) {
|
|
3185
|
-
if (!Node7.isCallExpression(node)) return false;
|
|
3186
|
-
const expr = node.getExpression();
|
|
3187
|
-
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
3188
|
-
}
|
|
3189
|
-
function decoratorStringArg(decoratorExpr) {
|
|
3190
|
-
if (!decoratorExpr) return void 0;
|
|
3191
|
-
if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3192
|
-
if (Node7.isArrayLiteralExpression(decoratorExpr)) {
|
|
3193
|
-
const first = decoratorExpr.getElements()[0];
|
|
3194
|
-
if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
|
|
3195
|
-
}
|
|
3196
|
-
return void 0;
|
|
3197
|
-
}
|
|
3198
|
-
function parseDefineContractCall(callExpr) {
|
|
3199
|
-
if (!Node7.isCallExpression(callExpr)) return null;
|
|
3200
|
-
const callee = callExpr.getExpression();
|
|
3201
|
-
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3202
|
-
if (calleeName !== "defineContract") return null;
|
|
3203
|
-
const args = callExpr.getArguments();
|
|
3204
|
-
const optsArg = args[0];
|
|
3205
|
-
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3206
|
-
let query = null;
|
|
3207
|
-
let body = null;
|
|
3208
|
-
let response = "unknown";
|
|
3209
|
-
let bodyZodText = null;
|
|
3210
|
-
let queryZodText = null;
|
|
3211
|
-
for (const prop of optsArg.getProperties()) {
|
|
3212
|
-
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3213
|
-
const propName = prop.getName();
|
|
3214
|
-
const val = prop.getInitializer();
|
|
3215
|
-
if (!val) continue;
|
|
3216
|
-
if (propName === "query") {
|
|
3217
|
-
query = zodAstToTs(val);
|
|
3218
|
-
queryZodText = val.getText();
|
|
3219
|
-
} else if (propName === "body") {
|
|
3220
|
-
body = zodAstToTs(val);
|
|
3221
|
-
bodyZodText = val.getText();
|
|
3222
|
-
} else if (propName === "response") {
|
|
3223
|
-
response = zodAstToTs(val);
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3227
|
-
}
|
|
3228
|
-
function deriveClassSegment(className) {
|
|
3229
|
-
const noSuffix = className.replace(/Controller$/, "");
|
|
3230
|
-
if (!noSuffix) {
|
|
3231
|
-
throw new Error(
|
|
3232
|
-
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3233
|
-
);
|
|
3234
|
-
}
|
|
3235
|
-
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3236
|
-
}
|
|
3237
|
-
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3238
|
-
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3239
|
-
const methodPortion = methodAs ?? methodName;
|
|
3240
|
-
return `${classPortion}.${methodPortion}`;
|
|
3241
|
-
}
|
|
3242
|
-
function joinPaths(prefix, suffix) {
|
|
3243
|
-
if (!prefix && !suffix) return "/";
|
|
3244
|
-
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3245
|
-
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3246
|
-
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3247
|
-
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3248
|
-
const combined = p + s;
|
|
3249
|
-
return combined === "" ? "/" : combined;
|
|
3250
|
-
}
|
|
3251
|
-
function extractParams(path) {
|
|
3252
|
-
const matches = path.matchAll(/:(\w+)/g);
|
|
3253
|
-
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3254
|
-
}
|
|
3255
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3256
|
-
if (depth <= 0) return "unknown";
|
|
3257
|
-
if (Node7.isArrayTypeNode(typeNode)) {
|
|
3258
|
-
const elementType = typeNode.getElementTypeNode();
|
|
3259
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3260
|
-
}
|
|
3261
|
-
if (Node7.isUnionTypeNode(typeNode)) {
|
|
3262
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3263
|
-
}
|
|
3264
|
-
if (Node7.isIntersectionTypeNode(typeNode)) {
|
|
3265
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3266
|
-
}
|
|
3267
|
-
if (Node7.isParenthesizedTypeNode(typeNode)) {
|
|
2649
|
+
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
3268
2650
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3269
2651
|
}
|
|
3270
|
-
if (
|
|
2652
|
+
if (Node6.isTypeReference(typeNode)) {
|
|
3271
2653
|
const typeName = typeNode.getTypeName();
|
|
3272
|
-
const name =
|
|
2654
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3273
2655
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
3274
2656
|
if (name === "Date") return "string";
|
|
3275
2657
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
3276
2658
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
3277
2659
|
return "unknown";
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3282
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3283
|
-
}
|
|
3284
|
-
return "unknown";
|
|
3285
|
-
}
|
|
3286
|
-
if (name === "Collection") {
|
|
3287
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3288
|
-
const firstTypeArg = typeArgs[0];
|
|
3289
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3290
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
3291
|
-
}
|
|
3292
|
-
return "Array<unknown>";
|
|
3293
|
-
}
|
|
3294
|
-
if (name === "Opt" || name === "Loaded") {
|
|
3295
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3296
|
-
const firstTypeArg = typeArgs[0];
|
|
3297
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3298
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3299
|
-
}
|
|
3300
|
-
return "unknown";
|
|
2660
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
2661
|
+
if (wrapperMode) {
|
|
2662
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3301
2663
|
}
|
|
3302
|
-
if (name
|
|
3303
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3304
|
-
const firstTypeArg = typeArgs[0];
|
|
3305
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3306
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
3307
|
-
}
|
|
3308
|
-
return "Array<unknown>";
|
|
3309
|
-
}
|
|
3310
|
-
if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
|
|
2664
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
3311
2665
|
return typeNode.getText();
|
|
3312
2666
|
}
|
|
3313
|
-
if (name === "Promise") {
|
|
3314
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3315
|
-
const firstTypeArg = typeArgs[0];
|
|
3316
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3317
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3318
|
-
}
|
|
3319
|
-
return "unknown";
|
|
3320
|
-
}
|
|
3321
2667
|
const resolved = findType(name, sourceFile, project);
|
|
3322
2668
|
if (resolved) {
|
|
3323
2669
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -3333,6 +2679,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
3333
2679
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
3334
2680
|
return typeNode.getText();
|
|
3335
2681
|
}
|
|
2682
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
2683
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
2684
|
+
const firstTypeArg = typeArgs[0];
|
|
2685
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2686
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
2687
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2688
|
+
}
|
|
2689
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2690
|
+
}
|
|
3336
2691
|
function expandTypeDecl(result, project, depth) {
|
|
3337
2692
|
if (depth < 0) return "unknown";
|
|
3338
2693
|
switch (result.kind) {
|
|
@@ -3398,7 +2753,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
3398
2753
|
const paramArgs = paramDecorator.getArguments();
|
|
3399
2754
|
if (paramArgs.length === 0) continue;
|
|
3400
2755
|
const nameArg = paramArgs[0];
|
|
3401
|
-
if (!
|
|
2756
|
+
if (!Node6.isStringLiteral(nameArg)) continue;
|
|
3402
2757
|
const paramName = nameArg.getLiteralValue();
|
|
3403
2758
|
const typeNode = param.getTypeNode();
|
|
3404
2759
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -3411,13 +2766,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3411
2766
|
if (apiResponseDecorator) {
|
|
3412
2767
|
const args = apiResponseDecorator.getArguments();
|
|
3413
2768
|
const optsArg = args[0];
|
|
3414
|
-
if (optsArg &&
|
|
2769
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3415
2770
|
for (const prop of optsArg.getProperties()) {
|
|
3416
|
-
if (!
|
|
2771
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3417
2772
|
if (prop.getName() !== "type") continue;
|
|
3418
2773
|
const val = prop.getInitializer();
|
|
3419
2774
|
if (!val) continue;
|
|
3420
|
-
if (
|
|
2775
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3421
2776
|
const elements = val.getElements();
|
|
3422
2777
|
const firstEl = elements[0];
|
|
3423
2778
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -3437,7 +2792,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3437
2792
|
return "unknown";
|
|
3438
2793
|
}
|
|
3439
2794
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
3440
|
-
if (!
|
|
2795
|
+
if (!Node6.isIdentifier(node)) return "unknown";
|
|
3441
2796
|
const name = node.getText();
|
|
3442
2797
|
const resolved = findType(name, sourceFile, project);
|
|
3443
2798
|
if (resolved) {
|
|
@@ -3484,11 +2839,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3484
2839
|
if (apiResp) {
|
|
3485
2840
|
const args = apiResp.getArguments();
|
|
3486
2841
|
const optsArg = args[0];
|
|
3487
|
-
if (optsArg &&
|
|
2842
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3488
2843
|
for (const prop of optsArg.getProperties()) {
|
|
3489
|
-
if (
|
|
2844
|
+
if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
3490
2845
|
const val = prop.getInitializer();
|
|
3491
|
-
if (val &&
|
|
2846
|
+
if (val && Node6.isIdentifier(val)) {
|
|
3492
2847
|
const name = val.getText();
|
|
3493
2848
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3494
2849
|
if (localDecl?.isExported()) {
|
|
@@ -3505,60 +2860,243 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3505
2860
|
}
|
|
3506
2861
|
}
|
|
3507
2862
|
}
|
|
3508
|
-
let
|
|
3509
|
-
let
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
formWarnings.push(...
|
|
3520
|
-
|
|
2863
|
+
let bodySchema = null;
|
|
2864
|
+
let querySchema = null;
|
|
2865
|
+
const formWarnings = [];
|
|
2866
|
+
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
2867
|
+
if (bodyClass) {
|
|
2868
|
+
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
2869
|
+
formWarnings.push(...bodySchema.warnings);
|
|
2870
|
+
}
|
|
2871
|
+
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
2872
|
+
if (queryClass) {
|
|
2873
|
+
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
2874
|
+
formWarnings.push(...querySchema.warnings);
|
|
2875
|
+
}
|
|
2876
|
+
return {
|
|
2877
|
+
query,
|
|
2878
|
+
body,
|
|
2879
|
+
response,
|
|
2880
|
+
params: paramsType,
|
|
2881
|
+
queryRef,
|
|
2882
|
+
bodyRef,
|
|
2883
|
+
responseRef,
|
|
2884
|
+
filterFields: filterInfo?.fieldNames ?? null,
|
|
2885
|
+
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2886
|
+
filterSource: filterInfo?.source ?? null,
|
|
2887
|
+
formWarnings,
|
|
2888
|
+
bodySchema,
|
|
2889
|
+
querySchema
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
2893
|
+
for (const param of method.getParameters()) {
|
|
2894
|
+
if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
|
|
2895
|
+
const typeNode = param.getTypeNode();
|
|
2896
|
+
if (!typeNode) continue;
|
|
2897
|
+
const text = typeNode.getText().replace(/\[\]$/, "");
|
|
2898
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
2899
|
+
const resolved = findType(text, sourceFile, project);
|
|
2900
|
+
if (resolved && resolved.kind === "class") {
|
|
2901
|
+
return { decl: resolved.decl, file: resolved.file };
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return null;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
2908
|
+
import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2909
|
+
function zodAstToTs(node) {
|
|
2910
|
+
if (!Node7.isCallExpression(node)) return "unknown";
|
|
2911
|
+
const expr = node.getExpression();
|
|
2912
|
+
if (Node7.isPropertyAccessExpression(expr)) {
|
|
2913
|
+
const methodName = expr.getName();
|
|
2914
|
+
const receiver = expr.getExpression();
|
|
2915
|
+
if (methodName === "optional") {
|
|
2916
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
2917
|
+
}
|
|
2918
|
+
if (methodName === "nullable") {
|
|
2919
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
2920
|
+
}
|
|
2921
|
+
const args = node.getArguments();
|
|
2922
|
+
switch (methodName) {
|
|
2923
|
+
case "string":
|
|
2924
|
+
return "string";
|
|
2925
|
+
case "number":
|
|
2926
|
+
return "number";
|
|
2927
|
+
case "boolean":
|
|
2928
|
+
return "boolean";
|
|
2929
|
+
case "unknown":
|
|
2930
|
+
return "unknown";
|
|
2931
|
+
case "any":
|
|
2932
|
+
return "unknown";
|
|
2933
|
+
case "literal": {
|
|
2934
|
+
const lit = args[0];
|
|
2935
|
+
if (!lit) return "unknown";
|
|
2936
|
+
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
2937
|
+
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
2938
|
+
if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
|
|
2939
|
+
if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
|
|
2940
|
+
return "unknown";
|
|
2941
|
+
}
|
|
2942
|
+
case "enum": {
|
|
2943
|
+
const arrArg = args[0];
|
|
2944
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2945
|
+
const members = arrArg.getElements().map(
|
|
2946
|
+
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
2947
|
+
);
|
|
2948
|
+
return members.join(" | ");
|
|
2949
|
+
}
|
|
2950
|
+
case "array": {
|
|
2951
|
+
const inner = args[0];
|
|
2952
|
+
if (!inner) return "unknown";
|
|
2953
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
2954
|
+
}
|
|
2955
|
+
case "object": {
|
|
2956
|
+
const objArg = args[0];
|
|
2957
|
+
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
2958
|
+
const lines = [];
|
|
2959
|
+
for (const prop of objArg.getProperties()) {
|
|
2960
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
2961
|
+
const key = prop.getName();
|
|
2962
|
+
const valNode = prop.getInitializer();
|
|
2963
|
+
if (!valNode) continue;
|
|
2964
|
+
const tsType = zodAstToTs(valNode);
|
|
2965
|
+
const isOpt = isOptionalChain(valNode);
|
|
2966
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
2967
|
+
}
|
|
2968
|
+
return `{ ${lines.join("; ")} }`;
|
|
2969
|
+
}
|
|
2970
|
+
case "union": {
|
|
2971
|
+
const arrArg = args[0];
|
|
2972
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2973
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
2974
|
+
}
|
|
2975
|
+
case "record": {
|
|
2976
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
2977
|
+
if (!valArg) return "unknown";
|
|
2978
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
2979
|
+
}
|
|
2980
|
+
case "tuple": {
|
|
2981
|
+
const arrArg = args[0];
|
|
2982
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2983
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
2984
|
+
}
|
|
2985
|
+
default:
|
|
2986
|
+
return "unknown";
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return "unknown";
|
|
2990
|
+
}
|
|
2991
|
+
function isOptionalChain(node) {
|
|
2992
|
+
if (!Node7.isCallExpression(node)) return false;
|
|
2993
|
+
const expr = node.getExpression();
|
|
2994
|
+
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
2995
|
+
}
|
|
2996
|
+
function parseDefineContractCall(callExpr) {
|
|
2997
|
+
if (!Node7.isCallExpression(callExpr)) return null;
|
|
2998
|
+
const callee = callExpr.getExpression();
|
|
2999
|
+
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3000
|
+
if (calleeName !== "defineContract") return null;
|
|
3001
|
+
const args = callExpr.getArguments();
|
|
3002
|
+
const optsArg = args[0];
|
|
3003
|
+
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3004
|
+
let query = null;
|
|
3005
|
+
let body = null;
|
|
3006
|
+
let response = "unknown";
|
|
3007
|
+
let bodyZodText = null;
|
|
3008
|
+
let queryZodText = null;
|
|
3009
|
+
for (const prop of optsArg.getProperties()) {
|
|
3010
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3011
|
+
const propName = prop.getName();
|
|
3012
|
+
const val = prop.getInitializer();
|
|
3013
|
+
if (!val) continue;
|
|
3014
|
+
if (propName === "query") {
|
|
3015
|
+
query = zodAstToTs(val);
|
|
3016
|
+
queryZodText = val.getText();
|
|
3017
|
+
} else if (propName === "body") {
|
|
3018
|
+
body = zodAstToTs(val);
|
|
3019
|
+
bodyZodText = val.getText();
|
|
3020
|
+
} else if (propName === "response") {
|
|
3021
|
+
response = zodAstToTs(val);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/discovery/contracts-fast.ts
|
|
3028
|
+
async function discoverContractsFast(opts) {
|
|
3029
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3030
|
+
const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
|
|
3031
|
+
let project;
|
|
3032
|
+
try {
|
|
3033
|
+
project = new Project3({
|
|
3034
|
+
tsConfigFilePath: tsconfigPath,
|
|
3035
|
+
skipAddingFilesFromTsConfig: true,
|
|
3036
|
+
skipLoadingLibFiles: true,
|
|
3037
|
+
skipFileDependencyResolution: true
|
|
3038
|
+
});
|
|
3039
|
+
} catch {
|
|
3040
|
+
project = new Project3({
|
|
3041
|
+
skipAddingFilesFromTsConfig: true,
|
|
3042
|
+
skipLoadingLibFiles: true,
|
|
3043
|
+
skipFileDependencyResolution: true,
|
|
3044
|
+
compilerOptions: {
|
|
3045
|
+
allowJs: true,
|
|
3046
|
+
resolveJsonModule: false,
|
|
3047
|
+
strict: false
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3052
|
+
for (const f of files) {
|
|
3053
|
+
project.addSourceFileAtPath(f);
|
|
3054
|
+
}
|
|
3055
|
+
const routes = [];
|
|
3056
|
+
setDiscoveryContext(project, {
|
|
3057
|
+
projectRoot: cwd,
|
|
3058
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3059
|
+
});
|
|
3060
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
3061
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3521
3062
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3063
|
+
return routes;
|
|
3064
|
+
}
|
|
3065
|
+
function decoratorStringArg(decoratorExpr) {
|
|
3066
|
+
if (!decoratorExpr) return void 0;
|
|
3067
|
+
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3068
|
+
if (Node8.isArrayLiteralExpression(decoratorExpr)) {
|
|
3069
|
+
const first = decoratorExpr.getElements()[0];
|
|
3070
|
+
if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
|
|
3529
3071
|
}
|
|
3530
|
-
return
|
|
3531
|
-
query,
|
|
3532
|
-
body,
|
|
3533
|
-
response,
|
|
3534
|
-
params: paramsType,
|
|
3535
|
-
queryRef,
|
|
3536
|
-
bodyRef,
|
|
3537
|
-
responseRef,
|
|
3538
|
-
filterFields: filterInfo?.fieldNames ?? null,
|
|
3539
|
-
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
3540
|
-
filterSource: filterInfo?.source ?? null,
|
|
3541
|
-
bodyZodText,
|
|
3542
|
-
queryZodText,
|
|
3543
|
-
formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
|
|
3544
|
-
formWarnings,
|
|
3545
|
-
bodySchema,
|
|
3546
|
-
querySchema
|
|
3547
|
-
};
|
|
3072
|
+
return void 0;
|
|
3548
3073
|
}
|
|
3549
|
-
function
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
3556
|
-
const resolved = findType(text, sourceFile, project);
|
|
3557
|
-
if (resolved && resolved.kind === "class") {
|
|
3558
|
-
return { decl: resolved.decl, file: resolved.file };
|
|
3559
|
-
}
|
|
3074
|
+
function deriveClassSegment(className) {
|
|
3075
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
3076
|
+
if (!noSuffix) {
|
|
3077
|
+
throw new Error(
|
|
3078
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3079
|
+
);
|
|
3560
3080
|
}
|
|
3561
|
-
return
|
|
3081
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3082
|
+
}
|
|
3083
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3084
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3085
|
+
const methodPortion = methodAs ?? methodName;
|
|
3086
|
+
return `${classPortion}.${methodPortion}`;
|
|
3087
|
+
}
|
|
3088
|
+
function joinPaths(prefix, suffix) {
|
|
3089
|
+
if (!prefix && !suffix) return "/";
|
|
3090
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3091
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3092
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3093
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3094
|
+
const combined = p + s;
|
|
3095
|
+
return combined === "" ? "/" : combined;
|
|
3096
|
+
}
|
|
3097
|
+
function extractParams(path) {
|
|
3098
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
3099
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3562
3100
|
}
|
|
3563
3101
|
var HTTP_METHOD_DECORATORS = {
|
|
3564
3102
|
Get: "GET",
|
|
@@ -3570,176 +3108,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
3570
3108
|
Head: "HEAD",
|
|
3571
3109
|
All: "ALL"
|
|
3572
3110
|
};
|
|
3111
|
+
function resolveVerb(method) {
|
|
3112
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3113
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
3114
|
+
if (httpDecorator) {
|
|
3115
|
+
const httpArgs = httpDecorator.getArguments();
|
|
3116
|
+
const pathArg = httpArgs[0];
|
|
3117
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
function readAsDecorator(node, label) {
|
|
3123
|
+
const asDecorator = node.getDecorator("As");
|
|
3124
|
+
if (!asDecorator) return void 0;
|
|
3125
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
3126
|
+
if (!asName) {
|
|
3127
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
3128
|
+
}
|
|
3129
|
+
return asName;
|
|
3130
|
+
}
|
|
3131
|
+
function buildRoute(args) {
|
|
3132
|
+
const {
|
|
3133
|
+
className,
|
|
3134
|
+
methodName,
|
|
3135
|
+
resolvedMethod,
|
|
3136
|
+
combinedPath,
|
|
3137
|
+
classAs,
|
|
3138
|
+
methodAs,
|
|
3139
|
+
sourceFile,
|
|
3140
|
+
seenNames,
|
|
3141
|
+
contractSource
|
|
3142
|
+
} = args;
|
|
3143
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3144
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
3145
|
+
const existing = seenNames.get(routeName);
|
|
3146
|
+
if (existing !== void 0) {
|
|
3147
|
+
throw new Error(
|
|
3148
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
seenNames.set(routeName, qualifiedRef);
|
|
3152
|
+
return {
|
|
3153
|
+
method: resolvedMethod,
|
|
3154
|
+
path: combinedPath,
|
|
3155
|
+
name: routeName,
|
|
3156
|
+
params: extractParams(combinedPath),
|
|
3157
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3158
|
+
contract: { contractSource }
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
function extractContractRoute(args) {
|
|
3162
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
3163
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3164
|
+
if (!firstDecoratorArg) return null;
|
|
3165
|
+
let contractDef = null;
|
|
3166
|
+
let bodyZodRef = null;
|
|
3167
|
+
let queryZodRef = null;
|
|
3168
|
+
if (Node8.isCallExpression(firstDecoratorArg)) {
|
|
3169
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3170
|
+
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3171
|
+
const identName = firstDecoratorArg.getText();
|
|
3172
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
3173
|
+
if (!varDecl) {
|
|
3174
|
+
console.warn(
|
|
3175
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
3176
|
+
);
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
const initializer = varDecl.getInitializer();
|
|
3180
|
+
if (!initializer) return null;
|
|
3181
|
+
contractDef = parseDefineContractCall(initializer);
|
|
3182
|
+
if (contractDef && varDecl.isExported()) {
|
|
3183
|
+
const filePath = sourceFile.getFilePath();
|
|
3184
|
+
if (contractDef.body !== null) {
|
|
3185
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3186
|
+
}
|
|
3187
|
+
if (contractDef.query !== null) {
|
|
3188
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
} else {
|
|
3192
|
+
console.warn(
|
|
3193
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3194
|
+
);
|
|
3195
|
+
return null;
|
|
3196
|
+
}
|
|
3197
|
+
if (!contractDef) return null;
|
|
3198
|
+
if (!verb) return null;
|
|
3199
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
3200
|
+
const methodName = method.getName();
|
|
3201
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3202
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3203
|
+
return buildRoute({
|
|
3204
|
+
className,
|
|
3205
|
+
methodName,
|
|
3206
|
+
resolvedMethod: verb.httpMethod,
|
|
3207
|
+
combinedPath: resolvedPath,
|
|
3208
|
+
classAs,
|
|
3209
|
+
methodAs,
|
|
3210
|
+
sourceFile,
|
|
3211
|
+
seenNames,
|
|
3212
|
+
contractSource: {
|
|
3213
|
+
query: contractDef.query,
|
|
3214
|
+
body: contractDef.body,
|
|
3215
|
+
response: contractDef.response,
|
|
3216
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3217
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3218
|
+
// would drag server-only deps into the client bundle).
|
|
3219
|
+
bodyZodRef,
|
|
3220
|
+
bodyZodText: contractDef.bodyZodText,
|
|
3221
|
+
queryZodRef,
|
|
3222
|
+
queryZodText: contractDef.queryZodText
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
function extractDtoRoute(args) {
|
|
3227
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
3228
|
+
if (!verb) return null;
|
|
3229
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
3230
|
+
const methodName = method.getName();
|
|
3231
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3232
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3233
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3234
|
+
return buildRoute({
|
|
3235
|
+
className,
|
|
3236
|
+
methodName,
|
|
3237
|
+
resolvedMethod: verb.httpMethod,
|
|
3238
|
+
combinedPath: combined,
|
|
3239
|
+
classAs,
|
|
3240
|
+
methodAs,
|
|
3241
|
+
sourceFile,
|
|
3242
|
+
seenNames,
|
|
3243
|
+
contractSource: {
|
|
3244
|
+
query: dtoContract?.query ?? null,
|
|
3245
|
+
body: dtoContract?.body ?? null,
|
|
3246
|
+
response: dtoContract?.response ?? "unknown",
|
|
3247
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
3248
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3249
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
3250
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
3251
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3252
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
3253
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3254
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3255
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3573
3259
|
function extractFromSourceFile(sourceFile, project) {
|
|
3574
3260
|
const routes = [];
|
|
3575
3261
|
const seenNames = /* @__PURE__ */ new Map();
|
|
3576
|
-
const
|
|
3577
|
-
for (const cls of classes) {
|
|
3262
|
+
for (const cls of sourceFile.getClasses()) {
|
|
3578
3263
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
3579
3264
|
if (!controllerDecorator) continue;
|
|
3580
|
-
const
|
|
3581
|
-
const
|
|
3582
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
3265
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
3266
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
3583
3267
|
const className = cls.getName() ?? "Unknown";
|
|
3584
3268
|
for (const method of cls.getMethods()) {
|
|
3585
|
-
|
|
3586
|
-
let handlerPath = "";
|
|
3587
|
-
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3588
|
-
const httpDecorator = method.getDecorator(decoratorName);
|
|
3589
|
-
if (httpDecorator) {
|
|
3590
|
-
httpMethod = verb;
|
|
3591
|
-
const httpArgs = httpDecorator.getArguments();
|
|
3592
|
-
const pathArg = httpArgs[0];
|
|
3593
|
-
handlerPath = decoratorStringArg(pathArg) ?? "";
|
|
3594
|
-
break;
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
3269
|
+
const verb = resolveVerb(method);
|
|
3597
3270
|
const applyContractDecorator = method.getDecorator("ApplyContract");
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
contractDef = parseDefineContractCall(initializer);
|
|
3619
|
-
if (contractDef && varDecl.isExported()) {
|
|
3620
|
-
const filePath = sourceFile.getFilePath();
|
|
3621
|
-
if (contractDef.body !== null) {
|
|
3622
|
-
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3623
|
-
}
|
|
3624
|
-
if (contractDef.query !== null) {
|
|
3625
|
-
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3626
|
-
}
|
|
3627
|
-
}
|
|
3628
|
-
} else {
|
|
3629
|
-
console.warn(
|
|
3630
|
-
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3631
|
-
);
|
|
3632
|
-
continue;
|
|
3633
|
-
}
|
|
3634
|
-
if (!contractDef) continue;
|
|
3635
|
-
if (!httpMethod) continue;
|
|
3636
|
-
const resolvedMethod = httpMethod;
|
|
3637
|
-
const resolvedPath = joinPaths(prefix, handlerPath);
|
|
3638
|
-
const combined = resolvedPath;
|
|
3639
|
-
const params = extractParams(combined);
|
|
3640
|
-
const methodName = method.getName();
|
|
3641
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
3642
|
-
let classAs;
|
|
3643
|
-
if (classAsDecorator) {
|
|
3644
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
3645
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
3646
|
-
if (!classAsName) {
|
|
3647
|
-
throw new Error(
|
|
3648
|
-
`@As decorator on class ${className} must have a non-empty string argument.`
|
|
3649
|
-
);
|
|
3650
|
-
}
|
|
3651
|
-
classAs = classAsName;
|
|
3652
|
-
}
|
|
3653
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
3654
|
-
let methodAs;
|
|
3655
|
-
if (methodAsDecorator) {
|
|
3656
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
3657
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
3658
|
-
if (!methodAsName) {
|
|
3659
|
-
throw new Error(
|
|
3660
|
-
`@As decorator on ${className}.${methodName} must have a non-empty string argument.`
|
|
3661
|
-
);
|
|
3662
|
-
}
|
|
3663
|
-
methodAs = methodAsName;
|
|
3664
|
-
}
|
|
3665
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3666
|
-
const qualifiedRef = `${className}.${methodName}`;
|
|
3667
|
-
const existing = seenNames.get(routeName);
|
|
3668
|
-
if (existing !== void 0) {
|
|
3669
|
-
throw new Error(
|
|
3670
|
-
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3671
|
-
);
|
|
3672
|
-
}
|
|
3673
|
-
seenNames.set(routeName, qualifiedRef);
|
|
3674
|
-
routes.push({
|
|
3675
|
-
method: resolvedMethod,
|
|
3676
|
-
path: combined,
|
|
3677
|
-
name: routeName,
|
|
3678
|
-
params,
|
|
3679
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3680
|
-
contract: {
|
|
3681
|
-
contractSource: {
|
|
3682
|
-
query: contractDef.query,
|
|
3683
|
-
body: contractDef.body,
|
|
3684
|
-
response: contractDef.response,
|
|
3685
|
-
// Path A: capture both the importable ref and the raw text. The
|
|
3686
|
-
// emitter prefers inlining the text (client-safe — re-exporting from
|
|
3687
|
-
// a controller would drag server-only deps into the client bundle).
|
|
3688
|
-
bodyZodRef,
|
|
3689
|
-
bodyZodText: contractDef.bodyZodText,
|
|
3690
|
-
queryZodRef,
|
|
3691
|
-
queryZodText: contractDef.queryZodText
|
|
3692
|
-
}
|
|
3693
|
-
}
|
|
3694
|
-
});
|
|
3695
|
-
} else {
|
|
3696
|
-
if (!httpMethod) continue;
|
|
3697
|
-
const combined = joinPaths(prefix, handlerPath);
|
|
3698
|
-
const params = extractParams(combined);
|
|
3699
|
-
const methodName = method.getName();
|
|
3700
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
3701
|
-
let classAs;
|
|
3702
|
-
if (classAsDecorator) {
|
|
3703
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
3704
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
3705
|
-
if (classAsName) classAs = classAsName;
|
|
3706
|
-
}
|
|
3707
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
3708
|
-
let methodAs;
|
|
3709
|
-
if (methodAsDecorator) {
|
|
3710
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
3711
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
3712
|
-
if (methodAsName) methodAs = methodAsName;
|
|
3713
|
-
}
|
|
3714
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3715
|
-
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3716
|
-
routes.push({
|
|
3717
|
-
method: httpMethod,
|
|
3718
|
-
path: combined,
|
|
3719
|
-
name: routeName,
|
|
3720
|
-
params,
|
|
3721
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3722
|
-
contract: {
|
|
3723
|
-
contractSource: {
|
|
3724
|
-
query: dtoContract?.query ?? null,
|
|
3725
|
-
body: dtoContract?.body ?? null,
|
|
3726
|
-
response: dtoContract?.response ?? "unknown",
|
|
3727
|
-
queryRef: dtoContract?.queryRef ?? null,
|
|
3728
|
-
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3729
|
-
responseRef: dtoContract?.responseRef ?? null,
|
|
3730
|
-
filterFields: dtoContract?.filterFields ?? null,
|
|
3731
|
-
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3732
|
-
filterSource: dtoContract?.filterSource ?? null,
|
|
3733
|
-
bodyZodText: dtoContract?.bodyZodText ?? null,
|
|
3734
|
-
queryZodText: dtoContract?.queryZodText ?? null,
|
|
3735
|
-
formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
|
|
3736
|
-
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3737
|
-
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3738
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
});
|
|
3742
|
-
}
|
|
3271
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
3272
|
+
cls,
|
|
3273
|
+
method,
|
|
3274
|
+
applyContractDecorator,
|
|
3275
|
+
verb,
|
|
3276
|
+
prefix,
|
|
3277
|
+
className,
|
|
3278
|
+
sourceFile,
|
|
3279
|
+
seenNames
|
|
3280
|
+
}) : extractDtoRoute({
|
|
3281
|
+
cls,
|
|
3282
|
+
method,
|
|
3283
|
+
verb,
|
|
3284
|
+
prefix,
|
|
3285
|
+
className,
|
|
3286
|
+
sourceFile,
|
|
3287
|
+
project,
|
|
3288
|
+
seenNames
|
|
3289
|
+
});
|
|
3290
|
+
if (route) routes.push(route);
|
|
3743
3291
|
}
|
|
3744
3292
|
}
|
|
3745
3293
|
return routes;
|
|
@@ -3748,7 +3296,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3748
3296
|
// src/watch/lock-file.ts
|
|
3749
3297
|
import { open } from "fs/promises";
|
|
3750
3298
|
import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
|
|
3751
|
-
import { join as
|
|
3299
|
+
import { join as join12 } from "path";
|
|
3752
3300
|
var LOCK_FILE = ".watcher.lock";
|
|
3753
3301
|
function isProcessAlive(pid) {
|
|
3754
3302
|
try {
|
|
@@ -3760,7 +3308,7 @@ function isProcessAlive(pid) {
|
|
|
3760
3308
|
}
|
|
3761
3309
|
async function acquireLock(outDir) {
|
|
3762
3310
|
await mkdir8(outDir, { recursive: true });
|
|
3763
|
-
const lockPath =
|
|
3311
|
+
const lockPath = join12(outDir, LOCK_FILE);
|
|
3764
3312
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3765
3313
|
try {
|
|
3766
3314
|
const fd = await open(lockPath, "wx");
|
|
@@ -3800,7 +3348,7 @@ async function watch(config, onChange) {
|
|
|
3800
3348
|
if (lock === null) {
|
|
3801
3349
|
let holderPid = "unknown";
|
|
3802
3350
|
try {
|
|
3803
|
-
const raw = await readFile3(
|
|
3351
|
+
const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3804
3352
|
const data = JSON.parse(raw);
|
|
3805
3353
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3806
3354
|
} catch {
|
|
@@ -3828,7 +3376,7 @@ async function watch(config, onChange) {
|
|
|
3828
3376
|
}
|
|
3829
3377
|
let pagesDebounceTimer;
|
|
3830
3378
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3831
|
-
const pagesWatcher = chokidar.watch(
|
|
3379
|
+
const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
|
|
3832
3380
|
ignoreInitial: true,
|
|
3833
3381
|
persistent: true,
|
|
3834
3382
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3854,7 +3402,7 @@ async function watch(config, onChange) {
|
|
|
3854
3402
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3855
3403
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3856
3404
|
let contractsDebounceTimer;
|
|
3857
|
-
const contractsWatcher = chokidar.watch(
|
|
3405
|
+
const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
|
|
3858
3406
|
ignoreInitial: true,
|
|
3859
3407
|
persistent: true,
|
|
3860
3408
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3884,7 +3432,7 @@ async function watch(config, onChange) {
|
|
|
3884
3432
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3885
3433
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3886
3434
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3887
|
-
const formsWatcher = chokidar.watch(
|
|
3435
|
+
const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
|
|
3888
3436
|
ignoreInitial: true,
|
|
3889
3437
|
persistent: true,
|
|
3890
3438
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3911,7 +3459,7 @@ async function watch(config, onChange) {
|
|
|
3911
3459
|
}
|
|
3912
3460
|
|
|
3913
3461
|
// src/index.ts
|
|
3914
|
-
var VERSION = "0.
|
|
3462
|
+
var VERSION = "0.3.0";
|
|
3915
3463
|
|
|
3916
3464
|
// src/cli/codegen.ts
|
|
3917
3465
|
async function runCodegen(opts = {}) {
|
|
@@ -3939,15 +3487,51 @@ async function runCodegen(opts = {}) {
|
|
|
3939
3487
|
|
|
3940
3488
|
// src/cli/doctor.ts
|
|
3941
3489
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3942
|
-
import { appendFileSync, existsSync, readFileSync as
|
|
3943
|
-
import { join as
|
|
3490
|
+
import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
3491
|
+
import { join as join15 } from "path";
|
|
3944
3492
|
|
|
3945
3493
|
// src/cli/init.ts
|
|
3946
3494
|
import { execFileSync } from "child_process";
|
|
3947
|
-
import { readFileSync as
|
|
3495
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3948
3496
|
import { access as access2, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
|
|
3949
|
-
import { join as
|
|
3497
|
+
import { join as join14 } from "path";
|
|
3950
3498
|
import { createInterface } from "readline";
|
|
3499
|
+
|
|
3500
|
+
// src/cli/patch-utils.ts
|
|
3501
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3502
|
+
function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
|
|
3503
|
+
let raw;
|
|
3504
|
+
try {
|
|
3505
|
+
raw = readFileSync2(filePath, "utf8");
|
|
3506
|
+
} catch {
|
|
3507
|
+
return "skipped";
|
|
3508
|
+
}
|
|
3509
|
+
const json = JSON.parse(parse(raw));
|
|
3510
|
+
if (!mutator(json)) return "already";
|
|
3511
|
+
writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
|
|
3512
|
+
`, "utf8");
|
|
3513
|
+
return "patched";
|
|
3514
|
+
}
|
|
3515
|
+
function findAfterLastImport(content) {
|
|
3516
|
+
const lastImportIndex = content.lastIndexOf("\nimport ");
|
|
3517
|
+
if (lastImportIndex !== -1) {
|
|
3518
|
+
const endOfLine = content.indexOf("\n", lastImportIndex + 1);
|
|
3519
|
+
return endOfLine !== -1 ? endOfLine + 1 : content.length;
|
|
3520
|
+
}
|
|
3521
|
+
if (content.startsWith("import ")) {
|
|
3522
|
+
const endOfLine = content.indexOf("\n");
|
|
3523
|
+
return endOfLine !== -1 ? endOfLine + 1 : content.length;
|
|
3524
|
+
}
|
|
3525
|
+
return 0;
|
|
3526
|
+
}
|
|
3527
|
+
function insertImport(content, stmt) {
|
|
3528
|
+
const insertAt = findAfterLastImport(content);
|
|
3529
|
+
if (insertAt <= 0) return content;
|
|
3530
|
+
return `${content.slice(0, insertAt)}${stmt}
|
|
3531
|
+
${content.slice(insertAt)}`;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// src/cli/init.ts
|
|
3951
3535
|
var GITIGNORE_ENTRY = ".nestjs-inertia/";
|
|
3952
3536
|
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
3953
3537
|
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
@@ -3972,7 +3556,7 @@ ${bold(title)}`);
|
|
|
3972
3556
|
}
|
|
3973
3557
|
async function readPackageJson(cwd) {
|
|
3974
3558
|
try {
|
|
3975
|
-
const raw = await readFile4(
|
|
3559
|
+
const raw = await readFile4(join14(cwd, "package.json"), "utf8");
|
|
3976
3560
|
return JSON.parse(raw);
|
|
3977
3561
|
} catch {
|
|
3978
3562
|
return {};
|
|
@@ -4003,7 +3587,7 @@ async function detectTemplateEngine(cwd) {
|
|
|
4003
3587
|
async function detectPackageManager(cwd) {
|
|
4004
3588
|
async function exists(file) {
|
|
4005
3589
|
try {
|
|
4006
|
-
await access2(
|
|
3590
|
+
await access2(join14(cwd, file));
|
|
4007
3591
|
return true;
|
|
4008
3592
|
} catch {
|
|
4009
3593
|
return false;
|
|
@@ -4050,7 +3634,7 @@ async function writeIfNotExists(filePath, content, label) {
|
|
|
4050
3634
|
logCreated(label);
|
|
4051
3635
|
}
|
|
4052
3636
|
async function handleViteConfig(cwd, framework) {
|
|
4053
|
-
const filePath =
|
|
3637
|
+
const filePath = join14(cwd, "vite.config.ts");
|
|
4054
3638
|
if (await fileExists2(filePath)) {
|
|
4055
3639
|
const existing = await readFile4(filePath, "utf8");
|
|
4056
3640
|
const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
|
|
@@ -4108,7 +3692,7 @@ function installDeps(pkgManager, deps, dev) {
|
|
|
4108
3692
|
}
|
|
4109
3693
|
}
|
|
4110
3694
|
async function patchPackageJsonScripts(cwd, scripts) {
|
|
4111
|
-
const pkgPath =
|
|
3695
|
+
const pkgPath = join14(cwd, "package.json");
|
|
4112
3696
|
let pkg = {};
|
|
4113
3697
|
try {
|
|
4114
3698
|
pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
@@ -4133,32 +3717,16 @@ async function patchPackageJsonScripts(cwd, scripts) {
|
|
|
4133
3717
|
await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
4134
3718
|
`, "utf8");
|
|
4135
3719
|
}
|
|
4136
|
-
function findAfterLastImport(content) {
|
|
4137
|
-
const lastImportIndex = content.lastIndexOf("\nimport ");
|
|
4138
|
-
if (lastImportIndex !== -1) {
|
|
4139
|
-
const endOfLine = content.indexOf("\n", lastImportIndex + 1);
|
|
4140
|
-
return endOfLine !== -1 ? endOfLine + 1 : content.length;
|
|
4141
|
-
}
|
|
4142
|
-
if (content.startsWith("import ")) {
|
|
4143
|
-
const endOfLine = content.indexOf("\n");
|
|
4144
|
-
return endOfLine !== -1 ? endOfLine + 1 : content.length;
|
|
4145
|
-
}
|
|
4146
|
-
return 0;
|
|
4147
|
-
}
|
|
4148
3720
|
function patchAppModule(filePath, rootView) {
|
|
4149
3721
|
let content;
|
|
4150
3722
|
try {
|
|
4151
|
-
content =
|
|
3723
|
+
content = readFileSync3(filePath, "utf8");
|
|
4152
3724
|
} catch {
|
|
4153
3725
|
return "skipped";
|
|
4154
3726
|
}
|
|
4155
3727
|
let changed = false;
|
|
4156
3728
|
if (!content.includes("InertiaModule")) {
|
|
4157
|
-
|
|
4158
|
-
if (insertAt > 0) {
|
|
4159
|
-
content = `${content.slice(0, insertAt)}import { InertiaModule } from '@dudousxd/nestjs-inertia';
|
|
4160
|
-
${content.slice(insertAt)}`;
|
|
4161
|
-
}
|
|
3729
|
+
content = insertImport(content, "import { InertiaModule } from '@dudousxd/nestjs-inertia';");
|
|
4162
3730
|
if (!content.includes("from 'node:path'") && !content.includes('from "node:path"')) {
|
|
4163
3731
|
const insertAt2 = findAfterLastImport(content);
|
|
4164
3732
|
content = `${content.slice(0, insertAt2)}import { resolve } from 'node:path';
|
|
@@ -4176,11 +3744,7 @@ ${indent}}),${content.slice(bracketPos)}`;
|
|
|
4176
3744
|
}
|
|
4177
3745
|
}
|
|
4178
3746
|
if (!content.includes("HomeController")) {
|
|
4179
|
-
|
|
4180
|
-
if (insertAt > 0) {
|
|
4181
|
-
content = `${content.slice(0, insertAt)}import { HomeController } from './home.controller';
|
|
4182
|
-
${content.slice(insertAt)}`;
|
|
4183
|
-
}
|
|
3747
|
+
content = insertImport(content, "import { HomeController } from './home.controller';");
|
|
4184
3748
|
const controllersMatch = content.match(/controllers\s*:\s*\[/);
|
|
4185
3749
|
if (controllersMatch?.index !== void 0) {
|
|
4186
3750
|
const bracketPos = content.indexOf("[", controllersMatch.index) + 1;
|
|
@@ -4191,22 +3755,21 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
|
|
|
4191
3755
|
}
|
|
4192
3756
|
}
|
|
4193
3757
|
if (!changed) return "already";
|
|
4194
|
-
|
|
3758
|
+
writeFileSync2(filePath, content, "utf8");
|
|
4195
3759
|
return "patched";
|
|
4196
3760
|
}
|
|
4197
3761
|
function patchMainTs(filePath) {
|
|
4198
3762
|
let content;
|
|
4199
3763
|
try {
|
|
4200
|
-
content =
|
|
3764
|
+
content = readFileSync3(filePath, "utf8");
|
|
4201
3765
|
} catch {
|
|
4202
3766
|
return "skipped";
|
|
4203
3767
|
}
|
|
4204
3768
|
if (content.includes("setupInertiaVite")) return "already";
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
}
|
|
3769
|
+
content = insertImport(
|
|
3770
|
+
content,
|
|
3771
|
+
"import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';"
|
|
3772
|
+
);
|
|
4210
3773
|
const createMatch = content.match(
|
|
4211
3774
|
/(?:const|let)\s+(\w+)\s*=\s*await\s+NestFactory\.create[^;]+;/
|
|
4212
3775
|
);
|
|
@@ -4223,7 +3786,7 @@ ${content.slice(insertAt)}`;
|
|
|
4223
3786
|
});`;
|
|
4224
3787
|
content = `${content.slice(0, insertAfterPos)}
|
|
4225
3788
|
${viteSetup}${content.slice(insertAfterPos)}`;
|
|
4226
|
-
|
|
3789
|
+
writeFileSync2(filePath, content, "utf8");
|
|
4227
3790
|
return "patched";
|
|
4228
3791
|
}
|
|
4229
3792
|
function configTemplate(framework) {
|
|
@@ -4446,107 +4009,84 @@ export class HomeController {
|
|
|
4446
4009
|
}
|
|
4447
4010
|
`;
|
|
4448
4011
|
function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
|
|
4449
|
-
const filePath =
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
|
|
4463
|
-
`, "utf8");
|
|
4464
|
-
return "patched";
|
|
4012
|
+
const filePath = join14(cwd, filename);
|
|
4013
|
+
return patchJsonFile(
|
|
4014
|
+
filePath,
|
|
4015
|
+
(json) => {
|
|
4016
|
+
const exclude = json.exclude ?? [];
|
|
4017
|
+
if (exclude.includes(dir)) return false;
|
|
4018
|
+
exclude.push(dir);
|
|
4019
|
+
json.exclude = exclude;
|
|
4020
|
+
return true;
|
|
4021
|
+
},
|
|
4022
|
+
// Strip single-line comments before JSON.parse
|
|
4023
|
+
(raw) => raw.replace(/\/\/.*$/gm, "")
|
|
4024
|
+
);
|
|
4465
4025
|
}
|
|
4466
4026
|
function patchNestCliJson(cwd, shellDir) {
|
|
4467
|
-
const filePath =
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
outDir: `dist/${shellDir}`,
|
|
4485
|
-
watchAssets: true
|
|
4027
|
+
const filePath = join14(cwd, "nest-cli.json");
|
|
4028
|
+
return patchJsonFile(filePath, (json) => {
|
|
4029
|
+
const compiler = json.compilerOptions ?? {};
|
|
4030
|
+
const assets = compiler.assets ?? [];
|
|
4031
|
+
const alreadyHas = assets.some((a) => {
|
|
4032
|
+
if (typeof a === "string") return a.includes(shellDir);
|
|
4033
|
+
return String(a.include ?? "").includes(shellDir);
|
|
4034
|
+
});
|
|
4035
|
+
if (alreadyHas) return false;
|
|
4036
|
+
assets.push({
|
|
4037
|
+
include: `../${shellDir}/**/*`,
|
|
4038
|
+
outDir: `dist/${shellDir}`,
|
|
4039
|
+
watchAssets: true
|
|
4040
|
+
});
|
|
4041
|
+
compiler.assets = assets;
|
|
4042
|
+
json.compilerOptions = compiler;
|
|
4043
|
+
return true;
|
|
4486
4044
|
});
|
|
4487
|
-
compiler.assets = assets;
|
|
4488
|
-
json.compilerOptions = compiler;
|
|
4489
|
-
writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
|
|
4490
|
-
`, "utf8");
|
|
4491
|
-
return "patched";
|
|
4492
4045
|
}
|
|
4493
|
-
async function
|
|
4494
|
-
const cwd
|
|
4495
|
-
console.log(`
|
|
4496
|
-
${bold("nestjs-inertia init")}`);
|
|
4497
|
-
let framework = await detectFramework(cwd);
|
|
4498
|
-
if (!framework) {
|
|
4499
|
-
framework = await promptFramework();
|
|
4500
|
-
}
|
|
4501
|
-
const engine = await detectTemplateEngine(cwd);
|
|
4502
|
-
const engineLabel = engine === "html" ? "plain HTML" : engine;
|
|
4503
|
-
const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
|
|
4504
|
-
console.log(`
|
|
4505
|
-
Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
|
|
4506
|
-
const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
|
|
4507
|
-
const entryExt = framework === "react" ? "tsx" : "ts";
|
|
4508
|
-
const pageExt = framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte";
|
|
4046
|
+
async function scaffoldFiles(ctx) {
|
|
4047
|
+
const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
|
|
4509
4048
|
logSection("Scaffold files");
|
|
4510
4049
|
await writeIfNotExists(
|
|
4511
|
-
|
|
4050
|
+
join14(cwd, "nestjs-inertia.config.ts"),
|
|
4512
4051
|
configTemplate(framework),
|
|
4513
4052
|
"nestjs-inertia.config.ts"
|
|
4514
4053
|
);
|
|
4515
|
-
await writeIfNotExists(
|
|
4054
|
+
await writeIfNotExists(join14(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
|
|
4516
4055
|
await writeIfNotExists(
|
|
4517
|
-
|
|
4056
|
+
join14(cwd, "tsconfig.inertia.json"),
|
|
4518
4057
|
TSCONFIG_INERTIA_TEMPLATE,
|
|
4519
4058
|
"tsconfig.inertia.json"
|
|
4520
4059
|
);
|
|
4521
4060
|
await writeIfNotExists(
|
|
4522
|
-
|
|
4061
|
+
join14(cwd, "inertia", "tsconfig.json"),
|
|
4523
4062
|
INERTIA_TSCONFIG_TEMPLATE,
|
|
4524
4063
|
"inertia/tsconfig.json"
|
|
4525
4064
|
);
|
|
4526
4065
|
await writeIfNotExists(
|
|
4527
|
-
|
|
4066
|
+
join14(cwd, "inertia", shellFileName),
|
|
4528
4067
|
htmlShellTemplate(framework, engine),
|
|
4529
4068
|
`inertia/${shellFileName}`
|
|
4530
4069
|
);
|
|
4531
4070
|
await handleViteConfig(cwd, framework);
|
|
4532
4071
|
await writeIfNotExists(
|
|
4533
|
-
|
|
4072
|
+
join14(cwd, "inertia", "app", `client.${entryExt}`),
|
|
4534
4073
|
entryPointTemplate(framework),
|
|
4535
4074
|
`inertia/app/client.${entryExt}`
|
|
4536
4075
|
);
|
|
4537
4076
|
await writeIfNotExists(
|
|
4538
|
-
|
|
4077
|
+
join14(cwd, "inertia", "pages", `Home.${pageExt}`),
|
|
4539
4078
|
samplePageTemplate(framework),
|
|
4540
4079
|
`inertia/pages/Home.${pageExt}`
|
|
4541
4080
|
);
|
|
4542
4081
|
await writeIfNotExists(
|
|
4543
|
-
|
|
4082
|
+
join14(cwd, "src", "home.controller.ts"),
|
|
4544
4083
|
SAMPLE_CONTROLLER,
|
|
4545
4084
|
"src/home.controller.ts"
|
|
4546
4085
|
);
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
const
|
|
4086
|
+
}
|
|
4087
|
+
function patchServerAppModule(ctx) {
|
|
4088
|
+
const { cwd, rootView } = ctx;
|
|
4089
|
+
const appModulePath = join14(cwd, "src", "app.module.ts");
|
|
4550
4090
|
const appModuleResult = patchAppModule(appModulePath, rootView);
|
|
4551
4091
|
if (appModuleResult === "patched") {
|
|
4552
4092
|
logPatched("src/app.module.ts", "added InertiaModule.forRoot");
|
|
@@ -4558,7 +4098,9 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4558
4098
|
} else {
|
|
4559
4099
|
logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
|
|
4560
4100
|
}
|
|
4561
|
-
|
|
4101
|
+
}
|
|
4102
|
+
function patchServerMainTs(ctx) {
|
|
4103
|
+
const mainTsPath = join14(ctx.cwd, "src", "main.ts");
|
|
4562
4104
|
const mainTsResult = patchMainTs(mainTsPath);
|
|
4563
4105
|
if (mainTsResult === "patched") {
|
|
4564
4106
|
logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
|
|
@@ -4567,7 +4109,9 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4567
4109
|
} else {
|
|
4568
4110
|
logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
|
|
4569
4111
|
}
|
|
4570
|
-
|
|
4112
|
+
}
|
|
4113
|
+
function patchBuildConfigs(ctx) {
|
|
4114
|
+
const { cwd, shellDir } = ctx;
|
|
4571
4115
|
const nestCliResult = patchNestCliJson(cwd, shellDir);
|
|
4572
4116
|
if (nestCliResult === "patched") {
|
|
4573
4117
|
logPatched("nest-cli.json", `added asset copy for ${shellDir}/ \u2192 dist/${shellDir}/`);
|
|
@@ -4588,18 +4132,26 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4588
4132
|
);
|
|
4589
4133
|
}
|
|
4590
4134
|
}
|
|
4591
|
-
|
|
4135
|
+
}
|
|
4136
|
+
async function patchGitignoreAndDist(ctx) {
|
|
4137
|
+
const { cwd } = ctx;
|
|
4138
|
+
await patchGitignore(join14(cwd, ".gitignore"));
|
|
4592
4139
|
const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
|
|
4593
4140
|
if (tsconfigDistResult === "patched") {
|
|
4594
4141
|
logPatched("tsconfig.json", "excluded dist/ from server compilation");
|
|
4595
4142
|
} else if (tsconfigDistResult === "already") {
|
|
4596
4143
|
console.log(` ${cyan("\u2192")} tsconfig.json ${dim("(dist/ already excluded, skipped)")}`);
|
|
4597
4144
|
}
|
|
4598
|
-
|
|
4145
|
+
}
|
|
4146
|
+
async function scaffoldPackageScripts(ctx) {
|
|
4147
|
+
await patchPackageJsonScripts(ctx.cwd, {
|
|
4599
4148
|
"build:client": "vite build",
|
|
4600
4149
|
"build:ssr": "VITE_SSR=1 vite build --ssr",
|
|
4601
4150
|
"typecheck:inertia": "tsc --noEmit -p tsconfig.inertia.json"
|
|
4602
4151
|
});
|
|
4152
|
+
}
|
|
4153
|
+
async function installMissingDeps(ctx) {
|
|
4154
|
+
const { cwd, framework, opts } = ctx;
|
|
4603
4155
|
logSection("Install dependencies");
|
|
4604
4156
|
const pkg = await readPackageJson(cwd);
|
|
4605
4157
|
const installedDeps = allDeps(pkg);
|
|
@@ -4633,6 +4185,55 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4633
4185
|
installDeps(pkgManager, depsToInstall, false);
|
|
4634
4186
|
installDeps(pkgManager, devDepsToInstall, true);
|
|
4635
4187
|
}
|
|
4188
|
+
}
|
|
4189
|
+
var INIT_STEPS = [
|
|
4190
|
+
{ label: "scaffold files", run: scaffoldFiles },
|
|
4191
|
+
{
|
|
4192
|
+
label: "patch app.module.ts",
|
|
4193
|
+
run: (ctx) => {
|
|
4194
|
+
logSection("Patch existing files");
|
|
4195
|
+
patchServerAppModule(ctx);
|
|
4196
|
+
}
|
|
4197
|
+
},
|
|
4198
|
+
{ label: "patch main.ts", run: patchServerMainTs },
|
|
4199
|
+
{ label: "patch build configs", run: patchBuildConfigs },
|
|
4200
|
+
{ label: "patch .gitignore + dist exclude", run: patchGitignoreAndDist },
|
|
4201
|
+
{ label: "add package.json scripts", run: scaffoldPackageScripts },
|
|
4202
|
+
{ label: "install dependencies", run: installMissingDeps }
|
|
4203
|
+
];
|
|
4204
|
+
async function runInit(opts = {}) {
|
|
4205
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
4206
|
+
console.log(`
|
|
4207
|
+
${bold("nestjs-inertia init")}`);
|
|
4208
|
+
let framework = await detectFramework(cwd);
|
|
4209
|
+
if (!framework) {
|
|
4210
|
+
framework = await promptFramework();
|
|
4211
|
+
}
|
|
4212
|
+
const engine = await detectTemplateEngine(cwd);
|
|
4213
|
+
const engineLabel = engine === "html" ? "plain HTML" : engine;
|
|
4214
|
+
const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
|
|
4215
|
+
console.log(`
|
|
4216
|
+
Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
|
|
4217
|
+
const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
|
|
4218
|
+
const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
|
|
4219
|
+
const ctx = {
|
|
4220
|
+
opts,
|
|
4221
|
+
cwd,
|
|
4222
|
+
framework,
|
|
4223
|
+
engine,
|
|
4224
|
+
shellFileName,
|
|
4225
|
+
entryExt: framework === "react" ? "tsx" : "ts",
|
|
4226
|
+
pageExt: framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte",
|
|
4227
|
+
rootView,
|
|
4228
|
+
shellDir: rootView.split("/")[0]
|
|
4229
|
+
// e.g. "inertia" from "inertia/index.html"
|
|
4230
|
+
};
|
|
4231
|
+
for (const step of INIT_STEPS) {
|
|
4232
|
+
if (process.env.NESTJS_CODEGEN_DEBUG) {
|
|
4233
|
+
console.log(dim(` \xB7 ${step.label}`));
|
|
4234
|
+
}
|
|
4235
|
+
await step.run(ctx);
|
|
4236
|
+
}
|
|
4636
4237
|
console.log(`
|
|
4637
4238
|
${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
|
|
4638
4239
|
`);
|
|
@@ -4640,18 +4241,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
|
|
|
4640
4241
|
|
|
4641
4242
|
// src/cli/doctor.ts
|
|
4642
4243
|
function checkFileExists(cwd, file) {
|
|
4643
|
-
return existsSync(
|
|
4244
|
+
return existsSync(join15(cwd, file));
|
|
4644
4245
|
}
|
|
4645
4246
|
function readJson(path) {
|
|
4646
4247
|
try {
|
|
4647
|
-
const raw =
|
|
4248
|
+
const raw = readFileSync4(path, "utf8").replace(/\/\/.*$/gm, "");
|
|
4648
4249
|
return JSON.parse(raw);
|
|
4649
4250
|
} catch {
|
|
4650
4251
|
return null;
|
|
4651
4252
|
}
|
|
4652
4253
|
}
|
|
4653
4254
|
function writeJsonField(filePath, dotPath, value) {
|
|
4654
|
-
const raw =
|
|
4255
|
+
const raw = readFileSync4(filePath, "utf8");
|
|
4655
4256
|
const stripped = raw.replace(/\/\/.*$/gm, "");
|
|
4656
4257
|
const obj = JSON.parse(stripped);
|
|
4657
4258
|
let target = obj;
|
|
@@ -4671,20 +4272,20 @@ function writeJsonField(filePath, dotPath, value) {
|
|
|
4671
4272
|
} else {
|
|
4672
4273
|
target[lastKey] = value;
|
|
4673
4274
|
}
|
|
4674
|
-
|
|
4275
|
+
writeFileSync3(filePath, `${JSON.stringify(obj, null, 2)}
|
|
4675
4276
|
`, "utf8");
|
|
4676
4277
|
}
|
|
4677
4278
|
function getPackageVersion(cwd, pkg) {
|
|
4678
4279
|
try {
|
|
4679
|
-
const pkgJson = readJson(
|
|
4280
|
+
const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
|
|
4680
4281
|
return pkgJson?.version ?? null;
|
|
4681
4282
|
} catch {
|
|
4682
4283
|
return null;
|
|
4683
4284
|
}
|
|
4684
4285
|
}
|
|
4685
4286
|
function detectPkgManager(cwd) {
|
|
4686
|
-
if (existsSync(
|
|
4687
|
-
if (existsSync(
|
|
4287
|
+
if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4288
|
+
if (existsSync(join15(cwd, "yarn.lock"))) return "yarn";
|
|
4688
4289
|
return "npm";
|
|
4689
4290
|
}
|
|
4690
4291
|
async function runDoctor(opts) {
|
|
@@ -4722,7 +4323,7 @@ async function runDoctor(opts) {
|
|
|
4722
4323
|
autoFix: () => runInit({ cwd })
|
|
4723
4324
|
});
|
|
4724
4325
|
if (foundShellDir) {
|
|
4725
|
-
const nestCliPath =
|
|
4326
|
+
const nestCliPath = join15(cwd, "nest-cli.json");
|
|
4726
4327
|
const nestCli = readJson(nestCliPath);
|
|
4727
4328
|
const compiler = nestCli?.compilerOptions ?? {};
|
|
4728
4329
|
const assets = compiler.assets ?? [];
|
|
@@ -4761,7 +4362,7 @@ async function runDoctor(opts) {
|
|
|
4761
4362
|
fix: "Run: nestjs-codegen codegen",
|
|
4762
4363
|
autoFix: () => runCodegen({ cwd })
|
|
4763
4364
|
});
|
|
4764
|
-
const tsconfigPath =
|
|
4365
|
+
const tsconfigPath = join15(cwd, "tsconfig.json");
|
|
4765
4366
|
const tsconfig = readJson(tsconfigPath);
|
|
4766
4367
|
const paths = tsconfig?.compilerOptions?.paths;
|
|
4767
4368
|
checks.push({
|
|
@@ -4772,7 +4373,7 @@ async function runDoctor(opts) {
|
|
|
4772
4373
|
});
|
|
4773
4374
|
const inertiaDir = foundShellDir ?? "inertia";
|
|
4774
4375
|
for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
|
|
4775
|
-
const tsc = readJson(
|
|
4376
|
+
const tsc = readJson(join15(cwd, tsconfigFile));
|
|
4776
4377
|
if (!tsc) continue;
|
|
4777
4378
|
const excl = tsc.exclude ?? [];
|
|
4778
4379
|
const excludesIt = excl.includes(inertiaDir);
|
|
@@ -4798,14 +4399,14 @@ async function runDoctor(opts) {
|
|
|
4798
4399
|
}
|
|
4799
4400
|
});
|
|
4800
4401
|
}
|
|
4801
|
-
const inertiaTsconfigPath =
|
|
4402
|
+
const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
|
|
4802
4403
|
const inertiaTsconfig = readJson(inertiaTsconfigPath);
|
|
4803
4404
|
checks.push({
|
|
4804
4405
|
name: "tsconfig.inertia.json exists",
|
|
4805
4406
|
pass: !!inertiaTsconfig,
|
|
4806
4407
|
fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
|
|
4807
4408
|
autoFix: () => {
|
|
4808
|
-
|
|
4409
|
+
writeFileSync3(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
|
|
4809
4410
|
}
|
|
4810
4411
|
});
|
|
4811
4412
|
if (inertiaTsconfig) {
|
|
@@ -4850,17 +4451,17 @@ async function runDoctor(opts) {
|
|
|
4850
4451
|
fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
|
|
4851
4452
|
});
|
|
4852
4453
|
}
|
|
4853
|
-
const innerTsconfigPath =
|
|
4454
|
+
const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
|
|
4854
4455
|
checks.push({
|
|
4855
4456
|
name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
|
|
4856
4457
|
pass: existsSync(innerTsconfigPath),
|
|
4857
4458
|
fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
|
|
4858
4459
|
autoFix: () => {
|
|
4859
|
-
|
|
4460
|
+
writeFileSync3(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
|
|
4860
4461
|
}
|
|
4861
4462
|
});
|
|
4862
4463
|
if (checkFileExists(cwd, "vite.config.ts")) {
|
|
4863
|
-
const viteContent =
|
|
4464
|
+
const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
|
|
4864
4465
|
checks.push({
|
|
4865
4466
|
name: "vite.config.ts has resolve.alias",
|
|
4866
4467
|
pass: viteContent.includes("resolve") && viteContent.includes("alias"),
|
|
@@ -4925,8 +4526,8 @@ async function runDoctor(opts) {
|
|
|
4925
4526
|
});
|
|
4926
4527
|
}
|
|
4927
4528
|
if (checkFileExists(cwd, ".gitignore")) {
|
|
4928
|
-
const gitignorePath =
|
|
4929
|
-
const gitignore =
|
|
4529
|
+
const gitignorePath = join15(cwd, ".gitignore");
|
|
4530
|
+
const gitignore = readFileSync4(gitignorePath, "utf8");
|
|
4930
4531
|
checks.push({
|
|
4931
4532
|
name: ".gitignore includes .nestjs-inertia/",
|
|
4932
4533
|
pass: gitignore.includes(".nestjs-inertia"),
|
|
@@ -4934,7 +4535,7 @@ async function runDoctor(opts) {
|
|
|
4934
4535
|
autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
|
|
4935
4536
|
});
|
|
4936
4537
|
}
|
|
4937
|
-
const pkgJsonPath =
|
|
4538
|
+
const pkgJsonPath = join15(cwd, "package.json");
|
|
4938
4539
|
const pkgJson = readJson(pkgJsonPath);
|
|
4939
4540
|
const scripts = pkgJson?.scripts ?? {};
|
|
4940
4541
|
checks.push({
|