@contractkit/plugin-typescript 0.32.0 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$ci.log +5 -5
- package/.turbo/turbo-test$colon$ci.log +22 -20
- package/CHANGELOG.md +94 -0
- package/README.md +9 -4
- 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.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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +538 -40
- package/dist/index.js.map +1 -1
- package/dist/ts-render.d.ts.map +1 -1
- package/llms.txt +111 -0
- package/package.json +2 -2
- package/src/codegen-contract.ts +58 -3
- package/src/codegen-mcp.ts +5 -0
- package/src/codegen-operation.ts +23 -1
- 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 +30 -3
- package/src/ts-render.ts +6 -0
- package/tests/codegen-contract.test.ts +124 -1
- package/tests/codegen-operation.test.ts +14 -0
- package/tests/codegen-sdk.test.ts +77 -6
- package/tests/pipeline.test.ts +24 -0
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
|
}
|
|
@@ -1251,6 +1537,9 @@ function generateOp(root, options = {}) {
|
|
|
1251
1537
|
if (luxonImports.length > 0) {
|
|
1252
1538
|
body.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
|
|
1253
1539
|
}
|
|
1540
|
+
if (uses("Decimal")) {
|
|
1541
|
+
body.push(DECIMAL_IMPORT);
|
|
1542
|
+
}
|
|
1254
1543
|
if (uses("parseAndValidate")) {
|
|
1255
1544
|
body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
|
|
1256
1545
|
}
|
|
@@ -1571,6 +1860,8 @@ function serverTsScalar(name) {
|
|
|
1571
1860
|
return "number";
|
|
1572
1861
|
case "bigint":
|
|
1573
1862
|
return "bigint";
|
|
1863
|
+
case "decimal":
|
|
1864
|
+
return "Decimal";
|
|
1574
1865
|
case "boolean":
|
|
1575
1866
|
return "boolean";
|
|
1576
1867
|
case "date":
|
|
@@ -1939,7 +2230,7 @@ function deriveTypeImportPath(file, template) {
|
|
|
1939
2230
|
__name(deriveTypeImportPath, "deriveTypeImportPath");
|
|
1940
2231
|
|
|
1941
2232
|
// src/index.ts
|
|
1942
|
-
import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform } from "@contractkit/core";
|
|
2233
|
+
import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform, computeModelsWithDecimal } from "@contractkit/core";
|
|
1943
2234
|
|
|
1944
2235
|
// src/codegen-sdk.ts
|
|
1945
2236
|
import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
|
|
@@ -2006,9 +2297,25 @@ function generateSdk(root, options = {}) {
|
|
|
2006
2297
|
const includeInternal = options.includeInternal ?? false;
|
|
2007
2298
|
const types = collectTypes2(root, options.modelsWithInput, options.modelsWithOutput, includeInternal);
|
|
2008
2299
|
const clientClassName = options.clientClassName ?? deriveClientClassName(root.file);
|
|
2300
|
+
const inlineRevivers = /* @__PURE__ */ new Map();
|
|
2301
|
+
const classBody = [];
|
|
2302
|
+
for (const route of root.routes) {
|
|
2303
|
+
for (const op of route.operations) {
|
|
2304
|
+
const mods = resolveModifiers2(route, op);
|
|
2305
|
+
if (!includeInternal && mods.includes("internal")) continue;
|
|
2306
|
+
classBody.push("");
|
|
2307
|
+
if (mods.includes("deprecated")) classBody.push(" /** @deprecated */");
|
|
2308
|
+
classBody.push(...generateMethod(route, op, root.file, options, inlineRevivers));
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const inlineReviverDecls = [
|
|
2312
|
+
...inlineRevivers.values()
|
|
2313
|
+
].flat();
|
|
2314
|
+
const decimalPrelude = decimalPreludeFor(inlineReviverDecls);
|
|
2009
2315
|
if (types.length > 0) {
|
|
2010
|
-
lines.push(...generateTypeImports2(types, root.file, options));
|
|
2316
|
+
lines.push(...generateTypeImports2(types, root.file, options, usedRevivers(classBody)));
|
|
2011
2317
|
}
|
|
2318
|
+
lines.push(...decimalPrelude.imports);
|
|
2012
2319
|
if (options.sdkOptionsPath && options.outPath) {
|
|
2013
2320
|
let rel = relative3(dirname3(options.outPath), options.sdkOptionsPath);
|
|
2014
2321
|
rel = rel.replace(/\.ts$/, ".js");
|
|
@@ -2106,21 +2413,21 @@ function generateSdk(root, options = {}) {
|
|
|
2106
2413
|
lines.push(...errorAliases);
|
|
2107
2414
|
lines.push("");
|
|
2108
2415
|
}
|
|
2416
|
+
if (decimalPrelude.decls.length > 0) {
|
|
2417
|
+
lines.push("");
|
|
2418
|
+
lines.push(...decimalPrelude.decls);
|
|
2419
|
+
}
|
|
2420
|
+
for (const decl of inlineRevivers.values()) {
|
|
2421
|
+
lines.push("");
|
|
2422
|
+
lines.push(...decl);
|
|
2423
|
+
}
|
|
2109
2424
|
lines.push("/**");
|
|
2110
2425
|
const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
|
|
2111
2426
|
lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
|
|
2112
2427
|
lines.push(" */");
|
|
2113
2428
|
lines.push(`export class ${clientClassName} {`);
|
|
2114
2429
|
lines.push(" constructor(private fetch: SdkFetch) {}");
|
|
2115
|
-
|
|
2116
|
-
for (const op of route.operations) {
|
|
2117
|
-
const mods = resolveModifiers2(route, op);
|
|
2118
|
-
if (!includeInternal && mods.includes("internal")) continue;
|
|
2119
|
-
lines.push("");
|
|
2120
|
-
if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
|
|
2121
|
-
lines.push(...generateMethod(route, op, root.file, options));
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2430
|
+
lines.push(...classBody);
|
|
2124
2431
|
lines.push("}");
|
|
2125
2432
|
lines.push("");
|
|
2126
2433
|
return lines.join("\n");
|
|
@@ -2130,25 +2437,77 @@ function generateClientMethods(root, options) {
|
|
|
2130
2437
|
const lines = [];
|
|
2131
2438
|
const methodNames = [];
|
|
2132
2439
|
const includeInternal = options.includeInternal ?? false;
|
|
2440
|
+
const inlineRevivers = /* @__PURE__ */ new Map();
|
|
2133
2441
|
for (const route of root.routes) {
|
|
2134
2442
|
for (const op of route.operations) {
|
|
2135
2443
|
const mods = resolveModifiers2(route, op);
|
|
2136
2444
|
if (!includeInternal && mods.includes("internal")) continue;
|
|
2137
2445
|
lines.push("");
|
|
2138
2446
|
if (mods.includes("deprecated")) lines.push(" /** @deprecated */");
|
|
2139
|
-
lines.push(...generateMethod(route, op, root.file, options));
|
|
2447
|
+
lines.push(...generateMethod(route, op, root.file, options, inlineRevivers));
|
|
2140
2448
|
methodNames.push(deriveMethodName(op, route));
|
|
2141
2449
|
}
|
|
2142
2450
|
}
|
|
2451
|
+
const declLines = [
|
|
2452
|
+
...inlineRevivers.values()
|
|
2453
|
+
].flat();
|
|
2454
|
+
const { decls } = decimalPreludeFor(declLines);
|
|
2455
|
+
const preludeLines = [
|
|
2456
|
+
...decls.length > 0 ? [
|
|
2457
|
+
"",
|
|
2458
|
+
...decls
|
|
2459
|
+
] : [],
|
|
2460
|
+
...[
|
|
2461
|
+
...inlineRevivers.values()
|
|
2462
|
+
].flatMap((decl) => [
|
|
2463
|
+
"",
|
|
2464
|
+
...decl
|
|
2465
|
+
])
|
|
2466
|
+
];
|
|
2143
2467
|
return {
|
|
2144
2468
|
lines,
|
|
2145
|
-
methodNames
|
|
2469
|
+
methodNames,
|
|
2470
|
+
preludeLines,
|
|
2471
|
+
needsDecimalImport: decls.length > 0
|
|
2146
2472
|
};
|
|
2147
2473
|
}
|
|
2148
2474
|
__name(generateClientMethods, "generateClientMethods");
|
|
2149
|
-
function
|
|
2475
|
+
function decimalPreludeFor(declLines) {
|
|
2476
|
+
if (!declLines.some((l) => l.includes("__dec("))) return {
|
|
2477
|
+
imports: [],
|
|
2478
|
+
decls: []
|
|
2479
|
+
};
|
|
2480
|
+
return {
|
|
2481
|
+
imports: [
|
|
2482
|
+
DECIMAL_IMPORT
|
|
2483
|
+
],
|
|
2484
|
+
decls: [
|
|
2485
|
+
DECIMAL_CONFIG_LINE,
|
|
2486
|
+
"",
|
|
2487
|
+
...DECIMAL_COERCE_DECL
|
|
2488
|
+
]
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
__name(decimalPreludeFor, "decimalPreludeFor");
|
|
2492
|
+
function usedRevivers(lines) {
|
|
2493
|
+
const found = /* @__PURE__ */ new Set();
|
|
2494
|
+
for (const m of lines.join("\n").matchAll(/\brevive[A-Z]\w*/g)) found.add(m[0]);
|
|
2495
|
+
return [
|
|
2496
|
+
...found
|
|
2497
|
+
].sort();
|
|
2498
|
+
}
|
|
2499
|
+
__name(usedRevivers, "usedRevivers");
|
|
2500
|
+
function generateMethod(route, op, file, options, inlineRevivers) {
|
|
2501
|
+
const revive = options.modelsWithDecimal && options.modelsWithDecimal.size > 0 && inlineRevivers ? {
|
|
2502
|
+
modelsWithDecimal: options.modelsWithDecimal,
|
|
2503
|
+
modelsWithOutput: options.modelsWithOutput,
|
|
2504
|
+
modelMap: options.modelMap,
|
|
2505
|
+
inlineRevivers,
|
|
2506
|
+
nameHint: ""
|
|
2507
|
+
} : void 0;
|
|
2150
2508
|
const lines = [];
|
|
2151
2509
|
const methodName = deriveMethodName(op, route);
|
|
2510
|
+
const mRevive = hint(revive, `${methodName.charAt(0).toUpperCase()}${methodName.slice(1)}`);
|
|
2152
2511
|
const httpMethod = op.method.toUpperCase();
|
|
2153
2512
|
const { modelsWithInput, modelsWithOutput } = options;
|
|
2154
2513
|
const params = buildMethodParams(route, op, modelsWithInput);
|
|
@@ -2276,23 +2635,23 @@ function generateMethod(route, op, file, options) {
|
|
|
2276
2635
|
lines.push(` switch (result.status) {`);
|
|
2277
2636
|
for (const resp of rest) {
|
|
2278
2637
|
lines.push(` case ${resp.statusCode}:`);
|
|
2279
|
-
lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true));
|
|
2638
|
+
lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true, mRevive));
|
|
2280
2639
|
}
|
|
2281
2640
|
lines.push(` default:`);
|
|
2282
|
-
lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true));
|
|
2641
|
+
lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true, mRevive));
|
|
2283
2642
|
lines.push(` }`);
|
|
2284
2643
|
} else if (primaryBodies.length > 1) {
|
|
2285
|
-
lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false));
|
|
2644
|
+
lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false, mRevive));
|
|
2286
2645
|
} else if (hasRespHeaders) {
|
|
2287
2646
|
const headerEntries = sdkHeaderEntries(respHeaders);
|
|
2288
2647
|
if (isVoid) {
|
|
2289
2648
|
lines.push(` return { headers: { ${headerEntries} } };`);
|
|
2290
2649
|
} else {
|
|
2291
|
-
lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
|
|
2650
|
+
lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput, hint(mRevive, primaryResponse.statusCode))};`);
|
|
2292
2651
|
lines.push(` return { data, headers: { ${headerEntries} } };`);
|
|
2293
2652
|
}
|
|
2294
2653
|
} else if (!isVoid) {
|
|
2295
|
-
lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
|
|
2654
|
+
lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput, hint(mRevive, primaryResponse.statusCode))};`);
|
|
2296
2655
|
}
|
|
2297
2656
|
lines.push(" }");
|
|
2298
2657
|
return lines;
|
|
@@ -2305,13 +2664,59 @@ function sdkDataType(body, modelsWithOutput) {
|
|
|
2305
2664
|
return renderOutputTsType(body.bodyType, modelsWithOutput);
|
|
2306
2665
|
}
|
|
2307
2666
|
__name(sdkDataType, "sdkDataType");
|
|
2308
|
-
function sdkReadExpr(body, modelsWithOutput) {
|
|
2667
|
+
function sdkReadExpr(body, modelsWithOutput, revive) {
|
|
2309
2668
|
const category = classifyContentType2(body.contentType);
|
|
2310
2669
|
if (category === "text") return "await result.text()";
|
|
2311
2670
|
if (category === "binary") return "await result.blob()";
|
|
2312
|
-
|
|
2671
|
+
const tsType = renderOutputTsType(body.bodyType, modelsWithOutput);
|
|
2672
|
+
const read = `await parseJson<${tsType}>(result)`;
|
|
2673
|
+
const reviver = reviveExprFor(body.bodyType, revive);
|
|
2674
|
+
if (!reviver) return read;
|
|
2675
|
+
return reviver.kind === "array" ? `(${read}).map(${reviver.name})` : `${reviver.name}(${read})`;
|
|
2313
2676
|
}
|
|
2314
2677
|
__name(sdkReadExpr, "sdkReadExpr");
|
|
2678
|
+
function hint(revive, segment) {
|
|
2679
|
+
if (!revive) return void 0;
|
|
2680
|
+
return {
|
|
2681
|
+
...revive,
|
|
2682
|
+
nameHint: `${revive.nameHint}${segment}`
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
__name(hint, "hint");
|
|
2686
|
+
function reviveExprFor(bodyType, ctx) {
|
|
2687
|
+
if (!ctx || ctx.modelsWithDecimal.size === 0) return null;
|
|
2688
|
+
const opts = {
|
|
2689
|
+
modelsWithDecimal: ctx.modelsWithDecimal,
|
|
2690
|
+
modelsWithOutput: ctx.modelsWithOutput,
|
|
2691
|
+
modelMap: ctx.modelMap
|
|
2692
|
+
};
|
|
2693
|
+
if (!typeReachesDecimal(bodyType, opts)) return null;
|
|
2694
|
+
const refName = /* @__PURE__ */ __name((t) => t.kind === "ref" ? t.name : t.kind === "lazy" ? refName(t.inner) : null, "refName");
|
|
2695
|
+
const pick = /* @__PURE__ */ __name((name) => reviveFnName(name, ctx.modelsWithOutput?.has(name) ? "output" : "base"), "pick");
|
|
2696
|
+
const direct = refName(bodyType);
|
|
2697
|
+
if (direct && ctx.modelsWithDecimal.has(direct)) return {
|
|
2698
|
+
name: pick(direct),
|
|
2699
|
+
kind: "value"
|
|
2700
|
+
};
|
|
2701
|
+
if (bodyType.kind === "array") {
|
|
2702
|
+
const item = refName(bodyType.item);
|
|
2703
|
+
if (item && ctx.modelsWithDecimal.has(item)) return {
|
|
2704
|
+
name: pick(item),
|
|
2705
|
+
kind: "array"
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
const fnName = `__revive${ctx.nameHint}`;
|
|
2709
|
+
if (!ctx.inlineRevivers.has(fnName)) {
|
|
2710
|
+
const decl = renderInlineReviver(fnName, renderOutputTsType(bodyType, ctx.modelsWithOutput), bodyType, opts);
|
|
2711
|
+
if (!decl) return null;
|
|
2712
|
+
ctx.inlineRevivers.set(fnName, decl);
|
|
2713
|
+
}
|
|
2714
|
+
return {
|
|
2715
|
+
name: fnName,
|
|
2716
|
+
kind: "value"
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
__name(reviveExprFor, "reviveExprFor");
|
|
2315
2720
|
function renderSdkHeadersShape(headers, modelsWithOutput) {
|
|
2316
2721
|
const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`);
|
|
2317
2722
|
return `{ ${fields.join("; ")} }`;
|
|
@@ -2358,7 +2763,7 @@ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
|
|
|
2358
2763
|
].join("; ")} }`);
|
|
2359
2764
|
}
|
|
2360
2765
|
__name(sdkResponseMembers, "sdkResponseMembers");
|
|
2361
|
-
function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
|
|
2766
|
+
function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive) {
|
|
2362
2767
|
const bodies = resp.bodies;
|
|
2363
2768
|
const headers = resp.headers ?? [];
|
|
2364
2769
|
const leading = includeStatus ? [
|
|
@@ -2379,7 +2784,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
|
|
|
2379
2784
|
const fields = [
|
|
2380
2785
|
...leading,
|
|
2381
2786
|
`contentType: '${bodies[0].contentType}'`,
|
|
2382
|
-
`data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
|
|
2787
|
+
`data: ${sdkReadExpr(bodies[0], modelsWithOutput, hint(revive, resp.statusCode))}`,
|
|
2383
2788
|
...trailing
|
|
2384
2789
|
];
|
|
2385
2790
|
return [
|
|
@@ -2392,7 +2797,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
|
|
|
2392
2797
|
const fields = [
|
|
2393
2798
|
...leading,
|
|
2394
2799
|
`contentType: readContentType(result) as ${cast}`,
|
|
2395
|
-
`data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
|
|
2800
|
+
`data: ${sdkReadExpr(bodies[0], modelsWithOutput, hint(revive, resp.statusCode))}`,
|
|
2396
2801
|
...trailing
|
|
2397
2802
|
];
|
|
2398
2803
|
return [
|
|
@@ -2402,11 +2807,11 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
|
|
|
2402
2807
|
const lines = [
|
|
2403
2808
|
`${indent}switch (readContentType(result)) {`
|
|
2404
2809
|
];
|
|
2405
|
-
for (const body of bodies.slice(1)) {
|
|
2810
|
+
for (const [i, body] of bodies.slice(1).entries()) {
|
|
2406
2811
|
const fields = [
|
|
2407
2812
|
...leading,
|
|
2408
2813
|
`contentType: '${body.contentType}'`,
|
|
2409
|
-
`data: ${sdkReadExpr(body, modelsWithOutput)}`,
|
|
2814
|
+
`data: ${sdkReadExpr(body, modelsWithOutput, hint(revive, `${resp.statusCode}_${i + 1}`))}`,
|
|
2410
2815
|
...trailing
|
|
2411
2816
|
];
|
|
2412
2817
|
lines.push(`${indent} case '${body.contentType}':`);
|
|
@@ -2416,7 +2821,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
|
|
|
2416
2821
|
const fallbackFields = [
|
|
2417
2822
|
...leading,
|
|
2418
2823
|
`contentType: '${first.contentType}'`,
|
|
2419
|
-
`data: ${sdkReadExpr(first, modelsWithOutput)}`,
|
|
2824
|
+
`data: ${sdkReadExpr(first, modelsWithOutput, hint(revive, `${resp.statusCode}_0`))}`,
|
|
2420
2825
|
...trailing
|
|
2421
2826
|
];
|
|
2422
2827
|
lines.push(`${indent} default:`);
|
|
@@ -2870,7 +3275,7 @@ function collectTypeNodeRefs2(type, out) {
|
|
|
2870
3275
|
}
|
|
2871
3276
|
}
|
|
2872
3277
|
__name(collectTypeNodeRefs2, "collectTypeNodeRefs");
|
|
2873
|
-
function generateTypeImports2(types, opFile, options) {
|
|
3278
|
+
function generateTypeImports2(types, opFile, options, revivers = []) {
|
|
2874
3279
|
const lines = [];
|
|
2875
3280
|
const { modelOutPaths, outPath } = options;
|
|
2876
3281
|
if (modelOutPaths && outPath) {
|
|
@@ -2892,6 +3297,8 @@ function generateTypeImports2(types, opFile, options) {
|
|
|
2892
3297
|
rel = rel.replace(/\.ts$/, ".js");
|
|
2893
3298
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
2894
3299
|
lines.push(`import type { ${names.sort().join(", ")} } from '${rel}';`);
|
|
3300
|
+
const fromHere = revivers.filter((r) => modelOutPaths.get(reviverModelName(r, names)) === typeOutPath);
|
|
3301
|
+
if (fromHere.length > 0) lines.push(`import { ${fromHere.sort().join(", ")} } from '${rel}';`);
|
|
2895
3302
|
}
|
|
2896
3303
|
for (const type of unresolved) {
|
|
2897
3304
|
const moduleName = pascalToDotCase(type);
|
|
@@ -2904,6 +3311,13 @@ function generateTypeImports2(types, opFile, options) {
|
|
|
2904
3311
|
return lines;
|
|
2905
3312
|
}
|
|
2906
3313
|
__name(generateTypeImports2, "generateTypeImports");
|
|
3314
|
+
function reviverModelName(reviver, namesInModule) {
|
|
3315
|
+
const stem = reviver.replace(/^revive/, "");
|
|
3316
|
+
if (namesInModule.includes(stem)) return stem;
|
|
3317
|
+
const base = stem.replace(/Output$/, "");
|
|
3318
|
+
return namesInModule.includes(base) ? base : stem;
|
|
3319
|
+
}
|
|
3320
|
+
__name(reviverModelName, "reviverModelName");
|
|
2907
3321
|
function deriveTypeImportPath2(file, template) {
|
|
2908
3322
|
const base = file.split("/").pop()?.replace(/\.(op|ck)$/, "") ?? "resource";
|
|
2909
3323
|
const module = base.split(".")[0] ?? base;
|
|
@@ -3009,6 +3423,7 @@ __name(generateSdkOptions, "generateSdkOptions");
|
|
|
3009
3423
|
var SCAFFOLD_DEP_VERSIONS = {
|
|
3010
3424
|
zod: "^4.3.6",
|
|
3011
3425
|
luxon: "^3.5.0",
|
|
3426
|
+
decimalJs: "^10.4.3",
|
|
3012
3427
|
typesLuxon: "^3.4.2",
|
|
3013
3428
|
typescript: "^6.0.3"
|
|
3014
3429
|
};
|
|
@@ -3016,6 +3431,7 @@ function generateSdkPackageJson(input) {
|
|
|
3016
3431
|
const dependencies = {};
|
|
3017
3432
|
if (input.deps.zod) dependencies.zod = SCAFFOLD_DEP_VERSIONS.zod;
|
|
3018
3433
|
if (input.deps.luxon) dependencies.luxon = SCAFFOLD_DEP_VERSIONS.luxon;
|
|
3434
|
+
if (input.deps.decimal) dependencies["decimal.js"] = SCAFFOLD_DEP_VERSIONS.decimalJs;
|
|
3019
3435
|
const devDependencies = {
|
|
3020
3436
|
typescript: SCAFFOLD_DEP_VERSIONS.typescript
|
|
3021
3437
|
};
|
|
@@ -3075,6 +3491,9 @@ function generateAreaClient(input) {
|
|
|
3075
3491
|
const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
|
|
3076
3492
|
const className = deriveAreaClientClassName(area);
|
|
3077
3493
|
const collectedMethodLines = [];
|
|
3494
|
+
const collectedRevivePrelude = [];
|
|
3495
|
+
let areaNeedsDecimalImport = false;
|
|
3496
|
+
const reviversByImportPath = /* @__PURE__ */ new Map();
|
|
3078
3497
|
const collectedErrorAliases = /* @__PURE__ */ new Set();
|
|
3079
3498
|
const seenMethods = /* @__PURE__ */ new Set();
|
|
3080
3499
|
const typesByImportPath = /* @__PURE__ */ new Map();
|
|
@@ -3086,7 +3505,9 @@ function generateAreaClient(input) {
|
|
|
3086
3505
|
let needsReadContentType = false;
|
|
3087
3506
|
for (const inline of inlineFiles) {
|
|
3088
3507
|
const includeInternal = inline.codegenOptions.includeInternal ?? false;
|
|
3089
|
-
const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
|
|
3508
|
+
const { lines: methodLines, methodNames, preludeLines, needsDecimalImport } = generateClientMethods(inline.root, inline.codegenOptions);
|
|
3509
|
+
collectedRevivePrelude.push(...preludeLines);
|
|
3510
|
+
if (needsDecimalImport) areaNeedsDecimalImport = true;
|
|
3090
3511
|
for (const name of methodNames) {
|
|
3091
3512
|
if (seenMethods.has(name)) {
|
|
3092
3513
|
throw new Error(`[sdk] duplicate method '${name}' in area '${area}': two area-level files contribute the same method. Disambiguate via 'sdk:' or move one into a subarea.`);
|
|
@@ -3100,6 +3521,16 @@ function generateAreaClient(input) {
|
|
|
3100
3521
|
if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
|
|
3101
3522
|
if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
|
|
3102
3523
|
if (sdkNeedsReadContentType(inline.root, includeInternal)) needsReadContentType = true;
|
|
3524
|
+
for (const reviver of usedRevivers(methodLines)) {
|
|
3525
|
+
const stem = reviver.replace(/^revive/, "");
|
|
3526
|
+
const modelOut = inline.codegenOptions.modelOutPaths?.get(stem) ?? inline.codegenOptions.modelOutPaths?.get(stem.replace(/Output$/, ""));
|
|
3527
|
+
if (!modelOut) continue;
|
|
3528
|
+
let rel = relative3(dirname3(outPath), modelOut).replace(/\.ts$/, ".js");
|
|
3529
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
3530
|
+
const set = reviversByImportPath.get(rel) ?? /* @__PURE__ */ new Set();
|
|
3531
|
+
set.add(reviver);
|
|
3532
|
+
reviversByImportPath.set(rel, set);
|
|
3533
|
+
}
|
|
3103
3534
|
const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
|
|
3104
3535
|
const { modelOutPaths } = inline.codegenOptions;
|
|
3105
3536
|
if (modelOutPaths) {
|
|
@@ -3138,12 +3569,17 @@ function generateAreaClient(input) {
|
|
|
3138
3569
|
...typesByImportPath.get(path)
|
|
3139
3570
|
].sort();
|
|
3140
3571
|
lines.push(`import type { ${names.join(", ")} } from '${path}';`);
|
|
3572
|
+
const revivers = reviversByImportPath.get(path);
|
|
3573
|
+
if (revivers && revivers.size > 0) lines.push(`import { ${[
|
|
3574
|
+
...revivers
|
|
3575
|
+
].sort().join(", ")} } from '${path}';`);
|
|
3141
3576
|
}
|
|
3142
3577
|
for (const t of [
|
|
3143
3578
|
...unresolvedTypes
|
|
3144
3579
|
].sort()) {
|
|
3145
3580
|
lines.push(`import type { ${t} } from './${pascalToDotCase(t)}.js';`);
|
|
3146
3581
|
}
|
|
3582
|
+
if (areaNeedsDecimalImport) lines.push(DECIMAL_IMPORT);
|
|
3147
3583
|
const importedClients = /* @__PURE__ */ new Set();
|
|
3148
3584
|
for (const sc of subareaClients) {
|
|
3149
3585
|
const key = `${sc.client.className}|${sc.client.importPath}`;
|
|
@@ -3156,6 +3592,10 @@ function generateAreaClient(input) {
|
|
|
3156
3592
|
lines.push(...collectedErrorAliases);
|
|
3157
3593
|
lines.push("");
|
|
3158
3594
|
}
|
|
3595
|
+
if (collectedRevivePrelude.length > 0) {
|
|
3596
|
+
lines.push(...collectedRevivePrelude);
|
|
3597
|
+
lines.push("");
|
|
3598
|
+
}
|
|
3159
3599
|
lines.push(`export class ${className} {`);
|
|
3160
3600
|
for (const sc of subareaClients) {
|
|
3161
3601
|
lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
|
|
@@ -3242,9 +3682,14 @@ function generatePlainTypes(root, context) {
|
|
|
3242
3682
|
...externalOutputRefs
|
|
3243
3683
|
])
|
|
3244
3684
|
].sort();
|
|
3685
|
+
const needsDecimal = rootNeedsScalar(root, "decimal") || (context?.emitRevivers && context.modelsWithDecimal ? root.models.some((m) => context.modelsWithDecimal.has(m.name)) : false);
|
|
3686
|
+
if (needsDecimal) lines.push(DECIMAL_IMPORT);
|
|
3245
3687
|
for (const ref of allExternalRefs) {
|
|
3246
3688
|
const importPath = resolveImportPath(ref, context);
|
|
3247
3689
|
lines.push(`import type { ${ref} } from '${importPath}';`);
|
|
3690
|
+
if (context?.emitRevivers && context.modelsWithDecimal?.has(ref)) {
|
|
3691
|
+
lines.push(`import { ${reviveFnName(ref)} } from '${importPath}';`);
|
|
3692
|
+
}
|
|
3248
3693
|
}
|
|
3249
3694
|
if (allExternalRefs.length > 0) lines.push("");
|
|
3250
3695
|
if (rootNeedsScalar(root, "json")) {
|
|
@@ -3259,10 +3704,32 @@ function generatePlainTypes(root, context) {
|
|
|
3259
3704
|
m.name,
|
|
3260
3705
|
m
|
|
3261
3706
|
]));
|
|
3707
|
+
const reviveOpts = context?.emitRevivers && context.modelsWithDecimal ? {
|
|
3708
|
+
modelsWithDecimal: context.modelsWithDecimal,
|
|
3709
|
+
modelsWithOutput: allModelsWithOutput,
|
|
3710
|
+
modelMap
|
|
3711
|
+
} : void 0;
|
|
3712
|
+
const bodyLines = [];
|
|
3262
3713
|
for (const model of topoSortModels(root.models)) {
|
|
3263
|
-
|
|
3714
|
+
bodyLines.push(...generateModel2(model, target, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
|
|
3715
|
+
if (reviveOpts) {
|
|
3716
|
+
const revivers = renderReviveFunctions(model, reviveOpts);
|
|
3717
|
+
if (revivers.length > 0) {
|
|
3718
|
+
bodyLines.push("");
|
|
3719
|
+
bodyLines.push(...revivers);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
bodyLines.push("");
|
|
3723
|
+
}
|
|
3724
|
+
if (needsDecimal) {
|
|
3725
|
+
lines.push("");
|
|
3726
|
+
lines.push(DECIMAL_CONFIG_LINE);
|
|
3727
|
+
}
|
|
3728
|
+
if (bodyLines.some((l) => l.includes("__dec("))) {
|
|
3729
|
+
lines.push(...DECIMAL_COERCE_DECL);
|
|
3264
3730
|
lines.push("");
|
|
3265
3731
|
}
|
|
3732
|
+
lines.push(...bodyLines);
|
|
3266
3733
|
return lines.join("\n");
|
|
3267
3734
|
}
|
|
3268
3735
|
__name(generatePlainTypes, "generatePlainTypes");
|
|
@@ -3701,6 +4168,9 @@ function scalarHelperLines(body) {
|
|
|
3701
4168
|
if (body.includes("_ZodDatetime")) {
|
|
3702
4169
|
lines.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
|
|
3703
4170
|
}
|
|
4171
|
+
if (body.includes("_ZodDecimal")) {
|
|
4172
|
+
lines.push(...DECIMAL_PRELUDE_LINES);
|
|
4173
|
+
}
|
|
3704
4174
|
if (body.includes("_ZodInterval")) {
|
|
3705
4175
|
lines.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
|
|
3706
4176
|
}
|
|
@@ -3814,6 +4284,7 @@ function generateMcpFile(root, options = {}) {
|
|
|
3814
4284
|
if (/\bInterval\b/.test(bodyWithHelpers)) luxon.push("Interval");
|
|
3815
4285
|
if (/\bDuration\b/.test(bodyWithHelpers)) luxon.push("Duration");
|
|
3816
4286
|
if (luxon.length > 0) imports.push(`import { ${luxon.join(", ")} } from 'luxon';`);
|
|
4287
|
+
if (/\bDecimal\b/.test(bodyWithHelpers)) imports.push(DECIMAL_IMPORT);
|
|
3817
4288
|
imports.push(`import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';`);
|
|
3818
4289
|
imports.push(`import type { McpToolHandler, McpToolHandlerMap, McpToolContext } from '@maroonedsoftware/mcp';`);
|
|
3819
4290
|
if (needsParseAndValidate) imports.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
|
|
@@ -4346,6 +4817,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4346
4817
|
const subConfigKey = stableSubConfig(config);
|
|
4347
4818
|
const modelsWithInput = inputs.modelsWithInput;
|
|
4348
4819
|
const modelsWithOutput = inputs.modelsWithOutput;
|
|
4820
|
+
const modelsWithDecimal = computeModelsWithDecimal(inputs.contractRoots.flatMap((r) => r.models));
|
|
4349
4821
|
const modelMap = buildModelMap(inputs.contractRoots);
|
|
4350
4822
|
const allFiles = [
|
|
4351
4823
|
...inputs.contractRoots.map((r) => r.file),
|
|
@@ -4385,6 +4857,9 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4385
4857
|
outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
4386
4858
|
modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
|
|
4387
4859
|
modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput),
|
|
4860
|
+
// Not covered by `root`: adding a decimal to a model in a *different* .ck file changes
|
|
4861
|
+
// this file's revivers with no change to `root` or the config.
|
|
4862
|
+
modelsWithDecimal: sliceModelSet(refs, ownNames, modelsWithDecimal),
|
|
4388
4863
|
sdkOptionsPath,
|
|
4389
4864
|
sub: subConfigKey
|
|
4390
4865
|
});
|
|
@@ -4398,7 +4873,9 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4398
4873
|
modelOutPaths: sdkModelOutPaths,
|
|
4399
4874
|
currentOutPath: typeOutPath,
|
|
4400
4875
|
modelsWithInput,
|
|
4401
|
-
modelsWithOutput
|
|
4876
|
+
modelsWithOutput,
|
|
4877
|
+
modelsWithDecimal,
|
|
4878
|
+
emitRevivers: true
|
|
4402
4879
|
});
|
|
4403
4880
|
} else {
|
|
4404
4881
|
let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
|
|
@@ -4408,6 +4885,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4408
4885
|
currentOutPath: typeOutPath,
|
|
4409
4886
|
modelsWithInput,
|
|
4410
4887
|
modelsWithOutput,
|
|
4888
|
+
modelsWithDecimal,
|
|
4889
|
+
emitRevivers: true,
|
|
4411
4890
|
jsonValueImportPath: rel
|
|
4412
4891
|
});
|
|
4413
4892
|
}
|
|
@@ -4469,6 +4948,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4469
4948
|
outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
4470
4949
|
modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
|
|
4471
4950
|
modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
|
|
4951
|
+
modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
|
|
4472
4952
|
sdkOptionsPath,
|
|
4473
4953
|
className,
|
|
4474
4954
|
includeInternal: config.includeInternal ?? false,
|
|
@@ -4487,6 +4967,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4487
4967
|
sdkOptionsPath,
|
|
4488
4968
|
modelsWithInput,
|
|
4489
4969
|
modelsWithOutput,
|
|
4970
|
+
modelsWithDecimal,
|
|
4971
|
+
modelMap,
|
|
4490
4972
|
includeInternal: config.includeInternal,
|
|
4491
4973
|
clientClassName: className
|
|
4492
4974
|
})
|
|
@@ -4511,6 +4993,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4511
4993
|
outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
4512
4994
|
modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
|
|
4513
4995
|
modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
|
|
4996
|
+
modelsWithDecimal: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithDecimal),
|
|
4514
4997
|
sdkOptionsPath,
|
|
4515
4998
|
includeInternal: config.includeInternal ?? false,
|
|
4516
4999
|
sub: subConfigKey
|
|
@@ -4528,6 +5011,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4528
5011
|
sdkOptionsPath,
|
|
4529
5012
|
modelsWithInput,
|
|
4530
5013
|
modelsWithOutput,
|
|
5014
|
+
modelsWithDecimal,
|
|
5015
|
+
modelMap,
|
|
4531
5016
|
includeInternal: config.includeInternal
|
|
4532
5017
|
})
|
|
4533
5018
|
}
|
|
@@ -4596,6 +5081,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4596
5081
|
outPathSlice: sliceOutPathMap(allInlineRefs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
4597
5082
|
modelsWithInput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithInput),
|
|
4598
5083
|
modelsWithOutput: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithOutput),
|
|
5084
|
+
modelsWithDecimal: sliceModelSet(allInlineRefs, /* @__PURE__ */ new Set(), modelsWithDecimal),
|
|
4599
5085
|
sdkOptionsPath,
|
|
4600
5086
|
includeInternal: config.includeInternal ?? false,
|
|
4601
5087
|
sub: subConfigKey
|
|
@@ -4609,6 +5095,8 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
|
|
|
4609
5095
|
sdkOptionsPath,
|
|
4610
5096
|
modelsWithInput,
|
|
4611
5097
|
modelsWithOutput,
|
|
5098
|
+
modelsWithDecimal,
|
|
5099
|
+
modelMap,
|
|
4612
5100
|
includeInternal: config.includeInternal
|
|
4613
5101
|
}
|
|
4614
5102
|
}));
|
|
@@ -4677,7 +5165,17 @@ ${rootExports.sort().join("\n")}
|
|
|
4677
5165
|
const coveredRoots = sdkContractEntries.map((e) => e.ast);
|
|
4678
5166
|
const deps = {
|
|
4679
5167
|
zod: !!config.zod,
|
|
4680
|
-
|
|
5168
|
+
// `duration` belongs here too: `generateContract` imports `Duration` from luxon for it,
|
|
5169
|
+
// so a contract whose only temporal scalar is a duration used to scaffold a package.json
|
|
5170
|
+
// with no luxon dependency and fail to compile.
|
|
5171
|
+
luxon: coveredRoots.some((r) => [
|
|
5172
|
+
"datetime",
|
|
5173
|
+
"date",
|
|
5174
|
+
"time",
|
|
5175
|
+
"duration",
|
|
5176
|
+
"interval"
|
|
5177
|
+
].some((name) => rootNeedsScalar(r, name))),
|
|
5178
|
+
decimal: coveredRoots.some((r) => rootNeedsScalar(r, "decimal"))
|
|
4681
5179
|
};
|
|
4682
5180
|
globalFiles.push({
|
|
4683
5181
|
relativePath: join2(sdkBase, "package.json"),
|