@contractkit/plugin-typescript 0.17.3 → 0.17.4
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 +6 -6
- package/.turbo/turbo-test$colon$ci.log +13 -13
- package/CHANGELOG.md +6 -0
- package/coverage/clover.xml +235 -224
- package/coverage/coverage-final.json +4 -4
- package/coverage/index.html +18 -18
- package/coverage/src/codegen-contract.ts.html +81 -81
- package/coverage/src/codegen-operation.ts.html +1 -1
- package/coverage/src/codegen-plain-types.ts.html +197 -101
- package/coverage/src/codegen-sdk.ts.html +1 -1
- package/coverage/src/index.html +19 -19
- package/coverage/src/index.ts.html +1 -1
- package/coverage/src/path-utils.ts.html +1 -1
- package/coverage/src/ts-render.ts.html +11 -11
- package/coverage/tests/helpers.ts.html +7 -7
- package/coverage/tests/index.html +1 -1
- package/dist/codegen-plain-types.d.ts.map +1 -1
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/codegen-plain-types.ts +44 -12
- package/tests/codegen-plain-types.test.ts +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractkit/plugin-typescript",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.4",
|
|
4
4
|
"description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marooned Software",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"@contractkit/core": "0.13.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@repo/config-
|
|
33
|
-
"@repo/config-
|
|
32
|
+
"@repo/config-typescript": "0.1.0",
|
|
33
|
+
"@repo/config-eslint": "0.3.1"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration",
|
|
@@ -55,8 +55,10 @@ export function generatePlainTypes(root: ContractRootNode, context?: ContractCod
|
|
|
55
55
|
lines.push('');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const modelMap = new Map(root.models.map(m => [m.name, m]));
|
|
59
|
+
|
|
58
60
|
for (const model of topoSortModels(root.models)) {
|
|
59
|
-
lines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, allModelsWithOutput));
|
|
61
|
+
lines.push(...generateModel(model, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
|
|
60
62
|
lines.push('');
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -65,7 +67,13 @@ export function generatePlainTypes(root: ContractRootNode, context?: ContractCod
|
|
|
65
67
|
|
|
66
68
|
// ─── Model ─────────────────────────────────────────────────────────────────
|
|
67
69
|
|
|
68
|
-
function generateModel(
|
|
70
|
+
function generateModel(
|
|
71
|
+
model: ModelNode,
|
|
72
|
+
outPath?: string,
|
|
73
|
+
modelsWithInput?: Set<string>,
|
|
74
|
+
modelsWithOutput?: Set<string>,
|
|
75
|
+
modelMap?: Map<string, ModelNode>,
|
|
76
|
+
): string[] {
|
|
69
77
|
// Type alias: Name : typeExpression
|
|
70
78
|
if (model.type) {
|
|
71
79
|
return generateTypeAlias(model, outPath, modelsWithInput, modelsWithOutput);
|
|
@@ -75,7 +83,7 @@ function generateModel(model: ModelNode, outPath?: string, modelsWithInput?: Set
|
|
|
75
83
|
// transitively references models that have Input variants (captured in modelsWithInput).
|
|
76
84
|
const needsInputSplit = model.fields.some(f => f.visibility !== 'normal') || (modelsWithInput?.has(model.name) ?? false);
|
|
77
85
|
|
|
78
|
-
const lines = needsInputSplit ? generateVisibilityModel(model, outPath, modelsWithInput) : generateSimpleModel(model, outPath);
|
|
86
|
+
const lines = needsInputSplit ? generateVisibilityModel(model, outPath, modelsWithInput, modelMap) : generateSimpleModel(model, outPath, modelMap);
|
|
79
87
|
|
|
80
88
|
if (modelsWithOutput?.has(model.name)) {
|
|
81
89
|
lines.push('');
|
|
@@ -84,6 +92,30 @@ function generateModel(model: ModelNode, outPath?: string, modelsWithInput?: Set
|
|
|
84
92
|
return lines;
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
/** Recursively collect every field name defined on `bases` and their ancestors. Used to detect
|
|
96
|
+
* fields that the child re-declares without an explicit `override` keyword — those still need an
|
|
97
|
+
* `Omit<Base, …>` wrap, otherwise the child's narrower/incompatible declaration collides with the
|
|
98
|
+
* inherited one. */
|
|
99
|
+
function collectInheritedFieldNames(bases: string[], modelMap: Map<string, ModelNode>): Set<string> {
|
|
100
|
+
const result = new Set<string>();
|
|
101
|
+
const visit = (name: string): void => {
|
|
102
|
+
const m = modelMap.get(name);
|
|
103
|
+
if (!m || m.type) return;
|
|
104
|
+
for (const f of m.fields) result.add(f.name);
|
|
105
|
+
for (const b of m.bases ?? []) visit(b);
|
|
106
|
+
};
|
|
107
|
+
for (const b of bases) visit(b);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Names of fields the child declaration overrides — explicit `override` plus any field whose
|
|
112
|
+
* name shadows an inherited one. The latter catches single-base redeclarations that omit the
|
|
113
|
+
* `override` keyword (e.g. narrowing `kind: BusinessRoleKind` → `kind: 'employee'`). */
|
|
114
|
+
function computeOverrideNames(model: ModelNode, modelMap?: Map<string, ModelNode>): string[] {
|
|
115
|
+
const inherited = modelMap ? collectInheritedFieldNames(model.bases ?? [], modelMap) : new Set<string>();
|
|
116
|
+
return model.fields.filter(f => f.override || inherited.has(f.name)).map(f => f.name);
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
function generateComments(model: ModelNode, outPath?: string): string[] {
|
|
88
120
|
const lines: string[] = [];
|
|
89
121
|
lines.push('/**');
|
|
@@ -113,11 +145,11 @@ function generateTypeAlias(model: ModelNode, outPath?: string, modelsWithInput?:
|
|
|
113
145
|
return lines;
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
/** Build the `extends` clause for a
|
|
117
|
-
*
|
|
118
|
-
* subclass can legally redeclare
|
|
119
|
-
* tolerates omit keys that don't appear on the base,
|
|
120
|
-
*
|
|
148
|
+
/** Build the `extends` clause for a model.
|
|
149
|
+
* Each entry in `overrideNames` is wrapped in `Omit<Base, 'name1' | 'name2'>` per base so the
|
|
150
|
+
* subclass can legally redeclare those fields with new (possibly incompatible) types.
|
|
151
|
+
* TypeScript's `Omit<T, K extends keyof any>` tolerates omit keys that don't appear on the base,
|
|
152
|
+
* so we apply the same omit list to every base without per-base field-set lookup. */
|
|
121
153
|
function buildExtendsClause(bases: string[], overrideNames: string[], baseNameResolver: (b: string) => string): string {
|
|
122
154
|
if (bases.length === 0) return '';
|
|
123
155
|
if (overrideNames.length === 0) return ` extends ${bases.map(baseNameResolver).join(', ')}`;
|
|
@@ -126,12 +158,12 @@ function buildExtendsClause(bases: string[], overrideNames: string[], baseNameRe
|
|
|
126
158
|
return ` extends ${wrapped.join(', ')}`;
|
|
127
159
|
}
|
|
128
160
|
|
|
129
|
-
function generateSimpleModel(model: ModelNode, outPath?: string): string[] {
|
|
161
|
+
function generateSimpleModel(model: ModelNode, outPath?: string, modelMap?: Map<string, ModelNode>): string[] {
|
|
130
162
|
const lines: string[] = [];
|
|
131
163
|
lines.push(...generateComments(model, outPath));
|
|
132
164
|
|
|
133
165
|
const bases = model.bases ?? [];
|
|
134
|
-
const overrideNames = model
|
|
166
|
+
const overrideNames = computeOverrideNames(model, modelMap);
|
|
135
167
|
lines.push(`export interface ${model.name}${buildExtendsClause(bases, overrideNames, b => b)} {`);
|
|
136
168
|
|
|
137
169
|
for (const field of model.fields) {
|
|
@@ -142,12 +174,12 @@ function generateSimpleModel(model: ModelNode, outPath?: string): string[] {
|
|
|
142
174
|
return lines;
|
|
143
175
|
}
|
|
144
176
|
|
|
145
|
-
function generateVisibilityModel(model: ModelNode, outPath?: string, modelsWithInput?: Set<string>): string[] {
|
|
177
|
+
function generateVisibilityModel(model: ModelNode, outPath?: string, modelsWithInput?: Set<string>, modelMap?: Map<string, ModelNode>): string[] {
|
|
146
178
|
const lines: string[] = [];
|
|
147
179
|
lines.push(...generateComments(model, outPath));
|
|
148
180
|
|
|
149
181
|
const bases = model.bases ?? [];
|
|
150
|
-
const overrideNames = model
|
|
182
|
+
const overrideNames = computeOverrideNames(model, modelMap);
|
|
151
183
|
|
|
152
184
|
// Read type — omit writeonly fields
|
|
153
185
|
const readFields = model.fields.filter(f => f.visibility !== 'writeonly');
|
|
@@ -419,6 +419,44 @@ describe('generatePlainTypes', () => {
|
|
|
419
419
|
const output = generatePlainTypes(root);
|
|
420
420
|
expect(output).toContain("export interface Test5 extends Omit<A, 'a' | 'b'> {");
|
|
421
421
|
});
|
|
422
|
+
|
|
423
|
+
it('omits implicit single-base redeclarations even without explicit override', () => {
|
|
424
|
+
// BusinessRoleOfficer-style: child redeclares `kind` without `override` keyword,
|
|
425
|
+
// narrowing to an incompatible literal — must wrap base in Omit to compile.
|
|
426
|
+
const root = contractRoot([
|
|
427
|
+
model('Employee', [field('kind', literalType('employee')), field('title', scalarType('string'))]),
|
|
428
|
+
model('Officer', [field('kind', literalType('officer')), field('isControlPerson', scalarType('boolean'))], {
|
|
429
|
+
bases: ['Employee'],
|
|
430
|
+
}),
|
|
431
|
+
]);
|
|
432
|
+
const output = generatePlainTypes(root);
|
|
433
|
+
expect(output).toContain("export interface Officer extends Omit<Employee, 'kind'> {");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('applies implicit-override Omit to both Read and Input interfaces', () => {
|
|
437
|
+
const root = contractRoot([
|
|
438
|
+
model('Employee', [
|
|
439
|
+
field('id', scalarType('uuid'), { visibility: 'readonly' }),
|
|
440
|
+
field('kind', literalType('employee')),
|
|
441
|
+
]),
|
|
442
|
+
model('Officer', [field('kind', literalType('officer')), field('isControlPerson', scalarType('boolean'))], {
|
|
443
|
+
bases: ['Employee'],
|
|
444
|
+
}),
|
|
445
|
+
]);
|
|
446
|
+
const output = generatePlainTypes(root);
|
|
447
|
+
expect(output).toContain("export interface Officer extends Omit<Employee, 'kind'> {");
|
|
448
|
+
expect(output).toContain("export interface OfficerInput extends Omit<EmployeeInput, 'kind'> {");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('detects redeclaration through transitive base chain', () => {
|
|
452
|
+
const root = contractRoot([
|
|
453
|
+
model('Root', [field('kind', scalarType('string'))]),
|
|
454
|
+
model('Mid', [], { bases: ['Root'] }),
|
|
455
|
+
model('Leaf', [field('kind', literalType('leaf'))], { bases: ['Mid'] }),
|
|
456
|
+
]);
|
|
457
|
+
const output = generatePlainTypes(root);
|
|
458
|
+
expect(output).toContain("export interface Leaf extends Omit<Mid, 'kind'> {");
|
|
459
|
+
});
|
|
422
460
|
});
|
|
423
461
|
|
|
424
462
|
// ─── Type alias Input variants ────────────────────────────────
|