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.
- package/CLAUDE.md +33 -13
- package/README.md +4 -4
- package/bin/dev.ts +5 -1
- package/demo/docs.json +14 -2
- package/demo/index.html +2 -2
- package/demo/src/capabilities.ts +109 -2
- package/demo/src/demo-nav.ts +137 -203
- package/demo/src/imports.ts +43 -9
- package/demo/src/index.ts +179 -29
- package/demo/src/playground-shared.ts +11 -4
- package/demo/src/playground.ts +2 -2
- package/demo/src/tjs-playground.ts +294 -11
- package/demo/src/ts-playground.ts +239 -0
- package/dist/index.js +135 -127
- package/dist/index.js.map +6 -5
- package/dist/src/cli/commands/emit.d.ts +3 -0
- package/dist/src/lang/emitters/dts.d.ts +48 -0
- package/dist/src/lang/emitters/from-ts.d.ts +2 -0
- package/dist/src/lang/index.d.ts +1 -0
- package/dist/tjs-batteries.js +3 -3
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-full.js +135 -127
- package/dist/tjs-full.js.map +6 -5
- package/dist/tjs-transpiler.js +2 -349
- package/dist/tjs-transpiler.js.map +4 -19
- package/package.json +1 -1
- package/src/cli/commands/emit.ts +26 -0
- package/src/cli/tjs.ts +4 -1
- package/src/lang/codegen.test.ts +55 -0
- package/src/lang/emitters/dts.test.ts +406 -0
- package/src/lang/emitters/dts.ts +588 -0
- package/src/lang/emitters/from-ts.ts +244 -20
- package/src/lang/index.ts +5 -0
- package/src/lang/typescript-syntax.test.ts +358 -0
|
@@ -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
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1820
|
+
merged,
|
|
1597
1821
|
sourceFile,
|
|
1598
1822
|
warnings
|
|
1599
1823
|
)
|
package/src/lang/index.ts
CHANGED