tjs-lang 0.5.4 → 0.6.0

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.
@@ -67,6 +67,8 @@ export interface FunctionTypeInfo {
67
67
  description?: string
68
68
  /** Generic type parameters with constraints/defaults */
69
69
  typeParams?: Record<string, TypeParamInfo>
70
+ /** Overload signatures (when function has TS overloads) */
71
+ overloads?: FunctionTypeInfo[]
70
72
  }
71
73
 
72
74
  export interface ClassTypeInfo {
@@ -183,6 +185,18 @@ function typeToExample(
183
185
  }
184
186
  return 'undefined'
185
187
  }
188
+ if (
189
+ typeName === 'Generator' ||
190
+ typeName === 'AsyncGenerator' ||
191
+ typeName === 'IterableIterator' ||
192
+ typeName === 'AsyncIterableIterator'
193
+ ) {
194
+ // Unwrap to yield type (first type argument)
195
+ if (typeRef.typeArguments?.length) {
196
+ return typeToExample(typeRef.typeArguments[0], checker, warnings, ctx)
197
+ }
198
+ return 'undefined'
199
+ }
186
200
  if (typeName === 'Record') {
187
201
  return '{}'
188
202
  }
@@ -457,6 +471,15 @@ function typeToInfo(
457
471
  if (typeName === 'Promise' && typeRef.typeArguments?.length) {
458
472
  return typeToInfo(typeRef.typeArguments[0], ctx)
459
473
  }
474
+ if (
475
+ (typeName === 'Generator' ||
476
+ typeName === 'AsyncGenerator' ||
477
+ typeName === 'IterableIterator' ||
478
+ typeName === 'AsyncIterableIterator') &&
479
+ typeRef.typeArguments?.length
480
+ ) {
481
+ return typeToInfo(typeRef.typeArguments[0], ctx)
482
+ }
460
483
 
461
484
  // Handle utility types
462
485
  if (typeRef.typeArguments?.length) {
@@ -916,7 +939,13 @@ function transformFunctionToTJS(
916
939
  warnings?: string[],
917
940
  includeLineNumber?: boolean
918
941
  ): string {
919
- const params = transformParams(node.parameters, sourceFile, warnings)
942
+ const degraded: string[] = []
943
+ const params = transformParams(
944
+ node.parameters,
945
+ sourceFile,
946
+ warnings,
947
+ degraded
948
+ )
920
949
 
921
950
  // Get line number (1-indexed) for source mapping
922
951
  const { line } = sourceFile.getLineAndCharacterOfPosition(
@@ -939,6 +968,18 @@ function transformFunctionToTJS(
939
968
  ? ` -! ${returnExample}`
940
969
  : ''
941
970
 
971
+ // Track degraded return type
972
+ if (node.type && (returnExample === 'any' || returnExample === 'undefined')) {
973
+ const originalReturn = node.type.getText(sourceFile)
974
+ if (
975
+ originalReturn !== 'any' &&
976
+ originalReturn !== 'unknown' &&
977
+ originalReturn !== 'void'
978
+ ) {
979
+ degraded.push(`return: ${originalReturn}`)
980
+ }
981
+ }
982
+
942
983
  // Get function body and strip TypeScript syntax using ts.transpileModule
943
984
  let body = ''
944
985
  if (node.body) {
@@ -959,17 +1000,101 @@ function transformFunctionToTJS(
959
1000
  body = '{ }'
960
1001
  }
961
1002
 
962
- // Check for async modifier
1003
+ // Check for async and generator modifiers
963
1004
  const isAsync = node.modifiers?.some(
964
1005
  (m) => m.kind === ts.SyntaxKind.AsyncKeyword
965
1006
  )
1007
+ const isGenerator = !!(node as ts.FunctionDeclaration).asteriskToken
966
1008
  const asyncPrefix = isAsync ? 'async ' : ''
1009
+ const funcKeyword = isGenerator ? 'function* ' : 'function '
1010
+
1011
+ // Emit migration comment if any types were degraded
1012
+ const degradedComment =
1013
+ degraded.length > 0
1014
+ ? `/* TODO: TS types degraded — ${degraded.join(', ')} */\n`
1015
+ : ''
967
1016
 
968
- return `${lineComment}${asyncPrefix}function ${funcName}(${params.join(
1017
+ return `${lineComment}${degradedComment}${asyncPrefix}${funcKeyword}${funcName}(${params.join(
969
1018
  ', '
970
1019
  )})${returnAnnotation} ${body}`
971
1020
  }
972
1021
 
1022
+ /**
1023
+ * Emit a full TJS overload group: the implementation (renamed) + wrapper signatures.
1024
+ * Each overload signature becomes a TJS function that delegates to the implementation.
1025
+ * TJS polymorphic dispatch merges the wrappers into a dispatcher automatically.
1026
+ */
1027
+ function emitOverloadGroup(
1028
+ signatures: ts.FunctionDeclaration[],
1029
+ implementation: ts.FunctionDeclaration,
1030
+ sourceFile: ts.SourceFile,
1031
+ warnings?: string[]
1032
+ ): string[] {
1033
+ const funcName = implementation.name?.getText(sourceFile) || ''
1034
+ const implName = `_${funcName}_impl`
1035
+ const results: string[] = []
1036
+
1037
+ // Emit the implementation as a renamed private function
1038
+ const implParams = transformParams(
1039
+ implementation.parameters,
1040
+ sourceFile,
1041
+ warnings
1042
+ )
1043
+ let implBody = '{ }'
1044
+ if (implementation.body) {
1045
+ const bodyText = implementation.body.getText(sourceFile)
1046
+ const transpiled = ts.transpileModule(bodyText, {
1047
+ compilerOptions: {
1048
+ target: ts.ScriptTarget.ESNext,
1049
+ module: ts.ModuleKind.ESNext,
1050
+ removeComments: false,
1051
+ },
1052
+ })
1053
+ implBody = transpiled.outputText.trim()
1054
+ }
1055
+ const isAsync = implementation.modifiers?.some(
1056
+ (m) => m.kind === ts.SyntaxKind.AsyncKeyword
1057
+ )
1058
+ const isGenerator = !!implementation.asteriskToken
1059
+ const asyncPrefix = isAsync ? 'async ' : ''
1060
+ const funcKeyword = isGenerator ? 'function* ' : 'function '
1061
+
1062
+ results.push(
1063
+ `${asyncPrefix}${funcKeyword}${implName}(${implParams.join(
1064
+ ', '
1065
+ )}) ${implBody}`
1066
+ )
1067
+
1068
+ // Emit each overload signature as a wrapper that delegates to the implementation
1069
+ for (const sig of signatures) {
1070
+ const params = transformParams(sig.parameters, sourceFile, warnings)
1071
+ const paramNames = sig.parameters.map((p) => p.name.getText(sourceFile))
1072
+ const returnExample = sig.type
1073
+ ? typeToExample(sig.type, undefined, warnings)
1074
+ : ''
1075
+ const returnAnnotation =
1076
+ returnExample && returnExample !== 'undefined' && returnExample !== 'any'
1077
+ ? ` -! ${returnExample}`
1078
+ : ''
1079
+
1080
+ const { line } = sourceFile.getLineAndCharacterOfPosition(
1081
+ sig.getStart(sourceFile)
1082
+ )
1083
+ const lineComment = `/* line ${line + 1} */\n`
1084
+ const returnKw = isGenerator ? 'yield* ' : 'return '
1085
+
1086
+ results.push(
1087
+ `${lineComment}${asyncPrefix}${funcKeyword}${funcName}(${params.join(
1088
+ ', '
1089
+ )})${returnAnnotation} { ${returnKw}${implName}(${paramNames.join(
1090
+ ', '
1091
+ )}) }`
1092
+ )
1093
+ }
1094
+
1095
+ return results
1096
+ }
1097
+
973
1098
  /**
974
1099
  * Transform TypeScript class to TJS class
975
1100
  * Converts TS type annotations to TJS example-based annotations
@@ -1065,10 +1190,12 @@ function transformClassToTJS(
1065
1190
  body = replacePrivateRefs(transpiled.outputText.trim())
1066
1191
  }
1067
1192
 
1193
+ const isGenerator = !!member.asteriskToken
1068
1194
  const staticPrefix = isStatic ? 'static ' : ''
1069
1195
  const asyncPrefix = isAsync ? 'async ' : ''
1196
+ const generatorStar = isGenerator ? '*' : ''
1070
1197
  members.push(
1071
- ` ${staticPrefix}${asyncPrefix}${methodName}(${params.join(
1198
+ ` ${staticPrefix}${asyncPrefix}${generatorStar}${methodName}(${params.join(
1072
1199
  ', '
1073
1200
  )})${returnAnnotation} ${body}`
1074
1201
  )
@@ -1165,7 +1292,8 @@ function transformClassToTJS(
1165
1292
  function transformParams(
1166
1293
  parameters: ts.NodeArray<ts.ParameterDeclaration>,
1167
1294
  sourceFile: ts.SourceFile,
1168
- warnings?: string[]
1295
+ warnings?: string[],
1296
+ degraded?: string[]
1169
1297
  ): string[] {
1170
1298
  const params: string[] = []
1171
1299
 
@@ -1181,6 +1309,13 @@ function transformParams(
1181
1309
  } else if (typeExample === 'any' || typeExample === 'undefined') {
1182
1310
  // any/undefined type - no annotation in TJS (bare name means any)
1183
1311
  params.push(name)
1312
+ // Record original TS type for migration comments
1313
+ if (degraded && param.type) {
1314
+ const originalType = param.type.getText(sourceFile)
1315
+ if (originalType !== 'any' && originalType !== 'unknown') {
1316
+ degraded.push(`${name}: ${originalType}`)
1317
+ }
1318
+ }
1184
1319
  } else if (isOptional) {
1185
1320
  // Optional without default - use union with undefined to preserve
1186
1321
  // three-state semantics (e.g. TS `flag?: boolean` can be true/false/undefined)
@@ -1482,7 +1617,22 @@ export function fromTS(
1482
1617
  typeAliases.set(node.name.getText(sourceFile), node.type)
1483
1618
  }
1484
1619
  if (ts.isInterfaceDeclaration(node)) {
1485
- interfaces.set(node.name.getText(sourceFile), node)
1620
+ const name = node.name.getText(sourceFile)
1621
+ const existing = interfaces.get(name)
1622
+ if (existing) {
1623
+ // Merge members (TS interface merging)
1624
+ const merged = ts.factory.updateInterfaceDeclaration(
1625
+ existing,
1626
+ existing.modifiers,
1627
+ existing.name,
1628
+ existing.typeParameters,
1629
+ existing.heritageClauses,
1630
+ [...existing.members, ...node.members]
1631
+ )
1632
+ interfaces.set(name, merged)
1633
+ } else {
1634
+ interfaces.set(name, node)
1635
+ }
1486
1636
  }
1487
1637
  ts.forEachChild(node, collectTypes)
1488
1638
  }
@@ -1496,6 +1646,36 @@ export function fromTS(
1496
1646
  warnings,
1497
1647
  }
1498
1648
 
1649
+ // Pre-scan: detect function overload groups
1650
+ // In TS, overloads are N bodyless signatures + 1 implementation with body
1651
+ const overloadGroups = new Map<
1652
+ string,
1653
+ {
1654
+ signatures: ts.FunctionDeclaration[] // body === undefined
1655
+ implementation: ts.FunctionDeclaration | null // has body
1656
+ }
1657
+ >()
1658
+ for (const stmt of sourceFile.statements) {
1659
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
1660
+ const name = stmt.name.getText(sourceFile)
1661
+ if (!overloadGroups.has(name)) {
1662
+ overloadGroups.set(name, { signatures: [], implementation: null })
1663
+ }
1664
+ const group = overloadGroups.get(name)!
1665
+ if (stmt.body) {
1666
+ group.implementation = stmt
1667
+ } else {
1668
+ group.signatures.push(stmt)
1669
+ }
1670
+ }
1671
+ }
1672
+ // Only keep groups that actually have overloads (signatures + implementation)
1673
+ for (const [name, group] of overloadGroups) {
1674
+ if (group.signatures.length === 0 || !group.implementation) {
1675
+ overloadGroups.delete(name)
1676
+ }
1677
+ }
1678
+
1499
1679
  // Walk top-level statements only (don't recurse into function bodies)
1500
1680
  for (const statement of sourceFile.statements) {
1501
1681
  let handled = false
@@ -1510,23 +1690,65 @@ export function fromTS(
1510
1690
  const funcName = statement.name.getText(sourceFile)
1511
1691
  handled = true
1512
1692
 
1513
- if (emitTJS) {
1514
- tjsFunctions.push(
1515
- transformFunctionToTJS(
1693
+ const overloadGroup = overloadGroups.get(funcName)
1694
+
1695
+ if (overloadGroup) {
1696
+ // This function is part of an overload group
1697
+ if (!statement.body) {
1698
+ // Skip bodyless signatures — handled when we encounter the implementation
1699
+ } else {
1700
+ // Implementation: emit the entire overload group
1701
+ if (emitTJS) {
1702
+ tjsFunctions.push(
1703
+ ...emitOverloadGroup(
1704
+ overloadGroup.signatures,
1705
+ statement,
1706
+ sourceFile,
1707
+ warnings
1708
+ )
1709
+ )
1710
+ } else {
1711
+ const overloads: FunctionTypeInfo[] = []
1712
+ for (const sig of overloadGroup.signatures) {
1713
+ overloads.push(
1714
+ extractFunctionMetadata(
1715
+ sig,
1716
+ sourceFile,
1717
+ warnings,
1718
+ resolutionCtx
1719
+ )
1720
+ )
1721
+ }
1722
+ const implInfo = extractFunctionMetadata(
1723
+ statement,
1724
+ sourceFile,
1725
+ warnings,
1726
+ resolutionCtx
1727
+ )
1728
+ implInfo.overloads = overloads
1729
+ metadata[funcName] = implInfo
1730
+ }
1731
+ }
1732
+ } else {
1733
+ // Normal (non-overloaded) function
1734
+ if (emitTJS) {
1735
+ tjsFunctions.push(
1736
+ transformFunctionToTJS(
1737
+ statement,
1738
+ sourceFile,
1739
+ undefined,
1740
+ warnings,
1741
+ true
1742
+ )
1743
+ )
1744
+ } else {
1745
+ metadata[funcName] = extractFunctionMetadata(
1516
1746
  statement,
1517
1747
  sourceFile,
1518
- undefined,
1519
1748
  warnings,
1520
- true
1749
+ resolutionCtx
1521
1750
  )
1522
- )
1523
- } else {
1524
- metadata[funcName] = extractFunctionMetadata(
1525
- statement,
1526
- sourceFile,
1527
- warnings,
1528
- resolutionCtx
1529
- )
1751
+ }
1530
1752
  }
1531
1753
  }
1532
1754
 
@@ -1592,8 +1814,10 @@ export function fromTS(
1592
1814
  const typeName = statement.name.getText(sourceFile)
1593
1815
  if (!seenTypeNames.has(typeName)) {
1594
1816
  seenTypeNames.add(typeName)
1817
+ // Use merged interface (handles declaration merging)
1818
+ const merged = interfaces.get(typeName) || statement
1595
1819
  const typeDecl = transformInterfaceToType(
1596
- statement,
1820
+ merged,
1597
1821
  sourceFile,
1598
1822
  warnings
1599
1823
  )
package/src/lang/index.ts CHANGED
@@ -41,6 +41,11 @@ export {
41
41
  type TJSTranspileResult,
42
42
  type TJSTypeInfo,
43
43
  } from './emitters/js'
44
+ export {
45
+ generateDTS,
46
+ typeDescriptorToTS,
47
+ type GenerateDTSOptions,
48
+ } from './emitters/dts'
44
49
  export {
45
50
  fromTS,
46
51
  type FromTSOptions,