@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.
@@ -1,2 +1,2 @@
1
- export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, C as CodegenExtension, o as EmittedFile, E as ExtensionContext, L as LeafModel, p as RequestModel, q as RequestShape, s as defineExtension, t as requestShape } from '../index-oH5t7x4G.cjs';
1
+ export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, C as CodegenExtension, o as EmittedFile, E as ExtensionContext, L as LeafModel, p as RequestModel, q as RequestShape, s as defineExtension, t as requestShape } from '../index-DA4uySjo.cjs';
2
2
  import 'ts-morph';
@@ -1,2 +1,2 @@
1
- export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, C as CodegenExtension, o as EmittedFile, E as ExtensionContext, L as LeafModel, p as RequestModel, q as RequestShape, s as defineExtension, t as requestShape } from '../index-oH5t7x4G.js';
1
+ export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, C as CodegenExtension, o as EmittedFile, E as ExtensionContext, L as LeafModel, p as RequestModel, q as RequestShape, s as defineExtension, t as requestShape } from '../index-DA4uySjo.js';
2
2
  import 'ts-morph';
@@ -111,6 +111,14 @@ interface SchemaModule {
111
111
  root: SchemaNode;
112
112
  named: Map<string, SchemaNode>;
113
113
  warnings: string[];
114
+ /**
115
+ * Names (keys of {@link named}) that are genuinely self/mutually recursive,
116
+ * i.e. reachable from themselves through a `lazyRef` back-edge. Adapters use
117
+ * this to break the TypeScript inference cycle (annotated const + hoisted
118
+ * structural type for zod/valibot; `this`/degrade for arktype). Absent or
119
+ * empty means no recursion.
120
+ */
121
+ recursive?: Set<string>;
114
122
  }
115
123
 
116
124
  /** Signals to an adapter what it must import to render the current output. */
@@ -121,12 +129,32 @@ interface AdapterUsage {
121
129
  interface RenderContext {
122
130
  /** Hoisted named schemas being emitted alongside the root. */
123
131
  named: Map<string, SchemaNode>;
132
+ /**
133
+ * Name of the hoisted schema currently being rendered, when rendering a named
134
+ * schema's own body. Lets an adapter express a self-reference specially (e.g.
135
+ * arktype's `this` keyword). Absent when rendering the root.
136
+ */
137
+ selfName?: string;
124
138
  }
125
139
  interface RenderedModule {
126
140
  /** Root schema source text, e.g. `"z.object({ email: z.string().email() })"`. */
127
141
  schemaText: string;
128
142
  /** name → schema source text, hoisted above the parent. */
129
143
  namedNestedSchemas: Map<string, string>;
144
+ /**
145
+ * name → hoisted TS `type` alias *body* for a recursive schema (e.g.
146
+ * `"{ field?: string; and?: Array<ColumnFilter> }"`), emitted as
147
+ * `type <TypeName> = <body>;` above the const. The alias name is derived from
148
+ * the schema name. Absent for adapters/schemas that don't need it (arktype,
149
+ * non-recursive schemas).
150
+ */
151
+ namedTypeAliases?: Map<string, string>;
152
+ /**
153
+ * name → const type annotation for a recursive schema (e.g.
154
+ * `"z.ZodType<ColumnFilter>"`), emitted as `const <name>: <annotation> = ...`
155
+ * to break the implicit-any self-reference cycle.
156
+ */
157
+ namedAnnotations?: Map<string, string>;
130
158
  warnings: string[];
131
159
  }
132
160
  /**
@@ -521,4 +549,4 @@ declare function requestShape(route: RouteDescriptor): RequestShape;
521
549
  /** Identity helper for authoring extensions with full type inference. */
522
550
  declare function defineExtension(ext: CodegenExtension): CodegenExtension;
523
551
 
524
- export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaModule as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type ResolvedFormsConfig as b, type ContractDescriptor as c, type ContractSource as d, type ControllerRef as e, type RenderContext as f, type RenderedModule as g, type SchemaNode as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type EmittedFile as o, type RequestModel as p, type RequestShape as q, resolveAdapter as r, defineExtension as s, requestShape as t };
552
+ export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaNode as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type SchemaModule as b, type ResolvedFormsConfig as c, type ContractDescriptor as d, type ContractSource as e, type ControllerRef as f, type RenderContext as g, type RenderedModule as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type EmittedFile as o, type RequestModel as p, type RequestShape as q, resolveAdapter as r, defineExtension as s, requestShape as t };
@@ -111,6 +111,14 @@ interface SchemaModule {
111
111
  root: SchemaNode;
112
112
  named: Map<string, SchemaNode>;
113
113
  warnings: string[];
114
+ /**
115
+ * Names (keys of {@link named}) that are genuinely self/mutually recursive,
116
+ * i.e. reachable from themselves through a `lazyRef` back-edge. Adapters use
117
+ * this to break the TypeScript inference cycle (annotated const + hoisted
118
+ * structural type for zod/valibot; `this`/degrade for arktype). Absent or
119
+ * empty means no recursion.
120
+ */
121
+ recursive?: Set<string>;
114
122
  }
115
123
 
116
124
  /** Signals to an adapter what it must import to render the current output. */
@@ -121,12 +129,32 @@ interface AdapterUsage {
121
129
  interface RenderContext {
122
130
  /** Hoisted named schemas being emitted alongside the root. */
123
131
  named: Map<string, SchemaNode>;
132
+ /**
133
+ * Name of the hoisted schema currently being rendered, when rendering a named
134
+ * schema's own body. Lets an adapter express a self-reference specially (e.g.
135
+ * arktype's `this` keyword). Absent when rendering the root.
136
+ */
137
+ selfName?: string;
124
138
  }
125
139
  interface RenderedModule {
126
140
  /** Root schema source text, e.g. `"z.object({ email: z.string().email() })"`. */
127
141
  schemaText: string;
128
142
  /** name → schema source text, hoisted above the parent. */
129
143
  namedNestedSchemas: Map<string, string>;
144
+ /**
145
+ * name → hoisted TS `type` alias *body* for a recursive schema (e.g.
146
+ * `"{ field?: string; and?: Array<ColumnFilter> }"`), emitted as
147
+ * `type <TypeName> = <body>;` above the const. The alias name is derived from
148
+ * the schema name. Absent for adapters/schemas that don't need it (arktype,
149
+ * non-recursive schemas).
150
+ */
151
+ namedTypeAliases?: Map<string, string>;
152
+ /**
153
+ * name → const type annotation for a recursive schema (e.g.
154
+ * `"z.ZodType<ColumnFilter>"`), emitted as `const <name>: <annotation> = ...`
155
+ * to break the implicit-any self-reference cycle.
156
+ */
157
+ namedAnnotations?: Map<string, string>;
130
158
  warnings: string[];
131
159
  }
132
160
  /**
@@ -521,4 +549,4 @@ declare function requestShape(route: RouteDescriptor): RequestShape;
521
549
  /** Identity helper for authoring extensions with full type inference. */
522
550
  declare function defineExtension(ext: CodegenExtension): CodegenExtension;
523
551
 
524
- export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaModule as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type ResolvedFormsConfig as b, type ContractDescriptor as c, type ContractSource as d, type ControllerRef as e, type RenderContext as f, type RenderedModule as g, type SchemaNode as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type EmittedFile as o, type RequestModel as p, type RequestShape as q, resolveAdapter as r, defineExtension as s, requestShape as t };
552
+ export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaNode as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type SchemaModule as b, type ResolvedFormsConfig as c, type ContractDescriptor as d, type ContractSource as e, type ControllerRef as f, type RenderContext as g, type RenderedModule as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type EmittedFile as o, type RequestModel as p, type RequestShape as q, resolveAdapter as r, defineExtension as s, requestShape as t };
package/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ __export(src_exports, {
42
42
  extractSchemaFromDto: () => extractSchemaFromDto,
43
43
  generate: () => generate,
44
44
  loadConfig: () => loadConfig,
45
+ renderTsType: () => renderTsType,
45
46
  resolveAdapter: () => resolveAdapter,
46
47
  resolveConfig: () => resolveConfig,
47
48
  watch: () => watch
@@ -1306,6 +1307,8 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1306
1307
  }
1307
1308
  const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1308
1309
  const irNamed = /* @__PURE__ */ new Map();
1310
+ const irTypeAliases = /* @__PURE__ */ new Map();
1311
+ const irAnnotations = /* @__PURE__ */ new Map();
1309
1312
  const decls = [];
1310
1313
  const mapEntries = [];
1311
1314
  let used = false;
@@ -1313,6 +1316,8 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1313
1316
  if (src.schema) {
1314
1317
  const r = adapter.renderModule(src.schema);
1315
1318
  for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1319
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
1320
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
1316
1321
  return { text: r.schemaText };
1317
1322
  }
1318
1323
  if (src.zodText) {
@@ -1386,7 +1391,13 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1386
1391
  for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1387
1392
  if (allNested.size > 0) {
1388
1393
  lines.push("// Hoisted nested schemas (shared across endpoints).");
1389
- for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
1394
+ for (const [n, alias] of irTypeAliases) {
1395
+ if (allNested.has(n)) lines.push(`${alias};`);
1396
+ }
1397
+ for (const [n, t] of allNested) {
1398
+ const annotation = irAnnotations.get(n);
1399
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
1400
+ }
1390
1401
  lines.push("");
1391
1402
  }
1392
1403
  lines.push(...decls);
@@ -1811,10 +1822,19 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1811
1822
  }
1812
1823
  return null;
1813
1824
  }
1825
+ var _findTypeCache = /* @__PURE__ */ new WeakMap();
1814
1826
  function findType(name, sourceFile, project) {
1827
+ let byKey = _findTypeCache.get(project);
1828
+ if (byKey === void 0) {
1829
+ byKey = /* @__PURE__ */ new Map();
1830
+ _findTypeCache.set(project, byKey);
1831
+ }
1832
+ const key = `${sourceFile.getFilePath()}\0${name}`;
1833
+ if (byKey.has(key)) return byKey.get(key) ?? null;
1815
1834
  const local = findTypeInFile(name, sourceFile);
1816
- if (local) return local;
1817
- return resolveImportedType(name, sourceFile, project);
1835
+ const result = local ?? resolveImportedType(name, sourceFile, project);
1836
+ byKey.set(key, result);
1837
+ return result;
1818
1838
  }
1819
1839
  var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
1820
1840
  function _localDeclForKinds(name, file, kinds) {
@@ -1851,6 +1871,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1851
1871
  if (_NON_REF_NAMES.has(refName)) return null;
1852
1872
  name = refName;
1853
1873
  }
1874
+ return _resolveNamedRef(name, sourceFile, project, opts);
1875
+ }
1876
+ var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
1877
+ function _resolveNamedRef(name, sourceFile, project, opts) {
1878
+ let byKey = _resolveNamedRefCache.get(project);
1879
+ if (byKey === void 0) {
1880
+ byKey = /* @__PURE__ */ new Map();
1881
+ _resolveNamedRefCache.set(project, byKey);
1882
+ }
1883
+ const kindsKey = [...opts.kinds].sort().join(",");
1884
+ const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
1885
+ if (byKey.has(key)) {
1886
+ const cached = byKey.get(key) ?? null;
1887
+ return cached ? { ...cached } : null;
1888
+ }
1889
+ const computed = _computeNamedRef(name, sourceFile, project, opts);
1890
+ byKey.set(key, computed);
1891
+ return computed ? { ...computed } : null;
1892
+ }
1893
+ function _computeNamedRef(name, sourceFile, project, opts) {
1854
1894
  if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
1855
1895
  return { name, filePath: sourceFile.getFilePath() };
1856
1896
  }
@@ -1928,10 +1968,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1928
1968
  depth: 0
1929
1969
  };
1930
1970
  const root = buildObject(classDecl, sourceFile, ctx);
1931
- for (const schemaName of ctx.recursiveSchemas) {
1932
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
1933
- }
1934
- return { root, named: ctx.named, warnings: ctx.warnings };
1971
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
1935
1972
  }
1936
1973
  function buildObject(classDecl, classFile, ctx) {
1937
1974
  const props = classDecl.getProperties();
@@ -1951,7 +1988,7 @@ function buildProperty(prop, classFile, ctx) {
1951
1988
  const dec = (n) => decorators.get(n);
1952
1989
  const typeNode = prop.getTypeNode();
1953
1990
  const typeText = typeNode?.getText() ?? "unknown";
1954
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1991
+ const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
1955
1992
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1956
1993
  if (has("ValidateNested") || typeRefName) {
1957
1994
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2082,18 +2119,27 @@ function baseFromType(typeText, isArrayType) {
2082
2119
  }
2083
2120
  }
2084
2121
  function buildNestedReference(className, fromFile, ctx) {
2085
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2122
+ if (ctx.visiting.has(className)) {
2086
2123
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2087
2124
  ctx.emittedClasses.set(className, reserved);
2088
2125
  ctx.recursiveSchemas.add(reserved);
2089
2126
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2090
2127
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2091
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2128
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2092
2129
  ctx.warnings.push(msg);
2093
2130
  console.warn(`[nestjs-codegen] ${msg}`);
2094
2131
  }
2095
2132
  return { kind: "lazyRef", name: reserved };
2096
2133
  }
2134
+ if (ctx.depth >= 8) {
2135
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2136
+ ctx.warnedDecorators.add(`deep:${className}`);
2137
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2138
+ ctx.warnings.push(msg);
2139
+ console.warn(`[nestjs-codegen] ${msg}`);
2140
+ }
2141
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2142
+ }
2097
2143
  const existing = ctx.emittedClasses.get(className);
2098
2144
  if (existing) return { kind: "ref", name: existing };
2099
2145
  const schemaName = aliasFor(className, ctx);
@@ -2219,17 +2265,31 @@ var import_ts_morph6 = require("ts-morph");
2219
2265
  var import_ts_morph5 = require("ts-morph");
2220
2266
 
2221
2267
  // src/discovery/enum-resolution.ts
2268
+ var _enumCache = /* @__PURE__ */ new WeakMap();
2222
2269
  function resolveEnumValues(name, sourceFile, project) {
2270
+ let byKey = _enumCache.get(project);
2271
+ if (byKey === void 0) {
2272
+ byKey = /* @__PURE__ */ new Map();
2273
+ _enumCache.set(project, byKey);
2274
+ }
2275
+ const key = `${sourceFile.getFilePath()}\0${name}`;
2276
+ if (byKey.has(key)) {
2277
+ const cached = byKey.get(key) ?? null;
2278
+ return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
2279
+ }
2223
2280
  const resolved = findType(name, sourceFile, project);
2224
- if (!resolved || resolved.kind !== "enum") return null;
2225
- let numeric = true;
2226
- const values = resolved.members.map((m) => {
2227
- const parsed = JSON.parse(m);
2228
- if (typeof parsed === "string") numeric = false;
2229
- return String(parsed);
2230
- });
2231
- if (values.length === 0) return null;
2232
- return { values, numeric };
2281
+ let result = null;
2282
+ if (resolved && resolved.kind === "enum") {
2283
+ let numeric = true;
2284
+ const values = resolved.members.map((m) => {
2285
+ const parsed = JSON.parse(m);
2286
+ if (typeof parsed === "string") numeric = false;
2287
+ return String(parsed);
2288
+ });
2289
+ if (values.length > 0) result = { values, numeric };
2290
+ }
2291
+ byKey.set(key, result);
2292
+ return result ? { values: [...result.values], numeric: result.numeric } : null;
2233
2293
  }
2234
2294
 
2235
2295
  // src/discovery/filter-field-types.ts
@@ -3498,8 +3558,57 @@ async function watch(config, onChange) {
3498
3558
  };
3499
3559
  }
3500
3560
 
3561
+ // src/ir/render-ts-type.ts
3562
+ function tsKey(name) {
3563
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
3564
+ }
3565
+ function renderTsType(node, ctx) {
3566
+ switch (node.kind) {
3567
+ case "string":
3568
+ return "string";
3569
+ case "number":
3570
+ return "number";
3571
+ case "boolean":
3572
+ return "boolean";
3573
+ case "date":
3574
+ return "Date";
3575
+ case "unknown":
3576
+ return "unknown";
3577
+ case "instanceof":
3578
+ return node.ctor;
3579
+ case "enum":
3580
+ return node.literals.join(" | ");
3581
+ case "literal":
3582
+ return node.raw;
3583
+ case "union":
3584
+ return node.options.map((o) => renderTsType(o, ctx)).join(" | ");
3585
+ case "array":
3586
+ return `Array<${renderTsType(node.element, ctx)}>`;
3587
+ case "optional":
3588
+ return `${renderTsType(node.inner, ctx)} | undefined`;
3589
+ case "annotated":
3590
+ return renderTsType(node.inner, ctx);
3591
+ case "object": {
3592
+ if (node.fields.length === 0) return node.passthrough ? "Record<string, unknown>" : "{}";
3593
+ const inner = node.fields.map((f) => {
3594
+ if (f.value.kind === "optional") {
3595
+ return `${tsKey(f.key)}?: ${renderTsType(f.value.inner, ctx)}`;
3596
+ }
3597
+ return `${tsKey(f.key)}: ${renderTsType(f.value, ctx)}`;
3598
+ }).join("; ");
3599
+ return `{ ${inner} }`;
3600
+ }
3601
+ case "ref":
3602
+ case "lazyRef": {
3603
+ if (ctx.recursive.has(node.name)) return ctx.typeNameFor(node.name);
3604
+ const target = ctx.named.get(node.name);
3605
+ return target ? renderTsType(target, ctx) : "unknown";
3606
+ }
3607
+ }
3608
+ }
3609
+
3501
3610
  // src/index.ts
3502
- var VERSION = "0.3.0";
3611
+ var VERSION = "0.4.1";
3503
3612
  // Annotate the CommonJS export names for ESM import in node:
3504
3613
  0 && (module.exports = {
3505
3614
  CodegenError,
@@ -3514,6 +3623,7 @@ var VERSION = "0.3.0";
3514
3623
  extractSchemaFromDto,
3515
3624
  generate,
3516
3625
  loadConfig,
3626
+ renderTsType,
3517
3627
  resolveAdapter,
3518
3628
  resolveConfig,
3519
3629
  watch