@ts-kizuna/cli 1.49.5

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.
@@ -0,0 +1,669 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import ts from "typescript";
4
+ import { AUTHORING_NAMES, CONTRACT_EXPORT_NAMES } from "@ts-kizuna/core/authoring-names";
5
+ import { contractFingerprint, serializeDeprecationMap } from "@ts-kizuna/core/generator";
6
+ import { createJiti } from "jiti";
7
+ //#region src/schema-exports.ts
8
+ /**
9
+ * Maps every exported schema reachable from `entryPath` to the verbatim JSDoc
10
+ * block on each of its fields, keyed by the exported const name (and, for
11
+ * aliased re-exports, by the original declared name too).
12
+ *
13
+ * Walks the whole reachable graph, both the entry's re-exports and every file
14
+ * it imports, so a schema defined in its own module and used via `import`
15
+ * (rather than re-exported from the contract entry) is still collected.
16
+ *
17
+ * Used to patch emitted `.d.ts` files, where a `z.ZodObject<{...}>` shape is
18
+ * keyed by the `declare const` name rather than a route or schema `meta.id`.
19
+ */
20
+ const collectExportedSchemaDocs = (entryPath) => {
21
+ const { resolve, cache } = makeResolverWithCache(entryPath);
22
+ const result = /* @__PURE__ */ new Map();
23
+ const visited = /* @__PURE__ */ new Set();
24
+ const record = (name, expression) => {
25
+ const fields = /* @__PURE__ */ new Map();
26
+ collectFieldDocs(expression, "", fields, resolve);
27
+ if (fields.size > 0) result.set(name, fields);
28
+ };
29
+ const collectFromFile = (filePath) => {
30
+ if (visited.has(filePath)) return;
31
+ visited.add(filePath);
32
+ const sourceFile = ts.createSourceFile(filePath, fs.readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true);
33
+ for (const statement of sourceFile.statements) {
34
+ if (ts.isVariableStatement(statement)) {
35
+ if (!(statement.modifiers ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
36
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) record(declaration.name.text, declaration.initializer);
37
+ continue;
38
+ }
39
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
40
+ const specifier = statement.moduleSpecifier.text;
41
+ if (!specifier.startsWith(".")) continue;
42
+ const target = resolveImportPath(path.dirname(filePath), specifier);
43
+ if (!target) continue;
44
+ const exportClause = statement.exportClause;
45
+ if (!exportClause) {
46
+ collectFromFile(target);
47
+ continue;
48
+ }
49
+ if (!ts.isNamedExports(exportClause)) continue;
50
+ const targetScope = buildFileScope(target, cache);
51
+ for (const element of exportClause.elements) {
52
+ const localName = (element.propertyName ?? element.name).text;
53
+ const exposedName = element.name.text;
54
+ const expression = targetScope.get(localName);
55
+ if (!expression) continue;
56
+ record(exposedName, expression);
57
+ if (localName !== exposedName) record(localName, expression);
58
+ }
59
+ }
60
+ };
61
+ collectFromFile(entryPath);
62
+ for (const filePath of [...cache.keys()]) collectFromFile(filePath);
63
+ return result;
64
+ };
65
+ //#endregion
66
+ //#region src/dts-jsdoc.ts
67
+ /**
68
+ * Zod wrapper types whose inner type is the first type argument. Navigating a
69
+ * field path unwraps these until it reaches a `ZodObject`. Mirrors the runtime
70
+ * wrapper set in `zod-internals.ts`.
71
+ */
72
+ const ELEMENT_WRAPPERS = new Set([
73
+ "ZodArray",
74
+ "ZodOptional",
75
+ "ZodNullable",
76
+ "ZodDefault",
77
+ "ZodReadonly",
78
+ "ZodNonOptional",
79
+ "ZodCatch",
80
+ "ZodPrefault"
81
+ ]);
82
+ const entityNameRight = (name) => ts.isQualifiedName(name) ? name.right.text : name.text;
83
+ /**
84
+ * The right-most type name, ignoring the qualifier, so `ZodObject`,
85
+ * `z.ZodObject`, and `import("zod").ZodObject` all read as `ZodObject`.
86
+ */
87
+ const rightmostTypeName = (node) => {
88
+ if (ts.isTypeReferenceNode(node)) return entityNameRight(node.typeName);
89
+ if (ts.isImportTypeNode(node)) return node.qualifier ? entityNameRight(node.qualifier) : void 0;
90
+ };
91
+ const typeArgumentsOf = (node) => {
92
+ if (ts.isTypeReferenceNode(node)) return node.typeArguments ?? [];
93
+ if (ts.isImportTypeNode(node)) return node.typeArguments ?? [];
94
+ return [];
95
+ };
96
+ const propertySignatureName = (node) => {
97
+ if (ts.isIdentifier(node)) return node.text;
98
+ if (ts.isStringLiteral(node)) return node.text;
99
+ if (ts.isNumericLiteral(node)) return node.text;
100
+ };
101
+ /**
102
+ * Walks a dotted field path through a Zod schema type to the leaf property
103
+ * signature. Unwraps element wrappers (`ZodArray`, `ZodOptional`, …) and reads
104
+ * each `ZodObject`'s shape from its first type argument. Returns undefined and
105
+ * never throws when the path can't be navigated (e.g. the shape was emitted as
106
+ * a named reference rather than a literal).
107
+ */
108
+ const findPropertySignature = (typeNode, segments) => {
109
+ let current = typeNode;
110
+ for (let index = 0; index < segments.length; index += 1) {
111
+ let guard = 0;
112
+ let name = rightmostTypeName(current);
113
+ while (name !== void 0 && ELEMENT_WRAPPERS.has(name)) {
114
+ const args = typeArgumentsOf(current);
115
+ if (args.length === 0) return void 0;
116
+ current = args[0];
117
+ name = rightmostTypeName(current);
118
+ guard += 1;
119
+ if (guard > 32) return void 0;
120
+ }
121
+ if (name !== "ZodObject") return void 0;
122
+ const shape = typeArgumentsOf(current)[0];
123
+ if (!shape || !ts.isTypeLiteralNode(shape)) return void 0;
124
+ const member = shape.members.find((candidate) => ts.isPropertySignature(candidate) && propertySignatureName(candidate.name) === segments[index]);
125
+ if (!member) return void 0;
126
+ if (index === segments.length - 1) return member;
127
+ if (!member.type) return void 0;
128
+ current = member.type;
129
+ }
130
+ };
131
+ const hasLeadingJsDoc = (member, fullText) => {
132
+ return (ts.getLeadingCommentRanges(fullText, member.getFullStart()) ?? []).some((range) => fullText.slice(range.pos, range.pos + 3) === "/**");
133
+ };
134
+ /**
135
+ * Re-indents a captured JSDoc block to `indent`: the opening `/**` keeps the
136
+ * property's own indentation (already present before the splice point), and each
137
+ * continuation line is re-prefixed so the `*` column lines up.
138
+ */
139
+ const reindentJsDoc = (block, indent) => block.split("\n").map((line, index) => index === 0 ? line.trim() : `${indent} ${line.trim()}`).join("\n");
140
+ const indentAt = (text, offset) => {
141
+ const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
142
+ return text.slice(lineStart, offset).match(/^[ \t]*/)?.[0] ?? "";
143
+ };
144
+ /**
145
+ * Maps each locally declared name to the names it's re-exported under in the
146
+ * same file, e.g. `export { i as UserSchema }` from a bundled chunk where the
147
+ * `declare const` is named `i`.
148
+ */
149
+ const localExportAliases = (sourceFile) => {
150
+ const aliases = /* @__PURE__ */ new Map();
151
+ for (const statement of sourceFile.statements) {
152
+ if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier) continue;
153
+ if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue;
154
+ for (const element of statement.exportClause.elements) {
155
+ const localName = (element.propertyName ?? element.name).text;
156
+ const list = aliases.get(localName) ?? [];
157
+ list.push(element.name.text);
158
+ aliases.set(localName, list);
159
+ }
160
+ }
161
+ return aliases;
162
+ };
163
+ const patchFile = (filePath, exportFieldMap) => {
164
+ const text = fs.readFileSync(filePath, "utf8");
165
+ const sourceFile = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true);
166
+ const aliases = localExportAliases(sourceFile);
167
+ const insertions = [];
168
+ for (const statement of sourceFile.statements) {
169
+ if (!ts.isVariableStatement(statement)) continue;
170
+ for (const declaration of statement.declarationList.declarations) {
171
+ if (!ts.isIdentifier(declaration.name) || !declaration.type) continue;
172
+ const fields = [declaration.name.text, ...aliases.get(declaration.name.text) ?? []].map((name) => exportFieldMap.get(name)).find(Boolean);
173
+ if (!fields) continue;
174
+ for (const [fieldPath, block] of fields) {
175
+ const member = findPropertySignature(declaration.type, fieldPath.split("."));
176
+ if (!member || hasLeadingJsDoc(member, text)) continue;
177
+ const offset = member.getStart(sourceFile);
178
+ const indent = indentAt(text, offset);
179
+ insertions.push({
180
+ offset,
181
+ text: `${reindentJsDoc(block, indent)}\n${indent}`
182
+ });
183
+ }
184
+ }
185
+ }
186
+ if (insertions.length === 0) return 0;
187
+ insertions.sort((left, right) => right.offset - left.offset);
188
+ let output = text;
189
+ for (const insertion of insertions) output = output.slice(0, insertion.offset) + insertion.text + output.slice(insertion.offset);
190
+ fs.writeFileSync(filePath, output, "utf8");
191
+ return insertions.length;
192
+ };
193
+ const DECLARATION_SUFFIXES = [
194
+ ".d.ts",
195
+ ".d.mts",
196
+ ".d.cts"
197
+ ];
198
+ const walkDeclarationFiles = (dir, into) => {
199
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
200
+ const full = path.join(dir, entry.name);
201
+ if (entry.isDirectory()) walkDeclarationFiles(full, into);
202
+ else if (entry.isFile() && DECLARATION_SUFFIXES.some((suffix) => entry.name.endsWith(suffix))) into.push(full);
203
+ }
204
+ };
205
+ /**
206
+ * Re-injects the JSDoc blocks from {@link collectExportedSchemaDocs} onto Zod
207
+ * schema shape properties in the `.d.ts` files under `distDir`. Declaration emit
208
+ * drops the comments an author wrote on schema fields; this restores them, full
209
+ * descriptions, `@deprecated`, `@example`, etc., so they reach `z.infer`
210
+ * consumers in other repos. Skips properties that already have JSDoc. Idempotent.
211
+ */
212
+ const patchDeclarationDocs = (distDir, exportFieldMap) => {
213
+ const files = [];
214
+ walkDeclarationFiles(distDir, files);
215
+ let filesChanged = 0;
216
+ let injections = 0;
217
+ for (const file of files) {
218
+ const count = patchFile(file, exportFieldMap);
219
+ if (count > 0) {
220
+ filesChanged += 1;
221
+ injections += count;
222
+ }
223
+ }
224
+ return {
225
+ filesScanned: files.length,
226
+ filesChanged,
227
+ injections
228
+ };
229
+ };
230
+ //#endregion
231
+ //#region src/deprecation-parser.ts
232
+ const SCHEMA_KEYS = new Set([
233
+ "body",
234
+ "query",
235
+ "headers"
236
+ ]);
237
+ const readDeprecatedMessage = (node) => {
238
+ const tag = ts.getJSDocTags(node).find((candidate) => candidate.tagName.text === "deprecated");
239
+ if (!tag) return void 0;
240
+ return ts.getTextOfJSDocComment(tag.comment) ?? "";
241
+ };
242
+ const propertyName = (node) => {
243
+ if (ts.isIdentifier(node)) return node.text;
244
+ if (ts.isStringLiteral(node)) return node.text;
245
+ if (ts.isNumericLiteral(node)) return node.text;
246
+ };
247
+ const resolveImportPath = (dir, specifier) => {
248
+ const base = specifier.replace(/\.js$/, "");
249
+ for (const candidate of [
250
+ `${base}.ts`,
251
+ `${base}/index.ts`,
252
+ base
253
+ ]) {
254
+ const full = path.resolve(dir, candidate);
255
+ if (fs.existsSync(full)) return full;
256
+ }
257
+ };
258
+ const buildFileScope = (filePath, cache) => {
259
+ if (cache.has(filePath)) return cache.get(filePath);
260
+ const scope = /* @__PURE__ */ new Map();
261
+ cache.set(filePath, scope);
262
+ const source = fs.readFileSync(filePath, "utf8");
263
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
264
+ for (const statement of sourceFile.statements) {
265
+ if (!ts.isVariableStatement(statement)) continue;
266
+ for (const decl of statement.declarationList.declarations) if (ts.isIdentifier(decl.name) && decl.initializer) scope.set(decl.name.text, decl.initializer);
267
+ }
268
+ for (const statement of sourceFile.statements) {
269
+ if (!ts.isImportDeclaration(statement)) continue;
270
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
271
+ const specifier = statement.moduleSpecifier.text;
272
+ if (!specifier.startsWith(".")) continue;
273
+ const importedPath = resolveImportPath(path.dirname(filePath), specifier);
274
+ if (!importedPath) continue;
275
+ const importedScope = buildFileScope(importedPath, cache);
276
+ const namedBindings = statement.importClause?.namedBindings;
277
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
278
+ for (const element of namedBindings.elements) {
279
+ const exportedName = (element.propertyName ?? element.name).text;
280
+ const localName = element.name.text;
281
+ const expr = importedScope.get(exportedName);
282
+ if (expr) scope.set(localName, expr);
283
+ }
284
+ }
285
+ return scope;
286
+ };
287
+ const makeResolverWithCache = (contractPath) => {
288
+ const cache = /* @__PURE__ */ new Map();
289
+ buildFileScope(contractPath, cache);
290
+ const resolve = (node) => {
291
+ const filePath = node.getSourceFile().fileName;
292
+ return (cache.get(filePath) ?? buildFileScope(filePath, cache)).get(node.text);
293
+ };
294
+ return {
295
+ resolve,
296
+ cache
297
+ };
298
+ };
299
+ const readObjectStringProperty = (object, name) => {
300
+ for (const property of object.properties) {
301
+ if (!ts.isPropertyAssignment(property)) continue;
302
+ if (!ts.isIdentifier(property.name) || property.name.text !== name) continue;
303
+ if (ts.isStringLiteral(property.initializer)) return property.initializer.text;
304
+ }
305
+ };
306
+ const readAstMetaId = (expr) => {
307
+ if (!ts.isCallExpression(expr)) return void 0;
308
+ const firstArg = expr.arguments[0];
309
+ if (isModelCall(expr)) {
310
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return void 0;
311
+ return readObjectStringProperty(firstArg, "title");
312
+ }
313
+ if (ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "meta") {
314
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return void 0;
315
+ return readObjectStringProperty(firstArg, "id") ?? readAstMetaId(expr.expression.expression);
316
+ }
317
+ };
318
+ const firstObjectLiteralIn = (node, resolve, visited) => {
319
+ if (ts.isObjectLiteralExpression(node)) return node;
320
+ if (ts.isIdentifier(node)) {
321
+ const name = node.text;
322
+ if (visited.has(name)) return void 0;
323
+ const referenced = resolve(node);
324
+ if (!referenced) return void 0;
325
+ if (ts.isArrowFunction(referenced) || ts.isFunctionExpression(referenced)) return void 0;
326
+ visited.add(name);
327
+ return firstObjectLiteralIn(referenced, resolve, visited);
328
+ }
329
+ if (ts.isCallExpression(node)) {
330
+ if (isContractChainCall(node) || isRoutesChainCall(node)) {
331
+ const routesArg = routesArgFrom(node);
332
+ if (routesArg) return firstObjectLiteralIn(routesArg, resolve, visited);
333
+ return;
334
+ }
335
+ if (isModelCall(node)) {
336
+ const modelSchema = extractCreateModelSchema(node);
337
+ return modelSchema ? firstObjectLiteralIn(modelSchema, resolve, visited) : void 0;
338
+ }
339
+ }
340
+ let found;
341
+ ts.forEachChild(node, (child) => {
342
+ if (found) return;
343
+ found = firstObjectLiteralIn(child, resolve, visited);
344
+ });
345
+ return found;
346
+ };
347
+ const findExtendBase = (node) => {
348
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return void 0;
349
+ if (node.expression.name.text === "extend") return node.expression.expression;
350
+ return findExtendBase(node.expression.expression);
351
+ };
352
+ const getFunctionBody = (func) => {
353
+ const { body } = func;
354
+ if (!ts.isBlock(body)) return body;
355
+ let result;
356
+ ts.forEachChild(body, (stmt) => {
357
+ if (!result && ts.isReturnStatement(stmt) && stmt.expression) result = stmt.expression;
358
+ });
359
+ return result ?? body;
360
+ };
361
+ const makeScopedResolver = (func, args, parent) => {
362
+ const substitutions = /* @__PURE__ */ new Map();
363
+ func.parameters.forEach((param, index) => {
364
+ const arg = args[index];
365
+ if (arg && ts.isIdentifier(param.name)) substitutions.set(param.name.text, arg);
366
+ });
367
+ return (node) => substitutions.get(node.text) ?? parent(node);
368
+ };
369
+ const extractCreateModelSchema = (node) => {
370
+ if (!ts.isCallExpression(node) || !isModelCall(node)) return void 0;
371
+ const firstArg = node.arguments[0];
372
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return void 0;
373
+ for (const property of firstArg.properties) {
374
+ if (!ts.isPropertyAssignment(property)) continue;
375
+ if (propertyName(property.name) === "schema") return property.initializer;
376
+ }
377
+ };
378
+ /**
379
+ * Walks a schema expression's fields, calling `visit` with each field's dot-path
380
+ * and its property node. Resolves identifiers, `Kizuna.model`, generic wrapper
381
+ * functions (e.g. `Pagination(Item)`), and `.extend()`; recurses into nested
382
+ * objects.
383
+ */
384
+ const walkSchemaFields = (schemaNode, prefix, resolve, visit) => {
385
+ const resolved = ts.isIdentifier(schemaNode) ? resolve(schemaNode) ?? schemaNode : schemaNode;
386
+ const modelSchema = extractCreateModelSchema(resolved);
387
+ if (modelSchema) {
388
+ walkSchemaFields(modelSchema, prefix, resolve, visit);
389
+ return;
390
+ }
391
+ if (ts.isCallExpression(resolved) && ts.isIdentifier(resolved.expression)) {
392
+ const funcExpr = resolve(resolved.expression);
393
+ if (funcExpr && (ts.isArrowFunction(funcExpr) || ts.isFunctionExpression(funcExpr))) {
394
+ walkSchemaFields(getFunctionBody(funcExpr), prefix, makeScopedResolver(funcExpr, resolved.arguments, resolve), visit);
395
+ return;
396
+ }
397
+ }
398
+ const extendBase = findExtendBase(resolved);
399
+ if (extendBase) walkSchemaFields(extendBase, prefix, resolve, visit);
400
+ const objectLiteral = firstObjectLiteralIn(schemaNode, resolve, /* @__PURE__ */ new Set());
401
+ if (!objectLiteral) return;
402
+ for (const property of objectLiteral.properties) {
403
+ if (ts.isShorthandPropertyAssignment(property)) {
404
+ visit(prefix === "" ? property.name.text : `${prefix}.${property.name.text}`, property);
405
+ continue;
406
+ }
407
+ if (!ts.isPropertyAssignment(property)) continue;
408
+ const fieldName = propertyName(property.name);
409
+ if (fieldName === void 0) continue;
410
+ const fieldPath = prefix === "" ? fieldName : `${prefix}.${fieldName}`;
411
+ visit(fieldPath, property);
412
+ walkSchemaFields(property.initializer, fieldPath, resolve, visit);
413
+ }
414
+ };
415
+ const collectFieldDeprecations = (schemaNode, prefix, into, resolve) => {
416
+ walkSchemaFields(schemaNode, prefix, resolve, (fieldPath, property) => {
417
+ const message = readDeprecatedMessage(property);
418
+ if (message !== void 0) into.set(fieldPath, message);
419
+ });
420
+ };
421
+ /**
422
+ * Returns the verbatim leading JSDoc block (`/** … *\/`) on a node, or undefined
423
+ * when it has none.
424
+ */
425
+ const readJsDocBlock = (node) => {
426
+ const sourceFile = node.getSourceFile();
427
+ const jsDoc = (ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) ?? []).filter((range) => sourceFile.text.slice(range.pos, range.pos + 3) === "/**").at(-1);
428
+ return jsDoc ? sourceFile.text.slice(jsDoc.pos, jsDoc.end) : void 0;
429
+ };
430
+ const collectFieldDocs = (schemaNode, prefix, into, resolve) => {
431
+ walkSchemaFields(schemaNode, prefix, resolve, (fieldPath, property) => {
432
+ const block = readJsDocBlock(property);
433
+ if (block !== void 0) into.set(fieldPath, block);
434
+ });
435
+ };
436
+ const isRouteLike = (obj) => obj.properties.some((prop) => {
437
+ const name = ts.isPropertyAssignment(prop) ? propertyName(prop.name) : ts.isShorthandPropertyAssignment(prop) ? prop.name.text : void 0;
438
+ return name === "method" || name === "path" || name === "responses";
439
+ });
440
+ const routePropertyName = (prop) => {
441
+ if (ts.isPropertyAssignment(prop)) return propertyName(prop.name);
442
+ if (ts.isShorthandPropertyAssignment(prop)) return prop.name.text;
443
+ };
444
+ const routePropertyInitializer = (prop) => ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
445
+ const buildMapFromRoutesLiteral = (routesLiteral, resolve, prefix = "") => {
446
+ const routes = /* @__PURE__ */ new Map();
447
+ const fields = /* @__PURE__ */ new Map();
448
+ for (const routeProperty of routesLiteral.properties) {
449
+ if (!ts.isPropertyAssignment(routeProperty) && !ts.isShorthandPropertyAssignment(routeProperty)) continue;
450
+ const routeName = routePropertyName(routeProperty);
451
+ if (routeName === void 0) continue;
452
+ const fullKey = prefix === "" ? routeName : `${prefix}.${routeName}`;
453
+ const routeMessage = readDeprecatedMessage(routeProperty);
454
+ if (routeMessage !== void 0) routes.set(fullKey, routeMessage);
455
+ const initializer = routePropertyInitializer(routeProperty);
456
+ const resolvedLiteral = ts.isObjectLiteralExpression(initializer) ? initializer : firstObjectLiteralIn(initializer, resolve, /* @__PURE__ */ new Set());
457
+ if (!resolvedLiteral) continue;
458
+ if (!isRouteLike(resolvedLiteral)) {
459
+ const sub = buildMapFromRoutesLiteral(resolvedLiteral, resolve, fullKey);
460
+ for (const [key, value] of sub.routes) routes.set(key, value);
461
+ for (const [key, value] of sub.fields) fields.set(key, value);
462
+ continue;
463
+ }
464
+ const deprecated = /* @__PURE__ */ new Map();
465
+ for (const subProperty of resolvedLiteral.properties) {
466
+ if (!ts.isPropertyAssignment(subProperty)) continue;
467
+ const subKey = propertyName(subProperty.name);
468
+ if (subKey === void 0) continue;
469
+ if (SCHEMA_KEYS.has(subKey)) {
470
+ collectFieldDeprecations(subProperty.initializer, subKey, deprecated, resolve);
471
+ continue;
472
+ }
473
+ if (subKey === "responses" && ts.isObjectLiteralExpression(subProperty.initializer)) for (const responseEntry of subProperty.initializer.properties) {
474
+ if (!ts.isPropertyAssignment(responseEntry)) continue;
475
+ const status = propertyName(responseEntry.name);
476
+ if (status === void 0) continue;
477
+ collectFieldDeprecations(responseEntry.initializer, `responses.${status}`, deprecated, resolve);
478
+ }
479
+ }
480
+ if (deprecated.size > 0) fields.set(fullKey, deprecated);
481
+ }
482
+ return {
483
+ routes,
484
+ fields
485
+ };
486
+ };
487
+ const findRouterCallInNode = (node) => {
488
+ if (ts.isCallExpression(node)) {
489
+ if (isMemberCall(node, AUTHORING_NAMES.router)) return node;
490
+ if (isContractChainCall(node) || isRoutesChainCall(node)) return node;
491
+ }
492
+ let found;
493
+ ts.forEachChild(node, (child) => {
494
+ if (found) return;
495
+ found = findRouterCallInNode(child);
496
+ });
497
+ return found;
498
+ };
499
+ /**
500
+ * True when `node` calls `member` on any receiver, e.g. `k.routes(...)`.
501
+ */
502
+ const isMemberCall = (node, member) => ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name) && node.expression.name.text === member;
503
+ const isRoutesChainCall = (node) => isMemberCall(node, AUTHORING_NAMES.routes);
504
+ const isContractChainCall = (node) => isMemberCall(node, AUTHORING_NAMES.contract);
505
+ const isModelCall = (node) => isMemberCall(node, AUTHORING_NAMES.model);
506
+ const isContractExportName = (name) => CONTRACT_EXPORT_NAMES.includes(name);
507
+ const routesArgFrom = (call) => {
508
+ if (isRoutesChainCall(call)) return call.arguments[call.arguments.length - 1];
509
+ if (isContractChainCall(call)) {
510
+ const arg = call.arguments[0];
511
+ if (arg && ts.isObjectLiteralExpression(arg)) for (const prop of arg.properties) {
512
+ if (ts.isPropertyAssignment(prop) && propertyName(prop.name) === "routes") return prop.initializer;
513
+ if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === "routes") return prop.name;
514
+ }
515
+ return arg;
516
+ }
517
+ const firstArg = call.arguments[0];
518
+ if (!firstArg) return void 0;
519
+ if (call.arguments.length === 2) return call.arguments[1];
520
+ return firstArg;
521
+ };
522
+ const collectExportedRoutesLiterals = (sourceFile, resolve, into) => {
523
+ for (const statement of sourceFile.statements) {
524
+ if (!ts.isVariableStatement(statement)) continue;
525
+ if (!(statement.modifiers ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
526
+ for (const declaration of statement.declarationList.declarations) {
527
+ if (!ts.isIdentifier(declaration.name)) continue;
528
+ const exportName = declaration.name.text;
529
+ if (!isContractExportName(exportName)) continue;
530
+ if (!declaration.initializer) continue;
531
+ let initializer = declaration.initializer;
532
+ if (ts.isIdentifier(initializer)) {
533
+ const resolved = resolve(initializer);
534
+ if (resolved) initializer = resolved;
535
+ }
536
+ if (!ts.isCallExpression(initializer)) continue;
537
+ initializer.expression;
538
+ if (isContractChainCall(initializer)) {
539
+ const routesArg = routesArgFrom(initializer);
540
+ const lit = routesArg ? firstObjectLiteralIn(routesArg, resolve, /* @__PURE__ */ new Set()) : void 0;
541
+ if (lit) into.push(lit);
542
+ }
543
+ }
544
+ }
545
+ for (const statement of sourceFile.statements) {
546
+ if (!ts.isExportDeclaration(statement)) continue;
547
+ if (!statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
548
+ const specifier = statement.moduleSpecifier.text;
549
+ if (!specifier.startsWith(".")) continue;
550
+ const exportClause = statement.exportClause;
551
+ if (!exportClause || !ts.isNamedExports(exportClause)) continue;
552
+ if (!exportClause.elements.some((element) => {
553
+ const exportedName = element.name.text;
554
+ return isContractExportName(exportedName);
555
+ })) continue;
556
+ const importedPath = resolveImportPath(path.dirname(sourceFile.fileName), specifier);
557
+ if (!importedPath) continue;
558
+ collectExportedRoutesLiterals(ts.createSourceFile(importedPath, fs.readFileSync(importedPath, "utf8"), ts.ScriptTarget.Latest, true), resolve, into);
559
+ }
560
+ };
561
+ const findAllRouteObjectLiterals = (sourceFile, resolve) => {
562
+ const results = [];
563
+ collectExportedRoutesLiterals(sourceFile, resolve, results);
564
+ if (results.length > 0) return results;
565
+ const routerCall = findRouterCallInNode(sourceFile);
566
+ if (!routerCall) return [];
567
+ const routesArg = routesArgFrom(routerCall);
568
+ const lit = routesArg ? firstObjectLiteralIn(routesArg, resolve, /* @__PURE__ */ new Set()) : void 0;
569
+ return lit ? [lit] : [];
570
+ };
571
+ const parseFromSource = (contractPath) => {
572
+ const source = fs.readFileSync(contractPath, "utf8");
573
+ const sourceFile = ts.createSourceFile(contractPath, source, ts.ScriptTarget.Latest, true);
574
+ const { resolve, cache } = makeResolverWithCache(contractPath);
575
+ const routeLiterals = findAllRouteObjectLiterals(sourceFile, resolve);
576
+ const routes = /* @__PURE__ */ new Map();
577
+ const fields = /* @__PURE__ */ new Map();
578
+ for (const literal of routeLiterals) {
579
+ const partial = buildMapFromRoutesLiteral(literal, resolve);
580
+ for (const [key, value] of partial.routes) routes.set(key, value);
581
+ for (const [key, value] of partial.fields) fields.set(key, value);
582
+ }
583
+ const schemas = /* @__PURE__ */ new Map();
584
+ for (const fileScope of cache.values()) for (const expr of fileScope.values()) {
585
+ const id = readAstMetaId(expr);
586
+ if (!id) continue;
587
+ const fieldDeprecations = /* @__PURE__ */ new Map();
588
+ collectFieldDeprecations(expr, "", fieldDeprecations, resolve);
589
+ if (fieldDeprecations.size > 0) schemas.set(id, fieldDeprecations);
590
+ }
591
+ return {
592
+ routes,
593
+ fields,
594
+ schemas
595
+ };
596
+ };
597
+ /**
598
+ * Parses a contract's `@deprecated` JSDoc tags into a {@link DeprecationMap}.
599
+ */
600
+ const createDeprecationMap = (contractPath) => parseFromSource(contractPath);
601
+ /**
602
+ * Parses each contract's `@deprecated` tags and writes them to
603
+ * `<outDir>/deprecations.json`, keyed by contract fingerprint. Generators read
604
+ * the entry matching the contract they generate. Returns the written path.
605
+ */
606
+ const writeKizunaDeprecations = (contracts, outDir) => {
607
+ const entries = {};
608
+ for (const { contract, contractPath } of contracts) entries[contractFingerprint(contract)] = serializeDeprecationMap(createDeprecationMap(contractPath));
609
+ const outputPath = path.join(outDir, "deprecations.json");
610
+ fs.mkdirSync(outDir, { recursive: true });
611
+ fs.writeFileSync(outputPath, JSON.stringify(entries, null, 2), "utf8");
612
+ return outputPath;
613
+ };
614
+ //#endregion
615
+ //#region src/load-contract.ts
616
+ /**
617
+ * Imports a contract module with jiti (so a `.ts` entry works without a build
618
+ * step) and returns the named export (default `contract`) or the default export.
619
+ * Returns undefined when neither is present.
620
+ */
621
+ const loadContract = async (contractPath, exportName = "contract") => {
622
+ const loaded = await createJiti(import.meta.url, { interopDefault: true }).import(contractPath);
623
+ return loaded[exportName] ?? loaded.default;
624
+ };
625
+ //#endregion
626
+ //#region src/lint-deprecations.ts
627
+ const editDistance = (a, b) => {
628
+ const rows = Array.from({ length: a.length + 1 }, (_, index) => [index, ...new Array(b.length).fill(0)]);
629
+ for (let column = 0; column <= b.length; column += 1) rows[0][column] = column;
630
+ for (let row = 1; row <= a.length; row += 1) for (let column = 1; column <= b.length; column += 1) {
631
+ const cost = a[row - 1] === b[column - 1] ? 0 : 1;
632
+ rows[row][column] = Math.min(rows[row - 1][column] + 1, rows[row][column - 1] + 1, rows[row - 1][column - 1] + cost);
633
+ }
634
+ return rows[a.length][b.length];
635
+ };
636
+ const lineOf = (text, index) => text.slice(0, index).split("\n").length;
637
+ /**
638
+ * Warns about JSDoc that would silently disable a deprecation, a misspelled
639
+ * `@deprecated` tag, or more than one on the same comment. Scans the contract
640
+ * source and the files it imports.
641
+ */
642
+ const lintDeprecations = (entryPath) => {
643
+ const { cache } = makeResolverWithCache(entryPath);
644
+ const warnings = [];
645
+ for (const filePath of cache.keys()) {
646
+ const text = fs.readFileSync(filePath, "utf8");
647
+ for (const block of text.matchAll(/\/\*\*[\s\S]*?\*\//g)) {
648
+ const blockStart = block.index;
649
+ const tags = [...block[0].matchAll(/@(\w+)/g)];
650
+ const deprecatedTags = tags.filter((tag) => tag[1] === "deprecated");
651
+ if (deprecatedTags.length > 1) warnings.push({
652
+ file: filePath,
653
+ line: lineOf(text, blockStart + deprecatedTags[1].index),
654
+ message: "Duplicate `@deprecated` tag in one comment, only the first message is used."
655
+ });
656
+ for (const tag of tags) {
657
+ const name = tag[1].toLowerCase();
658
+ if (name !== "deprecated" && name.startsWith("dep") && editDistance(name, "deprecated") <= 2) warnings.push({
659
+ file: filePath,
660
+ line: lineOf(text, blockStart + tag.index),
661
+ message: `\`@${tag[1]}\` looks like a typo of \`@deprecated\`, it will be ignored.`
662
+ });
663
+ }
664
+ }
665
+ }
666
+ return warnings;
667
+ };
668
+ //#endregion
669
+ export { patchDeclarationDocs as a, writeKizunaDeprecations as i, loadContract as n, collectExportedSchemaDocs as o, createDeprecationMap as r, lintDeprecations as t };