@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/ts-render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ts-render.d.ts","sourceRoot":"","sources":["../src/ts-render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAA6B,MAAM,mBAAmB,CAAC;AAErF,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,2GAA2G,CAAC;AAE7I,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;sFAEsF;AACtF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,6HAA6H;AAC7H,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD;AAID;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,GAAE,cAAyB,GAAG,MAAM,CAoC9F;
|
|
1
|
+
{"version":3,"file":"ts-render.d.ts","sourceRoot":"","sources":["../src/ts-render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAA6B,MAAM,mBAAmB,CAAC;AAErF,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,2GAA2G,CAAC;AAE7I,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;sFAEsF;AACtF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,6HAA6H;AAC7H,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD;AAID;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,GAAE,cAAyB,GAAG,MAAM,CAoC9F;AAsDD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,EAAE,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,GAAE,cAAyB,GAAG,MAAM,CA2BlI;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,GAAE,cAAyB,GAAG,MAAM,CA2BpI"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @contractkit/plugin-typescript
|
|
2
|
+
|
|
3
|
+
> ContractKit's TypeScript code generator. From the same `.ck` files it emits Koa routers, a
|
|
4
|
+
> TypeScript SDK client, Zod schemas, plain TypeScript types, and MCP tool handlers. Five
|
|
5
|
+
> independent sub-generators, each enabled by the presence of its config key.
|
|
6
|
+
|
|
7
|
+
Loaded by `@contractkit/cli` through `contractkit.config.json`. It consumes the AST produced by
|
|
8
|
+
`@contractkit/core`; you do not call it directly in a normal build.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add -D @contractkit/cli @contractkit/plugin-typescript
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configure
|
|
17
|
+
|
|
18
|
+
`plugins` is an **object** keyed by package name, not an array. Each sub-config is independent —
|
|
19
|
+
include only the ones you want; omitting a key disables that sub-generator entirely.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"rootDir": ".",
|
|
24
|
+
"patterns": ["contracts/**/*.ck"],
|
|
25
|
+
"plugins": {
|
|
26
|
+
"@contractkit/plugin-typescript": {
|
|
27
|
+
"server": {
|
|
28
|
+
"baseDir": "apps/api/",
|
|
29
|
+
"zod": true,
|
|
30
|
+
"output": {
|
|
31
|
+
"routes": "src/routes/{filename}.router.ts",
|
|
32
|
+
"types": "src/modules/{area}/types/{filename}.ts"
|
|
33
|
+
},
|
|
34
|
+
"servicePathTemplate": "#modules/{module}/{module}.service.js"
|
|
35
|
+
},
|
|
36
|
+
"sdk": {
|
|
37
|
+
"baseDir": "packages/sdk/",
|
|
38
|
+
"name": "acme",
|
|
39
|
+
"zod": true,
|
|
40
|
+
"output": {
|
|
41
|
+
"sdk": "src/{name}.sdk.ts",
|
|
42
|
+
"types": "src/{area}/types/{filename}.ts",
|
|
43
|
+
"clients": "src/{area}/{filename}.client.ts"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"zod": { "baseDir": "packages/schemas/", "output": "{filename}.schema.ts" },
|
|
47
|
+
"types": { "baseDir": "packages/types/", "output": "{filename}.types.ts" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Path templates accept `{filename}`, `{dir}`, `{area}`, and — in `sdk.output.sdk` — `{name}`.
|
|
54
|
+
|
|
55
|
+
## Sub-configs
|
|
56
|
+
|
|
57
|
+
| Key | Emits |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `server` | Koa routers from `operation` declarations, plus the type or Zod files they import |
|
|
60
|
+
| `sdk` | The SDK class, per-area operation clients, and their types |
|
|
61
|
+
| `zod` | Standalone Zod schemas, independent of `server` and `sdk` |
|
|
62
|
+
| `types` | Standalone plain TypeScript types, independent of `server` and `sdk` |
|
|
63
|
+
| `mcp` | Per-file MCP tool handlers, an aggregator, and an optional `POST /mcp` route |
|
|
64
|
+
|
|
65
|
+
Options worth knowing:
|
|
66
|
+
|
|
67
|
+
- `server.zod` / `sdk.zod` — when true, `output.types` emits Zod schema files instead of plain
|
|
68
|
+
interfaces. Off by default.
|
|
69
|
+
- `server.validateResponses` — each handler re-parses the service result through its declared
|
|
70
|
+
response schema before writing `ctx.body`. **Requires `zod: true`**; without it `output.types`
|
|
71
|
+
emits plain interfaces, which are types with no runtime value to parse through. A body that
|
|
72
|
+
transitively references a model with `format(input=…)`/`format(output=…)`, and a status whose
|
|
73
|
+
several mimes carry different body types, are left unvalidated.
|
|
74
|
+
- `sdk.scaffold` — write-once starter `package.json` and `tsconfig.json` at the SDK `baseDir`, so
|
|
75
|
+
the generated output is a publishable package on its own. Never overwritten once created, so your
|
|
76
|
+
edits survive. Dependency ranges are derived from the contracts.
|
|
77
|
+
- `includeInternal` on `server`/`sdk` — whether operations marked `internal` get handlers or client
|
|
78
|
+
methods. Defaults to `true` for `server`, `false` for `sdk`.
|
|
79
|
+
- `types.target` — `'client'` (default) or `'server'`; decides whether `binary` renders as `Blob`
|
|
80
|
+
or `Buffer`. The `server` and `sdk` sub-generators set it themselves.
|
|
81
|
+
- `mcp.output.types` must point at **Zod schema** files, not plain types — tools need a runtime
|
|
82
|
+
schema for argument validation and `z.toJSONSchema`. It falls back to `server.output.types` (when
|
|
83
|
+
`server.zod`) or the `zod` sub-config's output.
|
|
84
|
+
|
|
85
|
+
## Programmatic use
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createTypescriptPlugin } from '@contractkit/plugin-typescript';
|
|
89
|
+
|
|
90
|
+
const plugin = createTypescriptPlugin({ types: { output: '{filename}.types.ts' } }, process.cwd());
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Prefer the default export when loading through `contractkit.config.json`; the factory is for
|
|
94
|
+
building the plugin in code (tests, custom build scripts).
|
|
95
|
+
|
|
96
|
+
## Gotchas
|
|
97
|
+
|
|
98
|
+
- The four output sub-configs are independent. `zod: true` **inside** `server` is not the same thing
|
|
99
|
+
as the top-level `zod` sub-config: the first changes what `server.output.types` emits, the second
|
|
100
|
+
adds a separate standalone schema output.
|
|
101
|
+
- `validateResponses` silently does nothing useful without `zod: true`.
|
|
102
|
+
- Which statuses a router writes and which the SDK throws is decided by the contract, not by this
|
|
103
|
+
plugin — a `404(documented)` is the client's error contract, a bare `404:` is a status the service
|
|
104
|
+
produces. See the response-set helpers in `@contractkit/core`.
|
|
105
|
+
|
|
106
|
+
## More
|
|
107
|
+
|
|
108
|
+
- README: https://github.com/MaroonedSoftware/contractkit/blob/main/packages/plugin-typescript/README.md
|
|
109
|
+
- Configuration: https://github.com/MaroonedSoftware/contractkit/blob/main/docs/config.md
|
|
110
|
+
- SDK generation and subclient grouping: https://github.com/MaroonedSoftware/contractkit/blob/main/docs/tooling.md
|
|
111
|
+
- Full usage guide: https://github.com/MaroonedSoftware/contractkit/blob/main/llms-full.txt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractkit/plugin-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
".": "./dist/index.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@contractkit/core": "0.
|
|
30
|
+
"@contractkit/core": "0.28.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@repo/config-eslint": "0.3.1",
|
package/src/codegen-contract.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
} from '@contractkit/core';
|
|
24
24
|
import { escapeJsDocLines } from './ts-render.js';
|
|
25
25
|
import type { TsRenderTarget } from './ts-render.js';
|
|
26
|
+
import { DECIMAL_IMPORT, DECIMAL_PRELUDE_LINES } from './decimal-runtime.js';
|
|
27
|
+
import { renderReviveFunctions, reviveFnName, DECIMAL_COERCE_DECL } from './codegen-revive.js';
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Maps a ContractKit object mode to its Zod constructor name.
|
|
@@ -60,6 +62,13 @@ export interface ContractCodegenContext {
|
|
|
60
62
|
* `generateContract`, whose Zod schemas are server-shaped by construction. Default `'client'`.
|
|
61
63
|
*/
|
|
62
64
|
target?: TsRenderTarget;
|
|
65
|
+
/** Model names that carry a `decimal`, directly or transitively. */
|
|
66
|
+
modelsWithDecimal?: Set<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Emit `reviveX()` hydration functions alongside the schemas. Set only for SDK type files: a
|
|
69
|
+
* server handler receives decimals already parsed by `_ZodDecimal`, so it has nothing to revive.
|
|
70
|
+
*/
|
|
71
|
+
emitRevivers?: boolean;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
// ─── Public entry point ────────────────────────────────────────────────────
|
|
@@ -142,6 +151,7 @@ export function generateContract(root: ContractRootNode, context?: ContractCodeg
|
|
|
142
151
|
const needsBinary = rootNeedsScalar(root, 'binary');
|
|
143
152
|
const needsDatetime = rootNeedsScalar(root, 'datetime');
|
|
144
153
|
const needsJson = rootNeedsScalar(root, 'json');
|
|
154
|
+
const needsDecimal = rootNeedsScalar(root, 'decimal');
|
|
145
155
|
const externalRefs = collectExternalRefs(root);
|
|
146
156
|
const lines: string[] = [];
|
|
147
157
|
|
|
@@ -166,9 +176,14 @@ export function generateContract(root: ContractRootNode, context?: ContractCodeg
|
|
|
166
176
|
if (needsDuration) luxonImports.push('Duration');
|
|
167
177
|
if (needsInterval) luxonImports.push('Interval');
|
|
168
178
|
if (luxonImports.length > 0) lines.push(`import { ${luxonImports.join(', ')} } from 'luxon';`);
|
|
179
|
+
if (needsDecimal) lines.push(DECIMAL_IMPORT);
|
|
169
180
|
for (const ref of allExternalRefs) {
|
|
170
181
|
const importPath = resolveImportPath(ref, context);
|
|
171
|
-
|
|
182
|
+
// A cross-file model that carries a decimal contributes its reviver too — the local
|
|
183
|
+
// reviver calls it rather than re-deriving the other file's shape.
|
|
184
|
+
const names =
|
|
185
|
+
context?.emitRevivers && context.modelsWithDecimal?.has(ref) ? `${ref}, ${reviveFnName(ref)}` : ref;
|
|
186
|
+
lines.push(`import { ${names} } from '${importPath}';`);
|
|
172
187
|
}
|
|
173
188
|
lines.push('');
|
|
174
189
|
if (needsBinary) {
|
|
@@ -184,21 +199,45 @@ export function generateContract(root: ContractRootNode, context?: ContractCodeg
|
|
|
184
199
|
`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()!);`,
|
|
185
200
|
);
|
|
186
201
|
}
|
|
202
|
+
if (needsDecimal) {
|
|
203
|
+
lines.push(...DECIMAL_PRELUDE_LINES);
|
|
204
|
+
}
|
|
187
205
|
if (needsJson) {
|
|
188
206
|
lines.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
|
|
189
207
|
lines.push(
|
|
190
208
|
`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)]));`,
|
|
191
209
|
);
|
|
192
210
|
}
|
|
193
|
-
if (needsBinary || needsDatetime || needsInterval || needsJson) lines.push('');
|
|
211
|
+
if (needsBinary || needsDatetime || needsInterval || needsDecimal || needsJson) lines.push('');
|
|
194
212
|
|
|
195
213
|
const modelsWithWriteonly = new Set(root.models.filter(m => m.fields.some(f => f.visibility === 'writeonly')).map(m => m.name));
|
|
196
214
|
const modelMap = new Map(root.models.map(m => [m.name, m]));
|
|
197
215
|
|
|
216
|
+
const reviveOpts =
|
|
217
|
+
context?.emitRevivers && context.modelsWithDecimal
|
|
218
|
+
? { modelsWithDecimal: context.modelsWithDecimal, modelsWithOutput: allModelsWithOutput, modelMap }
|
|
219
|
+
: undefined;
|
|
220
|
+
|
|
221
|
+
const bodyLines: string[] = [];
|
|
198
222
|
for (const model of topoSortModels(root.models)) {
|
|
199
|
-
|
|
223
|
+
bodyLines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, modelsWithWriteonly, modelMap, allModelsWithOutput));
|
|
224
|
+
if (reviveOpts) {
|
|
225
|
+
const revivers = renderReviveFunctions(model, reviveOpts);
|
|
226
|
+
if (revivers.length > 0) {
|
|
227
|
+
bodyLines.push('');
|
|
228
|
+
bodyLines.push(...revivers);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
bodyLines.push('');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Decided from the emitted revivers rather than from a predicate over the AST, so the
|
|
235
|
+
// declaration and its uses cannot drift apart and leave an unused local behind.
|
|
236
|
+
if (bodyLines.some(l => l.includes('__dec('))) {
|
|
237
|
+
lines.push(...DECIMAL_COERCE_DECL);
|
|
200
238
|
lines.push('');
|
|
201
239
|
}
|
|
240
|
+
lines.push(...bodyLines);
|
|
202
241
|
|
|
203
242
|
return lines.join('\n');
|
|
204
243
|
}
|
|
@@ -631,6 +670,22 @@ function renderScalar(s: ScalarTypeNode): string {
|
|
|
631
670
|
if (s.max !== undefined) inner += `.max(${s.max}n)`;
|
|
632
671
|
return `z.preprocess((val) => typeof val === 'string' ? BigInt(val.replace(/n$/, '')) : val, ${inner})`;
|
|
633
672
|
}
|
|
673
|
+
case 'decimal': {
|
|
674
|
+
// Deliberately no output `.transform()`: `isRevalidatable` in codegen-operation treats
|
|
675
|
+
// every scalar as idempotent under re-parse, which `server.validateResponses` relies on.
|
|
676
|
+
// Preprocess passes an already-`Decimal` value straight through, so parse(parse(x)) is
|
|
677
|
+
// stable. Modelling this on `_ZodInterval` — which does transform — would break that.
|
|
678
|
+
const checks: string[] = [];
|
|
679
|
+
if (s.scale !== undefined) checks.push(`v.decimalPlaces() <= ${s.scale}`);
|
|
680
|
+
if (s.min !== undefined) checks.push(`v.gte('${escapeString(String(s.min))}')`);
|
|
681
|
+
if (s.max !== undefined) checks.push(`v.lte('${escapeString(String(s.max))}')`);
|
|
682
|
+
if (checks.length === 0) return '_ZodDecimal';
|
|
683
|
+
const messageParts: string[] = [];
|
|
684
|
+
if (s.scale !== undefined) messageParts.push(`at most ${s.scale} decimal place${s.scale === 1 ? '' : 's'}`);
|
|
685
|
+
if (s.min !== undefined) messageParts.push(`at least ${s.min}`);
|
|
686
|
+
if (s.max !== undefined) messageParts.push(`at most ${s.max}`);
|
|
687
|
+
return `_ZodDecimal.refine((v) => ${checks.join(' && ')}, { message: 'Must be ${escapeString(messageParts.join(', '))}' })`;
|
|
688
|
+
}
|
|
634
689
|
case 'boolean':
|
|
635
690
|
return `z.preprocess((v) => v === 'true' ? true : v === 'false' ? false : v, z.boolean())`;
|
|
636
691
|
case 'date': {
|
package/src/codegen-mcp.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveModifiers, emittedResponses } from '@contractkit/core';
|
|
|
3
3
|
import { renderType, renderInputType, pascalToDotCase } from './codegen-contract.js';
|
|
4
4
|
import { inferService, deriveModulePath, buildArgs, deriveBaseName } from './codegen-operation.js';
|
|
5
5
|
import { quoteKey, escapeSingleQuoted } from './ts-render.js';
|
|
6
|
+
import { DECIMAL_IMPORT, DECIMAL_PRELUDE_LINES } from './decimal-runtime.js';
|
|
6
7
|
import { basename, dirname, relative } from 'node:path';
|
|
7
8
|
|
|
8
9
|
// ─── Options ────────────────────────────────────────────────────────────────
|
|
@@ -289,6 +290,9 @@ function scalarHelperLines(body: string): string[] {
|
|
|
289
290
|
`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' }));`,
|
|
290
291
|
);
|
|
291
292
|
}
|
|
293
|
+
if (body.includes('_ZodDecimal')) {
|
|
294
|
+
lines.push(...DECIMAL_PRELUDE_LINES);
|
|
295
|
+
}
|
|
292
296
|
if (body.includes('_ZodInterval')) {
|
|
293
297
|
lines.push(
|
|
294
298
|
`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()!);`,
|
|
@@ -430,6 +434,7 @@ export function generateMcpFile(root: OpRootNode, options: McpCodegenOptions = {
|
|
|
430
434
|
if (/\bInterval\b/.test(bodyWithHelpers)) luxon.push('Interval');
|
|
431
435
|
if (/\bDuration\b/.test(bodyWithHelpers)) luxon.push('Duration');
|
|
432
436
|
if (luxon.length > 0) imports.push(`import { ${luxon.join(', ')} } from 'luxon';`);
|
|
437
|
+
if (/\bDecimal\b/.test(bodyWithHelpers)) imports.push(DECIMAL_IMPORT);
|
|
433
438
|
|
|
434
439
|
imports.push(`import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';`);
|
|
435
440
|
imports.push(`import type { McpToolHandler, McpToolHandlerMap, McpToolContext } from '@maroonedsoftware/mcp';`);
|
package/src/codegen-operation.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
modeToWrapper,
|
|
19
19
|
} from './codegen-contract.js';
|
|
20
20
|
import { renderOutputTsType, quoteKey, headerNameToProperty, escapeJsDocLines, escapeSingleQuoted } from './ts-render.js';
|
|
21
|
+
import { DECIMAL_IMPORT, DECIMAL_PRELUDE_LINES } from './decimal-runtime.js';
|
|
21
22
|
import { basename, dirname, relative } from 'path';
|
|
22
23
|
|
|
23
24
|
// ─── Content-type helpers ──────────────────────────────────────────────────
|
|
@@ -50,7 +51,15 @@ export function bodyTypesStructurallyEqual(a: ContractTypeNode, b: ContractTypeN
|
|
|
50
51
|
switch (a.kind) {
|
|
51
52
|
case 'scalar': {
|
|
52
53
|
const bb = b as typeof a;
|
|
53
|
-
return
|
|
54
|
+
return (
|
|
55
|
+
a.name === bb.name &&
|
|
56
|
+
a.min === bb.min &&
|
|
57
|
+
a.max === bb.max &&
|
|
58
|
+
a.len === bb.len &&
|
|
59
|
+
a.scale === bb.scale &&
|
|
60
|
+
a.regex === bb.regex &&
|
|
61
|
+
a.format === bb.format
|
|
62
|
+
);
|
|
54
63
|
}
|
|
55
64
|
case 'array': {
|
|
56
65
|
const bb = b as typeof a;
|
|
@@ -197,6 +206,9 @@ export function generateOp(root: OpRootNode, options: OpCodegenOptions = {}): st
|
|
|
197
206
|
`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' }));`,
|
|
198
207
|
);
|
|
199
208
|
}
|
|
209
|
+
if (references('_ZodDecimal')) {
|
|
210
|
+
helpers.push(...DECIMAL_PRELUDE_LINES);
|
|
211
|
+
}
|
|
200
212
|
if (references('_ZodInterval')) {
|
|
201
213
|
helpers.push(
|
|
202
214
|
`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()!);`,
|
|
@@ -247,6 +259,12 @@ export function generateOp(root: OpRootNode, options: OpCodegenOptions = {}): st
|
|
|
247
259
|
body.push(`import { ${luxonImports.join(', ')} } from 'luxon';`);
|
|
248
260
|
}
|
|
249
261
|
|
|
262
|
+
// Same `uses` gate: `Decimal` appears in the `_ZodDecimal` helper and in service-result
|
|
263
|
+
// annotations via `serverTsScalar`, and the helper text is folded into `generated` above.
|
|
264
|
+
if (uses('Decimal')) {
|
|
265
|
+
body.push(DECIMAL_IMPORT);
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
if (uses('parseAndValidate')) {
|
|
251
269
|
body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
|
|
252
270
|
}
|
|
@@ -645,6 +663,10 @@ function serverTsScalar(name: ScalarTypeNode['name']): string {
|
|
|
645
663
|
return 'number';
|
|
646
664
|
case 'bigint':
|
|
647
665
|
return 'bigint';
|
|
666
|
+
case 'decimal':
|
|
667
|
+
// Unlike the date scalars, this matches `renderTsScalar`'s wire view — `_ZodDecimal`
|
|
668
|
+
// has no output transform, so `z.infer` is a `Decimal` on both sides.
|
|
669
|
+
return 'Decimal';
|
|
648
670
|
case 'boolean':
|
|
649
671
|
return 'boolean';
|
|
650
672
|
case 'date':
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from './codegen-contract.js';
|
|
13
13
|
import { renderTsType, renderInputTsType, renderOutputTsType, quoteKey, escapeJsDocLines, JSON_VALUE_TYPE_DECL } from './ts-render.js';
|
|
14
14
|
import type { TsRenderTarget } from './ts-render.js';
|
|
15
|
+
import { DECIMAL_IMPORT, DECIMAL_CONFIG_LINE } from './decimal-runtime.js';
|
|
16
|
+
import { renderReviveFunctions, reviveFnName, DECIMAL_COERCE_DECL } from './codegen-revive.js';
|
|
15
17
|
|
|
16
18
|
// ─── Public entry point ────────────────────────────────────────────────────
|
|
17
19
|
|
|
@@ -44,10 +46,20 @@ export function generatePlainTypes(root: ContractRootNode, context?: ContractCod
|
|
|
44
46
|
const externalOutputRefs = allModelsWithOutput.size > 0 ? collectExternalOutputRefs(root, allModelsWithOutput) : [];
|
|
45
47
|
const allExternalRefs = [...new Set([...externalRefs, ...externalInputRefs, ...externalOutputRefs])].sort();
|
|
46
48
|
|
|
47
|
-
//
|
|
49
|
+
// Not `import type`: `renderTsScalar` maps `decimal` to `Decimal` in this mode too, so the class
|
|
50
|
+
// is a real runtime dependency of any consumer holding one — same position as in
|
|
51
|
+
// `generateContract`, which emits it ahead of the external model refs.
|
|
52
|
+
const needsDecimal = rootNeedsScalar(root, 'decimal') || (context?.emitRevivers && context.modelsWithDecimal ? root.models.some(m => context.modelsWithDecimal!.has(m.name)) : false);
|
|
53
|
+
if (needsDecimal) lines.push(DECIMAL_IMPORT);
|
|
54
|
+
|
|
55
|
+
// Type-only imports for external references. A cross-file model carrying a decimal also
|
|
56
|
+
// contributes its reviver, which is a value and so needs a second, non-type import.
|
|
48
57
|
for (const ref of allExternalRefs) {
|
|
49
58
|
const importPath = resolveImportPath(ref, context);
|
|
50
59
|
lines.push(`import type { ${ref} } from '${importPath}';`);
|
|
60
|
+
if (context?.emitRevivers && context.modelsWithDecimal?.has(ref)) {
|
|
61
|
+
lines.push(`import { ${reviveFnName(ref)} } from '${importPath}';`);
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
if (allExternalRefs.length > 0) lines.push('');
|
|
53
65
|
|
|
@@ -62,10 +74,38 @@ export function generatePlainTypes(root: ContractRootNode, context?: ContractCod
|
|
|
62
74
|
|
|
63
75
|
const modelMap = new Map(root.models.map(m => [m.name, m]));
|
|
64
76
|
|
|
77
|
+
const reviveOpts =
|
|
78
|
+
context?.emitRevivers && context.modelsWithDecimal
|
|
79
|
+
? { modelsWithDecimal: context.modelsWithDecimal, modelsWithOutput: allModelsWithOutput, modelMap }
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
const bodyLines: string[] = [];
|
|
65
83
|
for (const model of topoSortModels(root.models)) {
|
|
66
|
-
|
|
84
|
+
bodyLines.push(...generateModel(model, target, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
|
|
85
|
+
if (reviveOpts) {
|
|
86
|
+
const revivers = renderReviveFunctions(model, reviveOpts);
|
|
87
|
+
if (revivers.length > 0) {
|
|
88
|
+
bodyLines.push('');
|
|
89
|
+
bodyLines.push(...revivers);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
bodyLines.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Global decimal.js config belongs in any file holding a `Decimal`: there is no Zod schema in
|
|
96
|
+
// this mode, but `String(value)` and `JSON.stringify` still have to stay out of exponential form.
|
|
97
|
+
if (needsDecimal) {
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(DECIMAL_CONFIG_LINE);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Same rule as in `generateContract`: the helper is emitted only if the revivers actually
|
|
103
|
+
// reference it, so the two cannot drift and trip `noUnusedLocals`.
|
|
104
|
+
if (bodyLines.some(l => l.includes('__dec('))) {
|
|
105
|
+
lines.push(...DECIMAL_COERCE_DECL);
|
|
67
106
|
lines.push('');
|
|
68
107
|
}
|
|
108
|
+
lines.push(...bodyLines);
|
|
69
109
|
|
|
70
110
|
return lines.join('\n');
|
|
71
111
|
}
|