@dudousxd/nestjs-codegen 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @dudousxd/nestjs-codegen
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 6a6be24: perf: memoize type and enum resolution during generation — per-`Project` `WeakMap` caches for `findType`, `resolveTypeRef`'s named-symbol arm, and `resolveEnumValues`, so a type referenced N times is resolved once. Keyed by `Project` so each (watch) run gets a fresh cache; generated output is byte-identical.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - ed04cdc: Validate recursive DTOs instead of degrading them to `unknown`.
14
+
15
+ Self/mutually-recursive `@ValidateNested` DTOs (e.g. a `ColumnFilter` whose `and`/`or`
16
+ reference `ColumnFilter[]`) used to be degraded to `unknown` with a warning, dropping all
17
+ client-side validation for that field. They are now expanded with a real lazy schema:
18
+
19
+ - **zod / valibot** hoist a structural TS `type` alias and annotate the recursive const
20
+ (`z.ZodType<T>` / `v.GenericSchema<T>`) so the implicit-any self-reference cycle is broken;
21
+ the recursion site uses `z.lazy` / `v.lazy`.
22
+ - **arktype** uses the native `this` keyword for self-recursion. Mutual recursion (A ↔ B)
23
+ cannot be expressed per-schema without a scope, so the back-edge schema still degrades to
24
+ `unknown` with a clear warning — use the zod or valibot adapter for full validation there.
25
+
26
+ The over-deep nesting guard is now reported separately ("nesting too deep") instead of being
27
+ mislabelled as recursion. The raw-zod `defineContract` path is unchanged.
28
+
29
+ ### Patch Changes
30
+
31
+ - ed04cdc: Fix array detection for union types. A property typed `unknown | unknown[]` (or any
32
+ union whose text happens to end in `[]`) was mistakenly treated as an array and wrapped
33
+ in `z.array(...)`. Array detection now uses the AST (`ArrayTypeNode`) instead of a
34
+ `.endsWith('[]')` text check, so only genuine `T[]` properties become arrays.
35
+
3
36
  ## 0.3.0
4
37
 
5
38
  ### Minor Changes
package/dist/cli/main.cjs CHANGED
@@ -1285,6 +1285,8 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1285
1285
  }
1286
1286
  const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1287
1287
  const irNamed = /* @__PURE__ */ new Map();
1288
+ const irTypeAliases = /* @__PURE__ */ new Map();
1289
+ const irAnnotations = /* @__PURE__ */ new Map();
1288
1290
  const decls = [];
1289
1291
  const mapEntries = [];
1290
1292
  let used = false;
@@ -1292,6 +1294,8 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1292
1294
  if (src.schema) {
1293
1295
  const r = adapter.renderModule(src.schema);
1294
1296
  for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1297
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
1298
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
1295
1299
  return { text: r.schemaText };
1296
1300
  }
1297
1301
  if (src.zodText) {
@@ -1365,7 +1369,13 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1365
1369
  for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1366
1370
  if (allNested.size > 0) {
1367
1371
  lines.push("// Hoisted nested schemas (shared across endpoints).");
1368
- for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
1372
+ for (const [n, alias] of irTypeAliases) {
1373
+ if (allNested.has(n)) lines.push(`${alias};`);
1374
+ }
1375
+ for (const [n, t] of allNested) {
1376
+ const annotation = irAnnotations.get(n);
1377
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
1378
+ }
1369
1379
  lines.push("");
1370
1380
  }
1371
1381
  lines.push(...decls);
@@ -1790,10 +1800,19 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1790
1800
  }
1791
1801
  return null;
1792
1802
  }
1803
+ var _findTypeCache = /* @__PURE__ */ new WeakMap();
1793
1804
  function findType(name, sourceFile, project) {
1805
+ let byKey = _findTypeCache.get(project);
1806
+ if (byKey === void 0) {
1807
+ byKey = /* @__PURE__ */ new Map();
1808
+ _findTypeCache.set(project, byKey);
1809
+ }
1810
+ const key = `${sourceFile.getFilePath()}\0${name}`;
1811
+ if (byKey.has(key)) return byKey.get(key) ?? null;
1794
1812
  const local = findTypeInFile(name, sourceFile);
1795
- if (local) return local;
1796
- return resolveImportedType(name, sourceFile, project);
1813
+ const result = local ?? resolveImportedType(name, sourceFile, project);
1814
+ byKey.set(key, result);
1815
+ return result;
1797
1816
  }
1798
1817
  var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
1799
1818
  function _localDeclForKinds(name, file, kinds) {
@@ -1830,6 +1849,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1830
1849
  if (_NON_REF_NAMES.has(refName)) return null;
1831
1850
  name = refName;
1832
1851
  }
1852
+ return _resolveNamedRef(name, sourceFile, project, opts);
1853
+ }
1854
+ var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
1855
+ function _resolveNamedRef(name, sourceFile, project, opts) {
1856
+ let byKey = _resolveNamedRefCache.get(project);
1857
+ if (byKey === void 0) {
1858
+ byKey = /* @__PURE__ */ new Map();
1859
+ _resolveNamedRefCache.set(project, byKey);
1860
+ }
1861
+ const kindsKey = [...opts.kinds].sort().join(",");
1862
+ const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
1863
+ if (byKey.has(key)) {
1864
+ const cached = byKey.get(key) ?? null;
1865
+ return cached ? { ...cached } : null;
1866
+ }
1867
+ const computed = _computeNamedRef(name, sourceFile, project, opts);
1868
+ byKey.set(key, computed);
1869
+ return computed ? { ...computed } : null;
1870
+ }
1871
+ function _computeNamedRef(name, sourceFile, project, opts) {
1833
1872
  if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
1834
1873
  return { name, filePath: sourceFile.getFilePath() };
1835
1874
  }
@@ -1907,10 +1946,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1907
1946
  depth: 0
1908
1947
  };
1909
1948
  const root = buildObject(classDecl, sourceFile, ctx);
1910
- for (const schemaName of ctx.recursiveSchemas) {
1911
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
1912
- }
1913
- return { root, named: ctx.named, warnings: ctx.warnings };
1949
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
1914
1950
  }
1915
1951
  function buildObject(classDecl, classFile, ctx) {
1916
1952
  const props = classDecl.getProperties();
@@ -1930,7 +1966,7 @@ function buildProperty(prop, classFile, ctx) {
1930
1966
  const dec = (n) => decorators.get(n);
1931
1967
  const typeNode = prop.getTypeNode();
1932
1968
  const typeText = typeNode?.getText() ?? "unknown";
1933
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1969
+ const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
1934
1970
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1935
1971
  if (has("ValidateNested") || typeRefName) {
1936
1972
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2061,18 +2097,27 @@ function baseFromType(typeText, isArrayType) {
2061
2097
  }
2062
2098
  }
2063
2099
  function buildNestedReference(className, fromFile, ctx) {
2064
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2100
+ if (ctx.visiting.has(className)) {
2065
2101
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2066
2102
  ctx.emittedClasses.set(className, reserved);
2067
2103
  ctx.recursiveSchemas.add(reserved);
2068
2104
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2069
2105
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2070
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2106
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2071
2107
  ctx.warnings.push(msg);
2072
2108
  console.warn(`[nestjs-codegen] ${msg}`);
2073
2109
  }
2074
2110
  return { kind: "lazyRef", name: reserved };
2075
2111
  }
2112
+ if (ctx.depth >= 8) {
2113
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2114
+ ctx.warnedDecorators.add(`deep:${className}`);
2115
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2116
+ ctx.warnings.push(msg);
2117
+ console.warn(`[nestjs-codegen] ${msg}`);
2118
+ }
2119
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2120
+ }
2076
2121
  const existing = ctx.emittedClasses.get(className);
2077
2122
  if (existing) return { kind: "ref", name: existing };
2078
2123
  const schemaName = aliasFor(className, ctx);
@@ -2198,17 +2243,31 @@ var import_ts_morph6 = require("ts-morph");
2198
2243
  var import_ts_morph5 = require("ts-morph");
2199
2244
 
2200
2245
  // src/discovery/enum-resolution.ts
2246
+ var _enumCache = /* @__PURE__ */ new WeakMap();
2201
2247
  function resolveEnumValues(name, sourceFile, project) {
2248
+ let byKey = _enumCache.get(project);
2249
+ if (byKey === void 0) {
2250
+ byKey = /* @__PURE__ */ new Map();
2251
+ _enumCache.set(project, byKey);
2252
+ }
2253
+ const key = `${sourceFile.getFilePath()}\0${name}`;
2254
+ if (byKey.has(key)) {
2255
+ const cached = byKey.get(key) ?? null;
2256
+ return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
2257
+ }
2202
2258
  const resolved = findType(name, sourceFile, project);
2203
- if (!resolved || resolved.kind !== "enum") return null;
2204
- let numeric = true;
2205
- const values = resolved.members.map((m) => {
2206
- const parsed = JSON.parse(m);
2207
- if (typeof parsed === "string") numeric = false;
2208
- return String(parsed);
2209
- });
2210
- if (values.length === 0) return null;
2211
- return { values, numeric };
2259
+ let result = null;
2260
+ if (resolved && resolved.kind === "enum") {
2261
+ let numeric = true;
2262
+ const values = resolved.members.map((m) => {
2263
+ const parsed = JSON.parse(m);
2264
+ if (typeof parsed === "string") numeric = false;
2265
+ return String(parsed);
2266
+ });
2267
+ if (values.length > 0) result = { values, numeric };
2268
+ }
2269
+ byKey.set(key, result);
2270
+ return result ? { values: [...result.values], numeric: result.numeric } : null;
2212
2271
  }
2213
2272
 
2214
2273
  // src/discovery/filter-field-types.ts
@@ -3478,7 +3537,7 @@ async function watch(config, onChange) {
3478
3537
  }
3479
3538
 
3480
3539
  // src/index.ts
3481
- var VERSION = "0.3.0";
3540
+ var VERSION = "0.4.1";
3482
3541
 
3483
3542
  // src/cli/codegen.ts
3484
3543
  async function runCodegen(opts = {}) {