@dudousxd/nestjs-codegen 0.2.1 → 0.4.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 +57 -0
- package/dist/cli/main.cjs +1229 -1612
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1211 -1594
- 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-DA4uySjo.d.cts} +84 -41
- package/dist/{index-BwIRjOQA.d.ts → index-DA4uySjo.d.ts} +84 -41
- package/dist/index.cjs +1073 -1460
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -15
- package/dist/index.d.ts +56 -15
- package/dist/index.js +1043 -1430
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +934 -1365
- 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 +921 -1352
- 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,200 @@ 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 irTypeAliases = /* @__PURE__ */ new Map();
|
|
1255
|
+
const irAnnotations = /* @__PURE__ */ new Map();
|
|
1256
|
+
const decls = [];
|
|
1257
|
+
const mapEntries = [];
|
|
1258
|
+
let used = false;
|
|
1259
|
+
const renderSource = (src, rename) => {
|
|
1260
|
+
if (src.schema) {
|
|
1261
|
+
const r = adapter.renderModule(src.schema);
|
|
1262
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
1263
|
+
if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
|
|
1264
|
+
if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
|
|
1265
|
+
return { text: r.schemaText };
|
|
1266
|
+
}
|
|
1267
|
+
if (src.zodText) {
|
|
1268
|
+
if (!acceptsRawZod) {
|
|
1269
|
+
return {
|
|
1270
|
+
text: "",
|
|
1271
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
1275
|
+
}
|
|
1276
|
+
if (src.zodRef) {
|
|
1277
|
+
if (!acceptsRawZod) {
|
|
1278
|
+
return {
|
|
1279
|
+
text: "",
|
|
1280
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1284
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
1285
|
+
const member = src.zodRef.name.slice(root.length);
|
|
1286
|
+
return { text: `${alias}${member}` };
|
|
1287
|
+
}
|
|
1288
|
+
return null;
|
|
1289
|
+
};
|
|
1290
|
+
for (const entry of entries) {
|
|
1291
|
+
const block = [];
|
|
1292
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
1293
|
+
let bodyConst;
|
|
1294
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
1295
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
1296
|
+
}
|
|
1297
|
+
if (entry.body) {
|
|
1298
|
+
const rendered = renderSource(entry.body, rename);
|
|
1299
|
+
if (rendered?.warn) {
|
|
1300
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
1301
|
+
} else if (rendered) {
|
|
1302
|
+
used = true;
|
|
1303
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
1304
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
1305
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (entry.query) {
|
|
1309
|
+
const rendered = renderSource(entry.query, rename);
|
|
1310
|
+
if (rendered?.warn) {
|
|
1311
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
1312
|
+
} else if (rendered) {
|
|
1313
|
+
used = true;
|
|
1314
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
1315
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
1316
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (block.length === 0) continue;
|
|
1320
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
1321
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
1322
|
+
}
|
|
1323
|
+
if (!used) return null;
|
|
1324
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1325
|
+
if (acceptsRawZod) {
|
|
1326
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
1327
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
1328
|
+
} else {
|
|
1329
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1330
|
+
}
|
|
1331
|
+
lines.push(...importLines);
|
|
1332
|
+
lines.push("");
|
|
1333
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
1334
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
1335
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
1336
|
+
if (allNested.size > 0) {
|
|
1337
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1338
|
+
for (const [n, alias] of irTypeAliases) {
|
|
1339
|
+
if (allNested.has(n)) lines.push(`${alias};`);
|
|
1340
|
+
}
|
|
1341
|
+
for (const [n, t] of allNested) {
|
|
1342
|
+
const annotation = irAnnotations.get(n);
|
|
1343
|
+
lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
|
|
1344
|
+
}
|
|
1345
|
+
lines.push("");
|
|
1346
|
+
}
|
|
1347
|
+
lines.push(...decls);
|
|
1348
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
1349
|
+
lines.push("export const formSchemas = {");
|
|
1350
|
+
lines.push(...mapEntries);
|
|
1351
|
+
lines.push("} as const;");
|
|
1352
|
+
lines.push("");
|
|
1353
|
+
return lines.join("\n");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/emit/emit-index.ts
|
|
1357
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1358
|
+
import { join as join7 } from "path";
|
|
1359
|
+
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
1360
|
+
await mkdir4(outDir, { recursive: true });
|
|
1361
|
+
const exports = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
1362
|
+
if (hasContracts) {
|
|
1363
|
+
exports.push("export * from './api.js';");
|
|
1364
|
+
}
|
|
1365
|
+
if (hasForms) {
|
|
1366
|
+
exports.push("export * from './forms.js';");
|
|
1367
|
+
}
|
|
1368
|
+
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
|
|
1369
|
+
"\n"
|
|
1370
|
+
);
|
|
1371
|
+
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/emit/emit-pages.ts
|
|
1463
1375
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1464
|
-
import { join as
|
|
1376
|
+
import { join as join8, relative as relative5 } from "path";
|
|
1465
1377
|
async function emitPages(pages, outDir, _options = {}) {
|
|
1466
1378
|
await mkdir5(outDir, { recursive: true });
|
|
1467
1379
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
@@ -1482,7 +1394,7 @@ ${augBody}
|
|
|
1482
1394
|
}
|
|
1483
1395
|
${sharedPropsBlock}}
|
|
1484
1396
|
`;
|
|
1485
|
-
await writeFile5(
|
|
1397
|
+
await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
|
|
1486
1398
|
}
|
|
1487
1399
|
function buildSharedPropsBlock(sharedProps) {
|
|
1488
1400
|
if (!sharedProps) return "";
|
|
@@ -1513,11 +1425,11 @@ function needsQuotes(name) {
|
|
|
1513
1425
|
|
|
1514
1426
|
// src/emit/emit-routes.ts
|
|
1515
1427
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1516
|
-
import { join as
|
|
1428
|
+
import { join as join9 } from "path";
|
|
1517
1429
|
async function emitRoutes(routes, outDir) {
|
|
1518
1430
|
await mkdir6(outDir, { recursive: true });
|
|
1519
1431
|
const content = buildRoutesFile(routes);
|
|
1520
|
-
await writeFile6(
|
|
1432
|
+
await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
|
|
1521
1433
|
}
|
|
1522
1434
|
function buildRoutesFile(routes) {
|
|
1523
1435
|
if (routes.length === 0) {
|
|
@@ -1645,30 +1557,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1645
1557
|
propsExport: pagesConfig.propsExport,
|
|
1646
1558
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
1647
1559
|
});
|
|
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
|
-
}
|
|
1560
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
1672
1561
|
await emitPages(pages, config.codegen.outDir, {
|
|
1673
1562
|
propsExport: pagesConfig.propsExport,
|
|
1674
1563
|
sharedProps
|
|
@@ -1692,7 +1581,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1692
1581
|
if (extensions.length > 0) {
|
|
1693
1582
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1694
1583
|
for (const file of extraFiles) {
|
|
1695
|
-
const dest =
|
|
1584
|
+
const dest = join10(config.codegen.outDir, file.path);
|
|
1696
1585
|
await mkdir7(dirname2(dest), { recursive: true });
|
|
1697
1586
|
await writeFile7(dest, file.contents, "utf8");
|
|
1698
1587
|
}
|
|
@@ -1701,15 +1590,20 @@ async function generate(config, inputRoutes = []) {
|
|
|
1701
1590
|
|
|
1702
1591
|
// src/watch/watcher.ts
|
|
1703
1592
|
import { readFile as readFile3 } from "fs/promises";
|
|
1704
|
-
import { join as
|
|
1593
|
+
import { join as join13 } from "path";
|
|
1705
1594
|
import chokidar from "chokidar";
|
|
1706
1595
|
|
|
1707
1596
|
// src/discovery/contracts-fast.ts
|
|
1708
|
-
import { join as
|
|
1597
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
1709
1598
|
import fg2 from "fast-glob";
|
|
1710
1599
|
import {
|
|
1711
|
-
Node as
|
|
1712
|
-
Project as Project3
|
|
1600
|
+
Node as Node8,
|
|
1601
|
+
Project as Project3
|
|
1602
|
+
} from "ts-morph";
|
|
1603
|
+
|
|
1604
|
+
// src/discovery/dto-type-resolver.ts
|
|
1605
|
+
import {
|
|
1606
|
+
Node as Node6,
|
|
1713
1607
|
SyntaxKind as SyntaxKind3
|
|
1714
1608
|
} from "ts-morph";
|
|
1715
1609
|
|
|
@@ -1724,20 +1618,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
|
1724
1618
|
import {
|
|
1725
1619
|
Node as Node2
|
|
1726
1620
|
} from "ts-morph";
|
|
1727
|
-
var
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
return prev;
|
|
1732
|
-
}
|
|
1733
|
-
function restoreDiscoveryContext(ctx) {
|
|
1734
|
-
_ctx = ctx;
|
|
1621
|
+
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
1622
|
+
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
1623
|
+
function setDiscoveryContext(project, ctx) {
|
|
1624
|
+
_ctxByProject.set(project, ctx);
|
|
1735
1625
|
}
|
|
1736
|
-
function
|
|
1737
|
-
return
|
|
1738
|
-
}
|
|
1739
|
-
function _tsconfigPaths() {
|
|
1740
|
-
return _ctx.tsconfigPaths;
|
|
1626
|
+
function _ctxFor(project) {
|
|
1627
|
+
return _ctxByProject.get(project) ?? _EMPTY_CTX;
|
|
1741
1628
|
}
|
|
1742
1629
|
var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
|
|
1743
1630
|
function dbg(...args) {
|
|
@@ -1779,7 +1666,7 @@ function findTypeInFile(name, file) {
|
|
|
1779
1666
|
}
|
|
1780
1667
|
return null;
|
|
1781
1668
|
}
|
|
1782
|
-
function resolveModuleSpecifier(moduleSpecifier, sourceFile,
|
|
1669
|
+
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
1783
1670
|
if (moduleSpecifier.startsWith(".")) {
|
|
1784
1671
|
const dir = dirname3(sourceFile.getFilePath());
|
|
1785
1672
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
@@ -1789,8 +1676,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
|
|
|
1789
1676
|
resolve2(dir, moduleSpecifier, "index.ts")
|
|
1790
1677
|
];
|
|
1791
1678
|
}
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
1679
|
+
const ctx = _ctxFor(project);
|
|
1680
|
+
const baseUrl = ctx.projectRoot;
|
|
1681
|
+
const tsconfigPaths = ctx.tsconfigPaths;
|
|
1794
1682
|
dbg(
|
|
1795
1683
|
"resolveModuleSpecifier",
|
|
1796
1684
|
moduleSpecifier,
|
|
@@ -1936,7 +1824,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
1936
1824
|
if (!namedImport) continue;
|
|
1937
1825
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1938
1826
|
if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
1939
|
-
const tsconfigPaths =
|
|
1827
|
+
const tsconfigPaths = _ctxFor(project).tsconfigPaths;
|
|
1940
1828
|
const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
|
|
1941
1829
|
const prefix = p.replace("*", "");
|
|
1942
1830
|
return moduleSpecifier.startsWith(prefix);
|
|
@@ -2005,10 +1893,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
2005
1893
|
depth: 0
|
|
2006
1894
|
};
|
|
2007
1895
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
2008
|
-
|
|
2009
|
-
ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
|
|
2010
|
-
}
|
|
2011
|
-
return { root, named: ctx.named, warnings: ctx.warnings };
|
|
1896
|
+
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
2012
1897
|
}
|
|
2013
1898
|
function buildObject(classDecl, classFile, ctx) {
|
|
2014
1899
|
const props = classDecl.getProperties();
|
|
@@ -2028,7 +1913,7 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
2028
1913
|
const dec = (n) => decorators.get(n);
|
|
2029
1914
|
const typeNode = prop.getTypeNode();
|
|
2030
1915
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
2031
|
-
const isArrayType = !!typeNode &&
|
|
1916
|
+
const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
|
|
2032
1917
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
2033
1918
|
if (has("ValidateNested") || typeRefName) {
|
|
2034
1919
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
@@ -2159,18 +2044,27 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2159
2044
|
}
|
|
2160
2045
|
}
|
|
2161
2046
|
function buildNestedReference(className, fromFile, ctx) {
|
|
2162
|
-
if (ctx.visiting.has(className)
|
|
2047
|
+
if (ctx.visiting.has(className)) {
|
|
2163
2048
|
const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
|
|
2164
2049
|
ctx.emittedClasses.set(className, reserved);
|
|
2165
2050
|
ctx.recursiveSchemas.add(reserved);
|
|
2166
2051
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2167
2052
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
2168
|
-
const msg = `${className} is a recursive type
|
|
2053
|
+
const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
|
|
2169
2054
|
ctx.warnings.push(msg);
|
|
2170
2055
|
console.warn(`[nestjs-codegen] ${msg}`);
|
|
2171
2056
|
}
|
|
2172
2057
|
return { kind: "lazyRef", name: reserved };
|
|
2173
2058
|
}
|
|
2059
|
+
if (ctx.depth >= 8) {
|
|
2060
|
+
if (!ctx.warnedDecorators.has(`deep:${className}`)) {
|
|
2061
|
+
ctx.warnedDecorators.add(`deep:${className}`);
|
|
2062
|
+
const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
|
|
2063
|
+
ctx.warnings.push(msg);
|
|
2064
|
+
console.warn(`[nestjs-codegen] ${msg}`);
|
|
2065
|
+
}
|
|
2066
|
+
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2067
|
+
}
|
|
2174
2068
|
const existing = ctx.emittedClasses.get(className);
|
|
2175
2069
|
if (existing) return { kind: "ref", name: existing };
|
|
2176
2070
|
const schemaName = aliasFor(className, ctx);
|
|
@@ -2289,465 +2183,135 @@ function inSchemaFromDecorator(decorator) {
|
|
|
2289
2183
|
return null;
|
|
2290
2184
|
}
|
|
2291
2185
|
|
|
2292
|
-
// src/discovery/
|
|
2186
|
+
// src/discovery/filter-for.ts
|
|
2293
2187
|
import {
|
|
2294
|
-
Node as
|
|
2188
|
+
Node as Node5
|
|
2295
2189
|
} 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
|
-
};
|
|
2190
|
+
|
|
2191
|
+
// src/discovery/filter-field-types.ts
|
|
2192
|
+
import {
|
|
2193
|
+
Node as Node4,
|
|
2194
|
+
SyntaxKind as SyntaxKind2
|
|
2195
|
+
} from "ts-morph";
|
|
2196
|
+
|
|
2197
|
+
// src/discovery/enum-resolution.ts
|
|
2198
|
+
function resolveEnumValues(name, sourceFile, project) {
|
|
2199
|
+
const resolved = findType(name, sourceFile, project);
|
|
2200
|
+
if (!resolved || resolved.kind !== "enum") return null;
|
|
2201
|
+
let numeric = true;
|
|
2202
|
+
const values = resolved.members.map((m) => {
|
|
2203
|
+
const parsed = JSON.parse(m);
|
|
2204
|
+
if (typeof parsed === "string") numeric = false;
|
|
2205
|
+
return String(parsed);
|
|
2206
|
+
});
|
|
2207
|
+
if (values.length === 0) return null;
|
|
2208
|
+
return { values, numeric };
|
|
2345
2209
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2210
|
+
|
|
2211
|
+
// src/discovery/filter-field-types.ts
|
|
2212
|
+
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2213
|
+
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2214
|
+
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2215
|
+
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2216
|
+
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2217
|
+
function classifyTypeKeyword(raw) {
|
|
2218
|
+
const t = raw.toLowerCase();
|
|
2219
|
+
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2220
|
+
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2221
|
+
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2222
|
+
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2223
|
+
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2224
|
+
return null;
|
|
2358
2225
|
}
|
|
2359
|
-
function
|
|
2360
|
-
return
|
|
2226
|
+
function markNullable(r, nullable) {
|
|
2227
|
+
return nullable ? { ...r, nullable: true } : r;
|
|
2361
2228
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
const childName = typeRefName ?? singularClassName2(typeText);
|
|
2374
|
-
if (childName) {
|
|
2375
|
-
const childExpr = buildNestedReference2(childName, classFile, ctx);
|
|
2376
|
-
const wrapArray = has("IsArray") || isArrayType;
|
|
2377
|
-
let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
|
|
2378
|
-
expr2 = applyPresence2(expr2, decorators);
|
|
2379
|
-
return expr2;
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
let base = baseFromType2(typeText, isArrayType, ctx, classFile);
|
|
2383
|
-
const refinements = [];
|
|
2384
|
-
if (has("IsString")) base = "z.string()";
|
|
2385
|
-
if (has("IsBoolean")) base = "z.boolean()";
|
|
2386
|
-
if (has("IsDate")) base = "z.coerce.date()";
|
|
2387
|
-
if (has("IsNumber")) base = "z.number()";
|
|
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}`);
|
|
2229
|
+
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2230
|
+
if (Node4.isUnionTypeNode(typeNode)) {
|
|
2231
|
+
let nullable = false;
|
|
2232
|
+
const stringLits = [];
|
|
2233
|
+
const numberLits = [];
|
|
2234
|
+
const others = [];
|
|
2235
|
+
for (const member of typeNode.getTypeNodes()) {
|
|
2236
|
+
const kind = member.getKind();
|
|
2237
|
+
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2238
|
+
nullable = true;
|
|
2239
|
+
continue;
|
|
2450
2240
|
}
|
|
2241
|
+
if (Node4.isLiteralTypeNode(member)) {
|
|
2242
|
+
const lit = member.getLiteral();
|
|
2243
|
+
if (Node4.isStringLiteral(lit)) {
|
|
2244
|
+
stringLits.push(lit.getLiteralValue());
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
if (Node4.isNumericLiteral(lit)) {
|
|
2248
|
+
numberLits.push(lit.getText());
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2252
|
+
nullable = true;
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
others.push(member);
|
|
2257
|
+
}
|
|
2258
|
+
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2259
|
+
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2451
2260
|
}
|
|
2261
|
+
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2262
|
+
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2263
|
+
}
|
|
2264
|
+
if (others.length === 1) {
|
|
2265
|
+
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2266
|
+
return markNullable(inner, nullable || inner.nullable === true);
|
|
2267
|
+
}
|
|
2268
|
+
return markNullable({ kind: "unknown" }, nullable);
|
|
2452
2269
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2270
|
+
switch (typeNode.getKind()) {
|
|
2271
|
+
case SyntaxKind2.StringKeyword:
|
|
2272
|
+
return { kind: "string" };
|
|
2273
|
+
case SyntaxKind2.NumberKeyword:
|
|
2274
|
+
return { kind: "number" };
|
|
2275
|
+
case SyntaxKind2.BooleanKeyword:
|
|
2276
|
+
return { kind: "boolean" };
|
|
2277
|
+
case SyntaxKind2.AnyKeyword:
|
|
2278
|
+
case SyntaxKind2.UnknownKeyword:
|
|
2279
|
+
return { kind: "unknown" };
|
|
2280
|
+
default:
|
|
2281
|
+
break;
|
|
2456
2282
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2283
|
+
if (Node4.isTypeReference(typeNode)) {
|
|
2284
|
+
const refName = typeNode.getTypeName().getText();
|
|
2285
|
+
if (refName === "Date") return { kind: "date" };
|
|
2286
|
+
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
2287
|
+
const typeRef = opts?.resolveRef?.(refName) ?? null;
|
|
2288
|
+
const en = resolveEnumValues(refName, sourceFile, project);
|
|
2289
|
+
if (en) {
|
|
2290
|
+
const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
2291
|
+
return typeRef ? { ...base, typeRef } : base;
|
|
2292
|
+
}
|
|
2293
|
+
if (typeRef) return { kind: "unknown", typeRef };
|
|
2294
|
+
return { kind: "unknown" };
|
|
2460
2295
|
}
|
|
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" };
|
|
2296
|
+
if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
2297
|
+
return { kind: "unknown" };
|
|
2734
2298
|
}
|
|
2735
2299
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
2736
2300
|
for (const arg of args) {
|
|
2737
|
-
if (
|
|
2301
|
+
if (Node4.isArrowFunction(arg)) {
|
|
2738
2302
|
const body = arg.getBody();
|
|
2739
|
-
if (
|
|
2303
|
+
if (Node4.isIdentifier(body)) {
|
|
2740
2304
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2741
2305
|
if (en) return en;
|
|
2742
2306
|
}
|
|
2743
2307
|
}
|
|
2744
|
-
if (
|
|
2308
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2745
2309
|
const itemsProp = arg.getProperty("items");
|
|
2746
|
-
if (itemsProp &&
|
|
2310
|
+
if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
|
|
2747
2311
|
const init = itemsProp.getInitializer();
|
|
2748
|
-
if (init &&
|
|
2312
|
+
if (init && Node4.isArrowFunction(init)) {
|
|
2749
2313
|
const body = init.getBody();
|
|
2750
|
-
if (
|
|
2314
|
+
if (Node4.isIdentifier(body)) {
|
|
2751
2315
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2752
2316
|
if (en) return en;
|
|
2753
2317
|
}
|
|
@@ -2770,7 +2334,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2770
2334
|
return { kind: "string" };
|
|
2771
2335
|
}
|
|
2772
2336
|
for (const arg of args) {
|
|
2773
|
-
if (
|
|
2337
|
+
if (Node4.isStringLiteral(arg)) {
|
|
2774
2338
|
const raw = arg.getLiteralValue();
|
|
2775
2339
|
const kind = classifyTypeKeyword(raw);
|
|
2776
2340
|
if (kind) {
|
|
@@ -2778,11 +2342,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2778
2342
|
return { kind };
|
|
2779
2343
|
}
|
|
2780
2344
|
}
|
|
2781
|
-
if (
|
|
2345
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2782
2346
|
const enumProp = arg.getProperty("enum");
|
|
2783
|
-
if (enumProp &&
|
|
2347
|
+
if (enumProp && Node4.isPropertyAssignment(enumProp)) {
|
|
2784
2348
|
const init = enumProp.getInitializer();
|
|
2785
|
-
if (init &&
|
|
2349
|
+
if (init && Node4.isIdentifier(init)) {
|
|
2786
2350
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
2787
2351
|
if (en) {
|
|
2788
2352
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -2791,9 +2355,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2791
2355
|
}
|
|
2792
2356
|
}
|
|
2793
2357
|
const typeProp = arg.getProperty("type");
|
|
2794
|
-
if (typeProp &&
|
|
2358
|
+
if (typeProp && Node4.isPropertyAssignment(typeProp)) {
|
|
2795
2359
|
const init = typeProp.getInitializer();
|
|
2796
|
-
if (init &&
|
|
2360
|
+
if (init && Node4.isStringLiteral(init)) {
|
|
2797
2361
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
2798
2362
|
if (kind) return { kind };
|
|
2799
2363
|
}
|
|
@@ -2829,7 +2393,7 @@ function toFilterFieldType(name, r) {
|
|
|
2829
2393
|
|
|
2830
2394
|
// src/discovery/filter-for.ts
|
|
2831
2395
|
function classifyFilterForHint(typeInit) {
|
|
2832
|
-
if (
|
|
2396
|
+
if (Node5.isStringLiteral(typeInit)) {
|
|
2833
2397
|
switch (typeInit.getLiteralValue()) {
|
|
2834
2398
|
case "string":
|
|
2835
2399
|
return { kind: "string" };
|
|
@@ -2843,10 +2407,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
2843
2407
|
return null;
|
|
2844
2408
|
}
|
|
2845
2409
|
}
|
|
2846
|
-
if (
|
|
2410
|
+
if (Node5.isArrayLiteralExpression(typeInit)) {
|
|
2847
2411
|
const values = [];
|
|
2848
2412
|
for (const el of typeInit.getElements()) {
|
|
2849
|
-
if (!
|
|
2413
|
+
if (!Node5.isStringLiteral(el)) return null;
|
|
2850
2414
|
values.push(el.getLiteralValue());
|
|
2851
2415
|
}
|
|
2852
2416
|
if (values.length === 0) return null;
|
|
@@ -2881,11 +2445,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
2881
2445
|
if (!filterForDec) continue;
|
|
2882
2446
|
const args = filterForDec.getArguments();
|
|
2883
2447
|
const keyArg = args[0];
|
|
2884
|
-
const inputKey = keyArg &&
|
|
2448
|
+
const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
2885
2449
|
const optsArg = args[1];
|
|
2886
|
-
if (optsArg &&
|
|
2450
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
2887
2451
|
const typeProp = optsArg.getProperty("type");
|
|
2888
|
-
if (typeProp &&
|
|
2452
|
+
if (typeProp && Node5.isPropertyAssignment(typeProp)) {
|
|
2889
2453
|
const typeInit = typeProp.getInitializer();
|
|
2890
2454
|
if (typeInit) {
|
|
2891
2455
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -2908,14 +2472,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
2908
2472
|
const args = filterDecorator.getArguments();
|
|
2909
2473
|
if (args.length === 0) continue;
|
|
2910
2474
|
const filterClassArg = args[0];
|
|
2911
|
-
if (!filterClassArg || !
|
|
2475
|
+
if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
|
|
2912
2476
|
let source = "query";
|
|
2913
2477
|
const optionsArg = args[1];
|
|
2914
|
-
if (optionsArg &&
|
|
2478
|
+
if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
|
|
2915
2479
|
const sourceProp = optionsArg.getProperty("source");
|
|
2916
|
-
if (sourceProp &&
|
|
2480
|
+
if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
|
|
2917
2481
|
const init = sourceProp.getInitializer();
|
|
2918
|
-
if (init &&
|
|
2482
|
+
if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
2919
2483
|
source = "body";
|
|
2920
2484
|
}
|
|
2921
2485
|
}
|
|
@@ -2978,22 +2542,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
2978
2542
|
const args = dec.getArguments();
|
|
2979
2543
|
if (args.length === 0) continue;
|
|
2980
2544
|
const arg = args[0];
|
|
2981
|
-
if (
|
|
2545
|
+
if (Node5.isObjectLiteralExpression(arg)) {
|
|
2982
2546
|
const entityProp = arg.getProperty("entity");
|
|
2983
|
-
if (entityProp &&
|
|
2547
|
+
if (entityProp && Node5.isPropertyAssignment(entityProp)) {
|
|
2984
2548
|
const init = entityProp.getInitializer();
|
|
2985
|
-
if (init &&
|
|
2549
|
+
if (init && Node5.isArrowFunction(init)) {
|
|
2986
2550
|
const body = init.getBody();
|
|
2987
|
-
if (
|
|
2551
|
+
if (Node5.isIdentifier(body)) {
|
|
2988
2552
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
2989
2553
|
if (resolved?.kind === "class") return resolved.decl;
|
|
2990
2554
|
}
|
|
2991
2555
|
}
|
|
2992
2556
|
}
|
|
2993
2557
|
}
|
|
2994
|
-
if (
|
|
2558
|
+
if (Node5.isArrowFunction(arg)) {
|
|
2995
2559
|
const body = arg.getBody();
|
|
2996
|
-
if (
|
|
2560
|
+
if (Node5.isIdentifier(body)) {
|
|
2997
2561
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
2998
2562
|
if (resolved?.kind === "class") return resolved.decl;
|
|
2999
2563
|
}
|
|
@@ -3017,11 +2581,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3017
2581
|
const args = filterableDecorator.getArguments();
|
|
3018
2582
|
if (args.length === 0) return [];
|
|
3019
2583
|
const optionsArg = args[0];
|
|
3020
|
-
if (!
|
|
2584
|
+
if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
|
|
3021
2585
|
const entityProp = optionsArg.getProperty("entity");
|
|
3022
|
-
if (!entityProp || !
|
|
2586
|
+
if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
|
|
3023
2587
|
const entityInit = entityProp.getInitializer();
|
|
3024
|
-
if (!entityInit || !
|
|
2588
|
+
if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
|
|
3025
2589
|
const entityName = entityInit.getText();
|
|
3026
2590
|
const filterSourceFile = filterClass.getSourceFile();
|
|
3027
2591
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -3037,17 +2601,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3037
2601
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
3038
2602
|
if (relationsDecorator) {
|
|
3039
2603
|
const relArgs = relationsDecorator.getArguments();
|
|
3040
|
-
if (relArgs.length > 0 &&
|
|
2604
|
+
if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
|
|
3041
2605
|
for (const relProp of relArgs[0].getProperties()) {
|
|
3042
|
-
if (!
|
|
2606
|
+
if (!Node5.isPropertyAssignment(relProp)) continue;
|
|
3043
2607
|
const relInit = relProp.getInitializer();
|
|
3044
|
-
if (!relInit || !
|
|
2608
|
+
if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
|
|
3045
2609
|
const keysProp = relInit.getProperty("keys");
|
|
3046
|
-
if (!keysProp || !
|
|
2610
|
+
if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
|
|
3047
2611
|
const keysInit = keysProp.getInitializer();
|
|
3048
|
-
if (!keysInit || !
|
|
2612
|
+
if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
|
|
3049
2613
|
for (const el of keysInit.getElements()) {
|
|
3050
|
-
if (
|
|
2614
|
+
if (Node5.isStringLiteral(el)) {
|
|
3051
2615
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
3052
2616
|
}
|
|
3053
2617
|
}
|
|
@@ -3057,267 +2621,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3057
2621
|
return fields;
|
|
3058
2622
|
}
|
|
3059
2623
|
|
|
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
|
-
|
|
2624
|
+
// src/discovery/dto-type-resolver.ts
|
|
2625
|
+
var WRAPPER_TYPES = {
|
|
2626
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
2627
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
2628
|
+
// entity. Unwrap to the type argument.
|
|
2629
|
+
Ref: "unwrap",
|
|
2630
|
+
Reference: "unwrap",
|
|
2631
|
+
LoadedReference: "unwrap",
|
|
2632
|
+
IdentifiedReference: "unwrap",
|
|
2633
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
2634
|
+
Opt: "unwrap",
|
|
2635
|
+
Loaded: "unwrap",
|
|
2636
|
+
// Promise<T> — unwrap
|
|
2637
|
+
Promise: "unwrap",
|
|
2638
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
2639
|
+
Collection: "arrayOf",
|
|
2640
|
+
// Array<T> generic form
|
|
2641
|
+
Array: "arrayOf"
|
|
2642
|
+
};
|
|
2643
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
2644
|
+
"Record",
|
|
2645
|
+
"Omit",
|
|
2646
|
+
"Pick",
|
|
2647
|
+
"Partial",
|
|
2648
|
+
"Required",
|
|
2649
|
+
"Readonly",
|
|
2650
|
+
"Map",
|
|
2651
|
+
"Set"
|
|
2652
|
+
]);
|
|
2653
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
2654
|
+
if (depth <= 0) return "unknown";
|
|
2655
|
+
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2656
|
+
const elementType = typeNode.getElementTypeNode();
|
|
2657
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3083
2658
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
project.addSourceFileAtPath(f);
|
|
2659
|
+
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2660
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3087
2661
|
}
|
|
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);
|
|
2662
|
+
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2663
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3099
2664
|
}
|
|
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)) {
|
|
2665
|
+
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
3268
2666
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3269
2667
|
}
|
|
3270
|
-
if (
|
|
2668
|
+
if (Node6.isTypeReference(typeNode)) {
|
|
3271
2669
|
const typeName = typeNode.getTypeName();
|
|
3272
|
-
const name =
|
|
2670
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3273
2671
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
3274
2672
|
if (name === "Date") return "string";
|
|
3275
2673
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
3276
2674
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
3277
2675
|
return "unknown";
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3282
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3283
|
-
}
|
|
3284
|
-
return "unknown";
|
|
2676
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
2677
|
+
if (wrapperMode) {
|
|
2678
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3285
2679
|
}
|
|
3286
|
-
if (name
|
|
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";
|
|
3301
|
-
}
|
|
3302
|
-
if (name === "Array") {
|
|
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)) {
|
|
2680
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
3311
2681
|
return typeNode.getText();
|
|
3312
2682
|
}
|
|
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
2683
|
const resolved = findType(name, sourceFile, project);
|
|
3322
2684
|
if (resolved) {
|
|
3323
2685
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -3333,6 +2695,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
3333
2695
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
3334
2696
|
return typeNode.getText();
|
|
3335
2697
|
}
|
|
2698
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
2699
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
2700
|
+
const firstTypeArg = typeArgs[0];
|
|
2701
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2702
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
2703
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2704
|
+
}
|
|
2705
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2706
|
+
}
|
|
3336
2707
|
function expandTypeDecl(result, project, depth) {
|
|
3337
2708
|
if (depth < 0) return "unknown";
|
|
3338
2709
|
switch (result.kind) {
|
|
@@ -3398,7 +2769,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
3398
2769
|
const paramArgs = paramDecorator.getArguments();
|
|
3399
2770
|
if (paramArgs.length === 0) continue;
|
|
3400
2771
|
const nameArg = paramArgs[0];
|
|
3401
|
-
if (!
|
|
2772
|
+
if (!Node6.isStringLiteral(nameArg)) continue;
|
|
3402
2773
|
const paramName = nameArg.getLiteralValue();
|
|
3403
2774
|
const typeNode = param.getTypeNode();
|
|
3404
2775
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -3411,13 +2782,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3411
2782
|
if (apiResponseDecorator) {
|
|
3412
2783
|
const args = apiResponseDecorator.getArguments();
|
|
3413
2784
|
const optsArg = args[0];
|
|
3414
|
-
if (optsArg &&
|
|
2785
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3415
2786
|
for (const prop of optsArg.getProperties()) {
|
|
3416
|
-
if (!
|
|
2787
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3417
2788
|
if (prop.getName() !== "type") continue;
|
|
3418
2789
|
const val = prop.getInitializer();
|
|
3419
2790
|
if (!val) continue;
|
|
3420
|
-
if (
|
|
2791
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3421
2792
|
const elements = val.getElements();
|
|
3422
2793
|
const firstEl = elements[0];
|
|
3423
2794
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -3437,7 +2808,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3437
2808
|
return "unknown";
|
|
3438
2809
|
}
|
|
3439
2810
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
3440
|
-
if (!
|
|
2811
|
+
if (!Node6.isIdentifier(node)) return "unknown";
|
|
3441
2812
|
const name = node.getText();
|
|
3442
2813
|
const resolved = findType(name, sourceFile, project);
|
|
3443
2814
|
if (resolved) {
|
|
@@ -3484,11 +2855,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3484
2855
|
if (apiResp) {
|
|
3485
2856
|
const args = apiResp.getArguments();
|
|
3486
2857
|
const optsArg = args[0];
|
|
3487
|
-
if (optsArg &&
|
|
2858
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3488
2859
|
for (const prop of optsArg.getProperties()) {
|
|
3489
|
-
if (
|
|
2860
|
+
if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
3490
2861
|
const val = prop.getInitializer();
|
|
3491
|
-
if (val &&
|
|
2862
|
+
if (val && Node6.isIdentifier(val)) {
|
|
3492
2863
|
const name = val.getText();
|
|
3493
2864
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3494
2865
|
if (localDecl?.isExported()) {
|
|
@@ -3505,60 +2876,243 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3505
2876
|
}
|
|
3506
2877
|
}
|
|
3507
2878
|
}
|
|
3508
|
-
let
|
|
3509
|
-
let
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
formWarnings.push(...
|
|
3520
|
-
|
|
2879
|
+
let bodySchema = null;
|
|
2880
|
+
let querySchema = null;
|
|
2881
|
+
const formWarnings = [];
|
|
2882
|
+
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
2883
|
+
if (bodyClass) {
|
|
2884
|
+
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
2885
|
+
formWarnings.push(...bodySchema.warnings);
|
|
2886
|
+
}
|
|
2887
|
+
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
2888
|
+
if (queryClass) {
|
|
2889
|
+
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
2890
|
+
formWarnings.push(...querySchema.warnings);
|
|
2891
|
+
}
|
|
2892
|
+
return {
|
|
2893
|
+
query,
|
|
2894
|
+
body,
|
|
2895
|
+
response,
|
|
2896
|
+
params: paramsType,
|
|
2897
|
+
queryRef,
|
|
2898
|
+
bodyRef,
|
|
2899
|
+
responseRef,
|
|
2900
|
+
filterFields: filterInfo?.fieldNames ?? null,
|
|
2901
|
+
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2902
|
+
filterSource: filterInfo?.source ?? null,
|
|
2903
|
+
formWarnings,
|
|
2904
|
+
bodySchema,
|
|
2905
|
+
querySchema
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
2909
|
+
for (const param of method.getParameters()) {
|
|
2910
|
+
if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
|
|
2911
|
+
const typeNode = param.getTypeNode();
|
|
2912
|
+
if (!typeNode) continue;
|
|
2913
|
+
const text = typeNode.getText().replace(/\[\]$/, "");
|
|
2914
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
2915
|
+
const resolved = findType(text, sourceFile, project);
|
|
2916
|
+
if (resolved && resolved.kind === "class") {
|
|
2917
|
+
return { decl: resolved.decl, file: resolved.file };
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
return null;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
2924
|
+
import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2925
|
+
function zodAstToTs(node) {
|
|
2926
|
+
if (!Node7.isCallExpression(node)) return "unknown";
|
|
2927
|
+
const expr = node.getExpression();
|
|
2928
|
+
if (Node7.isPropertyAccessExpression(expr)) {
|
|
2929
|
+
const methodName = expr.getName();
|
|
2930
|
+
const receiver = expr.getExpression();
|
|
2931
|
+
if (methodName === "optional") {
|
|
2932
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
2933
|
+
}
|
|
2934
|
+
if (methodName === "nullable") {
|
|
2935
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
2936
|
+
}
|
|
2937
|
+
const args = node.getArguments();
|
|
2938
|
+
switch (methodName) {
|
|
2939
|
+
case "string":
|
|
2940
|
+
return "string";
|
|
2941
|
+
case "number":
|
|
2942
|
+
return "number";
|
|
2943
|
+
case "boolean":
|
|
2944
|
+
return "boolean";
|
|
2945
|
+
case "unknown":
|
|
2946
|
+
return "unknown";
|
|
2947
|
+
case "any":
|
|
2948
|
+
return "unknown";
|
|
2949
|
+
case "literal": {
|
|
2950
|
+
const lit = args[0];
|
|
2951
|
+
if (!lit) return "unknown";
|
|
2952
|
+
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
2953
|
+
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
2954
|
+
if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
|
|
2955
|
+
if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
|
|
2956
|
+
return "unknown";
|
|
2957
|
+
}
|
|
2958
|
+
case "enum": {
|
|
2959
|
+
const arrArg = args[0];
|
|
2960
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2961
|
+
const members = arrArg.getElements().map(
|
|
2962
|
+
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
2963
|
+
);
|
|
2964
|
+
return members.join(" | ");
|
|
2965
|
+
}
|
|
2966
|
+
case "array": {
|
|
2967
|
+
const inner = args[0];
|
|
2968
|
+
if (!inner) return "unknown";
|
|
2969
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
2970
|
+
}
|
|
2971
|
+
case "object": {
|
|
2972
|
+
const objArg = args[0];
|
|
2973
|
+
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
2974
|
+
const lines = [];
|
|
2975
|
+
for (const prop of objArg.getProperties()) {
|
|
2976
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
2977
|
+
const key = prop.getName();
|
|
2978
|
+
const valNode = prop.getInitializer();
|
|
2979
|
+
if (!valNode) continue;
|
|
2980
|
+
const tsType = zodAstToTs(valNode);
|
|
2981
|
+
const isOpt = isOptionalChain(valNode);
|
|
2982
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
2983
|
+
}
|
|
2984
|
+
return `{ ${lines.join("; ")} }`;
|
|
2985
|
+
}
|
|
2986
|
+
case "union": {
|
|
2987
|
+
const arrArg = args[0];
|
|
2988
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2989
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
2990
|
+
}
|
|
2991
|
+
case "record": {
|
|
2992
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
2993
|
+
if (!valArg) return "unknown";
|
|
2994
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
2995
|
+
}
|
|
2996
|
+
case "tuple": {
|
|
2997
|
+
const arrArg = args[0];
|
|
2998
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2999
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
3000
|
+
}
|
|
3001
|
+
default:
|
|
3002
|
+
return "unknown";
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return "unknown";
|
|
3006
|
+
}
|
|
3007
|
+
function isOptionalChain(node) {
|
|
3008
|
+
if (!Node7.isCallExpression(node)) return false;
|
|
3009
|
+
const expr = node.getExpression();
|
|
3010
|
+
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
3011
|
+
}
|
|
3012
|
+
function parseDefineContractCall(callExpr) {
|
|
3013
|
+
if (!Node7.isCallExpression(callExpr)) return null;
|
|
3014
|
+
const callee = callExpr.getExpression();
|
|
3015
|
+
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3016
|
+
if (calleeName !== "defineContract") return null;
|
|
3017
|
+
const args = callExpr.getArguments();
|
|
3018
|
+
const optsArg = args[0];
|
|
3019
|
+
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3020
|
+
let query = null;
|
|
3021
|
+
let body = null;
|
|
3022
|
+
let response = "unknown";
|
|
3023
|
+
let bodyZodText = null;
|
|
3024
|
+
let queryZodText = null;
|
|
3025
|
+
for (const prop of optsArg.getProperties()) {
|
|
3026
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3027
|
+
const propName = prop.getName();
|
|
3028
|
+
const val = prop.getInitializer();
|
|
3029
|
+
if (!val) continue;
|
|
3030
|
+
if (propName === "query") {
|
|
3031
|
+
query = zodAstToTs(val);
|
|
3032
|
+
queryZodText = val.getText();
|
|
3033
|
+
} else if (propName === "body") {
|
|
3034
|
+
body = zodAstToTs(val);
|
|
3035
|
+
bodyZodText = val.getText();
|
|
3036
|
+
} else if (propName === "response") {
|
|
3037
|
+
response = zodAstToTs(val);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/discovery/contracts-fast.ts
|
|
3044
|
+
async function discoverContractsFast(opts) {
|
|
3045
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3046
|
+
const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
|
|
3047
|
+
let project;
|
|
3048
|
+
try {
|
|
3049
|
+
project = new Project3({
|
|
3050
|
+
tsConfigFilePath: tsconfigPath,
|
|
3051
|
+
skipAddingFilesFromTsConfig: true,
|
|
3052
|
+
skipLoadingLibFiles: true,
|
|
3053
|
+
skipFileDependencyResolution: true
|
|
3054
|
+
});
|
|
3055
|
+
} catch {
|
|
3056
|
+
project = new Project3({
|
|
3057
|
+
skipAddingFilesFromTsConfig: true,
|
|
3058
|
+
skipLoadingLibFiles: true,
|
|
3059
|
+
skipFileDependencyResolution: true,
|
|
3060
|
+
compilerOptions: {
|
|
3061
|
+
allowJs: true,
|
|
3062
|
+
resolveJsonModule: false,
|
|
3063
|
+
strict: false
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3068
|
+
for (const f of files) {
|
|
3069
|
+
project.addSourceFileAtPath(f);
|
|
3070
|
+
}
|
|
3071
|
+
const routes = [];
|
|
3072
|
+
setDiscoveryContext(project, {
|
|
3073
|
+
projectRoot: cwd,
|
|
3074
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3075
|
+
});
|
|
3076
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
3077
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3521
3078
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3079
|
+
return routes;
|
|
3080
|
+
}
|
|
3081
|
+
function decoratorStringArg(decoratorExpr) {
|
|
3082
|
+
if (!decoratorExpr) return void 0;
|
|
3083
|
+
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3084
|
+
if (Node8.isArrayLiteralExpression(decoratorExpr)) {
|
|
3085
|
+
const first = decoratorExpr.getElements()[0];
|
|
3086
|
+
if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
|
|
3529
3087
|
}
|
|
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
|
-
};
|
|
3088
|
+
return void 0;
|
|
3548
3089
|
}
|
|
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
|
-
}
|
|
3090
|
+
function deriveClassSegment(className) {
|
|
3091
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
3092
|
+
if (!noSuffix) {
|
|
3093
|
+
throw new Error(
|
|
3094
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3095
|
+
);
|
|
3560
3096
|
}
|
|
3561
|
-
return
|
|
3097
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3098
|
+
}
|
|
3099
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3100
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3101
|
+
const methodPortion = methodAs ?? methodName;
|
|
3102
|
+
return `${classPortion}.${methodPortion}`;
|
|
3103
|
+
}
|
|
3104
|
+
function joinPaths(prefix, suffix) {
|
|
3105
|
+
if (!prefix && !suffix) return "/";
|
|
3106
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3107
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3108
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3109
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3110
|
+
const combined = p + s;
|
|
3111
|
+
return combined === "" ? "/" : combined;
|
|
3112
|
+
}
|
|
3113
|
+
function extractParams(path) {
|
|
3114
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
3115
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3562
3116
|
}
|
|
3563
3117
|
var HTTP_METHOD_DECORATORS = {
|
|
3564
3118
|
Get: "GET",
|
|
@@ -3570,176 +3124,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
3570
3124
|
Head: "HEAD",
|
|
3571
3125
|
All: "ALL"
|
|
3572
3126
|
};
|
|
3127
|
+
function resolveVerb(method) {
|
|
3128
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3129
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
3130
|
+
if (httpDecorator) {
|
|
3131
|
+
const httpArgs = httpDecorator.getArguments();
|
|
3132
|
+
const pathArg = httpArgs[0];
|
|
3133
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
return null;
|
|
3137
|
+
}
|
|
3138
|
+
function readAsDecorator(node, label) {
|
|
3139
|
+
const asDecorator = node.getDecorator("As");
|
|
3140
|
+
if (!asDecorator) return void 0;
|
|
3141
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
3142
|
+
if (!asName) {
|
|
3143
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
3144
|
+
}
|
|
3145
|
+
return asName;
|
|
3146
|
+
}
|
|
3147
|
+
function buildRoute(args) {
|
|
3148
|
+
const {
|
|
3149
|
+
className,
|
|
3150
|
+
methodName,
|
|
3151
|
+
resolvedMethod,
|
|
3152
|
+
combinedPath,
|
|
3153
|
+
classAs,
|
|
3154
|
+
methodAs,
|
|
3155
|
+
sourceFile,
|
|
3156
|
+
seenNames,
|
|
3157
|
+
contractSource
|
|
3158
|
+
} = args;
|
|
3159
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3160
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
3161
|
+
const existing = seenNames.get(routeName);
|
|
3162
|
+
if (existing !== void 0) {
|
|
3163
|
+
throw new Error(
|
|
3164
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
seenNames.set(routeName, qualifiedRef);
|
|
3168
|
+
return {
|
|
3169
|
+
method: resolvedMethod,
|
|
3170
|
+
path: combinedPath,
|
|
3171
|
+
name: routeName,
|
|
3172
|
+
params: extractParams(combinedPath),
|
|
3173
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3174
|
+
contract: { contractSource }
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
function extractContractRoute(args) {
|
|
3178
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
3179
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3180
|
+
if (!firstDecoratorArg) return null;
|
|
3181
|
+
let contractDef = null;
|
|
3182
|
+
let bodyZodRef = null;
|
|
3183
|
+
let queryZodRef = null;
|
|
3184
|
+
if (Node8.isCallExpression(firstDecoratorArg)) {
|
|
3185
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3186
|
+
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3187
|
+
const identName = firstDecoratorArg.getText();
|
|
3188
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
3189
|
+
if (!varDecl) {
|
|
3190
|
+
console.warn(
|
|
3191
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
3192
|
+
);
|
|
3193
|
+
return null;
|
|
3194
|
+
}
|
|
3195
|
+
const initializer = varDecl.getInitializer();
|
|
3196
|
+
if (!initializer) return null;
|
|
3197
|
+
contractDef = parseDefineContractCall(initializer);
|
|
3198
|
+
if (contractDef && varDecl.isExported()) {
|
|
3199
|
+
const filePath = sourceFile.getFilePath();
|
|
3200
|
+
if (contractDef.body !== null) {
|
|
3201
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3202
|
+
}
|
|
3203
|
+
if (contractDef.query !== null) {
|
|
3204
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
} else {
|
|
3208
|
+
console.warn(
|
|
3209
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3210
|
+
);
|
|
3211
|
+
return null;
|
|
3212
|
+
}
|
|
3213
|
+
if (!contractDef) return null;
|
|
3214
|
+
if (!verb) return null;
|
|
3215
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
3216
|
+
const methodName = method.getName();
|
|
3217
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3218
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3219
|
+
return buildRoute({
|
|
3220
|
+
className,
|
|
3221
|
+
methodName,
|
|
3222
|
+
resolvedMethod: verb.httpMethod,
|
|
3223
|
+
combinedPath: resolvedPath,
|
|
3224
|
+
classAs,
|
|
3225
|
+
methodAs,
|
|
3226
|
+
sourceFile,
|
|
3227
|
+
seenNames,
|
|
3228
|
+
contractSource: {
|
|
3229
|
+
query: contractDef.query,
|
|
3230
|
+
body: contractDef.body,
|
|
3231
|
+
response: contractDef.response,
|
|
3232
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3233
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3234
|
+
// would drag server-only deps into the client bundle).
|
|
3235
|
+
bodyZodRef,
|
|
3236
|
+
bodyZodText: contractDef.bodyZodText,
|
|
3237
|
+
queryZodRef,
|
|
3238
|
+
queryZodText: contractDef.queryZodText
|
|
3239
|
+
}
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
function extractDtoRoute(args) {
|
|
3243
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
3244
|
+
if (!verb) return null;
|
|
3245
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
3246
|
+
const methodName = method.getName();
|
|
3247
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3248
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3249
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3250
|
+
return buildRoute({
|
|
3251
|
+
className,
|
|
3252
|
+
methodName,
|
|
3253
|
+
resolvedMethod: verb.httpMethod,
|
|
3254
|
+
combinedPath: combined,
|
|
3255
|
+
classAs,
|
|
3256
|
+
methodAs,
|
|
3257
|
+
sourceFile,
|
|
3258
|
+
seenNames,
|
|
3259
|
+
contractSource: {
|
|
3260
|
+
query: dtoContract?.query ?? null,
|
|
3261
|
+
body: dtoContract?.body ?? null,
|
|
3262
|
+
response: dtoContract?.response ?? "unknown",
|
|
3263
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
3264
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3265
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
3266
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
3267
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3268
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
3269
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3270
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3271
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
3272
|
+
}
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3573
3275
|
function extractFromSourceFile(sourceFile, project) {
|
|
3574
3276
|
const routes = [];
|
|
3575
3277
|
const seenNames = /* @__PURE__ */ new Map();
|
|
3576
|
-
const
|
|
3577
|
-
for (const cls of classes) {
|
|
3278
|
+
for (const cls of sourceFile.getClasses()) {
|
|
3578
3279
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
3579
3280
|
if (!controllerDecorator) continue;
|
|
3580
|
-
const
|
|
3581
|
-
const
|
|
3582
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
3281
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
3282
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
3583
3283
|
const className = cls.getName() ?? "Unknown";
|
|
3584
3284
|
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
|
-
}
|
|
3285
|
+
const verb = resolveVerb(method);
|
|
3597
3286
|
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
|
-
}
|
|
3287
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
3288
|
+
cls,
|
|
3289
|
+
method,
|
|
3290
|
+
applyContractDecorator,
|
|
3291
|
+
verb,
|
|
3292
|
+
prefix,
|
|
3293
|
+
className,
|
|
3294
|
+
sourceFile,
|
|
3295
|
+
seenNames
|
|
3296
|
+
}) : extractDtoRoute({
|
|
3297
|
+
cls,
|
|
3298
|
+
method,
|
|
3299
|
+
verb,
|
|
3300
|
+
prefix,
|
|
3301
|
+
className,
|
|
3302
|
+
sourceFile,
|
|
3303
|
+
project,
|
|
3304
|
+
seenNames
|
|
3305
|
+
});
|
|
3306
|
+
if (route) routes.push(route);
|
|
3743
3307
|
}
|
|
3744
3308
|
}
|
|
3745
3309
|
return routes;
|
|
@@ -3748,7 +3312,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3748
3312
|
// src/watch/lock-file.ts
|
|
3749
3313
|
import { open } from "fs/promises";
|
|
3750
3314
|
import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
|
|
3751
|
-
import { join as
|
|
3315
|
+
import { join as join12 } from "path";
|
|
3752
3316
|
var LOCK_FILE = ".watcher.lock";
|
|
3753
3317
|
function isProcessAlive(pid) {
|
|
3754
3318
|
try {
|
|
@@ -3760,7 +3324,7 @@ function isProcessAlive(pid) {
|
|
|
3760
3324
|
}
|
|
3761
3325
|
async function acquireLock(outDir) {
|
|
3762
3326
|
await mkdir8(outDir, { recursive: true });
|
|
3763
|
-
const lockPath =
|
|
3327
|
+
const lockPath = join12(outDir, LOCK_FILE);
|
|
3764
3328
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3765
3329
|
try {
|
|
3766
3330
|
const fd = await open(lockPath, "wx");
|
|
@@ -3800,7 +3364,7 @@ async function watch(config, onChange) {
|
|
|
3800
3364
|
if (lock === null) {
|
|
3801
3365
|
let holderPid = "unknown";
|
|
3802
3366
|
try {
|
|
3803
|
-
const raw = await readFile3(
|
|
3367
|
+
const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3804
3368
|
const data = JSON.parse(raw);
|
|
3805
3369
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3806
3370
|
} catch {
|
|
@@ -3828,7 +3392,7 @@ async function watch(config, onChange) {
|
|
|
3828
3392
|
}
|
|
3829
3393
|
let pagesDebounceTimer;
|
|
3830
3394
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3831
|
-
const pagesWatcher = chokidar.watch(
|
|
3395
|
+
const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
|
|
3832
3396
|
ignoreInitial: true,
|
|
3833
3397
|
persistent: true,
|
|
3834
3398
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3854,7 +3418,7 @@ async function watch(config, onChange) {
|
|
|
3854
3418
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3855
3419
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3856
3420
|
let contractsDebounceTimer;
|
|
3857
|
-
const contractsWatcher = chokidar.watch(
|
|
3421
|
+
const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
|
|
3858
3422
|
ignoreInitial: true,
|
|
3859
3423
|
persistent: true,
|
|
3860
3424
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3884,7 +3448,7 @@ async function watch(config, onChange) {
|
|
|
3884
3448
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3885
3449
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3886
3450
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3887
|
-
const formsWatcher = chokidar.watch(
|
|
3451
|
+
const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
|
|
3888
3452
|
ignoreInitial: true,
|
|
3889
3453
|
persistent: true,
|
|
3890
3454
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3911,7 +3475,7 @@ async function watch(config, onChange) {
|
|
|
3911
3475
|
}
|
|
3912
3476
|
|
|
3913
3477
|
// src/index.ts
|
|
3914
|
-
var VERSION = "0.
|
|
3478
|
+
var VERSION = "0.4.0";
|
|
3915
3479
|
|
|
3916
3480
|
// src/cli/codegen.ts
|
|
3917
3481
|
async function runCodegen(opts = {}) {
|
|
@@ -3939,15 +3503,51 @@ async function runCodegen(opts = {}) {
|
|
|
3939
3503
|
|
|
3940
3504
|
// src/cli/doctor.ts
|
|
3941
3505
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3942
|
-
import { appendFileSync, existsSync, readFileSync as
|
|
3943
|
-
import { join as
|
|
3506
|
+
import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
3507
|
+
import { join as join15 } from "path";
|
|
3944
3508
|
|
|
3945
3509
|
// src/cli/init.ts
|
|
3946
3510
|
import { execFileSync } from "child_process";
|
|
3947
|
-
import { readFileSync as
|
|
3511
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3948
3512
|
import { access as access2, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
|
|
3949
|
-
import { join as
|
|
3513
|
+
import { join as join14 } from "path";
|
|
3950
3514
|
import { createInterface } from "readline";
|
|
3515
|
+
|
|
3516
|
+
// src/cli/patch-utils.ts
|
|
3517
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3518
|
+
function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
|
|
3519
|
+
let raw;
|
|
3520
|
+
try {
|
|
3521
|
+
raw = readFileSync2(filePath, "utf8");
|
|
3522
|
+
} catch {
|
|
3523
|
+
return "skipped";
|
|
3524
|
+
}
|
|
3525
|
+
const json = JSON.parse(parse(raw));
|
|
3526
|
+
if (!mutator(json)) return "already";
|
|
3527
|
+
writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
|
|
3528
|
+
`, "utf8");
|
|
3529
|
+
return "patched";
|
|
3530
|
+
}
|
|
3531
|
+
function findAfterLastImport(content) {
|
|
3532
|
+
const lastImportIndex = content.lastIndexOf("\nimport ");
|
|
3533
|
+
if (lastImportIndex !== -1) {
|
|
3534
|
+
const endOfLine = content.indexOf("\n", lastImportIndex + 1);
|
|
3535
|
+
return endOfLine !== -1 ? endOfLine + 1 : content.length;
|
|
3536
|
+
}
|
|
3537
|
+
if (content.startsWith("import ")) {
|
|
3538
|
+
const endOfLine = content.indexOf("\n");
|
|
3539
|
+
return endOfLine !== -1 ? endOfLine + 1 : content.length;
|
|
3540
|
+
}
|
|
3541
|
+
return 0;
|
|
3542
|
+
}
|
|
3543
|
+
function insertImport(content, stmt) {
|
|
3544
|
+
const insertAt = findAfterLastImport(content);
|
|
3545
|
+
if (insertAt <= 0) return content;
|
|
3546
|
+
return `${content.slice(0, insertAt)}${stmt}
|
|
3547
|
+
${content.slice(insertAt)}`;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// src/cli/init.ts
|
|
3951
3551
|
var GITIGNORE_ENTRY = ".nestjs-inertia/";
|
|
3952
3552
|
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
3953
3553
|
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
@@ -3972,7 +3572,7 @@ ${bold(title)}`);
|
|
|
3972
3572
|
}
|
|
3973
3573
|
async function readPackageJson(cwd) {
|
|
3974
3574
|
try {
|
|
3975
|
-
const raw = await readFile4(
|
|
3575
|
+
const raw = await readFile4(join14(cwd, "package.json"), "utf8");
|
|
3976
3576
|
return JSON.parse(raw);
|
|
3977
3577
|
} catch {
|
|
3978
3578
|
return {};
|
|
@@ -4003,7 +3603,7 @@ async function detectTemplateEngine(cwd) {
|
|
|
4003
3603
|
async function detectPackageManager(cwd) {
|
|
4004
3604
|
async function exists(file) {
|
|
4005
3605
|
try {
|
|
4006
|
-
await access2(
|
|
3606
|
+
await access2(join14(cwd, file));
|
|
4007
3607
|
return true;
|
|
4008
3608
|
} catch {
|
|
4009
3609
|
return false;
|
|
@@ -4050,7 +3650,7 @@ async function writeIfNotExists(filePath, content, label) {
|
|
|
4050
3650
|
logCreated(label);
|
|
4051
3651
|
}
|
|
4052
3652
|
async function handleViteConfig(cwd, framework) {
|
|
4053
|
-
const filePath =
|
|
3653
|
+
const filePath = join14(cwd, "vite.config.ts");
|
|
4054
3654
|
if (await fileExists2(filePath)) {
|
|
4055
3655
|
const existing = await readFile4(filePath, "utf8");
|
|
4056
3656
|
const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
|
|
@@ -4108,7 +3708,7 @@ function installDeps(pkgManager, deps, dev) {
|
|
|
4108
3708
|
}
|
|
4109
3709
|
}
|
|
4110
3710
|
async function patchPackageJsonScripts(cwd, scripts) {
|
|
4111
|
-
const pkgPath =
|
|
3711
|
+
const pkgPath = join14(cwd, "package.json");
|
|
4112
3712
|
let pkg = {};
|
|
4113
3713
|
try {
|
|
4114
3714
|
pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
@@ -4133,32 +3733,16 @@ async function patchPackageJsonScripts(cwd, scripts) {
|
|
|
4133
3733
|
await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
4134
3734
|
`, "utf8");
|
|
4135
3735
|
}
|
|
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
3736
|
function patchAppModule(filePath, rootView) {
|
|
4149
3737
|
let content;
|
|
4150
3738
|
try {
|
|
4151
|
-
content =
|
|
3739
|
+
content = readFileSync3(filePath, "utf8");
|
|
4152
3740
|
} catch {
|
|
4153
3741
|
return "skipped";
|
|
4154
3742
|
}
|
|
4155
3743
|
let changed = false;
|
|
4156
3744
|
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
|
-
}
|
|
3745
|
+
content = insertImport(content, "import { InertiaModule } from '@dudousxd/nestjs-inertia';");
|
|
4162
3746
|
if (!content.includes("from 'node:path'") && !content.includes('from "node:path"')) {
|
|
4163
3747
|
const insertAt2 = findAfterLastImport(content);
|
|
4164
3748
|
content = `${content.slice(0, insertAt2)}import { resolve } from 'node:path';
|
|
@@ -4176,11 +3760,7 @@ ${indent}}),${content.slice(bracketPos)}`;
|
|
|
4176
3760
|
}
|
|
4177
3761
|
}
|
|
4178
3762
|
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
|
-
}
|
|
3763
|
+
content = insertImport(content, "import { HomeController } from './home.controller';");
|
|
4184
3764
|
const controllersMatch = content.match(/controllers\s*:\s*\[/);
|
|
4185
3765
|
if (controllersMatch?.index !== void 0) {
|
|
4186
3766
|
const bracketPos = content.indexOf("[", controllersMatch.index) + 1;
|
|
@@ -4191,22 +3771,21 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
|
|
|
4191
3771
|
}
|
|
4192
3772
|
}
|
|
4193
3773
|
if (!changed) return "already";
|
|
4194
|
-
|
|
3774
|
+
writeFileSync2(filePath, content, "utf8");
|
|
4195
3775
|
return "patched";
|
|
4196
3776
|
}
|
|
4197
3777
|
function patchMainTs(filePath) {
|
|
4198
3778
|
let content;
|
|
4199
3779
|
try {
|
|
4200
|
-
content =
|
|
3780
|
+
content = readFileSync3(filePath, "utf8");
|
|
4201
3781
|
} catch {
|
|
4202
3782
|
return "skipped";
|
|
4203
3783
|
}
|
|
4204
3784
|
if (content.includes("setupInertiaVite")) return "already";
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
}
|
|
3785
|
+
content = insertImport(
|
|
3786
|
+
content,
|
|
3787
|
+
"import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';"
|
|
3788
|
+
);
|
|
4210
3789
|
const createMatch = content.match(
|
|
4211
3790
|
/(?:const|let)\s+(\w+)\s*=\s*await\s+NestFactory\.create[^;]+;/
|
|
4212
3791
|
);
|
|
@@ -4223,7 +3802,7 @@ ${content.slice(insertAt)}`;
|
|
|
4223
3802
|
});`;
|
|
4224
3803
|
content = `${content.slice(0, insertAfterPos)}
|
|
4225
3804
|
${viteSetup}${content.slice(insertAfterPos)}`;
|
|
4226
|
-
|
|
3805
|
+
writeFileSync2(filePath, content, "utf8");
|
|
4227
3806
|
return "patched";
|
|
4228
3807
|
}
|
|
4229
3808
|
function configTemplate(framework) {
|
|
@@ -4446,107 +4025,84 @@ export class HomeController {
|
|
|
4446
4025
|
}
|
|
4447
4026
|
`;
|
|
4448
4027
|
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";
|
|
4028
|
+
const filePath = join14(cwd, filename);
|
|
4029
|
+
return patchJsonFile(
|
|
4030
|
+
filePath,
|
|
4031
|
+
(json) => {
|
|
4032
|
+
const exclude = json.exclude ?? [];
|
|
4033
|
+
if (exclude.includes(dir)) return false;
|
|
4034
|
+
exclude.push(dir);
|
|
4035
|
+
json.exclude = exclude;
|
|
4036
|
+
return true;
|
|
4037
|
+
},
|
|
4038
|
+
// Strip single-line comments before JSON.parse
|
|
4039
|
+
(raw) => raw.replace(/\/\/.*$/gm, "")
|
|
4040
|
+
);
|
|
4465
4041
|
}
|
|
4466
4042
|
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
|
|
4043
|
+
const filePath = join14(cwd, "nest-cli.json");
|
|
4044
|
+
return patchJsonFile(filePath, (json) => {
|
|
4045
|
+
const compiler = json.compilerOptions ?? {};
|
|
4046
|
+
const assets = compiler.assets ?? [];
|
|
4047
|
+
const alreadyHas = assets.some((a) => {
|
|
4048
|
+
if (typeof a === "string") return a.includes(shellDir);
|
|
4049
|
+
return String(a.include ?? "").includes(shellDir);
|
|
4050
|
+
});
|
|
4051
|
+
if (alreadyHas) return false;
|
|
4052
|
+
assets.push({
|
|
4053
|
+
include: `../${shellDir}/**/*`,
|
|
4054
|
+
outDir: `dist/${shellDir}`,
|
|
4055
|
+
watchAssets: true
|
|
4056
|
+
});
|
|
4057
|
+
compiler.assets = assets;
|
|
4058
|
+
json.compilerOptions = compiler;
|
|
4059
|
+
return true;
|
|
4486
4060
|
});
|
|
4487
|
-
compiler.assets = assets;
|
|
4488
|
-
json.compilerOptions = compiler;
|
|
4489
|
-
writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
|
|
4490
|
-
`, "utf8");
|
|
4491
|
-
return "patched";
|
|
4492
4061
|
}
|
|
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";
|
|
4062
|
+
async function scaffoldFiles(ctx) {
|
|
4063
|
+
const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
|
|
4509
4064
|
logSection("Scaffold files");
|
|
4510
4065
|
await writeIfNotExists(
|
|
4511
|
-
|
|
4066
|
+
join14(cwd, "nestjs-inertia.config.ts"),
|
|
4512
4067
|
configTemplate(framework),
|
|
4513
4068
|
"nestjs-inertia.config.ts"
|
|
4514
4069
|
);
|
|
4515
|
-
await writeIfNotExists(
|
|
4070
|
+
await writeIfNotExists(join14(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
|
|
4516
4071
|
await writeIfNotExists(
|
|
4517
|
-
|
|
4072
|
+
join14(cwd, "tsconfig.inertia.json"),
|
|
4518
4073
|
TSCONFIG_INERTIA_TEMPLATE,
|
|
4519
4074
|
"tsconfig.inertia.json"
|
|
4520
4075
|
);
|
|
4521
4076
|
await writeIfNotExists(
|
|
4522
|
-
|
|
4077
|
+
join14(cwd, "inertia", "tsconfig.json"),
|
|
4523
4078
|
INERTIA_TSCONFIG_TEMPLATE,
|
|
4524
4079
|
"inertia/tsconfig.json"
|
|
4525
4080
|
);
|
|
4526
4081
|
await writeIfNotExists(
|
|
4527
|
-
|
|
4082
|
+
join14(cwd, "inertia", shellFileName),
|
|
4528
4083
|
htmlShellTemplate(framework, engine),
|
|
4529
4084
|
`inertia/${shellFileName}`
|
|
4530
4085
|
);
|
|
4531
4086
|
await handleViteConfig(cwd, framework);
|
|
4532
4087
|
await writeIfNotExists(
|
|
4533
|
-
|
|
4088
|
+
join14(cwd, "inertia", "app", `client.${entryExt}`),
|
|
4534
4089
|
entryPointTemplate(framework),
|
|
4535
4090
|
`inertia/app/client.${entryExt}`
|
|
4536
4091
|
);
|
|
4537
4092
|
await writeIfNotExists(
|
|
4538
|
-
|
|
4093
|
+
join14(cwd, "inertia", "pages", `Home.${pageExt}`),
|
|
4539
4094
|
samplePageTemplate(framework),
|
|
4540
4095
|
`inertia/pages/Home.${pageExt}`
|
|
4541
4096
|
);
|
|
4542
4097
|
await writeIfNotExists(
|
|
4543
|
-
|
|
4098
|
+
join14(cwd, "src", "home.controller.ts"),
|
|
4544
4099
|
SAMPLE_CONTROLLER,
|
|
4545
4100
|
"src/home.controller.ts"
|
|
4546
4101
|
);
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
const
|
|
4102
|
+
}
|
|
4103
|
+
function patchServerAppModule(ctx) {
|
|
4104
|
+
const { cwd, rootView } = ctx;
|
|
4105
|
+
const appModulePath = join14(cwd, "src", "app.module.ts");
|
|
4550
4106
|
const appModuleResult = patchAppModule(appModulePath, rootView);
|
|
4551
4107
|
if (appModuleResult === "patched") {
|
|
4552
4108
|
logPatched("src/app.module.ts", "added InertiaModule.forRoot");
|
|
@@ -4558,7 +4114,9 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4558
4114
|
} else {
|
|
4559
4115
|
logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
|
|
4560
4116
|
}
|
|
4561
|
-
|
|
4117
|
+
}
|
|
4118
|
+
function patchServerMainTs(ctx) {
|
|
4119
|
+
const mainTsPath = join14(ctx.cwd, "src", "main.ts");
|
|
4562
4120
|
const mainTsResult = patchMainTs(mainTsPath);
|
|
4563
4121
|
if (mainTsResult === "patched") {
|
|
4564
4122
|
logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
|
|
@@ -4567,7 +4125,9 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4567
4125
|
} else {
|
|
4568
4126
|
logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
|
|
4569
4127
|
}
|
|
4570
|
-
|
|
4128
|
+
}
|
|
4129
|
+
function patchBuildConfigs(ctx) {
|
|
4130
|
+
const { cwd, shellDir } = ctx;
|
|
4571
4131
|
const nestCliResult = patchNestCliJson(cwd, shellDir);
|
|
4572
4132
|
if (nestCliResult === "patched") {
|
|
4573
4133
|
logPatched("nest-cli.json", `added asset copy for ${shellDir}/ \u2192 dist/${shellDir}/`);
|
|
@@ -4588,18 +4148,26 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4588
4148
|
);
|
|
4589
4149
|
}
|
|
4590
4150
|
}
|
|
4591
|
-
|
|
4151
|
+
}
|
|
4152
|
+
async function patchGitignoreAndDist(ctx) {
|
|
4153
|
+
const { cwd } = ctx;
|
|
4154
|
+
await patchGitignore(join14(cwd, ".gitignore"));
|
|
4592
4155
|
const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
|
|
4593
4156
|
if (tsconfigDistResult === "patched") {
|
|
4594
4157
|
logPatched("tsconfig.json", "excluded dist/ from server compilation");
|
|
4595
4158
|
} else if (tsconfigDistResult === "already") {
|
|
4596
4159
|
console.log(` ${cyan("\u2192")} tsconfig.json ${dim("(dist/ already excluded, skipped)")}`);
|
|
4597
4160
|
}
|
|
4598
|
-
|
|
4161
|
+
}
|
|
4162
|
+
async function scaffoldPackageScripts(ctx) {
|
|
4163
|
+
await patchPackageJsonScripts(ctx.cwd, {
|
|
4599
4164
|
"build:client": "vite build",
|
|
4600
4165
|
"build:ssr": "VITE_SSR=1 vite build --ssr",
|
|
4601
4166
|
"typecheck:inertia": "tsc --noEmit -p tsconfig.inertia.json"
|
|
4602
4167
|
});
|
|
4168
|
+
}
|
|
4169
|
+
async function installMissingDeps(ctx) {
|
|
4170
|
+
const { cwd, framework, opts } = ctx;
|
|
4603
4171
|
logSection("Install dependencies");
|
|
4604
4172
|
const pkg = await readPackageJson(cwd);
|
|
4605
4173
|
const installedDeps = allDeps(pkg);
|
|
@@ -4633,6 +4201,55 @@ ${bold("nestjs-inertia init")}`);
|
|
|
4633
4201
|
installDeps(pkgManager, depsToInstall, false);
|
|
4634
4202
|
installDeps(pkgManager, devDepsToInstall, true);
|
|
4635
4203
|
}
|
|
4204
|
+
}
|
|
4205
|
+
var INIT_STEPS = [
|
|
4206
|
+
{ label: "scaffold files", run: scaffoldFiles },
|
|
4207
|
+
{
|
|
4208
|
+
label: "patch app.module.ts",
|
|
4209
|
+
run: (ctx) => {
|
|
4210
|
+
logSection("Patch existing files");
|
|
4211
|
+
patchServerAppModule(ctx);
|
|
4212
|
+
}
|
|
4213
|
+
},
|
|
4214
|
+
{ label: "patch main.ts", run: patchServerMainTs },
|
|
4215
|
+
{ label: "patch build configs", run: patchBuildConfigs },
|
|
4216
|
+
{ label: "patch .gitignore + dist exclude", run: patchGitignoreAndDist },
|
|
4217
|
+
{ label: "add package.json scripts", run: scaffoldPackageScripts },
|
|
4218
|
+
{ label: "install dependencies", run: installMissingDeps }
|
|
4219
|
+
];
|
|
4220
|
+
async function runInit(opts = {}) {
|
|
4221
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
4222
|
+
console.log(`
|
|
4223
|
+
${bold("nestjs-inertia init")}`);
|
|
4224
|
+
let framework = await detectFramework(cwd);
|
|
4225
|
+
if (!framework) {
|
|
4226
|
+
framework = await promptFramework();
|
|
4227
|
+
}
|
|
4228
|
+
const engine = await detectTemplateEngine(cwd);
|
|
4229
|
+
const engineLabel = engine === "html" ? "plain HTML" : engine;
|
|
4230
|
+
const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
|
|
4231
|
+
console.log(`
|
|
4232
|
+
Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
|
|
4233
|
+
const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
|
|
4234
|
+
const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
|
|
4235
|
+
const ctx = {
|
|
4236
|
+
opts,
|
|
4237
|
+
cwd,
|
|
4238
|
+
framework,
|
|
4239
|
+
engine,
|
|
4240
|
+
shellFileName,
|
|
4241
|
+
entryExt: framework === "react" ? "tsx" : "ts",
|
|
4242
|
+
pageExt: framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte",
|
|
4243
|
+
rootView,
|
|
4244
|
+
shellDir: rootView.split("/")[0]
|
|
4245
|
+
// e.g. "inertia" from "inertia/index.html"
|
|
4246
|
+
};
|
|
4247
|
+
for (const step of INIT_STEPS) {
|
|
4248
|
+
if (process.env.NESTJS_CODEGEN_DEBUG) {
|
|
4249
|
+
console.log(dim(` \xB7 ${step.label}`));
|
|
4250
|
+
}
|
|
4251
|
+
await step.run(ctx);
|
|
4252
|
+
}
|
|
4636
4253
|
console.log(`
|
|
4637
4254
|
${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
|
|
4638
4255
|
`);
|
|
@@ -4640,18 +4257,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
|
|
|
4640
4257
|
|
|
4641
4258
|
// src/cli/doctor.ts
|
|
4642
4259
|
function checkFileExists(cwd, file) {
|
|
4643
|
-
return existsSync(
|
|
4260
|
+
return existsSync(join15(cwd, file));
|
|
4644
4261
|
}
|
|
4645
4262
|
function readJson(path) {
|
|
4646
4263
|
try {
|
|
4647
|
-
const raw =
|
|
4264
|
+
const raw = readFileSync4(path, "utf8").replace(/\/\/.*$/gm, "");
|
|
4648
4265
|
return JSON.parse(raw);
|
|
4649
4266
|
} catch {
|
|
4650
4267
|
return null;
|
|
4651
4268
|
}
|
|
4652
4269
|
}
|
|
4653
4270
|
function writeJsonField(filePath, dotPath, value) {
|
|
4654
|
-
const raw =
|
|
4271
|
+
const raw = readFileSync4(filePath, "utf8");
|
|
4655
4272
|
const stripped = raw.replace(/\/\/.*$/gm, "");
|
|
4656
4273
|
const obj = JSON.parse(stripped);
|
|
4657
4274
|
let target = obj;
|
|
@@ -4671,20 +4288,20 @@ function writeJsonField(filePath, dotPath, value) {
|
|
|
4671
4288
|
} else {
|
|
4672
4289
|
target[lastKey] = value;
|
|
4673
4290
|
}
|
|
4674
|
-
|
|
4291
|
+
writeFileSync3(filePath, `${JSON.stringify(obj, null, 2)}
|
|
4675
4292
|
`, "utf8");
|
|
4676
4293
|
}
|
|
4677
4294
|
function getPackageVersion(cwd, pkg) {
|
|
4678
4295
|
try {
|
|
4679
|
-
const pkgJson = readJson(
|
|
4296
|
+
const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
|
|
4680
4297
|
return pkgJson?.version ?? null;
|
|
4681
4298
|
} catch {
|
|
4682
4299
|
return null;
|
|
4683
4300
|
}
|
|
4684
4301
|
}
|
|
4685
4302
|
function detectPkgManager(cwd) {
|
|
4686
|
-
if (existsSync(
|
|
4687
|
-
if (existsSync(
|
|
4303
|
+
if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4304
|
+
if (existsSync(join15(cwd, "yarn.lock"))) return "yarn";
|
|
4688
4305
|
return "npm";
|
|
4689
4306
|
}
|
|
4690
4307
|
async function runDoctor(opts) {
|
|
@@ -4722,7 +4339,7 @@ async function runDoctor(opts) {
|
|
|
4722
4339
|
autoFix: () => runInit({ cwd })
|
|
4723
4340
|
});
|
|
4724
4341
|
if (foundShellDir) {
|
|
4725
|
-
const nestCliPath =
|
|
4342
|
+
const nestCliPath = join15(cwd, "nest-cli.json");
|
|
4726
4343
|
const nestCli = readJson(nestCliPath);
|
|
4727
4344
|
const compiler = nestCli?.compilerOptions ?? {};
|
|
4728
4345
|
const assets = compiler.assets ?? [];
|
|
@@ -4761,7 +4378,7 @@ async function runDoctor(opts) {
|
|
|
4761
4378
|
fix: "Run: nestjs-codegen codegen",
|
|
4762
4379
|
autoFix: () => runCodegen({ cwd })
|
|
4763
4380
|
});
|
|
4764
|
-
const tsconfigPath =
|
|
4381
|
+
const tsconfigPath = join15(cwd, "tsconfig.json");
|
|
4765
4382
|
const tsconfig = readJson(tsconfigPath);
|
|
4766
4383
|
const paths = tsconfig?.compilerOptions?.paths;
|
|
4767
4384
|
checks.push({
|
|
@@ -4772,7 +4389,7 @@ async function runDoctor(opts) {
|
|
|
4772
4389
|
});
|
|
4773
4390
|
const inertiaDir = foundShellDir ?? "inertia";
|
|
4774
4391
|
for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
|
|
4775
|
-
const tsc = readJson(
|
|
4392
|
+
const tsc = readJson(join15(cwd, tsconfigFile));
|
|
4776
4393
|
if (!tsc) continue;
|
|
4777
4394
|
const excl = tsc.exclude ?? [];
|
|
4778
4395
|
const excludesIt = excl.includes(inertiaDir);
|
|
@@ -4798,14 +4415,14 @@ async function runDoctor(opts) {
|
|
|
4798
4415
|
}
|
|
4799
4416
|
});
|
|
4800
4417
|
}
|
|
4801
|
-
const inertiaTsconfigPath =
|
|
4418
|
+
const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
|
|
4802
4419
|
const inertiaTsconfig = readJson(inertiaTsconfigPath);
|
|
4803
4420
|
checks.push({
|
|
4804
4421
|
name: "tsconfig.inertia.json exists",
|
|
4805
4422
|
pass: !!inertiaTsconfig,
|
|
4806
4423
|
fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
|
|
4807
4424
|
autoFix: () => {
|
|
4808
|
-
|
|
4425
|
+
writeFileSync3(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
|
|
4809
4426
|
}
|
|
4810
4427
|
});
|
|
4811
4428
|
if (inertiaTsconfig) {
|
|
@@ -4850,17 +4467,17 @@ async function runDoctor(opts) {
|
|
|
4850
4467
|
fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
|
|
4851
4468
|
});
|
|
4852
4469
|
}
|
|
4853
|
-
const innerTsconfigPath =
|
|
4470
|
+
const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
|
|
4854
4471
|
checks.push({
|
|
4855
4472
|
name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
|
|
4856
4473
|
pass: existsSync(innerTsconfigPath),
|
|
4857
4474
|
fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
|
|
4858
4475
|
autoFix: () => {
|
|
4859
|
-
|
|
4476
|
+
writeFileSync3(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
|
|
4860
4477
|
}
|
|
4861
4478
|
});
|
|
4862
4479
|
if (checkFileExists(cwd, "vite.config.ts")) {
|
|
4863
|
-
const viteContent =
|
|
4480
|
+
const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
|
|
4864
4481
|
checks.push({
|
|
4865
4482
|
name: "vite.config.ts has resolve.alias",
|
|
4866
4483
|
pass: viteContent.includes("resolve") && viteContent.includes("alias"),
|
|
@@ -4925,8 +4542,8 @@ async function runDoctor(opts) {
|
|
|
4925
4542
|
});
|
|
4926
4543
|
}
|
|
4927
4544
|
if (checkFileExists(cwd, ".gitignore")) {
|
|
4928
|
-
const gitignorePath =
|
|
4929
|
-
const gitignore =
|
|
4545
|
+
const gitignorePath = join15(cwd, ".gitignore");
|
|
4546
|
+
const gitignore = readFileSync4(gitignorePath, "utf8");
|
|
4930
4547
|
checks.push({
|
|
4931
4548
|
name: ".gitignore includes .nestjs-inertia/",
|
|
4932
4549
|
pass: gitignore.includes(".nestjs-inertia"),
|
|
@@ -4934,7 +4551,7 @@ async function runDoctor(opts) {
|
|
|
4934
4551
|
autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
|
|
4935
4552
|
});
|
|
4936
4553
|
}
|
|
4937
|
-
const pkgJsonPath =
|
|
4554
|
+
const pkgJsonPath = join15(cwd, "package.json");
|
|
4938
4555
|
const pkgJson = readJson(pkgJsonPath);
|
|
4939
4556
|
const scripts = pkgJson?.scripts ?? {};
|
|
4940
4557
|
checks.push({
|