tjs-lang 0.8.4 → 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/CLAUDE.md +22 -0
- package/dist/index.js +66 -66
- package/dist/index.js.map +3 -3
- package/dist/src/lang/browser-from-ts.d.ts +38 -0
- package/dist/src/lang/browser.d.ts +13 -0
- package/dist/src/lang/ts-cdn-shim.d.ts +2 -0
- package/dist/tjs-browser-from-ts.js +58 -0
- package/dist/tjs-browser-from-ts.js.map +7 -0
- package/dist/tjs-browser.js +370 -0
- package/dist/tjs-browser.js.map +7 -0
- package/dist/tjs-from-ts.js +18 -18
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +1 -1
- package/dist/tjs-lang.js.map +2 -2
- package/llms.txt +2 -0
- package/package.json +15 -1
- package/src/lang/browser-bundle.test.ts +69 -0
- package/src/lang/browser-from-ts.ts +81 -0
- package/src/lang/browser.ts +13 -0
- package/src/lang/emitters/from-ts.ts +48 -17
- package/src/lang/from-ts.test.ts +48 -0
- package/src/lang/ts-cdn-shim.ts +35 -0
package/llms.txt
CHANGED
|
@@ -44,6 +44,8 @@ This file is a navigation index for AI agents. It does not contain the docs them
|
|
|
44
44
|
- `tjs-lang` → `src/index.ts` — main entry: `Agent`, `AgentVM`, `ajs`, `tjs`.
|
|
45
45
|
- `tjs-lang/lang` → `src/lang/transpiler.ts` — language tools only: `tjs`, `transpile`.
|
|
46
46
|
- `tjs-lang/lang/from-ts` → `src/lang/emitters/from-ts.ts` — TypeScript → TJS/JS transpilation.
|
|
47
|
+
- `tjs-lang/browser` → `src/lang/browser.ts` (dist `tjs-browser.js`) — **self-contained** TJS/AJS transpiler (acorn + tosijs-schema inlined); `import()` from any CDN, zero config.
|
|
48
|
+
- `tjs-lang/browser/from-ts` → `src/lang/browser-from-ts.ts` (dist `tjs-browser-from-ts.js`) — browser TS→TJS; lazy-loads the `typescript` compiler from a CDN (default `https://esm.sh/typescript@5` — the only CDN that serves it reliably; jsDelivr `+esm`/esm.run time out). Override with `fromTS(src, { typescriptUrl })` or preload `globalThis.__TJS_TS__`.
|
|
47
49
|
- `tjs-lang/vm` → `src/vm/index.ts` — VM and atoms.
|
|
48
50
|
- `tjs-lang/eval` → `src/lang/eval.ts` — `Eval`, `SafeFunction`.
|
|
49
51
|
- `tjs-lang/batteries` → `src/batteries/index.ts` — LM Studio integration (lazy, local-only).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.8.
|
|
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",
|
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
"lang/from-ts": [
|
|
34
34
|
"./dist/src/lang/emitters/from-ts.d.ts"
|
|
35
35
|
],
|
|
36
|
+
"browser": [
|
|
37
|
+
"./dist/src/lang/browser.d.ts"
|
|
38
|
+
],
|
|
39
|
+
"browser/from-ts": [
|
|
40
|
+
"./dist/src/lang/browser-from-ts.d.ts"
|
|
41
|
+
],
|
|
36
42
|
"vm": [
|
|
37
43
|
"./dist/src/vm/index.d.ts"
|
|
38
44
|
],
|
|
@@ -62,6 +68,14 @@
|
|
|
62
68
|
"types": "./dist/src/lang/emitters/from-ts.d.ts",
|
|
63
69
|
"default": "./dist/tjs-from-ts.js"
|
|
64
70
|
},
|
|
71
|
+
"./browser": {
|
|
72
|
+
"types": "./dist/src/lang/browser.d.ts",
|
|
73
|
+
"default": "./dist/tjs-browser.js"
|
|
74
|
+
},
|
|
75
|
+
"./browser/from-ts": {
|
|
76
|
+
"types": "./dist/src/lang/browser-from-ts.d.ts",
|
|
77
|
+
"default": "./dist/tjs-browser-from-ts.js"
|
|
78
|
+
},
|
|
65
79
|
"./vm": {
|
|
66
80
|
"bun": "./src/vm/index.ts",
|
|
67
81
|
"types": "./dist/src/vm/index.d.ts",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the browser bundles' defining property: they must be **self-contained**
|
|
3
|
+
* (no bare/node imports) so a single `import('https://cdn/.../tjs-browser.js')`
|
|
4
|
+
* works on any CDN with zero import-map/config. Builds in-test with esbuild, so
|
|
5
|
+
* it doesn't depend on `dist/` existing.
|
|
6
|
+
*
|
|
7
|
+
* CDN reality (verified in a real headless browser, 2026-06): the self-contained
|
|
8
|
+
* TJS/AJS bundle loads from ANY CDN; for the TypeScript path, the compiler is
|
|
9
|
+
* lazy-loaded and **esm.sh is the only CDN that reliably serves `typescript`**
|
|
10
|
+
* (jsDelivr `+esm` / esm.run time out on its ~10MB CJS; skypack is dead).
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'bun:test'
|
|
13
|
+
import { buildSync } from 'esbuild'
|
|
14
|
+
import { DEFAULT_TYPESCRIPT_URL } from './browser-from-ts'
|
|
15
|
+
|
|
16
|
+
const HERE = import.meta.dir
|
|
17
|
+
|
|
18
|
+
function bundle(entry: string, alias?: Record<string, string>): string {
|
|
19
|
+
const r = buildSync({
|
|
20
|
+
entryPoints: [`${HERE}/${entry}`],
|
|
21
|
+
bundle: true,
|
|
22
|
+
format: 'esm',
|
|
23
|
+
platform: 'neutral',
|
|
24
|
+
write: false,
|
|
25
|
+
external: [],
|
|
26
|
+
alias,
|
|
27
|
+
})
|
|
28
|
+
return r.outputFiles[0].text
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Bare (non-relative, non-URL) static + dynamic import specifiers. */
|
|
32
|
+
function bareImports(src: string): string[] {
|
|
33
|
+
const found: string[] = []
|
|
34
|
+
for (const m of src.matchAll(
|
|
35
|
+
/(?:^|[;\n}])import\s*(?:[^"';]*?from)?\s*["']([^"']+)["']/g
|
|
36
|
+
))
|
|
37
|
+
found.push(m[1])
|
|
38
|
+
for (const m of src.matchAll(/import\(\s*["']([^"']+)["']\s*\)/g))
|
|
39
|
+
found.push(m[1])
|
|
40
|
+
return [...new Set(found)].filter(
|
|
41
|
+
(s) => !s.startsWith('.') && !s.startsWith('http')
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('browser bundles are self-contained (CDN drop-in)', () => {
|
|
46
|
+
it('tjs-browser inlines all deps — no bare/node imports', () => {
|
|
47
|
+
const out = bundle('browser.ts')
|
|
48
|
+
// bareImports([]) already implies no `node:` imports (they'd be bare too);
|
|
49
|
+
// a raw `out.includes('node:')` would false-positive on `{node: ...}` props.
|
|
50
|
+
expect(bareImports(out)).toEqual([])
|
|
51
|
+
// sanity: acorn really got inlined (parser internals present)
|
|
52
|
+
expect(out.length).toBeGreaterThan(100_000)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('tjs-browser-from-ts: no bare imports, TS lazy (not inlined)', () => {
|
|
56
|
+
const out = bundle('browser-from-ts.ts', {
|
|
57
|
+
typescript: `${HERE}/ts-cdn-shim.ts`,
|
|
58
|
+
})
|
|
59
|
+
expect(bareImports(out)).toEqual([])
|
|
60
|
+
// typescript must NOT be bundled in (it's ~10MB) — the shim swaps it out
|
|
61
|
+
expect(out.length).toBeLessThan(300_000)
|
|
62
|
+
// the only runtime dependency is the configurable compiler URL
|
|
63
|
+
expect(out).toContain('esm.sh/typescript')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('default TypeScript CDN is esm.sh (the only one that serves it reliably)', () => {
|
|
67
|
+
expect(DEFAULT_TYPESCRIPT_URL).toBe('https://esm.sh/typescript@5')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser TypeScript→TJS — lazy-loads the TypeScript compiler from a CDN on
|
|
3
|
+
* demand, so the bundle stays small and only pays the (~MB) compiler download
|
|
4
|
+
* when you actually transpile TS. `acorn` + `tosijs-schema` are bundled in; the
|
|
5
|
+
* `typescript` import in `from-ts.ts` is aliased (at build time) to a Proxy that
|
|
6
|
+
* forwards to the lazily-loaded compiler (see `ts-cdn-shim.ts`).
|
|
7
|
+
*
|
|
8
|
+
* const { fromTS } = await import('https://cdn.jsdelivr.net/npm/tjs-lang/dist/tjs-browser-from-ts.js')
|
|
9
|
+
* const { code } = await fromTS('const x: number = 1', { emitTJS: true })
|
|
10
|
+
*
|
|
11
|
+
* Zero config by default. Override the compiler source with `typescriptUrl` (or
|
|
12
|
+
* preload it yourself onto `globalThis.__TJS_TS__`).
|
|
13
|
+
*/
|
|
14
|
+
import type { FromTSOptions, FromTSResult } from './emitters/from-ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default CDN for the TypeScript compiler. **esm.sh** — verified the only CDN
|
|
18
|
+
* that reliably serves `typescript` as ESM (default export = the compiler
|
|
19
|
+
* namespace), in ~700ms. The on-the-fly bundlers choke on typescript's ~10MB
|
|
20
|
+
* CommonJS size: jsDelivr `+esm`, esm.run → timeout; skypack → dead. So esm.sh
|
|
21
|
+
* it is. Override via `BrowserFromTSOptions.typescriptUrl` (e.g. a self-hosted
|
|
22
|
+
* copy) or preload your own compiler onto `globalThis.__TJS_TS__`.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: only the *TypeScript* path depends on this. The TJS/AJS transpiler
|
|
25
|
+
* (`tjs-lang/browser`) is fully self-contained and loads from ANY CDN.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_TYPESCRIPT_URL = 'https://esm.sh/typescript@5'
|
|
28
|
+
|
|
29
|
+
const TS_GLOBAL = '__TJS_TS__'
|
|
30
|
+
|
|
31
|
+
export interface BrowserFromTSOptions extends FromTSOptions {
|
|
32
|
+
/** Override the CDN URL the TypeScript compiler is lazy-loaded from. */
|
|
33
|
+
typescriptUrl?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let loading: Promise<void> | null = null
|
|
37
|
+
|
|
38
|
+
/** Lazy-load the TypeScript compiler (once) onto `globalThis.__TJS_TS__`. */
|
|
39
|
+
export function loadTypeScript(
|
|
40
|
+
url: string = DEFAULT_TYPESCRIPT_URL
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if ((globalThis as any)[TS_GLOBAL]) return Promise.resolve()
|
|
43
|
+
if (!loading) {
|
|
44
|
+
loading = import(/* @vite-ignore */ /* webpackIgnore: true */ url).then(
|
|
45
|
+
(mod) => {
|
|
46
|
+
const ts = mod?.default ?? mod
|
|
47
|
+
if (!ts || typeof ts.createSourceFile !== 'function') {
|
|
48
|
+
loading = null
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Loaded ${url} but it is not a usable TypeScript compiler ` +
|
|
51
|
+
`(no createSourceFile). Try a different typescriptUrl.`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
;(globalThis as any)[TS_GLOBAL] = ts
|
|
55
|
+
},
|
|
56
|
+
(e) => {
|
|
57
|
+
loading = null
|
|
58
|
+
throw e
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return loading
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Transpile TypeScript → TJS (or JS) in the browser. Lazy-loads the TypeScript
|
|
67
|
+
* compiler on first call, then runs the same `fromTS` logic as Node.
|
|
68
|
+
*/
|
|
69
|
+
export async function fromTS(
|
|
70
|
+
source: string,
|
|
71
|
+
options: BrowserFromTSOptions = {}
|
|
72
|
+
): Promise<FromTSResult> {
|
|
73
|
+
const { typescriptUrl, ...rest } = options
|
|
74
|
+
await loadTypeScript(typescriptUrl)
|
|
75
|
+
// Imported after the compiler is set; from-ts's `typescript` import is aliased
|
|
76
|
+
// to the Proxy shim, which now forwards to the loaded compiler.
|
|
77
|
+
const { fromTS: nodeFromTS } = await import('./emitters/from-ts')
|
|
78
|
+
return nodeFromTS(source, rest)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type { FromTSOptions, FromTSResult } from './emitters/from-ts'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained browser entry — the TJS/AJS transpiler with `acorn` and
|
|
3
|
+
* `tosijs-schema` bundled in (no external/bare imports). Build target
|
|
4
|
+
* `tjs-browser` (see `scripts/build.ts`) emits a single ESM file you can
|
|
5
|
+
* `import()` from any CDN with zero import-map / config:
|
|
6
|
+
*
|
|
7
|
+
* const { tjs } = await import('https://cdn.jsdelivr.net/npm/tjs-lang/dist/tjs-browser.js')
|
|
8
|
+
* const { code } = tjs("function greet(name: 'x'): '' { return 'hi' }")
|
|
9
|
+
*
|
|
10
|
+
* For TypeScript→TJS in the browser, use `tjs-lang/browser/from-ts` (which
|
|
11
|
+
* lazy-loads the TypeScript compiler from a CDN on demand).
|
|
12
|
+
*/
|
|
13
|
+
export * from './transpiler'
|
|
@@ -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
|
|
1105
|
-
|
|
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
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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' —
|
|
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
|
|
1294
|
-
|
|
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
|
|
1433
|
-
|
|
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
|
-
|
|
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,
|
|
1443
|
-
//
|
|
1444
|
-
|
|
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
|
}
|
package/src/lang/from-ts.test.ts
CHANGED
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time stand-in for the `typescript` module, used ONLY by the
|
|
3
|
+
* `tjs-browser-from-ts` bundle (via an esbuild `alias`). `from-ts.ts` does
|
|
4
|
+
* `import ts from 'typescript'` and accesses `ts.<x>` at call time; this Proxy
|
|
5
|
+
* forwards every access to the compiler that `browser-from-ts.ts` lazy-loads
|
|
6
|
+
* from a CDN and stashes on `globalThis.__TJS_TS__`.
|
|
7
|
+
*
|
|
8
|
+
* Because all of from-ts's `ts.<x>` uses are inside functions (run when `fromTS`
|
|
9
|
+
* is called, never at module load), the access is always deferred to after the
|
|
10
|
+
* compiler is loaded — so the swap needs no changes to `from-ts.ts`. tsc still
|
|
11
|
+
* typechecks `from-ts.ts` against the real `typescript` types; only the browser
|
|
12
|
+
* BUILD aliases the runtime to this shim.
|
|
13
|
+
*/
|
|
14
|
+
const TS_GLOBAL = '__TJS_TS__'
|
|
15
|
+
|
|
16
|
+
function compiler(): any {
|
|
17
|
+
const ts = (globalThis as any)[TS_GLOBAL]
|
|
18
|
+
if (!ts) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'TypeScript not loaded. Use fromTS() from tjs-lang/browser/from-ts, ' +
|
|
21
|
+
'which lazy-loads the compiler before transpiling.'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return ts
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const shim: any = new Proxy(
|
|
28
|
+
{},
|
|
29
|
+
{
|
|
30
|
+
get: (_t, prop) => compiler()[prop],
|
|
31
|
+
has: (_t, prop) => prop in compiler(),
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export default shim
|