tjs-lang 0.8.5 → 0.8.6

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.8.5",
3
+ "version": "0.8.6",
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",
@@ -1095,19 +1095,33 @@ function transformGenericInterfaceToGeneric(
1095
1095
  if (declarationAnnotation?.text) {
1096
1096
  parts.push(`declaration ${declarationAnnotation.text}`)
1097
1097
  } else {
1098
- // Auto-generate declaration block from interface members
1098
+ // Auto-generate declaration block from interface members. Each member's
1099
+ // type is converted to a TJS example via `typeToExample` (the same converter
1100
+ // the non-generic interface path uses) rather than emitted as raw TS — so
1101
+ // `path: string` → `path: ''`, `touch: () => void` → a FunctionPredicate
1102
+ // example, and un-representable shapes (call-signature objects, etc.) degrade
1103
+ // to a safe placeholder instead of producing TJS that won't re-parse.
1104
+ // (The `@tjs declaration { … }` annotation path above is verbatim and
1105
+ // unaffected — authors can still hand-write precise type signatures.)
1099
1106
  const declMembers: string[] = []
1100
1107
  for (const member of node.members) {
1101
1108
  if (ts.isPropertySignature(member) && member.name) {
1102
1109
  const propName = member.name.getText(sourceFile)
1103
1110
  const optional = member.questionToken ? '?' : ''
1104
- const typeText = member.type ? member.type.getText(sourceFile) : 'any'
1105
- declMembers.push(`${propName}${optional}: ${typeText}`)
1111
+ const example = member.type
1112
+ ? typeToExample(member.type, undefined, warnings)
1113
+ : 'null'
1114
+ declMembers.push(`${propName}${optional}: ${example}`)
1106
1115
  } else if (ts.isMethodSignature(member) && member.name) {
1107
- // Method: name(params): returnType
1108
- const methodText = member.getText(sourceFile).trim()
1109
- // Remove trailing semicolon if present
1110
- declMembers.push(methodText.replace(/;$/, ''))
1116
+ // Method → a function example built from its return type (params are
1117
+ // descriptive; the predicate above does the actual runtime check).
1118
+ const propName = member.name.getText(sourceFile)
1119
+ const ret = member.type
1120
+ ? typeToExample(member.type, undefined, warnings)
1121
+ : 'null'
1122
+ declMembers.push(
1123
+ `${propName}: FunctionPredicate('function', { returns: ${ret} })`
1124
+ )
1111
1125
  }
1112
1126
  }
1113
1127
  if (declMembers.length > 0) {
@@ -1288,10 +1302,17 @@ function transformTypeAliasToType(
1288
1302
 
1289
1303
  const example = typeToExample(node.type, undefined, warnings)
1290
1304
 
1291
- // 'any' and 'undefined' — preserve original TS body for DTS round-tripping
1305
+ // 'any' and 'undefined' — un-representable in TJS (intersections with
1306
+ // `typeof`/index signatures, etc.). Degrade to an empty Type (validates as
1307
+ // anything) and preserve the original TS body as a SINGLE-LINE comment so the
1308
+ // DTS emitter can recover it. The collapse is essential: a multi-line type
1309
+ // body would leave lines 2+ uncommented = raw TS leaking into the block =
1310
+ // unparseable. (Representable shapes never reach here — they convert above.)
1292
1311
  if (example === 'any' || example === 'undefined') {
1293
- const originalType = node.type.getText(sourceFile).trim()
1294
- // Include the TS type body so the DTS emitter can recover it
1312
+ const originalType = node.type
1313
+ .getText(sourceFile)
1314
+ .trim()
1315
+ .replace(/\s+/g, ' ')
1295
1316
  return `Type ${typeName} {\n // TS: ${originalType}\n}`
1296
1317
  }
1297
1318
 
@@ -1423,25 +1444,35 @@ function transformGenericTypeAliasToGeneric(
1423
1444
  const typeBody = node.type
1424
1445
 
1425
1446
  if (typeBody && ts.isTypeLiteralNode(typeBody)) {
1426
- // Object type literal: { item: T; count: number }
1447
+ // Object type literal: { item: T; count: number }. Convert each member's
1448
+ // type to a TJS example (degrades gracefully) rather than emitting raw TS.
1427
1449
  const declMembers: string[] = []
1428
1450
  for (const member of typeBody.members) {
1429
1451
  if (ts.isPropertySignature(member) && member.name) {
1430
1452
  const propName = member.name.getText(sourceFile)
1431
1453
  const optional = member.questionToken ? '?' : ''
1432
- const typeText = member.type ? member.type.getText(sourceFile) : 'any'
1433
- declMembers.push(`${propName}${optional}: ${typeText}`)
1454
+ const example = member.type
1455
+ ? typeToExample(member.type, undefined, warnings)
1456
+ : 'null'
1457
+ declMembers.push(`${propName}${optional}: ${example}`)
1434
1458
  } else if (ts.isMethodSignature(member) && member.name) {
1435
- declMembers.push(member.getText(sourceFile).trim().replace(/;$/, ''))
1459
+ const propName = member.name.getText(sourceFile)
1460
+ const ret = member.type
1461
+ ? typeToExample(member.type, undefined, warnings)
1462
+ : 'null'
1463
+ declMembers.push(
1464
+ `${propName}: FunctionPredicate('function', { returns: ${ret} })`
1465
+ )
1436
1466
  }
1437
1467
  }
1438
1468
  if (declMembers.length > 0) {
1439
1469
  parts.push(`declaration {\n ${declMembers.join('\n ')}\n }`)
1440
1470
  }
1441
1471
  } else if (typeBody) {
1442
- // Complex type (conditional, mapped, intersection, etc.)
1443
- // Pass through the TS type body verbatim
1444
- const typeText = typeBody.getText(sourceFile).trim()
1472
+ // Complex type (conditional, mapped, intersection, …) — un-representable.
1473
+ // Keep the TS body as a SINGLE-LINE comment (collapse newlines, else lines
1474
+ // 2+ leak as raw TS and won't re-parse).
1475
+ const typeText = typeBody.getText(sourceFile).trim().replace(/\s+/g, ' ')
1445
1476
  parts.push(`declaration {\n // TS: ${typeText}\n }`)
1446
1477
  }
1447
1478
  }
@@ -312,3 +312,51 @@ describe('@tjs annotations', () => {
312
312
  })
313
313
  })
314
314
  })
315
+
316
+ // Regression: TS→TJS must never emit TJS that won't re-parse. These real-world
317
+ // shapes (from tosijs) used to leak raw TS into Type/Generic blocks — generic
318
+ // interfaces emitted raw member types in `declaration {}`, and intersections /
319
+ // complex types emitted MULTI-LINE `// TS:` comments whose lines 2+ leaked as
320
+ // raw TS. fromTS now converts members via typeToExample and collapses
321
+ // un-representable bodies to a single-line comment (graceful degradation).
322
+ describe('TS→TJS round-trips (no raw-TS leak into TJS blocks)', () => {
323
+ const { tjs } = require('./index') as { tjs: (s: string) => { code: string } }
324
+ const roundTrips = (ts: string) => {
325
+ const emitted = fromTS(ts, { emitTJS: true }).code
326
+ expect(() => tjs(emitted)).not.toThrow() // emitted TJS must re-parse
327
+ return emitted
328
+ }
329
+
330
+ it('generic interface with complex members (arrow, generic arrow, call-sig)', () => {
331
+ const out = roundTrips(`export interface Acc<T = any> {
332
+ value: T
333
+ path: string
334
+ touch: () => void
335
+ bind: <E extends Element = Element>(el: E, b: any) => void
336
+ find: { (selector: (item: any) => any, value: any): any }
337
+ }`)
338
+ // members converted to examples, not raw TS
339
+ expect(out).toContain("path: ''")
340
+ expect(out).toContain('touch: FunctionPredicate')
341
+ expect(out).not.toMatch(/path: string\b/) // no raw type leak
342
+ })
343
+
344
+ it('intersection type alias (typeof / index signature) degrades, single-line', () => {
345
+ const out = roundTrips(`export type ProxyObj = Props<object> & {
346
+ [key: string]: ProxyObj | string | null
347
+ }`)
348
+ // un-representable → comment-only Type, collapsed to one line (no leak)
349
+ expect(out).toMatch(/\/\/ TS:.*&/)
350
+ expect(out).not.toMatch(/\n\s*\[key: string\]:/) // the body didn't leak raw
351
+ })
352
+
353
+ it('generic type alias with object body + arrow member', () => {
354
+ roundTrips(`export type Wrap<T> = { value: T; build: (x: T) => T }`)
355
+ })
356
+
357
+ it('plain type alias with arrow + union return still works', () => {
358
+ roundTrips(
359
+ `export type AnyFunction = (...args: any[]) => any | Promise<any>`
360
+ )
361
+ })
362
+ })