@contractkit/plugin-typescript 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$ci.log +7 -7
- package/.turbo/turbo-test$colon$ci.log +12 -12
- package/CHANGELOG.md +19 -0
- package/coverage/clover.xml +260 -167
- package/coverage/coverage-final.json +1 -1
- package/coverage/index.html +20 -20
- package/coverage/src/codegen-contract.ts.html +1 -1
- package/coverage/src/codegen-operation.ts.html +1 -1
- package/coverage/src/codegen-plain-types.ts.html +1 -1
- package/coverage/src/codegen-sdk.ts.html +1 -1
- package/coverage/src/index.html +20 -20
- package/coverage/src/index.ts.html +1052 -275
- package/coverage/src/path-utils.ts.html +1 -1
- package/coverage/src/ts-render.ts.html +1 -1
- package/coverage/tests/helpers.ts.html +1 -1
- package/coverage/tests/index.html +1 -1
- package/dist/index.d.ts +8 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +446 -149
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +481 -222
- package/tests/codegen-server.test.ts +3 -1
package/src/index.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { resolve, join, relative, dirname, basename } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync, rmSync, readdirSync, rmdirSync } from 'node:fs';
|
|
2
3
|
import { generateContract } from './codegen-contract.js';
|
|
3
4
|
import { generateOp } from './codegen-operation.js';
|
|
4
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ContractKitPlugin,
|
|
7
|
+
PluginContext,
|
|
8
|
+
ContractRootNode,
|
|
9
|
+
OpRootNode,
|
|
10
|
+
ModelNode,
|
|
11
|
+
IncrementalManifest,
|
|
12
|
+
IncrementalUnit,
|
|
13
|
+
IncrementalOutputFile,
|
|
14
|
+
} from '@contractkit/core';
|
|
15
|
+
import {
|
|
16
|
+
runIncrementalCodegen,
|
|
17
|
+
parseIncrementalManifest,
|
|
18
|
+
emptyIncrementalManifest,
|
|
19
|
+
hashFingerprint,
|
|
20
|
+
collectTransitiveModelRefs,
|
|
21
|
+
collectTypeRefs,
|
|
22
|
+
} from '@contractkit/core';
|
|
5
23
|
import {
|
|
6
24
|
generateSdk,
|
|
7
25
|
generateSdkOptions,
|
|
@@ -33,149 +51,313 @@ import {
|
|
|
33
51
|
export interface ServerConfig {
|
|
34
52
|
/** Directory (relative to rootDir) where server files are written. Default: rootDir. */
|
|
35
53
|
baseDir?: string;
|
|
36
|
-
/**
|
|
37
|
-
* When true, `output.types` emits Zod schema files (via `generateContract`).
|
|
38
|
-
* When false/omitted, `output.types` emits plain TypeScript interfaces.
|
|
39
|
-
*/
|
|
54
|
+
/** When true, `output.types` emits Zod schema files (via `generateContract`). When false/omitted, emits plain TypeScript. */
|
|
40
55
|
zod?: boolean;
|
|
41
56
|
output?: {
|
|
42
|
-
/** Path template for Koa router files. Supports {filename}, {dir}, {area}.
|
|
57
|
+
/** Path template for Koa router files. Supports {filename}, {dir}, {area}. */
|
|
43
58
|
routes?: string;
|
|
44
|
-
/**
|
|
45
|
-
* Path template for type/schema files. Supports {filename}, {dir}, {area}.
|
|
46
|
-
* Generates Zod schemas when `zod: true`, otherwise plain TypeScript interfaces.
|
|
47
|
-
*/
|
|
59
|
+
/** Path template for type/schema files. Supports {filename}, {dir}, {area}. */
|
|
48
60
|
types?: string;
|
|
49
61
|
};
|
|
50
|
-
/** Import path template for service implementations.
|
|
62
|
+
/** Import path template for service implementations. */
|
|
51
63
|
servicePathTemplate?: string;
|
|
52
|
-
/**
|
|
53
|
-
* Whether to emit handlers for operations marked `internal`. Defaults to `true` —
|
|
54
|
-
* the server still needs routes for internal endpoints. Set to `false` to omit them.
|
|
55
|
-
*/
|
|
64
|
+
/** Whether to emit handlers for `internal` operations. Default true. */
|
|
56
65
|
includeInternal?: boolean;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
export interface SdkConfig {
|
|
60
|
-
/** Directory (relative to rootDir) where SDK files are written. Default: rootDir. */
|
|
61
69
|
baseDir?: string;
|
|
62
|
-
/** Name used for the aggregator SDK class (e.g. "homegrown" → `HomegrownSdk`). */
|
|
63
70
|
name?: string;
|
|
64
|
-
/**
|
|
65
|
-
* When true, `output.types` emits Zod schema files (via `generateContract`).
|
|
66
|
-
* When false/omitted, `output.types` emits plain TypeScript interfaces.
|
|
67
|
-
*/
|
|
68
71
|
zod?: boolean;
|
|
69
72
|
output?: {
|
|
70
|
-
/** Path template for the SDK aggregator file. Supports {name}. Default: `sdk.ts`. */
|
|
71
73
|
sdk?: string;
|
|
72
|
-
/** Path template for SDK type files. Supports {filename}, {dir}, {area}, {subarea}. */
|
|
73
74
|
types?: string;
|
|
74
|
-
/** Path template for client class files. Supports {filename}, {dir}, {area}, {subarea}. */
|
|
75
75
|
clients?: string;
|
|
76
76
|
};
|
|
77
|
-
/**
|
|
78
|
-
* Whether to emit SDK methods for operations marked `internal`. Defaults to `false` —
|
|
79
|
-
* internal ops are omitted from the SDK so consumers don't pick them up. Set to `true`
|
|
80
|
-
* for an internal-use SDK that should expose them.
|
|
81
|
-
*/
|
|
82
77
|
includeInternal?: boolean;
|
|
83
78
|
}
|
|
84
79
|
|
|
85
80
|
export interface ZodConfig {
|
|
86
|
-
/** Directory (relative to rootDir) where Zod schema files are written. Default: rootDir. */
|
|
87
81
|
baseDir?: string;
|
|
88
|
-
/** Output path template. Supports {filename}, {dir}. Default: `{filename}.schema.ts` alongside source. */
|
|
89
82
|
output?: string;
|
|
90
83
|
}
|
|
91
84
|
|
|
92
85
|
export interface TypesConfig {
|
|
93
|
-
/** Directory (relative to rootDir) where plain TypeScript type files are written. Default: rootDir. */
|
|
94
86
|
baseDir?: string;
|
|
95
|
-
/** Output path template. Supports {filename}, {dir}. Default: `{filename}.types.ts` alongside source. */
|
|
96
87
|
output?: string;
|
|
97
88
|
}
|
|
98
89
|
|
|
99
90
|
export interface TypescriptPluginConfig {
|
|
100
|
-
/** Generate Koa router files from `operation` declarations. */
|
|
101
91
|
server?: ServerConfig;
|
|
102
|
-
/** Generate TypeScript SDK client files from `operation` declarations. */
|
|
103
92
|
sdk?: SdkConfig;
|
|
104
|
-
/** Generate Zod schema files from `contract` declarations. */
|
|
105
93
|
zod?: ZodConfig;
|
|
106
|
-
/** Generate plain TypeScript interface/type files from `contract` declarations (no Zod runtime). */
|
|
107
94
|
types?: TypesConfig;
|
|
108
95
|
}
|
|
109
96
|
|
|
110
|
-
// ───
|
|
97
|
+
// ─── Caching constants ─────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** Bumped when the codegen output shape changes in a way that should bust every per-file fingerprint. */
|
|
100
|
+
export const TYPESCRIPT_CODEGEN_VERSION = '1';
|
|
101
|
+
|
|
102
|
+
const MANIFEST_FILENAME = '.contractkit-typescript-manifest.json';
|
|
103
|
+
|
|
104
|
+
// ─── Plugin entry points ──────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const plugin: ContractKitPlugin = {
|
|
107
|
+
name: 'typescript',
|
|
108
|
+
async generateTargets(inputs, ctx) {
|
|
109
|
+
const config = ctx.options as TypescriptPluginConfig;
|
|
110
|
+
await runTypescriptCodegen(inputs, ctx, config, ctx.rootDir);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default plugin;
|
|
115
|
+
|
|
116
|
+
/** Build a `@contractkit/plugin-typescript` instance with explicit configuration, for programmatic use. */
|
|
117
|
+
export function createTypescriptPlugin(config: TypescriptPluginConfig, rootDir: string): ContractKitPlugin {
|
|
118
|
+
return {
|
|
119
|
+
name: 'typescript',
|
|
120
|
+
async generateTargets(inputs, ctx) {
|
|
121
|
+
await runTypescriptCodegen(inputs, ctx, config, rootDir);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Shared orchestration. Each sub-generator (server / sdk / zod / types) contributes a
|
|
128
|
+
* set of cacheable units (per-file fingerprints) plus a set of always-regenerated global
|
|
129
|
+
* files (aggregators, barrels, sdk-options). Units share a single manifest so the cache
|
|
130
|
+
* survives cross-cutting reads — the manifest lives at `<rootDir>/.contractkit-typescript-manifest.json`.
|
|
131
|
+
*
|
|
132
|
+
* Honors `ctx.cacheEnabled` — `--force` bypasses the manifest entirely.
|
|
133
|
+
*/
|
|
134
|
+
async function runTypescriptCodegen(
|
|
135
|
+
inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0],
|
|
136
|
+
ctx: PluginContext,
|
|
137
|
+
config: TypescriptPluginConfig,
|
|
138
|
+
rootDir: string,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const manifestPath = resolve(rootDir, MANIFEST_FILENAME);
|
|
141
|
+
const prevManifest: IncrementalManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
|
|
142
|
+
|
|
143
|
+
const units: IncrementalUnit[] = [];
|
|
144
|
+
const globalFiles: IncrementalOutputFile[] = [];
|
|
145
|
+
|
|
146
|
+
if (config.server) collectServerOutput(config.server, rootDir, inputs, units);
|
|
147
|
+
if (config.sdk) collectSdkOutput(config.sdk, rootDir, inputs, units, globalFiles);
|
|
148
|
+
if (config.zod) collectZodOutput(config.zod, rootDir, inputs, units);
|
|
149
|
+
if (config.types) collectTypesOutput(config.types, rootDir, inputs, units);
|
|
150
|
+
|
|
151
|
+
const result = runIncrementalCodegen({
|
|
152
|
+
codegenVersion: TYPESCRIPT_CODEGEN_VERSION,
|
|
153
|
+
manifestFilename: manifestPath,
|
|
154
|
+
prevManifest,
|
|
155
|
+
globalFiles,
|
|
156
|
+
units,
|
|
157
|
+
// Paths are absolute, so existsSync works directly.
|
|
158
|
+
fileExists: existsSync,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
deleteStalePaths(result.deletedPaths);
|
|
162
|
+
|
|
163
|
+
for (const { relativePath, content } of result.filesToWrite) {
|
|
164
|
+
ctx.emitFile(relativePath, content);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Cross-file dependency analysis ────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Build a quick lookup from model name → its definition. */
|
|
171
|
+
function buildModelMap(contractRoots: readonly ContractRootNode[]): Map<string, ModelNode> {
|
|
172
|
+
const map = new Map<string, ModelNode>();
|
|
173
|
+
for (const root of contractRoots) {
|
|
174
|
+
for (const model of root.models) map.set(model.name, model);
|
|
175
|
+
}
|
|
176
|
+
return map;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Collect every model referenced by this contract root (own models' fields + bases). Used to slice cross-file fingerprint inputs to just what this file actually depends on. */
|
|
180
|
+
function collectContractRootRefs(root: ContractRootNode, modelMap: Map<string, ModelNode>): Set<string> {
|
|
181
|
+
const seeds: Parameters<typeof collectTypeRefs>[0][] = [];
|
|
182
|
+
for (const m of root.models) {
|
|
183
|
+
if (m.type) seeds.push(m.type);
|
|
184
|
+
for (const f of m.fields) seeds.push(f.type);
|
|
185
|
+
if (m.bases) {
|
|
186
|
+
for (const b of m.bases) seeds.push({ kind: 'ref', name: b } as Parameters<typeof collectTypeRefs>[0]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return collectTransitiveModelRefs(seeds, modelMap);
|
|
190
|
+
}
|
|
111
191
|
|
|
112
|
-
|
|
192
|
+
/** Collect every model referenced by an op root's routes/operations (transitive). */
|
|
193
|
+
function collectOpRootRefs(root: OpRootNode, modelMap: Map<string, ModelNode>): Set<string> {
|
|
194
|
+
const seeds: Parameters<typeof collectTypeRefs>[0][] = [];
|
|
195
|
+
for (const route of root.routes) {
|
|
196
|
+
if (route.params) seeds.push(...paramSourceTypes(route.params));
|
|
197
|
+
for (const op of route.operations) {
|
|
198
|
+
if (op.query) seeds.push(...paramSourceTypes(op.query));
|
|
199
|
+
if (op.headers) seeds.push(...paramSourceTypes(op.headers));
|
|
200
|
+
if (op.request) {
|
|
201
|
+
for (const body of op.request.bodies) seeds.push(body.bodyType);
|
|
202
|
+
}
|
|
203
|
+
for (const resp of op.responses) {
|
|
204
|
+
if (resp.bodyType) seeds.push(resp.bodyType);
|
|
205
|
+
if (resp.headers) {
|
|
206
|
+
for (const h of resp.headers) seeds.push(h.type);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return collectTransitiveModelRefs(seeds, modelMap);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function paramSourceTypes(src: NonNullable<OpRootNode['routes'][number]['params']>): Parameters<typeof collectTypeRefs>[0][] {
|
|
215
|
+
const out: Parameters<typeof collectTypeRefs>[0][] = [];
|
|
216
|
+
if (src.kind === 'params') {
|
|
217
|
+
for (const n of src.nodes) out.push(n.type);
|
|
218
|
+
} else if (src.kind === 'ref') {
|
|
219
|
+
out.push({ kind: 'ref', name: src.name } as Parameters<typeof collectTypeRefs>[0]);
|
|
220
|
+
} else if (src.kind === 'type') {
|
|
221
|
+
out.push(src.node);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Build a sorted, JSON-stable record of (modelName -> outPath) for refs this unit depends on. */
|
|
227
|
+
function sliceOutPathMap(refs: Set<string>, modelOutPaths: Map<string, string>, modelsWithInput: Set<string>, modelsWithOutput: Set<string>): Record<string, string> {
|
|
228
|
+
const slice: Record<string, string> = {};
|
|
229
|
+
for (const ref of [...refs].sort()) {
|
|
230
|
+
const p = modelOutPaths.get(ref);
|
|
231
|
+
if (p) slice[ref] = p;
|
|
232
|
+
if (modelsWithInput.has(ref)) {
|
|
233
|
+
const ip = modelOutPaths.get(`${ref}Input`);
|
|
234
|
+
if (ip) slice[`${ref}Input`] = ip;
|
|
235
|
+
}
|
|
236
|
+
if (modelsWithOutput.has(ref)) {
|
|
237
|
+
const op = modelOutPaths.get(`${ref}Output`);
|
|
238
|
+
if (op) slice[`${ref}Output`] = op;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return slice;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Slice modelsWithInput/Output to only the names relevant to this unit. */
|
|
245
|
+
function sliceModelSet(refs: Set<string>, ownNames: Set<string>, set: Set<string>): string[] {
|
|
246
|
+
const result: string[] = [];
|
|
247
|
+
for (const name of set) {
|
|
248
|
+
if (refs.has(name) || ownNames.has(name)) result.push(name);
|
|
249
|
+
}
|
|
250
|
+
return result.sort();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Server sub-generator ──────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function collectServerOutput(
|
|
113
256
|
config: ServerConfig,
|
|
114
257
|
rootDir: string,
|
|
115
258
|
inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0],
|
|
116
|
-
|
|
259
|
+
units: IncrementalUnit[],
|
|
117
260
|
): void {
|
|
118
261
|
const serverBase = resolve(rootDir, config.baseDir ?? '.');
|
|
119
262
|
const modelsWithInput = inputs.modelsWithInput as Set<string>;
|
|
120
263
|
const modelsWithOutput = inputs.modelsWithOutput as Set<string>;
|
|
264
|
+
const modelMap = buildModelMap(inputs.contractRoots);
|
|
121
265
|
const allFiles = [...inputs.contractRoots.map(r => r.file), ...inputs.opRoots.map(r => r.file)];
|
|
122
266
|
const commonRoot = commonDir(allFiles, rootDir);
|
|
267
|
+
const subConfigKey = stableSubConfig(config);
|
|
123
268
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
269
|
+
// Pre-pass: register all model → outPath. Cross-file refs need to resolve correctly,
|
|
270
|
+
// which means we need the COMPLETE map (not a slice) — even though each unit's fingerprint
|
|
271
|
+
// only includes its own slice.
|
|
272
|
+
const serverModelOutPaths = new Map<string, string>();
|
|
273
|
+
const typeEntries: { ast: ContractRootNode; typeOutPath: string }[] = [];
|
|
129
274
|
if (config.output?.types) {
|
|
130
|
-
serverModelOutPaths = new Map();
|
|
131
|
-
|
|
132
|
-
// Pass 1: register all model → outPath entries before generating content,
|
|
133
|
-
// so cross-file type refs resolve correctly.
|
|
134
|
-
const typeEntries: { ast: (typeof inputs.contractRoots)[number]; typeOutPath: string }[] = [];
|
|
135
275
|
for (const ast of inputs.contractRoots) {
|
|
136
276
|
const typeOutPath = computeContractOutPath(ast.file, serverBase, config.output.types, '.ts', commonRoot, ast.meta);
|
|
137
277
|
typeEntries.push({ ast, typeOutPath });
|
|
138
278
|
for (const model of ast.models) {
|
|
139
279
|
serverModelOutPaths.set(model.name, typeOutPath);
|
|
140
|
-
if (modelsWithInput.has(model.name)) {
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
if (modelsWithOutput.has(model.name)) {
|
|
144
|
-
serverModelOutPaths.set(`${model.name}Output`, typeOutPath);
|
|
145
|
-
}
|
|
280
|
+
if (modelsWithInput.has(model.name)) serverModelOutPaths.set(`${model.name}Input`, typeOutPath);
|
|
281
|
+
if (modelsWithOutput.has(model.name)) serverModelOutPaths.set(`${model.name}Output`, typeOutPath);
|
|
146
282
|
}
|
|
147
283
|
}
|
|
284
|
+
}
|
|
148
285
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
286
|
+
// ── Per-contract-root types unit ──
|
|
287
|
+
for (const { ast, typeOutPath } of typeEntries) {
|
|
288
|
+
const refs = collectContractRootRefs(ast, modelMap);
|
|
289
|
+
const ownNames = new Set(ast.models.map(m => m.name));
|
|
290
|
+
const fingerprint = hashFingerprint({
|
|
291
|
+
kind: 'server-types',
|
|
292
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
293
|
+
outPath: typeOutPath,
|
|
294
|
+
root: ast,
|
|
295
|
+
outPathSlice: sliceOutPathMap(refs, serverModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
296
|
+
modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
|
|
297
|
+
modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput),
|
|
298
|
+
sub: subConfigKey,
|
|
299
|
+
});
|
|
300
|
+
units.push({
|
|
301
|
+
key: `server-types::${typeOutPath}`,
|
|
302
|
+
fingerprint,
|
|
303
|
+
render: () => {
|
|
304
|
+
const renderCtx = {
|
|
305
|
+
modelOutPaths: serverModelOutPaths,
|
|
306
|
+
currentOutPath: typeOutPath,
|
|
307
|
+
modelsWithInput,
|
|
308
|
+
modelsWithOutput,
|
|
309
|
+
};
|
|
310
|
+
const content = config.zod ? generateContract(ast, renderCtx) : generatePlainTypes(ast, renderCtx);
|
|
311
|
+
return [{ relativePath: typeOutPath, content }];
|
|
312
|
+
},
|
|
313
|
+
});
|
|
155
314
|
}
|
|
156
315
|
|
|
157
|
-
// ──
|
|
316
|
+
// ── Per-op-root router unit ──
|
|
158
317
|
for (const ast of inputs.opRoots) {
|
|
159
318
|
const outPath = computeOpOutPath(ast.file, serverBase, config.output?.routes, '.router.ts', commonRoot, ast.meta);
|
|
160
|
-
const
|
|
161
|
-
|
|
319
|
+
const refs = collectOpRootRefs(ast, modelMap);
|
|
320
|
+
const fingerprint = hashFingerprint({
|
|
321
|
+
kind: 'server-router',
|
|
322
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
162
323
|
outPath,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
modelsWithOutput,
|
|
166
|
-
|
|
324
|
+
root: ast,
|
|
325
|
+
// The router imports types from each contract root's type file; the slice covers exactly that.
|
|
326
|
+
outPathSlice: sliceOutPathMap(refs, serverModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
327
|
+
modelsWithInput: sliceModelSet(refs, new Set(), modelsWithInput),
|
|
328
|
+
modelsWithOutput: sliceModelSet(refs, new Set(), modelsWithOutput),
|
|
329
|
+
servicePathTemplate: config.servicePathTemplate ?? null,
|
|
330
|
+
includeInternal: config.includeInternal ?? true,
|
|
331
|
+
sub: subConfigKey,
|
|
332
|
+
});
|
|
333
|
+
units.push({
|
|
334
|
+
key: `server-router::${outPath}`,
|
|
335
|
+
fingerprint,
|
|
336
|
+
render: () => [
|
|
337
|
+
{
|
|
338
|
+
relativePath: outPath,
|
|
339
|
+
content: generateOp(ast, {
|
|
340
|
+
servicePathTemplate: config.servicePathTemplate,
|
|
341
|
+
outPath,
|
|
342
|
+
modelOutPaths: serverModelOutPaths,
|
|
343
|
+
modelsWithInput,
|
|
344
|
+
modelsWithOutput,
|
|
345
|
+
includeInternal: config.includeInternal,
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
],
|
|
167
349
|
});
|
|
168
|
-
emitFile(outPath, content);
|
|
169
350
|
}
|
|
170
351
|
}
|
|
171
352
|
|
|
172
|
-
// ─── SDK
|
|
353
|
+
// ─── SDK sub-generator ─────────────────────────────────────────────────────
|
|
173
354
|
|
|
174
|
-
function
|
|
355
|
+
function collectSdkOutput(
|
|
175
356
|
config: SdkConfig,
|
|
176
357
|
rootDir: string,
|
|
177
358
|
inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0],
|
|
178
|
-
|
|
359
|
+
units: IncrementalUnit[],
|
|
360
|
+
globalFiles: IncrementalOutputFile[],
|
|
179
361
|
): void {
|
|
180
362
|
const sdkBase = config.baseDir ? resolve(rootDir, config.baseDir) : rootDir;
|
|
181
363
|
const sdkName = config.name;
|
|
@@ -184,22 +366,22 @@ function runSdkGeneration(
|
|
|
184
366
|
? join(sdkBase, TEMPLATE_VAR_RE.test(sdkOutput) ? resolveTemplate(sdkOutput, { name: sdkName ?? 'sdk' }) : sdkOutput)
|
|
185
367
|
: join(sdkBase, 'sdk.ts');
|
|
186
368
|
const sdkOptionsPath = join(dirname(sdkEntryPath), 'sdk-options.ts');
|
|
369
|
+
const subConfigKey = stableSubConfig(config);
|
|
187
370
|
|
|
188
371
|
const modelsWithInput = inputs.modelsWithInput as Set<string>;
|
|
189
372
|
const modelsWithOutput = inputs.modelsWithOutput as Set<string>;
|
|
373
|
+
const modelMap = buildModelMap(inputs.contractRoots);
|
|
190
374
|
const allFiles = [...inputs.contractRoots.map(r => r.file), ...inputs.opRoots.map(r => r.file)];
|
|
191
375
|
const ckCommonRoot = commonDir(allFiles, rootDir);
|
|
192
376
|
|
|
193
|
-
|
|
377
|
+
const sdkModelOutPaths = new Map<string, string>();
|
|
194
378
|
const sdkTypePaths: string[] = [];
|
|
195
379
|
const sdkClientInfos: { outPath: string; className: string; propertyName: string }[] = [];
|
|
196
380
|
|
|
197
|
-
// ── SDK
|
|
381
|
+
// ── Pre-pass: SDK type files ──
|
|
382
|
+
const sdkContractEntries: { ast: ContractRootNode; typeOutPath: string }[] = [];
|
|
198
383
|
if (config.output?.types) {
|
|
199
|
-
sdkModelOutPaths = new Map<string, string>();
|
|
200
384
|
const publicTypes = computePubliclyReachableTypes(inputs.opRoots, inputs.contractRoots, modelsWithInput, modelsWithOutput);
|
|
201
|
-
|
|
202
|
-
const sdkContractEntries: { ast: (typeof inputs.contractRoots)[number]; typeOutPath: string }[] = [];
|
|
203
385
|
for (const ast of inputs.contractRoots) {
|
|
204
386
|
const typeOutPath = computeSdkTypeOutPath(ast.file, sdkBase, config.output.types, ckCommonRoot, ast.meta);
|
|
205
387
|
if (!typeOutPath) continue;
|
|
@@ -212,48 +394,63 @@ function runSdkGeneration(
|
|
|
212
394
|
if (modelsWithOutput.has(model.name)) sdkModelOutPaths.set(`${model.name}Output`, typeOutPath);
|
|
213
395
|
}
|
|
214
396
|
}
|
|
397
|
+
}
|
|
215
398
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
399
|
+
// ── SDK type units ──
|
|
400
|
+
for (const { ast, typeOutPath } of sdkContractEntries) {
|
|
401
|
+
const refs = collectContractRootRefs(ast, modelMap);
|
|
402
|
+
const ownNames = new Set(ast.models.map(m => m.name));
|
|
403
|
+
const fingerprint = hashFingerprint({
|
|
404
|
+
kind: 'sdk-types',
|
|
405
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
406
|
+
outPath: typeOutPath,
|
|
407
|
+
root: ast,
|
|
408
|
+
outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
409
|
+
modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
|
|
410
|
+
modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput),
|
|
411
|
+
sdkOptionsPath,
|
|
412
|
+
sub: subConfigKey,
|
|
413
|
+
});
|
|
414
|
+
units.push({
|
|
415
|
+
key: `sdk-types::${typeOutPath}`,
|
|
416
|
+
fingerprint,
|
|
417
|
+
render: () => {
|
|
418
|
+
let content: string;
|
|
419
|
+
if (config.zod) {
|
|
420
|
+
content = generateContract(ast, {
|
|
421
|
+
modelOutPaths: sdkModelOutPaths,
|
|
422
|
+
currentOutPath: typeOutPath,
|
|
423
|
+
modelsWithInput,
|
|
424
|
+
modelsWithOutput,
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
let rel = relative(dirname(typeOutPath), sdkOptionsPath).replace(/\.ts$/, '.js');
|
|
428
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
429
|
+
content = generatePlainTypes(ast, {
|
|
430
|
+
modelOutPaths: sdkModelOutPaths,
|
|
431
|
+
currentOutPath: typeOutPath,
|
|
432
|
+
modelsWithInput,
|
|
433
|
+
modelsWithOutput,
|
|
434
|
+
jsonValueImportPath: rel,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return [{ relativePath: typeOutPath, content }];
|
|
438
|
+
},
|
|
439
|
+
});
|
|
238
440
|
}
|
|
239
441
|
|
|
240
|
-
// ──
|
|
241
|
-
// Group opRoots by (area, subarea):
|
|
242
|
-
// - area + subarea → leaf client emitted as <Area><Subarea>Client in its own file
|
|
243
|
-
// - area only → no standalone file; methods inlined into <Area>Client in sdk.ts
|
|
244
|
-
// - neither → flat top-level client (legacy behavior)
|
|
442
|
+
// ── Bucket op roots by area/subarea ──
|
|
245
443
|
interface AreaBucket {
|
|
246
|
-
leaves: { ast:
|
|
247
|
-
inlineRoots:
|
|
444
|
+
leaves: { ast: OpRootNode; outPath: string; subarea: string }[];
|
|
445
|
+
inlineRoots: OpRootNode[];
|
|
248
446
|
}
|
|
249
447
|
const areaBuckets = new Map<string, AreaBucket>();
|
|
250
|
-
const topLevelEntries: { ast:
|
|
448
|
+
const topLevelEntries: { ast: OpRootNode; outPath: string }[] = [];
|
|
251
449
|
|
|
252
450
|
if (config.output?.clients) {
|
|
253
451
|
for (const ast of inputs.opRoots) {
|
|
254
452
|
const sdkOutPath = computeSdkOutPath(ast.file, sdkBase, config.output.clients, ckCommonRoot, ast.meta);
|
|
255
453
|
if (!sdkOutPath || !hasPublicOperations(ast, config.includeInternal)) continue;
|
|
256
|
-
|
|
257
454
|
const { area, subarea } = getAreaSubarea(ast);
|
|
258
455
|
if (area && subarea) {
|
|
259
456
|
const bucket = areaBuckets.get(area) ?? { leaves: [], inlineRoots: [] };
|
|
@@ -268,46 +465,91 @@ function runSdkGeneration(
|
|
|
268
465
|
}
|
|
269
466
|
}
|
|
270
467
|
|
|
271
|
-
//
|
|
272
|
-
for (const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
468
|
+
// ── Per-leaf-client (area+subarea) units ──
|
|
469
|
+
for (const [area, bucket] of areaBuckets.entries()) {
|
|
470
|
+
for (const leaf of bucket.leaves) {
|
|
471
|
+
const className = deriveSubareaClientClassName(area, leaf.subarea);
|
|
472
|
+
sdkClientInfos.push({ outPath: leaf.outPath, className, propertyName: deriveSubareaPropertyName(leaf.subarea) });
|
|
473
|
+
const refs = collectOpRootRefs(leaf.ast, modelMap);
|
|
474
|
+
const fingerprint = hashFingerprint({
|
|
475
|
+
kind: 'sdk-leaf-client',
|
|
476
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
477
|
+
outPath: leaf.outPath,
|
|
478
|
+
root: leaf.ast,
|
|
479
|
+
outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
480
|
+
modelsWithInput: sliceModelSet(refs, new Set(), modelsWithInput),
|
|
481
|
+
modelsWithOutput: sliceModelSet(refs, new Set(), modelsWithOutput),
|
|
281
482
|
sdkOptionsPath,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
483
|
+
className,
|
|
484
|
+
includeInternal: config.includeInternal ?? false,
|
|
485
|
+
sub: subConfigKey,
|
|
486
|
+
});
|
|
487
|
+
units.push({
|
|
488
|
+
key: `sdk-leaf-client::${leaf.outPath}`,
|
|
489
|
+
fingerprint,
|
|
490
|
+
render: () => [
|
|
491
|
+
{
|
|
492
|
+
relativePath: leaf.outPath,
|
|
493
|
+
content: generateSdk(leaf.ast, {
|
|
494
|
+
typeImportPathTemplate: undefined,
|
|
495
|
+
outPath: leaf.outPath,
|
|
496
|
+
modelOutPaths: sdkModelOutPaths,
|
|
497
|
+
sdkOptionsPath,
|
|
498
|
+
modelsWithInput,
|
|
499
|
+
modelsWithOutput,
|
|
500
|
+
includeInternal: config.includeInternal,
|
|
501
|
+
clientClassName: className,
|
|
502
|
+
}),
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
});
|
|
506
|
+
}
|
|
288
507
|
}
|
|
508
|
+
|
|
509
|
+
// ── Top-level (no area) client units ──
|
|
289
510
|
for (const { ast, outPath } of topLevelEntries) {
|
|
290
511
|
const className = deriveClientClassName(ast.file);
|
|
291
512
|
sdkClientInfos.push({ outPath, className, propertyName: deriveClientPropertyName(ast.file) });
|
|
292
|
-
|
|
513
|
+
const refs = collectOpRootRefs(ast, modelMap);
|
|
514
|
+
const fingerprint = hashFingerprint({
|
|
515
|
+
kind: 'sdk-top-client',
|
|
516
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
293
517
|
outPath,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
518
|
+
root: ast,
|
|
519
|
+
outPathSlice: sliceOutPathMap(refs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
520
|
+
modelsWithInput: sliceModelSet(refs, new Set(), modelsWithInput),
|
|
521
|
+
modelsWithOutput: sliceModelSet(refs, new Set(), modelsWithOutput),
|
|
522
|
+
sdkOptionsPath,
|
|
523
|
+
includeInternal: config.includeInternal ?? false,
|
|
524
|
+
sub: subConfigKey,
|
|
525
|
+
});
|
|
526
|
+
units.push({
|
|
527
|
+
key: `sdk-top-client::${outPath}`,
|
|
528
|
+
fingerprint,
|
|
529
|
+
render: () => [
|
|
530
|
+
{
|
|
531
|
+
relativePath: outPath,
|
|
532
|
+
content: generateSdk(ast, {
|
|
533
|
+
typeImportPathTemplate: undefined,
|
|
534
|
+
outPath,
|
|
535
|
+
modelOutPaths: sdkModelOutPaths,
|
|
536
|
+
sdkOptionsPath,
|
|
537
|
+
modelsWithInput,
|
|
538
|
+
modelsWithOutput,
|
|
539
|
+
includeInternal: config.includeInternal,
|
|
540
|
+
}),
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
});
|
|
304
544
|
}
|
|
305
545
|
}
|
|
306
546
|
|
|
307
|
-
// ── sdk-options
|
|
308
|
-
|
|
547
|
+
// ── Global files: sdk-options, aggregator, barrels, root index ──
|
|
548
|
+
// The aggregator inlines area-only op roots, so its content depends on each inline root's
|
|
549
|
+
// full AST. Caching it gains little — the codegen is fast and any inline-root change rebuilds
|
|
550
|
+
// the file anyway. Same for barrels (one line per imported file). Always regenerate.
|
|
551
|
+
globalFiles.push({ relativePath: sdkOptionsPath, content: generateSdkOptions() });
|
|
309
552
|
|
|
310
|
-
// ── sdk.ts aggregator ──
|
|
311
553
|
const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0;
|
|
312
554
|
if (hasAnything) {
|
|
313
555
|
const sdkEntryDir = dirname(sdkEntryPath);
|
|
@@ -363,26 +605,18 @@ function runSdkGeneration(
|
|
|
363
605
|
})),
|
|
364
606
|
}));
|
|
365
607
|
|
|
366
|
-
|
|
367
|
-
sdkEntryPath,
|
|
368
|
-
generateSdkAggregator({
|
|
369
|
-
|
|
370
|
-
areas,
|
|
371
|
-
sdkOptionsImportPath,
|
|
372
|
-
sdkClassName,
|
|
373
|
-
}),
|
|
374
|
-
);
|
|
608
|
+
globalFiles.push({
|
|
609
|
+
relativePath: sdkEntryPath,
|
|
610
|
+
content: generateSdkAggregator({ topLevelClients, areas, sdkOptionsImportPath, sdkClassName }),
|
|
611
|
+
});
|
|
375
612
|
}
|
|
376
613
|
|
|
377
|
-
// ── Barrel files ──
|
|
378
614
|
const sdkSrcDir = dirname(sdkEntryPath);
|
|
379
615
|
const sdkTypeBarrels = generateBarrelFiles(sdkTypePaths);
|
|
380
|
-
for (const barrel of sdkTypeBarrels)
|
|
616
|
+
for (const barrel of sdkTypeBarrels) globalFiles.push({ relativePath: barrel.outPath, content: barrel.content });
|
|
381
617
|
|
|
382
618
|
const rootExports: string[] = [`export * from './${basename(sdkOptionsPath).replace(/\.ts$/, '.js')}';`];
|
|
383
|
-
if (hasAnything) {
|
|
384
|
-
rootExports.push(`export * from './${basename(sdkEntryPath).replace(/\.ts$/, '.js')}';`);
|
|
385
|
-
}
|
|
619
|
+
if (hasAnything) rootExports.push(`export * from './${basename(sdkEntryPath).replace(/\.ts$/, '.js')}';`);
|
|
386
620
|
for (const c of sdkClientInfos) {
|
|
387
621
|
let rel = relative(sdkSrcDir, c.outPath).replace(/\.ts$/, '.js');
|
|
388
622
|
if (!rel.startsWith('.')) rel = './' + rel;
|
|
@@ -393,26 +627,30 @@ function runSdkGeneration(
|
|
|
393
627
|
if (!rel.startsWith('.')) rel = './' + rel;
|
|
394
628
|
rootExports.push(`export * from '${rel}';`);
|
|
395
629
|
}
|
|
396
|
-
|
|
630
|
+
globalFiles.push({
|
|
631
|
+
relativePath: join(sdkSrcDir, 'index.ts'),
|
|
632
|
+
content: `// Auto-generated barrel file\n${rootExports.sort().join('\n')}\n`,
|
|
633
|
+
});
|
|
397
634
|
}
|
|
398
635
|
|
|
399
|
-
// ─── Zod
|
|
636
|
+
// ─── Zod sub-generator ─────────────────────────────────────────────────────
|
|
400
637
|
|
|
401
|
-
function
|
|
638
|
+
function collectZodOutput(
|
|
402
639
|
config: ZodConfig,
|
|
403
640
|
rootDir: string,
|
|
404
641
|
inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0],
|
|
405
|
-
|
|
642
|
+
units: IncrementalUnit[],
|
|
406
643
|
): void {
|
|
407
644
|
const zodBase = resolve(rootDir, config.baseDir ?? '.');
|
|
408
645
|
const allFiles = [...inputs.contractRoots.map(r => r.file), ...inputs.opRoots.map(r => r.file)];
|
|
409
646
|
const commonRoot = commonDir(allFiles, rootDir);
|
|
410
647
|
const modelsWithInput = inputs.modelsWithInput as Set<string>;
|
|
411
648
|
const modelsWithOutput = inputs.modelsWithOutput as Set<string>;
|
|
649
|
+
const modelMap = buildModelMap(inputs.contractRoots);
|
|
650
|
+
const subConfigKey = stableSubConfig(config);
|
|
412
651
|
|
|
413
|
-
// Pre-pass: register all model → outPath before generating, so cross-file imports resolve.
|
|
414
652
|
const modelOutPaths = new Map<string, string>();
|
|
415
|
-
const entries: { ast:
|
|
653
|
+
const entries: { ast: ContractRootNode; outPath: string }[] = [];
|
|
416
654
|
for (const ast of inputs.contractRoots) {
|
|
417
655
|
const outPath = computeContractOutPath(ast.file, zodBase, config.output, '.schema.ts', commonRoot, ast.meta);
|
|
418
656
|
entries.push({ ast, outPath });
|
|
@@ -424,33 +662,49 @@ function runZodGeneration(
|
|
|
424
662
|
}
|
|
425
663
|
|
|
426
664
|
for (const { ast, outPath } of entries) {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
665
|
+
const refs = collectContractRootRefs(ast, modelMap);
|
|
666
|
+
const ownNames = new Set(ast.models.map(m => m.name));
|
|
667
|
+
const fingerprint = hashFingerprint({
|
|
668
|
+
kind: 'zod',
|
|
669
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
670
|
+
outPath,
|
|
671
|
+
root: ast,
|
|
672
|
+
outPathSlice: sliceOutPathMap(refs, modelOutPaths, modelsWithInput, modelsWithOutput),
|
|
673
|
+
modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
|
|
674
|
+
modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput),
|
|
675
|
+
sub: subConfigKey,
|
|
676
|
+
});
|
|
677
|
+
units.push({
|
|
678
|
+
key: `zod::${outPath}`,
|
|
679
|
+
fingerprint,
|
|
680
|
+
render: () => [
|
|
681
|
+
{
|
|
682
|
+
relativePath: outPath,
|
|
683
|
+
content: generateContract(ast, { modelOutPaths, currentOutPath: outPath, modelsWithInput, modelsWithOutput }),
|
|
684
|
+
},
|
|
685
|
+
],
|
|
432
686
|
});
|
|
433
|
-
emitFile(outPath, content);
|
|
434
687
|
}
|
|
435
688
|
}
|
|
436
689
|
|
|
437
|
-
// ───
|
|
690
|
+
// ─── Plain types sub-generator ─────────────────────────────────────────────
|
|
438
691
|
|
|
439
|
-
function
|
|
692
|
+
function collectTypesOutput(
|
|
440
693
|
config: TypesConfig,
|
|
441
694
|
rootDir: string,
|
|
442
695
|
inputs: Parameters<NonNullable<ContractKitPlugin['generateTargets']>>[0],
|
|
443
|
-
|
|
696
|
+
units: IncrementalUnit[],
|
|
444
697
|
): void {
|
|
445
698
|
const typesBase = resolve(rootDir, config.baseDir ?? '.');
|
|
446
699
|
const allFiles = [...inputs.contractRoots.map(r => r.file), ...inputs.opRoots.map(r => r.file)];
|
|
447
700
|
const commonRoot = commonDir(allFiles, rootDir);
|
|
448
701
|
const modelsWithInput = inputs.modelsWithInput as Set<string>;
|
|
449
702
|
const modelsWithOutput = inputs.modelsWithOutput as Set<string>;
|
|
703
|
+
const modelMap = buildModelMap(inputs.contractRoots);
|
|
704
|
+
const subConfigKey = stableSubConfig(config);
|
|
450
705
|
|
|
451
|
-
// Pre-pass: register all model → outPath before generating.
|
|
452
706
|
const modelOutPaths = new Map<string, string>();
|
|
453
|
-
const entries: { ast:
|
|
707
|
+
const entries: { ast: ContractRootNode; outPath: string }[] = [];
|
|
454
708
|
for (const ast of inputs.contractRoots) {
|
|
455
709
|
const outPath = computeContractOutPath(ast.file, typesBase, config.output, '.types.ts', commonRoot, ast.meta);
|
|
456
710
|
entries.push({ ast, outPath });
|
|
@@ -462,65 +716,70 @@ function runTypesGeneration(
|
|
|
462
716
|
}
|
|
463
717
|
|
|
464
718
|
for (const { ast, outPath } of entries) {
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
719
|
+
const refs = collectContractRootRefs(ast, modelMap);
|
|
720
|
+
const ownNames = new Set(ast.models.map(m => m.name));
|
|
721
|
+
const fingerprint = hashFingerprint({
|
|
722
|
+
kind: 'plain-types',
|
|
723
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
724
|
+
outPath,
|
|
725
|
+
root: ast,
|
|
726
|
+
outPathSlice: sliceOutPathMap(refs, modelOutPaths, modelsWithInput, modelsWithOutput),
|
|
727
|
+
modelsWithInput: sliceModelSet(refs, ownNames, modelsWithInput),
|
|
728
|
+
modelsWithOutput: sliceModelSet(refs, ownNames, modelsWithOutput),
|
|
729
|
+
sub: subConfigKey,
|
|
730
|
+
});
|
|
731
|
+
units.push({
|
|
732
|
+
key: `plain-types::${outPath}`,
|
|
733
|
+
fingerprint,
|
|
734
|
+
render: () => [
|
|
735
|
+
{
|
|
736
|
+
relativePath: outPath,
|
|
737
|
+
content: generatePlainTypes(ast, { modelOutPaths, currentOutPath: outPath, modelsWithInput, modelsWithOutput }),
|
|
738
|
+
},
|
|
739
|
+
],
|
|
470
740
|
});
|
|
471
|
-
emitFile(outPath, content);
|
|
472
741
|
}
|
|
473
742
|
}
|
|
474
743
|
|
|
475
|
-
// ───
|
|
744
|
+
// ─── Manifest IO + cleanup ─────────────────────────────────────────────────
|
|
476
745
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
746
|
+
function readManifest(manifestPath: string): IncrementalManifest {
|
|
747
|
+
if (!existsSync(manifestPath)) return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
|
|
748
|
+
try {
|
|
749
|
+
return parseIncrementalManifest(readFileSync(manifestPath, 'utf-8'));
|
|
750
|
+
} catch {
|
|
751
|
+
return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
482
754
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
755
|
+
function deleteStalePaths(absPaths: string[]): void {
|
|
756
|
+
if (absPaths.length === 0) return;
|
|
757
|
+
const removedDirs = new Set<string>();
|
|
758
|
+
for (const abs of absPaths) {
|
|
759
|
+
if (existsSync(abs)) {
|
|
760
|
+
rmSync(abs, { force: true });
|
|
761
|
+
removedDirs.add(dirname(abs));
|
|
488
762
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
763
|
+
}
|
|
764
|
+
// Walk up affected dirs and remove if empty. Bounded — stops at filesystem root or first non-empty dir.
|
|
765
|
+
for (const dir of removedDirs) {
|
|
766
|
+
let current = dir;
|
|
767
|
+
while (current.length > 1) {
|
|
768
|
+
try {
|
|
769
|
+
if (readdirSync(current).length === 0) {
|
|
770
|
+
rmdirSync(current);
|
|
771
|
+
current = dirname(current);
|
|
772
|
+
} else {
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
494
778
|
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export default plugin;
|
|
499
|
-
|
|
500
|
-
// ─── Factory: for programmatic use with explicit config ────────────────────
|
|
779
|
+
}
|
|
780
|
+
}
|
|
501
781
|
|
|
502
|
-
/**
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
* via `contractkit.config.json`.
|
|
506
|
-
*/
|
|
507
|
-
export function createTypescriptPlugin(config: TypescriptPluginConfig, rootDir: string): ContractKitPlugin {
|
|
508
|
-
return {
|
|
509
|
-
name: 'typescript',
|
|
510
|
-
cacheKey: `typescript:${JSON.stringify(config)}`,
|
|
511
|
-
async generateTargets(inputs, ctx) {
|
|
512
|
-
if (config.server) {
|
|
513
|
-
runServerGeneration(config.server, rootDir, inputs, ctx.emitFile.bind(ctx));
|
|
514
|
-
}
|
|
515
|
-
if (config.sdk) {
|
|
516
|
-
runSdkGeneration(config.sdk, rootDir, inputs, ctx.emitFile.bind(ctx));
|
|
517
|
-
}
|
|
518
|
-
if (config.zod) {
|
|
519
|
-
runZodGeneration(config.zod, rootDir, inputs, ctx.emitFile.bind(ctx));
|
|
520
|
-
}
|
|
521
|
-
if (config.types) {
|
|
522
|
-
runTypesGeneration(config.types, rootDir, inputs, ctx.emitFile.bind(ctx));
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
};
|
|
782
|
+
/** Stringify a sub-config so it can participate in fingerprints. JSON.stringify gives stable output for typical config shapes. */
|
|
783
|
+
function stableSubConfig(config: unknown): string {
|
|
784
|
+
return JSON.stringify(config ?? null);
|
|
526
785
|
}
|