@dudousxd/nestjs-codegen 0.3.0 → 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.
@@ -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);
@@ -1928,10 +1939,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1928
1939
  depth: 0
1929
1940
  };
1930
1941
  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 };
1942
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
1935
1943
  }
1936
1944
  function buildObject(classDecl, classFile, ctx) {
1937
1945
  const props = classDecl.getProperties();
@@ -1951,7 +1959,7 @@ function buildProperty(prop, classFile, ctx) {
1951
1959
  const dec = (n) => decorators.get(n);
1952
1960
  const typeNode = prop.getTypeNode();
1953
1961
  const typeText = typeNode?.getText() ?? "unknown";
1954
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1962
+ const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
1955
1963
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1956
1964
  if (has("ValidateNested") || typeRefName) {
1957
1965
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2082,18 +2090,27 @@ function baseFromType(typeText, isArrayType) {
2082
2090
  }
2083
2091
  }
2084
2092
  function buildNestedReference(className, fromFile, ctx) {
2085
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2093
+ if (ctx.visiting.has(className)) {
2086
2094
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2087
2095
  ctx.emittedClasses.set(className, reserved);
2088
2096
  ctx.recursiveSchemas.add(reserved);
2089
2097
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2090
2098
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2091
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2099
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2092
2100
  ctx.warnings.push(msg);
2093
2101
  console.warn(`[nestjs-codegen] ${msg}`);
2094
2102
  }
2095
2103
  return { kind: "lazyRef", name: reserved };
2096
2104
  }
2105
+ if (ctx.depth >= 8) {
2106
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2107
+ ctx.warnedDecorators.add(`deep:${className}`);
2108
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2109
+ ctx.warnings.push(msg);
2110
+ console.warn(`[nestjs-codegen] ${msg}`);
2111
+ }
2112
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2113
+ }
2097
2114
  const existing = ctx.emittedClasses.get(className);
2098
2115
  if (existing) return { kind: "ref", name: existing };
2099
2116
  const schemaName = aliasFor(className, ctx);
@@ -3498,8 +3515,57 @@ async function watch(config, onChange) {
3498
3515
  };
3499
3516
  }
3500
3517
 
3518
+ // src/ir/render-ts-type.ts
3519
+ function tsKey(name) {
3520
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
3521
+ }
3522
+ function renderTsType(node, ctx) {
3523
+ switch (node.kind) {
3524
+ case "string":
3525
+ return "string";
3526
+ case "number":
3527
+ return "number";
3528
+ case "boolean":
3529
+ return "boolean";
3530
+ case "date":
3531
+ return "Date";
3532
+ case "unknown":
3533
+ return "unknown";
3534
+ case "instanceof":
3535
+ return node.ctor;
3536
+ case "enum":
3537
+ return node.literals.join(" | ");
3538
+ case "literal":
3539
+ return node.raw;
3540
+ case "union":
3541
+ return node.options.map((o) => renderTsType(o, ctx)).join(" | ");
3542
+ case "array":
3543
+ return `Array<${renderTsType(node.element, ctx)}>`;
3544
+ case "optional":
3545
+ return `${renderTsType(node.inner, ctx)} | undefined`;
3546
+ case "annotated":
3547
+ return renderTsType(node.inner, ctx);
3548
+ case "object": {
3549
+ if (node.fields.length === 0) return node.passthrough ? "Record<string, unknown>" : "{}";
3550
+ const inner = node.fields.map((f) => {
3551
+ if (f.value.kind === "optional") {
3552
+ return `${tsKey(f.key)}?: ${renderTsType(f.value.inner, ctx)}`;
3553
+ }
3554
+ return `${tsKey(f.key)}: ${renderTsType(f.value, ctx)}`;
3555
+ }).join("; ");
3556
+ return `{ ${inner} }`;
3557
+ }
3558
+ case "ref":
3559
+ case "lazyRef": {
3560
+ if (ctx.recursive.has(node.name)) return ctx.typeNameFor(node.name);
3561
+ const target = ctx.named.get(node.name);
3562
+ return target ? renderTsType(target, ctx) : "unknown";
3563
+ }
3564
+ }
3565
+ }
3566
+
3501
3567
  // src/index.ts
3502
- var VERSION = "0.3.0";
3568
+ var VERSION = "0.4.0";
3503
3569
  // Annotate the CommonJS export names for ESM import in node:
3504
3570
  0 && (module.exports = {
3505
3571
  CodegenError,
@@ -3514,6 +3580,7 @@ var VERSION = "0.3.0";
3514
3580
  extractSchemaFromDto,
3515
3581
  generate,
3516
3582
  loadConfig,
3583
+ renderTsType,
3517
3584
  resolveAdapter,
3518
3585
  resolveConfig,
3519
3586
  watch