ts-class-to-openapi 1.4.1 → 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 +284 -192
  2. package/dist/index.mjs +284 -192
  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",
@@ -190,6 +237,7 @@ var SchemaTransformer = class SchemaTransformer {
190
237
  autoCleanup;
191
238
  classFileIndex = /* @__PURE__ */ new Map();
192
239
  transformCallIndex = /* @__PURE__ */ new Map();
240
+ nonGenericTransformCalls = /* @__PURE__ */ new Set();
193
241
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
194
242
  this.maxCacheSize = options.maxCacheSize ?? 100;
195
243
  this.autoCleanup = options.autoCleanup ?? true;
@@ -236,78 +284,77 @@ var SchemaTransformer = class SchemaTransformer {
236
284
  }
237
285
  getPropertiesByClassMembers(members, parentClassNode, genericTypeMap = /* @__PURE__ */ new Map()) {
238
286
  const properties = [];
239
- for (const member of members) {
240
- var _member$name;
241
- if (typescript.default.isPropertyDeclaration(member) && member.name && ((_member$name = member.name) === null || _member$name === void 0 ? void 0 : _member$name.text)) {
242
- var _typeArguments;
243
- if (member.modifiers) {
244
- if (member.modifiers.some((m) => m.kind === typescript.default.SyntaxKind.PrivateKeyword || m.kind === typescript.default.SyntaxKind.ProtectedKeyword)) continue;
245
- }
246
- const propertyName = member.name.text;
247
- const type = this.getPropertyType(member, genericTypeMap);
248
- const decorators = this.extractDecorators(member);
249
- const isOptional = !!member.questionToken;
250
- const isGeneric = this.isPropertyTypeGeneric(member);
251
- const isEnum = this.isEnum(member);
252
- const isPrimitive = this.isPrimitiveType(type) || isEnum;
253
- const isClassType = this.isClassType(member);
254
- const isArray = this.isArrayProperty(member);
255
- const isTypeLiteral = this.isTypeLiteral(member);
256
- let genericClassReference = void 0;
257
- if (isGeneric && !isPrimitive) {
258
- const baseTypeName = type.replace(/\[\]$/, "").trim();
259
- if (!this.isPrimitiveType(baseTypeName)) {
260
- const matches = this.classFileIndex.get(baseTypeName);
261
- if (matches && matches.length > 0 && matches[0]) genericClassReference = matches[0].node;
262
- }
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;
263
308
  }
264
- const property = {
265
- name: propertyName,
266
- type,
267
- decorators,
268
- isOptional,
269
- isGeneric,
270
- originalProperty: member,
271
- isPrimitive,
272
- isClassType,
273
- isArray,
274
- isEnum,
275
- isRef: false,
276
- isTypeLiteral,
277
- genericClassReference
278
- };
279
- if (property.isClassType) {
280
- const declaration = this.getDeclarationProperty(property);
281
- if (parentClassNode) {
282
- if (declaration && declaration.name && this.checker.getSymbolAtLocation(declaration.name) === this.checker.getSymbolAtLocation(parentClassNode.name)) property.isRef = true;
283
- }
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;
284
329
  }
285
- if (property.isTypeLiteral && property.originalProperty.type && ((_typeArguments = property.originalProperty.type.typeArguments) === null || _typeArguments === void 0 ? void 0 : _typeArguments.length) === 1) {
286
- const typeArguments = property.originalProperty.type.typeArguments;
287
- if (typeArguments && typeArguments[0]) {
288
- const firstTypeArg = typeArguments[0];
289
- if (typescript.default.isTypeReferenceNode(firstTypeArg)) {
290
- const symbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
291
- if (symbol && symbol.declarations) {
292
- const classDeclaration = symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
293
- if (classDeclaration && typescript.default.isClassDeclaration(classDeclaration)) property.typeLiteralClassReference = classDeclaration;
294
- }
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;
295
340
  }
296
341
  }
297
342
  }
298
- properties.push(property);
299
343
  }
344
+ properties.push(property);
300
345
  }
301
346
  return properties;
302
347
  }
303
348
  getPropertyType(property, genericTypeMap = /* @__PURE__ */ new Map()) {
304
349
  if (property.type) return this.getTypeNodeToString(property.type, genericTypeMap);
305
350
  const type = this.checker.getTypeAtLocation(property);
306
- return this.getStringFromType(type);
351
+ return this.checker.typeToString(type);
307
352
  }
308
353
  getTypeNodeToString(typeNode, genericTypeMap = /* @__PURE__ */ new Map()) {
309
- if (typescript.default.isTypeReferenceNode(typeNode) && typescript.default.isIdentifier(typeNode.typeName)) {
310
- 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;
311
358
  if (genericTypeMap.has(typeName)) return genericTypeMap.get(typeName);
312
359
  if (typeName.toLowerCase() === "uploadfile") return "UploadFile";
313
360
  if (typeName.toLowerCase() === "uploadfiledto") return "UploadFileDto";
@@ -319,7 +366,7 @@ var SchemaTransformer = class SchemaTransformer {
319
366
  }
320
367
  return this.resolveGenericType(typeNode);
321
368
  }
322
- return typeNode.typeName.text;
369
+ return typeName;
323
370
  }
324
371
  switch (typeNode.kind) {
325
372
  case typescript.default.SyntaxKind.StringKeyword: return constants.jsPrimitives.String.type;
@@ -334,31 +381,31 @@ var SchemaTransformer = class SchemaTransformer {
334
381
  if (meaningfulTypes.length > 0 && meaningfulTypes[0]) return meaningfulTypes[0];
335
382
  if (types.length > 0 && types[0]) return types[0];
336
383
  return "object";
337
- default:
338
- if (typescript.default.isIndexedAccessTypeNode(typeNode)) {
339
- const resolvedType = this.checker.getTypeAtLocation(typeNode);
340
- const resolved = this.checker.typeToString(resolvedType);
341
- if (this.isPrimitiveType(resolved)) return resolved;
342
- }
343
- const typeText = typeNode.getText();
344
- if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
345
- if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
346
- if (typeText.includes("Buffer") || typeText.includes("Uint8Array")) return constants.jsPrimitives.Buffer.type;
347
- 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
+ }
348
394
  }
349
395
  }
350
396
  resolveGenericType(typeNode) {
351
- 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;
352
400
  const typeArguments = typeNode.typeArguments;
353
401
  if (!typeArguments || typeArguments.length === 0) return typeName;
354
402
  const type = this.checker.getTypeAtLocation(typeNode);
355
- const resolvedType = this.getStringFromType(type);
356
- 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
+ }
357
407
  return typeName;
358
408
  }
359
- getStringFromType(type) {
360
- return this.checker.typeToString(type);
361
- }
362
409
  extractDecorators(member) {
363
410
  const decorators = [];
364
411
  if (member.modifiers) {
@@ -378,6 +425,7 @@ var SchemaTransformer = class SchemaTransformer {
378
425
  }
379
426
  getDecoratorName(callExpression) {
380
427
  if (typescript.default.isIdentifier(callExpression.expression)) return callExpression.expression.text;
428
+ if (typescript.default.isPropertyAccessExpression(callExpression.expression)) return callExpression.expression.name.text;
381
429
  return "unknown";
382
430
  }
383
431
  getDecoratorArguments(callExpression) {
@@ -390,7 +438,13 @@ var SchemaTransformer = class SchemaTransformer {
390
438
  });
391
439
  }
392
440
  getSafeDecoratorArgument(arg) {
393
- 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
+ }
394
448
  return arg;
395
449
  }
396
450
  isPropertyTypeGeneric(property) {
@@ -414,15 +468,18 @@ var SchemaTransformer = class SchemaTransformer {
414
468
  isGenericTypeFromSymbol(type) {
415
469
  if (this.isSimpleArrayType(type)) return false;
416
470
  if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) return true;
417
- if (type.typeArguments && type.typeArguments.length > 0 && type.typeArguments[0].symbol.getName() === "Array") {
418
- const symbol = type.getSymbol();
419
- if (symbol && symbol.getName() === "Array") {
420
- const elementType = type.typeArguments[0];
421
- if (elementType) return this.isUtilityTypeFromType(elementType);
422
- 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;
423
482
  }
424
- const elementType = type.typeArguments[0];
425
- return this.isUtilityTypeFromType(elementType);
426
483
  }
427
484
  if (type.flags & typescript.default.TypeFlags.TypeParameter) return true;
428
485
  if (type.flags & typescript.default.TypeFlags.Conditional) return true;
@@ -454,13 +511,18 @@ var SchemaTransformer = class SchemaTransformer {
454
511
  isSimpleArrayType(type) {
455
512
  const symbol = type.getSymbol();
456
513
  if (!symbol || symbol.getName() !== "Array") return false;
457
- if (type.typeArguments && type.typeArguments.length === 1) {
458
- const elementType = type.typeArguments[0];
459
- if (!elementType) return false;
460
- if (this.isUtilityTypeFromType(elementType)) return false;
461
- if (elementType.typeArguments && elementType.typeArguments.length > 0) return false;
462
- if (type.typeArguments && type.typeArguments[0].symbol && type.typeArguments[0].symbol.getName() !== "Array") return false;
463
- 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
+ }
464
526
  }
465
527
  return false;
466
528
  }
@@ -518,23 +580,40 @@ var SchemaTransformer = class SchemaTransformer {
518
580
  return matches[0];
519
581
  }
520
582
  findBestMatch(cls, matches) {
521
- let instance = {};
522
- try {
523
- instance = new cls();
524
- } catch {
525
- instance = {};
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
+ }
526
595
  }
527
- const instanceProperties = Object.keys(instance);
528
- let matchesMap = {};
529
- matches.forEach((match, index) => {
530
- let fountProperties = 0;
531
- match.node.members.map((member) => {
532
- if (member.name && instanceProperties.includes(member.name.getText())) fountProperties++;
533
- });
534
- matchesMap[index] = fountProperties;
535
- });
536
- const maxMatches = Math.max(...Object.values(matchesMap));
537
- return matches[Object.values(matchesMap).findIndex((value) => value === maxMatches)];
596
+ return bestMatch;
597
+ }
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();
605
+ try {
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;
538
617
  }
539
618
  getFilteredSourceFiles(sourceOptions) {
540
619
  if (sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.isExternal) return this.program.getSourceFiles().filter((sf) => {
@@ -549,77 +628,54 @@ var SchemaTransformer = class SchemaTransformer {
549
628
  if (!propertyDeclaration.type) return false;
550
629
  let typeNode = propertyDeclaration.type;
551
630
  if (typescript.default.isArrayTypeNode(typeNode)) typeNode = typeNode.elementType;
552
- if (typescript.default.isTypeReferenceNode(typeNode)) {
553
- const type = this.checker.getTypeAtLocation(typeNode);
554
- 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];
555
634
  }
635
+ if (typescript.default.isTypeReferenceNode(typeNode)) return !!(this.checker.getTypeAtLocation(typeNode).flags & typescript.default.TypeFlags.EnumLike);
556
636
  return false;
557
637
  }
558
- isClassType(propertyDeclaration) {
559
- if (!propertyDeclaration.type) return false;
560
- if (this.isArrayProperty(propertyDeclaration)) {
561
- var _propertyDeclaration$;
562
- let elementType;
563
- if (typescript.default.isArrayTypeNode(propertyDeclaration.type)) elementType = propertyDeclaration.type.elementType;
564
- else if (typescript.default.isTypeReferenceNode(propertyDeclaration.type) && ((_propertyDeclaration$ = propertyDeclaration.type.typeArguments) === null || _propertyDeclaration$ === void 0 ? void 0 : _propertyDeclaration$[0])) elementType = propertyDeclaration.type.typeArguments[0];
565
- if (!elementType) return false;
566
- if (typescript.default.isTypeReferenceNode(elementType) && elementType.typeArguments && elementType.typeArguments.length > 0) {
567
- const firstTypeArg = elementType.typeArguments[0];
568
- if (firstTypeArg) {
569
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
570
- if (argSymbol && argSymbol.declarations) {
571
- if (argSymbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl))) return true;
572
- }
573
- }
574
- }
575
- const symbol = this.checker.getTypeAtLocation(elementType).getSymbol();
576
- if (symbol && symbol.declarations) return symbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl));
577
- return false;
578
- } else {
579
- if (typescript.default.isTypeReferenceNode(propertyDeclaration.type) && propertyDeclaration.type.typeArguments && propertyDeclaration.type.typeArguments.length > 0) {
580
- const firstTypeArg = propertyDeclaration.type.typeArguments[0];
581
- if (firstTypeArg) {
582
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
583
- if (argSymbol && argSymbol.declarations) {
584
- if (argSymbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl))) return true;
585
- }
586
- }
587
- }
588
- const symbol = this.checker.getTypeAtLocation(propertyDeclaration.type).getSymbol();
589
- if (symbol && symbol.declarations) return symbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl));
590
- return false;
591
- }
592
- }
593
- getDeclarationProperty(property) {
594
- if (!property.originalProperty.type) return;
595
- if (typescript.default.isArrayTypeNode(property.originalProperty.type)) {
596
- const elementType = property.originalProperty.type.elementType;
597
- if (typescript.default.isTypeReferenceNode(elementType) && elementType.typeArguments && elementType.typeArguments.length > 0) {
598
- const firstTypeArg = elementType.typeArguments[0];
599
- if (firstTypeArg) {
600
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
601
- if (argSymbol && argSymbol.declarations) {
602
- const classDecl = argSymbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
603
- if (classDecl) return classDecl;
604
- }
605
- }
606
- }
607
- const symbol = this.checker.getTypeAtLocation(elementType).getSymbol();
608
- if (symbol && symbol.declarations) return symbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl)) || symbol.declarations[0];
609
- return;
610
- }
611
- if (typescript.default.isTypeReferenceNode(property.originalProperty.type) && property.originalProperty.type.typeArguments && property.originalProperty.type.typeArguments.length > 0) {
612
- 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];
613
647
  if (firstTypeArg) {
614
648
  const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
615
649
  if (argSymbol && argSymbol.declarations) {
616
- const classDecl = argSymbol.declarations.find((decl) => typescript.default.isClassDeclaration(decl));
617
- if (classDecl) return classDecl;
650
+ if (argSymbol.declarations.some((decl) => typescript.default.isClassDeclaration(decl))) return argSymbol;
618
651
  }
619
652
  }
620
653
  }
621
- const symbol = this.checker.getTypeAtLocation(property.originalProperty.type).getSymbol();
622
- 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];
623
679
  }
624
680
  isArrayProperty(propertyDeclaration) {
625
681
  if (!propertyDeclaration.type) return false;
@@ -666,16 +722,28 @@ var SchemaTransformer = class SchemaTransformer {
666
722
  transformedSchema,
667
723
  declaration: property.genericClassReference
668
724
  });
669
- else schema = {
670
- type: "object",
671
- properties: {},
672
- additionalProperties: true
673
- };
674
- else schema = {
675
- type: "object",
676
- properties: {},
677
- additionalProperties: true
678
- };
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
+ }
679
747
  this.applyDecorators(property, schema);
680
748
  return schema;
681
749
  }
@@ -771,7 +839,12 @@ var SchemaTransformer = class SchemaTransformer {
771
839
  if (enumSchema) return enumSchema;
772
840
  }
773
841
  const propertySchema = { type: "object" };
774
- 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();
775
848
  let isFile = false;
776
849
  switch (propertyType) {
777
850
  case constants.jsPrimitives.String.value:
@@ -808,17 +881,26 @@ var SchemaTransformer = class SchemaTransformer {
808
881
  propertySchema.type = constants.jsPrimitives.Symbol.value;
809
882
  break;
810
883
  case constants.jsPrimitives.Object.value:
884
+ case "unknown":
885
+ case "any":
811
886
  propertySchema.type = constants.jsPrimitives.Object.value;
887
+ propertySchema.additionalProperties = true;
812
888
  break;
813
889
  default: propertySchema.type = constants.jsPrimitives.String.value;
814
890
  }
815
891
  if (property.isArray) {
816
892
  delete propertySchema.format;
817
- propertySchema.type = `array`;
818
- propertySchema.items = {
819
- type: isFile ? constants.jsPrimitives.UploadFile.value : propertyType,
893
+ const resolvedItemType = propertySchema.type;
894
+ const itemSchema = {
895
+ type: isFile ? constants.jsPrimitives.UploadFile.value : resolvedItemType,
820
896
  format: isFile ? constants.jsPrimitives.UploadFile.format : propertySchema.format
821
897
  };
898
+ if (propertySchema.additionalProperties) {
899
+ itemSchema.additionalProperties = true;
900
+ delete propertySchema.additionalProperties;
901
+ }
902
+ propertySchema.type = `array`;
903
+ propertySchema.items = itemSchema;
822
904
  }
823
905
  return propertySchema;
824
906
  }
@@ -1016,21 +1098,24 @@ var SchemaTransformer = class SchemaTransformer {
1016
1098
  const callee = node.expression;
1017
1099
  if (typescript.default.isIdentifier(callee) && callee.text === "transform" || typescript.default.isPropertyAccessExpression(callee) && callee.name.text === "transform") {
1018
1100
  const firstArg = node.arguments[0];
1019
- const typeArgs = firstArg.typeArguments;
1020
- if (typeArgs && typeArgs.length > 0) {
1021
- const baseExpr = firstArg.expression ?? firstArg;
1022
- if (typescript.default.isIdentifier(baseExpr)) {
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) {
1023
1105
  var _this$classFileIndex$;
1024
- const classNode = (_this$classFileIndex$ = this.classFileIndex.get(baseExpr.text)) === null || _this$classFileIndex$ === void 0 || (_this$classFileIndex$ = _this$classFileIndex$[0]) === null || _this$classFileIndex$ === void 0 ? void 0 : _this$classFileIndex$.node;
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;
1025
1107
  if (classNode === null || classNode === void 0 ? void 0 : classNode.typeParameters) {
1026
1108
  const typeMap = /* @__PURE__ */ new Map();
1027
1109
  classNode.typeParameters.forEach((param, i) => {
1028
- const typeArg = typeArgs[i];
1110
+ const typeArg = firstArg.typeArguments[i];
1029
1111
  if (typeArg) typeMap.set(param.name.text, this.getTypeNodeToString(typeArg, /* @__PURE__ */ new Map()));
1030
1112
  });
1031
- if (typeMap.size > 0) this.transformCallIndex.set(baseExpr.text, typeMap);
1113
+ if (typeMap.size > 0) this.transformCallIndex.set(className, typeMap);
1032
1114
  }
1033
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);
1034
1119
  }
1035
1120
  }
1036
1121
  }
@@ -1053,7 +1138,14 @@ var SchemaTransformer = class SchemaTransformer {
1053
1138
  }
1054
1139
  };
1055
1140
  }
1056
- const genericTypeMap = this.transformCallIndex.get(cls.name) ?? /* @__PURE__ */ new Map();
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
+ }
1057
1149
  const hasGenericArgs = genericTypeMap.size > 0;
1058
1150
  if (!hasGenericArgs && this.classCache.has(cls)) return this.classCache.get(cls);
1059
1151
  let schema = {
package/dist/index.mjs CHANGED
@@ -3,6 +3,53 @@ import path from "path";
3
3
  //#region src/transformer.fixtures.ts
4
4
  const constants = {
5
5
  TS_CONFIG_DEFAULT_PATH: path.resolve(process.cwd(), "tsconfig.json"),
6
+ JS_KEYWORDS: new Set([
7
+ "constructor",
8
+ "static",
9
+ "get",
10
+ "set",
11
+ "async",
12
+ "return",
13
+ "if",
14
+ "else",
15
+ "for",
16
+ "while",
17
+ "switch",
18
+ "case",
19
+ "break",
20
+ "throw",
21
+ "new",
22
+ "delete",
23
+ "typeof",
24
+ "void",
25
+ "try",
26
+ "catch",
27
+ "finally",
28
+ "this",
29
+ "super",
30
+ "class",
31
+ "const",
32
+ "let",
33
+ "var",
34
+ "function",
35
+ "yield",
36
+ "await",
37
+ "import",
38
+ "export",
39
+ "default",
40
+ "extends",
41
+ "implements",
42
+ "in",
43
+ "of",
44
+ "do",
45
+ "with",
46
+ "continue",
47
+ "instanceof",
48
+ "true",
49
+ "false",
50
+ "null",
51
+ "undefined"
52
+ ]),
6
53
  jsPrimitives: {
7
54
  String: {
8
55
  type: "String",
@@ -165,6 +212,7 @@ var SchemaTransformer = class SchemaTransformer {
165
212
  autoCleanup;
166
213
  classFileIndex = /* @__PURE__ */ new Map();
167
214
  transformCallIndex = /* @__PURE__ */ new Map();
215
+ nonGenericTransformCalls = /* @__PURE__ */ new Set();
168
216
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
169
217
  this.maxCacheSize = options.maxCacheSize ?? 100;
170
218
  this.autoCleanup = options.autoCleanup ?? true;
@@ -211,78 +259,77 @@ var SchemaTransformer = class SchemaTransformer {
211
259
  }
212
260
  getPropertiesByClassMembers(members, parentClassNode, genericTypeMap = /* @__PURE__ */ new Map()) {
213
261
  const properties = [];
214
- for (const member of members) {
215
- var _member$name;
216
- if (ts.isPropertyDeclaration(member) && member.name && ((_member$name = member.name) === null || _member$name === void 0 ? void 0 : _member$name.text)) {
217
- var _typeArguments;
218
- if (member.modifiers) {
219
- if (member.modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword)) continue;
220
- }
221
- const propertyName = member.name.text;
222
- const type = this.getPropertyType(member, genericTypeMap);
223
- const decorators = this.extractDecorators(member);
224
- const isOptional = !!member.questionToken;
225
- const isGeneric = this.isPropertyTypeGeneric(member);
226
- const isEnum = this.isEnum(member);
227
- const isPrimitive = this.isPrimitiveType(type) || isEnum;
228
- const isClassType = this.isClassType(member);
229
- const isArray = this.isArrayProperty(member);
230
- const isTypeLiteral = this.isTypeLiteral(member);
231
- let genericClassReference = void 0;
232
- if (isGeneric && !isPrimitive) {
233
- const baseTypeName = type.replace(/\[\]$/, "").trim();
234
- if (!this.isPrimitiveType(baseTypeName)) {
235
- const matches = this.classFileIndex.get(baseTypeName);
236
- if (matches && matches.length > 0 && matches[0]) genericClassReference = matches[0].node;
237
- }
262
+ for (const member of members) if (ts.isPropertyDeclaration(member) && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))) {
263
+ var _property$originalPro;
264
+ if (member.modifiers) {
265
+ if (member.modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword)) continue;
266
+ }
267
+ const propertyName = member.name.text;
268
+ const type = this.getPropertyType(member, genericTypeMap);
269
+ const decorators = this.extractDecorators(member);
270
+ const isOptional = !!member.questionToken;
271
+ const isGeneric = this.isPropertyTypeGeneric(member);
272
+ const isEnum = this.isEnum(member);
273
+ const isPrimitive = this.isPrimitiveType(type) || isEnum;
274
+ const isClassType = this.isClassType(member);
275
+ const isArray = this.isArrayProperty(member);
276
+ const isTypeLiteral = this.isTypeLiteral(member);
277
+ let genericClassReference = void 0;
278
+ if (isGeneric && !isPrimitive) {
279
+ const baseTypeName = type.replace(/\[\]$/, "").trim();
280
+ if (!this.isPrimitiveType(baseTypeName)) {
281
+ const matches = this.classFileIndex.get(baseTypeName);
282
+ if (matches && matches.length > 0 && matches[0]) genericClassReference = matches[0].node;
238
283
  }
239
- const property = {
240
- name: propertyName,
241
- type,
242
- decorators,
243
- isOptional,
244
- isGeneric,
245
- originalProperty: member,
246
- isPrimitive,
247
- isClassType,
248
- isArray,
249
- isEnum,
250
- isRef: false,
251
- isTypeLiteral,
252
- genericClassReference
253
- };
254
- if (property.isClassType) {
255
- const declaration = this.getDeclarationProperty(property);
256
- if (parentClassNode) {
257
- if (declaration && declaration.name && this.checker.getSymbolAtLocation(declaration.name) === this.checker.getSymbolAtLocation(parentClassNode.name)) property.isRef = true;
258
- }
284
+ }
285
+ const property = {
286
+ name: propertyName,
287
+ type,
288
+ decorators,
289
+ isOptional,
290
+ isGeneric,
291
+ originalProperty: member,
292
+ isPrimitive,
293
+ isClassType,
294
+ isArray,
295
+ isEnum,
296
+ isRef: false,
297
+ isTypeLiteral,
298
+ genericClassReference
299
+ };
300
+ if (property.isClassType) {
301
+ const declaration = this.getDeclarationProperty(property);
302
+ if (parentClassNode) {
303
+ if (declaration && declaration.name && this.checker.getSymbolAtLocation(declaration.name) === this.checker.getSymbolAtLocation(parentClassNode.name)) property.isRef = true;
259
304
  }
260
- if (property.isTypeLiteral && property.originalProperty.type && ((_typeArguments = property.originalProperty.type.typeArguments) === null || _typeArguments === void 0 ? void 0 : _typeArguments.length) === 1) {
261
- const typeArguments = property.originalProperty.type.typeArguments;
262
- if (typeArguments && typeArguments[0]) {
263
- const firstTypeArg = typeArguments[0];
264
- if (ts.isTypeReferenceNode(firstTypeArg)) {
265
- const symbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
266
- if (symbol && symbol.declarations) {
267
- const classDeclaration = symbol.declarations.find((decl) => ts.isClassDeclaration(decl));
268
- if (classDeclaration && ts.isClassDeclaration(classDeclaration)) property.typeLiteralClassReference = classDeclaration;
269
- }
305
+ }
306
+ if (property.isTypeLiteral && property.originalProperty.type !== void 0 && ts.isTypeReferenceNode(property.originalProperty.type) && ((_property$originalPro = property.originalProperty.type.typeArguments) === null || _property$originalPro === void 0 ? void 0 : _property$originalPro.length) === 1) {
307
+ const typeArguments = property.originalProperty.type.typeArguments;
308
+ if (typeArguments && typeArguments[0]) {
309
+ const firstTypeArg = typeArguments[0];
310
+ if (ts.isTypeReferenceNode(firstTypeArg)) {
311
+ const symbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
312
+ if (symbol && symbol.declarations) {
313
+ const classDeclaration = symbol.declarations.find((decl) => ts.isClassDeclaration(decl));
314
+ if (classDeclaration && ts.isClassDeclaration(classDeclaration)) property.typeLiteralClassReference = classDeclaration;
270
315
  }
271
316
  }
272
317
  }
273
- properties.push(property);
274
318
  }
319
+ properties.push(property);
275
320
  }
276
321
  return properties;
277
322
  }
278
323
  getPropertyType(property, genericTypeMap = /* @__PURE__ */ new Map()) {
279
324
  if (property.type) return this.getTypeNodeToString(property.type, genericTypeMap);
280
325
  const type = this.checker.getTypeAtLocation(property);
281
- return this.getStringFromType(type);
326
+ return this.checker.typeToString(type);
282
327
  }
283
328
  getTypeNodeToString(typeNode, genericTypeMap = /* @__PURE__ */ new Map()) {
284
- if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
285
- const typeName = typeNode.typeName.text;
329
+ if (ts.isTypeReferenceNode(typeNode)) {
330
+ let typeName;
331
+ if (ts.isIdentifier(typeNode.typeName)) typeName = typeNode.typeName.text;
332
+ else typeName = typeNode.typeName.right.text;
286
333
  if (genericTypeMap.has(typeName)) return genericTypeMap.get(typeName);
287
334
  if (typeName.toLowerCase() === "uploadfile") return "UploadFile";
288
335
  if (typeName.toLowerCase() === "uploadfiledto") return "UploadFileDto";
@@ -294,7 +341,7 @@ var SchemaTransformer = class SchemaTransformer {
294
341
  }
295
342
  return this.resolveGenericType(typeNode);
296
343
  }
297
- return typeNode.typeName.text;
344
+ return typeName;
298
345
  }
299
346
  switch (typeNode.kind) {
300
347
  case ts.SyntaxKind.StringKeyword: return constants.jsPrimitives.String.type;
@@ -309,31 +356,31 @@ var SchemaTransformer = class SchemaTransformer {
309
356
  if (meaningfulTypes.length > 0 && meaningfulTypes[0]) return meaningfulTypes[0];
310
357
  if (types.length > 0 && types[0]) return types[0];
311
358
  return "object";
312
- default:
313
- if (ts.isIndexedAccessTypeNode(typeNode)) {
314
- const resolvedType = this.checker.getTypeAtLocation(typeNode);
315
- const resolved = this.checker.typeToString(resolvedType);
316
- if (this.isPrimitiveType(resolved)) return resolved;
317
- }
318
- const typeText = typeNode.getText();
319
- if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
320
- if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
321
- if (typeText.includes("Buffer") || typeText.includes("Uint8Array")) return constants.jsPrimitives.Buffer.type;
322
- return typeText;
359
+ default: {
360
+ const resolvedType = this.checker.getTypeAtLocation(typeNode);
361
+ const resolved = this.checker.typeToString(resolvedType);
362
+ if (genericTypeMap && genericTypeMap.has(resolved)) return genericTypeMap.get(resolved);
363
+ if (this.isPrimitiveType(resolved)) return resolved;
364
+ if (resolved === "Date") return constants.jsPrimitives.Date.type;
365
+ if (resolved === "Buffer") return constants.jsPrimitives.Buffer.type;
366
+ if (resolved === "Uint8Array") return constants.jsPrimitives.Uint8Array.type;
367
+ return resolved;
368
+ }
323
369
  }
324
370
  }
325
371
  resolveGenericType(typeNode) {
326
- const typeName = typeNode.typeName.text;
372
+ let typeName;
373
+ if (ts.isIdentifier(typeNode.typeName)) typeName = typeNode.typeName.text;
374
+ else typeName = typeNode.typeName.right.text;
327
375
  const typeArguments = typeNode.typeArguments;
328
376
  if (!typeArguments || typeArguments.length === 0) return typeName;
329
377
  const type = this.checker.getTypeAtLocation(typeNode);
330
- const resolvedType = this.getStringFromType(type);
331
- if (resolvedType && resolvedType !== typeName && !resolvedType.includes("any")) return resolvedType;
378
+ if (!(type.flags & ts.TypeFlags.Any)) {
379
+ const resolvedType = this.checker.typeToString(type);
380
+ if (resolvedType && resolvedType !== typeName) return resolvedType;
381
+ }
332
382
  return typeName;
333
383
  }
334
- getStringFromType(type) {
335
- return this.checker.typeToString(type);
336
- }
337
384
  extractDecorators(member) {
338
385
  const decorators = [];
339
386
  if (member.modifiers) {
@@ -353,6 +400,7 @@ var SchemaTransformer = class SchemaTransformer {
353
400
  }
354
401
  getDecoratorName(callExpression) {
355
402
  if (ts.isIdentifier(callExpression.expression)) return callExpression.expression.text;
403
+ if (ts.isPropertyAccessExpression(callExpression.expression)) return callExpression.expression.name.text;
356
404
  return "unknown";
357
405
  }
358
406
  getDecoratorArguments(callExpression) {
@@ -365,7 +413,13 @@ var SchemaTransformer = class SchemaTransformer {
365
413
  });
366
414
  }
367
415
  getSafeDecoratorArgument(arg) {
368
- if (arg && typeof arg === "object" && "kind" in arg) return arg.getText();
416
+ if (arg && typeof arg === "object" && "kind" in arg) {
417
+ const node = arg;
418
+ const type = this.checker.getTypeAtLocation(node);
419
+ if (type.isNumberLiteral()) return type.value;
420
+ if (type.isStringLiteral()) return type.value;
421
+ return this.checker.typeToString(type);
422
+ }
369
423
  return arg;
370
424
  }
371
425
  isPropertyTypeGeneric(property) {
@@ -389,15 +443,18 @@ var SchemaTransformer = class SchemaTransformer {
389
443
  isGenericTypeFromSymbol(type) {
390
444
  if (this.isSimpleArrayType(type)) return false;
391
445
  if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) return true;
392
- if (type.typeArguments && type.typeArguments.length > 0 && type.typeArguments[0].symbol.getName() === "Array") {
393
- const symbol = type.getSymbol();
394
- if (symbol && symbol.getName() === "Array") {
395
- const elementType = type.typeArguments[0];
396
- if (elementType) return this.isUtilityTypeFromType(elementType);
397
- return false;
446
+ if (type.flags & ts.TypeFlags.Object && type.objectFlags & ts.ObjectFlags.Reference) {
447
+ var _typeArgs$;
448
+ const typeArgs = this.checker.getTypeArguments(type);
449
+ if (typeArgs.length > 0 && ((_typeArgs$ = typeArgs[0]) === null || _typeArgs$ === void 0 || (_typeArgs$ = _typeArgs$.getSymbol()) === null || _typeArgs$ === void 0 ? void 0 : _typeArgs$.getName()) === "Array") {
450
+ const symbol = type.getSymbol();
451
+ if (symbol && symbol.getName() === "Array") {
452
+ const elementType = typeArgs[0];
453
+ return elementType ? this.isUtilityTypeFromType(elementType) : false;
454
+ }
455
+ const elementType = typeArgs[0];
456
+ return elementType ? this.isUtilityTypeFromType(elementType) : false;
398
457
  }
399
- const elementType = type.typeArguments[0];
400
- return this.isUtilityTypeFromType(elementType);
401
458
  }
402
459
  if (type.flags & ts.TypeFlags.TypeParameter) return true;
403
460
  if (type.flags & ts.TypeFlags.Conditional) return true;
@@ -429,13 +486,18 @@ var SchemaTransformer = class SchemaTransformer {
429
486
  isSimpleArrayType(type) {
430
487
  const symbol = type.getSymbol();
431
488
  if (!symbol || symbol.getName() !== "Array") return false;
432
- if (type.typeArguments && type.typeArguments.length === 1) {
433
- const elementType = type.typeArguments[0];
434
- if (!elementType) return false;
435
- if (this.isUtilityTypeFromType(elementType)) return false;
436
- if (elementType.typeArguments && elementType.typeArguments.length > 0) return false;
437
- if (type.typeArguments && type.typeArguments[0].symbol && type.typeArguments[0].symbol.getName() !== "Array") return false;
438
- return true;
489
+ if (type.flags & ts.TypeFlags.Object && type.objectFlags & ts.ObjectFlags.Reference) {
490
+ const typeArgs = this.checker.getTypeArguments(type);
491
+ if (typeArgs.length === 1) {
492
+ const elementType = typeArgs[0];
493
+ if (this.isUtilityTypeFromType(elementType)) return false;
494
+ if (elementType.flags & ts.TypeFlags.Object && elementType.objectFlags & ts.ObjectFlags.Reference) {
495
+ if (this.checker.getTypeArguments(elementType).length > 0) return false;
496
+ }
497
+ const elementSymbol = elementType.getSymbol();
498
+ if (elementSymbol && elementSymbol.getName() !== "Array") return false;
499
+ return true;
500
+ }
439
501
  }
440
502
  return false;
441
503
  }
@@ -493,23 +555,40 @@ var SchemaTransformer = class SchemaTransformer {
493
555
  return matches[0];
494
556
  }
495
557
  findBestMatch(cls, matches) {
496
- let instance = {};
497
- try {
498
- instance = new cls();
499
- } catch {
500
- instance = {};
558
+ const runtimeProps = this.extractRuntimePropertyNames(cls);
559
+ let bestMatch;
560
+ let bestScore = -1;
561
+ for (const match of matches) {
562
+ let score = 0;
563
+ for (const member of match.node.members) if (ts.isPropertyDeclaration(member) && member.name && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))) {
564
+ if (runtimeProps.has(member.name.text)) score++;
565
+ }
566
+ if (score > bestScore) {
567
+ bestScore = score;
568
+ bestMatch = match;
569
+ }
501
570
  }
502
- const instanceProperties = Object.keys(instance);
503
- let matchesMap = {};
504
- matches.forEach((match, index) => {
505
- let fountProperties = 0;
506
- match.node.members.map((member) => {
507
- if (member.name && instanceProperties.includes(member.name.getText())) fountProperties++;
508
- });
509
- matchesMap[index] = fountProperties;
510
- });
511
- const maxMatches = Math.max(...Object.values(matchesMap));
512
- return matches[Object.values(matchesMap).findIndex((value) => value === maxMatches)];
571
+ return bestMatch;
572
+ }
573
+ /**
574
+ * Safely extracts property names from a class constructor without instantiation.
575
+ * Parses the class source via Function.prototype.toString() and inspects
576
+ * the prototype for method names.
577
+ */
578
+ extractRuntimePropertyNames(cls) {
579
+ const names = /* @__PURE__ */ new Set();
580
+ try {
581
+ const source = Function.prototype.toString.call(cls);
582
+ const thisAssign = /this\.(\w+)\s*=/g;
583
+ let m;
584
+ while ((m = thisAssign.exec(source)) !== null) if (m[1]) names.add(m[1]);
585
+ const classField = /[;}](\w+)(?=[=;}\s])/g;
586
+ while ((m = classField.exec(source)) !== null) if (m[1] && !constants.JS_KEYWORDS.has(m[1])) names.add(m[1]);
587
+ } catch {}
588
+ try {
589
+ for (const name of Object.getOwnPropertyNames(cls.prototype)) if (name !== "constructor") names.add(name);
590
+ } catch {}
591
+ return names;
513
592
  }
514
593
  getFilteredSourceFiles(sourceOptions) {
515
594
  if (sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.isExternal) return this.program.getSourceFiles().filter((sf) => {
@@ -524,77 +603,54 @@ var SchemaTransformer = class SchemaTransformer {
524
603
  if (!propertyDeclaration.type) return false;
525
604
  let typeNode = propertyDeclaration.type;
526
605
  if (ts.isArrayTypeNode(typeNode)) typeNode = typeNode.elementType;
527
- if (ts.isTypeReferenceNode(typeNode)) {
528
- const type = this.checker.getTypeAtLocation(typeNode);
529
- return !!(type.flags & ts.TypeFlags.Enum) || !!(type.flags & ts.TypeFlags.EnumLiteral);
606
+ if (ts.isUnionTypeNode(typeNode)) {
607
+ const nonNullTypes = typeNode.types.filter((t) => t.kind !== ts.SyntaxKind.NullKeyword && t.kind !== ts.SyntaxKind.UndefinedKeyword);
608
+ if (nonNullTypes.length === 1 && nonNullTypes[0]) typeNode = nonNullTypes[0];
530
609
  }
610
+ if (ts.isTypeReferenceNode(typeNode)) return !!(this.checker.getTypeAtLocation(typeNode).flags & ts.TypeFlags.EnumLike);
531
611
  return false;
532
612
  }
533
- isClassType(propertyDeclaration) {
534
- if (!propertyDeclaration.type) return false;
535
- if (this.isArrayProperty(propertyDeclaration)) {
536
- var _propertyDeclaration$;
537
- let elementType;
538
- if (ts.isArrayTypeNode(propertyDeclaration.type)) elementType = propertyDeclaration.type.elementType;
539
- else if (ts.isTypeReferenceNode(propertyDeclaration.type) && ((_propertyDeclaration$ = propertyDeclaration.type.typeArguments) === null || _propertyDeclaration$ === void 0 ? void 0 : _propertyDeclaration$[0])) elementType = propertyDeclaration.type.typeArguments[0];
540
- if (!elementType) return false;
541
- if (ts.isTypeReferenceNode(elementType) && elementType.typeArguments && elementType.typeArguments.length > 0) {
542
- const firstTypeArg = elementType.typeArguments[0];
543
- if (firstTypeArg) {
544
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
545
- if (argSymbol && argSymbol.declarations) {
546
- if (argSymbol.declarations.some((decl) => ts.isClassDeclaration(decl))) return true;
547
- }
548
- }
549
- }
550
- const symbol = this.checker.getTypeAtLocation(elementType).getSymbol();
551
- if (symbol && symbol.declarations) return symbol.declarations.some((decl) => ts.isClassDeclaration(decl));
552
- return false;
553
- } else {
554
- if (ts.isTypeReferenceNode(propertyDeclaration.type) && propertyDeclaration.type.typeArguments && propertyDeclaration.type.typeArguments.length > 0) {
555
- const firstTypeArg = propertyDeclaration.type.typeArguments[0];
556
- if (firstTypeArg) {
557
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
558
- if (argSymbol && argSymbol.declarations) {
559
- if (argSymbol.declarations.some((decl) => ts.isClassDeclaration(decl))) return true;
560
- }
561
- }
562
- }
563
- const symbol = this.checker.getTypeAtLocation(propertyDeclaration.type).getSymbol();
564
- if (symbol && symbol.declarations) return symbol.declarations.some((decl) => ts.isClassDeclaration(decl));
565
- return false;
566
- }
567
- }
568
- getDeclarationProperty(property) {
569
- if (!property.originalProperty.type) return;
570
- if (ts.isArrayTypeNode(property.originalProperty.type)) {
571
- const elementType = property.originalProperty.type.elementType;
572
- if (ts.isTypeReferenceNode(elementType) && elementType.typeArguments && elementType.typeArguments.length > 0) {
573
- const firstTypeArg = elementType.typeArguments[0];
574
- if (firstTypeArg) {
575
- const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
576
- if (argSymbol && argSymbol.declarations) {
577
- const classDecl = argSymbol.declarations.find((decl) => ts.isClassDeclaration(decl));
578
- if (classDecl) return classDecl;
579
- }
580
- }
581
- }
582
- const symbol = this.checker.getTypeAtLocation(elementType).getSymbol();
583
- if (symbol && symbol.declarations) return symbol.declarations.find((decl) => ts.isClassDeclaration(decl)) || symbol.declarations[0];
584
- return;
585
- }
586
- if (ts.isTypeReferenceNode(property.originalProperty.type) && property.originalProperty.type.typeArguments && property.originalProperty.type.typeArguments.length > 0) {
587
- const firstTypeArg = property.originalProperty.type.typeArguments[0];
613
+ /**
614
+ * Resolves a type node to its underlying symbol via the type-checker.
615
+ * For type references with type arguments (e.g., PayloadEntity<Person>),
616
+ * it checks the first type argument for a class declaration first.
617
+ * Returns the ts.Symbol or undefined.
618
+ */
619
+ resolveClassSymbolFromTypeNode(typeNode) {
620
+ if (ts.isTypeReferenceNode(typeNode) && typeNode.typeArguments && typeNode.typeArguments.length > 0) {
621
+ const firstTypeArg = typeNode.typeArguments[0];
588
622
  if (firstTypeArg) {
589
623
  const argSymbol = this.checker.getTypeAtLocation(firstTypeArg).getSymbol();
590
624
  if (argSymbol && argSymbol.declarations) {
591
- const classDecl = argSymbol.declarations.find((decl) => ts.isClassDeclaration(decl));
592
- if (classDecl) return classDecl;
625
+ if (argSymbol.declarations.some((decl) => ts.isClassDeclaration(decl))) return argSymbol;
593
626
  }
594
627
  }
595
628
  }
596
- const symbol = this.checker.getTypeAtLocation(property.originalProperty.type).getSymbol();
597
- if (symbol && symbol.declarations) return symbol.declarations.find((decl) => ts.isClassDeclaration(decl)) || symbol.declarations[0];
629
+ return this.checker.getTypeAtLocation(typeNode).getSymbol() ?? void 0;
630
+ }
631
+ /**
632
+ * Resolves the element type node from an array property declaration.
633
+ * Handles both `T[]` and `Array<T>` syntax.
634
+ */
635
+ resolveArrayElementTypeNode(propertyDeclaration) {
636
+ var _propertyDeclaration$;
637
+ if (!propertyDeclaration.type) return void 0;
638
+ if (ts.isArrayTypeNode(propertyDeclaration.type)) return propertyDeclaration.type.elementType;
639
+ if (ts.isTypeReferenceNode(propertyDeclaration.type) && ((_propertyDeclaration$ = propertyDeclaration.type.typeArguments) === null || _propertyDeclaration$ === void 0 ? void 0 : _propertyDeclaration$[0])) return propertyDeclaration.type.typeArguments[0];
640
+ }
641
+ isClassType(propertyDeclaration) {
642
+ if (!propertyDeclaration.type) return false;
643
+ const typeNode = this.isArrayProperty(propertyDeclaration) ? this.resolveArrayElementTypeNode(propertyDeclaration) : propertyDeclaration.type;
644
+ if (!typeNode) return false;
645
+ const symbol = this.resolveClassSymbolFromTypeNode(typeNode);
646
+ if (symbol && symbol.declarations) return symbol.declarations.some((decl) => ts.isClassDeclaration(decl));
647
+ return false;
648
+ }
649
+ getDeclarationProperty(property) {
650
+ if (!property.originalProperty.type) return;
651
+ const typeNode = ts.isArrayTypeNode(property.originalProperty.type) ? property.originalProperty.type.elementType : property.originalProperty.type;
652
+ const symbol = this.resolveClassSymbolFromTypeNode(typeNode);
653
+ if (symbol && symbol.declarations) return symbol.declarations.find((decl) => ts.isClassDeclaration(decl)) ?? symbol.declarations[0];
598
654
  }
599
655
  isArrayProperty(propertyDeclaration) {
600
656
  if (!propertyDeclaration.type) return false;
@@ -641,16 +697,28 @@ var SchemaTransformer = class SchemaTransformer {
641
697
  transformedSchema,
642
698
  declaration: property.genericClassReference
643
699
  });
644
- else schema = {
645
- type: "object",
646
- properties: {},
647
- additionalProperties: true
648
- };
649
- else schema = {
650
- type: "object",
651
- properties: {},
652
- additionalProperties: true
653
- };
700
+ else {
701
+ const inner = {
702
+ type: "object",
703
+ properties: {},
704
+ additionalProperties: true
705
+ };
706
+ schema = property.isArray ? {
707
+ type: "array",
708
+ items: inner
709
+ } : inner;
710
+ }
711
+ else {
712
+ const inner = {
713
+ type: "object",
714
+ properties: {},
715
+ additionalProperties: true
716
+ };
717
+ schema = property.isArray ? {
718
+ type: "array",
719
+ items: inner
720
+ } : inner;
721
+ }
654
722
  this.applyDecorators(property, schema);
655
723
  return schema;
656
724
  }
@@ -746,7 +814,12 @@ var SchemaTransformer = class SchemaTransformer {
746
814
  if (enumSchema) return enumSchema;
747
815
  }
748
816
  const propertySchema = { type: "object" };
749
- const propertyType = property.type.toLowerCase().replace("[]", "").trim();
817
+ let baseTypeNode = property.originalProperty.type;
818
+ if (property.isArray && baseTypeNode) baseTypeNode = this.resolveArrayElementTypeNode(property.originalProperty) ?? baseTypeNode;
819
+ const resolvedType = this.checker.getTypeAtLocation(baseTypeNode ?? property.originalProperty);
820
+ let propertyType;
821
+ if (resolvedType.flags & ts.TypeFlags.TypeParameter) propertyType = property.type.toLowerCase().replace(/\[\]$/, "").trim();
822
+ else propertyType = this.checker.typeToString(resolvedType).toLowerCase();
750
823
  let isFile = false;
751
824
  switch (propertyType) {
752
825
  case constants.jsPrimitives.String.value:
@@ -783,17 +856,26 @@ var SchemaTransformer = class SchemaTransformer {
783
856
  propertySchema.type = constants.jsPrimitives.Symbol.value;
784
857
  break;
785
858
  case constants.jsPrimitives.Object.value:
859
+ case "unknown":
860
+ case "any":
786
861
  propertySchema.type = constants.jsPrimitives.Object.value;
862
+ propertySchema.additionalProperties = true;
787
863
  break;
788
864
  default: propertySchema.type = constants.jsPrimitives.String.value;
789
865
  }
790
866
  if (property.isArray) {
791
867
  delete propertySchema.format;
792
- propertySchema.type = `array`;
793
- propertySchema.items = {
794
- type: isFile ? constants.jsPrimitives.UploadFile.value : propertyType,
868
+ const resolvedItemType = propertySchema.type;
869
+ const itemSchema = {
870
+ type: isFile ? constants.jsPrimitives.UploadFile.value : resolvedItemType,
795
871
  format: isFile ? constants.jsPrimitives.UploadFile.format : propertySchema.format
796
872
  };
873
+ if (propertySchema.additionalProperties) {
874
+ itemSchema.additionalProperties = true;
875
+ delete propertySchema.additionalProperties;
876
+ }
877
+ propertySchema.type = `array`;
878
+ propertySchema.items = itemSchema;
797
879
  }
798
880
  return propertySchema;
799
881
  }
@@ -991,21 +1073,24 @@ var SchemaTransformer = class SchemaTransformer {
991
1073
  const callee = node.expression;
992
1074
  if (ts.isIdentifier(callee) && callee.text === "transform" || ts.isPropertyAccessExpression(callee) && callee.name.text === "transform") {
993
1075
  const firstArg = node.arguments[0];
994
- const typeArgs = firstArg.typeArguments;
995
- if (typeArgs && typeArgs.length > 0) {
996
- const baseExpr = firstArg.expression ?? firstArg;
997
- if (ts.isIdentifier(baseExpr)) {
1076
+ if (ts.isExpressionWithTypeArguments(firstArg) && firstArg.typeArguments && firstArg.typeArguments.length > 0) {
1077
+ const baseExpr = firstArg.expression;
1078
+ const className = ts.isIdentifier(baseExpr) ? baseExpr.text : ts.isPropertyAccessExpression(baseExpr) ? baseExpr.name.text : void 0;
1079
+ if (className) {
998
1080
  var _this$classFileIndex$;
999
- const classNode = (_this$classFileIndex$ = this.classFileIndex.get(baseExpr.text)) === null || _this$classFileIndex$ === void 0 || (_this$classFileIndex$ = _this$classFileIndex$[0]) === null || _this$classFileIndex$ === void 0 ? void 0 : _this$classFileIndex$.node;
1081
+ 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;
1000
1082
  if (classNode === null || classNode === void 0 ? void 0 : classNode.typeParameters) {
1001
1083
  const typeMap = /* @__PURE__ */ new Map();
1002
1084
  classNode.typeParameters.forEach((param, i) => {
1003
- const typeArg = typeArgs[i];
1085
+ const typeArg = firstArg.typeArguments[i];
1004
1086
  if (typeArg) typeMap.set(param.name.text, this.getTypeNodeToString(typeArg, /* @__PURE__ */ new Map()));
1005
1087
  });
1006
- if (typeMap.size > 0) this.transformCallIndex.set(baseExpr.text, typeMap);
1088
+ if (typeMap.size > 0) this.transformCallIndex.set(className, typeMap);
1007
1089
  }
1008
1090
  }
1091
+ } else {
1092
+ const className = ts.isIdentifier(firstArg) ? firstArg.text : ts.isPropertyAccessExpression(firstArg) ? firstArg.name.text : void 0;
1093
+ if (className) this.nonGenericTransformCalls.add(className);
1009
1094
  }
1010
1095
  }
1011
1096
  }
@@ -1028,7 +1113,14 @@ var SchemaTransformer = class SchemaTransformer {
1028
1113
  }
1029
1114
  };
1030
1115
  }
1031
- const genericTypeMap = this.transformCallIndex.get(cls.name) ?? /* @__PURE__ */ new Map();
1116
+ const genericTypeMap = /* @__PURE__ */ new Map();
1117
+ if (result.node.typeParameters) {
1118
+ const indexedTypeMap = this.transformCallIndex.get(cls.name);
1119
+ const hasNonGenericCall = this.nonGenericTransformCalls.has(cls.name);
1120
+ const allHaveDefaults = result.node.typeParameters.every((p) => !!p.default);
1121
+ if (indexedTypeMap && !(hasNonGenericCall && allHaveDefaults)) for (const [key, value] of indexedTypeMap) genericTypeMap.set(key, value);
1122
+ else for (const param of result.node.typeParameters) if (param.default) genericTypeMap.set(param.name.text, this.getTypeNodeToString(param.default, /* @__PURE__ */ new Map()));
1123
+ }
1032
1124
  const hasGenericArgs = genericTypeMap.size > 0;
1033
1125
  if (!hasGenericArgs && this.classCache.has(cls)) return this.classCache.get(cls);
1034
1126
  let schema = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-class-to-openapi",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Transform TypeScript classes into OpenAPI 3.1.0 schema objects, which support class-validator decorators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",