@contractkit/plugin-typescript 0.31.2 → 0.33.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/index.js CHANGED
@@ -77,6 +77,8 @@ function renderTsScalar(name, target) {
77
77
  return "number";
78
78
  case "bigint":
79
79
  return "bigint";
80
+ case "decimal":
81
+ return "Decimal";
80
82
  case "boolean":
81
83
  return "boolean";
82
84
  case "date":
@@ -161,6 +163,251 @@ function renderOutputTsType(type, modelsWithOutput, target = "client") {
161
163
  }
162
164
  __name(renderOutputTsType, "renderOutputTsType");
163
165
 
166
+ // src/decimal-runtime.ts
167
+ var DECIMAL_IMPORT = `import { Decimal } from 'decimal.js';`;
168
+ var DECIMAL_CONFIG_LINE = `Decimal.set({ toExpNeg: -9e15, toExpPos: 9e15 });`;
169
+ var DECIMAL_ZOD_SCHEMA_LINE = `const _ZodDecimal = z.preprocess((val) => { if (typeof val !== 'string') return val; try { return new Decimal(val); } catch { return val; } }, z.custom<Decimal>((val) => Decimal.isDecimal(val), { message: 'Must be an exact decimal sent as a quoted string, e.g. "1250.00"' }));`;
170
+ var DECIMAL_PRELUDE_LINES = [
171
+ DECIMAL_CONFIG_LINE,
172
+ DECIMAL_ZOD_SCHEMA_LINE
173
+ ];
174
+
175
+ // src/codegen-revive.ts
176
+ var DECIMAL_COERCE_DECL = [
177
+ `const __dec = (v: unknown, path: string): Decimal => {`,
178
+ ` if (typeof v !== 'string') {`,
179
+ ` throw new TypeError(\`ContractKit: expected a decimal string at '\${path}', received \${typeof v} \u2014 decimals must be sent as quoted JSON strings.\`);`,
180
+ ` }`,
181
+ ` try {`,
182
+ ` return new Decimal(v);`,
183
+ ` } catch {`,
184
+ ` throw new TypeError(\`ContractKit: '\${v}' at '\${path}' is not a valid decimal.\`);`,
185
+ ` }`,
186
+ `};`
187
+ ];
188
+ function reviveFnName(model, variant = "base") {
189
+ return `revive${model}${variant === "output" ? "Output" : ""}`;
190
+ }
191
+ __name(reviveFnName, "reviveFnName");
192
+ function applyCase(name, caseTransform) {
193
+ if (!caseTransform || caseTransform === "camel") return name;
194
+ if (caseTransform === "snake") return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
195
+ return name.charAt(0).toUpperCase() + name.slice(1);
196
+ }
197
+ __name(applyCase, "applyCase");
198
+ function typeReachesDecimal(type, opts) {
199
+ switch (type.kind) {
200
+ case "scalar":
201
+ return type.name === "decimal";
202
+ case "ref":
203
+ return opts.modelsWithDecimal.has(type.name);
204
+ case "array":
205
+ return typeReachesDecimal(type.item, opts);
206
+ case "lazy":
207
+ return typeReachesDecimal(type.inner, opts);
208
+ case "tuple":
209
+ return type.items.some((t) => typeReachesDecimal(t, opts));
210
+ case "record":
211
+ return typeReachesDecimal(type.value, opts);
212
+ case "union":
213
+ case "discriminatedUnion":
214
+ case "intersection":
215
+ return type.members.some((t) => typeReachesDecimal(t, opts));
216
+ case "inlineObject":
217
+ return type.fields.some((f) => typeReachesDecimal(f.type, opts));
218
+ default:
219
+ return false;
220
+ }
221
+ }
222
+ __name(typeReachesDecimal, "typeReachesDecimal");
223
+ var Scope = class Scope2 {
224
+ static {
225
+ __name(this, "Scope");
226
+ }
227
+ n = 0;
228
+ next(prefix) {
229
+ return `__${prefix}${this.n++}`;
230
+ }
231
+ };
232
+ function emit(slot, type, path, opts, scope, variant) {
233
+ switch (type.kind) {
234
+ case "scalar":
235
+ return type.name === "decimal" ? [
236
+ `${slot} = __dec(${slot}, '${path}');`
237
+ ] : [];
238
+ case "ref":
239
+ return opts.modelsWithDecimal.has(type.name) ? [
240
+ `${reviveRefName(type.name, opts, variant)}(${slot} as never);`
241
+ ] : [];
242
+ case "lazy":
243
+ return emit(slot, type.inner, path, opts, scope, variant);
244
+ case "array": {
245
+ if (!typeReachesDecimal(type.item, opts)) return [];
246
+ const arr = scope.next("a");
247
+ const i = scope.next("i");
248
+ const inner = emit(`${arr}[${i}]`, type.item, `${path}[]`, opts, scope, variant);
249
+ return [
250
+ `{`,
251
+ ` const ${arr} = ${slot} as unknown[];`,
252
+ ` for (let ${i} = 0; ${i} < ${arr}.length; ${i}++) {`,
253
+ ...inner.map((l) => ` ${l}`),
254
+ ` }`,
255
+ `}`
256
+ ];
257
+ }
258
+ case "tuple": {
259
+ const items = type.items.flatMap((t, idx) => typeReachesDecimal(t, opts) ? emit(`(${slot} as unknown[])[${idx}]`, t, `${path}[${idx}]`, opts, scope, variant) : []);
260
+ return items;
261
+ }
262
+ case "record": {
263
+ if (!typeReachesDecimal(type.value, opts)) return [];
264
+ const rec = scope.next("r");
265
+ const k = scope.next("k");
266
+ const inner = emit(`${rec}[${k}]`, type.value, `${path}{}`, opts, scope, variant);
267
+ return [
268
+ `{`,
269
+ ` const ${rec} = ${slot} as Record<string, unknown>;`,
270
+ ` for (const ${k} of Object.keys(${rec})) {`,
271
+ ...inner.map((l) => ` ${l}`),
272
+ ` }`,
273
+ `}`
274
+ ];
275
+ }
276
+ case "inlineObject": {
277
+ const relevant = type.fields.filter((f) => typeReachesDecimal(f.type, opts));
278
+ if (relevant.length === 0) return [];
279
+ const obj = scope.next("o");
280
+ const body = relevant.flatMap((f) => fieldStatements(obj, f, path, opts, scope, variant, void 0));
281
+ return [
282
+ `{`,
283
+ ` const ${obj} = ${slot} as Record<string, unknown>;`,
284
+ ...body.map((l) => ` ${l}`),
285
+ `}`
286
+ ];
287
+ }
288
+ case "intersection":
289
+ return type.members.flatMap((m) => emit(slot, m, path, opts, scope, variant));
290
+ case "union": {
291
+ const real = type.members.filter((m) => !(m.kind === "scalar" && m.name === "null"));
292
+ const target = real.find((m) => typeReachesDecimal(m, opts));
293
+ if (!target) return [];
294
+ const inner = emit(slot, target, path, opts, scope, variant);
295
+ return [
296
+ `if (${slot} != null) {`,
297
+ ...inner.map((l) => ` ${l}`),
298
+ `}`
299
+ ];
300
+ }
301
+ case "discriminatedUnion": {
302
+ const branches = [];
303
+ const disc = scope.next("d");
304
+ for (const member of type.members) {
305
+ if (!typeReachesDecimal(member, opts)) continue;
306
+ const tag = discriminatorTag(member, type.discriminator, opts);
307
+ const inner = emit(slot, member, path, opts, scope, variant);
308
+ if (inner.length === 0) continue;
309
+ if (tag === void 0) {
310
+ continue;
311
+ }
312
+ branches.push(` if (${disc} === ${JSON.stringify(tag)}) {`, ...inner.map((l) => ` ${l}`), ` }`);
313
+ }
314
+ if (branches.length === 0) return [];
315
+ return [
316
+ `{`,
317
+ ` const ${disc} = (${slot} as Record<string, unknown>)[${JSON.stringify(type.discriminator)}];`,
318
+ ...branches,
319
+ `}`
320
+ ];
321
+ }
322
+ default:
323
+ return [];
324
+ }
325
+ }
326
+ __name(emit, "emit");
327
+ function discriminatorTag(member, discriminator, opts) {
328
+ const fields = member.kind === "inlineObject" ? member.fields : member.kind === "ref" ? opts.modelMap?.get(member.name)?.fields : void 0;
329
+ const field = fields?.find((f) => f.name === discriminator);
330
+ if (field?.type.kind === "literal") return field.type.value;
331
+ if (field?.type.kind === "enum" && field.type.values.length === 1) return field.type.values[0];
332
+ return void 0;
333
+ }
334
+ __name(discriminatorTag, "discriminatorTag");
335
+ function fieldStatements(objVar, field, path, opts, scope, variant, outputCase) {
336
+ const key = variant === "output" ? applyCase(field.name, outputCase) : field.name;
337
+ const slot = `${objVar}[${JSON.stringify(key)}]`;
338
+ const inner = emit(slot, field.type, `${path}.${key}`, opts, scope, variant);
339
+ if (inner.length === 0) return [];
340
+ if (field.type.kind === "union") return inner;
341
+ if (field.optional || field.nullable) {
342
+ return [
343
+ `if (${slot} != null) {`,
344
+ ...inner.map((l) => ` ${l}`),
345
+ `}`
346
+ ];
347
+ }
348
+ return inner;
349
+ }
350
+ __name(fieldStatements, "fieldStatements");
351
+ function reviveRefName(name, opts, variant) {
352
+ if (variant === "output" && opts.modelsWithOutput?.has(name)) return reviveFnName(name, "output");
353
+ return reviveFnName(name, "base");
354
+ }
355
+ __name(reviveRefName, "reviveRefName");
356
+ function renderInlineReviver(fnName, tsType, type, opts, variant = "output") {
357
+ if (!typeReachesDecimal(type, opts)) return null;
358
+ const scope = new Scope();
359
+ const body = emit("__v[0]", type, fnName.replace(/^__revive/, ""), opts, scope, variant);
360
+ if (body.length === 0) return null;
361
+ return [
362
+ `/** Rehydrates the \`decimal\` fields of one response body. Mutates and returns \`raw\`. */`,
363
+ `function ${fnName}(raw: ${tsType}): ${tsType} {`,
364
+ ` const __v = [raw] as unknown[];`,
365
+ ...body.map((l) => ` ${l}`),
366
+ ` return __v[0] as ${tsType};`,
367
+ `}`
368
+ ];
369
+ }
370
+ __name(renderInlineReviver, "renderInlineReviver");
371
+ function renderReviveFunctions(model, opts) {
372
+ if (!opts.modelsWithDecimal.has(model.name)) return [];
373
+ const lines = renderOne(model, opts, "base");
374
+ if (opts.modelsWithOutput?.has(model.name)) {
375
+ lines.push("");
376
+ lines.push(...renderOne(model, opts, "output"));
377
+ }
378
+ return lines;
379
+ }
380
+ __name(renderReviveFunctions, "renderReviveFunctions");
381
+ function renderOne(model, opts, variant) {
382
+ const scope = new Scope();
383
+ const typeName = `${model.name}${variant === "output" ? "Output" : ""}`;
384
+ const fnName = reviveFnName(model.name, variant);
385
+ if (model.type) {
386
+ const body2 = emit("__v[0]", model.type, model.name, opts, scope, variant);
387
+ if (body2.length === 0) return [];
388
+ return [
389
+ `/** Rehydrates every \`decimal\` in a ${typeName} from its wire string. Mutates and returns \`raw\`. */`,
390
+ `export function ${fnName}(raw: ${typeName}): ${typeName} {`,
391
+ ` const __v = [raw] as unknown[];`,
392
+ ...body2.map((l) => ` ${l}`),
393
+ ` return __v[0] as ${typeName};`,
394
+ `}`
395
+ ];
396
+ }
397
+ const obj = scope.next("o");
398
+ const body = model.fields.flatMap((f) => typeReachesDecimal(f.type, opts) ? fieldStatements(obj, f, model.name, opts, scope, variant, model.outputCase) : []);
399
+ if (body.length === 0) return [];
400
+ return [
401
+ `/** Rehydrates every \`decimal\` in a ${typeName} from its wire string. Mutates and returns \`raw\`. */`,
402
+ `export function ${fnName}(raw: ${typeName}): ${typeName} {`,
403
+ ` const ${obj} = raw as unknown as Record<string, unknown>;`,
404
+ ...body.map((l) => ` ${l}`),
405
+ ` return raw;`,
406
+ `}`
407
+ ];
408
+ }
409
+ __name(renderOne, "renderOne");
410
+
164
411
  // src/codegen-contract.ts
165
412
  function modeToWrapper(mode) {
166
413
  switch (mode) {
@@ -225,6 +472,7 @@ function generateContract(root, context) {
225
472
  const needsBinary = rootNeedsScalar(root, "binary");
226
473
  const needsDatetime = rootNeedsScalar(root, "datetime");
227
474
  const needsJson = rootNeedsScalar(root, "json");
475
+ const needsDecimal = rootNeedsScalar(root, "decimal");
228
476
  const externalRefs = collectExternalRefs(root);
229
477
  const lines = [];
230
478
  const externalModelsWithInput = context?.modelsWithInput ?? /* @__PURE__ */ new Set();
@@ -254,9 +502,11 @@ function generateContract(root, context) {
254
502
  if (needsDuration) luxonImports.push("Duration");
255
503
  if (needsInterval) luxonImports.push("Interval");
256
504
  if (luxonImports.length > 0) lines.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
505
+ if (needsDecimal) lines.push(DECIMAL_IMPORT);
257
506
  for (const ref of allExternalRefs) {
258
507
  const importPath = resolveImportPath(ref, context);
259
- lines.push(`import { ${ref} } from '${importPath}';`);
508
+ const names = context?.emitRevivers && context.modelsWithDecimal?.has(ref) ? `${ref}, ${reviveFnName(ref)}` : ref;
509
+ lines.push(`import { ${names} } from '${importPath}';`);
260
510
  }
261
511
  lines.push("");
262
512
  if (needsBinary) {
@@ -268,20 +518,41 @@ function generateContract(root, context) {
268
518
  if (needsInterval) {
269
519
  lines.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
270
520
  }
521
+ if (needsDecimal) {
522
+ lines.push(...DECIMAL_PRELUDE_LINES);
523
+ }
271
524
  if (needsJson) {
272
525
  lines.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
273
526
  lines.push(`const _ZodJson: z.ZodType<_JsonValue> = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(_ZodJson), z.record(z.string(), _ZodJson)]));`);
274
527
  }
275
- if (needsBinary || needsDatetime || needsInterval || needsJson) lines.push("");
528
+ if (needsBinary || needsDatetime || needsInterval || needsDecimal || needsJson) lines.push("");
276
529
  const modelsWithWriteonly = new Set(root.models.filter((m) => m.fields.some((f) => f.visibility === "writeonly")).map((m) => m.name));
277
530
  const modelMap = new Map(root.models.map((m) => [
278
531
  m.name,
279
532
  m
280
533
  ]));
534
+ const reviveOpts = context?.emitRevivers && context.modelsWithDecimal ? {
535
+ modelsWithDecimal: context.modelsWithDecimal,
536
+ modelsWithOutput: allModelsWithOutput,
537
+ modelMap
538
+ } : void 0;
539
+ const bodyLines = [];
281
540
  for (const model of topoSortModels(root.models)) {
282
- lines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, modelsWithWriteonly, modelMap, allModelsWithOutput));
541
+ bodyLines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, modelsWithWriteonly, modelMap, allModelsWithOutput));
542
+ if (reviveOpts) {
543
+ const revivers = renderReviveFunctions(model, reviveOpts);
544
+ if (revivers.length > 0) {
545
+ bodyLines.push("");
546
+ bodyLines.push(...revivers);
547
+ }
548
+ }
549
+ bodyLines.push("");
550
+ }
551
+ if (bodyLines.some((l) => l.includes("__dec("))) {
552
+ lines.push(...DECIMAL_COERCE_DECL);
283
553
  lines.push("");
284
554
  }
555
+ lines.push(...bodyLines);
285
556
  return lines.join("\n");
286
557
  }
287
558
  __name(generateContract, "generateContract");
@@ -349,8 +620,8 @@ function generateSimpleModel(model, outPath) {
349
620
  lines.push(...inputBody.map((l) => ` ${l}`));
350
621
  lines.push(`}).transform(data => ({`);
351
622
  for (const field of model.fields) {
352
- const inputKey = applyCase(field.name, inputCase);
353
- const outputKey = applyCase(field.name, outputCase);
623
+ const inputKey = applyCase2(field.name, inputCase);
624
+ const outputKey = applyCase2(field.name, outputCase);
354
625
  if (field.optional) {
355
626
  const guard = hasInputTransform ? `data.${inputKey} != null` : `data.${inputKey} !== undefined`;
356
627
  lines.push(` ...(${guard} ? { ${quoteKey2(outputKey)}: data.${inputKey} } : {}),`);
@@ -473,12 +744,12 @@ function camelToPascal(s) {
473
744
  return s.charAt(0).toUpperCase() + s.slice(1);
474
745
  }
475
746
  __name(camelToPascal, "camelToPascal");
476
- function applyCase(name, caseTransform) {
747
+ function applyCase2(name, caseTransform) {
477
748
  if (!caseTransform || caseTransform === "camel") return name;
478
749
  if (caseTransform === "snake") return camelToSnake(name);
479
750
  return camelToPascal(name);
480
751
  }
481
- __name(applyCase, "applyCase");
752
+ __name(applyCase2, "applyCase");
482
753
  function renderFields(fields, defaultMode) {
483
754
  return fields.flatMap((f) => renderField(f, defaultMode));
484
755
  }
@@ -613,6 +884,18 @@ function renderScalar(s) {
613
884
  if (s.max !== void 0) inner += `.max(${s.max}n)`;
614
885
  return `z.preprocess((val) => typeof val === 'string' ? BigInt(val.replace(/n$/, '')) : val, ${inner})`;
615
886
  }
887
+ case "decimal": {
888
+ const checks = [];
889
+ if (s.scale !== void 0) checks.push(`v.decimalPlaces() <= ${s.scale}`);
890
+ if (s.min !== void 0) checks.push(`v.gte('${escapeString(String(s.min))}')`);
891
+ if (s.max !== void 0) checks.push(`v.lte('${escapeString(String(s.max))}')`);
892
+ if (checks.length === 0) return "_ZodDecimal";
893
+ const messageParts = [];
894
+ if (s.scale !== void 0) messageParts.push(`at most ${s.scale} decimal place${s.scale === 1 ? "" : "s"}`);
895
+ if (s.min !== void 0) messageParts.push(`at least ${s.min}`);
896
+ if (s.max !== void 0) messageParts.push(`at most ${s.max}`);
897
+ return `_ZodDecimal.refine((v) => ${checks.join(" && ")}, { message: 'Must be ${escapeString(messageParts.join(", "))}' })`;
898
+ }
616
899
  case "boolean":
617
900
  return `z.preprocess((v) => v === 'true' ? true : v === 'false' ? false : v, z.boolean())`;
618
901
  case "date": {
@@ -1127,7 +1410,7 @@ function bodyTypesStructurallyEqual(a, b) {
1127
1410
  switch (a.kind) {
1128
1411
  case "scalar": {
1129
1412
  const bb = b;
1130
- return a.name === bb.name && a.min === bb.min && a.max === bb.max && a.len === bb.len && a.regex === bb.regex && a.format === bb.format;
1413
+ return a.name === bb.name && a.min === bb.min && a.max === bb.max && a.len === bb.len && a.scale === bb.scale && a.regex === bb.regex && a.format === bb.format;
1131
1414
  }
1132
1415
  case "array": {
1133
1416
  const bb = b;
@@ -1207,6 +1490,9 @@ function generateOp(root, options = {}) {
1207
1490
  if (references("_ZodDatetime")) {
1208
1491
  helpers.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
1209
1492
  }
1493
+ if (references("_ZodDecimal")) {
1494
+ helpers.push(...DECIMAL_PRELUDE_LINES);
1495
+ }
1210
1496
  if (references("_ZodInterval")) {
1211
1497
  helpers.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
1212
1498
  }
@@ -1235,12 +1521,13 @@ function generateOp(root, options = {}) {
1235
1521
  if (koaImports.length > 0) {
1236
1522
  body.push(`import { ${koaImports.join(", ")} } from '@maroonedsoftware/koa';`);
1237
1523
  }
1238
- for (const svc of services) {
1524
+ for (const svc of services.filter(uses)) {
1239
1525
  const modulePath = root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate);
1240
1526
  body.push(`import { ${svc} } from '${modulePath}';`);
1241
1527
  }
1242
- if (types.length > 0) {
1243
- body.push(...generateTypeImports(types, root.file, options));
1528
+ const usedTypes = types.filter(uses);
1529
+ if (usedTypes.length > 0) {
1530
+ body.push(...generateTypeImports(usedTypes, root.file, options));
1244
1531
  }
1245
1532
  const luxonImports = [
1246
1533
  "DateTime",
@@ -1250,6 +1537,9 @@ function generateOp(root, options = {}) {
1250
1537
  if (luxonImports.length > 0) {
1251
1538
  body.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
1252
1539
  }
1540
+ if (uses("Decimal")) {
1541
+ body.push(DECIMAL_IMPORT);
1542
+ }
1253
1543
  if (uses("parseAndValidate")) {
1254
1544
  body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
1255
1545
  }
@@ -1359,9 +1649,11 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1359
1649
  const respHeaders = resp?.headers ?? [];
1360
1650
  const hasRespHeaders = respHeaders.length > 0;
1361
1651
  const headersAnnotation = hasRespHeaders ? renderHeadersAnnotation(respHeaders, options.modelsWithOutput) : "";
1652
+ let bodySchema;
1362
1653
  if (bodies.length === 1) {
1363
1654
  const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput);
1364
1655
  if (prelude) lines.push(` ${prelude}`);
1656
+ bodySchema = responseBodySchema(bodies[0].bodyType, options, prelude ? "resultType" : void 0);
1365
1657
  lines.push(` const service = ctx.container.get(${className});`);
1366
1658
  if (hasRespHeaders) {
1367
1659
  lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
@@ -1369,10 +1661,12 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1369
1661
  lines.push(` const result: ${annotation} = ${call};`);
1370
1662
  }
1371
1663
  } else if (bodies.length > 1) {
1372
- const { members, preludes } = renderResponseMembers(resp, options, {
1664
+ const rendered = renderResponseMembers(resp, options, {
1373
1665
  includeStatus: false,
1374
1666
  varPrefix: "result"
1375
1667
  });
1668
+ const { members, preludes } = rendered;
1669
+ bodySchema = rendered.bodySchema;
1376
1670
  for (const prelude of preludes) lines.push(` ${prelude}`);
1377
1671
  lines.push(` const service = ctx.container.get(${className});`);
1378
1672
  lines.push(` const result: ${members.join(" | ")} = ${call};`);
@@ -1389,10 +1683,10 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1389
1683
  lines.push(...headerSetLines(respHeaders, " "));
1390
1684
  if (bodies.length === 1) {
1391
1685
  lines.push(` ctx.type = '${bodies[0].contentType}';`);
1392
- lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
1686
+ lines.push(` ctx.body = ${responseBodyExpr(hasRespHeaders ? "result.body" : "result", bodySchema)};`);
1393
1687
  } else if (bodies.length > 1) {
1394
1688
  lines.push(` ctx.type = result.contentType;`);
1395
- lines.push(` ctx.body = result.body;`);
1689
+ lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchema)};`);
1396
1690
  }
1397
1691
  return lines;
1398
1692
  }
@@ -1401,6 +1695,7 @@ function generateMultiStatusResult(emitted, className, call, options) {
1401
1695
  const lines = [];
1402
1696
  const members = [];
1403
1697
  const preludes = [];
1698
+ const bodySchemas = /* @__PURE__ */ new Map();
1404
1699
  for (const resp of emitted) {
1405
1700
  const rendered = renderResponseMembers(resp, options, {
1406
1701
  includeStatus: true,
@@ -1408,6 +1703,7 @@ function generateMultiStatusResult(emitted, className, call, options) {
1408
1703
  });
1409
1704
  members.push(...rendered.members);
1410
1705
  preludes.push(...rendered.preludes);
1706
+ bodySchemas.set(resp.statusCode, rendered.bodySchema);
1411
1707
  }
1412
1708
  for (const prelude of preludes) lines.push(` ${prelude}`);
1413
1709
  lines.push(` const service = ctx.container.get(${className});`);
@@ -1422,7 +1718,7 @@ function generateMultiStatusResult(emitted, className, call, options) {
1422
1718
  lines.push(...headerSetLines(resp.headers ?? [], " "));
1423
1719
  if (resp.bodies.length > 0) {
1424
1720
  lines.push(` ctx.type = result.contentType;`);
1425
- lines.push(` ctx.body = result.body;`);
1721
+ lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchemas.get(resp.statusCode))};`);
1426
1722
  }
1427
1723
  lines.push(` break;`);
1428
1724
  }
@@ -1455,6 +1751,7 @@ function renderResponseMembers(resp, options, opts) {
1455
1751
  if (uniform) {
1456
1752
  const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput, `${opts.varPrefix}Type`);
1457
1753
  if (prelude) preludes.push(prelude);
1754
+ const bodySchema = responseBodySchema(bodies[0].bodyType, options, prelude ? `${opts.varPrefix}Type` : void 0);
1458
1755
  const contentType = bodies.map((b) => `'${b.contentType}'`).join(" | ");
1459
1756
  return {
1460
1757
  members: [
@@ -1465,7 +1762,8 @@ function renderResponseMembers(resp, options, opts) {
1465
1762
  ...trailing
1466
1763
  ].join("; ")} }`
1467
1764
  ],
1468
- preludes
1765
+ preludes,
1766
+ bodySchema
1469
1767
  };
1470
1768
  }
1471
1769
  const members = bodies.map((b, i) => {
@@ -1562,6 +1860,8 @@ function serverTsScalar(name) {
1562
1860
  return "number";
1563
1861
  case "bigint":
1564
1862
  return "bigint";
1863
+ case "decimal":
1864
+ return "Decimal";
1565
1865
  case "boolean":
1566
1866
  return "boolean";
1567
1867
  case "date":
@@ -1613,6 +1913,46 @@ function formatTypeAnnotation(bodyType, modelsWithOutput, varName = "resultType"
1613
1913
  };
1614
1914
  }
1615
1915
  __name(formatTypeAnnotation, "formatTypeAnnotation");
1916
+ function isRevalidatable(type, modelsWithOutput, modelsWithTransform) {
1917
+ const rec = /* @__PURE__ */ __name((t) => isRevalidatable(t, modelsWithOutput, modelsWithTransform), "rec");
1918
+ switch (type.kind) {
1919
+ case "ref":
1920
+ return !modelsWithOutput?.has(type.name) && !modelsWithTransform?.has(type.name);
1921
+ case "array":
1922
+ return rec(type.item);
1923
+ case "tuple":
1924
+ return type.items.every(rec);
1925
+ case "record":
1926
+ return rec(type.key) && rec(type.value);
1927
+ case "intersection": {
1928
+ const [first, ...rest] = type.members;
1929
+ if (!first) return true;
1930
+ if (rest.length === 0) return rec(first);
1931
+ const usesExtendChain = first.kind === "ref" && rest.every((m) => m.kind === "ref" || m.kind === "inlineObject");
1932
+ return usesExtendChain && type.members.every(rec);
1933
+ }
1934
+ case "union":
1935
+ case "discriminatedUnion":
1936
+ return type.members.every(rec);
1937
+ case "inlineObject":
1938
+ return type.fields.every((f) => rec(f.type));
1939
+ case "lazy":
1940
+ return rec(type.inner);
1941
+ default:
1942
+ return true;
1943
+ }
1944
+ }
1945
+ __name(isRevalidatable, "isRevalidatable");
1946
+ function responseBodySchema(bodyType, options, preludeVar) {
1947
+ if (!options.validateResponses) return void 0;
1948
+ if (!isRevalidatable(bodyType, options.modelsWithOutput, options.modelsWithTransform)) return void 0;
1949
+ return preludeVar ?? renderType(bodyType);
1950
+ }
1951
+ __name(responseBodySchema, "responseBodySchema");
1952
+ function responseBodyExpr(value, schema) {
1953
+ return schema ? `await parseAndValidate(${value}, ${schema}, 500)` : value;
1954
+ }
1955
+ __name(responseBodyExpr, "responseBodyExpr");
1616
1956
  function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", modelsWithInput) {
1617
1957
  if (!source) return [];
1618
1958
  const lines = [];
@@ -1890,7 +2230,7 @@ function deriveTypeImportPath(file, template) {
1890
2230
  __name(deriveTypeImportPath, "deriveTypeImportPath");
1891
2231
 
1892
2232
  // src/index.ts
1893
- import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs } from "@contractkit/core";
2233
+ import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform, computeModelsWithDecimal } from "@contractkit/core";
1894
2234
 
1895
2235
  // src/codegen-sdk.ts
1896
2236
  import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
@@ -1957,9 +2297,25 @@ function generateSdk(root, options = {}) {
1957
2297
  const includeInternal = options.includeInternal ?? false;
1958
2298
  const types = collectTypes2(root, options.modelsWithInput, options.modelsWithOutput, includeInternal);
1959
2299
  const clientClassName = options.clientClassName ?? deriveClientClassName(root.file);
2300
+ const inlineRevivers = /* @__PURE__ */ new Map();
2301
+ const classBody = [];
2302
+ for (const route of root.routes) {
2303
+ for (const op of route.operations) {
2304
+ const mods = resolveModifiers2(route, op);
2305
+ if (!includeInternal && mods.includes("internal")) continue;
2306
+ classBody.push("");
2307
+ if (mods.includes("deprecated")) classBody.push(" /** @deprecated */");
2308
+ classBody.push(...generateMethod(route, op, root.file, options, inlineRevivers));
2309
+ }
2310
+ }
2311
+ const inlineReviverDecls = [
2312
+ ...inlineRevivers.values()
2313
+ ].flat();
2314
+ const decimalPrelude = decimalPreludeFor(inlineReviverDecls);
1960
2315
  if (types.length > 0) {
1961
- lines.push(...generateTypeImports2(types, root.file, options));
2316
+ lines.push(...generateTypeImports2(types, root.file, options, usedRevivers(classBody)));
1962
2317
  }
2318
+ lines.push(...decimalPrelude.imports);
1963
2319
  if (options.sdkOptionsPath && options.outPath) {
1964
2320
  let rel = relative3(dirname3(options.outPath), options.sdkOptionsPath);
1965
2321
  rel = rel.replace(/\.ts$/, ".js");
@@ -2057,21 +2413,21 @@ function generateSdk(root, options = {}) {
2057
2413
  lines.push(...errorAliases);
2058
2414
  lines.push("");
2059
2415
  }
2416
+ if (decimalPrelude.decls.length > 0) {
2417
+ lines.push("");
2418
+ lines.push(...decimalPrelude.decls);
2419
+ }
2420
+ for (const decl of inlineRevivers.values()) {
2421
+ lines.push("");
2422
+ lines.push(...decl);
2423
+ }
2060
2424
  lines.push("/**");
2061
2425
  const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
2062
2426
  lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
2063
2427
  lines.push(" */");
2064
2428
  lines.push(`export class ${clientClassName} {`);
2065
2429
  lines.push(" constructor(private fetch: SdkFetch) {}");
2066
- for (const route of root.routes) {
2067
- for (const op of route.operations) {
2068
- const mods = resolveModifiers2(route, op);
2069
- if (!includeInternal && mods.includes("internal")) continue;
2070
- lines.push("");
2071
- if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
2072
- lines.push(...generateMethod(route, op, root.file, options));
2073
- }
2074
- }
2430
+ lines.push(...classBody);
2075
2431
  lines.push("}");
2076
2432
  lines.push("");
2077
2433
  return lines.join("\n");
@@ -2081,25 +2437,77 @@ function generateClientMethods(root, options) {
2081
2437
  const lines = [];
2082
2438
  const methodNames = [];
2083
2439
  const includeInternal = options.includeInternal ?? false;
2440
+ const inlineRevivers = /* @__PURE__ */ new Map();
2084
2441
  for (const route of root.routes) {
2085
2442
  for (const op of route.operations) {
2086
2443
  const mods = resolveModifiers2(route, op);
2087
2444
  if (!includeInternal && mods.includes("internal")) continue;
2088
2445
  lines.push("");
2089
2446
  if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
2090
- lines.push(...generateMethod(route, op, root.file, options));
2447
+ lines.push(...generateMethod(route, op, root.file, options, inlineRevivers));
2091
2448
  methodNames.push(deriveMethodName(op, route));
2092
2449
  }
2093
2450
  }
2451
+ const declLines = [
2452
+ ...inlineRevivers.values()
2453
+ ].flat();
2454
+ const { decls } = decimalPreludeFor(declLines);
2455
+ const preludeLines = [
2456
+ ...decls.length > 0 ? [
2457
+ "",
2458
+ ...decls
2459
+ ] : [],
2460
+ ...[
2461
+ ...inlineRevivers.values()
2462
+ ].flatMap((decl) => [
2463
+ "",
2464
+ ...decl
2465
+ ])
2466
+ ];
2094
2467
  return {
2095
2468
  lines,
2096
- methodNames
2469
+ methodNames,
2470
+ preludeLines,
2471
+ needsDecimalImport: decls.length > 0
2097
2472
  };
2098
2473
  }
2099
2474
  __name(generateClientMethods, "generateClientMethods");
2100
- function generateMethod(route, op, file, options) {
2475
+ function decimalPreludeFor(declLines) {
2476
+ if (!declLines.some((l) => l.includes("__dec("))) return {
2477
+ imports: [],
2478
+ decls: []
2479
+ };
2480
+ return {
2481
+ imports: [
2482
+ DECIMAL_IMPORT
2483
+ ],
2484
+ decls: [
2485
+ DECIMAL_CONFIG_LINE,
2486
+ "",
2487
+ ...DECIMAL_COERCE_DECL
2488
+ ]
2489
+ };
2490
+ }
2491
+ __name(decimalPreludeFor, "decimalPreludeFor");
2492
+ function usedRevivers(lines) {
2493
+ const found = /* @__PURE__ */ new Set();
2494
+ for (const m of lines.join("\n").matchAll(/\brevive[A-Z]\w*/g)) found.add(m[0]);
2495
+ return [
2496
+ ...found
2497
+ ].sort();
2498
+ }
2499
+ __name(usedRevivers, "usedRevivers");
2500
+ function generateMethod(route, op, file, options, inlineRevivers) {
2501
+ const revive = options.modelsWithDecimal && options.modelsWithDecimal.size > 0 && inlineRevivers ? {
2502
+ modelsWithDecimal: options.modelsWithDecimal,
2503
+ modelsWithOutput: options.modelsWithOutput,
2504
+ modelMap: options.modelMap,
2505
+ inlineRevivers,
2506
+ nameHint: ""
2507
+ } : void 0;
2101
2508
  const lines = [];
2102
2509
  const methodName = deriveMethodName(op, route);
2510
+ const mRevive = hint(revive, `${methodName.charAt(0).toUpperCase()}${methodName.slice(1)}`);
2103
2511
  const httpMethod = op.method.toUpperCase();
2104
2512
  const { modelsWithInput, modelsWithOutput } = options;
2105
2513
  const params = buildMethodParams(route, op, modelsWithInput);
@@ -2227,23 +2635,23 @@ function generateMethod(route, op, file, options) {
2227
2635
  lines.push(` switch (result.status) {`);
2228
2636
  for (const resp of rest) {
2229
2637
  lines.push(` case ${resp.statusCode}:`);
2230
- lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true));
2638
+ lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true, mRevive));
2231
2639
  }
2232
2640
  lines.push(` default:`);
2233
- lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true));
2641
+ lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true, mRevive));
2234
2642
  lines.push(` }`);
2235
2643
  } else if (primaryBodies.length > 1) {
2236
- lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false));
2644
+ lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false, mRevive));
2237
2645
  } else if (hasRespHeaders) {
2238
2646
  const headerEntries = sdkHeaderEntries(respHeaders);
2239
2647
  if (isVoid) {
2240
2648
  lines.push(` return { headers: { ${headerEntries} } };`);
2241
2649
  } else {
2242
- lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
2650
+ lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput, hint(mRevive, primaryResponse.statusCode))};`);
2243
2651
  lines.push(` return { data, headers: { ${headerEntries} } };`);
2244
2652
  }
2245
2653
  } else if (!isVoid) {
2246
- lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
2654
+ lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput, hint(mRevive, primaryResponse.statusCode))};`);
2247
2655
  }
2248
2656
  lines.push(" }");
2249
2657
  return lines;
@@ -2256,13 +2664,59 @@ function sdkDataType(body, modelsWithOutput) {
2256
2664
  return renderOutputTsType(body.bodyType, modelsWithOutput);
2257
2665
  }
2258
2666
  __name(sdkDataType, "sdkDataType");
2259
- function sdkReadExpr(body, modelsWithOutput) {
2667
+ function sdkReadExpr(body, modelsWithOutput, revive) {
2260
2668
  const category = classifyContentType2(body.contentType);
2261
2669
  if (category === "text") return "await result.text()";
2262
2670
  if (category === "binary") return "await result.blob()";
2263
- return `await parseJson<${renderOutputTsType(body.bodyType, modelsWithOutput)}>(result)`;
2671
+ const tsType = renderOutputTsType(body.bodyType, modelsWithOutput);
2672
+ const read = `await parseJson<${tsType}>(result)`;
2673
+ const reviver = reviveExprFor(body.bodyType, revive);
2674
+ if (!reviver) return read;
2675
+ return reviver.kind === "array" ? `(${read}).map(${reviver.name})` : `${reviver.name}(${read})`;
2264
2676
  }
2265
2677
  __name(sdkReadExpr, "sdkReadExpr");
2678
+ function hint(revive, segment) {
2679
+ if (!revive) return void 0;
2680
+ return {
2681
+ ...revive,
2682
+ nameHint: `${revive.nameHint}${segment}`
2683
+ };
2684
+ }
2685
+ __name(hint, "hint");
2686
+ function reviveExprFor(bodyType, ctx) {
2687
+ if (!ctx || ctx.modelsWithDecimal.size === 0) return null;
2688
+ const opts = {
2689
+ modelsWithDecimal: ctx.modelsWithDecimal,
2690
+ modelsWithOutput: ctx.modelsWithOutput,
2691
+ modelMap: ctx.modelMap
2692
+ };
2693
+ if (!typeReachesDecimal(bodyType, opts)) return null;
2694
+ const refName = /* @__PURE__ */ __name((t) => t.kind === "ref" ? t.name : t.kind === "lazy" ? refName(t.inner) : null, "refName");
2695
+ const pick = /* @__PURE__ */ __name((name) => reviveFnName(name, ctx.modelsWithOutput?.has(name) ? "output" : "base"), "pick");
2696
+ const direct = refName(bodyType);
2697
+ if (direct && ctx.modelsWithDecimal.has(direct)) return {
2698
+ name: pick(direct),
2699
+ kind: "value"
2700
+ };
2701
+ if (bodyType.kind === "array") {
2702
+ const item = refName(bodyType.item);
2703
+ if (item && ctx.modelsWithDecimal.has(item)) return {
2704
+ name: pick(item),
2705
+ kind: "array"
2706
+ };
2707
+ }
2708
+ const fnName = `__revive${ctx.nameHint}`;
2709
+ if (!ctx.inlineRevivers.has(fnName)) {
2710
+ const decl = renderInlineReviver(fnName, renderOutputTsType(bodyType, ctx.modelsWithOutput), bodyType, opts);
2711
+ if (!decl) return null;
2712
+ ctx.inlineRevivers.set(fnName, decl);
2713
+ }
2714
+ return {
2715
+ name: fnName,
2716
+ kind: "value"
2717
+ };
2718
+ }
2719
+ __name(reviveExprFor, "reviveExprFor");
2266
2720
  function renderSdkHeadersShape(headers, modelsWithOutput) {
2267
2721
  const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`);
2268
2722
  return `{ ${fields.join("; ")} }`;
@@ -2309,7 +2763,7 @@ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
2309
2763
  ].join("; ")} }`);
2310
2764
  }
2311
2765
  __name(sdkResponseMembers, "sdkResponseMembers");
2312
- function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2766
+ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive) {
2313
2767
  const bodies = resp.bodies;
2314
2768
  const headers = resp.headers ?? [];
2315
2769
  const leading = includeStatus ? [
@@ -2330,7 +2784,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2330
2784
  const fields = [
2331
2785
  ...leading,
2332
2786
  `contentType: '${bodies[0].contentType}'`,
2333
- `data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
2787
+ `data: ${sdkReadExpr(bodies[0], modelsWithOutput, hint(revive, resp.statusCode))}`,
2334
2788
  ...trailing
2335
2789
  ];
2336
2790
  return [
@@ -2343,7 +2797,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2343
2797
  const fields = [
2344
2798
  ...leading,
2345
2799
  `contentType: readContentType(result) as ${cast}`,
2346
- `data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
2800
+ `data: ${sdkReadExpr(bodies[0], modelsWithOutput, hint(revive, resp.statusCode))}`,
2347
2801
  ...trailing
2348
2802
  ];
2349
2803
  return [
@@ -2353,11 +2807,11 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2353
2807
  const lines = [
2354
2808
  `${indent}switch (readContentType(result)) {`
2355
2809
  ];
2356
- for (const body of bodies.slice(1)) {
2810
+ for (const [i, body] of bodies.slice(1).entries()) {
2357
2811
  const fields = [
2358
2812
  ...leading,
2359
2813
  `contentType: '${body.contentType}'`,
2360
- `data: ${sdkReadExpr(body, modelsWithOutput)}`,
2814
+ `data: ${sdkReadExpr(body, modelsWithOutput, hint(revive, `${resp.statusCode}_${i + 1}`))}`,
2361
2815
  ...trailing
2362
2816
  ];
2363
2817
  lines.push(`${indent} case '${body.contentType}':`);
@@ -2367,7 +2821,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2367
2821
  const fallbackFields = [
2368
2822
  ...leading,
2369
2823
  `contentType: '${first.contentType}'`,
2370
- `data: ${sdkReadExpr(first, modelsWithOutput)}`,
2824
+ `data: ${sdkReadExpr(first, modelsWithOutput, hint(revive, `${resp.statusCode}_0`))}`,
2371
2825
  ...trailing
2372
2826
  ];
2373
2827
  lines.push(`${indent} default:`);
@@ -2821,7 +3275,7 @@ function collectTypeNodeRefs2(type, out) {
2821
3275
  }
2822
3276
  }
2823
3277
  __name(collectTypeNodeRefs2, "collectTypeNodeRefs");
2824
- function generateTypeImports2(types, opFile, options) {
3278
+ function generateTypeImports2(types, opFile, options, revivers = []) {
2825
3279
  const lines = [];
2826
3280
  const { modelOutPaths, outPath } = options;
2827
3281
  if (modelOutPaths && outPath) {
@@ -2843,6 +3297,8 @@ function generateTypeImports2(types, opFile, options) {
2843
3297
  rel = rel.replace(/\.ts$/, ".js");
2844
3298
  if (!rel.startsWith(".")) rel = "./" + rel;
2845
3299
  lines.push(`import type { ${names.sort().join(", ")} } from '${rel}';`);
3300
+ const fromHere = revivers.filter((r) => modelOutPaths.get(reviverModelName(r, names)) === typeOutPath);
3301
+ if (fromHere.length > 0) lines.push(`import { ${fromHere.sort().join(", ")} } from '${rel}';`);
2846
3302
  }
2847
3303
  for (const type of unresolved) {
2848
3304
  const moduleName = pascalToDotCase(type);
@@ -2855,6 +3311,13 @@ function generateTypeImports2(types, opFile, options) {
2855
3311
  return lines;
2856
3312
  }
2857
3313
  __name(generateTypeImports2, "generateTypeImports");
3314
+ function reviverModelName(reviver, namesInModule) {
3315
+ const stem = reviver.replace(/^revive/, "");
3316
+ if (namesInModule.includes(stem)) return stem;
3317
+ const base = stem.replace(/Output$/, "");
3318
+ return namesInModule.includes(base) ? base : stem;
3319
+ }
3320
+ __name(reviverModelName, "reviverModelName");
2858
3321
  function deriveTypeImportPath2(file, template) {
2859
3322
  const base = file.split("/").pop()?.replace(/\.(op|ck)$/, "") ?? "resource";
2860
3323
  const module = base.split(".")[0] ?? base;
@@ -2960,6 +3423,7 @@ __name(generateSdkOptions, "generateSdkOptions");
2960
3423
  var SCAFFOLD_DEP_VERSIONS = {
2961
3424
  zod: "^4.3.6",
2962
3425
  luxon: "^3.5.0",
3426
+ decimalJs: "^10.4.3",
2963
3427
  typesLuxon: "^3.4.2",
2964
3428
  typescript: "^6.0.3"
2965
3429
  };
@@ -2967,6 +3431,7 @@ function generateSdkPackageJson(input) {
2967
3431
  const dependencies = {};
2968
3432
  if (input.deps.zod) dependencies.zod = SCAFFOLD_DEP_VERSIONS.zod;
2969
3433
  if (input.deps.luxon) dependencies.luxon = SCAFFOLD_DEP_VERSIONS.luxon;
3434
+ if (input.deps.decimal) dependencies["decimal.js"] = SCAFFOLD_DEP_VERSIONS.decimalJs;
2970
3435
  const devDependencies = {
2971
3436
  typescript: SCAFFOLD_DEP_VERSIONS.typescript
2972
3437
  };
@@ -3026,6 +3491,9 @@ function generateAreaClient(input) {
3026
3491
  const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
3027
3492
  const className = deriveAreaClientClassName(area);
3028
3493
  const collectedMethodLines = [];
3494
+ const collectedRevivePrelude = [];
3495
+ let areaNeedsDecimalImport = false;
3496
+ const reviversByImportPath = /* @__PURE__ */ new Map();
3029
3497
  const collectedErrorAliases = /* @__PURE__ */ new Set();
3030
3498
  const seenMethods = /* @__PURE__ */ new Set();
3031
3499
  const typesByImportPath = /* @__PURE__ */ new Map();
@@ -3037,7 +3505,9 @@ function generateAreaClient(input) {
3037
3505
  let needsReadContentType = false;
3038
3506
  for (const inline of inlineFiles) {
3039
3507
  const includeInternal = inline.codegenOptions.includeInternal ?? false;
3040
- const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
3508
+ const { lines: methodLines, methodNames, preludeLines, needsDecimalImport } = generateClientMethods(inline.root, inline.codegenOptions);
3509
+ collectedRevivePrelude.push(...preludeLines);
3510
+ if (needsDecimalImport) areaNeedsDecimalImport = true;
3041
3511
  for (const name of methodNames) {
3042
3512
  if (seenMethods.has(name)) {
3043
3513
  throw new Error(`[sdk] duplicate method '${name}' in area '${area}': two area-level files contribute the same method. Disambiguate via 'sdk:' or move one into a subarea.`);
@@ -3051,6 +3521,16 @@ function generateAreaClient(input) {
3051
3521
  if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
3052
3522
  if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
3053
3523
  if (sdkNeedsReadContentType(inline.root, includeInternal)) needsReadContentType = true;
3524
+ for (const reviver of usedRevivers(methodLines)) {
3525
+ const stem = reviver.replace(/^revive/, "");
3526
+ const modelOut = inline.codegenOptions.modelOutPaths?.get(stem) ?? inline.codegenOptions.modelOutPaths?.get(stem.replace(/Output$/, ""));
3527
+ if (!modelOut) continue;
3528
+ let rel = relative3(dirname3(outPath), modelOut).replace(/\.ts$/, ".js");
3529
+ if (!rel.startsWith(".")) rel = "./" + rel;
3530
+ const set = reviversByImportPath.get(rel) ?? /* @__PURE__ */ new Set();
3531
+ set.add(reviver);
3532
+ reviversByImportPath.set(rel, set);
3533
+ }
3054
3534
  const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
3055
3535
  const { modelOutPaths } = inline.codegenOptions;
3056
3536
  if (modelOutPaths) {
@@ -3089,12 +3569,17 @@ function generateAreaClient(input) {
3089
3569
  ...typesByImportPath.get(path)
3090
3570
  ].sort();
3091
3571
  lines.push(`import type { ${names.join(", ")} } from '${path}';`);
3572
+ const revivers = reviversByImportPath.get(path);
3573
+ if (revivers && revivers.size > 0) lines.push(`import { ${[
3574
+ ...revivers
3575
+ ].sort().join(", ")} } from '${path}';`);
3092
3576
  }
3093
3577
  for (const t of [
3094
3578
  ...unresolvedTypes
3095
3579
  ].sort()) {
3096
3580
  lines.push(`import type { ${t} } from './${pascalToDotCase(t)}.js';`);
3097
3581
  }
3582
+ if (areaNeedsDecimalImport) lines.push(DECIMAL_IMPORT);
3098
3583
  const importedClients = /* @__PURE__ */ new Set();
3099
3584
  for (const sc of subareaClients) {
3100
3585
  const key = `${sc.client.className}|${sc.client.importPath}`;
@@ -3107,6 +3592,10 @@ function generateAreaClient(input) {
3107
3592
  lines.push(...collectedErrorAliases);
3108
3593
  lines.push("");
3109
3594
  }
3595
+ if (collectedRevivePrelude.length > 0) {
3596
+ lines.push(...collectedRevivePrelude);
3597
+ lines.push("");
3598
+ }
3110
3599
  lines.push(`export class ${className} {`);
3111
3600
  for (const sc of subareaClients) {
3112
3601
  lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
@@ -3193,9 +3682,14 @@ function generatePlainTypes(root, context) {
3193
3682
  ...externalOutputRefs
3194
3683
  ])
3195
3684
  ].sort();
3685
+ const needsDecimal = rootNeedsScalar(root, "decimal") || (context?.emitRevivers && context.modelsWithDecimal ? root.models.some((m) => context.modelsWithDecimal.has(m.name)) : false);
3686
+ if (needsDecimal) lines.push(DECIMAL_IMPORT);
3196
3687
  for (const ref of allExternalRefs) {
3197
3688
  const importPath = resolveImportPath(ref, context);
3198
3689
  lines.push(`import type { ${ref} } from '${importPath}';`);
3690
+ if (context?.emitRevivers && context.modelsWithDecimal?.has(ref)) {
3691
+ lines.push(`import { ${reviveFnName(ref)} } from '${importPath}';`);
3692
+ }
3199
3693
  }
3200
3694
  if (allExternalRefs.length > 0) lines.push("");
3201
3695
  if (rootNeedsScalar(root, "json")) {
@@ -3210,10 +3704,32 @@ function generatePlainTypes(root, context) {
3210
3704
  m.name,
3211
3705
  m
3212
3706
  ]));
3707
+ const reviveOpts = context?.emitRevivers && context.modelsWithDecimal ? {
3708
+ modelsWithDecimal: context.modelsWithDecimal,
3709
+ modelsWithOutput: allModelsWithOutput,
3710
+ modelMap
3711
+ } : void 0;
3712
+ const bodyLines = [];
3213
3713
  for (const model of topoSortModels(root.models)) {
3214
- lines.push(...generateModel2(model, target, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
3714
+ bodyLines.push(...generateModel2(model, target, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
3715
+ if (reviveOpts) {
3716
+ const revivers = renderReviveFunctions(model, reviveOpts);
3717
+ if (revivers.length > 0) {
3718
+ bodyLines.push("");
3719
+ bodyLines.push(...revivers);
3720
+ }
3721
+ }
3722
+ bodyLines.push("");
3723
+ }
3724
+ if (needsDecimal) {
3725
+ lines.push("");
3726
+ lines.push(DECIMAL_CONFIG_LINE);
3727
+ }
3728
+ if (bodyLines.some((l) => l.includes("__dec("))) {
3729
+ lines.push(...DECIMAL_COERCE_DECL);
3215
3730
  lines.push("");
3216
3731
  }
3732
+ lines.push(...bodyLines);
3217
3733
  return lines.join("\n");
3218
3734
  }
3219
3735
  __name(generatePlainTypes, "generatePlainTypes");
@@ -3652,6 +4168,9 @@ function scalarHelperLines(body) {
3652
4168
  if (body.includes("_ZodDatetime")) {
3653
4169
  lines.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
3654
4170
  }
4171
+ if (body.includes("_ZodDecimal")) {
4172
+ lines.push(...DECIMAL_PRELUDE_LINES);
4173
+ }
3655
4174
  if (body.includes("_ZodInterval")) {
3656
4175
  lines.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
3657
4176
  }
@@ -3765,6 +4284,7 @@ function generateMcpFile(root, options = {}) {
3765
4284
  if (/\bInterval\b/.test(bodyWithHelpers)) luxon.push("Interval");
3766
4285
  if (/\bDuration\b/.test(bodyWithHelpers)) luxon.push("Duration");
3767
4286
  if (luxon.length > 0) imports.push(`import { ${luxon.join(", ")} } from 'luxon';`);
4287
+ if (/\bDecimal\b/.test(bodyWithHelpers)) imports.push(DECIMAL_IMPORT);
3768
4288
  imports.push(`import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';`);
3769
4289
  imports.push(`import type { McpToolHandler, McpToolHandlerMap, McpToolContext } from '@maroonedsoftware/mcp';`);
3770
4290
  if (needsParseAndValidate) imports.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
@@ -4061,7 +4581,14 @@ function createTypescriptPlugin(config, rootDir) {
4061
4581
  };
4062
4582
  }
4063
4583
  __name(createTypescriptPlugin, "createTypescriptPlugin");
4584
+ function assertValidConfig(config) {
4585
+ if (config.server?.validateResponses && !config.server.zod) {
4586
+ throw new Error("plugin-typescript: server.validateResponses requires server.zod: true \u2014 without it output.types emits plain TypeScript interfaces, which are types with no runtime schema value for the router to validate against.");
4587
+ }
4588
+ }
4589
+ __name(assertValidConfig, "assertValidConfig");
4064
4590
  async function runTypescriptCodegen(inputs, ctx, config, rootDir) {
4591
+ assertValidConfig(config);
4065
4592
  const manifestPath = resolve2(ctx.cacheDir, CACHE_MANIFEST_FILENAME);
4066
4593
  const prevManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
4067
4594
  const units = [];
@@ -4178,6 +4705,7 @@ function collectServerOutput(config, rootDir, inputs, units) {
4178
4705
  const serverBase = resolve2(rootDir, config.baseDir ?? ".");
4179
4706
  const modelsWithInput = inputs.modelsWithInput;
4180
4707
  const modelsWithOutput = inputs.modelsWithOutput;
4708
+ const modelsWithTransform = computeModelsWithCaseTransform(inputs.contractRoots.flatMap((r) => r.models));
4181
4709
  const modelMap = buildModelMap(inputs.contractRoots);
4182
4710
  const allFiles = [
4183
4711
  ...inputs.contractRoots.map((r) => r.file),
@@ -4250,6 +4778,10 @@ function collectServerOutput(config, rootDir, inputs, units) {
4250
4778
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4251
4779
  servicePathTemplate: config.servicePathTemplate ?? null,
4252
4780
  includeInternal: config.includeInternal ?? true,
4781
+ // Not covered by `sub`: adding `format(input=snake)` to a *different* .ck file changes
4782
+ // this router's output with no change to `root` or the config.
4783
+ modelsWithTransform: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithTransform),
4784
+ validateResponses: config.validateResponses ?? false,
4253
4785
  sub: subConfigKey
4254
4786
  });
4255
4787
  units.push({
@@ -4264,7 +4796,9 @@ function collectServerOutput(config, rootDir, inputs, units) {
4264
4796
  modelOutPaths: serverModelOutPaths,
4265
4797
  modelsWithInput,
4266
4798
  modelsWithOutput,
4267
- includeInternal: config.includeInternal
4799
+ modelsWithTransform,
4800
+ includeInternal: config.includeInternal,
4801
+ validateResponses: config.validateResponses
4268
4802
  })
4269
4803
  }
4270
4804
  ], "render")
@@ -4283,6 +4817,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4283
4817
  const subConfigKey = stableSubConfig(config);
4284
4818
  const modelsWithInput = inputs.modelsWithInput;
4285
4819
  const modelsWithOutput = inputs.modelsWithOutput;
4820
+ const modelsWithDecimal = computeModelsWithDecimal(inputs.contractRoots.flatMap((r) => r.models));
4286
4821
  const modelMap = buildModelMap(inputs.contractRoots);
4287
4822
  const allFiles = [
4288
4823
  ...inputs.contractRoots.map((r) => r.file),
@@ -4322,6 +4857,9 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4322
4857
  outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4323
4858
  modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
4324
4859
  modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput),
4860
+ // Not covered by `root`: adding a decimal to a model in a *different* .ck file changes
4861
+ // this file's revivers with no change to `root` or the config.
4862
+ modelsWithDecimal: sliceModelSet(refs, ownNames, modelsWithDecimal),
4325
4863
  sdkOptionsPath,
4326
4864
  sub: subConfigKey
4327
4865
  });
@@ -4335,7 +4873,9 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4335
4873
  modelOutPaths: sdkModelOutPaths,
4336
4874
  currentOutPath: typeOutPath,
4337
4875
  modelsWithInput,
4338
- modelsWithOutput
4876
+ modelsWithOutput,
4877
+ modelsWithDecimal,
4878
+ emitRevivers: true
4339
4879
  });
4340
4880
  } else {
4341
4881
  let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
@@ -4345,6 +4885,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4345
4885
  currentOutPath: typeOutPath,
4346
4886
  modelsWithInput,
4347
4887
  modelsWithOutput,
4888
+ modelsWithDecimal,
4889
+ emitRevivers: true,
4348
4890
  jsonValueImportPath: rel
4349
4891
  });
4350
4892
  }
@@ -4406,6 +4948,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4406
4948
  outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4407
4949
  modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4408
4950
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4951
+ modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
4409
4952
  sdkOptionsPath,
4410
4953
  className,
4411
4954
  includeInternal: config.includeInternal ?? false,
@@ -4424,6 +4967,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4424
4967
  sdkOptionsPath,
4425
4968
  modelsWithInput,
4426
4969
  modelsWithOutput,
4970
+ modelsWithDecimal,
4971
+ modelMap,
4427
4972
  includeInternal: config.includeInternal,
4428
4973
  clientClassName: className
4429
4974
  })
@@ -4448,6 +4993,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4448
4993
  outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4449
4994
  modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4450
4995
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4996
+ modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
4451
4997
  sdkOptionsPath,
4452
4998
  includeInternal: config.includeInternal ?? false,
4453
4999
  sub: subConfigKey
@@ -4465,6 +5011,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4465
5011
  sdkOptionsPath,
4466
5012
  modelsWithInput,
4467
5013
  modelsWithOutput,
5014
+ modelsWithDecimal,
5015
+ modelMap,
4468
5016
  includeInternal: config.includeInternal
4469
5017
  })
4470
5018
  }
@@ -4533,6 +5081,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4533
5081
  outPathSlice: sliceOutPathMap(allInlineRefs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4534
5082
  modelsWithInput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithInput),
4535
5083
  modelsWithOutput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithOutput),
5084
+ modelsWithDecimal: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithDecimal),
4536
5085
  sdkOptionsPath,
4537
5086
  includeInternal: config.includeInternal ?? false,
4538
5087
  sub: subConfigKey
@@ -4546,6 +5095,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4546
5095
  sdkOptionsPath,
4547
5096
  modelsWithInput,
4548
5097
  modelsWithOutput,
5098
+ modelsWithDecimal,
5099
+ modelMap,
4549
5100
  includeInternal: config.includeInternal
4550
5101
  }
4551
5102
  }));
@@ -4614,7 +5165,17 @@ ${rootExports.sort().join("\n")}
4614
5165
  const coveredRoots = sdkContractEntries.map((e) => e.ast);
4615
5166
  const deps = {
4616
5167
  zod: !!config.zod,
4617
- luxon: coveredRoots.some((r) => rootNeedsScalar(r, "datetime") || rootNeedsScalar(r, "date") || rootNeedsScalar(r, "time") || rootNeedsScalar(r, "interval"))
5168
+ // `duration` belongs here too: `generateContract` imports `Duration` from luxon for it,
5169
+ // so a contract whose only temporal scalar is a duration used to scaffold a package.json
5170
+ // with no luxon dependency and fail to compile.
5171
+ luxon: coveredRoots.some((r) => [
5172
+ "datetime",
5173
+ "date",
5174
+ "time",
5175
+ "duration",
5176
+ "interval"
5177
+ ].some((name) => rootNeedsScalar(r, name))),
5178
+ decimal: coveredRoots.some((r) => rootNeedsScalar(r, "decimal"))
4618
5179
  };
4619
5180
  globalFiles.push({
4620
5181
  relativePath: join2(sdkBase, "package.json"),