@supalive/codegen 1.8.1 → 1.9.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/README.md CHANGED
@@ -85,6 +85,154 @@ const { files } = generate({ entry: "src/server/procedures.ts" });
85
85
  `extractClient` (build the intermediate representation) and `emit` (turn the IR
86
86
  into files) are also exported for advanced use.
87
87
 
88
+ ## Return type overrides
89
+
90
+ By default the codegen infers everything from TypeScript types: `number` →
91
+ `double`, `bigint` → `BigInt`, `Date` → `DateTime`, etc. The `returns` property
92
+ on procedure configs lets you override specific fields for the generated client
93
+ without changing your handler's return type.
94
+
95
+ Model naming uses the `.modelName()` Zod extension method — no extra imports
96
+ needed (it patches all Zod types when you import from `@supalive/core/procedure`).
97
+
98
+ ### Default (no `returns`)
99
+
100
+ Everything inferred from the handler's TS return type:
101
+
102
+ ```ts
103
+ const listCards = query({
104
+ args: z.object({ limit: z.number().int().default(20) }),
105
+ handler: async (_ctx, _input): Promise<{ items: { id: string; name: string }[] }> => ({
106
+ items: [],
107
+ }),
108
+ });
109
+ // → ListCardsResult { items: List<ListCardsItem> }
110
+ ```
111
+
112
+ ### `.modelName("Name")` — rename a model
113
+
114
+ Give a generated model a custom name instead of the auto-derived one:
115
+
116
+ ```ts
117
+ const listCards = query({
118
+ args: z.object({ limit: z.number().int().default(20) }),
119
+ returns: z.array(
120
+ z.object({ id: z.string(), name: z.string() }).modelName("CardItem")
121
+ ),
122
+ handler: async (_ctx, _input) => [] as { id: string; name: string }[],
123
+ });
124
+ // → result type: List<CardItem> (not List<ListCardsResultItem>)
125
+ ```
126
+
127
+ `.modelName("Foo")` on any Zod schema tells the codegen to rename the
128
+ model to `Foo`. Combine with field-level overrides like `z.number().int()` to
129
+ also patch the TS-inferred types.
130
+
131
+ ### Partial field overrides (implicit)
132
+
133
+ Only the fields you list in the `returns` schema are overridden; the rest stay
134
+ inferred from TS. No `partial()` wrapper needed:
135
+
136
+ ```ts
137
+ const storeCards = query({
138
+ args: z.object({}),
139
+ returns: z.object({
140
+ total: z.number().int(), // override: number → int
141
+ // 'cards' is NOT listed → stays inferred from TS
142
+ }),
143
+ handler: async (_ctx, _input): Promise<{
144
+ cards: { id: string; name: string; balance: number }[];
145
+ total: number;
146
+ }> => ({ cards: [], total: 0 }),
147
+ });
148
+ // → StoreCardsResult { cards: List<...>, total: int }
149
+ ```
150
+
151
+ ### Combining `.modelName()` + `.int()`
152
+
153
+ The most common pattern: rename nested models and patch numeric types while
154
+ keeping the parent result inferred from TS:
155
+
156
+ ```ts
157
+ const storeCards = query({
158
+ args: z.object({}),
159
+ returns: z.object({
160
+ value: z.array(
161
+ z.object({
162
+ balanceCents: z.number().int(), // double → int
163
+ }).modelName("StoreCardItem")
164
+ ),
165
+ }),
166
+ handler: async (ctx, input) => {
167
+ const cards = await ctx.db.query(GiftCardSchema).select().get();
168
+ return {
169
+ cursor: "...",
170
+ value: cards,
171
+ };
172
+ },
173
+ });
174
+ // → StoreCardsResult { cursor: String, value: List<StoreCardItem> }
175
+ // StoreCardItem { id, balanceCents: int, ... }
176
+ ```
177
+
178
+ ### Procedure-level list return with model naming
179
+
180
+ When a procedure returns a bare list (not wrapped in an object), use
181
+ `z.array()` at the top level of `returns`:
182
+
183
+ ```ts
184
+ const cardLedger = query({
185
+ args: z.object({ cardId: z.string() }),
186
+ returns: z.array(
187
+ z.object({
188
+ amountCents: z.number().int(),
189
+ balanceAfterCents: z.number().int(),
190
+ }).modelName("CardLedgerItem")
191
+ ),
192
+ handler: async (ctx, { cardId }) =>
193
+ ctx.db.query(CardTransactionSchema).select()
194
+ .where((f) => f.eq("cardId", cardId)).get(),
195
+ });
196
+ // → result type: List<CardLedgerItem>
197
+ // CardLedgerItem { ..., amountCents: int, balanceAfterCents: int, ... }
198
+ ```
199
+
200
+ ### Nested model overrides
201
+
202
+ Override fields inside a list element's model by nesting `.modelName()` +
203
+ field overrides:
204
+
205
+ ```ts
206
+ const nestedOverride = query({
207
+ args: z.object({}),
208
+ returns: z.object({
209
+ items: z.array(
210
+ z.object({
211
+ id: z.string(),
212
+ balance: z.number().int(), // override int
213
+ name: z.string(),
214
+ }).modelName("NestedItem")
215
+ ),
216
+ }),
217
+ handler: async (_ctx, _input) => ({
218
+ items: [] as { id: string; balance: number; name: string }[],
219
+ }),
220
+ });
221
+ // → NestedItem { id: String, balance: int, name: String }
222
+ ```
223
+
224
+ ### Summary of helpers
225
+
226
+ | Helper | Purpose | Example |
227
+ | --- | --- | --- |
228
+ | `.modelName("Name")` | Rename a model in generated output | `z.object({ ... }).modelName("CardItem")` |
229
+ | `z.number().int()` | Patch a field's type from `double` to `int` | `z.object({ balance: z.number().int() })` |
230
+ | `z.bigint()` | Patch a field's type to `BigInt` | `z.object({ revision: z.bigint() })` |
231
+
232
+ All `returns` helpers are purely for codegen — they have no effect at runtime.
233
+ The Zod schemas are not used for runtime validation of the handler's return
234
+ value.
235
+
88
236
  ## License
89
237
 
90
238
  MIT
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-CTW5fpJ-.js";
2
+ import { n as generateToDisk, r as readEntryConfig } from "./src-C8EGEwt7.js";
3
3
  import path from "node:path";
4
4
  //#region src/cli.ts
5
5
  function parseArgs(argv) {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as emit, c as loadSchemaEnums, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, s as SchemaEnumRegistry, t as generate } from "./src-CTW5fpJ-.js";
1
+ import { a as emit, c as loadSchemaEnums, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, s as SchemaEnumRegistry, t as generate } from "./src-C8EGEwt7.js";
2
2
  export { SchemaEnumRegistry, emit, extractClient, generate, generateToDisk, loadSchemaEnums, readEntryConfig, resolveConfig };
@@ -80,6 +80,139 @@ function scanZodArgMeta(routerDecl) {
80
80
  } catch {}
81
81
  return result;
82
82
  }
83
+ /** From a `query({ returns: ..., handler })` call, get the output schema. */
84
+ function getOutputSchema(call) {
85
+ if (!Node.isCallExpression(call)) return void 0;
86
+ const arg = call.getArguments()[0];
87
+ if (!arg || !Node.isObjectLiteralExpression(arg)) return void 0;
88
+ const returnsProp = arg.getProperty("returns");
89
+ if (!returnsProp || !Node.isPropertyAssignment(returnsProp)) return void 0;
90
+ return returnsProp.getInitializer();
91
+ }
92
+ /**
93
+ * Scan each procedure's `returns` Zod schema into a structured metadata tree.
94
+ * Returns `Map<procName, OutputSchemaMeta>`.
95
+ */
96
+ function scanZodOutputMeta(routerDecl) {
97
+ const result = /* @__PURE__ */ new Map();
98
+ try {
99
+ const proceduresObj = getProceduresObjectLiteral(routerDecl);
100
+ if (!proceduresObj) return result;
101
+ for (const prop of proceduresObj.getProperties()) {
102
+ const key = getPropertyKey(prop);
103
+ if (!key) continue;
104
+ const call = resolveProcedureCall(prop);
105
+ if (!call) continue;
106
+ const outputSchema = getOutputSchema(call);
107
+ if (!outputSchema) continue;
108
+ const meta = scanOutputSchemaNode(outputSchema);
109
+ if (meta) result.set(key, meta);
110
+ }
111
+ } catch {}
112
+ return result;
113
+ }
114
+ /**
115
+ * Recursively scan a Zod schema expression, extracting field metadata and
116
+ * `__genType` model names. Returns `null` if no metadata found.
117
+ */
118
+ function scanOutputSchemaNode(node) {
119
+ if (!node) return null;
120
+ let resolved = node;
121
+ if (Node.isIdentifier(resolved)) {
122
+ const init = resolveIdentifierInit(resolved);
123
+ if (init) resolved = init;
124
+ }
125
+ let modelName;
126
+ let isObject = false;
127
+ let baseCallNode;
128
+ let arrayElementNode;
129
+ let cur = resolved;
130
+ while (cur && Node.isCallExpression(cur)) {
131
+ const expr = cur.getExpression();
132
+ if (Node.isPropertyAccessExpression(expr)) {
133
+ const method = expr.getName();
134
+ if (method === "meta") {
135
+ const metaArg = cur.getArguments()[0];
136
+ if (metaArg && Node.isObjectLiteralExpression(metaArg)) {
137
+ const genTypeProp = metaArg.getProperty("__genType");
138
+ if (genTypeProp && Node.isPropertyAssignment(genTypeProp)) {
139
+ const init = genTypeProp.getInitializer();
140
+ if (init && Node.isStringLiteral(init)) modelName = init.getLiteralValue();
141
+ }
142
+ }
143
+ cur = expr.getExpression();
144
+ } else if (method === "modelName") {
145
+ const nameArg = cur.getArguments()[0];
146
+ if (nameArg && Node.isStringLiteral(nameArg)) modelName = nameArg.getLiteralValue();
147
+ cur = expr.getExpression();
148
+ } else if (method === "array") {
149
+ arrayElementNode = cur.getArguments()[0];
150
+ cur = expr.getExpression();
151
+ } else if (/^(strict|loose)?[oO]bject$/.test(method)) {
152
+ isObject = true;
153
+ baseCallNode = cur;
154
+ break;
155
+ } else cur = expr.getExpression();
156
+ } else break;
157
+ }
158
+ if (!isObject && cur && Node.isCallExpression(cur)) {
159
+ const expr = cur.getExpression();
160
+ if (Node.isIdentifier(expr) || Node.isPropertyAccessExpression(expr)) {
161
+ const exprText = expr.getText();
162
+ if (/(^|\.)(strict|loose)?[oO]bject$/.test(exprText)) {
163
+ isObject = true;
164
+ baseCallNode = cur;
165
+ }
166
+ }
167
+ }
168
+ if (isObject && baseCallNode && Node.isCallExpression(baseCallNode)) {
169
+ const arg = baseCallNode.getArguments()[0];
170
+ if (!arg || !Node.isObjectLiteralExpression(arg)) return modelName ? {
171
+ fieldMeta: /* @__PURE__ */ new Map(),
172
+ modelName,
173
+ nested: /* @__PURE__ */ new Map()
174
+ } : null;
175
+ const fieldMeta = /* @__PURE__ */ new Map();
176
+ const nested = /* @__PURE__ */ new Map();
177
+ for (const prop of arg.getProperties()) {
178
+ if (!Node.isPropertyAssignment(prop)) continue;
179
+ const key = prop.getName();
180
+ const init = prop.getInitializer();
181
+ if (!init) continue;
182
+ const methods = topLevelZodMethods(init);
183
+ const meta = {};
184
+ if (methods.has("bigint")) meta.numeric = "bigint";
185
+ else if (methods.has("int")) meta.numeric = "int";
186
+ if (methods.has("default") || methods.has("catch") || methods.has("prefault")) meta.optional = true;
187
+ const description = topLevelZodDescription(init);
188
+ if (description) meta.description = description;
189
+ if (meta.numeric || meta.optional || meta.description) fieldMeta.set(key, meta);
190
+ const nestedMeta = scanOutputSchemaNode(init);
191
+ if (nestedMeta) nested.set(key, nestedMeta);
192
+ }
193
+ return {
194
+ fieldMeta,
195
+ modelName,
196
+ nested
197
+ };
198
+ }
199
+ if (arrayElementNode) {
200
+ const elementMeta = scanOutputSchemaNode(arrayElementNode);
201
+ if (elementMeta || modelName) return {
202
+ fieldMeta: /* @__PURE__ */ new Map(),
203
+ modelName,
204
+ nested: /* @__PURE__ */ new Map([["__element", elementMeta ?? {
205
+ fieldMeta: /* @__PURE__ */ new Map(),
206
+ nested: /* @__PURE__ */ new Map()
207
+ }]])
208
+ };
209
+ }
210
+ return modelName ? {
211
+ fieldMeta: /* @__PURE__ */ new Map(),
212
+ modelName,
213
+ nested: /* @__PURE__ */ new Map()
214
+ } : null;
215
+ }
83
216
  function getProceduresObjectLiteral(routerDecl) {
84
217
  let init;
85
218
  if (Node.isVariableDeclaration(routerDecl)) init = routerDecl.getInitializer();
@@ -123,9 +256,6 @@ function followToCall(node, depth) {
123
256
  /**
124
257
  * Given an Identifier that may be an import binding, follow through the import
125
258
  * chain to the actual exported declaration's initializer node.
126
- *
127
- * Handles: `import { listItems } from "./procs.js"` → the exported VariableDeclaration
128
- * `import { x } from "./reexport.js"` where that file re-exports → recursive follow
129
259
  */
130
260
  function followImport(id) {
131
261
  const sourceFile = id.getSourceFile();
@@ -167,9 +297,6 @@ function followImport(id) {
167
297
  }
168
298
  }
169
299
  }
170
- /** Extract the initializer expression from a definition node, unwrapping any
171
- * container nodes (VariableStatement, ExportNamedDeclaration, etc.) that
172
- * ts-morph may return for cross-file imports. */
173
300
  function definitionInitializer(def) {
174
301
  if (Node.isVariableDeclaration(def)) return def.getInitializer();
175
302
  if (Node.isVariableStatement(def)) for (const d of def.getDeclarationList().getDeclarations()) {
@@ -181,7 +308,6 @@ function definitionInitializer(def) {
181
308
  if (init) return init;
182
309
  }
183
310
  }
184
- /** Resolve an Identifier to its initializer, following definitions across files. */
185
311
  function resolveIdentifierInit(id) {
186
312
  if (!Node.isIdentifier(id)) return void 0;
187
313
  for (const def of id.getDefinitionNodes()) {
@@ -226,11 +352,6 @@ function scanObjectSchema(schema) {
226
352
  }
227
353
  return map;
228
354
  }
229
- /**
230
- * Names of the methods called at the top level of a Zod field expression.
231
- * For `z.number().int().default(0)` → `{ number, int, default }`. Nested
232
- * schemas (inside an object argument) are not descended into.
233
- */
234
355
  function topLevelZodMethods(node) {
235
356
  const methods = /* @__PURE__ */ new Set();
236
357
  let cur = node;
@@ -243,11 +364,6 @@ function topLevelZodMethods(node) {
243
364
  }
244
365
  return methods;
245
366
  }
246
- /**
247
- * The description string from a field's top-level `.describe("...")` or
248
- * `.meta({ description: "..." })`, walking the method chain (outermost first,
249
- * so the outermost description wins).
250
- */
251
367
  function topLevelZodDescription(node) {
252
368
  let cur = node;
253
369
  while (cur && Node.isCallExpression(cur)) {
@@ -285,79 +401,6 @@ function findZodObjectLiteral(node) {
285
401
  }
286
402
  return cur && Node.isObjectLiteralExpression(cur) ? cur : void 0;
287
403
  }
288
- /**
289
- * Read `@genType` JSDoc tags from procedure declarations for return type overrides.
290
- *
291
- * Syntax:
292
- * - Object shape: `/** @genType {{value: List<StoreCardItem>}} *​/` — overrides specific fields
293
- * - Direct type: `/** @genType List<StoreCardItem> *​/` — overrides the whole return type
294
- *
295
- * Returns a map of procedure name → raw `@genType` value string.
296
- */
297
- function scanProcedureTypeOverrides(routerDecl) {
298
- const result = /* @__PURE__ */ new Map();
299
- try {
300
- const proceduresObj = getProceduresObjectLiteral(routerDecl);
301
- if (!proceduresObj) return result;
302
- for (const prop of proceduresObj.getProperties()) {
303
- const key = getPropertyKey(prop);
304
- if (!key) continue;
305
- const typeValue = procedureTypeTag(prop);
306
- if (typeValue) result.set(key, typeValue);
307
- }
308
- } catch {}
309
- return result;
310
- }
311
- /** Read the `@genType` tag from a procedure declaration's JSDoc. */
312
- function procedureTypeTag(prop) {
313
- const tag = findTypeTagOnNode(prop);
314
- if (tag) return tag;
315
- let idNode;
316
- if (Node.isShorthandPropertyAssignment(prop)) idNode = prop.getNameNode();
317
- else if (Node.isPropertyAssignment(prop)) idNode = prop.getInitializer();
318
- if (idNode && Node.isIdentifier(idNode)) {
319
- for (const def of idNode.getDefinitionNodes()) if (Node.isVariableDeclaration(def)) {
320
- const stmt = def.getVariableStatement();
321
- if (stmt) {
322
- const tag = findTypeTagOnNode(stmt);
323
- if (tag) return tag;
324
- }
325
- }
326
- for (const def of idNode.getDefinitionNodes()) if (Node.isImportSpecifier(def)) {
327
- const sourceFile = def.getImportDeclaration().getModuleSpecifierSourceFile();
328
- if (sourceFile) {
329
- const exported = sourceFile.getExportedDeclarations().get(def.getName());
330
- if (exported && exported.length > 0) {
331
- const decl = exported[0];
332
- if (Node.isVariableDeclaration(decl)) {
333
- const stmt = decl.getVariableStatement();
334
- if (stmt) {
335
- const tag = findTypeTagOnNode(stmt);
336
- if (tag) return tag;
337
- }
338
- }
339
- }
340
- }
341
- }
342
- }
343
- }
344
- /** Extract the `@genType` tag value from a JSDocable node, if present. */
345
- function findTypeTagOnNode(node) {
346
- if (!Node.isJSDocable(node)) return void 0;
347
- const docs = node.getJsDocs();
348
- if (docs.length === 0) return void 0;
349
- const jd = docs[docs.length - 1];
350
- for (const tag of jd.getTags()) if (tag.getTagName() === "genType") {
351
- const text = tag.compilerNode.typeExpression?.getText?.()?.trim();
352
- if (text) return text;
353
- const comment = tag.getComment();
354
- if (typeof comment === "string") return comment.trim();
355
- if (Array.isArray(comment)) {
356
- const joined = comment.map((c) => c.getText?.() ?? "").join("").trim();
357
- if (joined) return joined;
358
- }
359
- }
360
- }
361
404
  //#endregion
362
405
  //#region src/names.ts
363
406
  /** PascalCase an arbitrary string (splitting on any non-alphanumeric run). */
@@ -548,8 +591,9 @@ function extractClient(config, schemaEnums = SchemaEnumRegistry.empty()) {
548
591
  if (!proceduresSym) throw new Error(`Export "${config.router}" does not look like a Supalive router (no "procedures" property).`);
549
592
  const proceduresType = proceduresSym.getTypeAtLocation(routerDecl);
550
593
  const argMeta = scanZodArgMeta(routerDecl);
594
+ const outputMeta = scanZodOutputMeta(routerDecl);
551
595
  const procDocs = scanProcedureDocs(routerDecl);
552
- const extractor = new Extractor(routerDecl, config, argMeta, scanProcedureTypeOverrides(routerDecl), project.getTypeChecker(), schemaEnums);
596
+ const extractor = new Extractor(routerDecl, config, argMeta, outputMeta, project.getTypeChecker(), schemaEnums);
553
597
  const procedures = [];
554
598
  for (const procSym of proceduresType.getProperties()) {
555
599
  const name = procSym.getName();
@@ -573,7 +617,7 @@ var Extractor = class {
573
617
  locationNode;
574
618
  config;
575
619
  argMeta;
576
- procTypeOverrides;
620
+ outputMeta;
577
621
  tc;
578
622
  schemaEnums;
579
623
  modelRegistry = /* @__PURE__ */ new Map();
@@ -583,11 +627,11 @@ var Extractor = class {
583
627
  enumsFinalized = false;
584
628
  nameCounts = /* @__PURE__ */ new Map();
585
629
  visiting = /* @__PURE__ */ new Set();
586
- constructor(locationNode, config, argMeta, procTypeOverrides, tc, schemaEnums) {
630
+ constructor(locationNode, config, argMeta, outputMeta, tc, schemaEnums) {
587
631
  this.locationNode = locationNode;
588
632
  this.config = config;
589
633
  this.argMeta = argMeta;
590
- this.procTypeOverrides = procTypeOverrides;
634
+ this.outputMeta = outputMeta;
591
635
  this.tc = tc;
592
636
  this.schemaEnums = schemaEnums;
593
637
  }
@@ -601,36 +645,6 @@ var Extractor = class {
601
645
  return;
602
646
  }
603
647
  }
604
- /**
605
- * Read an `@genType` JSDoc tag from a property and parse it into a DartType.
606
- *
607
- * Supported formats:
608
- * - `/** @genType StoreCardItem *​/` → model with that name
609
- * - `/** @genType List<StoreCardItem> *​/` → list with model element
610
- * - `/** @genType String *​/`, `/** @genType int *​/`, etc. → primitive
611
- */
612
- readTypeOverride(prop) {
613
- try {
614
- const decls = prop.getDeclarations();
615
- if (!decls || decls.length === 0) return null;
616
- const node = decls[0];
617
- if (!Node.isJSDocable(node)) return null;
618
- const jsDocs = node.getJsDocs();
619
- if (jsDocs.length === 0) return null;
620
- const typeTag = jsDocs[jsDocs.length - 1].getTags().find((t) => t.getTagName() === "genType");
621
- if (!typeTag) return null;
622
- let raw = typeTag.compilerNode.typeExpression?.getText?.()?.trim();
623
- if (!raw) {
624
- const comment = typeTag.getComment();
625
- if (typeof comment === "string") raw = comment.trim();
626
- else if (Array.isArray(comment)) raw = comment.map((c) => c.getText?.() ?? "").join("").trim();
627
- }
628
- if (!raw) return null;
629
- return parseTypeOverride(raw);
630
- } catch {
631
- return null;
632
- }
633
- }
634
648
  models() {
635
649
  this.finalizeEnums();
636
650
  return [...this.modelRegistry.values()];
@@ -663,77 +677,9 @@ var Extractor = class {
663
677
  }
664
678
  const ret = unwrapPromise(sig.getReturnType());
665
679
  const { hasNull, hasUndefined } = splitNullish(ret);
666
- const procOverrideRaw = this.procTypeOverrides.get(name);
667
- let resultType;
668
- if (procOverrideRaw) {
669
- const parsed = parseProcTypeOverride(procOverrideRaw);
670
- if (parsed) if (parsed.kind === "direct") {
671
- const core = splitNullish(ret).core;
672
- if (core.length > 0) {
673
- if (parsed.dartType.kind === "model") this.walkSingle(core[0], parsed.dartType.name);
674
- else if (parsed.dartType.kind === "list" && parsed.dartType.element.kind === "model") {
675
- const actualEl = core[0].isArray() ? core[0].getArrayElementTypeOrThrow() : core[0];
676
- this.walkSingle(actualEl, parsed.dartType.element.name);
677
- }
678
- }
679
- resultType = parsed.dartType;
680
- } else {
681
- const core = splitNullish(ret).core;
682
- const actualType = core.length > 0 ? core[0] : void 0;
683
- const modelName = pascal(name) + "Result";
684
- const fields = [];
685
- const overrideKeys = new Set(parsed.fields.keys());
686
- for (const [fieldName, fieldType] of parsed.fields) {
687
- if (actualType && actualType.isObject()) {
688
- const actualProp = actualType.getProperty(fieldName);
689
- if (actualProp) {
690
- const actualPropType = actualProp.getTypeAtLocation(this.locationNode);
691
- if (fieldType.kind === "model") this.walkSingle(actualPropType, fieldType.name);
692
- else if (fieldType.kind === "list" && fieldType.element.kind === "model") {
693
- const core2 = splitNullish(actualPropType).core;
694
- if (core2.length > 0) {
695
- const el = core2[0].isArray() ? core2[0].getArrayElementTypeOrThrow() : core2[0];
696
- this.walkSingle(el, fieldType.element.name);
697
- }
698
- }
699
- }
700
- }
701
- fields.push({
702
- name: camel(fieldName),
703
- jsonKey: fieldName,
704
- type: fieldType,
705
- optional: false,
706
- nullable: false
707
- });
708
- }
709
- if (actualType && actualType.isObject()) for (const prop of actualType.getProperties()) {
710
- const jsonKey = prop.getName();
711
- if (overrideKeys.has(jsonKey)) continue;
712
- if (prop.getFlags() & ts.SymbolFlags.Method) continue;
713
- const { core: propCore, hasNull, hasUndefined } = splitNullish(prop.getTypeAtLocation(this.locationNode));
714
- const optional = hasUndefined;
715
- const nullable = hasNull;
716
- const fieldHint = modelName.replace(/Result$/, "") + pascal(jsonKey);
717
- const dartType = propCore.length > 0 ? propCore.length === 1 ? this.walkSingle(propCore[0], fieldHint) : this.walkUnion(propCore, { hint: fieldHint }) : { kind: "dynamic" };
718
- fields.push({
719
- name: camel(jsonKey),
720
- jsonKey,
721
- type: dartType,
722
- optional,
723
- nullable
724
- });
725
- }
726
- this.modelRegistry.set(modelName, {
727
- name: modelName,
728
- fields
729
- });
730
- resultType = {
731
- kind: "model",
732
- name: modelName
733
- };
734
- }
735
- else resultType = this.walk(ret, pascal(name) + "Result");
736
- } else resultType = this.walk(ret, pascal(name) + "Result");
680
+ const resultType = this.walk(ret, pascal(name) + "Result");
681
+ const outputSchemaMeta = this.outputMeta.get(name);
682
+ if (outputSchemaMeta) this.applyOutputMeta(resultType, outputSchemaMeta);
737
683
  return {
738
684
  name,
739
685
  kind,
@@ -744,6 +690,41 @@ var Extractor = class {
744
690
  resultNullable: hasNull || hasUndefined
745
691
  };
746
692
  }
693
+ /**
694
+ * Recursively apply output schema metadata to a DartType tree.
695
+ * Patches numeric types (int/bigint), renames models via __genType,
696
+ * and recurses into nested objects and lists.
697
+ */
698
+ applyOutputMeta(dt, meta) {
699
+ if (meta.modelName && dt.kind === "model") {
700
+ const existing = this.modelRegistry.get(dt.name);
701
+ if (existing && !this.modelRegistry.has(meta.modelName)) {
702
+ const renamed = {
703
+ ...existing,
704
+ name: meta.modelName
705
+ };
706
+ this.modelRegistry.set(meta.modelName, renamed);
707
+ this.modelRegistry.delete(dt.name);
708
+ }
709
+ dt.name = meta.modelName;
710
+ }
711
+ if (dt.kind === "model") {
712
+ const model = this.modelRegistry.get(dt.name);
713
+ if (model) for (const f of model.fields) {
714
+ const fMeta = meta.fieldMeta.get(f.jsonKey);
715
+ if (fMeta) {
716
+ if (fMeta.numeric && (f.type.kind === "double" || f.type.kind === "int")) f.type = { kind: fMeta.numeric };
717
+ if (fMeta.optional) f.optional = true;
718
+ if (fMeta.description && !f.doc) f.doc = fMeta.description;
719
+ }
720
+ const nestedMeta = meta.nested.get(f.jsonKey);
721
+ if (nestedMeta) this.applyOutputMeta(f.type, nestedMeta);
722
+ }
723
+ } else if (dt.kind === "list") {
724
+ const elMeta = meta.nested.get("__element");
725
+ if (elMeta) this.applyOutputMeta(dt.element, elMeta);
726
+ }
727
+ }
747
728
  walk(type, hint) {
748
729
  const { core } = splitNullish(type);
749
730
  if (core.length === 0) return { kind: "dynamic" };
@@ -817,19 +798,8 @@ var Extractor = class {
817
798
  const { core, hasNull, hasUndefined } = splitNullish(propType);
818
799
  const optional = declaredOptional || hasUndefined;
819
800
  const nullable = hasNull;
820
- const typeOverride = this.readTypeOverride(prop);
821
801
  let dartType;
822
- if (typeOverride) if (typeOverride.kind === "list" && typeOverride.element.kind === "model") if (core.length > 0) {
823
- const actualEl = core[0].isArray() ? core[0].getArrayElementTypeOrThrow() : core[0];
824
- dartType = {
825
- kind: "list",
826
- element: this.walkSingle(actualEl, typeOverride.element.name)
827
- };
828
- } else dartType = typeOverride;
829
- else if (typeOverride.kind === "model") if (core.length > 0) dartType = this.walkSingle(core[0], typeOverride.name);
830
- else dartType = typeOverride;
831
- else dartType = typeOverride;
832
- else if (core.length === 0) dartType = { kind: "dynamic" };
802
+ if (core.length === 0) dartType = { kind: "dynamic" };
833
803
  else if (core.length === 1) {
834
804
  const fieldHint = name.replace(/Result$/, "") + pascal(jsonKey);
835
805
  dartType = this.walkSingle(core[0], fieldHint);
@@ -1118,101 +1088,6 @@ function cleanStem(modelName) {
1118
1088
  }
1119
1089
  return singularizeWord(s);
1120
1090
  }
1121
- const PRIMITIVE_MAP = {
1122
- string: "string",
1123
- int: "int",
1124
- double: "double",
1125
- bool: "bool",
1126
- bigint: "bigint",
1127
- bytes: "bytes",
1128
- datetime: "datetime",
1129
- dynamic: "dynamic"
1130
- };
1131
- /**
1132
- * Parse an `@type` override value into a DartType.
1133
- *
1134
- * - `"StoreCardItem"` → `{ kind: "model", name: "StoreCardItem" }`
1135
- * - `"List<StoreCardItem>"` → `{ kind: "list", element: { kind: "model", name: "StoreCardItem" } }`
1136
- * - `"String"` → `{ kind: "string" }`
1137
- * - `"int"` → `{ kind: "int" }`
1138
- */
1139
- function parseTypeOverride(raw) {
1140
- const listMatch = raw.match(/^List<(.+)>$/);
1141
- if (listMatch) {
1142
- const innerType = parseTypeOverride(listMatch[1].trim());
1143
- return innerType ? {
1144
- kind: "list",
1145
- element: innerType
1146
- } : null;
1147
- }
1148
- const prim = PRIMITIVE_MAP[raw.toLowerCase()];
1149
- if (prim) return { kind: prim };
1150
- return {
1151
- kind: "model",
1152
- name: raw
1153
- };
1154
- }
1155
- /**
1156
- * Parse a procedure-level `@genType` value.
1157
- *
1158
- * - `{{value: List<StoreCardItem>}}` → object shape with field overrides
1159
- * - `List<StoreCardItem>` → direct type override
1160
- * - `StoreCardResult` → direct model name override
1161
- */
1162
- function parseProcTypeOverride(raw) {
1163
- const objectMatch = raw.match(/^\{([\s\S]+)\}$/);
1164
- if (objectMatch) {
1165
- let inner = objectMatch[1];
1166
- if (inner.startsWith("{")) inner = inner.slice(1);
1167
- if (inner.endsWith("}")) inner = inner.slice(0, -1);
1168
- const fields = parseFieldOverrides(inner);
1169
- return fields.size > 0 ? {
1170
- kind: "object",
1171
- fields
1172
- } : null;
1173
- }
1174
- const dartType = parseTypeOverride(raw);
1175
- return dartType ? {
1176
- kind: "direct",
1177
- dartType
1178
- } : null;
1179
- }
1180
- /**
1181
- * Parse comma-separated `field: Type` entries inside an object shape override.
1182
- *
1183
- * Handles nested `List<T>` types (the colon inside `List<...>` must not split
1184
- * the field name). Examples:
1185
- * - `"value: List<StoreCardItem>"` → `{ value: { kind: "list", ... } }`
1186
- * - `"name: String, count: int"` → two entries
1187
- */
1188
- function parseFieldOverrides(raw) {
1189
- const fields = /* @__PURE__ */ new Map();
1190
- const parts = splitOutsideAngles(raw);
1191
- for (const part of parts) {
1192
- const colonIdx = part.indexOf(":");
1193
- if (colonIdx === -1) continue;
1194
- const fieldName = part.slice(0, colonIdx).trim();
1195
- const typeStr = part.slice(colonIdx + 1).trim();
1196
- if (!fieldName || !typeStr) continue;
1197
- const dartType = parseTypeOverride(typeStr);
1198
- if (dartType) fields.set(fieldName, dartType);
1199
- }
1200
- return fields;
1201
- }
1202
- /** Split a string by commas, ignoring commas inside `<...>` angle brackets. */
1203
- function splitOutsideAngles(raw) {
1204
- const parts = [];
1205
- let depth = 0;
1206
- let start = 0;
1207
- for (let i = 0; i < raw.length; i++) if (raw[i] === "<") depth++;
1208
- else if (raw[i] === ">") depth = Math.max(0, depth - 1);
1209
- else if (raw[i] === "," && depth === 0) {
1210
- parts.push(raw.slice(start, i));
1211
- start = i + 1;
1212
- }
1213
- parts.push(raw.slice(start));
1214
- return parts;
1215
- }
1216
1091
  /** The bare candidate name for a structural (non-schema) enum group. */
1217
1092
  function fallbackBase(g) {
1218
1093
  return pascal(g.field ?? g.hint);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supalive/codegen",
3
- "version": "1.8.1",
3
+ "version": "1.9.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",