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