@contractkit/plugin-typescript 0.31.2 → 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.
- package/.turbo/turbo-build$colon$ci.log +5 -5
- package/.turbo/turbo-test$colon$ci.log +18 -18
- package/CHANGELOG.md +83 -0
- package/README.md +3 -0
- package/dist/codegen-operation.d.ts +12 -0
- package/dist/codegen-operation.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/codegen-operation.ts +109 -9
- package/src/index.ts +30 -0
- package/tests/codegen-operation.test.ts +252 -2
- package/tests/codegen-server.test.ts +48 -0
- package/tests/helpers.ts +5 -0
- package/tests/pipeline.test.ts +11 -2
|
@@ -5,9 +5,9 @@ $ tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly
|
|
|
5
5
|
[34mCLI[39m tsup v8.5.1
|
|
6
6
|
[34mCLI[39m Target: esnext
|
|
7
7
|
[34mESM[39m Build start
|
|
8
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
9
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
10
|
-
[32mESM[39m ⚡️ Build success in
|
|
8
|
+
[32mESM[39m [1mdist/index.js [22m[32m191.67 KB[39m
|
|
9
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m428.46 KB[39m
|
|
10
|
+
[32mESM[39m ⚡️ Build success in 328ms
|
|
11
11
|
[34mDTS[39m Build start
|
|
12
|
-
[32mDTS[39m ⚡️ Build success in
|
|
13
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
12
|
+
[32mDTS[39m ⚡️ Build success in 3774ms
|
|
13
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m5.08 KB[39m
|
|
@@ -3,34 +3,34 @@ $ vitest run --coverage
|
|
|
3
3
|
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.5 [39m[90m/home/runner/work/ContractKit/ContractKit/packages/plugin-typescript[39m
|
|
4
4
|
[2mCoverage enabled with [22m[33mv8[39m
|
|
5
5
|
|
|
6
|
-
[32m✓[39m tests/codegen-
|
|
7
|
-
[32m✓[39m tests/codegen-
|
|
8
|
-
[32m✓[39m tests/codegen-
|
|
9
|
-
[32m✓[39m tests/codegen-plain-types.test.ts [2m([22m[2m66 tests[22m[2m)[22m[32m
|
|
10
|
-
[32m✓[39m tests/codegen-server.test.ts [2m([22m[
|
|
11
|
-
[32m✓[39m tests/pipeline.test.ts [2m([22m[
|
|
12
|
-
[32m✓[39m tests/codegen-mcp.test.ts [2m([22m[2m24 tests[22m[2m)[22m[32m
|
|
13
|
-
[32m✓[39m tests/escaping-security.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m
|
|
6
|
+
[32m✓[39m tests/codegen-contract.test.ts [2m([22m[2m128 tests[22m[2m)[22m[32m 165[2mms[22m[39m
|
|
7
|
+
[32m✓[39m tests/codegen-sdk.test.ts [2m([22m[2m152 tests[22m[2m)[22m[33m 307[2mms[22m[39m
|
|
8
|
+
[32m✓[39m tests/codegen-operation.test.ts [2m([22m[2m154 tests[22m[2m)[22m[33m 317[2mms[22m[39m
|
|
9
|
+
[32m✓[39m tests/codegen-plain-types.test.ts [2m([22m[2m66 tests[22m[2m)[22m[32m 70[2mms[22m[39m
|
|
10
|
+
[32m✓[39m tests/codegen-server.test.ts [2m([22m[2m30 tests[22m[2m)[22m[32m 123[2mms[22m[39m
|
|
11
|
+
[32m✓[39m tests/pipeline.test.ts [2m([22m[2m29 tests[22m[2m)[22m[32m 279[2mms[22m[39m
|
|
12
|
+
[32m✓[39m tests/codegen-mcp.test.ts [2m([22m[2m24 tests[22m[2m)[22m[32m 55[2mms[22m[39m
|
|
13
|
+
[32m✓[39m tests/escaping-security.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 38[2mms[22m[39m
|
|
14
14
|
|
|
15
15
|
[2m Test Files [22m [1m[32m8 passed[39m[22m[90m (8)[39m
|
|
16
|
-
[2m Tests [22m [1m[
|
|
17
|
-
[2m Start at [22m
|
|
18
|
-
[2m Duration [22m
|
|
16
|
+
[2m Tests [22m [1m[32m601 passed[39m[22m[90m (601)[39m
|
|
17
|
+
[2m Start at [22m 16:35:30
|
|
18
|
+
[2m Duration [22m 8.90s[2m (transform 6.21s, setup 0ms, import 18.05s, tests 1.36s, environment 7ms)[22m
|
|
19
19
|
|
|
20
20
|
[34m % [39m[2mCoverage report from [22m[33mv8[39m
|
|
21
21
|
-------------------|---------|----------|---------|---------|-------------------
|
|
22
22
|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
23
23
|
-------------------|---------|----------|---------|---------|-------------------
|
|
24
|
-
All files |
|
|
25
|
-
src | 83.
|
|
26
|
-
...n-contract.ts |
|
|
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 |
|
|
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 |
|
|
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.
|
|
35
|
-
helpers.ts | 91.
|
|
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,88 @@
|
|
|
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
|
+
|
|
3
86
|
## 0.31.2
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
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;
|
|
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 {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
1243
|
-
|
|
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
|
|
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
|
-
|
|
4328
|
+
modelsWithTransform,
|
|
4329
|
+
includeInternal: config.includeInternal,
|
|
4330
|
+
validateResponses: config.validateResponses
|
|
4268
4331
|
})
|
|
4269
4332
|
}
|
|
4270
4333
|
], "render")
|