@contractkit/plugin-typescript 0.28.2 → 0.29.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 +19 -19
- package/CHANGELOG.md +10 -0
- package/README.md +4 -0
- package/dist/codegen-contract.d.ts +7 -0
- package/dist/codegen-contract.d.ts.map +1 -1
- package/dist/codegen-operation.d.ts.map +1 -1
- package/dist/codegen-plain-types.d.ts +3 -0
- package/dist/codegen-plain-types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -61
- package/dist/index.js.map +1 -1
- package/dist/ts-render.d.ts +21 -3
- package/dist/ts-render.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/codegen-contract.ts +7 -0
- package/src/codegen-operation.ts +65 -5
- package/src/codegen-plain-types.ts +47 -22
- package/src/index.ts +21 -1
- package/src/ts-render.ts +52 -32
- package/tests/codegen-operation.test.ts +71 -0
- package/tests/codegen-plain-types.test.ts +48 -0
- package/tests/codegen-server.test.ts +37 -0
package/dist/ts-render.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ContractTypeNode } from '@contractkit/core';
|
|
2
|
+
/** Declaration emitted into generated files that reference the `json` scalar. */
|
|
2
3
|
export declare const JSON_VALUE_TYPE_DECL = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
|
|
4
|
+
/** Quote a property name unless it is already a valid bare TypeScript identifier. */
|
|
3
5
|
export declare function quoteKey(name: string): string;
|
|
4
6
|
/** Escape text for safe inclusion inside a JSDoc block comment: neutralize the
|
|
5
7
|
* block-comment terminator sequence and split embedded newlines into separate
|
|
@@ -9,18 +11,34 @@ export declare function escapeJsDocLines(text: string): string[];
|
|
|
9
11
|
export declare function escapeSingleQuoted(s: string): string;
|
|
10
12
|
/** Convert an HTTP header name (e.g. `preference-applied`, `X-Request-ID`, `ETag`) to camelCase for use as a JS property. */
|
|
11
13
|
export declare function headerNameToProperty(name: string): string;
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Which runtime the emitted types describe. A few scalars have no single correct TypeScript
|
|
16
|
+
* type: `binary` is a `Blob` in a fetch-based client but a `Buffer` on a Node server. Everything
|
|
17
|
+
* else renders identically for both targets. Defaults to `'client'`.
|
|
18
|
+
*/
|
|
19
|
+
export type TsRenderTarget = 'client' | 'server';
|
|
20
|
+
/**
|
|
21
|
+
* Render a contract type as a plain TypeScript type expression. Model refs render as their bare
|
|
22
|
+
* name; use `renderInputTsType` / `renderOutputTsType` to substitute Input/Output variants.
|
|
23
|
+
*
|
|
24
|
+
* @param target Runtime the type describes; only `binary` differs (`Buffer` vs `Blob`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderTsType(type: ContractTypeNode, target?: TsRenderTarget): string;
|
|
13
27
|
/**
|
|
14
28
|
* Like renderTsType, but substitutes model refs with their Input variant
|
|
15
29
|
* when the model has visibility modifiers. Used for request-side types
|
|
16
30
|
* (body, params, query, headers).
|
|
31
|
+
*
|
|
32
|
+
* @param target Runtime the type describes; only `binary` differs (`Buffer` vs `Blob`).
|
|
17
33
|
*/
|
|
18
|
-
export declare function renderInputTsType(type: ContractTypeNode, modelsWithInput?: Set<string
|
|
34
|
+
export declare function renderInputTsType(type: ContractTypeNode, modelsWithInput?: Set<string>, target?: TsRenderTarget): string;
|
|
19
35
|
/**
|
|
20
36
|
* Like renderTsType, but substitutes model refs with their Output variant
|
|
21
37
|
* (post-transform wire shape) when the model has format(output=...) or
|
|
22
38
|
* transitively references one. Used for response-side types in routers
|
|
23
39
|
* and SDK return types.
|
|
40
|
+
*
|
|
41
|
+
* @param target Runtime the type describes; only `binary` differs (`Buffer` vs `Blob`).
|
|
24
42
|
*/
|
|
25
|
-
export declare function renderOutputTsType(type: ContractTypeNode, modelsWithOutput?: Set<string
|
|
43
|
+
export declare function renderOutputTsType(type: ContractTypeNode, modelsWithOutput?: Set<string>, target?: TsRenderTarget): string;
|
|
26
44
|
//# sourceMappingURL=ts-render.d.ts.map
|
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,eAAO,MAAM,oBAAoB,2GAA2G,CAAC;AAE7I,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,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,
|
|
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;AAgDD;;;;;;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/package.json
CHANGED
package/src/codegen-contract.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
collectExternalOutputRefs as ckCollectExternalOutputRefs,
|
|
23
23
|
} from '@contractkit/core';
|
|
24
24
|
import { escapeJsDocLines } from './ts-render.js';
|
|
25
|
+
import type { TsRenderTarget } from './ts-render.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Maps a ContractKit object mode to its Zod constructor name.
|
|
@@ -53,6 +54,12 @@ export interface ContractCodegenContext {
|
|
|
53
54
|
modelsWithOutput?: Set<string>;
|
|
54
55
|
/** If set, import JsonValue from this path instead of re-declaring it (avoids barrel re-export conflicts) */
|
|
55
56
|
jsonValueImportPath?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Runtime the emitted plain types describe. Only affects scalars whose TypeScript type differs
|
|
59
|
+
* per runtime (`binary` → `Buffer` on the server, `Blob` in the client). Ignored by
|
|
60
|
+
* `generateContract`, whose Zod schemas are server-shaped by construction. Default `'client'`.
|
|
61
|
+
*/
|
|
62
|
+
target?: TsRenderTarget;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
// ─── Public entry point ────────────────────────────────────────────────────
|
package/src/codegen-operation.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpRootNode, OpRouteNode, OpOperationNode, ContractTypeNode, ParamSource, ObjectMode } from '@contractkit/core';
|
|
1
|
+
import type { OpRootNode, OpRouteNode, OpOperationNode, ContractTypeNode, ScalarTypeNode, ParamSource, ObjectMode } from '@contractkit/core';
|
|
2
2
|
import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType } from '@contractkit/core';
|
|
3
3
|
import {
|
|
4
4
|
renderType,
|
|
@@ -153,8 +153,14 @@ export function generateOp(root: OpRootNode, options: OpCodegenOptions = {}): st
|
|
|
153
153
|
body.push(...generateTypeImports(types, root.file, options));
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// luxon is needed for date/time/datetime (DateTime), duration (Duration) and interval (Interval);
|
|
157
|
+
// the rendered Zod schemas and the service-result annotations both reference these classes.
|
|
158
|
+
const luxonImports: string[] = [];
|
|
159
|
+
if (opNeedsDateTime(root)) luxonImports.push('DateTime');
|
|
160
|
+
if (opNeedsScalar(root, 'duration')) luxonImports.push('Duration');
|
|
161
|
+
if (opNeedsScalar(root, 'interval')) luxonImports.push('Interval');
|
|
162
|
+
if (luxonImports.length > 0) {
|
|
163
|
+
body.push(`import { ${luxonImports.join(', ')} } from 'luxon';`);
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
if (needsParseAndValidate) {
|
|
@@ -174,6 +180,11 @@ export function generateOp(root: OpRootNode, options: OpCodegenOptions = {}): st
|
|
|
174
180
|
`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' }));`,
|
|
175
181
|
);
|
|
176
182
|
}
|
|
183
|
+
if (opNeedsScalar(root, 'interval')) {
|
|
184
|
+
helpers.push(
|
|
185
|
+
`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()!);`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
177
188
|
if (opNeedsScalar(root, 'json')) {
|
|
178
189
|
helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
|
|
179
190
|
helpers.push(
|
|
@@ -317,7 +328,10 @@ function generateHandler(route: OpRouteNode, op: OpOperationNode, root: OpRootNo
|
|
|
317
328
|
const hasRespHeaders = respHeaders.length > 0;
|
|
318
329
|
const headersAnnotation = hasRespHeaders
|
|
319
330
|
? `{ ${respHeaders
|
|
320
|
-
.map(
|
|
331
|
+
.map(
|
|
332
|
+
h =>
|
|
333
|
+
`${quoteKey(headerNameToProperty(h.name))}${h.optional ? '?' : ''}: ${renderOutputTsType(h.type, options.modelsWithOutput, 'server')}`,
|
|
334
|
+
)
|
|
321
335
|
.join('; ')} }`
|
|
322
336
|
: '';
|
|
323
337
|
|
|
@@ -442,6 +456,52 @@ export function buildArgs(route: OpRouteNode, op: OpOperationNode): string {
|
|
|
442
456
|
return args.join(', ');
|
|
443
457
|
}
|
|
444
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Map a `.ck` scalar to the TypeScript type a server handler sees, i.e. `z.infer` of the schema
|
|
461
|
+
* `renderType` emits for that scalar. This is deliberately NOT `renderTsScalar` from ts-render:
|
|
462
|
+
* that one describes the wire/SDK view (`binary` → `Blob`, dates → `string`), while the router
|
|
463
|
+
* runs on Node against the parsed Zod output (`binary` → `Buffer`, dates → luxon `DateTime`).
|
|
464
|
+
*/
|
|
465
|
+
function serverTsScalar(name: ScalarTypeNode['name']): string {
|
|
466
|
+
switch (name) {
|
|
467
|
+
case 'string':
|
|
468
|
+
case 'email':
|
|
469
|
+
case 'url':
|
|
470
|
+
case 'uuid':
|
|
471
|
+
return 'string';
|
|
472
|
+
case 'number':
|
|
473
|
+
case 'int':
|
|
474
|
+
return 'number';
|
|
475
|
+
case 'bigint':
|
|
476
|
+
return 'bigint';
|
|
477
|
+
case 'boolean':
|
|
478
|
+
return 'boolean';
|
|
479
|
+
case 'date':
|
|
480
|
+
case 'time':
|
|
481
|
+
case 'datetime':
|
|
482
|
+
return 'DateTime';
|
|
483
|
+
case 'duration':
|
|
484
|
+
return 'Duration';
|
|
485
|
+
case 'interval':
|
|
486
|
+
// _ZodInterval transforms to an ISO string, so the inferred output type is string.
|
|
487
|
+
return 'string';
|
|
488
|
+
case 'binary':
|
|
489
|
+
return 'Buffer';
|
|
490
|
+
case 'json':
|
|
491
|
+
return '_JsonValue';
|
|
492
|
+
case 'object':
|
|
493
|
+
return 'Record<string, unknown>';
|
|
494
|
+
case 'null':
|
|
495
|
+
return 'null';
|
|
496
|
+
case 'unknown':
|
|
497
|
+
return 'unknown';
|
|
498
|
+
default: {
|
|
499
|
+
const _exhaustive: never = name;
|
|
500
|
+
throw new Error(`plugin-typescript: unmapped scalar '${String(_exhaustive)}' — add a case`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
445
505
|
function formatTypeAnnotation(bodyType: ContractTypeNode, modelsWithOutput?: Set<string>): { annotation: string; prelude?: string } {
|
|
446
506
|
if (bodyType.kind === 'array') {
|
|
447
507
|
const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput);
|
|
@@ -451,7 +511,7 @@ function formatTypeAnnotation(bodyType: ContractTypeNode, modelsWithOutput?: Set
|
|
|
451
511
|
const name = modelsWithOutput?.has(bodyType.name) ? `${bodyType.name}Output` : bodyType.name;
|
|
452
512
|
return { annotation: name };
|
|
453
513
|
}
|
|
454
|
-
if (bodyType.kind === 'scalar') return { annotation: bodyType.name };
|
|
514
|
+
if (bodyType.kind === 'scalar') return { annotation: serverTsScalar(bodyType.name) };
|
|
455
515
|
// For complex types, extract schema into a variable so the result line stays readable
|
|
456
516
|
const schema = renderType(bodyType);
|
|
457
517
|
return {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
rootNeedsScalar,
|
|
12
12
|
} from './codegen-contract.js';
|
|
13
13
|
import { renderTsType, renderInputTsType, renderOutputTsType, quoteKey, escapeJsDocLines, JSON_VALUE_TYPE_DECL } from './ts-render.js';
|
|
14
|
+
import type { TsRenderTarget } from './ts-render.js';
|
|
14
15
|
|
|
15
16
|
// ─── Public entry point ────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -19,8 +20,12 @@ import { renderTsType, renderInputTsType, renderOutputTsType, quoteKey, escapeJs
|
|
|
19
20
|
* Unlike `generateContract()` which produces Zod schemas, this emits
|
|
20
21
|
* vanilla TypeScript `interface` and `type` declarations suitable
|
|
21
22
|
* for SDK consumers that don't need runtime validation.
|
|
23
|
+
*
|
|
24
|
+
* @param context Import resolution and Input/Output variant sets. `context.target` selects the
|
|
25
|
+
* runtime the types describe (`'server'` renders `binary` as `Buffer`, `'client'` as `Blob`).
|
|
22
26
|
*/
|
|
23
27
|
export function generatePlainTypes(root: ContractRootNode, context?: ContractCodegenContext): string {
|
|
28
|
+
const target: TsRenderTarget = context?.target ?? 'client';
|
|
24
29
|
const externalRefs = collectExternalRefs(root);
|
|
25
30
|
const lines: string[] = [];
|
|
26
31
|
|
|
@@ -58,7 +63,7 @@ export function generatePlainTypes(root: ContractRootNode, context?: ContractCod
|
|
|
58
63
|
const modelMap = new Map(root.models.map(m => [m.name, m]));
|
|
59
64
|
|
|
60
65
|
for (const model of topoSortModels(root.models)) {
|
|
61
|
-
lines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
|
|
66
|
+
lines.push(...generateModel(model, target, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
|
|
62
67
|
lines.push('');
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -69,6 +74,7 @@ export function generatePlainTypes(root: ContractRootNode, context?: ContractCod
|
|
|
69
74
|
|
|
70
75
|
function generateModel(
|
|
71
76
|
model: ModelNode,
|
|
77
|
+
target: TsRenderTarget,
|
|
72
78
|
outPath?: string,
|
|
73
79
|
modelsWithInput?: Set<string>,
|
|
74
80
|
modelsWithOutput?: Set<string>,
|
|
@@ -76,18 +82,20 @@ function generateModel(
|
|
|
76
82
|
): string[] {
|
|
77
83
|
// Type alias: Name : typeExpression
|
|
78
84
|
if (model.type) {
|
|
79
|
-
return generateTypeAlias(model, outPath, modelsWithInput, modelsWithOutput);
|
|
85
|
+
return generateTypeAlias(model, target, outPath, modelsWithInput, modelsWithOutput);
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
// A model needs Input/read split if it has visibility-modified fields OR if it
|
|
83
89
|
// transitively references models that have Input variants (captured in modelsWithInput).
|
|
84
90
|
const needsInputSplit = model.fields.some(f => f.visibility !== 'normal') || (modelsWithInput?.has(model.name) ?? false);
|
|
85
91
|
|
|
86
|
-
const lines = needsInputSplit
|
|
92
|
+
const lines = needsInputSplit
|
|
93
|
+
? generateVisibilityModel(model, target, outPath, modelsWithInput, modelMap)
|
|
94
|
+
: generateSimpleModel(model, target, outPath, modelMap);
|
|
87
95
|
|
|
88
96
|
if (modelsWithOutput?.has(model.name)) {
|
|
89
97
|
lines.push('');
|
|
90
|
-
lines.push(...generateOutputModel(model, modelsWithOutput));
|
|
98
|
+
lines.push(...generateOutputModel(model, target, modelsWithOutput));
|
|
91
99
|
}
|
|
92
100
|
return lines;
|
|
93
101
|
}
|
|
@@ -132,15 +140,21 @@ function generateComments(model: ModelNode, outPath?: string): string[] {
|
|
|
132
140
|
return lines;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
function generateTypeAlias(
|
|
143
|
+
function generateTypeAlias(
|
|
144
|
+
model: ModelNode,
|
|
145
|
+
target: TsRenderTarget,
|
|
146
|
+
outPath?: string,
|
|
147
|
+
modelsWithInput?: Set<string>,
|
|
148
|
+
modelsWithOutput?: Set<string>,
|
|
149
|
+
): string[] {
|
|
136
150
|
const lines: string[] = [];
|
|
137
151
|
lines.push(...generateComments(model, outPath));
|
|
138
|
-
lines.push(`export type ${model.name} = ${renderTsType(model.type
|
|
152
|
+
lines.push(`export type ${model.name} = ${renderTsType(model.type!, target)};`);
|
|
139
153
|
if (modelsWithInput?.has(model.name)) {
|
|
140
|
-
lines.push(`export type ${model.name}Input = ${renderInputTsType(model.type!, modelsWithInput)};`);
|
|
154
|
+
lines.push(`export type ${model.name}Input = ${renderInputTsType(model.type!, modelsWithInput, target)};`);
|
|
141
155
|
}
|
|
142
156
|
if (modelsWithOutput?.has(model.name)) {
|
|
143
|
-
lines.push(`export type ${model.name}Output = ${renderOutputTsType(model.type!, modelsWithOutput)};`);
|
|
157
|
+
lines.push(`export type ${model.name}Output = ${renderOutputTsType(model.type!, modelsWithOutput, target)};`);
|
|
144
158
|
}
|
|
145
159
|
return lines;
|
|
146
160
|
}
|
|
@@ -158,7 +172,7 @@ function buildExtendsClause(bases: string[], overrideNames: string[], baseNameRe
|
|
|
158
172
|
return ` extends ${wrapped.join(', ')}`;
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
function generateSimpleModel(model: ModelNode, outPath?: string, modelMap?: Map<string, ModelNode>): string[] {
|
|
175
|
+
function generateSimpleModel(model: ModelNode, target: TsRenderTarget, outPath?: string, modelMap?: Map<string, ModelNode>): string[] {
|
|
162
176
|
const lines: string[] = [];
|
|
163
177
|
lines.push(...generateComments(model, outPath));
|
|
164
178
|
|
|
@@ -167,14 +181,20 @@ function generateSimpleModel(model: ModelNode, outPath?: string, modelMap?: Map<
|
|
|
167
181
|
lines.push(`export interface ${model.name}${buildExtendsClause(bases, overrideNames, b => b)} {`);
|
|
168
182
|
|
|
169
183
|
for (const field of model.fields) {
|
|
170
|
-
lines.push(` ${renderField(field)}`);
|
|
184
|
+
lines.push(` ${renderField(field, target)}`);
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
lines.push('}');
|
|
174
188
|
return lines;
|
|
175
189
|
}
|
|
176
190
|
|
|
177
|
-
function generateVisibilityModel(
|
|
191
|
+
function generateVisibilityModel(
|
|
192
|
+
model: ModelNode,
|
|
193
|
+
target: TsRenderTarget,
|
|
194
|
+
outPath?: string,
|
|
195
|
+
modelsWithInput?: Set<string>,
|
|
196
|
+
modelMap?: Map<string, ModelNode>,
|
|
197
|
+
): string[] {
|
|
178
198
|
const lines: string[] = [];
|
|
179
199
|
lines.push(...generateComments(model, outPath));
|
|
180
200
|
|
|
@@ -185,7 +205,7 @@ function generateVisibilityModel(model: ModelNode, outPath?: string, modelsWithI
|
|
|
185
205
|
const readFields = model.fields.filter(f => f.visibility !== 'writeonly');
|
|
186
206
|
lines.push(`export interface ${model.name}${buildExtendsClause(bases, overrideNames, b => b)} {`);
|
|
187
207
|
for (const field of readFields) {
|
|
188
|
-
lines.push(` ${renderField(field)}`);
|
|
208
|
+
lines.push(` ${renderField(field, target)}`);
|
|
189
209
|
}
|
|
190
210
|
lines.push('}');
|
|
191
211
|
lines.push('');
|
|
@@ -196,7 +216,7 @@ function generateVisibilityModel(model: ModelNode, outPath?: string, modelsWithI
|
|
|
196
216
|
const inputResolver = (b: string) => (modelsWithInput?.has(b) ? `${b}Input` : b);
|
|
197
217
|
lines.push(`export interface ${model.name}Input${buildExtendsClause(bases, overrideNames, inputResolver)} {`);
|
|
198
218
|
for (const field of writeFields) {
|
|
199
|
-
lines.push(` ${modelsWithInput ? renderInputField(field, modelsWithInput) : renderField(field)}`);
|
|
219
|
+
lines.push(` ${modelsWithInput ? renderInputField(field, modelsWithInput, target) : renderField(field, target)}`);
|
|
200
220
|
}
|
|
201
221
|
lines.push('}');
|
|
202
222
|
|
|
@@ -217,9 +237,9 @@ function withFieldJsDoc(jsdocParts: string[], line: string): string {
|
|
|
217
237
|
return `/**\n${body}\n */\n ${line}`;
|
|
218
238
|
}
|
|
219
239
|
|
|
220
|
-
function renderField(field: FieldNode): string {
|
|
240
|
+
function renderField(field: FieldNode, target: TsRenderTarget): string {
|
|
221
241
|
const opt = field.optional || field.default !== undefined ? '?' : '';
|
|
222
|
-
let typeStr = renderTsType(field.type);
|
|
242
|
+
let typeStr = renderTsType(field.type, target);
|
|
223
243
|
if (field.nullable) typeStr += ' | null';
|
|
224
244
|
const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
|
|
225
245
|
const jsdocParts: string[] = [];
|
|
@@ -228,9 +248,9 @@ function renderField(field: FieldNode): string {
|
|
|
228
248
|
return withFieldJsDoc(jsdocParts, line);
|
|
229
249
|
}
|
|
230
250
|
|
|
231
|
-
function renderInputField(field: FieldNode, modelsWithInput: Set<string
|
|
251
|
+
function renderInputField(field: FieldNode, modelsWithInput: Set<string>, target: TsRenderTarget): string {
|
|
232
252
|
const opt = field.optional || field.default !== undefined ? '?' : '';
|
|
233
|
-
let typeStr = renderInputTsType(field.type, modelsWithInput);
|
|
253
|
+
let typeStr = renderInputTsType(field.type, modelsWithInput, target);
|
|
234
254
|
if (field.nullable) typeStr += ' | null';
|
|
235
255
|
const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
|
|
236
256
|
const jsdocParts: string[] = [];
|
|
@@ -264,7 +284,7 @@ function applyOutputCase(name: string, c: 'camel' | 'snake' | 'pascal' | undefin
|
|
|
264
284
|
* ancestor has format(...) (see `flattenFormatChain` in codegen-contract); we mirror that here
|
|
265
285
|
* so the plain interface matches the wire shape produced by the Zod transform.
|
|
266
286
|
*/
|
|
267
|
-
function generateOutputModel(model: ModelNode, modelsWithOutput: Set<string>): string[] {
|
|
287
|
+
function generateOutputModel(model: ModelNode, target: TsRenderTarget, modelsWithOutput: Set<string>): string[] {
|
|
268
288
|
const lines: string[] = [];
|
|
269
289
|
const outputCase = model.outputCase && model.outputCase !== 'camel' ? model.outputCase : undefined;
|
|
270
290
|
const readFields = model.fields.filter(f => f.visibility !== 'writeonly');
|
|
@@ -279,7 +299,7 @@ function generateOutputModel(model: ModelNode, modelsWithOutput: Set<string>): s
|
|
|
279
299
|
: '';
|
|
280
300
|
lines.push(`export interface ${model.name}Output${baseExt} {`);
|
|
281
301
|
for (const field of readFields) {
|
|
282
|
-
lines.push(` ${renderOutputField(field, model.outputCase, modelsWithOutput)}`);
|
|
302
|
+
lines.push(` ${renderOutputField(field, model.outputCase, modelsWithOutput, target)}`);
|
|
283
303
|
}
|
|
284
304
|
lines.push('}');
|
|
285
305
|
return lines;
|
|
@@ -288,16 +308,21 @@ function generateOutputModel(model: ModelNode, modelsWithOutput: Set<string>): s
|
|
|
288
308
|
// Direct hit: emit a flat interface with renamed keys.
|
|
289
309
|
lines.push(`export interface ${model.name}Output {`);
|
|
290
310
|
for (const field of readFields) {
|
|
291
|
-
lines.push(` ${renderOutputField(field, outputCase, modelsWithOutput)}`);
|
|
311
|
+
lines.push(` ${renderOutputField(field, outputCase, modelsWithOutput, target)}`);
|
|
292
312
|
}
|
|
293
313
|
lines.push('}');
|
|
294
314
|
return lines;
|
|
295
315
|
}
|
|
296
316
|
|
|
297
|
-
function renderOutputField(
|
|
317
|
+
function renderOutputField(
|
|
318
|
+
field: FieldNode,
|
|
319
|
+
outputCase: 'camel' | 'snake' | 'pascal' | undefined,
|
|
320
|
+
modelsWithOutput: Set<string>,
|
|
321
|
+
target: TsRenderTarget,
|
|
322
|
+
): string {
|
|
298
323
|
const opt = field.optional || field.default !== undefined ? '?' : '';
|
|
299
324
|
const key = applyOutputCase(field.name, outputCase);
|
|
300
|
-
let typeStr = renderOutputTsType(field.type, modelsWithOutput);
|
|
325
|
+
let typeStr = renderOutputTsType(field.type, modelsWithOutput, target);
|
|
301
326
|
if (field.nullable) typeStr += ' | null';
|
|
302
327
|
const line = `${quoteKey(key)}${opt}: ${typeStr};`;
|
|
303
328
|
const jsdocParts: string[] = [];
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
|
|
58
58
|
// ─── Sub-config interfaces ─────────────────────────────────────────────────
|
|
59
59
|
|
|
60
|
+
/** Koa server output: routers, and the type or Zod schema files they import. */
|
|
60
61
|
export interface ServerConfig {
|
|
61
62
|
/** Directory (relative to rootDir) where server files are written. Default: rootDir. */
|
|
62
63
|
baseDir?: string;
|
|
@@ -74,6 +75,7 @@ export interface ServerConfig {
|
|
|
74
75
|
includeInternal?: boolean;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/** TypeScript SDK client output: the client class, per-area operation clients, and their types. */
|
|
77
79
|
export interface SdkConfig {
|
|
78
80
|
baseDir?: string;
|
|
79
81
|
name?: string;
|
|
@@ -95,16 +97,25 @@ export interface SdkConfig {
|
|
|
95
97
|
scaffold?: boolean;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/** Standalone Zod schema output, independent of the server and SDK sub-generators. */
|
|
98
101
|
export interface ZodConfig {
|
|
99
102
|
baseDir?: string;
|
|
100
103
|
output?: string;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/** Standalone plain TypeScript type output, independent of the server and SDK sub-generators. */
|
|
103
107
|
export interface TypesConfig {
|
|
104
108
|
baseDir?: string;
|
|
105
109
|
output?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Runtime the emitted types describe. Affects scalars whose TypeScript type is runtime-specific:
|
|
112
|
+
* `binary` renders as `Buffer` for `'server'` and `Blob` for `'client'`. Default `'client'`.
|
|
113
|
+
* The `server` and `sdk` sub-generators set this themselves.
|
|
114
|
+
*/
|
|
115
|
+
target?: 'client' | 'server';
|
|
106
116
|
}
|
|
107
117
|
|
|
118
|
+
/** MCP tool output: per-op-file handlers, the aggregator, and the optional POST route. */
|
|
108
119
|
export interface McpConfig {
|
|
109
120
|
/** Directory (relative to rootDir) where MCP files are written. Default: rootDir. */
|
|
110
121
|
baseDir?: string;
|
|
@@ -132,6 +143,7 @@ export interface McpConfig {
|
|
|
132
143
|
includeInternal?: boolean;
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
/** Top-level plugin config. Each sub-config that is present enables its sub-generator. */
|
|
135
147
|
export interface TypescriptPluginConfig {
|
|
136
148
|
server?: ServerConfig;
|
|
137
149
|
sdk?: SdkConfig;
|
|
@@ -355,6 +367,8 @@ function collectServerOutput(
|
|
|
355
367
|
currentOutPath: typeOutPath,
|
|
356
368
|
modelsWithInput,
|
|
357
369
|
modelsWithOutput,
|
|
370
|
+
// These types are consumed by Koa handlers, so `binary` is a Buffer, not a Blob.
|
|
371
|
+
target: 'server' as const,
|
|
358
372
|
};
|
|
359
373
|
const content = config.zod ? generateContract(ast, renderCtx) : generatePlainTypes(ast, renderCtx);
|
|
360
374
|
return [{ relativePath: typeOutPath, content }];
|
|
@@ -861,7 +875,13 @@ function collectTypesOutput(
|
|
|
861
875
|
render: () => [
|
|
862
876
|
{
|
|
863
877
|
relativePath: outPath,
|
|
864
|
-
content: generatePlainTypes(ast, {
|
|
878
|
+
content: generatePlainTypes(ast, {
|
|
879
|
+
modelOutPaths,
|
|
880
|
+
currentOutPath: outPath,
|
|
881
|
+
modelsWithInput,
|
|
882
|
+
modelsWithOutput,
|
|
883
|
+
target: config.target,
|
|
884
|
+
}),
|
|
865
885
|
},
|
|
866
886
|
],
|
|
867
887
|
});
|
package/src/ts-render.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ContractTypeNode, FieldNode, ScalarTypeNode } from '@contractkit/core';
|
|
2
2
|
|
|
3
|
+
/** Declaration emitted into generated files that reference the `json` scalar. */
|
|
3
4
|
export const JSON_VALUE_TYPE_DECL = 'export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };';
|
|
4
5
|
|
|
6
|
+
/** Quote a property name unless it is already a valid bare TypeScript identifier. */
|
|
5
7
|
export function quoteKey(name: string): string {
|
|
6
8
|
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
|
|
7
9
|
}
|
|
@@ -31,12 +33,25 @@ export function headerNameToProperty(name: string): string {
|
|
|
31
33
|
|
|
32
34
|
// ─── TypeScript type rendering ────────────────────────────────────────────
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Which runtime the emitted types describe. A few scalars have no single correct TypeScript
|
|
38
|
+
* type: `binary` is a `Blob` in a fetch-based client but a `Buffer` on a Node server. Everything
|
|
39
|
+
* else renders identically for both targets. Defaults to `'client'`.
|
|
40
|
+
*/
|
|
41
|
+
export type TsRenderTarget = 'client' | 'server';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render a contract type as a plain TypeScript type expression. Model refs render as their bare
|
|
45
|
+
* name; use `renderInputTsType` / `renderOutputTsType` to substitute Input/Output variants.
|
|
46
|
+
*
|
|
47
|
+
* @param target Runtime the type describes; only `binary` differs (`Buffer` vs `Blob`).
|
|
48
|
+
*/
|
|
49
|
+
export function renderTsType(type: ContractTypeNode, target: TsRenderTarget = 'client'): string {
|
|
35
50
|
switch (type.kind) {
|
|
36
51
|
case 'scalar':
|
|
37
|
-
return renderTsScalar(type.name);
|
|
52
|
+
return renderTsScalar(type.name, target);
|
|
38
53
|
case 'array': {
|
|
39
|
-
const inner = renderTsType(type.item);
|
|
54
|
+
const inner = renderTsType(type.item, target);
|
|
40
55
|
const needsParens =
|
|
41
56
|
type.item.kind === 'union' ||
|
|
42
57
|
type.item.kind === 'discriminatedUnion' ||
|
|
@@ -45,31 +60,31 @@ export function renderTsType(type: ContractTypeNode): string {
|
|
|
45
60
|
return needsParens ? `(${inner})[]` : `${inner}[]`;
|
|
46
61
|
}
|
|
47
62
|
case 'tuple':
|
|
48
|
-
return `[${type.items.map(renderTsType).join(', ')}]`;
|
|
63
|
+
return `[${type.items.map(i => renderTsType(i, target)).join(', ')}]`;
|
|
49
64
|
case 'record':
|
|
50
|
-
return `Record<${renderTsType(type.key)}, ${renderTsType(type.value)}>`;
|
|
65
|
+
return `Record<${renderTsType(type.key, target)}, ${renderTsType(type.value, target)}>`;
|
|
51
66
|
case 'enum':
|
|
52
67
|
return type.values.map(v => `'${escapeSingleQuoted(v)}'`).join(' | ');
|
|
53
68
|
case 'literal':
|
|
54
69
|
return typeof type.value === 'string' ? `'${escapeSingleQuoted(type.value)}'` : String(type.value);
|
|
55
70
|
case 'union':
|
|
56
|
-
return type.members.map(renderTsType).join(' | ');
|
|
71
|
+
return type.members.map(m => renderTsType(m, target)).join(' | ');
|
|
57
72
|
case 'discriminatedUnion':
|
|
58
|
-
return type.members.map(renderTsType).join(' | ');
|
|
73
|
+
return type.members.map(m => renderTsType(m, target)).join(' | ');
|
|
59
74
|
case 'intersection':
|
|
60
|
-
return type.members.map(renderTsType).join(' & ');
|
|
75
|
+
return type.members.map(m => renderTsType(m, target)).join(' & ');
|
|
61
76
|
case 'ref':
|
|
62
77
|
return type.name;
|
|
63
78
|
case 'lazy':
|
|
64
|
-
return renderTsType(type.inner);
|
|
79
|
+
return renderTsType(type.inner, target);
|
|
65
80
|
case 'inlineObject':
|
|
66
|
-
return renderTsInlineObject(type.fields);
|
|
81
|
+
return renderTsInlineObject(type.fields, target);
|
|
67
82
|
default:
|
|
68
83
|
return 'unknown';
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
function renderTsScalar(name: ScalarTypeNode['name']): string {
|
|
87
|
+
function renderTsScalar(name: ScalarTypeNode['name'], target: TsRenderTarget): string {
|
|
73
88
|
switch (name) {
|
|
74
89
|
case 'string':
|
|
75
90
|
case 'email':
|
|
@@ -96,7 +111,8 @@ function renderTsScalar(name: ScalarTypeNode['name']): string {
|
|
|
96
111
|
case 'object':
|
|
97
112
|
return 'Record<string, unknown>';
|
|
98
113
|
case 'binary':
|
|
99
|
-
|
|
114
|
+
// Node servers hand the handler a Buffer (matching `_ZodBinary`); fetch clients get a Blob.
|
|
115
|
+
return target === 'server' ? 'Buffer' : 'Blob';
|
|
100
116
|
case 'json':
|
|
101
117
|
return 'JsonValue';
|
|
102
118
|
default: {
|
|
@@ -106,10 +122,10 @@ function renderTsScalar(name: ScalarTypeNode['name']): string {
|
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
function renderTsInlineObject(fields: FieldNode[]): string {
|
|
125
|
+
function renderTsInlineObject(fields: FieldNode[], target: TsRenderTarget): string {
|
|
110
126
|
const entries = fields.map(f => {
|
|
111
127
|
const opt = f.optional ? '?' : '';
|
|
112
|
-
return `${quoteKey(f.name)}${opt}: ${renderTsType(f.type)}`;
|
|
128
|
+
return `${quoteKey(f.name)}${opt}: ${renderTsType(f.type, target)}`;
|
|
113
129
|
});
|
|
114
130
|
return `{ ${entries.join('; ')} }`;
|
|
115
131
|
}
|
|
@@ -118,14 +134,16 @@ function renderTsInlineObject(fields: FieldNode[]): string {
|
|
|
118
134
|
* Like renderTsType, but substitutes model refs with their Input variant
|
|
119
135
|
* when the model has visibility modifiers. Used for request-side types
|
|
120
136
|
* (body, params, query, headers).
|
|
137
|
+
*
|
|
138
|
+
* @param target Runtime the type describes; only `binary` differs (`Buffer` vs `Blob`).
|
|
121
139
|
*/
|
|
122
|
-
export function renderInputTsType(type: ContractTypeNode, modelsWithInput?: Set<string
|
|
123
|
-
if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type);
|
|
140
|
+
export function renderInputTsType(type: ContractTypeNode, modelsWithInput?: Set<string>, target: TsRenderTarget = 'client'): string {
|
|
141
|
+
if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type, target);
|
|
124
142
|
switch (type.kind) {
|
|
125
143
|
case 'ref':
|
|
126
144
|
return modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
|
|
127
145
|
case 'array': {
|
|
128
|
-
const inner = renderInputTsType(type.item, modelsWithInput);
|
|
146
|
+
const inner = renderInputTsType(type.item, modelsWithInput, target);
|
|
129
147
|
const needsParens =
|
|
130
148
|
type.item.kind === 'union' ||
|
|
131
149
|
type.item.kind === 'discriminatedUnion' ||
|
|
@@ -134,17 +152,17 @@ export function renderInputTsType(type: ContractTypeNode, modelsWithInput?: Set<
|
|
|
134
152
|
return needsParens ? `(${inner})[]` : `${inner}[]`;
|
|
135
153
|
}
|
|
136
154
|
case 'intersection':
|
|
137
|
-
return type.members.map(m => renderInputTsType(m, modelsWithInput)).join(' & ');
|
|
155
|
+
return type.members.map(m => renderInputTsType(m, modelsWithInput, target)).join(' & ');
|
|
138
156
|
case 'union':
|
|
139
|
-
return type.members.map(m => renderInputTsType(m, modelsWithInput)).join(' | ');
|
|
157
|
+
return type.members.map(m => renderInputTsType(m, modelsWithInput, target)).join(' | ');
|
|
140
158
|
case 'discriminatedUnion':
|
|
141
|
-
return type.members.map(m => renderInputTsType(m, modelsWithInput)).join(' | ');
|
|
159
|
+
return type.members.map(m => renderInputTsType(m, modelsWithInput, target)).join(' | ');
|
|
142
160
|
case 'inlineObject':
|
|
143
|
-
return `{ ${type.fields.map(f => `${quoteKey(f.name)}${f.optional ? '?' : ''}: ${renderInputTsType(f.type, modelsWithInput)}`).join('; ')} }`;
|
|
161
|
+
return `{ ${type.fields.map(f => `${quoteKey(f.name)}${f.optional ? '?' : ''}: ${renderInputTsType(f.type, modelsWithInput, target)}`).join('; ')} }`;
|
|
144
162
|
case 'lazy':
|
|
145
|
-
return renderInputTsType(type.inner, modelsWithInput);
|
|
163
|
+
return renderInputTsType(type.inner, modelsWithInput, target);
|
|
146
164
|
default:
|
|
147
|
-
return renderTsType(type);
|
|
165
|
+
return renderTsType(type, target);
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
|
|
@@ -153,14 +171,16 @@ export function renderInputTsType(type: ContractTypeNode, modelsWithInput?: Set<
|
|
|
153
171
|
* (post-transform wire shape) when the model has format(output=...) or
|
|
154
172
|
* transitively references one. Used for response-side types in routers
|
|
155
173
|
* and SDK return types.
|
|
174
|
+
*
|
|
175
|
+
* @param target Runtime the type describes; only `binary` differs (`Buffer` vs `Blob`).
|
|
156
176
|
*/
|
|
157
|
-
export function renderOutputTsType(type: ContractTypeNode, modelsWithOutput?: Set<string
|
|
158
|
-
if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type);
|
|
177
|
+
export function renderOutputTsType(type: ContractTypeNode, modelsWithOutput?: Set<string>, target: TsRenderTarget = 'client'): string {
|
|
178
|
+
if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type, target);
|
|
159
179
|
switch (type.kind) {
|
|
160
180
|
case 'ref':
|
|
161
181
|
return modelsWithOutput.has(type.name) ? `${type.name}Output` : type.name;
|
|
162
182
|
case 'array': {
|
|
163
|
-
const inner = renderOutputTsType(type.item, modelsWithOutput);
|
|
183
|
+
const inner = renderOutputTsType(type.item, modelsWithOutput, target);
|
|
164
184
|
const needsParens =
|
|
165
185
|
type.item.kind === 'union' ||
|
|
166
186
|
type.item.kind === 'discriminatedUnion' ||
|
|
@@ -169,16 +189,16 @@ export function renderOutputTsType(type: ContractTypeNode, modelsWithOutput?: Se
|
|
|
169
189
|
return needsParens ? `(${inner})[]` : `${inner}[]`;
|
|
170
190
|
}
|
|
171
191
|
case 'intersection':
|
|
172
|
-
return type.members.map(m => renderOutputTsType(m, modelsWithOutput)).join(' & ');
|
|
192
|
+
return type.members.map(m => renderOutputTsType(m, modelsWithOutput, target)).join(' & ');
|
|
173
193
|
case 'union':
|
|
174
|
-
return type.members.map(m => renderOutputTsType(m, modelsWithOutput)).join(' | ');
|
|
194
|
+
return type.members.map(m => renderOutputTsType(m, modelsWithOutput, target)).join(' | ');
|
|
175
195
|
case 'discriminatedUnion':
|
|
176
|
-
return type.members.map(m => renderOutputTsType(m, modelsWithOutput)).join(' | ');
|
|
196
|
+
return type.members.map(m => renderOutputTsType(m, modelsWithOutput, target)).join(' | ');
|
|
177
197
|
case 'inlineObject':
|
|
178
|
-
return `{ ${type.fields.map(f => `${quoteKey(f.name)}${f.optional ? '?' : ''}: ${renderOutputTsType(f.type, modelsWithOutput)}`).join('; ')} }`;
|
|
198
|
+
return `{ ${type.fields.map(f => `${quoteKey(f.name)}${f.optional ? '?' : ''}: ${renderOutputTsType(f.type, modelsWithOutput, target)}`).join('; ')} }`;
|
|
179
199
|
case 'lazy':
|
|
180
|
-
return renderOutputTsType(type.inner, modelsWithOutput);
|
|
200
|
+
return renderOutputTsType(type.inner, modelsWithOutput, target);
|
|
181
201
|
default:
|
|
182
|
-
return renderTsType(type);
|
|
202
|
+
return renderTsType(type, target);
|
|
183
203
|
}
|
|
184
204
|
}
|