@contractkit/plugin-typescript 0.33.3 → 0.34.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.
Files changed (40) hide show
  1. package/.turbo/turbo-build$colon$ci.log +4 -4
  2. package/.turbo/turbo-test$colon$ci.log +26 -25
  3. package/CHANGELOG.md +285 -0
  4. package/dist/codegen-contract.d.ts +22 -6
  5. package/dist/codegen-contract.d.ts.map +1 -1
  6. package/dist/codegen-mcp.d.ts.map +1 -1
  7. package/dist/codegen-operation.d.ts.map +1 -1
  8. package/dist/codegen-plain-types.d.ts.map +1 -1
  9. package/dist/codegen-revive.d.ts +38 -4
  10. package/dist/codegen-revive.d.ts.map +1 -1
  11. package/dist/codegen-sdk.d.ts +2 -0
  12. package/dist/codegen-sdk.d.ts.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +421 -202
  16. package/dist/index.js.map +1 -1
  17. package/dist/path-utils.d.ts +9 -0
  18. package/dist/path-utils.d.ts.map +1 -1
  19. package/dist/ts-render.d.ts +14 -0
  20. package/dist/ts-render.d.ts.map +1 -1
  21. package/llms.txt +14 -0
  22. package/package.json +3 -2
  23. package/src/codegen-contract.ts +84 -68
  24. package/src/codegen-mcp.ts +22 -11
  25. package/src/codegen-operation.ts +32 -19
  26. package/src/codegen-plain-types.ts +15 -8
  27. package/src/codegen-revive.ts +140 -20
  28. package/src/codegen-sdk.ts +322 -63
  29. package/src/index.ts +46 -4
  30. package/src/path-utils.ts +10 -0
  31. package/src/ts-render.ts +26 -0
  32. package/tests/codegen-contract.test.ts +87 -32
  33. package/tests/codegen-mcp.test.ts +23 -1
  34. package/tests/codegen-operation.test.ts +68 -8
  35. package/tests/codegen-plain-types.test.ts +13 -5
  36. package/tests/codegen-sdk.test.ts +265 -10
  37. package/tests/codegen-server.test.ts +43 -1
  38. package/tests/helpers.ts +7 -2
  39. package/tests/pipeline.test.ts +51 -4
  40. package/tests/ts-render.test.ts +29 -0
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ import { resolve as resolve2, join as join2, relative as relative7, dirname as d
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, rmdirSync } from "fs";
7
7
 
8
8
  // src/codegen-contract.ts
9
- import { relative, dirname } from "path";
9
+ import { relative as relative2, dirname as dirname2 } from "path";
10
10
  import { collectTypeRefs, computeModelsWithOutput as ckComputeModelsWithOutput, collectExternalOutputRefs as ckCollectExternalOutputRefs } from "@contractkit/core";
11
11
 
12
12
  // src/ts-render.ts
13
+ import { dirname, relative } from "path";
13
14
  var JSON_VALUE_TYPE_DECL = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
14
15
  function quoteKey(name) {
15
16
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
@@ -23,6 +24,12 @@ function escapeSingleQuoted(s) {
23
24
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
24
25
  }
25
26
  __name(escapeSingleQuoted, "escapeSingleQuoted");
27
+ function sourceLink(label, outPath, sourceFile, line) {
28
+ const rel = outPath ? relative(dirname(outPath), sourceFile) : sourceFile;
29
+ const href = rel.startsWith(".") ? rel : `./${rel}`;
30
+ return `[${label}](${href}${line === void 0 ? "" : `#L${line}`})`;
31
+ }
32
+ __name(sourceLink, "sourceLink");
26
33
  function headerNameToProperty(name) {
27
34
  const parts = name.split(/[-_]/).filter(Boolean);
28
35
  return parts.map((p, i) => {
@@ -84,7 +91,9 @@ function renderTsScalar(name, target) {
84
91
  case "date":
85
92
  case "time":
86
93
  case "datetime":
94
+ return "DateTime";
87
95
  case "duration":
96
+ return "Duration";
88
97
  case "interval":
89
98
  return "string";
90
99
  case "null":
@@ -173,18 +182,66 @@ var DECIMAL_PRELUDE_LINES = [
173
182
  ];
174
183
 
175
184
  // 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
- ];
185
+ var COERCE_DECLS = {
186
+ "__dec(": [
187
+ `const __dec = (v: unknown, path: string): Decimal => {`,
188
+ ` if (typeof v !== 'string') {`,
189
+ ` throw new TypeError(\`ContractKit: expected a decimal string at '\${path}', received \${typeof v} \u2014 decimals must be sent as quoted JSON strings.\`);`,
190
+ ` }`,
191
+ ` try {`,
192
+ ` return new Decimal(v);`,
193
+ ` } catch {`,
194
+ ` throw new TypeError(\`ContractKit: '\${v}' at '\${path}' is not a valid decimal.\`);`,
195
+ ` }`,
196
+ `};`
197
+ ],
198
+ "__dt(": [
199
+ `const __dt = (v: unknown, path: string): DateTime => {`,
200
+ ` if (typeof v !== 'string') {`,
201
+ ` throw new TypeError(\`ContractKit: expected an ISO 8601 string at '\${path}', received \${typeof v}.\`);`,
202
+ ` }`,
203
+ ` const d = DateTime.fromISO(v);`,
204
+ ` if (!d.isValid) throw new TypeError(\`ContractKit: '\${v}' at '\${path}' is not a valid ISO 8601 datetime.\`);`,
205
+ ` return d;`,
206
+ `};`
207
+ ],
208
+ "__dtf(": [
209
+ `const __dtf = (v: unknown, path: string, fmt: string): DateTime => {`,
210
+ ` if (typeof v !== 'string') {`,
211
+ ` throw new TypeError(\`ContractKit: expected a string at '\${path}' in format \${fmt}, received \${typeof v}.\`);`,
212
+ ` }`,
213
+ ` const d = DateTime.fromFormat(v, fmt);`,
214
+ ` if (!d.isValid) throw new TypeError(\`ContractKit: '\${v}' at '\${path}' does not match format \${fmt}.\`);`,
215
+ ` return d;`,
216
+ `};`
217
+ ],
218
+ "__dur(": [
219
+ `const __dur = (v: unknown, path: string): Duration => {`,
220
+ ` if (typeof v !== 'string') {`,
221
+ ` throw new TypeError(\`ContractKit: expected an ISO 8601 duration string at '\${path}', received \${typeof v}.\`);`,
222
+ ` }`,
223
+ ` const d = Duration.fromISO(v);`,
224
+ ` if (!d.isValid) throw new TypeError(\`ContractKit: '\${v}' at '\${path}' is not a valid ISO 8601 duration.\`);`,
225
+ ` return d;`,
226
+ `};`
227
+ ]
228
+ };
229
+ var DECIMAL_COERCE_DECL = COERCE_DECLS["__dec("];
230
+ function coerceDeclsFor(lines) {
231
+ const haystack = lines.join("\n");
232
+ return Object.entries(COERCE_DECLS).filter(([prefix]) => haystack.includes(prefix)).flatMap(([, decl]) => decl);
233
+ }
234
+ __name(coerceDeclsFor, "coerceDeclsFor");
235
+ function coerceLuxonImports(lines) {
236
+ const haystack = lines.join("\n");
237
+ const needed = /* @__PURE__ */ new Set();
238
+ if (haystack.includes("__dt(") || haystack.includes("__dtf(")) needed.add("DateTime");
239
+ if (haystack.includes("__dur(")) needed.add("Duration");
240
+ return [
241
+ ...needed
242
+ ].sort();
243
+ }
244
+ __name(coerceLuxonImports, "coerceLuxonImports");
188
245
  function reviveFnName(model, variant = "base") {
189
246
  return `revive${model}${variant === "output" ? "Output" : ""}`;
190
247
  }
@@ -195,10 +252,17 @@ function applyCase(name, caseTransform) {
195
252
  return name.charAt(0).toUpperCase() + name.slice(1);
196
253
  }
197
254
  __name(applyCase, "applyCase");
255
+ var DEFAULT_REVIVABLE_SCALARS = /* @__PURE__ */ new Set([
256
+ "decimal",
257
+ "date",
258
+ "time",
259
+ "datetime",
260
+ "duration"
261
+ ]);
198
262
  function typeReachesDecimal(type, opts) {
199
263
  switch (type.kind) {
200
264
  case "scalar":
201
- return type.name === "decimal";
265
+ return (opts.revivableScalars ?? DEFAULT_REVIVABLE_SCALARS).has(type.name);
202
266
  case "ref":
203
267
  return opts.modelsWithDecimal.has(type.name);
204
268
  case "array":
@@ -220,6 +284,34 @@ function typeReachesDecimal(type, opts) {
220
284
  }
221
285
  }
222
286
  __name(typeReachesDecimal, "typeReachesDecimal");
287
+ function scalarCoercion(type, slot, path, opts) {
288
+ if (!(opts.revivableScalars ?? DEFAULT_REVIVABLE_SCALARS).has(type.name)) return [];
289
+ switch (type.name) {
290
+ case "decimal":
291
+ return [
292
+ `${slot} = __dec(${slot}, '${path}');`
293
+ ];
294
+ case "datetime":
295
+ return [
296
+ `${slot} = __dt(${slot}, '${path}');`
297
+ ];
298
+ case "duration":
299
+ return [
300
+ `${slot} = __dur(${slot}, '${path}');`
301
+ ];
302
+ case "date":
303
+ return [
304
+ `${slot} = __dtf(${slot}, '${path}', '${type.format ?? "yyyy-MM-dd"}');`
305
+ ];
306
+ case "time":
307
+ return [
308
+ `${slot} = __dtf(${slot}, '${path}', '${type.format ?? "HH:mm:ss"}');`
309
+ ];
310
+ default:
311
+ return [];
312
+ }
313
+ }
314
+ __name(scalarCoercion, "scalarCoercion");
223
315
  var Scope = class Scope2 {
224
316
  static {
225
317
  __name(this, "Scope");
@@ -232,9 +324,7 @@ var Scope = class Scope2 {
232
324
  function emit(slot, type, path, opts, scope, variant) {
233
325
  switch (type.kind) {
234
326
  case "scalar":
235
- return type.name === "decimal" ? [
236
- `${slot} = __dec(${slot}, '${path}');`
237
- ] : [];
327
+ return scalarCoercion(type, slot, path, opts);
238
328
  case "ref":
239
329
  return opts.modelsWithDecimal.has(type.name) ? [
240
330
  `${reviveRefName(type.name, opts, variant)}(${slot} as never);`
@@ -386,7 +476,7 @@ function renderOne(model, opts, variant) {
386
476
  const body2 = emit("__v[0]", model.type, model.name, opts, scope, variant);
387
477
  if (body2.length === 0) return [];
388
478
  return [
389
- `/** Rehydrates every \`decimal\` in a ${typeName} from its wire string. Mutates and returns \`raw\`. */`,
479
+ `/** Rehydrates every wire-encoded scalar in a ${typeName} into its runtime type. Mutates and returns \`raw\`. */`,
390
480
  `export function ${fnName}(raw: ${typeName}): ${typeName} {`,
391
481
  ` const __v = [raw] as unknown[];`,
392
482
  ...body2.map((l) => ` ${l}`),
@@ -398,7 +488,7 @@ function renderOne(model, opts, variant) {
398
488
  const body = model.fields.flatMap((f) => typeReachesDecimal(f.type, opts) ? fieldStatements(obj, f, model.name, opts, scope, variant, model.outputCase) : []);
399
489
  if (body.length === 0) return [];
400
490
  return [
401
- `/** Rehydrates every \`decimal\` in a ${typeName} from its wire string. Mutates and returns \`raw\`. */`,
491
+ `/** Rehydrates every wire-encoded scalar in a ${typeName} into its runtime type. Mutates and returns \`raw\`. */`,
402
492
  `export function ${fnName}(raw: ${typeName}): ${typeName} {`,
403
493
  ` const ${obj} = raw as unknown as Record<string, unknown>;`,
404
494
  ...body.map((l) => ` ${l}`),
@@ -459,8 +549,7 @@ function generateComments(model, outPath) {
459
549
  if (model.description) {
460
550
  for (const l of escapeJsDocLines(model.description)) lines.push(` * ${l}`);
461
551
  }
462
- const relPath = outPath ? relative(dirname(outPath), model.loc.file) : model.loc.file;
463
- lines.push(` * generated from [${model.name}](file://./${relPath}#L${model.loc.line})`);
552
+ lines.push(` * generated from ${sourceLink(model.name, outPath, model.loc.file, model.loc.line)}`);
464
553
  lines.push("*/");
465
554
  return lines;
466
555
  }
@@ -510,7 +599,7 @@ function generateContract(root, context) {
510
599
  }
511
600
  lines.push("");
512
601
  if (needsBinary) {
513
- lines.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
602
+ lines.push(context?.target === "server" ? `const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });` : `const _ZodBinary = z.custom<Blob>((val) => val instanceof Blob, { error: 'Must be binary data' });`);
514
603
  }
515
604
  if (needsDatetime) {
516
605
  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' }));`);
@@ -526,7 +615,6 @@ function generateContract(root, context) {
526
615
  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)]));`);
527
616
  }
528
617
  if (needsBinary || needsDatetime || needsInterval || needsDecimal || needsJson) lines.push("");
529
- const modelsWithWriteonly = new Set(root.models.filter((m) => m.fields.some((f) => f.visibility === "writeonly")).map((m) => m.name));
530
618
  const modelMap = new Map(root.models.map((m) => [
531
619
  m.name,
532
620
  m
@@ -538,7 +626,7 @@ function generateContract(root, context) {
538
626
  } : void 0;
539
627
  const bodyLines = [];
540
628
  for (const model of topoSortModels(root.models)) {
541
- bodyLines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, modelsWithWriteonly, modelMap, allModelsWithOutput));
629
+ bodyLines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, modelMap, allModelsWithOutput));
542
630
  if (reviveOpts) {
543
631
  const revivers = renderReviveFunctions(model, reviveOpts);
544
632
  if (revivers.length > 0) {
@@ -548,8 +636,9 @@ function generateContract(root, context) {
548
636
  }
549
637
  bodyLines.push("");
550
638
  }
551
- if (bodyLines.some((l) => l.includes("__dec("))) {
552
- lines.push(...DECIMAL_COERCE_DECL);
639
+ const coerceDecls = coerceDeclsFor(bodyLines);
640
+ if (coerceDecls.length > 0) {
641
+ lines.push(...coerceDecls);
553
642
  lines.push("");
554
643
  }
555
644
  lines.push(...bodyLines);
@@ -579,13 +668,13 @@ function flattenFormatChain(model, modelMap) {
579
668
  };
580
669
  }
581
670
  __name(flattenFormatChain, "flattenFormatChain");
582
- function generateModel(model, outPath, modelsWithInput, modelsWithWriteonly, modelMap, modelsWithOutput) {
671
+ function generateModel(model, outPath, modelsWithInput, modelMap, modelsWithOutput) {
583
672
  if (model.type) {
584
673
  return generateTypeAlias(model, outPath, modelsWithInput, modelsWithOutput);
585
674
  }
586
675
  const effective = modelMap ? flattenFormatChain(model, modelMap) : model;
587
676
  const needsInputSplit = effective.fields.some((f) => f.visibility !== "normal") || (modelsWithInput?.has(effective.name) ?? false);
588
- const lines = needsInputSplit ? generateThreeSchemaModel(effective, outPath, modelsWithInput, modelsWithWriteonly, modelMap) : generateSimpleModel(effective, outPath);
677
+ const lines = needsInputSplit ? generateThreeSchemaModel(effective, outPath, modelsWithInput, modelMap) : generateSimpleModel(effective, outPath);
589
678
  if (modelsWithOutput?.has(effective.name)) {
590
679
  lines.push(`export type ${effective.name}Output = z.output<typeof ${effective.name}>;`);
591
680
  }
@@ -674,26 +763,13 @@ function collectEffectiveWritableFieldNames(modelName, modelMap) {
674
763
  return result;
675
764
  }
676
765
  __name(collectEffectiveWritableFieldNames, "collectEffectiveWritableFieldNames");
677
- function generateThreeSchemaModel(model, outPath, modelsWithInput, modelsWithWriteonly, modelMap) {
766
+ function generateThreeSchemaModel(model, outPath, modelsWithInput, modelMap) {
678
767
  const lines = [];
679
768
  const name = model.name;
680
769
  lines.push(...generateComments(model, outPath));
681
770
  const wrapper = modeToWrapper(model.mode ?? "strict");
682
771
  const allFields = model.fields;
683
- const hasWriteonly = allFields.some((f) => f.visibility === "writeonly");
684
772
  const bases = model.bases ?? [];
685
- if (hasWriteonly) {
686
- const baseBody = renderFields(allFields, model.mode);
687
- if (bases.length > 0) {
688
- const { head, tail } = buildExtendChain(bases, (b) => modelsWithWriteonly?.has(b) ? `${b}Base` : b);
689
- lines.push(`const ${name}Base = ${head}${tail}.extend({`);
690
- } else {
691
- lines.push(`const ${name}Base = ${wrapper}({`);
692
- }
693
- lines.push(...baseBody.map((l) => ` ${l}`));
694
- lines.push(`});`);
695
- lines.push("");
696
- }
697
773
  const readFields = allFields.filter((f) => f.visibility !== "writeonly");
698
774
  const readBody = renderFields(readFields, model.mode);
699
775
  if (bases.length > 0) {
@@ -790,10 +866,7 @@ function renderFieldsAsSnakeCase(fields, defaultMode) {
790
866
  });
791
867
  }
792
868
  __name(renderFieldsAsSnakeCase, "renderFieldsAsSnakeCase");
793
- function renderField(field, defaultMode) {
794
- const lines = [];
795
- if (field.deprecated) lines.push("/** @deprecated */");
796
- let expr = renderType(field.type, void 0, defaultMode);
869
+ function applyFieldModifiers(expr, field) {
797
870
  if (field.nullable) expr += ".nullable()";
798
871
  if (field.default !== void 0) {
799
872
  const dv = typeof field.default === "string" ? `"${escapeString(field.default)}"` : String(field.default);
@@ -802,6 +875,14 @@ function renderField(field, defaultMode) {
802
875
  expr += ".optional()";
803
876
  }
804
877
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
878
+ return expr;
879
+ }
880
+ __name(applyFieldModifiers, "applyFieldModifiers");
881
+ function renderField(field, defaultMode) {
882
+ const lines = [];
883
+ if (field.deprecated) lines.push("/** @deprecated */");
884
+ let expr = renderType(field.type, void 0, defaultMode);
885
+ expr = applyFieldModifiers(expr, field);
805
886
  lines.push(`${quoteKey2(field.name)}: ${expr},`);
806
887
  return lines;
807
888
  }
@@ -855,6 +936,7 @@ function regexHasAnchor(source) {
855
936
  return backslashes % 2 === 0;
856
937
  }
857
938
  __name(regexHasAnchor, "regexHasAnchor");
939
+ var NUMERIC_PREPROCESS = `(v) => (typeof v === 'string' && v.trim() !== '' ? Number(v) : v)`;
858
940
  function renderScalar(s) {
859
941
  switch (s.name) {
860
942
  case "string": {
@@ -866,17 +948,12 @@ function renderScalar(s) {
866
948
  if (s.regex) e += `.regex(${renderRegexLiteral(s.regex)})`;
867
949
  return e;
868
950
  }
869
- case "number": {
870
- let e = "z.coerce.number()";
871
- if (s.min !== void 0) e += `.min(${s.min})`;
872
- if (s.max !== void 0) e += `.max(${s.max})`;
873
- return e;
874
- }
951
+ case "number":
875
952
  case "int": {
876
- let e = "z.coerce.number().int()";
877
- if (s.min !== void 0) e += `.min(${s.min})`;
878
- if (s.max !== void 0) e += `.max(${s.max})`;
879
- return e;
953
+ let inner = s.name === "int" ? "z.number().int()" : "z.number()";
954
+ if (s.min !== void 0) inner += `.min(${s.min})`;
955
+ if (s.max !== void 0) inner += `.max(${s.max})`;
956
+ return `z.preprocess(${NUMERIC_PREPROCESS}, ${inner})`;
880
957
  }
881
958
  case "bigint": {
882
959
  let inner = "z.bigint()";
@@ -1107,14 +1184,7 @@ function renderInputField(field, modelsWithInput, defaultMode) {
1107
1184
  const lines = [];
1108
1185
  if (field.deprecated) lines.push("/** @deprecated */");
1109
1186
  let expr = renderInputType(field.type, modelsWithInput, defaultMode);
1110
- if (field.nullable) expr += ".nullable()";
1111
- if (field.default !== void 0) {
1112
- const dv = typeof field.default === "string" ? `"${escapeString(field.default)}"` : String(field.default);
1113
- expr += `.default(${dv})`;
1114
- } else if (field.optional) {
1115
- expr += ".optional()";
1116
- }
1117
- if (field.description) expr += `.describe("${escapeString(field.description)}")`;
1187
+ expr = applyFieldModifiers(expr, field);
1118
1188
  lines.push(`${quoteKey2(field.name)}: ${expr},`);
1119
1189
  return lines;
1120
1190
  }
@@ -1167,14 +1237,7 @@ ${fieldLines}
1167
1237
  __name(renderQueryType, "renderQueryType");
1168
1238
  function renderQueryField(field, modelsWithInput, defaultMode) {
1169
1239
  let expr = field.type.kind === "array" ? renderQueryType(field.type, modelsWithInput, defaultMode) : modelsWithInput ? renderInputType(field.type, modelsWithInput, defaultMode) : renderType(field.type, void 0, defaultMode);
1170
- if (field.nullable) expr += ".nullable()";
1171
- if (field.default !== void 0) {
1172
- const dv = typeof field.default === "string" ? `"${escapeString(field.default)}"` : String(field.default);
1173
- expr += `.default(${dv})`;
1174
- } else if (field.optional) {
1175
- expr += ".optional()";
1176
- }
1177
- if (field.description) expr += `.describe("${escapeString(field.description)}")`;
1240
+ expr = applyFieldModifiers(expr, field);
1178
1241
  return `${quoteKey2(field.name)}: ${expr},`;
1179
1242
  }
1180
1243
  __name(renderQueryField, "renderQueryField");
@@ -1371,8 +1434,8 @@ function resolveImportPath(refName, context) {
1371
1434
  if (context) {
1372
1435
  const refOutPath = context.modelOutPaths.get(refName);
1373
1436
  if (refOutPath) {
1374
- const fromDir = dirname(context.currentOutPath);
1375
- let rel = relative(fromDir, refOutPath);
1437
+ const fromDir = dirname2(context.currentOutPath);
1438
+ let rel = relative2(fromDir, refOutPath);
1376
1439
  rel = rel.replace(/\.ts$/, ".js");
1377
1440
  if (!rel.startsWith(".")) rel = "./" + rel;
1378
1441
  return rel;
@@ -1388,8 +1451,8 @@ function pascalToDotCase(name) {
1388
1451
  __name(pascalToDotCase, "pascalToDotCase");
1389
1452
 
1390
1453
  // src/codegen-operation.ts
1391
- import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType, emittedResponses } from "@contractkit/core";
1392
- import { basename, dirname as dirname2, relative as relative2 } from "path";
1454
+ import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType, emittedResponses, PATH_PARAM_RE_G, toIdentifier } from "@contractkit/core";
1455
+ import { basename, dirname as dirname3, relative as relative3 } from "path";
1393
1456
  function bodyParserToken(contentType) {
1394
1457
  switch (classifyContentType(contentType)) {
1395
1458
  case "urlencoded":
@@ -1468,8 +1531,7 @@ function generateOp(root, options = {}) {
1468
1531
  const lines = [];
1469
1532
  lines.push("");
1470
1533
  lines.push("/**");
1471
- const relFile = options.outPath ? relative2(dirname2(options.outPath), root.file) : root.file;
1472
- lines.push(` * generated from [${basename(root.file)}](file://./${relFile})`);
1534
+ lines.push(` * generated from ${sourceLink(basename(root.file), options.outPath, root.file)}`);
1473
1535
  lines.push("*/");
1474
1536
  lines.push(`export const ${routerName} = ServerKitRouter();`);
1475
1537
  lines.push("");
@@ -1569,8 +1631,7 @@ function generateHandler(route, op, root, options) {
1569
1631
  if (desc) {
1570
1632
  for (const l of escapeJsDocLines(desc)) lines.push(` * ${l}`);
1571
1633
  }
1572
- const relFile = outPath ? relative2(dirname2(outPath), file) : file;
1573
- lines.push(` * from [${basename(file)}](file://./${relFile}#L${op.loc.line})`);
1634
+ lines.push(` * from ${sourceLink(basename(file), outPath, file, op.loc.line)}`);
1574
1635
  const effectiveSecurity = resolveSecurity(route, op, root);
1575
1636
  if (effectiveSecurity === SECURITY_NONE) {
1576
1637
  lines.push(` * anonymous access, no security required`);
@@ -1580,7 +1641,7 @@ function generateHandler(route, op, root, options) {
1580
1641
  if (mods.includes("deprecated")) lines.push(` * @deprecated`);
1581
1642
  lines.push("*/");
1582
1643
  const method = op.method;
1583
- const path = route.path.replace(/\{(\w+)\}/g, ":$1");
1644
+ const path = route.path.replace(PATH_PARAM_RE_G, (_m, name) => `:${toIdentifier(name)}`);
1584
1645
  const bodies = op.request?.bodies ?? [];
1585
1646
  const hasBody = bodies.length > 0;
1586
1647
  const isSingleMultipart = bodies.length === 1 && bodies[0].contentType === "multipart/form-data";
@@ -1679,7 +1740,7 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1679
1740
  }
1680
1741
  }
1681
1742
  lines.push("");
1682
- lines.push(` ctx.status = ${resp?.statusCode ?? op.responses[0]?.statusCode ?? 200};`);
1743
+ lines.push(` ctx.status = ${resp?.statusCode ?? 204};`);
1683
1744
  lines.push(...headerSetLines(respHeaders, " "));
1684
1745
  if (bodies.length === 1) {
1685
1746
  lines.push(` ctx.type = '${bodies[0].contentType}';`);
@@ -1833,7 +1894,7 @@ function buildArgs(route, op) {
1833
1894
  const args = [];
1834
1895
  if (route.params) {
1835
1896
  if (route.params.kind === "params") {
1836
- args.push(...route.params.nodes.map((p) => p.name));
1897
+ args.push(...route.params.nodes.map((p) => toIdentifier(p.name)));
1837
1898
  } else {
1838
1899
  args.push("params");
1839
1900
  }
@@ -1963,18 +2024,17 @@ function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", mo
1963
2024
  lines.push("");
1964
2025
  } else if (source.kind === "params") {
1965
2026
  if (source.nodes.length > 0) {
1966
- const lhs = varName === "params" ? `{ ${source.nodes.map((p) => p.name).join(", ")} }` : varName;
2027
+ const isPathParams = ctxExpr === "ctx.params";
2028
+ const bind = /* @__PURE__ */ __name((name) => isPathParams ? toIdentifier(name) : name, "bind");
2029
+ const lhs = varName === "params" ? `{ ${source.nodes.map((p) => bind(p.name)).join(", ")} }` : varName;
1967
2030
  lines.push(` const ${lhs} = await parseAndValidate(`);
1968
2031
  lines.push(` ${ctxExpr},`);
1969
2032
  lines.push(` ${modeToWrapper(mode)}({`);
1970
2033
  for (const param of source.nodes) {
1971
- const key = isValidIdentifier2(param.name) ? param.name : `'${param.name}'`;
1972
- if (isQuery && param.type.kind === "array") {
1973
- const inner = renderType(param.type);
1974
- lines.push(` ${key}: z.preprocess((v) => typeof v === 'string' ? v.split(',') : v, ${inner}),`);
1975
- } else {
1976
- lines.push(` ${key}: ${renderType(param.type)},`);
1977
- }
2034
+ const bound = bind(param.name);
2035
+ const key = isValidIdentifier2(bound) ? bound : `'${bound}'`;
2036
+ const base = isQuery ? renderQueryType(param.type, modelsWithInput) : renderInputType(param.type, modelsWithInput);
2037
+ lines.push(` ${key}: ${applyFieldModifiers(base, param)},`);
1978
2038
  }
1979
2039
  lines.push(` })${suffix},`);
1980
2040
  lines.push(` );`);
@@ -2004,9 +2064,9 @@ function generateTypeImports(types, opFile, options) {
2004
2064
  unresolved.push(type);
2005
2065
  }
2006
2066
  }
2007
- const fromDir = dirname2(outPath);
2067
+ const fromDir = dirname3(outPath);
2008
2068
  for (const [typeOutPath, names] of byFile) {
2009
- let rel = relative2(fromDir, typeOutPath);
2069
+ let rel = relative3(fromDir, typeOutPath);
2010
2070
  rel = rel.replace(/\.ts$/, ".js");
2011
2071
  if (!rel.startsWith(".")) rel = "./" + rel;
2012
2072
  lines.push(`import { ${names.sort().join(", ")} } from '${rel}';`);
@@ -2230,11 +2290,11 @@ function deriveTypeImportPath(file, template) {
2230
2290
  __name(deriveTypeImportPath, "deriveTypeImportPath");
2231
2291
 
2232
2292
  // src/index.ts
2233
- import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform, computeModelsWithDecimal } from "@contractkit/core";
2293
+ import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform, computeModelsWithScalar } from "@contractkit/core";
2234
2294
 
2235
2295
  // src/codegen-sdk.ts
2236
- import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
2237
- import { basename as basename2, dirname as dirname3, relative as relative3 } from "path";
2296
+ import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses, PATH_PARAM_RE_G as PATH_PARAM_RE_G2, toIdentifier as toIdentifier2 } from "@contractkit/core";
2297
+ import { basename as basename2, dirname as dirname4, relative as relative4 } from "path";
2238
2298
  function jsonOrFormSerialize(varName, contentType) {
2239
2299
  if (contentType === "application/x-www-form-urlencoded") {
2240
2300
  return `new URLSearchParams(${varName} as unknown as Record<string, string>).toString()`;
@@ -2304,7 +2364,6 @@ function generateSdk(root, options = {}) {
2304
2364
  const mods = resolveModifiers2(route, op);
2305
2365
  if (!includeInternal && mods.includes("internal")) continue;
2306
2366
  classBody.push("");
2307
- if (mods.includes("deprecated")) classBody.push(" /** @deprecated */");
2308
2367
  classBody.push(...generateMethod(route, op, root.file, options, inlineRevivers));
2309
2368
  }
2310
2369
  }
@@ -2312,19 +2371,29 @@ function generateSdk(root, options = {}) {
2312
2371
  ...inlineRevivers.values()
2313
2372
  ].flat();
2314
2373
  const decimalPrelude = decimalPreludeFor(inlineReviverDecls);
2315
- if (types.length > 0) {
2316
- lines.push(...generateTypeImports2(types, root.file, options, usedRevivers(classBody)));
2374
+ const errorAliases = generateErrorBodyAliases(root, options);
2375
+ const referenced = referencedTypes(types, [
2376
+ ...classBody,
2377
+ ...errorAliases,
2378
+ ...inlineReviverDecls
2379
+ ]);
2380
+ if (referenced.length > 0) {
2381
+ lines.push(...generateTypeImports2(referenced, root.file, options, usedRevivers(classBody)));
2317
2382
  }
2318
2383
  lines.push(...decimalPrelude.imports);
2384
+ const headerLuxon = headerLuxonImport(classBody, decimalPrelude.imports);
2385
+ if (headerLuxon) lines.push(headerLuxon);
2319
2386
  if (options.sdkOptionsPath && options.outPath) {
2320
- let rel = relative3(dirname3(options.outPath), options.sdkOptionsPath);
2387
+ let rel = relative4(dirname4(options.outPath), options.sdkOptionsPath);
2321
2388
  rel = rel.replace(/\.ts$/, ".js");
2322
2389
  if (!rel.startsWith(".")) rel = "./" + rel;
2323
2390
  const jsonImport = sdkNeedsJson(root, includeInternal) ? ", JsonValue" : "";
2324
2391
  lines.push(`import type { SdkFetch${jsonImport} } from '${rel}';`);
2325
2392
  const valueImports = [];
2326
2393
  if (sdkNeedsBigIntReplacer(root, includeInternal)) valueImports.push("bigIntReplacer");
2327
- if (sdkNeedsBigIntReviver(root, includeInternal)) valueImports.push("parseJson");
2394
+ if (sdkParsesJsonResponse(root, includeInternal)) {
2395
+ valueImports.push(sdkResponsesUseBigInt(root, options, includeInternal) ? "parseJsonWithBigInt as parseJson" : "parseJson");
2396
+ }
2328
2397
  if (sdkNeedsQueryString(root, includeInternal)) valueImports.push("buildQueryString");
2329
2398
  if (sdkNeedsReadContentType(root, includeInternal)) valueImports.push("readContentType");
2330
2399
  if (valueImports.length > 0) {
@@ -2401,14 +2470,13 @@ function generateSdk(root, options = {}) {
2401
2470
  lines.push("}");
2402
2471
  lines.push("");
2403
2472
  lines.push("export async function parseJson<T>(res: Response): Promise<T> {");
2404
- lines.push(" return JSON.parse(await res.text(), bigIntReviver) as T;");
2473
+ lines.push(sdkResponsesUseBigInt(root, options, includeInternal) ? " return JSON.parse(await res.text(), bigIntReviver) as T;" : " return JSON.parse(await res.text()) as T;");
2405
2474
  lines.push("}");
2406
2475
  }
2407
2476
  if (sdkNeedsJson(root, includeInternal) && !(options.sdkOptionsPath && options.outPath)) {
2408
2477
  lines.push(JSON_VALUE_TYPE_DECL);
2409
2478
  }
2410
2479
  lines.push("");
2411
- const errorAliases = generateErrorBodyAliases(root, options);
2412
2480
  if (errorAliases.length > 0) {
2413
2481
  lines.push(...errorAliases);
2414
2482
  lines.push("");
@@ -2422,8 +2490,7 @@ function generateSdk(root, options = {}) {
2422
2490
  lines.push(...decl);
2423
2491
  }
2424
2492
  lines.push("/**");
2425
- const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
2426
- lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
2493
+ lines.push(` * generated from ${sourceLink(basename2(root.file), options.outPath, root.file)}`);
2427
2494
  lines.push(" */");
2428
2495
  lines.push(`export class ${clientClassName} {`);
2429
2496
  lines.push(" constructor(private fetch: SdkFetch) {}");
@@ -2443,7 +2510,6 @@ function generateClientMethods(root, options) {
2443
2510
  const mods = resolveModifiers2(route, op);
2444
2511
  if (!includeInternal && mods.includes("internal")) continue;
2445
2512
  lines.push("");
2446
- if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
2447
2513
  lines.push(...generateMethod(route, op, root.file, options, inlineRevivers));
2448
2514
  methodNames.push(deriveMethodName(op, route));
2449
2515
  }
@@ -2473,22 +2539,45 @@ function generateClientMethods(root, options) {
2473
2539
  }
2474
2540
  __name(generateClientMethods, "generateClientMethods");
2475
2541
  function decimalPreludeFor(declLines) {
2476
- if (!declLines.some((l) => l.includes("__dec("))) return {
2542
+ const decls = coerceDeclsFor(declLines);
2543
+ if (decls.length === 0) return {
2477
2544
  imports: [],
2478
2545
  decls: []
2479
2546
  };
2547
+ const imports = [];
2548
+ const preamble = [];
2549
+ if (declLines.some((l) => l.includes("__dec("))) {
2550
+ imports.push(DECIMAL_IMPORT);
2551
+ preamble.push(DECIMAL_CONFIG_LINE, "");
2552
+ }
2553
+ const luxon = coerceLuxonImports(declLines);
2554
+ if (luxon.length > 0) imports.push(`import { ${luxon.join(", ")} } from 'luxon';`);
2480
2555
  return {
2481
- imports: [
2482
- DECIMAL_IMPORT
2483
- ],
2556
+ imports,
2484
2557
  decls: [
2485
- DECIMAL_CONFIG_LINE,
2486
- "",
2487
- ...DECIMAL_COERCE_DECL
2558
+ ...preamble,
2559
+ ...decls
2488
2560
  ]
2489
2561
  };
2490
2562
  }
2491
2563
  __name(decimalPreludeFor, "decimalPreludeFor");
2564
+ function headerLuxonImport(methodLines, existingImports) {
2565
+ if (existingImports.some((l) => l.includes("from 'luxon'"))) return void 0;
2566
+ const needed = [
2567
+ "DateTime",
2568
+ "Duration"
2569
+ ].filter((c) => methodLines.some((l) => l.includes(`${c}.from`)));
2570
+ return needed.length > 0 ? `import { ${needed.join(", ")} } from 'luxon';` : void 0;
2571
+ }
2572
+ __name(headerLuxonImport, "headerLuxonImport");
2573
+ function referencedTypes(types, emitted) {
2574
+ const haystack = emitted.join("\n");
2575
+ return types.filter((t) => {
2576
+ const escaped = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2577
+ return new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(haystack);
2578
+ });
2579
+ }
2580
+ __name(referencedTypes, "referencedTypes");
2492
2581
  function usedRevivers(lines) {
2493
2582
  const found = /* @__PURE__ */ new Set();
2494
2583
  for (const m of lines.join("\n").matchAll(/\brevive[A-Z]\w*/g)) found.add(m[0]);
@@ -2507,6 +2596,7 @@ function generateMethod(route, op, file, options, inlineRevivers) {
2507
2596
  } : void 0;
2508
2597
  const lines = [];
2509
2598
  const methodName = deriveMethodName(op, route);
2599
+ const where = `${op.method.toUpperCase()} ${route.path}`;
2510
2600
  const mRevive = hint(revive, `${methodName.charAt(0).toUpperCase()}${methodName.slice(1)}`);
2511
2601
  const httpMethod = op.method.toUpperCase();
2512
2602
  const { modelsWithInput, modelsWithOutput } = options;
@@ -2538,11 +2628,13 @@ function generateMethod(route, op, file, options, inlineRevivers) {
2538
2628
  const expectStatuses = observable.filter((r) => r.statusCode < 200 || r.statusCode >= 300).map((r) => r.statusCode);
2539
2629
  const desc = op.description ?? route.description;
2540
2630
  const errorBodyName = thrown.some((r) => r.bodies.length > 0) ? errorBodyTypeName(route, op) : void 0;
2541
- if (op.name || desc || errorBodyName) {
2631
+ const deprecated = resolveModifiers2(route, op).includes("deprecated");
2632
+ if (op.name || desc || errorBodyName || deprecated) {
2542
2633
  const tags = [];
2543
2634
  if (op.name) tags.push(`@name ${op.name}`);
2544
2635
  if (desc) tags.push(`@description ${desc}`);
2545
2636
  if (errorBodyName) tags.push(`@throws {SdkError<${errorBodyName}>} on ${thrown.map((r) => r.statusCode).join(", ")}`);
2637
+ if (deprecated) tags.push("@deprecated");
2546
2638
  const contentLines = tags.flatMap((t) => escapeJsDocLines(t));
2547
2639
  if (contentLines.length === 1) {
2548
2640
  lines.push(` /** ${contentLines[0]} */`);
@@ -2635,15 +2727,15 @@ function generateMethod(route, op, file, options, inlineRevivers) {
2635
2727
  lines.push(` switch (result.status) {`);
2636
2728
  for (const resp of rest) {
2637
2729
  lines.push(` case ${resp.statusCode}:`);
2638
- lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true, mRevive));
2730
+ lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true, mRevive, where));
2639
2731
  }
2640
2732
  lines.push(` default:`);
2641
- lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true, mRevive));
2733
+ lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true, mRevive, where));
2642
2734
  lines.push(` }`);
2643
2735
  } else if (primaryBodies.length > 1) {
2644
- lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false, mRevive));
2736
+ lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false, mRevive, where));
2645
2737
  } else if (hasRespHeaders) {
2646
- const headerEntries = sdkHeaderEntries(respHeaders);
2738
+ const headerEntries = sdkHeaderEntries(respHeaders, where);
2647
2739
  if (isVoid) {
2648
2740
  lines.push(` return { headers: { ${headerEntries} } };`);
2649
2741
  } else {
@@ -2687,6 +2779,7 @@ function reviveExprFor(bodyType, ctx) {
2687
2779
  if (!ctx || ctx.modelsWithDecimal.size === 0) return null;
2688
2780
  const opts = {
2689
2781
  modelsWithDecimal: ctx.modelsWithDecimal,
2782
+ revivableScalars: ctx.revivableScalars,
2690
2783
  modelsWithOutput: ctx.modelsWithOutput,
2691
2784
  modelMap: ctx.modelMap
2692
2785
  };
@@ -2722,8 +2815,48 @@ function renderSdkHeadersShape(headers, modelsWithOutput) {
2722
2815
  return `{ ${fields.join("; ")} }`;
2723
2816
  }
2724
2817
  __name(renderSdkHeadersShape, "renderSdkHeadersShape");
2725
- function sdkHeaderEntries(headers) {
2726
- return headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2818
+ function sdkHeaderEntry(h, where) {
2819
+ const raw = `result.headers.get('${h.name}')`;
2820
+ const key = quoteKey(headerNameToProperty(h.name));
2821
+ const scalar = h.type.kind === "scalar" ? h.type.name : void 0;
2822
+ const convert = /* @__PURE__ */ __name((expr) => `${key}: ${h.optional ? `${raw} === null ? undefined : ${expr(`${raw}!`)}` : expr(`${raw}!`)}`, "convert");
2823
+ switch (scalar) {
2824
+ case "string":
2825
+ case "email":
2826
+ case "url":
2827
+ case "uuid":
2828
+ case "interval":
2829
+ case "unknown":
2830
+ return `${key}: ${raw}${h.optional ? " ?? undefined" : "!"}`;
2831
+ // Temporals are Luxon objects since the SDK started reviving them, so the shape
2832
+ // `renderOutputTsType` produces says `DateTime` and the raw string no longer satisfies it.
2833
+ // The format for date and time comes from the contract, as it does in the reviver.
2834
+ case "datetime":
2835
+ return convert((v) => `DateTime.fromISO(${v})`);
2836
+ case "duration":
2837
+ return convert((v) => `Duration.fromISO(${v})`);
2838
+ case "date":
2839
+ return convert((v) => `DateTime.fromFormat(${v}, '${h.type.kind === "scalar" && h.type.format || "yyyy-MM-dd"}')`);
2840
+ case "time":
2841
+ return convert((v) => `DateTime.fromFormat(${v}, '${h.type.kind === "scalar" && h.type.format || "HH:mm:ss"}')`);
2842
+ case "number":
2843
+ case "int":
2844
+ return `${key}: ${h.optional ? `${raw} === null ? undefined : Number(${raw})` : `Number(${raw})`}`;
2845
+ case "boolean":
2846
+ return `${key}: ${h.optional ? `${raw} === null ? undefined : ${raw} === 'true'` : `${raw} === 'true'`}`;
2847
+ case "bigint":
2848
+ return convert((v) => `BigInt(${v})`);
2849
+ default:
2850
+ throw new Error(`Response header '${h.name}' on ${where} is declared as ${describeHeaderType(h.type)}, which cannot be read from an HTTP header. Header values arrive as strings \u2014 declare it as string, email, url, uuid, a date/time type, int, number, boolean or bigint.`);
2851
+ }
2852
+ }
2853
+ __name(sdkHeaderEntry, "sdkHeaderEntry");
2854
+ function describeHeaderType(type) {
2855
+ return type.kind === "scalar" ? `the '${type.name}' scalar` : type.kind === "ref" ? `the model '${type.name}'` : `${type.kind === "array" ? "an" : "a"} ${type.kind}`;
2856
+ }
2857
+ __name(describeHeaderType, "describeHeaderType");
2858
+ function sdkHeaderEntries(headers, where) {
2859
+ return headers.map((h) => sdkHeaderEntry(h, where)).join(", ");
2727
2860
  }
2728
2861
  __name(sdkHeaderEntries, "sdkHeaderEntries");
2729
2862
  function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
@@ -2763,14 +2896,14 @@ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
2763
2896
  ].join("; ")} }`);
2764
2897
  }
2765
2898
  __name(sdkResponseMembers, "sdkResponseMembers");
2766
- function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive) {
2899
+ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, where) {
2767
2900
  const bodies = resp.bodies;
2768
2901
  const headers = resp.headers ?? [];
2769
2902
  const leading = includeStatus ? [
2770
2903
  `status: ${resp.statusCode}`
2771
2904
  ] : [];
2772
2905
  const trailing = headers.length > 0 ? [
2773
- `headers: { ${sdkHeaderEntries(headers)} }`
2906
+ `headers: { ${sdkHeaderEntries(headers, where)} }`
2774
2907
  ] : [];
2775
2908
  if (bodies.length === 0) {
2776
2909
  return [
@@ -2855,9 +2988,11 @@ function generateErrorBodyAliases(root, options) {
2855
2988
  return lines;
2856
2989
  }
2857
2990
  __name(generateErrorBodyAliases, "generateErrorBodyAliases");
2858
- function buildUrlExpression(path, _) {
2859
- return path.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, name) => {
2860
- return `\${encodeURIComponent(${name})}`;
2991
+ function buildUrlExpression(path, params) {
2992
+ return path.replace(PATH_PARAM_RE_G2, (_m, name) => {
2993
+ if (!params || params.kind === "params") return `\${encodeURIComponent(${toIdentifier2(name)})}`;
2994
+ const access = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? `params.${name}` : `params[${JSON.stringify(name)}]`;
2995
+ return `\${encodeURIComponent(String(${access}))}`;
2861
2996
  });
2862
2997
  }
2863
2998
  __name(buildUrlExpression, "buildUrlExpression");
@@ -2867,7 +3002,7 @@ function buildMethodParams(route, op, modelsWithInput) {
2867
3002
  if (route.params.kind === "params") {
2868
3003
  for (const p of route.params.nodes) {
2869
3004
  params.push({
2870
- name: p.name,
3005
+ name: toIdentifier2(p.name),
2871
3006
  type: renderInputTsType(p.type, modelsWithInput),
2872
3007
  optional: false
2873
3008
  });
@@ -2951,55 +3086,44 @@ function buildMethodParams(route, op, modelsWithInput) {
2951
3086
  optional: false
2952
3087
  });
2953
3088
  }
2954
- if (op.query) {
2955
- if (op.query.kind === "params") {
2956
- const fields = op.query.nodes.map((p) => `${quoteKey(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2957
- params.push({
2958
- name: "query",
2959
- type: `{ ${fields} }`,
2960
- optional: true
2961
- });
2962
- } else if (op.query.kind === "ref") {
2963
- const typeName = modelsWithInput?.has(op.query.name) ? `${op.query.name}Input` : op.query.name;
2964
- params.push({
2965
- name: "query",
2966
- type: typeName,
2967
- optional: true
2968
- });
2969
- } else {
2970
- params.push({
2971
- name: "query",
2972
- type: renderInputTsType(op.query.node, modelsWithInput),
2973
- optional: true
2974
- });
2975
- }
3089
+ if (op.query) params.push(inlineArgParam("query", op.query, modelsWithInput));
3090
+ if (op.headers) params.push(inlineArgParam("customHeaders", op.headers, modelsWithInput));
3091
+ return normaliseOptionalOrder(params);
3092
+ }
3093
+ __name(buildMethodParams, "buildMethodParams");
3094
+ function inlineArgParam(name, source, modelsWithInput) {
3095
+ if (source.kind === "params") {
3096
+ const isOptional = /* @__PURE__ */ __name((p) => Boolean(p.optional) || p.default !== void 0, "isOptional");
3097
+ const fields = source.nodes.map((p) => `${quoteKey(p.name)}${isOptional(p) ? "?" : ""}: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
3098
+ return {
3099
+ name,
3100
+ type: `{ ${fields} }`,
3101
+ optional: source.nodes.every(isOptional)
3102
+ };
2976
3103
  }
2977
- if (op.headers) {
2978
- if (op.headers.kind === "params") {
2979
- const fields = op.headers.nodes.map((p) => `${quoteKey(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2980
- params.push({
2981
- name: "customHeaders",
2982
- type: `{ ${fields} }`,
2983
- optional: true
2984
- });
2985
- } else if (op.headers.kind === "ref") {
2986
- const typeName = modelsWithInput?.has(op.headers.name) ? `${op.headers.name}Input` : op.headers.name;
2987
- params.push({
2988
- name: "customHeaders",
2989
- type: typeName,
2990
- optional: true
2991
- });
2992
- } else {
2993
- params.push({
2994
- name: "customHeaders",
2995
- type: renderInputTsType(op.headers.node, modelsWithInput),
2996
- optional: true
2997
- });
2998
- }
3104
+ if (source.kind === "ref") {
3105
+ const typeName = modelsWithInput?.has(source.name) ? `${source.name}Input` : source.name;
3106
+ return {
3107
+ name,
3108
+ type: typeName,
3109
+ optional: true
3110
+ };
2999
3111
  }
3000
- return params;
3112
+ return {
3113
+ name,
3114
+ type: renderInputTsType(source.node, modelsWithInput),
3115
+ optional: true
3116
+ };
3001
3117
  }
3002
- __name(buildMethodParams, "buildMethodParams");
3118
+ __name(inlineArgParam, "inlineArgParam");
3119
+ function normaliseOptionalOrder(params) {
3120
+ const lastRequired = params.reduce((last, p, i) => p.optional ? last : i, -1);
3121
+ return params.map((p, i) => i < lastRequired ? {
3122
+ ...p,
3123
+ optional: false
3124
+ } : p);
3125
+ }
3126
+ __name(normaliseOptionalOrder, "normaliseOptionalOrder");
3003
3127
  function deriveMethodName(op, route) {
3004
3128
  if (op.sdk) return op.sdk;
3005
3129
  if (op.name) return nameToMethodName(op.name);
@@ -3215,7 +3339,7 @@ function sdkNeedsBigIntReplacer(root, includeInternal = false) {
3215
3339
  return false;
3216
3340
  }
3217
3341
  __name(sdkNeedsBigIntReplacer, "sdkNeedsBigIntReplacer");
3218
- function sdkNeedsBigIntReviver(root, includeInternal = false) {
3342
+ function sdkParsesJsonResponse(root, includeInternal = false) {
3219
3343
  for (const route of root.routes) {
3220
3344
  for (const op of route.operations) {
3221
3345
  if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
@@ -3226,7 +3350,44 @@ function sdkNeedsBigIntReviver(root, includeInternal = false) {
3226
3350
  }
3227
3351
  return false;
3228
3352
  }
3229
- __name(sdkNeedsBigIntReviver, "sdkNeedsBigIntReviver");
3353
+ __name(sdkParsesJsonResponse, "sdkParsesJsonResponse");
3354
+ function sdkResponsesUseBigInt(root, options, includeInternal = false) {
3355
+ const tainted = options.modelsWithBigInt;
3356
+ const reachesBigInt = /* @__PURE__ */ __name((type) => {
3357
+ switch (type.kind) {
3358
+ case "ref":
3359
+ return tainted?.has(type.name) ?? false;
3360
+ case "array":
3361
+ return reachesBigInt(type.item);
3362
+ case "lazy":
3363
+ return reachesBigInt(type.inner);
3364
+ case "tuple":
3365
+ return type.items.some(reachesBigInt);
3366
+ case "record":
3367
+ return reachesBigInt(type.value);
3368
+ case "union":
3369
+ case "discriminatedUnion":
3370
+ case "intersection":
3371
+ return type.members.some(reachesBigInt);
3372
+ case "inlineObject":
3373
+ return type.fields.some((f) => reachesBigInt(f.type));
3374
+ default:
3375
+ return typeNeedsScalar(type, "bigint");
3376
+ }
3377
+ }, "reachesBigInt");
3378
+ for (const route of root.routes) {
3379
+ for (const op of route.operations) {
3380
+ if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
3381
+ for (const resp of op.responses) {
3382
+ for (const body of resp.bodies) {
3383
+ if (classifyContentType2(body.contentType) === "json" && reachesBigInt(body.bodyType)) return true;
3384
+ }
3385
+ }
3386
+ }
3387
+ }
3388
+ return false;
3389
+ }
3390
+ __name(sdkResponsesUseBigInt, "sdkResponsesUseBigInt");
3230
3391
  function sdkNeedsJson(root, includeInternal = false) {
3231
3392
  for (const route of root.routes) {
3232
3393
  for (const op of route.operations) {
@@ -3291,9 +3452,9 @@ function generateTypeImports2(types, opFile, options, revivers = []) {
3291
3452
  unresolved.push(type);
3292
3453
  }
3293
3454
  }
3294
- const fromDir = dirname3(outPath);
3455
+ const fromDir = dirname4(outPath);
3295
3456
  for (const [typeOutPath, names] of byFile) {
3296
- let rel = relative3(fromDir, typeOutPath);
3457
+ let rel = relative4(fromDir, typeOutPath);
3297
3458
  rel = rel.replace(/\.ts$/, ".js");
3298
3459
  if (!rel.startsWith(".")) rel = "./" + rel;
3299
3460
  lines.push(`import type { ${names.sort().join(", ")} } from '${rel}';`);
@@ -3413,7 +3574,20 @@ function generateSdkOptions() {
3413
3574
  " return qs ? `?${qs}` : '';",
3414
3575
  "}",
3415
3576
  "",
3577
+ "/**",
3578
+ " * Read a JSON response body.",
3579
+ " *",
3580
+ " * No reviver: `bigIntReviver` matches any string of the form `123n` anywhere in the",
3581
+ " * document, so a contract with no bigint field would still have a legitimate string like",
3582
+ ' * "123n" silently turned into a BigInt. Clients whose contracts do use bigint import',
3583
+ " * `parseJsonWithBigInt` under this name instead.",
3584
+ " */",
3416
3585
  "export async function parseJson<T>(res: Response): Promise<T> {",
3586
+ " return JSON.parse(await res.text()) as T;",
3587
+ "}",
3588
+ "",
3589
+ "/** `parseJson` for contracts that declare a bigint, applying the `123n` reviver. */",
3590
+ "export async function parseJsonWithBigInt<T>(res: Response): Promise<T> {",
3417
3591
  " return JSON.parse(await res.text(), bigIntReviver) as T;",
3418
3592
  "}",
3419
3593
  ""
@@ -3500,6 +3674,7 @@ function generateAreaClient(input) {
3500
3674
  const unresolvedTypes = /* @__PURE__ */ new Set();
3501
3675
  let needsJson = false;
3502
3676
  let needsBigIntReplacer = false;
3677
+ let needsParseJson = false;
3503
3678
  let needsBigIntReviver = false;
3504
3679
  let needsQueryString = false;
3505
3680
  let needsReadContentType = false;
@@ -3518,27 +3693,32 @@ function generateAreaClient(input) {
3518
3693
  for (const alias of generateErrorBodyAliases(inline.root, inline.codegenOptions)) collectedErrorAliases.add(alias);
3519
3694
  if (sdkNeedsJson(inline.root, includeInternal)) needsJson = true;
3520
3695
  if (sdkNeedsBigIntReplacer(inline.root, includeInternal)) needsBigIntReplacer = true;
3521
- if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
3696
+ if (sdkParsesJsonResponse(inline.root, includeInternal)) needsParseJson = true;
3697
+ if (sdkResponsesUseBigInt(inline.root, inline.codegenOptions, includeInternal)) needsBigIntReviver = true;
3522
3698
  if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
3523
3699
  if (sdkNeedsReadContentType(inline.root, includeInternal)) needsReadContentType = true;
3524
3700
  for (const reviver of usedRevivers(methodLines)) {
3525
3701
  const stem = reviver.replace(/^revive/, "");
3526
3702
  const modelOut = inline.codegenOptions.modelOutPaths?.get(stem) ?? inline.codegenOptions.modelOutPaths?.get(stem.replace(/Output$/, ""));
3527
3703
  if (!modelOut) continue;
3528
- let rel = relative3(dirname3(outPath), modelOut).replace(/\.ts$/, ".js");
3704
+ let rel = relative4(dirname4(outPath), modelOut).replace(/\.ts$/, ".js");
3529
3705
  if (!rel.startsWith(".")) rel = "./" + rel;
3530
3706
  const set = reviversByImportPath.get(rel) ?? /* @__PURE__ */ new Set();
3531
3707
  set.add(reviver);
3532
3708
  reviversByImportPath.set(rel, set);
3533
3709
  }
3534
- const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
3710
+ const typesForFile = referencedTypes(collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal), [
3711
+ ...methodLines,
3712
+ ...generateErrorBodyAliases(inline.root, inline.codegenOptions),
3713
+ ...preludeLines
3714
+ ]);
3535
3715
  const { modelOutPaths } = inline.codegenOptions;
3536
3716
  if (modelOutPaths) {
3537
- const fromDir = dirname3(outPath);
3717
+ const fromDir = dirname4(outPath);
3538
3718
  for (const t of typesForFile) {
3539
3719
  const typeOutPath = modelOutPaths.get(t);
3540
3720
  if (typeOutPath) {
3541
- let rel = relative3(fromDir, typeOutPath).replace(/\.ts$/, ".js");
3721
+ let rel = relative4(fromDir, typeOutPath).replace(/\.ts$/, ".js");
3542
3722
  if (!rel.startsWith(".")) rel = "./" + rel;
3543
3723
  const set = typesByImportPath.get(rel) ?? /* @__PURE__ */ new Set();
3544
3724
  set.add(t);
@@ -3549,14 +3729,14 @@ function generateAreaClient(input) {
3549
3729
  }
3550
3730
  }
3551
3731
  }
3552
- let sdkOptionsRel = relative3(dirname3(outPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3732
+ let sdkOptionsRel = relative4(dirname4(outPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3553
3733
  if (!sdkOptionsRel.startsWith(".")) sdkOptionsRel = "./" + sdkOptionsRel;
3554
3734
  const lines = [];
3555
3735
  const jsonImport = needsJson ? ", JsonValue" : "";
3556
3736
  lines.push(`import type { SdkFetch${jsonImport} } from '${sdkOptionsRel}';`);
3557
3737
  const valueImports = [];
3558
3738
  if (needsBigIntReplacer) valueImports.push("bigIntReplacer");
3559
- if (needsBigIntReviver) valueImports.push("parseJson");
3739
+ if (needsParseJson) valueImports.push(needsBigIntReviver ? "parseJsonWithBigInt as parseJson" : "parseJson");
3560
3740
  if (needsQueryString) valueImports.push("buildQueryString");
3561
3741
  if (needsReadContentType) valueImports.push("readContentType");
3562
3742
  if (valueImports.length > 0) {
@@ -3580,6 +3760,8 @@ function generateAreaClient(input) {
3580
3760
  lines.push(`import type { ${t} } from './${pascalToDotCase(t)}.js';`);
3581
3761
  }
3582
3762
  if (areaNeedsDecimalImport) lines.push(DECIMAL_IMPORT);
3763
+ const areaHeaderLuxon = headerLuxonImport(collectedMethodLines, collectedRevivePrelude);
3764
+ if (areaHeaderLuxon) lines.push(areaHeaderLuxon);
3583
3765
  const importedClients = /* @__PURE__ */ new Set();
3584
3766
  for (const sc of subareaClients) {
3585
3767
  const key = `${sc.client.className}|${sc.client.importPath}`;
@@ -3655,7 +3837,6 @@ function generateSdkAggregator(input) {
3655
3837
  __name(generateSdkAggregator, "generateSdkAggregator");
3656
3838
 
3657
3839
  // src/codegen-plain-types.ts
3658
- import { relative as relative4, dirname as dirname4 } from "path";
3659
3840
  import { computeModelsWithOutput, collectExternalOutputRefs } from "@contractkit/core";
3660
3841
  function generatePlainTypes(root, context) {
3661
3842
  const target = context?.target ?? "client";
@@ -3684,6 +3865,10 @@ function generatePlainTypes(root, context) {
3684
3865
  ].sort();
3685
3866
  const needsDecimal = rootNeedsScalar(root, "decimal") || (context?.emitRevivers && context.modelsWithDecimal ? root.models.some((m) => context.modelsWithDecimal.has(m.name)) : false);
3686
3867
  if (needsDecimal) lines.push(DECIMAL_IMPORT);
3868
+ const luxonImports = [];
3869
+ if (rootNeedsScalar(root, "date") || rootNeedsScalar(root, "time") || rootNeedsScalar(root, "datetime")) luxonImports.push("DateTime");
3870
+ if (rootNeedsScalar(root, "duration")) luxonImports.push("Duration");
3871
+ if (luxonImports.length > 0) lines.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
3687
3872
  for (const ref of allExternalRefs) {
3688
3873
  const importPath = resolveImportPath(ref, context);
3689
3874
  lines.push(`import type { ${ref} } from '${importPath}';`);
@@ -3725,8 +3910,9 @@ function generatePlainTypes(root, context) {
3725
3910
  lines.push("");
3726
3911
  lines.push(DECIMAL_CONFIG_LINE);
3727
3912
  }
3728
- if (bodyLines.some((l) => l.includes("__dec("))) {
3729
- lines.push(...DECIMAL_COERCE_DECL);
3913
+ const coerceDecls = coerceDeclsFor(bodyLines);
3914
+ if (coerceDecls.length > 0) {
3915
+ lines.push(...coerceDecls);
3730
3916
  lines.push("");
3731
3917
  }
3732
3918
  lines.push(...bodyLines);
@@ -3772,8 +3958,7 @@ function generateComments2(model, outPath) {
3772
3958
  if (model.description) {
3773
3959
  for (const l of escapeJsDocLines(model.description)) lines.push(` * ${l}`);
3774
3960
  }
3775
- const relPath = outPath ? relative4(dirname4(outPath), model.loc.file) : model.loc.file;
3776
- lines.push(` * generated from [${model.name}](file://./${relPath}#L${model.loc.line})`);
3961
+ lines.push(` * generated from ${sourceLink(model.name, outPath, model.loc.file, model.loc.line)}`);
3777
3962
  lines.push(" */");
3778
3963
  return lines;
3779
3964
  }
@@ -3919,7 +4104,7 @@ function renderOutputField(field, outputCase, modelsWithOutput, target) {
3919
4104
  __name(renderOutputField, "renderOutputField");
3920
4105
 
3921
4106
  // src/codegen-mcp.ts
3922
- import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2 } from "@contractkit/core";
4107
+ import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2, toIdentifier as toIdentifier3 } from "@contractkit/core";
3923
4108
  import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
3924
4109
  function mcpConfig(op) {
3925
4110
  return op.mcp && typeof op.mcp === "object" ? op.mcp : void 0;
@@ -3976,7 +4161,7 @@ function buildArgsProps(route, op, modelsWithInput) {
3976
4161
  if (route.params.kind === "params") {
3977
4162
  for (const node of route.params.nodes) {
3978
4163
  props.push({
3979
- key: node.name,
4164
+ key: toIdentifier3(node.name),
3980
4165
  expr: renderInputType(node.type, modelsWithInput),
3981
4166
  optional: false
3982
4167
  });
@@ -4205,9 +4390,8 @@ function renderToolClass(plan, file, options) {
4205
4390
  const { route, op, toolName, className, argsConstName } = plan;
4206
4391
  const cfg = mcpConfig(op);
4207
4392
  const lines = [];
4208
- const relFile = options.outPath ? relative5(dirname5(options.outPath), file) : file;
4209
4393
  lines.push("/**");
4210
- lines.push(` * from [${basename3(file)}](file://./${relFile}#L${op.loc.line})`);
4394
+ lines.push(` * from ${sourceLink(basename3(file), options.outPath, file, op.loc.line)}`);
4211
4395
  lines.push(" */");
4212
4396
  lines.push("@Injectable()");
4213
4397
  lines.push(`export class ${className} implements McpToolHandler {`);
@@ -4231,7 +4415,8 @@ function renderToolClass(plan, file, options) {
4231
4415
  const callArgs = buildArgs(route, op);
4232
4416
  const isVoid = !primaryResponseBody(op);
4233
4417
  const structured = !!outExpr;
4234
- lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
4418
+ const argsParam = destructure.length > 0 ? "args" : "_args";
4419
+ lines.push(` async handle(${argsParam}: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {`);
4235
4420
  if (destructure.length > 0) {
4236
4421
  lines.push(` const { ${destructure.join(", ")} } = await parseAndValidate(args, ${argsConstName});`);
4237
4422
  }
@@ -4301,9 +4486,8 @@ function generateMcpFile(root, options = {}) {
4301
4486
  imports.push(`import { ${svc} } from '${mod}';`);
4302
4487
  }
4303
4488
  imports.push(...schemaImportLines(collectSchemaIds(plans, options.modelsWithInput), options));
4304
- const relFile = options.outPath ? relative5(dirname5(options.outPath), root.file) : root.file;
4305
4489
  const header = `// Auto-generated MCP tools
4306
- // generated from [${basename3(root.file)}](file://./${relFile})`;
4490
+ // generated from ${sourceLink(basename3(root.file), options.outPath, root.file)}`;
4307
4491
  return `${header}
4308
4492
  ${imports.join("\n")}
4309
4493
 
@@ -4320,11 +4504,19 @@ function generateMcpAggregator(entries) {
4320
4504
  lines.push(`import { McpToolHandlerMap } from '@maroonedsoftware/mcp';`);
4321
4505
  for (const e of sorted) lines.push(`import { ${e.registerFn} } from '${e.importPath}';`);
4322
4506
  lines.push("");
4323
- lines.push("/** Build + register the MCP tool catalog. Call once at startup. */");
4507
+ lines.push("/**");
4508
+ lines.push(" * Build the MCP tool catalog.");
4509
+ lines.push(" *");
4510
+ lines.push(" * Bind it to the `McpToolHandlerMap` token from a factory, which is what supplies the");
4511
+ lines.push(" * `Container` needed to resolve each handler:");
4512
+ lines.push(" *");
4513
+ lines.push(" * ```ts");
4514
+ lines.push(" * registry.register(McpToolHandlerMap).useFactory(registerMcpTools).asSingleton();");
4515
+ lines.push(" * ```");
4516
+ lines.push(" */");
4324
4517
  lines.push("export function registerMcpTools(container: Container): McpToolHandlerMap {");
4325
4518
  lines.push(" const map = new McpToolHandlerMap();");
4326
4519
  for (const e of sorted) lines.push(` ${e.registerFn}(map, container);`);
4327
- lines.push(" container.register(McpToolHandlerMap, { useValue: map });");
4328
4520
  lines.push(" return map;");
4329
4521
  lines.push("}");
4330
4522
  return lines.join("\n") + "\n";
@@ -4335,7 +4527,7 @@ function generateMcpRouter(options = {}) {
4335
4527
  return `import { ServerKitRouter, bodyParserMiddleware, requireSignature } from '@maroonedsoftware/koa';
4336
4528
  import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
4337
4529
 
4338
- /** Mount the MCP endpoint onto a ServerKit router. Call \`registerMcpTools(container)\` at startup. */
4530
+ /** Mount the MCP endpoint onto a ServerKit router. Bind \`registerMcpTools\` to the \`McpToolHandlerMap\` token. */
4339
4531
  export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
4340
4532
  router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
4341
4533
  const dispatcher = ctx.container.get(McpDispatcher);
@@ -4369,6 +4561,7 @@ function assertWithinBase(baseOutDir, outPath) {
4369
4561
  return outPath;
4370
4562
  }
4371
4563
  __name(assertWithinBase, "assertWithinBase");
4564
+ var TEMPLATE_VAR_RE_G = /\{(\w+)\}/g;
4372
4565
  function resolveTemplate(template, vars) {
4373
4566
  return template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
4374
4567
  }
@@ -4563,7 +4756,10 @@ function computePubliclyReachableTypes(opAsts, contractAsts, modelsWithInput, mo
4563
4756
  __name(computePubliclyReachableTypes, "computePubliclyReachableTypes");
4564
4757
 
4565
4758
  // src/index.ts
4566
- var TYPESCRIPT_CODEGEN_VERSION = "1";
4759
+ var BIGINT_SCALARS = /* @__PURE__ */ new Set([
4760
+ "bigint"
4761
+ ]);
4762
+ var TYPESCRIPT_CODEGEN_VERSION = "2";
4567
4763
  var CACHE_MANIFEST_FILENAME = "typescript-manifest.json";
4568
4764
  var plugin = {
4569
4765
  name: "typescript",
@@ -4608,11 +4804,19 @@ async function runTypescriptCodegen(inputs, ctx, config, rootDir) {
4608
4804
  fileExists: existsSync
4609
4805
  });
4610
4806
  deleteStalePaths(result.deletedPaths);
4807
+ const unresolved = /* @__PURE__ */ new Set();
4611
4808
  for (const { relativePath, content, ifAbsent } of result.filesToWrite) {
4809
+ for (const [, key] of relativePath.matchAll(TEMPLATE_VAR_RE_G)) unresolved.add(`${key}::${relativePath}`);
4612
4810
  ctx.emitFile(relativePath, content, ifAbsent ? {
4613
4811
  ifAbsent: true
4614
4812
  } : void 0);
4615
4813
  }
4814
+ for (const entry of [
4815
+ ...unresolved
4816
+ ].sort()) {
4817
+ const [key, outPath] = entry.split("::");
4818
+ ctx.warn?.(`Output path template variable {${key}} has no value, so '${outPath}' contains it literally. Declare it in the source file's 'options { keys { ${key}: ... } }' block, or remove it from the path template.`);
4819
+ }
4616
4820
  writeManifest(manifestPath, result.manifest);
4617
4821
  }
4618
4822
  __name(runTypescriptCodegen, "runTypescriptCodegen");
@@ -4818,7 +5022,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4818
5022
  const subConfigKey = stableSubConfig(config);
4819
5023
  const modelsWithInput = inputs.modelsWithInput;
4820
5024
  const modelsWithOutput = inputs.modelsWithOutput;
4821
- const modelsWithDecimal = computeModelsWithDecimal(inputs.contractRoots.flatMap((r) => r.models));
5025
+ const modelsWithDecimal = computeModelsWithScalar(inputs.contractRoots.flatMap((r) => r.models), DEFAULT_REVIVABLE_SCALARS);
5026
+ const modelsWithBigInt = computeModelsWithScalar(inputs.contractRoots.flatMap((r) => r.models), BIGINT_SCALARS);
4822
5027
  const modelMap = buildModelMap(inputs.contractRoots);
4823
5028
  const allFiles = [
4824
5029
  ...inputs.contractRoots.map((r) => r.file),
@@ -4861,6 +5066,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4861
5066
  // Not covered by `root`: adding a decimal to a model in a *different* .ck file changes
4862
5067
  // this file's revivers with no change to `root` or the config.
4863
5068
  modelsWithDecimal: sliceModelSet(refs, ownNames, modelsWithDecimal),
5069
+ modelsWithBigInt: sliceModelSet(refs, ownNames, modelsWithBigInt),
4864
5070
  sdkOptionsPath,
4865
5071
  sub: subConfigKey
4866
5072
  });
@@ -4876,7 +5082,10 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4876
5082
  modelsWithInput,
4877
5083
  modelsWithOutput,
4878
5084
  modelsWithDecimal,
4879
- emitRevivers: true
5085
+ emitRevivers: true,
5086
+ // An SDK client runs in a browser as readily as in Node, and its scaffold
5087
+ // declares no `@types/node`.
5088
+ target: "client"
4880
5089
  });
4881
5090
  } else {
4882
5091
  let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
@@ -4950,6 +5159,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4950
5159
  modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4951
5160
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4952
5161
  modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
5162
+ modelsWithBigInt: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithBigInt),
4953
5163
  sdkOptionsPath,
4954
5164
  className,
4955
5165
  includeInternal: config.includeInternal ?? false,
@@ -4969,6 +5179,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4969
5179
  modelsWithInput,
4970
5180
  modelsWithOutput,
4971
5181
  modelsWithDecimal,
5182
+ modelsWithBigInt,
4972
5183
  modelMap,
4973
5184
  includeInternal: config.includeInternal,
4974
5185
  clientClassName: className
@@ -4995,6 +5206,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
4995
5206
  modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4996
5207
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4997
5208
  modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
5209
+ modelsWithBigInt: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithBigInt),
4998
5210
  sdkOptionsPath,
4999
5211
  includeInternal: config.includeInternal ?? false,
5000
5212
  sub: subConfigKey
@@ -5013,6 +5225,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
5013
5225
  modelsWithInput,
5014
5226
  modelsWithOutput,
5015
5227
  modelsWithDecimal,
5228
+ modelsWithBigInt,
5016
5229
  modelMap,
5017
5230
  includeInternal: config.includeInternal
5018
5231
  })
@@ -5083,6 +5296,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
5083
5296
  modelsWithInput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithInput),
5084
5297
  modelsWithOutput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithOutput),
5085
5298
  modelsWithDecimal: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithDecimal),
5299
+ modelsWithBigInt: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithBigInt),
5086
5300
  sdkOptionsPath,
5087
5301
  includeInternal: config.includeInternal ?? false,
5088
5302
  sub: subConfigKey
@@ -5097,6 +5311,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
5097
5311
  modelsWithInput,
5098
5312
  modelsWithOutput,
5099
5313
  modelsWithDecimal,
5314
+ modelsWithBigInt,
5100
5315
  modelMap,
5101
5316
  includeInternal: config.includeInternal
5102
5317
  }
@@ -5238,11 +5453,15 @@ function collectZodOutput(config, rootDir, inputs, units) {
5238
5453
  render: /* @__PURE__ */ __name(() => [
5239
5454
  {
5240
5455
  relativePath: outPath,
5456
+ // Server-shaped, which is what this sub-generator has always emitted. The
5457
+ // standalone `zod:` output has no target option of its own; only the SDK's
5458
+ // schemas are client-shaped, and they pass their own target.
5241
5459
  content: generateContract(ast, {
5242
5460
  modelOutPaths,
5243
5461
  currentOutPath: outPath,
5244
5462
  modelsWithInput,
5245
- modelsWithOutput
5463
+ modelsWithOutput,
5464
+ target: "server"
5246
5465
  })
5247
5466
  }
5248
5467
  ], "render")