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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.6.35",
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
@@ -20,7 +20,7 @@ import { emit } from './commands/emit'
20
20
  import { convert } from './commands/convert'
21
21
  import { test } from './commands/test'
22
22
 
23
- const VERSION = '0.6.35'
23
+ const VERSION = '0.6.37'
24
24
 
25
25
  const HELP = `
26
26
  tjs - Typed JavaScript CLI
@@ -211,7 +211,7 @@ class User {
211
211
  expect(code).toContain("constructor(name: '')")
212
212
  })
213
213
 
214
- it('converts private fields to # syntax', () => {
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
- expect(code).toContain('#count')
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(nonNullTypes[0], checker)
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
- // First pass: collect private field mappings (TS private -> JS #)
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
- for (const member of node.members) {
1604
- if (ts.isPropertyDeclaration(member) && member.name) {
1605
- const propName = member.name.getText(sourceFile)
1606
- const isPrivate = member.modifiers?.some(
1607
- (m) => m.kind === ts.SyntaxKind.PrivateKeyword
1608
- )
1609
- if (isPrivate && !propName.startsWith('#')) {
1610
- privateFieldMap.set(propName, `#${propName}`)
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(statement, sourceFile, warnings)
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 for error reporting
2733
+ // Include source file annotation and TJS mode directives
2681
2734
  const sourceFileName = filename || 'unknown'
2682
- const header = `/* tjs <- ${sourceFileName} */\n\n`
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 fields replaced in all access patterns', () => {
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
- // this.cache -> this.#cache
1484
- expect(result.code).toContain('this.#cache')
1485
- // f.cache -> f.#cache (instance via variable)
1486
- expect(result.code).toContain('f.#cache')
1487
- // Foo.instances -> Foo.#instances (static via class name)
1488
- expect(result.code).toContain('Foo.#instances')
1489
- // No un-hashed private access left (dot followed by bare name)
1490
- expect(result.code).not.toContain('.cache')
1491
- expect(result.code).not.toContain('.instances')
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 renamed
1504
- expect(result.code).toContain('#_name')
1505
- expect(result.code).toContain('this.#_name')
1506
- // Public getter NOT renamed
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 not confused with object literal key', () => {
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 this.#value
1522
- expect(result.code).toContain('this.#value')
1523
- // object key { value: ... } stays as 'value' (not '#value')
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"