@contractkit/plugin-typescript 0.31.1 → 0.32.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.
@@ -5,9 +5,9 @@ $ tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly
5
5
  CLI tsup v8.5.1
6
6
  CLI Target: esnext
7
7
  ESM Build start
8
- ESM dist/index.js 188.58 KB
9
- ESM dist/index.js.map 418.71 KB
10
- ESM ⚡️ Build success in 589ms
8
+ ESM dist/index.js 191.67 KB
9
+ ESM dist/index.js.map 428.46 KB
10
+ ESM ⚡️ Build success in 328ms
11
11
  DTS Build start
12
- DTS ⚡️ Build success in 7825ms
13
- DTS dist/index.d.ts 4.55 KB
12
+ DTS ⚡️ Build success in 3774ms
13
+ DTS dist/index.d.ts 5.08 KB
@@ -3,34 +3,34 @@ $ vitest run --coverage
3
3
   RUN  v4.1.5 /home/runner/work/ContractKit/ContractKit/packages/plugin-typescript
4
4
  Coverage enabled with v8
5
5
 
6
- ✓ tests/codegen-contract.test.ts (128 tests) 249ms
7
- ✓ tests/codegen-sdk.test.ts (152 tests) 296ms
8
- ✓ tests/codegen-operation.test.ts (129 tests) 235ms
9
- ✓ tests/codegen-server.test.ts (25 tests) 107ms
10
- ✓ tests/codegen-plain-types.test.ts (66 tests) 46ms
11
- ✓ tests/pipeline.test.ts (28 tests) 320ms
12
- ✓ tests/codegen-mcp.test.ts (24 tests) 42ms
13
- ✓ tests/escaping-security.test.ts (18 tests) 65ms
6
+ ✓ tests/codegen-contract.test.ts (128 tests) 165ms
7
+ ✓ tests/codegen-sdk.test.ts (152 tests) 307ms
8
+ ✓ tests/codegen-operation.test.ts (154 tests) 317ms
9
+ ✓ tests/codegen-plain-types.test.ts (66 tests) 70ms
10
+ ✓ tests/codegen-server.test.ts (30 tests) 123ms
11
+ ✓ tests/pipeline.test.ts (29 tests) 279ms
12
+ ✓ tests/codegen-mcp.test.ts (24 tests) 55ms
13
+ ✓ tests/escaping-security.test.ts (18 tests) 38ms
14
14
 
15
15
   Test Files  8 passed (8)
16
-  Tests  570 passed (570)
17
-  Start at  17:32:01
18
-  Duration  11.21s (transform 7.13s, setup 0ms, import 22.29s, tests 1.36s, environment 2ms)
16
+  Tests  601 passed (601)
17
+  Start at  16:35:30
18
+  Duration  8.90s (transform 6.21s, setup 0ms, import 18.05s, tests 1.36s, environment 7ms)
19
19
 
20
20
   % Coverage report from v8
21
21
  -------------------|---------|----------|---------|---------|-------------------
22
22
  File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
23
23
  -------------------|---------|----------|---------|---------|-------------------
24
- All files | 83.71 | 78.1 | 86.07 | 86.33 |
25
- src | 83.57 | 77.84 | 85.81 | 86.18 |
26
- ...n-contract.ts | 88.1 | 82.91 | 90.08 | 89.26 | ...1095,1100-1101
24
+ All files | 84.1 | 78.42 | 86.54 | 86.6 |
25
+ src | 83.97 | 78.12 | 86.28 | 86.46 |
26
+ ...n-contract.ts | 88.69 | 83.12 | 90.08 | 89.98 | ...1095,1100-1101
27
27
  codegen-mcp.ts | 82.79 | 75.47 | 84.21 | 86.02 | ...93,298-299,382
28
- ...-operation.ts | 82.35 | 80.68 | 76.19 | 84.83 | ...99-910,915-916
28
+ ...-operation.ts | 83.05 | 80.67 | 79.71 | 85.23 | ...1010,1015-1016
29
29
  ...lain-types.ts | 91.57 | 81.98 | 96.87 | 94.44 | ...31,157,269,275
30
30
  codegen-sdk.ts | 89.92 | 83.94 | 90.62 | 92.67 | ...1411,1425-1426
31
- index.ts | 66.25 | 53.15 | 72.72 | 70.29 | ...1022,1038-1057
31
+ index.ts | 67.64 | 55.61 | 73.68 | 70.93 | ...1052,1068-1087
32
32
  path-utils.ts | 70.28 | 51.13 | 100 | 73.5 | ...65-168,216-231
33
33
  ts-render.ts | 85 | 87.36 | 79.16 | 89.23 | 83,159,192-200
34
- tests | 91.07 | 90.24 | 90 | 93.87 |
35
- helpers.ts | 91.07 | 90.24 | 90 | 93.87 | 110-118
34
+ tests | 91.22 | 92.68 | 90.32 | 94 |
35
+ helpers.ts | 91.22 | 92.68 | 90.32 | 94 | 115-123
36
36
  -------------------|---------|----------|---------|---------|-------------------
package/CHANGELOG.md CHANGED
@@ -1,5 +1,96 @@
1
1
  # @contractkit/contractkit-plugin-typescript
2
2
 
3
+ ## 0.32.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5dc2693: Add `server.validateResponses` — the generated Koa router can now validate what it sends, not just
8
+ what it receives
9
+
10
+ Handlers have always run request params, query, headers and body through `parseAndValidate`. The
11
+ service's return value got nothing: it was type-annotated and assigned straight to `ctx.body`, so a
12
+ service returning a shape its own contract forbids shipped it to the client unchanged. With
13
+ `server.validateResponses: true` the result is re-parsed against the declared response schema and
14
+ the _parsed_ value is written:
15
+
16
+ ```ts
17
+ const result: User = await service.getById(id);
18
+
19
+ ctx.status = 200;
20
+ ctx.type = 'application/json';
21
+ ctx.body = await parseAndValidate(result, User, 500);
22
+ ```
23
+
24
+ Because the parsed value is what reaches the wire, `mode(strip)` now actually strips extra keys off
25
+ responses.
26
+ - **Opt-in, and off by default**, because turning it on surfaces real drift. TypeScript only
27
+ excess-property-checks object _literals_, so a service returning a database row with undeclared
28
+ columns satisfies `const result: User` today and quietly ships them; under the default `strict`
29
+ mode that becomes a 500. That is the flag working, but it is not a change to make on a Friday.
30
+ - **`zod: true` is a hard prerequisite.** Without it `output.types` emits plain interfaces — types
31
+ with no runtime schema value to validate against. Setting `validateResponses` alone now fails the
32
+ build with an explicit message instead of emitting code that cannot compile. This is the plugin's
33
+ first config assertion; it runs for both the default export and `createTypescriptPlugin`.
34
+ - **Requires `@maroonedsoftware/zod` 0.6.1 or later** for the `statusCode` argument. Failures are
35
+ raised as `500`, not the `400` a request-side failure gets — a service breaking its own contract
36
+ is a server fault. At 5xx that package puts the field-level map on `internalDetails` rather than
37
+ `details`, so `errorMiddleware` keeps it out of the response body and on the log path.
38
+
39
+ Two kinds of response body are deliberately left unvalidated, and generate exactly as before:
40
+ - **Anything transitively referencing a model with `format(input=…)` or `format(output=…)`.** Those
41
+ schemas transform keys between wire and developer-facing casing, and the service already returns
42
+ the post-transform shape, so re-parsing it through the same schema would fail on every key. Note
43
+ this needs a wider set than `modelsWithOutput`, which seeds only from `outputCase` because only
44
+ that case needs an `Output` type alias — a `format(input=snake)`-only model is just as
45
+ untouchable. `@contractkit/core` gains `computeModelsWithCaseTransform` for it.
46
+ - **A status whose several content types carry different body shapes**, where `contentType` and
47
+ `body` are correlated across union members. Matching shapes (`image/png` and `image/jpeg` both
48
+ `binary`) share one schema and validate normally.
49
+
50
+ One wart worth knowing: `ctx.status` and any `ctx.set(…)` response headers are written before the
51
+ body, so a validation failure raises its 500 with the success path's headers already set.
52
+
53
+ Projects that do not set the flag generate byte-identical routers.
54
+
55
+ ### Patch Changes
56
+
57
+ - 5dc2693: Stop the generated Koa router emitting imports nothing in the file references
58
+
59
+ `collectTypes` and `collectServices` walk the AST, and the AST over-approximates what a router
60
+ actually uses in three ways:
61
+ - A model with an `Input` or `Output` variant contributed **both** its base name and the variant,
62
+ even when only the variant is ever annotated. A response typed `AuthTokenOutput` emitted
63
+ `import { AuthToken, AuthTokenOutput }`, and a request body validated against `CreateUserInput`
64
+ emitted `import { CreateUser, CreateUserInput }`.
65
+ - Both collectors walk every operation, including the `internal` ones `includeInternal: false`
66
+ drops. A router whose only operation was excluded still imported that operation's service and
67
+ response model, with no handler left to use either.
68
+
69
+ In a consuming project with `noUnusedLocals` — or the equivalent lint rule — each of those is a
70
+ compile error in generated code the user cannot edit.
71
+
72
+ Every collected service and model name is now filtered through the same `uses` gate that already
73
+ prunes `parseAndValidate`, `requirePolicy`, `MultipartBody` and the luxon imports: a name is
74
+ imported only if it appears in the generated body. This is the approach the file's own comment
75
+ already argued for — deciding imports from the emitted text rather than from predicates over the
76
+ AST that have to be kept in step with it by hand.
77
+
78
+ Names that are genuinely used are unaffected, including a base model used as the runtime schema
79
+ under `server.validateResponses`. `@contractkit/plugin-typescript`'s MCP output does not have this
80
+ gap — its schema ids and service imports are both derived from the emitted tool plans.
81
+
82
+ - Updated dependencies [aea5e21]
83
+ - Updated dependencies [5dc2693]
84
+ - @contractkit/core@0.27.0
85
+
86
+ ## 0.31.2
87
+
88
+ ### Patch Changes
89
+
90
+ - ca1c139: Declare the MIT license explicitly: every package now ships a `LICENSE` file in its published tarball and sets `"license": "MIT"` in its manifest, so license scanners and registries report the terms correctly.
91
+ - Updated dependencies [ca1c139]
92
+ - @contractkit/core@0.26.1
93
+
3
94
  ## 0.31.1
4
95
 
5
96
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marooned Software
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -61,6 +61,7 @@ Generates Koa router files from `operation` declarations and optionally type fil
61
61
  | `output.routes` | `string` | — | Path template for router files |
62
62
  | `output.types` | `string` | — | Path template for type/schema files |
63
63
  | `servicePathTemplate` | `string` | — | Import path template for service implementations |
64
+ | `validateResponses` | `boolean` | `false` | Re-parse the service result against its response schema before writing `ctx.body`. Requires `zod: true`. |
64
65
 
65
66
  Each generated router imports handler implementations from a service module. The `servicePathTemplate` controls where that import points. Template variables: `{module}`.
66
67
 
@@ -127,6 +128,8 @@ Contracts without visibility modifiers generate a single `Model` schema.
127
128
 
128
129
  Each operation file generates one Koa router. Request bodies and path/query params are validated against the Zod schemas (when `zod: true`) or plain types. Handlers are expected to be exported from the service module specified by `servicePathTemplate`.
129
130
 
131
+ Responses are only type-annotated by default. With `validateResponses: true` (which requires `zod: true`) the service's return value is re-parsed against its declared response schema and the parsed value is written to `ctx.body`, so a service returning a shape the contract does not allow fails with a 500 instead of shipping it. See [docs/config.md](../../docs/config.md#validateresponses) for the caveats — notably that models using `format(input=…)`/`format(output=…)` are skipped.
132
+
130
133
  ### SDK client shape (from `operation`)
131
134
 
132
135
  Operation files cluster on the SDK based on `keys.area` and `keys.subarea` (set in each file's `options { keys: { ... } }` block):
@@ -21,6 +21,18 @@ export interface OpCodegenOptions {
21
21
  * from the generated router entirely.
22
22
  */
23
23
  includeInternal?: boolean;
24
+ /**
25
+ * Re-parse the service result through its declared response schema before writing `ctx.body`,
26
+ * and write the parsed value. Requires the type file to hold Zod schemas (`server.zod`) —
27
+ * plain interfaces are types, with no runtime schema value to validate against. Default false.
28
+ */
29
+ validateResponses?: boolean;
30
+ /**
31
+ * Set of model names whose schema applies a `format(...)` key transform, directly or through a
32
+ * referenced model. Response bodies touching one are left unvalidated: the service returns the
33
+ * post-transform shape, which the schema itself cannot re-parse.
34
+ */
35
+ modelsWithTransform?: Set<string>;
24
36
  }
25
37
  /**
26
38
  * Generate a Koa router module for every operation in `root`, including the imports, type
@@ -1 +1 @@
1
- {"version":3,"file":"codegen-operation.d.ts","sourceRoot":"","sources":["../src/codegen-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,UAAU,EACV,WAAW,EACX,eAAe,EAGf,gBAAgB,EAInB,MAAM,mBAAmB,CAAC;AAiC3B;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAkE5F;AAID,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,qFAAqF;IACrF,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAgGnF;AAwRD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAY7H;AAoBD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,eAAe,GAAG,MAAM,CAqBzE;AAqWD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWnD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ/E"}
1
+ {"version":3,"file":"codegen-operation.d.ts","sourceRoot":"","sources":["../src/codegen-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,UAAU,EACV,WAAW,EACX,eAAe,EAGf,gBAAgB,EAInB,MAAM,mBAAmB,CAAC;AAiC3B;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAkE5F;AAID,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,qFAAqF;IACrF,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAuGnF;AAwSD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAY7H;AAoBD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,eAAe,GAAG,MAAM,CAqBzE;AAsaD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWnD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ/E"}
package/dist/index.d.ts CHANGED
@@ -15,6 +15,15 @@ export interface ServerConfig {
15
15
  servicePathTemplate?: string;
16
16
  /** Whether to emit handlers for `internal` operations. Default true. */
17
17
  includeInternal?: boolean;
18
+ /**
19
+ * When true, each handler re-parses the service result through its declared response schema
20
+ * before writing `ctx.body`, and writes the parsed value. Requires `zod: true` — without it
21
+ * `output.types` emits plain interfaces, which are types with no runtime schema value.
22
+ *
23
+ * A body that transitively references a model with `format(input=...)`/`format(output=...)`, and
24
+ * a status whose several mimes carry different body types, are left unvalidated. Default false.
25
+ */
26
+ validateResponses?: boolean;
18
27
  }
19
28
  /** TypeScript SDK client output: the client class, per-area operation clients, and their types. */
20
29
  export interface SdkConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EAQpB,MAAM,mBAAmB,CAAC;AA8C3B,gFAAgF;AAChF,MAAM,WAAW,YAAY;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6HAA6H;IAC7H,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,8EAA8E;QAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,+EAA+E;QAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,iGAAiG;AACjG,MAAM,WAAW,WAAW;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAChC;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACtB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE;QACL,oHAAoH;QACpH,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,0GAA0G;QAC1G,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,yFAAyF;QACzF,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;;;WAIG;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,0FAA0F;AAC1F,MAAM,WAAW,sBAAsB;IACnC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,GAAG,CAAC,EAAE,SAAS,CAAC;CACnB;AAID,yGAAyG;AACzG,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAO9C,QAAA,MAAM,MAAM,EAAE,iBAMb,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,2GAA2G;AAC3G,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAOzG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EAQpB,MAAM,mBAAmB,CAAC;AA+C3B,gFAAgF;AAChF,MAAM,WAAW,YAAY;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6HAA6H;IAC7H,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,8EAA8E;QAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,+EAA+E;QAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,iGAAiG;AACjG,MAAM,WAAW,WAAW;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAChC;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACtB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE;QACL,oHAAoH;QACpH,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,0GAA0G;QAC1G,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,yFAAyF;QACzF,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;;;WAIG;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,0FAA0F;AAC1F,MAAM,WAAW,sBAAsB;IACnC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,GAAG,CAAC,EAAE,SAAS,CAAC;CACnB;AAID,yGAAyG;AACzG,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAO9C,QAAA,MAAM,MAAM,EAAE,iBAMb,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,2GAA2G;AAC3G,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAOzG"}
package/dist/index.js CHANGED
@@ -1235,12 +1235,13 @@ function generateOp(root, options = {}) {
1235
1235
  if (koaImports.length > 0) {
1236
1236
  body.push(`import { ${koaImports.join(", ")} } from '@maroonedsoftware/koa';`);
1237
1237
  }
1238
- for (const svc of services) {
1238
+ for (const svc of services.filter(uses)) {
1239
1239
  const modulePath = root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate);
1240
1240
  body.push(`import { ${svc} } from '${modulePath}';`);
1241
1241
  }
1242
- if (types.length > 0) {
1243
- body.push(...generateTypeImports(types, root.file, options));
1242
+ const usedTypes = types.filter(uses);
1243
+ if (usedTypes.length > 0) {
1244
+ body.push(...generateTypeImports(usedTypes, root.file, options));
1244
1245
  }
1245
1246
  const luxonImports = [
1246
1247
  "DateTime",
@@ -1359,9 +1360,11 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1359
1360
  const respHeaders = resp?.headers ?? [];
1360
1361
  const hasRespHeaders = respHeaders.length > 0;
1361
1362
  const headersAnnotation = hasRespHeaders ? renderHeadersAnnotation(respHeaders, options.modelsWithOutput) : "";
1363
+ let bodySchema;
1362
1364
  if (bodies.length === 1) {
1363
1365
  const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput);
1364
1366
  if (prelude) lines.push(` ${prelude}`);
1367
+ bodySchema = responseBodySchema(bodies[0].bodyType, options, prelude ? "resultType" : void 0);
1365
1368
  lines.push(` const service = ctx.container.get(${className});`);
1366
1369
  if (hasRespHeaders) {
1367
1370
  lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
@@ -1369,10 +1372,12 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1369
1372
  lines.push(` const result: ${annotation} = ${call};`);
1370
1373
  }
1371
1374
  } else if (bodies.length > 1) {
1372
- const { members, preludes } = renderResponseMembers(resp, options, {
1375
+ const rendered = renderResponseMembers(resp, options, {
1373
1376
  includeStatus: false,
1374
1377
  varPrefix: "result"
1375
1378
  });
1379
+ const { members, preludes } = rendered;
1380
+ bodySchema = rendered.bodySchema;
1376
1381
  for (const prelude of preludes) lines.push(` ${prelude}`);
1377
1382
  lines.push(` const service = ctx.container.get(${className});`);
1378
1383
  lines.push(` const result: ${members.join(" | ")} = ${call};`);
@@ -1389,10 +1394,10 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1389
1394
  lines.push(...headerSetLines(respHeaders, " "));
1390
1395
  if (bodies.length === 1) {
1391
1396
  lines.push(` ctx.type = '${bodies[0].contentType}';`);
1392
- lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
1397
+ lines.push(` ctx.body = ${responseBodyExpr(hasRespHeaders ? "result.body" : "result", bodySchema)};`);
1393
1398
  } else if (bodies.length > 1) {
1394
1399
  lines.push(` ctx.type = result.contentType;`);
1395
- lines.push(` ctx.body = result.body;`);
1400
+ lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchema)};`);
1396
1401
  }
1397
1402
  return lines;
1398
1403
  }
@@ -1401,6 +1406,7 @@ function generateMultiStatusResult(emitted, className, call, options) {
1401
1406
  const lines = [];
1402
1407
  const members = [];
1403
1408
  const preludes = [];
1409
+ const bodySchemas = /* @__PURE__ */ new Map();
1404
1410
  for (const resp of emitted) {
1405
1411
  const rendered = renderResponseMembers(resp, options, {
1406
1412
  includeStatus: true,
@@ -1408,6 +1414,7 @@ function generateMultiStatusResult(emitted, className, call, options) {
1408
1414
  });
1409
1415
  members.push(...rendered.members);
1410
1416
  preludes.push(...rendered.preludes);
1417
+ bodySchemas.set(resp.statusCode, rendered.bodySchema);
1411
1418
  }
1412
1419
  for (const prelude of preludes) lines.push(` ${prelude}`);
1413
1420
  lines.push(` const service = ctx.container.get(${className});`);
@@ -1422,7 +1429,7 @@ function generateMultiStatusResult(emitted, className, call, options) {
1422
1429
  lines.push(...headerSetLines(resp.headers ?? [], " "));
1423
1430
  if (resp.bodies.length > 0) {
1424
1431
  lines.push(` ctx.type = result.contentType;`);
1425
- lines.push(` ctx.body = result.body;`);
1432
+ lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchemas.get(resp.statusCode))};`);
1426
1433
  }
1427
1434
  lines.push(` break;`);
1428
1435
  }
@@ -1455,6 +1462,7 @@ function renderResponseMembers(resp, options, opts) {
1455
1462
  if (uniform) {
1456
1463
  const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput, `${opts.varPrefix}Type`);
1457
1464
  if (prelude) preludes.push(prelude);
1465
+ const bodySchema = responseBodySchema(bodies[0].bodyType, options, prelude ? `${opts.varPrefix}Type` : void 0);
1458
1466
  const contentType = bodies.map((b) => `'${b.contentType}'`).join(" | ");
1459
1467
  return {
1460
1468
  members: [
@@ -1465,7 +1473,8 @@ function renderResponseMembers(resp, options, opts) {
1465
1473
  ...trailing
1466
1474
  ].join("; ")} }`
1467
1475
  ],
1468
- preludes
1476
+ preludes,
1477
+ bodySchema
1469
1478
  };
1470
1479
  }
1471
1480
  const members = bodies.map((b, i) => {
@@ -1613,6 +1622,46 @@ function formatTypeAnnotation(bodyType, modelsWithOutput, varName = "resultType"
1613
1622
  };
1614
1623
  }
1615
1624
  __name(formatTypeAnnotation, "formatTypeAnnotation");
1625
+ function isRevalidatable(type, modelsWithOutput, modelsWithTransform) {
1626
+ const rec = /* @__PURE__ */ __name((t) => isRevalidatable(t, modelsWithOutput, modelsWithTransform), "rec");
1627
+ switch (type.kind) {
1628
+ case "ref":
1629
+ return !modelsWithOutput?.has(type.name) && !modelsWithTransform?.has(type.name);
1630
+ case "array":
1631
+ return rec(type.item);
1632
+ case "tuple":
1633
+ return type.items.every(rec);
1634
+ case "record":
1635
+ return rec(type.key) && rec(type.value);
1636
+ case "intersection": {
1637
+ const [first, ...rest] = type.members;
1638
+ if (!first) return true;
1639
+ if (rest.length === 0) return rec(first);
1640
+ const usesExtendChain = first.kind === "ref" && rest.every((m) => m.kind === "ref" || m.kind === "inlineObject");
1641
+ return usesExtendChain && type.members.every(rec);
1642
+ }
1643
+ case "union":
1644
+ case "discriminatedUnion":
1645
+ return type.members.every(rec);
1646
+ case "inlineObject":
1647
+ return type.fields.every((f) => rec(f.type));
1648
+ case "lazy":
1649
+ return rec(type.inner);
1650
+ default:
1651
+ return true;
1652
+ }
1653
+ }
1654
+ __name(isRevalidatable, "isRevalidatable");
1655
+ function responseBodySchema(bodyType, options, preludeVar) {
1656
+ if (!options.validateResponses) return void 0;
1657
+ if (!isRevalidatable(bodyType, options.modelsWithOutput, options.modelsWithTransform)) return void 0;
1658
+ return preludeVar ?? renderType(bodyType);
1659
+ }
1660
+ __name(responseBodySchema, "responseBodySchema");
1661
+ function responseBodyExpr(value, schema) {
1662
+ return schema ? `await parseAndValidate(${value}, ${schema}, 500)` : value;
1663
+ }
1664
+ __name(responseBodyExpr, "responseBodyExpr");
1616
1665
  function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", modelsWithInput) {
1617
1666
  if (!source) return [];
1618
1667
  const lines = [];
@@ -1890,7 +1939,7 @@ function deriveTypeImportPath(file, template) {
1890
1939
  __name(deriveTypeImportPath, "deriveTypeImportPath");
1891
1940
 
1892
1941
  // src/index.ts
1893
- import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs } from "@contractkit/core";
1942
+ import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs, computeModelsWithCaseTransform } from "@contractkit/core";
1894
1943
 
1895
1944
  // src/codegen-sdk.ts
1896
1945
  import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
@@ -4061,7 +4110,14 @@ function createTypescriptPlugin(config, rootDir) {
4061
4110
  };
4062
4111
  }
4063
4112
  __name(createTypescriptPlugin, "createTypescriptPlugin");
4113
+ function assertValidConfig(config) {
4114
+ if (config.server?.validateResponses && !config.server.zod) {
4115
+ 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.");
4116
+ }
4117
+ }
4118
+ __name(assertValidConfig, "assertValidConfig");
4064
4119
  async function runTypescriptCodegen(inputs, ctx, config, rootDir) {
4120
+ assertValidConfig(config);
4065
4121
  const manifestPath = resolve2(ctx.cacheDir, CACHE_MANIFEST_FILENAME);
4066
4122
  const prevManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
4067
4123
  const units = [];
@@ -4178,6 +4234,7 @@ function collectServerOutput(config, rootDir, inputs, units) {
4178
4234
  const serverBase = resolve2(rootDir, config.baseDir ?? ".");
4179
4235
  const modelsWithInput = inputs.modelsWithInput;
4180
4236
  const modelsWithOutput = inputs.modelsWithOutput;
4237
+ const modelsWithTransform = computeModelsWithCaseTransform(inputs.contractRoots.flatMap((r) => r.models));
4181
4238
  const modelMap = buildModelMap(inputs.contractRoots);
4182
4239
  const allFiles = [
4183
4240
  ...inputs.contractRoots.map((r) => r.file),
@@ -4250,6 +4307,10 @@ function collectServerOutput(config, rootDir, inputs, units) {
4250
4307
  modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4251
4308
  servicePathTemplate: config.servicePathTemplate ?? null,
4252
4309
  includeInternal: config.includeInternal ?? true,
4310
+ // Not covered by `sub`: adding `format(input=snake)` to a *different* .ck file changes
4311
+ // this router's output with no change to `root` or the config.
4312
+ modelsWithTransform: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithTransform),
4313
+ validateResponses: config.validateResponses ?? false,
4253
4314
  sub: subConfigKey
4254
4315
  });
4255
4316
  units.push({
@@ -4264,7 +4325,9 @@ function collectServerOutput(config, rootDir, inputs, units) {
4264
4325
  modelOutPaths: serverModelOutPaths,
4265
4326
  modelsWithInput,
4266
4327
  modelsWithOutput,
4267
- includeInternal: config.includeInternal
4328
+ modelsWithTransform,
4329
+ includeInternal: config.includeInternal,
4330
+ validateResponses: config.validateResponses
4268
4331
  })
4269
4332
  }
4270
4333
  ], "render")