tjs-lang 0.6.35 → 0.6.37
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/dist/index.js +89 -86
- package/dist/index.js.map +3 -3
- package/dist/tjs-full.js +89 -86
- package/dist/tjs-full.js.map +3 -3
- package/package.json +1 -1
- package/src/cli/tjs.ts +1 -1
- package/src/lang/codegen.test.ts +4 -3
- package/src/lang/emitters/from-ts.ts +68 -14
- package/src/lang/typescript-syntax.test.ts +20 -22
- package/src/use-cases/tosijs-convert-issues.test.ts +83 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.37",
|
|
4
4
|
"description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
package/src/cli/tjs.ts
CHANGED
package/src/lang/codegen.test.ts
CHANGED
|
@@ -211,7 +211,7 @@ class User {
|
|
|
211
211
|
expect(code).toContain("constructor(name: '')")
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
-
it('
|
|
214
|
+
it('strips private keyword without converting to # syntax', () => {
|
|
215
215
|
const ts = `
|
|
216
216
|
class Counter {
|
|
217
217
|
private count: number = 0
|
|
@@ -220,9 +220,10 @@ class Counter {
|
|
|
220
220
|
`
|
|
221
221
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
expect(code).toContain('this.#count')
|
|
223
|
+
// private keyword stripped, field name kept as-is
|
|
225
224
|
expect(code).not.toContain('private')
|
|
225
|
+
expect(code).not.toContain('#count')
|
|
226
|
+
expect(code).toContain('this.count')
|
|
226
227
|
})
|
|
227
228
|
|
|
228
229
|
it('converts method return types', () => {
|
|
@@ -545,7 +545,14 @@ function typeToExample(
|
|
|
545
545
|
|
|
546
546
|
if (nonNullTypes.length === 1 && (hasNull || hasUndefined)) {
|
|
547
547
|
// Nullable type: T | null -> T | null
|
|
548
|
-
const baseExample = typeToExample(
|
|
548
|
+
const baseExample = typeToExample(
|
|
549
|
+
nonNullTypes[0],
|
|
550
|
+
checker,
|
|
551
|
+
warnings,
|
|
552
|
+
ctx
|
|
553
|
+
)
|
|
554
|
+
// any | null/undefined is just any — don't emit 'any | null'
|
|
555
|
+
if (baseExample === 'any') return 'any'
|
|
549
556
|
if (hasNull) return `${baseExample} | null`
|
|
550
557
|
if (hasUndefined) return `${baseExample} | undefined`
|
|
551
558
|
}
|
|
@@ -1573,7 +1580,8 @@ function transformClassToTJS(
|
|
|
1573
1580
|
node: ts.ClassDeclaration,
|
|
1574
1581
|
sourceFile: ts.SourceFile,
|
|
1575
1582
|
warnings?: string[],
|
|
1576
|
-
ctx?: TypeResolutionContext
|
|
1583
|
+
ctx?: TypeResolutionContext,
|
|
1584
|
+
convertPrivateToHash = false
|
|
1577
1585
|
): string {
|
|
1578
1586
|
// Build type parameter map from class-level generics
|
|
1579
1587
|
let resolveCtx = ctx
|
|
@@ -1598,16 +1606,19 @@ function transformClassToTJS(
|
|
|
1598
1606
|
)?.types[0]
|
|
1599
1607
|
const extendsClause = extendsType?.expression?.getText(sourceFile)
|
|
1600
1608
|
|
|
1601
|
-
//
|
|
1609
|
+
// With TjsClass: convert TS private to JS # (true runtime privacy).
|
|
1610
|
+
// Without TjsClass: strip the keyword, keep the name (TS private is compile-time only).
|
|
1602
1611
|
const privateFieldMap = new Map<string, string>()
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1612
|
+
if (convertPrivateToHash) {
|
|
1613
|
+
for (const member of node.members) {
|
|
1614
|
+
if (ts.isPropertyDeclaration(member) && member.name) {
|
|
1615
|
+
const propName = member.name.getText(sourceFile)
|
|
1616
|
+
const isPrivate = member.modifiers?.some(
|
|
1617
|
+
(m) => m.kind === ts.SyntaxKind.PrivateKeyword
|
|
1618
|
+
)
|
|
1619
|
+
if (isPrivate && !propName.startsWith('#')) {
|
|
1620
|
+
privateFieldMap.set(propName, `#${propName}`)
|
|
1621
|
+
}
|
|
1611
1622
|
}
|
|
1612
1623
|
}
|
|
1613
1624
|
}
|
|
@@ -2068,6 +2079,37 @@ interface TjsAnnotation {
|
|
|
2068
2079
|
text?: string // raw text for predicate/example/declaration
|
|
2069
2080
|
}
|
|
2070
2081
|
|
|
2082
|
+
/** Valid TJS mode directives */
|
|
2083
|
+
const VALID_TJS_MODES = new Set([
|
|
2084
|
+
'TjsStrict',
|
|
2085
|
+
'TjsEquals',
|
|
2086
|
+
'TjsClass',
|
|
2087
|
+
'TjsDate',
|
|
2088
|
+
'TjsNoeval',
|
|
2089
|
+
'TjsNoVar',
|
|
2090
|
+
'TjsStandard',
|
|
2091
|
+
'TjsSafeEval',
|
|
2092
|
+
])
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* Extract TJS mode directives from /* @tjs ... * / comments.
|
|
2096
|
+
* e.g. /* @tjs TjsClass TjsEquals * / → ['TjsClass', 'TjsEquals']
|
|
2097
|
+
*/
|
|
2098
|
+
function extractTjsModes(source: string): string[] {
|
|
2099
|
+
const modes: string[] = []
|
|
2100
|
+
const re = /\/\*\s*@tjs\s+((?:Tjs\w+\s*)+)\*\//g
|
|
2101
|
+
let m
|
|
2102
|
+
while ((m = re.exec(source)) !== null) {
|
|
2103
|
+
const words = m[1].trim().split(/\s+/)
|
|
2104
|
+
for (const word of words) {
|
|
2105
|
+
if (VALID_TJS_MODES.has(word) && !modes.includes(word)) {
|
|
2106
|
+
modes.push(word)
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return modes
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2071
2113
|
/**
|
|
2072
2114
|
* Extract @tjs annotations from source comments.
|
|
2073
2115
|
*
|
|
@@ -2234,6 +2276,11 @@ export function fromTS(
|
|
|
2234
2276
|
// Extract @tjs annotations for enriching TJS output
|
|
2235
2277
|
const tjsAnnotations = emitTJS ? extractTjsAnnotations(source) : []
|
|
2236
2278
|
|
|
2279
|
+
// Extract TJS mode directives from /* @tjs TjsClass ... */ comments
|
|
2280
|
+
const tjsModes = extractTjsModes(source)
|
|
2281
|
+
const hasTjsClass =
|
|
2282
|
+
tjsModes.includes('TjsClass') || tjsModes.includes('TjsStrict')
|
|
2283
|
+
|
|
2237
2284
|
// Parse TypeScript
|
|
2238
2285
|
const sourceFile = ts.createSourceFile(
|
|
2239
2286
|
filename,
|
|
@@ -2577,7 +2624,13 @@ export function fromTS(
|
|
|
2577
2624
|
const className = statement.name.getText(sourceFile)
|
|
2578
2625
|
handled = true
|
|
2579
2626
|
if (emitTJS) {
|
|
2580
|
-
const classDecl = transformClassToTJS(
|
|
2627
|
+
const classDecl = transformClassToTJS(
|
|
2628
|
+
statement,
|
|
2629
|
+
sourceFile,
|
|
2630
|
+
warnings,
|
|
2631
|
+
undefined,
|
|
2632
|
+
hasTjsClass
|
|
2633
|
+
)
|
|
2581
2634
|
tjsFunctions.push(classDecl)
|
|
2582
2635
|
} else {
|
|
2583
2636
|
classMetadata[className] = extractClassMetadata(
|
|
@@ -2677,9 +2730,10 @@ export function fromTS(
|
|
|
2677
2730
|
// Emit any remaining doc comments (after all statements)
|
|
2678
2731
|
emitDocCommentsBefore(Infinity)
|
|
2679
2732
|
|
|
2680
|
-
// Include source file annotation
|
|
2733
|
+
// Include source file annotation and TJS mode directives
|
|
2681
2734
|
const sourceFileName = filename || 'unknown'
|
|
2682
|
-
const
|
|
2735
|
+
const modesLine = tjsModes.length > 0 ? tjsModes.join('\n') + '\n\n' : ''
|
|
2736
|
+
const header = `${modesLine}/* tjs <- ${sourceFileName} */\n\n`
|
|
2683
2737
|
|
|
2684
2738
|
// Append embedded test comments (they were extracted from original source)
|
|
2685
2739
|
const testsSection =
|
|
@@ -1463,7 +1463,9 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1463
1463
|
expect(result.code).toContain('export class Foo')
|
|
1464
1464
|
})
|
|
1465
1465
|
|
|
1466
|
-
test('private
|
|
1466
|
+
test('private keyword stripped without converting to #', () => {
|
|
1467
|
+
// TS private is compile-time only — converting to # changes semantics
|
|
1468
|
+
// and breaks external access via (obj as any)._field
|
|
1467
1469
|
const result = fromTS(
|
|
1468
1470
|
`class Foo {
|
|
1469
1471
|
private cache: any = null
|
|
@@ -1480,18 +1482,15 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1480
1482
|
}`,
|
|
1481
1483
|
{ emitTJS: true }
|
|
1482
1484
|
)
|
|
1483
|
-
//
|
|
1484
|
-
expect(result.code).toContain('
|
|
1485
|
-
//
|
|
1486
|
-
expect(result.code).toContain('
|
|
1487
|
-
|
|
1488
|
-
expect(result.code).toContain('
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
})
|
|
1493
|
-
|
|
1494
|
-
test('private _backing field with public getter preserved correctly', () => {
|
|
1485
|
+
// private keyword stripped
|
|
1486
|
+
expect(result.code).not.toContain('private')
|
|
1487
|
+
// field names kept as-is (no # conversion)
|
|
1488
|
+
expect(result.code).not.toContain('#cache')
|
|
1489
|
+
expect(result.code).not.toContain('#instances')
|
|
1490
|
+
expect(result.code).toContain('this.cache')
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
test('private _backing field kept as-is with getter/setter', () => {
|
|
1495
1494
|
const result = fromTS(
|
|
1496
1495
|
`class User {
|
|
1497
1496
|
private _name: string = ''
|
|
@@ -1500,15 +1499,15 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1500
1499
|
}`,
|
|
1501
1500
|
{ emitTJS: true }
|
|
1502
1501
|
)
|
|
1503
|
-
// Backing field
|
|
1504
|
-
expect(result.code).toContain('#_name')
|
|
1505
|
-
expect(result.code).toContain('this
|
|
1506
|
-
// Public getter
|
|
1502
|
+
// Backing field kept as _name (not #_name)
|
|
1503
|
+
expect(result.code).not.toContain('#_name')
|
|
1504
|
+
expect(result.code).toContain('this._name')
|
|
1505
|
+
// Public getter/setter preserved
|
|
1507
1506
|
expect(result.code).toContain('get name()')
|
|
1508
1507
|
expect(result.code).toContain('set name(')
|
|
1509
1508
|
})
|
|
1510
1509
|
|
|
1511
|
-
test('private field name
|
|
1510
|
+
test('private field name unchanged in object literals', () => {
|
|
1512
1511
|
const result = fromTS(
|
|
1513
1512
|
`class Store {
|
|
1514
1513
|
private value: any = null
|
|
@@ -1518,10 +1517,9 @@ describe('fromTS — tosijs conversion edge cases', () => {
|
|
|
1518
1517
|
}`,
|
|
1519
1518
|
{ emitTJS: true }
|
|
1520
1519
|
)
|
|
1521
|
-
// this.value
|
|
1522
|
-
expect(result.code).toContain('this
|
|
1523
|
-
|
|
1524
|
-
expect(result.code).toContain('{ value:')
|
|
1520
|
+
// this.value stays as-is (not this.#value)
|
|
1521
|
+
expect(result.code).toContain('this.value')
|
|
1522
|
+
expect(result.code).not.toContain('#value')
|
|
1525
1523
|
})
|
|
1526
1524
|
|
|
1527
1525
|
test('symbol type produces Symbol example', () => {
|
|
@@ -92,6 +92,89 @@ console.log(result)
|
|
|
92
92
|
expect(result).toBe('') // [].join(', ') === ''
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
+
test('literal "any" emitted as runtime value in interface/type', () => {
|
|
96
|
+
// In tosijs form-validation.ts, an interface has a property typed as `any`:
|
|
97
|
+
// interface FormValidation { validity: any; ... }
|
|
98
|
+
//
|
|
99
|
+
// The TS→TJS→JS pipeline emits a Type() call with the literal `any`
|
|
100
|
+
// as a JS value: Type('FormValidation', undefined, { validity: any | undefined })
|
|
101
|
+
// which throws: ReferenceError: any is not defined
|
|
102
|
+
//
|
|
103
|
+
// The converter should emit null or skip the field for `any` types.
|
|
104
|
+
|
|
105
|
+
const source = `
|
|
106
|
+
export interface FormValidation {
|
|
107
|
+
internals: any
|
|
108
|
+
validity: any | undefined
|
|
109
|
+
validationMessage: string
|
|
110
|
+
willValidate: boolean
|
|
111
|
+
}
|
|
112
|
+
`
|
|
113
|
+
const tjsResult = fromTS(source, { emitTJS: true, filename: 'any-type.ts' })
|
|
114
|
+
const jsResult = tjs(tjsResult.code, {
|
|
115
|
+
filename: 'any-type.ts',
|
|
116
|
+
runTests: false,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Should not contain bare `any` as a runtime identifier
|
|
120
|
+
const safeCode = jsResult.code.replace(/^export /gm, '')
|
|
121
|
+
expect(() => {
|
|
122
|
+
new Function(safeCode)()
|
|
123
|
+
}).not.toThrow()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('TS private keyword should not convert to # (changes semantics)', () => {
|
|
127
|
+
// In tosijs component.ts:
|
|
128
|
+
// class Component { private static _tagName: string = '' }
|
|
129
|
+
// function elementCreator(componentClass: typeof Component) {
|
|
130
|
+
// componentClass._tagName = 'my-tag' // valid TS — private is compile-time only
|
|
131
|
+
// }
|
|
132
|
+
//
|
|
133
|
+
// The converter rewrites `private _tagName` to `#_tagName`, which makes
|
|
134
|
+
// the field a true JS private field. External access then fails:
|
|
135
|
+
// TypeError: Cannot access invalid private field
|
|
136
|
+
//
|
|
137
|
+
// TS `private` is compile-time access control, not JS `#` runtime privacy.
|
|
138
|
+
// The converter should leave `private` fields as regular fields (strip the keyword).
|
|
139
|
+
|
|
140
|
+
const source = `
|
|
141
|
+
class Component {
|
|
142
|
+
private static _tagName: string = ''
|
|
143
|
+
|
|
144
|
+
static getTag() {
|
|
145
|
+
return this._tagName
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setup(cls: typeof Component) {
|
|
150
|
+
(cls as any)._tagName = 'my-tag'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setup(Component)
|
|
154
|
+
const tag = Component.getTag()
|
|
155
|
+
`
|
|
156
|
+
const tjsResult = fromTS(source, {
|
|
157
|
+
emitTJS: true,
|
|
158
|
+
filename: 'private-kw.ts',
|
|
159
|
+
})
|
|
160
|
+
const jsResult = tjs(tjsResult.code, {
|
|
161
|
+
filename: 'private-kw.ts',
|
|
162
|
+
runTests: false,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Should not convert `private` to `#`
|
|
166
|
+
expect(jsResult.code).not.toContain('#_tagName')
|
|
167
|
+
|
|
168
|
+
// The code should execute without error
|
|
169
|
+
const safeCode = jsResult.code.replace(/^export /gm, '')
|
|
170
|
+
expect(() => {
|
|
171
|
+
const fn = new Function(safeCode + '\n return tag;')
|
|
172
|
+
const result = fn()
|
|
173
|
+
if (result !== 'my-tag')
|
|
174
|
+
throw new Error(`Expected 'my-tag', got '${result}'`)
|
|
175
|
+
}).not.toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
95
178
|
test('shorthand property assignment in destructuring converts', () => {
|
|
96
179
|
// component.test.ts fails to convert with:
|
|
97
180
|
// "Shorthand property assignments are valid only in destructuring patterns"
|