ts-class-to-openapi 1.4.0 → 1.4.2

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.
Files changed (3) hide show
  1. package/dist/index.cjs +347 -196
  2. package/dist/index.mjs +347 -196
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -28,6 +28,53 @@ path = __toESM(path);
28
28
  //#region src/transformer.fixtures.ts
29
29
  const constants = {
30
30
  TS_CONFIG_DEFAULT_PATH: path.default.resolve(process.cwd(), "tsconfig.json"),
31
+ JS_KEYWORDS: new Set([
32
+ "constructor",
33
+ "static",
34
+ "get",
35
+ "set",
36
+ "async",
37
+ "return",
38
+ "if",
39
+ "else",
40
+ "for",
41
+ "while",
42
+ "switch",
43
+ "case",
44
+ "break",
45
+ "throw",
46
+ "new",
47
+ "delete",
48
+ "typeof",
49
+ "void",
50
+ "try",
51
+ "catch",
52
+ "finally",
53
+ "this",
54
+ "super",
55
+ "class",
56
+ "const",
57
+ "let",
58
+ "var",
59
+ "function",
60
+ "yield",
61
+ "await",
62
+ "import",
63
+ "export",
64
+ "default",
65
+ "extends",
66
+ "implements",
67
+ "in",
68
+ "of",
69
+ "do",
70
+ "with",
71
+ "continue",
72
+ "instanceof",
73
+ "true",
74
+ "false",
75
+ "null",
76
+ "undefined"
77
+ ]),
31
78
  jsPrimitives: {
32
79
  String: {
33
80
  type: "String",
@@ -189,6 +236,8 @@ var SchemaTransformer = class SchemaTransformer {
189
236
  maxCacheSize;
190
237
  autoCleanup;
191
238
  classFileIndex = /* @__PURE__ */ new Map();
239
+ transformCallIndex = /* @__PURE__ */ new Map();
240
+ nonGenericTransformCalls = /* @__PURE__ */ new Set();
192
241
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
193
242
  this.maxCacheSize = options.maxCacheSize ?? 100;
194
243
  this.autoCleanup = options.autoCleanup ?? true;
@@ -200,19 +249,7 @@ var SchemaTransformer = class SchemaTransformer {
200
249
  const { options: tsOptions, fileNames } = typescript.default.parseJsonConfigFileContent(config, typescript.default.sys, "./");
201
250
  this.program = typescript.default.createProgram(fileNames, tsOptions);
202
251
  this.checker = this.program.getTypeChecker();
203
- this.program.getSourceFiles().forEach((sf) => {
204
- sf.statements.forEach((stmt) => {
205
- if (typescript.default.isClassDeclaration(stmt) && stmt.name) {
206
- const name = stmt.name.text;
207
- const entry = this.classFileIndex.get(name) || [];
208
- entry.push({
209
- sourceFile: sf,
210
- node: stmt
211
- });
212
- this.classFileIndex.set(name, entry);
213
- }
214
- });
215
- });
252
+ this.buildTransformCallIndex();
216
253
  }
217
254
  getPropertiesByClassDeclaration(classNode, visitedDeclarations = /* @__PURE__ */ new Set(), genericTypeMap = /* @__PURE__ */ new Map()) {
218
255
  if (visitedDeclarations.has(classNode)) return [];
@@ -247,76 +284,89 @@ var SchemaTransformer = class SchemaTransformer {
247
284
  }
248
285
  getPropertiesByClassMembers(members, parentClassNode, genericTypeMap = /* @__PURE__ */ new Map()) {
249
286
  const properties = [];
250
- for (const member of members) {
251
- var _member$name;
252
- if (typescript.default.isPropertyDeclaration(member) && member.name && ((_member$name = member.name) === null || _member$name === void 0 ? void 0 : _member$name.text)) {
253
- var _typeArguments;
254
- if (member.modifiers) {
255
- if (member.modifiers.some((m) => m.kind === typescript.default.SyntaxKind.PrivateKeyword || m.kind === typescript.default.SyntaxKind.ProtectedKeyword)) continue;
287
+ for (const member of members) if (typescript.default.isPropertyDeclaration(member) && (typescript.default.isIdentifier(member.name) || typescript.default.isStringLiteral(member.name))) {
288
+ var _property$originalPro;
289
+ if (member.modifiers) {
290
+ if (member.modifiers.some((m) => m.kind === typescript.default.SyntaxKind.PrivateKeyword || m.kind === typescript.default.SyntaxKind.ProtectedKeyword)) continue;
291
+ }
292
+ const propertyName = member.name.text;
293
+ const type = this.getPropertyType(member, genericTypeMap);
294
+ const decorators = this.extractDecorators(member);
295
+ const isOptional = !!member.questionToken;
296
+ const isGeneric = this.isPropertyTypeGeneric(member);
297
+ const isEnum = this.isEnum(member);
298
+ const isPrimitive = this.isPrimitiveType(type) || isEnum;
299
+ const isClassType = this.isClassType(member);
300
+ const isArray = this.isArrayProperty(member);
301
+ const isTypeLiteral = this.isTypeLiteral(member);
302
+ let genericClassReference = void 0;
303
+ if (isGeneric && !isPrimitive) {
304
+ const baseTypeName = type.replace(/\[\]$/, "").trim();
305
+ if (!this.isPrimitiveType(baseTypeName)) {
306
+ const matches = this.classFileIndex.get(baseTypeName);
307
+ if (matches && matches.length > 0 && matches[0]) genericClassReference = matches[0].node;
256
308
  }
257
- const propertyName = member.name.text;
258
- const type = this.getPropertyType(member, genericTypeMap);
259
- const decorators = this.extractDecorators(member);
260
- const isOptional = !!member.questionToken;
261
- const isGeneric = this.isPropertyTypeGeneric(member);
262
- const isEnum = this.isEnum(member);
263
- const property = {
264
- name: propertyName,
265
- type,
266
- decorators,
267
- isOptional,
268
- isGeneric,
269
- originalProperty: member,
270
- isPrimitive: this.isPrimitiveType(type) || isEnum,
271
- isClassType: this.isClassType(member),
272
- isArray: this.isArrayProperty(member),
273
- isEnum,
274
- isRef: false,
275
- isTypeLiteral: this.isTypeLiteral(member)
276
- };
277
- if (property.isClassType) {
278
- const declaration = this.getDeclarationProperty(property);
279
- if (parentClassNode) {
280
- if (declaration && declaration.name && this.checker.getSymbolAtLocation(declaration.name) === this.checker.getSymbolAtLocation(parentClassNode.name)) property.isRef = true;
281
- }
309
+ }
310
+ const property = {
311
+ name: propertyName,
312
+ type,
313
+ decorators,
314
+ isOptional,
315
+ isGeneric,
316
+ originalProperty: member,
317
+ isPrimitive,
318
+ isClassType,
319
+ isArray,
320
+ isEnum,
321
+ isRef: false,
322
+ isTypeLiteral,
323
+ genericClassReference
324
+ };
325
+ if (property.isClassType) {
326
+ const declaration = this.getDeclarationProperty(property);
327
+ if (parentClassNode) {
328
+ if (declaration && declaration.name && this.checker.getSymbolAtLocation(declaration.name) === this.checker.getSymbolAtLocation(parentClassNode.name)) property.isRef = true;
282
329
  }
283
- if (property.isTypeLiteral && property.originalProperty.type && ((_typeArguments = property.originalProperty.type.typeArguments) === null || _typeArguments === void 0 ? void 0 : _typeArguments.length) === 1) {
284
- const typeArguments = property.originalProperty.type.typeArguments;
285
- if (typeArguments && typeArguments[0]) {
286
- const firstTypeArg = typeArguments[0];
287
- if (typescript.default.isTypeReferenceNode(firstTypeArg)) {
288
- const symbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
289
- if (symbol && symbol.declarations) {
290
- const classDeclaration = symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
291
- if (classDeclaration && typescript.default.isClassDeclaration(classDeclaration)) property.typeLiteralClassReference = classDeclaration;
292
- }
330
+ }
331
+ if (property.isTypeLiteral && property.originalProperty.type !== void 0 && typescript.default.isTypeReferenceNode(property.originalProperty.type) && ((_property$originalPro = property.originalProperty.type.typeArguments) === null || _property$originalPro === void 0 ? void 0 : _property$originalPro.length) === 1) {
332
+ const typeArguments = property.originalProperty.type.typeArguments;
333
+ if (typeArguments && typeArguments[0]) {
334
+ const firstTypeArg = typeArguments[0];
335
+ if (typescript.default.isTypeReferenceNode(firstTypeArg)) {
336
+ const symbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
337
+ if (symbol && symbol.declarations) {
338
+ const classDeclaration = symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
339
+ if (classDeclaration && typescript.default.isClassDeclaration(classDeclaration)) property.typeLiteralClassReference = classDeclaration;
293
340
  }
294
341
  }
295
342
  }
296
- properties.push(property);
297
343
  }
344
+ properties.push(property);
298
345
  }
299
346
  return properties;
300
347
  }
301
348
  getPropertyType(property, genericTypeMap = /* @__PURE__ */ new Map()) {
302
349
  if (property.type) return this.getTypeNodeToString(property.type, genericTypeMap);
303
350
  const type = this.checker.getTypeAtLocation(property);
304
- return this.getStringFromType(type);
351
+ return this.checker.typeToString(type);
305
352
  }
306
353
  getTypeNodeToString(typeNode, genericTypeMap = /* @__PURE__ */ new Map()) {
307
- if (typescript.default.isTypeReferenceNode(typeNode) && typescript.default.isIdentifier(typeNode.typeName)) {
308
- const typeName = typeNode.typeName.text;
354
+ if (typescript.default.isTypeReferenceNode(typeNode)) {
355
+ let typeName;
356
+ if (typescript.default.isIdentifier(typeNode.typeName)) typeName = typeNode.typeName.text;
357
+ else typeName = typeNode.typeName.right.text;
309
358
  if (genericTypeMap.has(typeName)) return genericTypeMap.get(typeName);
310
359
  if (typeName.toLowerCase() === "uploadfile") return "UploadFile";
311
360
  if (typeName.toLowerCase() === "uploadfiledto") return "UploadFileDto";
312
361
  if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
362
+ if (typeName === "Array") return `${this.getTypeNodeToString(typeNode.typeArguments[0], genericTypeMap)}[]`;
313
363
  const firstTypeArg = typeNode.typeArguments[0];
314
364
  if (firstTypeArg && typescript.default.isTypeReferenceNode(firstTypeArg) && typescript.default.isIdentifier(firstTypeArg.typeName)) {
315
365
  if (firstTypeArg.typeName.text.toLowerCase() === "uploadfile") return "UploadFile";
316
366
  }
317
367
  return this.resolveGenericType(typeNode);
318
368
  }
319
- return typeNode.typeName.text;
369
+ return typeName;
320
370
  }
321
371
  switch (typeNode.kind) {
322
372
  case typescript.default.SyntaxKind.StringKeyword: return constants.jsPrimitives.String.type;
@@ -331,31 +381,31 @@ var SchemaTransformer = class SchemaTransformer {
331
381
  if (meaningfulTypes.length > 0 && meaningfulTypes[0]) return meaningfulTypes[0];
332
382
  if (types.length > 0 && types[0]) return types[0];
333
383
  return "object";
334
- default:
335
- if (typescript.default.isIndexedAccessTypeNode(typeNode)) {
336
- const resolvedType = this.checker.getTypeAtLocation(typeNode);
337
- const resolved = this.checker.typeToString(resolvedType);
338
- if (this.isPrimitiveType(resolved)) return resolved;
339
- }
340
- const typeText = typeNode.getText();
341
- if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
342
- if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
343
- if (typeText.includes("Buffer") || typeText.includes("Uint8Array")) return constants.jsPrimitives.Buffer.type;
344
- return typeText;
384
+ default: {
385
+ const resolvedType = this.checker.getTypeAtLocation(typeNode);
386
+ const resolved = this.checker.typeToString(resolvedType);
387
+ if (genericTypeMap && genericTypeMap.has(resolved)) return genericTypeMap.get(resolved);
388
+ if (this.isPrimitiveType(resolved)) return resolved;
389
+ if (resolved === "Date") return constants.jsPrimitives.Date.type;
390
+ if (resolved === "Buffer") return constants.jsPrimitives.Buffer.type;
391
+ if (resolved === "Uint8Array") return constants.jsPrimitives.Uint8Array.type;
392
+ return resolved;
393
+ }
345
394
  }
346
395
  }
347
396
  resolveGenericType(typeNode) {
348
- const typeName = typeNode.typeName.text;
397
+ let typeName;
398
+ if (typescript.default.isIdentifier(typeNode.typeName)) typeName = typeNode.typeName.text;
399
+ else typeName = typeNode.typeName.right.text;
349
400
  const typeArguments = typeNode.typeArguments;
350
401
  if (!typeArguments || typeArguments.length === 0) return typeName;
351
402
  const type = this.checker.getTypeAtLocation(typeNode);
352
- const resolvedType = this.getStringFromType(type);
353
- if (resolvedType && resolvedType !== typeName && !resolvedType.includes("any")) return resolvedType;
403
+ if (!(type.flags & typescript.default.TypeFlags.Any)) {
404
+ const resolvedType = this.checker.typeToString(type);
405
+ if (resolvedType && resolvedType !== typeName) return resolvedType;
406
+ }
354
407
  return typeName;
355
408
  }
356
- getStringFromType(type) {
357
- return this.checker.typeToString(type);
358
- }
359
409
  extractDecorators(member) {
360
410
  const decorators = [];
361
411
  if (member.modifiers) {
@@ -375,6 +425,7 @@ var SchemaTransformer = class SchemaTransformer {
375
425
  }
376
426
  getDecoratorName(callExpression) {
377
427
  if (typescript.default.isIdentifier(callExpression.expression)) return callExpression.expression.text;
428
+ if (typescript.default.isPropertyAccessExpression(callExpression.expression)) return callExpression.expression.name.text;
378
429
  return "unknown";
379
430
  }
380
431
  getDecoratorArguments(callExpression) {
@@ -387,7 +438,13 @@ var SchemaTransformer = class SchemaTransformer {
387
438
  });
388
439
  }
389
440
  getSafeDecoratorArgument(arg) {
390
- if (arg && typeof arg === "object" && "kind" in arg) return arg.getText();
441
+ if (arg && typeof arg === "object" && "kind" in arg) {
442
+ const node = arg;
443
+ const type = this.checker.getTypeAtLocation(node);
444
+ if (type.isNumberLiteral()) return type.value;
445
+ if (type.isStringLiteral()) return type.value;
446
+ return this.checker.typeToString(type);
447
+ }
391
448
  return arg;
392
449
  }
393
450
  isPropertyTypeGeneric(property) {
@@ -411,15 +468,18 @@ var SchemaTransformer = class SchemaTransformer {
411
468
  isGenericTypeFromSymbol(type) {
412
469
  if (this.isSimpleArrayType(type)) return false;
413
470
  if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) return true;
414
- if (type.typeArguments && type.typeArguments.length > 0) {
415
- const symbol = type.getSymbol();
416
- if (symbol && symbol.getName() === "Array") {
417
- const elementType = type.typeArguments[0];
418
- if (elementType) return this.isUtilityTypeFromType(elementType);
419
- return false;
471
+ if (type.flags & typescript.default.TypeFlags.Object && type.objectFlags & typescript.default.ObjectFlags.Reference) {
472
+ var _typeArgs$;
473
+ const typeArgs = this.checker.getTypeArguments(type);
474
+ if (typeArgs.length > 0 && ((_typeArgs$ = typeArgs[0]) === null || _typeArgs$ === void 0 || (_typeArgs$ = _typeArgs$.getSymbol()) === null || _typeArgs$ === void 0 ? void 0 : _typeArgs$.getName()) === "Array") {
475
+ const symbol = type.getSymbol();
476
+ if (symbol && symbol.getName() === "Array") {
477
+ const elementType = typeArgs[0];
478
+ return elementType ? this.isUtilityTypeFromType(elementType) : false;
479
+ }
480
+ const elementType = typeArgs[0];
481
+ return elementType ? this.isUtilityTypeFromType(elementType) : false;
420
482
  }
421
- const elementType = type.typeArguments[0];
422
- return this.isUtilityTypeFromType(elementType);
423
483
  }
424
484
  if (type.flags & typescript.default.TypeFlags.TypeParameter) return true;
425
485
  if (type.flags & typescript.default.TypeFlags.Conditional) return true;
@@ -451,12 +511,18 @@ var SchemaTransformer = class SchemaTransformer {
451
511
  isSimpleArrayType(type) {
452
512
  const symbol = type.getSymbol();
453
513
  if (!symbol || symbol.getName() !== "Array") return false;
454
- if (type.typeArguments && type.typeArguments.length === 1) {
455
- const elementType = type.typeArguments[0];
456
- if (!elementType) return false;
457
- if (this.isUtilityTypeFromType(elementType)) return false;
458
- if (elementType.typeArguments && elementType.typeArguments.length > 0) return false;
459
- return true;
514
+ if (type.flags & typescript.default.TypeFlags.Object && type.objectFlags & typescript.default.ObjectFlags.Reference) {
515
+ const typeArgs = this.checker.getTypeArguments(type);
516
+ if (typeArgs.length === 1) {
517
+ const elementType = typeArgs[0];
518
+ if (this.isUtilityTypeFromType(elementType)) return false;
519
+ if (elementType.flags & typescript.default.TypeFlags.Object && elementType.objectFlags & typescript.default.ObjectFlags.Reference) {
520
+ if (this.checker.getTypeArguments(elementType).length > 0) return false;
521
+ }
522
+ const elementSymbol = elementType.getSymbol();
523
+ if (elementSymbol && elementSymbol.getName() !== "Array") return false;
524
+ return true;
525
+ }
460
526
  }
461
527
  return false;
462
528
  }
@@ -513,40 +579,41 @@ var SchemaTransformer = class SchemaTransformer {
513
579
  }
514
580
  return matches[0];
515
581
  }
516
- checkTypeMatch(value, typeNode) {
517
- const runtimeType = typeof value;
518
- if (runtimeType === "string" && typeNode.kind === typescript.default.SyntaxKind.StringKeyword) return true;
519
- if (runtimeType === "number" && typeNode.kind === typescript.default.SyntaxKind.NumberKeyword) return true;
520
- if (runtimeType === "boolean" && typeNode.kind === typescript.default.SyntaxKind.BooleanKeyword) return true;
521
- if (Array.isArray(value) && typescript.default.isArrayTypeNode(typeNode)) {
522
- if (value.length === 0) return true;
523
- const firstItem = value[0];
524
- const elementType = typeNode.elementType;
525
- return this.checkTypeMatch(firstItem, elementType);
526
- }
527
- if (runtimeType === "object" && value !== null && !Array.isArray(value)) {
528
- if (typescript.default.isTypeReferenceNode(typeNode) || typeNode.kind === typescript.default.SyntaxKind.ObjectKeyword) return true;
582
+ findBestMatch(cls, matches) {
583
+ const runtimeProps = this.extractRuntimePropertyNames(cls);
584
+ let bestMatch;
585
+ let bestScore = -1;
586
+ for (const match of matches) {
587
+ let score = 0;
588
+ for (const member of match.node.members) if (typescript.default.isPropertyDeclaration(member) && member.name && (typescript.default.isIdentifier(member.name) || typescript.default.isStringLiteral(member.name))) {
589
+ if (runtimeProps.has(member.name.text)) score++;
590
+ }
591
+ if (score > bestScore) {
592
+ bestScore = score;
593
+ bestMatch = match;
594
+ }
529
595
  }
530
- return false;
596
+ return bestMatch;
531
597
  }
532
- findBestMatch(cls, matches) {
533
- let instance = {};
598
+ /**
599
+ * Safely extracts property names from a class constructor without instantiation.
600
+ * Parses the class source via Function.prototype.toString() and inspects
601
+ * the prototype for method names.
602
+ */
603
+ extractRuntimePropertyNames(cls) {
604
+ const names = /* @__PURE__ */ new Set();
534
605
  try {
535
- instance = new cls();
536
- } catch {
537
- instance = {};
538
- }
539
- const instanceProperties = Object.keys(instance);
540
- let matchesMap = {};
541
- matches.forEach((match, index) => {
542
- let fountProperties = 0;
543
- match.node.members.map((member) => {
544
- if (member.name && instanceProperties.includes(member.name.getText())) fountProperties++;
545
- });
546
- matchesMap[index] = fountProperties;
547
- });
548
- const maxMatches = Math.max(...Object.values(matchesMap));
549
- return matches[Object.values(matchesMap).findIndex((value) => value === maxMatches)];
606
+ const source = Function.prototype.toString.call(cls);
607
+ const thisAssign = /this\.(\w+)\s*=/g;
608
+ let m;
609
+ while ((m = thisAssign.exec(source)) !== null) if (m[1]) names.add(m[1]);
610
+ const classField = /[;}](\w+)(?=[=;}\s])/g;
611
+ while ((m = classField.exec(source)) !== null) if (m[1] && !constants.JS_KEYWORDS.has(m[1])) names.add(m[1]);
612
+ } catch {}
613
+ try {
614
+ for (const name of Object.getOwnPropertyNames(cls.prototype)) if (name !== "constructor") names.add(name);
615
+ } catch {}
616
+ return names;
550
617
  }
551
618
  getFilteredSourceFiles(sourceOptions) {
552
619
  if (sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.isExternal) return this.program.getSourceFiles().filter((sf) => {
@@ -561,77 +628,60 @@ var SchemaTransformer = class SchemaTransformer {
561
628
  if (!propertyDeclaration.type) return false;
562
629
  let typeNode = propertyDeclaration.type;
563
630
  if (typescript.default.isArrayTypeNode(typeNode)) typeNode = typeNode.elementType;
564
- if (typescript.default.isTypeReferenceNode(typeNode)) {
565
- const type = this.checker.getTypeAtLocation(typeNode);
566
- return !!(type.flags & typescript.default.TypeFlags.Enum) || !!(type.flags & typescript.default.TypeFlags.EnumLiteral);
631
+ if (typescript.default.isUnionTypeNode(typeNode)) {
632
+ const nonNullTypes = typeNode.types.filter((t) => t.kind !== typescript.default.SyntaxKind.NullKeyword && t.kind !== typescript.default.SyntaxKind.UndefinedKeyword);
633
+ if (nonNullTypes.length === 1 && nonNullTypes[0]) typeNode = nonNullTypes[0];
567
634
  }
635
+ if (typescript.default.isTypeReferenceNode(typeNode)) return !!(this.checker.getTypeAtLocation(typeNode).flags & typescript.default.TypeFlags.EnumLike);
568
636
  return false;
569
637
  }
570
- isClassType(propertyDeclaration) {
571
- if (!propertyDeclaration.type) return false;
572
- if (this.isArrayProperty(propertyDeclaration)) {
573
- const elementType = propertyDeclaration.type.elementType;
574
- if (typescript.default.isTypeReferenceNode(elementType) && elementType.typeArguments && elementType.typeArguments.length > 0) {
575
- const firstTypeArg = elementType.typeArguments[0];
576
- if (firstTypeArg) {
577
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
578
- if (argSymbol && argSymbol.declarations) {
579
- if (argSymbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl))) return true;
580
- }
581
- }
582
- }
583
- const symbol = this.checker.getTypeAtLocation(elementType).getSymbol();
584
- if (symbol && symbol.declarations) return symbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl));
585
- return false;
586
- } else {
587
- if (typescript.default.isTypeReferenceNode(propertyDeclaration.type) && propertyDeclaration.type.typeArguments && propertyDeclaration.type.typeArguments.length > 0) {
588
- const firstTypeArg = propertyDeclaration.type.typeArguments[0];
589
- if (firstTypeArg) {
590
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
591
- if (argSymbol && argSymbol.declarations) {
592
- if (argSymbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl))) return true;
593
- }
594
- }
595
- }
596
- const symbol = this.checker.getTypeAtLocation(propertyDeclaration.type).getSymbol();
597
- if (symbol && symbol.declarations) return symbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl));
598
- return false;
599
- }
600
- }
601
- getDeclarationProperty(property) {
602
- if (!property.originalProperty.type) return;
603
- if (typescript.default.isArrayTypeNode(property.originalProperty.type)) {
604
- const elementType = property.originalProperty.type.elementType;
605
- if (typescript.default.isTypeReferenceNode(elementType) && elementType.typeArguments && elementType.typeArguments.length > 0) {
606
- const firstTypeArg = elementType.typeArguments[0];
607
- if (firstTypeArg) {
608
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
609
- if (argSymbol && argSymbol.declarations) {
610
- const classDecl = argSymbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
611
- if (classDecl) return classDecl;
612
- }
613
- }
614
- }
615
- const symbol = this.checker.getTypeAtLocation(elementType).getSymbol();
616
- if (symbol && symbol.declarations) return symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl)) || symbol.declarations[0];
617
- return;
618
- }
619
- if (typescript.default.isTypeReferenceNode(property.originalProperty.type) && property.originalProperty.type.typeArguments && property.originalProperty.type.typeArguments.length > 0) {
620
- const firstTypeArg = property.originalProperty.type.typeArguments[0];
638
+ /**
639
+ * Resolves a type node to its underlying symbol via the type-checker.
640
+ * For type references with type arguments (e.g., PayloadEntity<Person>),
641
+ * it checks the first type argument for a class declaration first.
642
+ * Returns the ts.Symbol or undefined.
643
+ */
644
+ resolveClassSymbolFromTypeNode(typeNode) {
645
+ if (typescript.default.isTypeReferenceNode(typeNode) && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
646
+ const firstTypeArg = typeNode.typeArguments[0];
621
647
  if (firstTypeArg) {
622
648
  const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
623
649
  if (argSymbol && argSymbol.declarations) {
624
- const classDecl = argSymbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
625
- if (classDecl) return classDecl;
650
+ if (argSymbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl))) return argSymbol;
626
651
  }
627
652
  }
628
653
  }
629
- const symbol = this.checker.getTypeAtLocation(property.originalProperty.type).getSymbol();
630
- if (symbol && symbol.declarations) return symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl)) || symbol.declarations[0];
654
+ return this.checker.getTypeAtLocation(typeNode).getSymbol() ?? void 0;
655
+ }
656
+ /**
657
+ * Resolves the element type node from an array property declaration.
658
+ * Handles both `T[]` and `Array<T>` syntax.
659
+ */
660
+ resolveArrayElementTypeNode(propertyDeclaration) {
661
+ var _propertyDeclaration$;
662
+ if (!propertyDeclaration.type) return void 0;
663
+ if (typescript.default.isArrayTypeNode(propertyDeclaration.type)) return propertyDeclaration.type.elementType;
664
+ if (typescript.default.isTypeReferenceNode(propertyDeclaration.type) && ((_propertyDeclaration$ = propertyDeclaration.type.typeArguments) === null || _propertyDeclaration$ === void 0 ? void 0 : _propertyDeclaration$[0])) return propertyDeclaration.type.typeArguments[0];
665
+ }
666
+ isClassType(propertyDeclaration) {
667
+ if (!propertyDeclaration.type) return false;
668
+ const typeNode = this.isArrayProperty(propertyDeclaration) ? this.resolveArrayElementTypeNode(propertyDeclaration) : propertyDeclaration.type;
669
+ if (!typeNode) return false;
670
+ const symbol = this.resolveClassSymbolFromTypeNode(typeNode);
671
+ if (symbol && symbol.declarations) return symbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl));
672
+ return false;
673
+ }
674
+ getDeclarationProperty(property) {
675
+ if (!property.originalProperty.type) return;
676
+ const typeNode = typescript.default.isArrayTypeNode(property.originalProperty.type) ? property.originalProperty.type.elementType : property.originalProperty.type;
677
+ const symbol = this.resolveClassSymbolFromTypeNode(typeNode);
678
+ if (symbol && symbol.declarations) return symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl)) ?? symbol.declarations[0];
631
679
  }
632
680
  isArrayProperty(propertyDeclaration) {
633
681
  if (!propertyDeclaration.type) return false;
634
- return typescript.default.isArrayTypeNode(propertyDeclaration.type);
682
+ if (typescript.default.isArrayTypeNode(propertyDeclaration.type)) return true;
683
+ if (typescript.default.isTypeReferenceNode(propertyDeclaration.type) && typescript.default.isIdentifier(propertyDeclaration.type.typeName) && propertyDeclaration.type.typeName.text === "Array") return true;
684
+ return false;
635
685
  }
636
686
  getSchemaFromProperties({ properties, visitedClass, transformedSchema, classDeclaration }) {
637
687
  let schema = {};
@@ -666,11 +716,34 @@ var SchemaTransformer = class SchemaTransformer {
666
716
  visitedClass,
667
717
  transformedSchema
668
718
  });
669
- else schema = {
670
- type: "object",
671
- properties: {},
672
- additionalProperties: true
673
- };
719
+ else if (property.isGeneric) if (property.genericClassReference) schema = this.getSchemaFromClass({
720
+ isArray: property.isArray,
721
+ visitedClass,
722
+ transformedSchema,
723
+ declaration: property.genericClassReference
724
+ });
725
+ else {
726
+ const inner = {
727
+ type: "object",
728
+ properties: {},
729
+ additionalProperties: true
730
+ };
731
+ schema = property.isArray ? {
732
+ type: "array",
733
+ items: inner
734
+ } : inner;
735
+ }
736
+ else {
737
+ const inner = {
738
+ type: "object",
739
+ properties: {},
740
+ additionalProperties: true
741
+ };
742
+ schema = property.isArray ? {
743
+ type: "array",
744
+ items: inner
745
+ } : inner;
746
+ }
674
747
  this.applyDecorators(property, schema);
675
748
  return schema;
676
749
  }
@@ -766,7 +839,12 @@ var SchemaTransformer = class SchemaTransformer {
766
839
  if (enumSchema) return enumSchema;
767
840
  }
768
841
  const propertySchema = { type: "object" };
769
- const propertyType = property.type.toLowerCase().replace("[]", "").trim();
842
+ let baseTypeNode = property.originalProperty.type;
843
+ if (property.isArray && baseTypeNode) baseTypeNode = this.resolveArrayElementTypeNode(property.originalProperty) ?? baseTypeNode;
844
+ const resolvedType = this.checker.getTypeAtLocation(baseTypeNode ?? property.originalProperty);
845
+ let propertyType;
846
+ if (resolvedType.flags & typescript.default.TypeFlags.TypeParameter) propertyType = property.type.toLowerCase().replace(/\[\]$/, "").trim();
847
+ else propertyType = this.checker.typeToString(resolvedType).toLowerCase();
770
848
  let isFile = false;
771
849
  switch (propertyType) {
772
850
  case constants.jsPrimitives.String.value:
@@ -803,17 +881,26 @@ var SchemaTransformer = class SchemaTransformer {
803
881
  propertySchema.type = constants.jsPrimitives.Symbol.value;
804
882
  break;
805
883
  case constants.jsPrimitives.Object.value:
884
+ case "unknown":
885
+ case "any":
806
886
  propertySchema.type = constants.jsPrimitives.Object.value;
887
+ propertySchema.additionalProperties = true;
807
888
  break;
808
889
  default: propertySchema.type = constants.jsPrimitives.String.value;
809
890
  }
810
891
  if (property.isArray) {
811
892
  delete propertySchema.format;
812
- propertySchema.type = `array`;
813
- propertySchema.items = {
814
- type: isFile ? constants.jsPrimitives.UploadFile.value : propertyType,
893
+ const resolvedItemType = propertySchema.type;
894
+ const itemSchema = {
895
+ type: isFile ? constants.jsPrimitives.UploadFile.value : resolvedItemType,
815
896
  format: isFile ? constants.jsPrimitives.UploadFile.format : propertySchema.format
816
897
  };
898
+ if (propertySchema.additionalProperties) {
899
+ itemSchema.additionalProperties = true;
900
+ delete propertySchema.additionalProperties;
901
+ }
902
+ propertySchema.type = `array`;
903
+ propertySchema.items = itemSchema;
817
904
  }
818
905
  return propertySchema;
819
906
  }
@@ -986,12 +1073,58 @@ var SchemaTransformer = class SchemaTransformer {
986
1073
  break;
987
1074
  }
988
1075
  }
1076
+ /**
1077
+ * Scans all non-declaration source files in the program once and records
1078
+ * every call of the form `transform(Foo<Bar, Baz>)`, keyed by class name.
1079
+ * This means generic resolution in transform() is a pure O(1) Map lookup
1080
+ * with no runtime stack inspection.
1081
+ */
1082
+ buildTransformCallIndex() {
1083
+ this.program.getSourceFiles().forEach((sf) => {
1084
+ if (sf.isDeclarationFile) return;
1085
+ sf.statements.forEach((stmt) => {
1086
+ if (typescript.default.isClassDeclaration(stmt) && stmt.name) {
1087
+ const name = stmt.name.text;
1088
+ const entry = this.classFileIndex.get(name) || [];
1089
+ entry.push({
1090
+ sourceFile: sf,
1091
+ node: stmt
1092
+ });
1093
+ this.classFileIndex.set(name, entry);
1094
+ }
1095
+ });
1096
+ const visit = (node) => {
1097
+ if (typescript.default.isCallExpression(node) && node.arguments.length > 0) {
1098
+ const callee = node.expression;
1099
+ if (typescript.default.isIdentifier(callee) && callee.text === "transform" || typescript.default.isPropertyAccessExpression(callee) && callee.name.text === "transform") {
1100
+ const firstArg = node.arguments[0];
1101
+ if (typescript.default.isExpressionWithTypeArguments(firstArg) && firstArg.typeArguments && firstArg.typeArguments.length > 0) {
1102
+ const baseExpr = firstArg.expression;
1103
+ const className = typescript.default.isIdentifier(baseExpr) ? baseExpr.text : typescript.default.isPropertyAccessExpression(baseExpr) ? baseExpr.name.text : void 0;
1104
+ if (className) {
1105
+ var _this$classFileIndex$;
1106
+ const classNode = (_this$classFileIndex$ = this.classFileIndex.get(className)) === null || _this$classFileIndex$ === void 0 || (_this$classFileIndex$ = _this$classFileIndex$[0]) === null || _this$classFileIndex$ === void 0 ? void 0 : _this$classFileIndex$.node;
1107
+ if (classNode === null || classNode === void 0 ? void 0 : classNode.typeParameters) {
1108
+ const typeMap = /* @__PURE__ */ new Map();
1109
+ classNode.typeParameters.forEach((param, i) => {
1110
+ const typeArg = firstArg.typeArguments[i];
1111
+ if (typeArg) typeMap.set(param.name.text, this.getTypeNodeToString(typeArg, /* @__PURE__ */ new Map()));
1112
+ });
1113
+ if (typeMap.size > 0) this.transformCallIndex.set(className, typeMap);
1114
+ }
1115
+ }
1116
+ } else {
1117
+ const className = typescript.default.isIdentifier(firstArg) ? firstArg.text : typescript.default.isPropertyAccessExpression(firstArg) ? firstArg.name.text : void 0;
1118
+ if (className) this.nonGenericTransformCalls.add(className);
1119
+ }
1120
+ }
1121
+ }
1122
+ typescript.default.forEachChild(node, visit);
1123
+ };
1124
+ visit(sf);
1125
+ });
1126
+ }
989
1127
  transform(cls, sourceOptions) {
990
- if (this.classCache.has(cls)) return this.classCache.get(cls);
991
- let schema = {
992
- type: "object",
993
- properties: {}
994
- };
995
1128
  const result = this.getSourceFileByClass(cls, sourceOptions);
996
1129
  if (!result || !(result === null || result === void 0 ? void 0 : result.sourceFile)) {
997
1130
  console.warn(`Class ${cls.name} not found in any source file.`);
@@ -1005,11 +1138,29 @@ var SchemaTransformer = class SchemaTransformer {
1005
1138
  }
1006
1139
  };
1007
1140
  }
1008
- const properties = this.getPropertiesByClassDeclaration(result.node);
1141
+ const genericTypeMap = /* @__PURE__ */ new Map();
1142
+ if (result.node.typeParameters) {
1143
+ const indexedTypeMap = this.transformCallIndex.get(cls.name);
1144
+ const hasNonGenericCall = this.nonGenericTransformCalls.has(cls.name);
1145
+ const allHaveDefaults = result.node.typeParameters.every((p) => !!p.default);
1146
+ if (indexedTypeMap && !(hasNonGenericCall && allHaveDefaults)) for (const [key, value] of indexedTypeMap) genericTypeMap.set(key, value);
1147
+ else for (const param of result.node.typeParameters) if (param.default) genericTypeMap.set(param.name.text, this.getTypeNodeToString(param.default, /* @__PURE__ */ new Map()));
1148
+ }
1149
+ const hasGenericArgs = genericTypeMap.size > 0;
1150
+ if (!hasGenericArgs && this.classCache.has(cls)) return this.classCache.get(cls);
1151
+ let schema = {
1152
+ type: "object",
1153
+ properties: {}
1154
+ };
1155
+ const properties = this.getPropertiesByClassDeclaration(result.node, void 0, genericTypeMap);
1009
1156
  schema = this.getSchemaFromProperties({
1010
1157
  properties,
1011
1158
  classDeclaration: result.node
1012
1159
  });
1160
+ if (!hasGenericArgs) this.classCache.set(cls, {
1161
+ name: cls.name,
1162
+ schema
1163
+ });
1013
1164
  return {
1014
1165
  name: cls.name,
1015
1166
  schema