@supalive/codegen 1.1.0 → 1.2.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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as generateToDisk, r as readEntryConfig } from "./src-BD-RIuCu.js";
2
+ import { n as generateToDisk, r as readEntryConfig } from "./src-DIj2CWMc.js";
3
3
  import path from "node:path";
4
4
  //#region src/cli.ts
5
5
  function parseArgs(argv) {
package/dist/index.d.ts CHANGED
@@ -62,10 +62,14 @@ interface FieldIR {
62
62
  optional: boolean;
63
63
  /** May be explicitly null (Zod `.nullable()` / TS `| null`). */
64
64
  nullable: boolean;
65
+ /** Doc comment copied from the TS source (JSDoc or Zod `.describe()`). */
66
+ doc?: string;
65
67
  }
66
68
  interface ModelIR {
67
69
  name: string;
68
70
  fields: FieldIR[];
71
+ /** Doc comment copied from the TS type declaration, when it has one. */
72
+ doc?: string;
69
73
  }
70
74
  interface EnumValueIR {
71
75
  dartName: string;
@@ -86,6 +90,8 @@ interface ProcedureIR {
86
90
  result: DartType;
87
91
  /** Whether the result may be null (TS `T | null`). */
88
92
  resultNullable: boolean;
93
+ /** Doc comment copied from the procedure's TS declaration (JSDoc). */
94
+ doc?: string;
89
95
  }
90
96
  interface ClientIR {
91
97
  clientClassName: string;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as emit, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, t as generate } from "./src-BD-RIuCu.js";
1
+ import { a as emit, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, t as generate } from "./src-DIj2CWMc.js";
2
2
  export { emit, extractClient, generate, generateToDisk, readEntryConfig, resolveConfig };
@@ -2,6 +2,49 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { Node, Project, ts } from "ts-morph";
4
4
  //#region src/zod-schema.ts
5
+ /**
6
+ * Map each procedure to the doc comment on its declaration, so the generated
7
+ * client carries the same descriptions the backend documents. Reads the JSDoc
8
+ * on the procedures-map entry, or (for a shorthand like `{ staffLogin }`)
9
+ * follows the identifier to its `export const staffLogin = query({...})` and
10
+ * reads that statement's JSDoc. Best-effort: any failure yields no doc.
11
+ */
12
+ function scanProcedureDocs(routerDecl) {
13
+ const result = /* @__PURE__ */ new Map();
14
+ try {
15
+ const proceduresObj = getProceduresObjectLiteral(routerDecl);
16
+ if (!proceduresObj) return result;
17
+ for (const prop of proceduresObj.getProperties()) {
18
+ const key = getPropertyKey(prop);
19
+ if (!key) continue;
20
+ const doc = procedureDocText(prop);
21
+ if (doc) result.set(key, doc);
22
+ }
23
+ } catch {}
24
+ return result;
25
+ }
26
+ /** JSDoc on a procedures-map entry, or on the const it references. */
27
+ function procedureDocText(prop) {
28
+ const own = jsDocText(prop);
29
+ if (own) return own;
30
+ let idNode;
31
+ if (Node.isShorthandPropertyAssignment(prop)) idNode = prop.getNameNode();
32
+ else if (Node.isPropertyAssignment(prop)) idNode = prop.getInitializer();
33
+ if (idNode && Node.isIdentifier(idNode)) {
34
+ for (const def of idNode.getDefinitionNodes()) if (Node.isVariableDeclaration(def)) {
35
+ const stmt = def.getVariableStatement();
36
+ const doc = stmt ? jsDocText(stmt) : void 0;
37
+ if (doc) return doc;
38
+ }
39
+ }
40
+ }
41
+ /** The trimmed description of a node's last JSDoc block, if any. */
42
+ function jsDocText(node) {
43
+ const docs = Node.isJSDocable(node) ? node.getJsDocs() : [];
44
+ if (docs.length === 0) return void 0;
45
+ const desc = docs[docs.length - 1].getDescription().trim();
46
+ return desc.length > 0 ? desc : void 0;
47
+ }
5
48
  function scanZodArgMeta(routerDecl) {
6
49
  const result = /* @__PURE__ */ new Map();
7
50
  try {
@@ -76,7 +119,9 @@ function scanObjectSchema(schema) {
76
119
  if (methods.has("bigint")) meta.numeric = "bigint";
77
120
  else if (methods.has("int")) meta.numeric = "int";
78
121
  if (methods.has("default") || methods.has("catch") || methods.has("prefault")) meta.optional = true;
79
- if (meta.numeric || meta.optional) map.set(key, meta);
122
+ const description = topLevelZodDescription(init);
123
+ if (description) meta.description = description;
124
+ if (meta.numeric || meta.optional || meta.description) map.set(key, meta);
80
125
  }
81
126
  return map;
82
127
  }
@@ -97,6 +142,34 @@ function topLevelZodMethods(node) {
97
142
  }
98
143
  return methods;
99
144
  }
145
+ /**
146
+ * The description string from a field's top-level `.describe("...")` or
147
+ * `.meta({ description: "..." })`, walking the method chain (outermost first,
148
+ * so the outermost description wins).
149
+ */
150
+ function topLevelZodDescription(node) {
151
+ let cur = node;
152
+ while (cur && Node.isCallExpression(cur)) {
153
+ const expr = cur.getExpression();
154
+ if (!Node.isPropertyAccessExpression(expr)) break;
155
+ const method = expr.getName();
156
+ if (method === "describe") {
157
+ const arg = cur.getArguments()[0];
158
+ if (arg && Node.isStringLiteral(arg)) return arg.getLiteralValue();
159
+ }
160
+ if (method === "meta") {
161
+ const arg = cur.getArguments()[0];
162
+ if (arg && Node.isObjectLiteralExpression(arg)) {
163
+ const d = arg.getProperty("description");
164
+ if (d && Node.isPropertyAssignment(d)) {
165
+ const init = d.getInitializer();
166
+ if (init && Node.isStringLiteral(init)) return init.getLiteralValue();
167
+ }
168
+ }
169
+ }
170
+ cur = expr.getExpression();
171
+ }
172
+ }
100
173
  function findZodObjectLiteral(node) {
101
174
  let cur = node;
102
175
  while (cur && Node.isCallExpression(cur)) {
@@ -115,20 +188,26 @@ function findZodObjectLiteral(node) {
115
188
  //#region src/extract.ts
116
189
  /** Load the router from the entry file and extract a ClientIR. */
117
190
  function extractClient(config) {
118
- const exported = new Project({
191
+ const project = new Project({
119
192
  tsConfigFilePath: config.tsconfig,
120
193
  skipAddingFilesFromTsConfig: config.tsconfig ? false : true,
121
194
  compilerOptions: config.tsconfig ? void 0 : {
122
195
  strict: true,
123
196
  target: ts.ScriptTarget.ES2022
124
197
  }
125
- }).addSourceFileAtPath(path.resolve(config.entry)).getExportedDeclarations().get(config.router);
198
+ });
199
+ const entry = project.addSourceFileAtPath(path.resolve(config.entry));
200
+ const opts = project.getCompilerOptions();
201
+ if (!(opts.strictNullChecks ?? opts.strict ?? false)) console.warn("[supalive-codegen] strictNullChecks is disabled" + (config.tsconfig ? ` in ${config.tsconfig}` : "") + ". Nullable/optional fields (`T | null`, `field?`) will be generated as non-nullable, because TypeScript erases those unions when strictNullChecks is off. Set `strict: true` (or `strictNullChecks: true`) in your tsconfig for correct output.");
202
+ const exported = entry.getExportedDeclarations().get(config.router);
126
203
  if (!exported || exported.length === 0) throw new Error(`Could not find an export named "${config.router}" in ${config.entry}. Set { router: "<name>" } in your config or pass --router.`);
127
204
  const routerDecl = exported[0];
128
205
  const proceduresSym = routerDecl.getType().getProperty("procedures");
129
206
  if (!proceduresSym) throw new Error(`Export "${config.router}" does not look like a Supalive router (no "procedures" property).`);
130
207
  const proceduresType = proceduresSym.getTypeAtLocation(routerDecl);
131
- const extractor = new Extractor(routerDecl, config, scanZodArgMeta(routerDecl));
208
+ const argMeta = scanZodArgMeta(routerDecl);
209
+ const procDocs = scanProcedureDocs(routerDecl);
210
+ const extractor = new Extractor(routerDecl, config, argMeta, project.getTypeChecker());
132
211
  const procedures = [];
133
212
  for (const procSym of proceduresType.getProperties()) {
134
213
  const name = procSym.getName();
@@ -136,6 +215,8 @@ function extractClient(config) {
136
215
  const ir = extractor.procedure(name, procType);
137
216
  if (!ir) continue;
138
217
  if (ir.internal && !config.includeInternal) continue;
218
+ const doc = procDocs.get(name);
219
+ if (doc) ir.doc = doc;
139
220
  procedures.push(ir);
140
221
  }
141
222
  procedures.sort((a, b) => a.name.localeCompare(b.name));
@@ -150,16 +231,28 @@ var Extractor = class {
150
231
  locationNode;
151
232
  config;
152
233
  argMeta;
234
+ tc;
153
235
  modelRegistry = /* @__PURE__ */ new Map();
154
236
  enumRegistry = /* @__PURE__ */ new Map();
155
237
  /** Value-set signature → already-registered enum name, for structural dedup. */
156
238
  enumBySignature = /* @__PURE__ */ new Map();
157
239
  nameCounts = /* @__PURE__ */ new Map();
158
240
  visiting = /* @__PURE__ */ new Set();
159
- constructor(locationNode, config, argMeta) {
241
+ constructor(locationNode, config, argMeta, tc) {
160
242
  this.locationNode = locationNode;
161
243
  this.config = config;
162
244
  this.argMeta = argMeta;
245
+ this.tc = tc;
246
+ }
247
+ /** Doc comment attached to a symbol's declaration (resolves TS aliases). */
248
+ symbolDoc(sym) {
249
+ try {
250
+ const parts = sym.compilerSymbol.getDocumentationComment(this.tc.compilerObject);
251
+ const text = ts.displayPartsToString(parts).trim();
252
+ return text.length > 0 ? text : void 0;
253
+ } catch {
254
+ return;
255
+ }
163
256
  }
164
257
  models() {
165
258
  return [...this.modelRegistry.values()];
@@ -186,6 +279,7 @@ var Extractor = class {
186
279
  if (!meta) continue;
187
280
  if (meta.numeric && (f.type.kind === "double" || f.type.kind === "int")) f.type = { kind: meta.numeric };
188
281
  if (meta.optional) f.optional = true;
282
+ if (meta.description && !f.doc) f.doc = meta.description;
189
283
  }
190
284
  }
191
285
  const ret = unwrapPromise(sig.getReturnType());
@@ -285,13 +379,17 @@ var Extractor = class {
285
379
  jsonKey,
286
380
  type: dartType,
287
381
  optional,
288
- nullable
382
+ nullable,
383
+ doc: this.symbolDoc(prop)
289
384
  });
290
385
  }
291
386
  this.visiting.delete(name);
387
+ const modelSym = type.getAliasSymbol() ?? type.getSymbol();
388
+ const doc = modelSym ? this.symbolDoc(modelSym) : void 0;
292
389
  this.modelRegistry.set(name, {
293
390
  name,
294
- fields
391
+ fields,
392
+ doc
295
393
  });
296
394
  return {
297
395
  kind: "model",
@@ -457,10 +555,53 @@ function singular(hint) {
457
555
  if (hint.endsWith("s") && !hint.endsWith("ss")) return hint.slice(0, -1);
458
556
  return hint + "Item";
459
557
  }
558
+ /**
559
+ * Dart reserved words — none may be used as a bare identifier (e.g. an enum
560
+ * constant), so a wire value like `void` must be escaped.
561
+ */
562
+ const DART_RESERVED = /* @__PURE__ */ new Set([
563
+ "assert",
564
+ "break",
565
+ "case",
566
+ "catch",
567
+ "class",
568
+ "const",
569
+ "continue",
570
+ "default",
571
+ "do",
572
+ "else",
573
+ "enum",
574
+ "extends",
575
+ "false",
576
+ "final",
577
+ "finally",
578
+ "for",
579
+ "if",
580
+ "in",
581
+ "is",
582
+ "new",
583
+ "null",
584
+ "rethrow",
585
+ "return",
586
+ "super",
587
+ "switch",
588
+ "this",
589
+ "throw",
590
+ "true",
591
+ "try",
592
+ "var",
593
+ "void",
594
+ "while",
595
+ "with"
596
+ ]);
597
+ /** Suffix a trailing `_` when [name] is a Dart reserved word. */
598
+ function escapeDartId(name) {
599
+ return DART_RESERVED.has(name) ? `${name}_` : name;
600
+ }
460
601
  function enumValueName(wire) {
461
602
  const c = camel(wire);
462
603
  if (!c || /^[0-9]/.test(c)) return "v" + pascal(wire);
463
- return c;
604
+ return escapeDartId(c);
464
605
  }
465
606
  //#endregion
466
607
  //#region src/emit.ts
@@ -474,6 +615,11 @@ function emit(ir) {
474
615
  contents: emitClient(ir)
475
616
  }];
476
617
  }
618
+ /** Render source doc text as Dart `///` lines at [indent], or "" when absent. */
619
+ function docComment(text, indent) {
620
+ if (!text) return "";
621
+ return text.replace(/\r\n?/g, "\n").split("\n").map((line) => `${indent}///${line.length ? ` ${line}` : ""}`).join("\n") + "\n";
622
+ }
477
623
  /** Dart type name for a non-null [DartType]. */
478
624
  function typeName(t) {
479
625
  switch (t.kind) {
@@ -601,7 +747,7 @@ function emitModel(m) {
601
747
  if (f.optional && f.nullable) return ` this.${f.name} = const Optional.absent()`;
602
748
  return !(f.optional || f.nullable) ? ` required this.${f.name}` : ` this.${f.name}`;
603
749
  }).join(",\n");
604
- const fields = m.fields.map((f) => ` final ${fieldType(f)} ${f.name};`).join("\n");
750
+ const fields = m.fields.map((f) => `${docComment(f.doc, " ")} final ${fieldType(f)} ${f.name};`).join("\n");
605
751
  const fromJson = m.fields.map((f) => emitFromJsonField(f)).join("\n");
606
752
  const toJson = m.fields.map((f) => emitToJsonField(f)).join("\n");
607
753
  const copyParams = m.fields.map((f) => ` Object? ${f.name} = _undefined`).join(",\n");
@@ -621,7 +767,7 @@ function emitModel(m) {
621
767
  }).join(" &&\n ");
622
768
  const hashParts = m.fields.map((f) => isDeep(f) ? `deepHashCode(${f.name})` : f.name).join(", ");
623
769
  const hashBody = m.fields.length === 0 ? "runtimeType.hashCode" : m.fields.length <= 19 ? `Object.hash(runtimeType, ${hashParts})` : `Object.hashAll([runtimeType, ${hashParts}])`;
624
- return `class ${m.name} implements SupaliveModel {
770
+ return `${docComment(m.doc, "")}class ${m.name} implements SupaliveModel {
625
771
  const ${m.name}(${m.fields.length ? "{\n" + ctorParams + ",\n }" : ""});
626
772
 
627
773
  ${fields}
@@ -725,7 +871,7 @@ function emitEndpoint(p) {
725
871
  const endpointClass = p.kind === "query" ? "QueryEndpoint" : p.kind === "mutation" ? "MutationEndpoint" : "ActionEndpoint";
726
872
  const args = argsTypeName(p);
727
873
  const result = resultTypeName(p);
728
- return ` late final ${p.name} = ${endpointClass}<${args}, ${result}>(
874
+ return `${docComment(p.doc, " ")} late final ${p.name} = ${endpointClass}<${args}, ${result}>(
729
875
  _client,
730
876
  '${p.name}',
731
877
  ${encodeArgs(p)},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supalive/codegen",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Generate type-safe client code (Dart, and more) from a Supalive TypeScript router.",
5
5
  "author": "Rebaz Raouf",
6
6
  "license": "MIT",
@@ -41,7 +41,8 @@
41
41
  "scripts": {
42
42
  "build": "tsdown",
43
43
  "dev": "node --import tsx ./src/cli.ts",
44
- "typecheck": "tsc --noEmit"
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest --run"
45
46
  },
46
47
  "dependencies": {
47
48
  "ts-morph": "^24.0.0"