@contractkit/plugin-typescript 0.32.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }
@@ -1251,6 +1537,9 @@ function generateOp(root, options = {}) {
1251
1537
  if (luxonImports.length > 0) {
1252
1538
  body.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
1253
1539
  }
1540
+ if (uses("Decimal")) {
1541
+ body.push(DECIMAL_IMPORT);
1542
+ }
1254
1543
  if (uses("parseAndValidate")) {
1255
1544
  body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
1256
1545
  }
@@ -1571,6 +1860,8 @@ function serverTsScalar(name) {
1571
1860
  return "number";
1572
1861
  case "bigint":
1573
1862
  return "bigint";
1863
+ case "decimal":
1864
+ return "Decimal";
1574
1865
  case "boolean":
1575
1866
  return "boolean";
1576
1867
  case "date":
@@ -1939,7 +2230,7 @@ function deriveTypeImportPath(file, template) {
1939
2230
  __name(deriveTypeImportPath, "deriveTypeImportPath");
1940
2231
 
1941
2232
  // src/index.ts
1942
- import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform } from "@contractkit/core";
2233
+ import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform, computeModelsWithDecimal } from "@contractkit/core";
1943
2234
 
1944
2235
  // src/codegen-sdk.ts
1945
2236
  import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
@@ -2006,9 +2297,25 @@ function generateSdk(root, options = {}) {
2006
2297
  const includeInternal = options.includeInternal ?? false;
2007
2298
  const types = collectTypes2(root, options.modelsWithInput, options.modelsWithOutput, includeInternal);
2008
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);
2009
2315
  if (types.length > 0) {
2010
- lines.push(...generateTypeImports2(types, root.file, options));
2316
+ lines.push(...generateTypeImports2(types, root.file, options, usedRevivers(classBody)));
2011
2317
  }
2318
+ lines.push(...decimalPrelude.imports);
2012
2319
  if (options.sdkOptionsPath && options.outPath) {
2013
2320
  let rel = relative3(dirname3(options.outPath), options.sdkOptionsPath);
2014
2321
  rel = rel.replace(/\.ts$/, ".js");
@@ -2106,21 +2413,21 @@ function generateSdk(root, options = {}) {
2106
2413
  lines.push(...errorAliases);
2107
2414
  lines.push("");
2108
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
+ }
2109
2424
  lines.push("/**");
2110
2425
  const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
2111
2426
  lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
2112
2427
  lines.push(" */");
2113
2428
  lines.push(`export class ${clientClassName} {`);
2114
2429
  lines.push(" constructor(private fetch: SdkFetch) {}");
2115
- for (const route of root.routes) {
2116
- for (const op of route.operations) {
2117
- const mods = resolveModifiers2(route, op);
2118
- if (!includeInternal && mods.includes("internal")) continue;
2119
- lines.push("");
2120
- if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
2121
- lines.push(...generateMethod(route, op, root.file, options));
2122
- }
2123
- }
2430
+ lines.push(...classBody);
2124
2431
  lines.push("}");
2125
2432
  lines.push("");
2126
2433
  return lines.join("\n");
@@ -2130,25 +2437,77 @@ function generateClientMethods(root, options) {
2130
2437
  const lines = [];
2131
2438
  const methodNames = [];
2132
2439
  const includeInternal = options.includeInternal ?? false;
2440
+ const inlineRevivers = /* @__PURE__ */ new Map();
2133
2441
  for (const route of root.routes) {
2134
2442
  for (const op of route.operations) {
2135
2443
  const mods = resolveModifiers2(route, op);
2136
2444
  if (!includeInternal && mods.includes("internal")) continue;
2137
2445
  lines.push("");
2138
2446
  if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
2139
- lines.push(...generateMethod(route, op, root.file, options));
2447
+ lines.push(...generateMethod(route, op, root.file, options, inlineRevivers));
2140
2448
  methodNames.push(deriveMethodName(op, route));
2141
2449
  }
2142
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
+ ];
2143
2467
  return {
2144
2468
  lines,
2145
- methodNames
2469
+ methodNames,
2470
+ preludeLines,
2471
+ needsDecimalImport: decls.length > 0
2146
2472
  };
2147
2473
  }
2148
2474
  __name(generateClientMethods, "generateClientMethods");
2149
- 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;
2150
2508
  const lines = [];
2151
2509
  const methodName = deriveMethodName(op, route);
2510
+ const mRevive = hint(revive, `${methodName.charAt(0).toUpperCase()}${methodName.slice(1)}`);
2152
2511
  const httpMethod = op.method.toUpperCase();
2153
2512
  const { modelsWithInput, modelsWithOutput } = options;
2154
2513
  const params = buildMethodParams(route, op, modelsWithInput);
@@ -2276,23 +2635,23 @@ function generateMethod(route, op, file, options) {
2276
2635
  lines.push(` switch (result.status) {`);
2277
2636
  for (const resp of rest) {
2278
2637
  lines.push(` case ${resp.statusCode}:`);
2279
- lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true));
2638
+ lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true, mRevive));
2280
2639
  }
2281
2640
  lines.push(` default:`);
2282
- lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true));
2641
+ lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true, mRevive));
2283
2642
  lines.push(` }`);
2284
2643
  } else if (primaryBodies.length > 1) {
2285
- lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false));
2644
+ lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false, mRevive));
2286
2645
  } else if (hasRespHeaders) {
2287
2646
  const headerEntries = sdkHeaderEntries(respHeaders);
2288
2647
  if (isVoid) {
2289
2648
  lines.push(` return { headers: { ${headerEntries} } };`);
2290
2649
  } else {
2291
- lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
2650
+ lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput, hint(mRevive, primaryResponse.statusCode))};`);
2292
2651
  lines.push(` return { data, headers: { ${headerEntries} } };`);
2293
2652
  }
2294
2653
  } else if (!isVoid) {
2295
- lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
2654
+ lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput, hint(mRevive, primaryResponse.statusCode))};`);
2296
2655
  }
2297
2656
  lines.push(" }");
2298
2657
  return lines;
@@ -2305,13 +2664,59 @@ function sdkDataType(body, modelsWithOutput) {
2305
2664
  return renderOutputTsType(body.bodyType, modelsWithOutput);
2306
2665
  }
2307
2666
  __name(sdkDataType, "sdkDataType");
2308
- function sdkReadExpr(body, modelsWithOutput) {
2667
+ function sdkReadExpr(body, modelsWithOutput, revive) {
2309
2668
  const category = classifyContentType2(body.contentType);
2310
2669
  if (category === "text") return "await result.text()";
2311
2670
  if (category === "binary") return "await result.blob()";
2312
- 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})`;
2313
2676
  }
2314
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");
2315
2720
  function renderSdkHeadersShape(headers, modelsWithOutput) {
2316
2721
  const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`);
2317
2722
  return `{ ${fields.join("; ")} }`;
@@ -2358,7 +2763,7 @@ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
2358
2763
  ].join("; ")} }`);
2359
2764
  }
2360
2765
  __name(sdkResponseMembers, "sdkResponseMembers");
2361
- function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2766
+ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive) {
2362
2767
  const bodies = resp.bodies;
2363
2768
  const headers = resp.headers ?? [];
2364
2769
  const leading = includeStatus ? [
@@ -2379,7 +2784,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2379
2784
  const fields = [
2380
2785
  ...leading,
2381
2786
  `contentType: '${bodies[0].contentType}'`,
2382
- `data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
2787
+ `data: ${sdkReadExpr(bodies[0], modelsWithOutput, hint(revive, resp.statusCode))}`,
2383
2788
  ...trailing
2384
2789
  ];
2385
2790
  return [
@@ -2392,7 +2797,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2392
2797
  const fields = [
2393
2798
  ...leading,
2394
2799
  `contentType: readContentType(result) as ${cast}`,
2395
- `data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
2800
+ `data: ${sdkReadExpr(bodies[0], modelsWithOutput, hint(revive, resp.statusCode))}`,
2396
2801
  ...trailing
2397
2802
  ];
2398
2803
  return [
@@ -2402,11 +2807,11 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2402
2807
  const lines = [
2403
2808
  `${indent}switch (readContentType(result)) {`
2404
2809
  ];
2405
- for (const body of bodies.slice(1)) {
2810
+ for (const [i, body] of bodies.slice(1).entries()) {
2406
2811
  const fields = [
2407
2812
  ...leading,
2408
2813
  `contentType: '${body.contentType}'`,
2409
- `data: ${sdkReadExpr(body, modelsWithOutput)}`,
2814
+ `data: ${sdkReadExpr(body, modelsWithOutput, hint(revive, `${resp.statusCode}_${i + 1}`))}`,
2410
2815
  ...trailing
2411
2816
  ];
2412
2817
  lines.push(`${indent} case '${body.contentType}':`);
@@ -2416,7 +2821,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2416
2821
  const fallbackFields = [
2417
2822
  ...leading,
2418
2823
  `contentType: '${first.contentType}'`,
2419
- `data: ${sdkReadExpr(first, modelsWithOutput)}`,
2824
+ `data: ${sdkReadExpr(first, modelsWithOutput, hint(revive, `${resp.statusCode}_0`))}`,
2420
2825
  ...trailing
2421
2826
  ];
2422
2827
  lines.push(`${indent} default:`);
@@ -2870,7 +3275,7 @@ function collectTypeNodeRefs2(type, out) {
2870
3275
  }
2871
3276
  }
2872
3277
  __name(collectTypeNodeRefs2, "collectTypeNodeRefs");
2873
- function generateTypeImports2(types, opFile, options) {
3278
+ function generateTypeImports2(types, opFile, options, revivers = []) {
2874
3279
  const lines = [];
2875
3280
  const { modelOutPaths, outPath } = options;
2876
3281
  if (modelOutPaths && outPath) {
@@ -2892,6 +3297,8 @@ function generateTypeImports2(types, opFile, options) {
2892
3297
  rel = rel.replace(/\.ts$/, ".js");
2893
3298
  if (!rel.startsWith(".")) rel = "./" + rel;
2894
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}';`);
2895
3302
  }
2896
3303
  for (const type of unresolved) {
2897
3304
  const moduleName = pascalToDotCase(type);
@@ -2904,6 +3311,13 @@ function generateTypeImports2(types, opFile, options) {
2904
3311
  return lines;
2905
3312
  }
2906
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");
2907
3321
  function deriveTypeImportPath2(file, template) {
2908
3322
  const base = file.split("/").pop()?.replace(/\.(op|ck)$/, "") ?? "resource";
2909
3323
  const module = base.split(".")[0] ?? base;
@@ -3009,6 +3423,7 @@ __name(generateSdkOptions, "generateSdkOptions");
3009
3423
  var SCAFFOLD_DEP_VERSIONS = {
3010
3424
  zod: "^4.3.6",
3011
3425
  luxon: "^3.5.0",
3426
+ decimalJs: "^10.4.3",
3012
3427
  typesLuxon: "^3.4.2",
3013
3428
  typescript: "^6.0.3"
3014
3429
  };
@@ -3016,6 +3431,7 @@ function generateSdkPackageJson(input) {
3016
3431
  const dependencies = {};
3017
3432
  if (input.deps.zod) dependencies.zod = SCAFFOLD_DEP_VERSIONS.zod;
3018
3433
  if (input.deps.luxon) dependencies.luxon = SCAFFOLD_DEP_VERSIONS.luxon;
3434
+ if (input.deps.decimal) dependencies["decimal.js"] = SCAFFOLD_DEP_VERSIONS.decimalJs;
3019
3435
  const devDependencies = {
3020
3436
  typescript: SCAFFOLD_DEP_VERSIONS.typescript
3021
3437
  };
@@ -3075,6 +3491,9 @@ function generateAreaClient(input) {
3075
3491
  const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
3076
3492
  const className = deriveAreaClientClassName(area);
3077
3493
  const collectedMethodLines = [];
3494
+ const collectedRevivePrelude = [];
3495
+ let areaNeedsDecimalImport = false;
3496
+ const reviversByImportPath = /* @__PURE__ */ new Map();
3078
3497
  const collectedErrorAliases = /* @__PURE__ */ new Set();
3079
3498
  const seenMethods = /* @__PURE__ */ new Set();
3080
3499
  const typesByImportPath = /* @__PURE__ */ new Map();
@@ -3086,7 +3505,9 @@ function generateAreaClient(input) {
3086
3505
  let needsReadContentType = false;
3087
3506
  for (const inline of inlineFiles) {
3088
3507
  const includeInternal = inline.codegenOptions.includeInternal ?? false;
3089
- 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;
3090
3511
  for (const name of methodNames) {
3091
3512
  if (seenMethods.has(name)) {
3092
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.`);
@@ -3100,6 +3521,16 @@ function generateAreaClient(input) {
3100
3521
  if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
3101
3522
  if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
3102
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
+ }
3103
3534
  const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
3104
3535
  const { modelOutPaths } = inline.codegenOptions;
3105
3536
  if (modelOutPaths) {
@@ -3138,12 +3569,17 @@ function generateAreaClient(input) {
3138
3569
  ...typesByImportPath.get(path)
3139
3570
  ].sort();
3140
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}';`);
3141
3576
  }
3142
3577
  for (const t of [
3143
3578
  ...unresolvedTypes
3144
3579
  ].sort()) {
3145
3580
  lines.push(`import type { ${t} } from './${pascalToDotCase(t)}.js';`);
3146
3581
  }
3582
+ if (areaNeedsDecimalImport) lines.push(DECIMAL_IMPORT);
3147
3583
  const importedClients = /* @__PURE__ */ new Set();
3148
3584
  for (const sc of subareaClients) {
3149
3585
  const key = `${sc.client.className}|${sc.client.importPath}`;
@@ -3156,6 +3592,10 @@ function generateAreaClient(input) {
3156
3592
  lines.push(...collectedErrorAliases);
3157
3593
  lines.push("");
3158
3594
  }
3595
+ if (collectedRevivePrelude.length > 0) {
3596
+ lines.push(...collectedRevivePrelude);
3597
+ lines.push("");
3598
+ }
3159
3599
  lines.push(`export class ${className} {`);
3160
3600
  for (const sc of subareaClients) {
3161
3601
  lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
@@ -3242,9 +3682,14 @@ function generatePlainTypes(root, context) {
3242
3682
  ...externalOutputRefs
3243
3683
  ])
3244
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);
3245
3687
  for (const ref of allExternalRefs) {
3246
3688
  const importPath = resolveImportPath(ref, context);
3247
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
+ }
3248
3693
  }
3249
3694
  if (allExternalRefs.length > 0) lines.push("");
3250
3695
  if (rootNeedsScalar(root, "json")) {
@@ -3259,10 +3704,32 @@ function generatePlainTypes(root, context) {
3259
3704
  m.name,
3260
3705
  m
3261
3706
  ]));
3707
+ const reviveOpts = context?.emitRevivers && context.modelsWithDecimal ? {
3708
+ modelsWithDecimal: context.modelsWithDecimal,
3709
+ modelsWithOutput: allModelsWithOutput,
3710
+ modelMap
3711
+ } : void 0;
3712
+ const bodyLines = [];
3262
3713
  for (const model of topoSortModels(root.models)) {
3263
- 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);
3264
3730
  lines.push("");
3265
3731
  }
3732
+ lines.push(...bodyLines);
3266
3733
  return lines.join("\n");
3267
3734
  }
3268
3735
  __name(generatePlainTypes, "generatePlainTypes");
@@ -3701,6 +4168,9 @@ function scalarHelperLines(body) {
3701
4168
  if (body.includes("_ZodDatetime")) {
3702
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' }));`);
3703
4170
  }
4171
+ if (body.includes("_ZodDecimal")) {
4172
+ lines.push(...DECIMAL_PRELUDE_LINES);
4173
+ }
3704
4174
  if (body.includes("_ZodInterval")) {
3705
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()!);`);
3706
4176
  }
@@ -3814,6 +4284,7 @@ function generateMcpFile(root, options = {}) {
3814
4284
  if (/\bInterval\b/.test(bodyWithHelpers)) luxon.push("Interval");
3815
4285
  if (/\bDuration\b/.test(bodyWithHelpers)) luxon.push("Duration");
3816
4286
  if (luxon.length > 0) imports.push(`import { ${luxon.join(", ")} } from 'luxon';`);
4287
+ if (/\bDecimal\b/.test(bodyWithHelpers)) imports.push(DECIMAL_IMPORT);
3817
4288
  imports.push(`import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';`);
3818
4289
  imports.push(`import type { McpToolHandler, McpToolHandlerMap, McpToolContext } from '@maroonedsoftware/mcp';`);
3819
4290
  if (needsParseAndValidate) imports.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
@@ -4346,6 +4817,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4346
4817
  const subConfigKey = stableSubConfig(config);
4347
4818
  const modelsWithInput = inputs.modelsWithInput;
4348
4819
  const modelsWithOutput = inputs.modelsWithOutput;
4820
+ const modelsWithDecimal = computeModelsWithDecimal(inputs.contractRoots.flatMap((r) => r.models));
4349
4821
  const modelMap = buildModelMap(inputs.contractRoots);
4350
4822
  const allFiles = [
4351
4823
  ...inputs.contractRoots.map((r) => r.file),
@@ -4385,6 +4857,9 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4385
4857
  outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4386
4858
  modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
4387
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),
4388
4863
  sdkOptionsPath,
4389
4864
  sub: subConfigKey
4390
4865
  });
@@ -4398,7 +4873,9 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4398
4873
  modelOutPaths: sdkModelOutPaths,
4399
4874
  currentOutPath: typeOutPath,
4400
4875
  modelsWithInput,
4401
- modelsWithOutput
4876
+ modelsWithOutput,
4877
+ modelsWithDecimal,
4878
+ emitRevivers: true
4402
4879
  });
4403
4880
  } else {
4404
4881
  let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
@@ -4408,6 +4885,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4408
4885
  currentOutPath: typeOutPath,
4409
4886
  modelsWithInput,
4410
4887
  modelsWithOutput,
4888
+ modelsWithDecimal,
4889
+ emitRevivers: true,
4411
4890
  jsonValueImportPath: rel
4412
4891
  });
4413
4892
  }
@@ -4469,6 +4948,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4469
4948
  outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4470
4949
  modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4471
4950
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4951
+ modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
4472
4952
  sdkOptionsPath,
4473
4953
  className,
4474
4954
  includeInternal: config.includeInternal ?? false,
@@ -4487,6 +4967,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4487
4967
  sdkOptionsPath,
4488
4968
  modelsWithInput,
4489
4969
  modelsWithOutput,
4970
+ modelsWithDecimal,
4971
+ modelMap,
4490
4972
  includeInternal: config.includeInternal,
4491
4973
  clientClassName: className
4492
4974
  })
@@ -4511,6 +4993,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4511
4993
  outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4512
4994
  modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4513
4995
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4996
+ modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
4514
4997
  sdkOptionsPath,
4515
4998
  includeInternal: config.includeInternal ?? false,
4516
4999
  sub: subConfigKey
@@ -4528,6 +5011,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4528
5011
  sdkOptionsPath,
4529
5012
  modelsWithInput,
4530
5013
  modelsWithOutput,
5014
+ modelsWithDecimal,
5015
+ modelMap,
4531
5016
  includeInternal: config.includeInternal
4532
5017
  })
4533
5018
  }
@@ -4596,6 +5081,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4596
5081
  outPathSlice: sliceOutPathMap(allInlineRefs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
4597
5082
  modelsWithInput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithInput),
4598
5083
  modelsWithOutput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithOutput),
5084
+ modelsWithDecimal: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithDecimal),
4599
5085
  sdkOptionsPath,
4600
5086
  includeInternal: config.includeInternal ?? false,
4601
5087
  sub: subConfigKey
@@ -4609,6 +5095,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4609
5095
  sdkOptionsPath,
4610
5096
  modelsWithInput,
4611
5097
  modelsWithOutput,
5098
+ modelsWithDecimal,
5099
+ modelMap,
4612
5100
  includeInternal: config.includeInternal
4613
5101
  }
4614
5102
  }));
@@ -4677,7 +5165,17 @@ ${rootExports.sort().join("\n")}
4677
5165
  const coveredRoots = sdkContractEntries.map((e) => e.ast);
4678
5166
  const deps = {
4679
5167
  zod: !!config.zod,
4680
- 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"))
4681
5179
  };
4682
5180
  globalFiles.push({
4683
5181
  relativePath: join2(sdkBase, "package.json"),