@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.
- package/.turbo/turbo-build$colon$ci.log +4 -4
- package/.turbo/turbo-test$colon$ci.log +26 -25
- package/CHANGELOG.md +285 -0
- package/dist/codegen-contract.d.ts +22 -6
- package/dist/codegen-contract.d.ts.map +1 -1
- package/dist/codegen-mcp.d.ts.map +1 -1
- package/dist/codegen-operation.d.ts.map +1 -1
- package/dist/codegen-plain-types.d.ts.map +1 -1
- package/dist/codegen-revive.d.ts +38 -4
- package/dist/codegen-revive.d.ts.map +1 -1
- package/dist/codegen-sdk.d.ts +2 -0
- package/dist/codegen-sdk.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +421 -202
- package/dist/index.js.map +1 -1
- package/dist/path-utils.d.ts +9 -0
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/ts-render.d.ts +14 -0
- package/dist/ts-render.d.ts.map +1 -1
- package/llms.txt +14 -0
- package/package.json +3 -2
- package/src/codegen-contract.ts +84 -68
- package/src/codegen-mcp.ts +22 -11
- package/src/codegen-operation.ts +32 -19
- package/src/codegen-plain-types.ts +15 -8
- package/src/codegen-revive.ts +140 -20
- package/src/codegen-sdk.ts +322 -63
- package/src/index.ts +46 -4
- package/src/path-utils.ts +10 -0
- package/src/ts-render.ts +26 -0
- package/tests/codegen-contract.test.ts +87 -32
- package/tests/codegen-mcp.test.ts +23 -1
- package/tests/codegen-operation.test.ts +68 -8
- package/tests/codegen-plain-types.test.ts +13 -5
- package/tests/codegen-sdk.test.ts +265 -10
- package/tests/codegen-server.test.ts +43 -1
- package/tests/helpers.ts +7 -2
- package/tests/pipeline.test.ts +51 -4
- 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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
552
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
877
|
-
if (s.min !== void 0)
|
|
878
|
-
if (s.max !== void 0)
|
|
879
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1375
|
-
let rel =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 ??
|
|
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
|
|
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
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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 =
|
|
2067
|
+
const fromDir = dirname3(outPath);
|
|
2008
2068
|
for (const [typeOutPath, names] of byFile) {
|
|
2009
|
-
let rel =
|
|
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,
|
|
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
|
|
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
|
-
|
|
2316
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2726
|
-
|
|
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(
|
|
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
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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 (
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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
|
|
3112
|
+
return {
|
|
3113
|
+
name,
|
|
3114
|
+
type: renderInputTsType(source.node, modelsWithInput),
|
|
3115
|
+
optional: true
|
|
3116
|
+
};
|
|
3001
3117
|
}
|
|
3002
|
-
__name(
|
|
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
|
|
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(
|
|
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 =
|
|
3455
|
+
const fromDir = dirname4(outPath);
|
|
3295
3456
|
for (const [typeOutPath, names] of byFile) {
|
|
3296
|
-
let rel =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
3729
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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("/**
|
|
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.
|
|
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
|
|
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 =
|
|
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")
|