tjs-lang 0.6.13 → 0.6.15
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 +40 -17
- package/demo/docs.json +1 -1
- package/dist/index.js +119 -116
- package/dist/index.js.map +8 -8
- package/dist/src/lang/parser-transforms.d.ts +15 -0
- package/dist/src/lang/runtime.d.ts +4 -2
- package/dist/src/types/Type.d.ts +41 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/tjs-full.js +119 -116
- package/dist/tjs-full.js.map +8 -8
- package/dist/tjs-vm.js +37 -37
- package/dist/tjs-vm.js.map +4 -4
- package/docs/function-predicate-design.md +180 -0
- package/package.json +1 -1
- package/src/cli/tjs.ts +1 -1
- package/src/lang/emitters/dts.test.ts +58 -0
- package/src/lang/emitters/dts.ts +84 -0
- package/src/lang/emitters/from-ts.ts +217 -6
- package/src/lang/function-predicate.test.ts +188 -0
- package/src/lang/parser-transforms.ts +103 -0
- package/src/lang/parser.ts +2 -0
- package/src/lang/runtime.ts +4 -0
- package/src/lang/typescript-syntax.test.ts +154 -5
- package/src/types/Type.ts +148 -0
- package/src/types/index.ts +5 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# FunctionPredicate: Design Notes
|
|
2
|
+
|
|
3
|
+
_First-class function types in TJS, using the same pattern as Type/Generic._
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
TJS has no way to express "this parameter must be a function with this
|
|
10
|
+
signature." Currently:
|
|
11
|
+
|
|
12
|
+
- `() => void` in TypeScript becomes `undefined` in fromTS output
|
|
13
|
+
- There's no TJS syntax for function-typed parameters
|
|
14
|
+
- Callbacks, event handlers, and higher-order functions lose their
|
|
15
|
+
type information at the boundary
|
|
16
|
+
|
|
17
|
+
## Design Principles
|
|
18
|
+
|
|
19
|
+
1. **Functions are values** — a function should be usable as a type example,
|
|
20
|
+
just like `0` means "integer" and `''` means "string"
|
|
21
|
+
2. **FunctionPredicate should work like Type/Generic** — same pattern of
|
|
22
|
+
predicate-based checking, introspection via metadata
|
|
23
|
+
3. **The return contract is part of the type** — `->`, `-?`, and `-!` are
|
|
24
|
+
meaningful distinctions in the function's contract
|
|
25
|
+
|
|
26
|
+
## The Three Return Contracts
|
|
27
|
+
|
|
28
|
+
| Marker | Name | Meaning |
|
|
29
|
+
|--------|------|---------|
|
|
30
|
+
| `->` | `returns` | Verified at transpile time (signature test) |
|
|
31
|
+
| `-?` | `checkedReturns` | Verified at transpile time AND runtime |
|
|
32
|
+
| `-!` | `assertReturns` | Declared but not verified (metadata only) |
|
|
33
|
+
|
|
34
|
+
These are not just build options — they describe the **trust level** of
|
|
35
|
+
the function's return type. A function with `-?` makes a stronger promise
|
|
36
|
+
than one with `-!`.
|
|
37
|
+
|
|
38
|
+
## Syntax: Function as Type Example
|
|
39
|
+
|
|
40
|
+
The most TJS-idiomatic approach — a function IS its own type:
|
|
41
|
+
|
|
42
|
+
```tjs
|
|
43
|
+
// This function's signature IS a type
|
|
44
|
+
function formatter(input: '', options: { locale: 'en' }) -? '' {
|
|
45
|
+
return input
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// fn must match formatter's contract
|
|
49
|
+
function process(fn: formatter) {
|
|
50
|
+
const result = fn('hello', { locale: 'fr' })
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The runtime check for `fn: formatter`:
|
|
55
|
+
1. `typeof fn === 'function'`
|
|
56
|
+
2. `fn.__tjs` exists (it's a TJS-typed function)
|
|
57
|
+
3. `fn.__tjs.params` shape-matches `formatter.__tjs.params`
|
|
58
|
+
4. `fn.__tjs.returns` matches `formatter.__tjs.returns`
|
|
59
|
+
|
|
60
|
+
Untyped functions (no `__tjs`) would fail the check — they don't have
|
|
61
|
+
the metadata to verify against. Use `!` (unsafe) to skip the check for
|
|
62
|
+
interop with plain JS callbacks.
|
|
63
|
+
|
|
64
|
+
## Syntax: Explicit FunctionPredicate
|
|
65
|
+
|
|
66
|
+
For cases where you want to declare a function type without writing an
|
|
67
|
+
example function:
|
|
68
|
+
|
|
69
|
+
```tjs
|
|
70
|
+
FunctionPredicate Formatter {
|
|
71
|
+
description: 'formats a string with locale options'
|
|
72
|
+
params: { input: '', options: { locale: 'en' } }
|
|
73
|
+
returns: ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Or with checked returns:
|
|
77
|
+
FunctionPredicate Validator {
|
|
78
|
+
params: { value: null }
|
|
79
|
+
checkedReturns: false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Or declared-only returns:
|
|
83
|
+
FunctionPredicate Callback {
|
|
84
|
+
params: { event: { type: '', target: null } }
|
|
85
|
+
assertReturns: undefined
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Syntax: FunctionPredicate from Function
|
|
90
|
+
|
|
91
|
+
Create a type from an existing function's metadata:
|
|
92
|
+
|
|
93
|
+
```tjs
|
|
94
|
+
function myFormatter(input: '', options: { locale: 'en' }) -? '' {
|
|
95
|
+
return input
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract the type from the function
|
|
99
|
+
FunctionPredicate Formatter(myFormatter, 'string formatter with locale')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This is analogous to `Type Name 'example'` — the function itself is the
|
|
103
|
+
example value, and its `__tjs` metadata defines the type.
|
|
104
|
+
|
|
105
|
+
## Runtime Representation
|
|
106
|
+
|
|
107
|
+
A FunctionPredicate at runtime would be an object with:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
{
|
|
111
|
+
check(fn) { ... }, // returns boolean
|
|
112
|
+
params: { ... }, // param descriptors
|
|
113
|
+
returns: { ... }, // return type descriptor
|
|
114
|
+
returnContract: 'checked' | 'returns' | 'assert',
|
|
115
|
+
description: '...',
|
|
116
|
+
default: exampleFn, // the example function, if provided
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This matches the shape of `Type()` — `check`, `default`, `description`.
|
|
121
|
+
|
|
122
|
+
## Validation Levels
|
|
123
|
+
|
|
124
|
+
When checking `fn: SomeType` where SomeType is a FunctionPredicate:
|
|
125
|
+
|
|
126
|
+
| Check | What it verifies |
|
|
127
|
+
|-------|------------------|
|
|
128
|
+
| `typeof fn === 'function'` | It's callable |
|
|
129
|
+
| `fn.__tjs` exists | It's a TJS-typed function |
|
|
130
|
+
| Param count matches | Same arity (or compatible) |
|
|
131
|
+
| Param types match | Each param's type descriptor matches |
|
|
132
|
+
| Return type matches | Return type descriptor matches |
|
|
133
|
+
| Return contract | At least as strict as required |
|
|
134
|
+
|
|
135
|
+
Return contract strictness: `checkedReturns` (-?) > `returns` (->) > `assertReturns` (-!).
|
|
136
|
+
A `checkedReturns` function satisfies any requirement.
|
|
137
|
+
A `returns` function satisfies `returns` or `assertReturns`.
|
|
138
|
+
An `assertReturns` function only satisfies `assertReturns`.
|
|
139
|
+
|
|
140
|
+
## Compatibility with Untyped Functions
|
|
141
|
+
|
|
142
|
+
Plain JS functions have no `__tjs` metadata. Options:
|
|
143
|
+
|
|
144
|
+
1. **Strict**: Reject untyped functions (safe but hostile to JS interop)
|
|
145
|
+
2. **Lenient**: Accept any function, only validate if `__tjs` exists
|
|
146
|
+
3. **Unsafe marker**: Use `!` to skip the check for known-untyped callbacks
|
|
147
|
+
|
|
148
|
+
Option 3 is most consistent with TJS's existing patterns:
|
|
149
|
+
|
|
150
|
+
```tjs
|
|
151
|
+
// Strict — fn must have matching __tjs metadata
|
|
152
|
+
function process(fn: formatter) { ... }
|
|
153
|
+
|
|
154
|
+
// Lenient — fn just needs to be callable
|
|
155
|
+
function process(! fn: formatter) { ... }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Relationship to Existing Features
|
|
159
|
+
|
|
160
|
+
- **Type**: FunctionPredicate IS a Type — just one that checks function
|
|
161
|
+
signatures specifically. Could be implemented as a special case of Type
|
|
162
|
+
with a built-in predicate that introspects `__tjs`.
|
|
163
|
+
- **Generic**: FunctionPredicate could be generic too —
|
|
164
|
+
`FunctionPredicate Mapper<T, U> { params: { value: T }, returns: U }`
|
|
165
|
+
- **declaration block**: FunctionPredicates would benefit from declaration
|
|
166
|
+
blocks for `.d.ts` emission, same as Generic.
|
|
167
|
+
|
|
168
|
+
## Implementation Path
|
|
169
|
+
|
|
170
|
+
1. **Runtime**: Add `FunctionPredicate()` to the TJS runtime alongside
|
|
171
|
+
`Type()` and `Generic()`. Returns a type guard that checks `__tjs`
|
|
172
|
+
metadata on functions.
|
|
173
|
+
2. **Parser**: Recognize `FunctionPredicate` as a declaration keyword
|
|
174
|
+
(same as `Type`, `Generic`). Parse the block or function-argument form.
|
|
175
|
+
3. **Metadata**: The `__tjs` metadata for the return type already includes
|
|
176
|
+
`type` — add a `contract` field for the marker.
|
|
177
|
+
4. **fromTS**: When converting `(x: number) => string` types, emit a
|
|
178
|
+
FunctionPredicate instead of `undefined`.
|
|
179
|
+
5. **Inference**: When a function is used as a `:` param type, check if
|
|
180
|
+
it has `__tjs` metadata and validate the caller's function against it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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
|
@@ -437,6 +437,64 @@ export Generic Box<T> {
|
|
|
437
437
|
})
|
|
438
438
|
})
|
|
439
439
|
|
|
440
|
+
describe('generateDTS — FunctionPredicate declarations', () => {
|
|
441
|
+
it('should emit FunctionPredicate as TS function type', () => {
|
|
442
|
+
const source = `
|
|
443
|
+
export FunctionPredicate Callback {
|
|
444
|
+
params: { x: 0, y: '' }
|
|
445
|
+
returns: false
|
|
446
|
+
}
|
|
447
|
+
`
|
|
448
|
+
const result = transpileToJS(source, { runTests: false })
|
|
449
|
+
const dts = generateDTS(result, source)
|
|
450
|
+
|
|
451
|
+
expect(dts).toContain(
|
|
452
|
+
'export type Callback = (x: number, y: string) => boolean;'
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('should emit FunctionPredicate with no params as zero-arg function', () => {
|
|
457
|
+
const source = `
|
|
458
|
+
export FunctionPredicate Thunk {
|
|
459
|
+
returns: 0
|
|
460
|
+
}
|
|
461
|
+
`
|
|
462
|
+
const result = transpileToJS(source, { runTests: false })
|
|
463
|
+
const dts = generateDTS(result, source)
|
|
464
|
+
|
|
465
|
+
expect(dts).toContain('export type Thunk = () => number;')
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('should emit FunctionPredicate with no return as void', () => {
|
|
469
|
+
const source = `
|
|
470
|
+
export FunctionPredicate SideEffect {
|
|
471
|
+
params: { msg: '' }
|
|
472
|
+
}
|
|
473
|
+
`
|
|
474
|
+
const result = transpileToJS(source, { runTests: false })
|
|
475
|
+
const dts = generateDTS(result, source)
|
|
476
|
+
|
|
477
|
+
expect(dts).toContain('export type SideEffect = (msg: string) => void;')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should not emit non-exported FunctionPredicate when exports exist', () => {
|
|
481
|
+
const source = `
|
|
482
|
+
FunctionPredicate Internal {
|
|
483
|
+
params: { x: 0 }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function use(fn: Internal) {
|
|
487
|
+
return fn(1)
|
|
488
|
+
}
|
|
489
|
+
`
|
|
490
|
+
const result = transpileToJS(source, { runTests: false })
|
|
491
|
+
const dts = generateDTS(result, source)
|
|
492
|
+
|
|
493
|
+
expect(dts).not.toContain('type Internal')
|
|
494
|
+
expect(dts).toContain('export declare function use(')
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
440
498
|
describe('generateDTS — mixed declarations', () => {
|
|
441
499
|
it('should handle file with functions, classes, types, and generics', () => {
|
|
442
500
|
const source = `
|
package/src/lang/emitters/dts.ts
CHANGED
|
@@ -169,6 +169,12 @@ function detectExports(source: string): Map<string, ExportInfo> {
|
|
|
169
169
|
result.set(m[1], { exported: true, isDefault: false })
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// export FunctionPredicate Name
|
|
173
|
+
const fpRe = /^[ \t]*export\s+FunctionPredicate\s+(\w+)/gm
|
|
174
|
+
while ((m = fpRe.exec(source)) !== null) {
|
|
175
|
+
result.set(m[1], { exported: true, isDefault: false })
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
// export { Name, Name2, ... } — re-export form
|
|
173
179
|
const reExportRe = /^[ \t]*export\s*\{([^}]+)\}/gm
|
|
174
180
|
while ((m = reExportRe.exec(source)) !== null) {
|
|
@@ -184,6 +190,62 @@ function detectExports(source: string): Map<string, ExportInfo> {
|
|
|
184
190
|
return result
|
|
185
191
|
}
|
|
186
192
|
|
|
193
|
+
/** Info about a FunctionPredicate declaration */
|
|
194
|
+
interface FunctionPredicateInfo {
|
|
195
|
+
params: { name: string; example: string }[]
|
|
196
|
+
returns?: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Detect FunctionPredicate declarations and extract their param/return specs */
|
|
200
|
+
function detectFunctionPredicates(
|
|
201
|
+
source: string
|
|
202
|
+
): Map<string, FunctionPredicateInfo> {
|
|
203
|
+
const result = new Map<string, FunctionPredicateInfo>()
|
|
204
|
+
|
|
205
|
+
// Block form: FunctionPredicate Name { params: { ... } returns: ... }
|
|
206
|
+
const blockRe = /^[ \t]*(?:export\s+)?FunctionPredicate\s+(\w+)\s*\{/gm
|
|
207
|
+
let m
|
|
208
|
+
while ((m = blockRe.exec(source)) !== null) {
|
|
209
|
+
const name = m[1]
|
|
210
|
+
const blockStart = m.index + m[0].length - 1
|
|
211
|
+
|
|
212
|
+
// Find matching closing brace
|
|
213
|
+
let depth = 1
|
|
214
|
+
let i = blockStart + 1
|
|
215
|
+
while (i < source.length && depth > 0) {
|
|
216
|
+
if (source[i] === '{') depth++
|
|
217
|
+
else if (source[i] === '}') depth--
|
|
218
|
+
i++
|
|
219
|
+
}
|
|
220
|
+
const body = source.slice(blockStart + 1, i - 1)
|
|
221
|
+
|
|
222
|
+
// Extract params object: params: { key: value, ... }
|
|
223
|
+
const params: FunctionPredicateInfo['params'] = []
|
|
224
|
+
const paramsMatch = body.match(/params\s*:\s*\{([^}]*)\}/)
|
|
225
|
+
if (paramsMatch) {
|
|
226
|
+
const paramsStr = paramsMatch[1]
|
|
227
|
+
const paramEntries = splitParams(paramsStr)
|
|
228
|
+
for (const entry of paramEntries) {
|
|
229
|
+
const kv = entry.match(/^(\w+)\s*:\s*(.+)$/)
|
|
230
|
+
if (kv) {
|
|
231
|
+
params.push({ name: kv[1], example: kv[2].trim() })
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Extract returns value
|
|
237
|
+
let returns: string | undefined
|
|
238
|
+
const returnsMatch = body.match(/returns\s*:\s*(.+?)(?:\n|$)/)
|
|
239
|
+
if (returnsMatch) {
|
|
240
|
+
returns = returnsMatch[1].trim()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
result.set(name, { params, returns })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
|
|
187
249
|
/** Info about a class extracted from source */
|
|
188
250
|
interface ClassInfo {
|
|
189
251
|
name: string
|
|
@@ -535,6 +597,28 @@ export function generateDTS(
|
|
|
535
597
|
emitted.add(name)
|
|
536
598
|
}
|
|
537
599
|
|
|
600
|
+
// Emit FunctionPredicate declarations as TS function types.
|
|
601
|
+
// FunctionPredicate Callback { params: { x: 0 } returns: '' }
|
|
602
|
+
// → export type Callback = (x: number) => string;
|
|
603
|
+
const funcPreds = detectFunctionPredicates(source)
|
|
604
|
+
for (const [name, fpInfo] of funcPreds) {
|
|
605
|
+
if (emitted.has(name)) continue
|
|
606
|
+
|
|
607
|
+
const exportInfo = exports.get(name)
|
|
608
|
+
const isExported = hasAnyExport ? !!exportInfo?.exported : true
|
|
609
|
+
if (!isExported) continue
|
|
610
|
+
|
|
611
|
+
const tsParams = fpInfo.params
|
|
612
|
+
.map((p) => `${p.name}: ${inferTSTypeFromExample(p.example)}`)
|
|
613
|
+
.join(', ')
|
|
614
|
+
const tsReturn =
|
|
615
|
+
fpInfo.returns !== undefined
|
|
616
|
+
? inferTSTypeFromExample(fpInfo.returns)
|
|
617
|
+
: 'void'
|
|
618
|
+
lines.push(`export type ${name} = (${tsParams}) => ${tsReturn};`)
|
|
619
|
+
emitted.add(name)
|
|
620
|
+
}
|
|
621
|
+
|
|
538
622
|
if (options.moduleName) {
|
|
539
623
|
const indented = lines.map((l) => ` ${l}`).join('\n')
|
|
540
624
|
return `declare module '${options.moduleName}' {\n${indented}\n}\n`
|
|
@@ -124,6 +124,158 @@ interface TypeResolutionContext {
|
|
|
124
124
|
typeParams?: Map<string, { constraint?: ts.TypeNode; default?: ts.TypeNode }>
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* DOM interface types — not constructible but common in TS signatures.
|
|
129
|
+
* Map to {} (opaque object) so params stay annotated and required
|
|
130
|
+
* rather than degrading to bare names.
|
|
131
|
+
*/
|
|
132
|
+
const domInterfaceTypes = new Set([
|
|
133
|
+
// Events
|
|
134
|
+
'Event',
|
|
135
|
+
'CustomEvent',
|
|
136
|
+
'MouseEvent',
|
|
137
|
+
'KeyboardEvent',
|
|
138
|
+
'PointerEvent',
|
|
139
|
+
'TouchEvent',
|
|
140
|
+
'FocusEvent',
|
|
141
|
+
'InputEvent',
|
|
142
|
+
'CompositionEvent',
|
|
143
|
+
'WheelEvent',
|
|
144
|
+
'DragEvent',
|
|
145
|
+
'AnimationEvent',
|
|
146
|
+
'TransitionEvent',
|
|
147
|
+
'ClipboardEvent',
|
|
148
|
+
'UIEvent',
|
|
149
|
+
'ProgressEvent',
|
|
150
|
+
'ErrorEvent',
|
|
151
|
+
'MessageEvent',
|
|
152
|
+
'PopStateEvent',
|
|
153
|
+
'HashChangeEvent',
|
|
154
|
+
'PageTransitionEvent',
|
|
155
|
+
'StorageEvent',
|
|
156
|
+
'BeforeUnloadEvent',
|
|
157
|
+
'SubmitEvent',
|
|
158
|
+
// Event targets / misc
|
|
159
|
+
'EventTarget',
|
|
160
|
+
'EventListener',
|
|
161
|
+
// Nodes
|
|
162
|
+
'Node',
|
|
163
|
+
'Element',
|
|
164
|
+
'HTMLElement',
|
|
165
|
+
'SVGElement',
|
|
166
|
+
'Document',
|
|
167
|
+
'DocumentFragment',
|
|
168
|
+
'ShadowRoot',
|
|
169
|
+
'Text',
|
|
170
|
+
'Comment',
|
|
171
|
+
'Attr',
|
|
172
|
+
// Specific HTML elements
|
|
173
|
+
'HTMLInputElement',
|
|
174
|
+
'HTMLTextAreaElement',
|
|
175
|
+
'HTMLSelectElement',
|
|
176
|
+
'HTMLButtonElement',
|
|
177
|
+
'HTMLFormElement',
|
|
178
|
+
'HTMLAnchorElement',
|
|
179
|
+
'HTMLImageElement',
|
|
180
|
+
'HTMLVideoElement',
|
|
181
|
+
'HTMLAudioElement',
|
|
182
|
+
'HTMLCanvasElement',
|
|
183
|
+
'HTMLDivElement',
|
|
184
|
+
'HTMLSpanElement',
|
|
185
|
+
'HTMLParagraphElement',
|
|
186
|
+
'HTMLTableElement',
|
|
187
|
+
'HTMLTemplateElement',
|
|
188
|
+
'HTMLSlotElement',
|
|
189
|
+
'HTMLDialogElement',
|
|
190
|
+
'HTMLDetailsElement',
|
|
191
|
+
'HTMLLabelElement',
|
|
192
|
+
'HTMLOptionElement',
|
|
193
|
+
'HTMLIFrameElement',
|
|
194
|
+
'HTMLScriptElement',
|
|
195
|
+
'HTMLStyleElement',
|
|
196
|
+
'HTMLLinkElement',
|
|
197
|
+
'HTMLMetaElement',
|
|
198
|
+
'HTMLHeadElement',
|
|
199
|
+
'HTMLBodyElement',
|
|
200
|
+
'HTMLMediaElement',
|
|
201
|
+
// SVG elements
|
|
202
|
+
'SVGSVGElement',
|
|
203
|
+
'SVGPathElement',
|
|
204
|
+
'SVGGElement',
|
|
205
|
+
'SVGCircleElement',
|
|
206
|
+
'SVGRectElement',
|
|
207
|
+
'SVGTextElement',
|
|
208
|
+
'SVGLineElement',
|
|
209
|
+
'SVGPolygonElement',
|
|
210
|
+
// Collections / lists
|
|
211
|
+
'NodeList',
|
|
212
|
+
'HTMLCollection',
|
|
213
|
+
'NamedNodeMap',
|
|
214
|
+
'DOMTokenList',
|
|
215
|
+
'DOMStringMap',
|
|
216
|
+
'CSSStyleDeclaration',
|
|
217
|
+
'DOMRect',
|
|
218
|
+
'DOMRectReadOnly',
|
|
219
|
+
'DOMPoint',
|
|
220
|
+
'DOMMatrix',
|
|
221
|
+
// Ranges / selection
|
|
222
|
+
'Range',
|
|
223
|
+
'Selection',
|
|
224
|
+
'StaticRange',
|
|
225
|
+
// Observers
|
|
226
|
+
'MutationObserver',
|
|
227
|
+
'MutationRecord',
|
|
228
|
+
'IntersectionObserver',
|
|
229
|
+
'IntersectionObserverEntry',
|
|
230
|
+
'ResizeObserver',
|
|
231
|
+
'ResizeObserverEntry',
|
|
232
|
+
'PerformanceObserver',
|
|
233
|
+
'PerformanceEntry',
|
|
234
|
+
// Window / global
|
|
235
|
+
'Window',
|
|
236
|
+
'Location',
|
|
237
|
+
'History',
|
|
238
|
+
'Navigator',
|
|
239
|
+
'Screen',
|
|
240
|
+
'Storage',
|
|
241
|
+
// Canvas / media
|
|
242
|
+
'CanvasRenderingContext2D',
|
|
243
|
+
'WebGLRenderingContext',
|
|
244
|
+
'WebGL2RenderingContext',
|
|
245
|
+
'OffscreenCanvas',
|
|
246
|
+
'ImageData',
|
|
247
|
+
'ImageBitmap',
|
|
248
|
+
'MediaStream',
|
|
249
|
+
'MediaRecorder',
|
|
250
|
+
'AudioContext',
|
|
251
|
+
'AudioNode',
|
|
252
|
+
'AudioBuffer',
|
|
253
|
+
// Workers / messaging
|
|
254
|
+
'Worker',
|
|
255
|
+
'SharedWorker',
|
|
256
|
+
'ServiceWorker',
|
|
257
|
+
'ServiceWorkerRegistration',
|
|
258
|
+
'BroadcastChannel',
|
|
259
|
+
'MessageChannel',
|
|
260
|
+
'MessagePort',
|
|
261
|
+
// Other Web APIs
|
|
262
|
+
'WebSocket',
|
|
263
|
+
'XMLHttpRequest',
|
|
264
|
+
'FileReader',
|
|
265
|
+
'FileList',
|
|
266
|
+
'DataTransfer',
|
|
267
|
+
'Crypto',
|
|
268
|
+
'SubtleCrypto',
|
|
269
|
+
'CryptoKey',
|
|
270
|
+
'Geolocation',
|
|
271
|
+
'Notification',
|
|
272
|
+
'PermissionStatus',
|
|
273
|
+
'MediaQueryList',
|
|
274
|
+
'TreeWalker',
|
|
275
|
+
'NodeIterator',
|
|
276
|
+
'ClipboardItem',
|
|
277
|
+
])
|
|
278
|
+
|
|
127
279
|
/**
|
|
128
280
|
* Convert a TypeScript type node to a TJS example value string
|
|
129
281
|
*
|
|
@@ -221,6 +373,10 @@ function typeToExample(
|
|
|
221
373
|
Error: "new Error('example')",
|
|
222
374
|
TypeError: "new TypeError('example')",
|
|
223
375
|
RangeError: "new RangeError('example')",
|
|
376
|
+
SyntaxError: "new SyntaxError('example')",
|
|
377
|
+
ReferenceError: "new ReferenceError('example')",
|
|
378
|
+
URIError: "new URIError('example')",
|
|
379
|
+
EvalError: "new EvalError('example')",
|
|
224
380
|
// Date/Regex
|
|
225
381
|
Date: 'new Date()',
|
|
226
382
|
RegExp: '/example/',
|
|
@@ -239,7 +395,7 @@ function typeToExample(
|
|
|
239
395
|
Uint8ClampedArray: 'new Uint8ClampedArray(0)',
|
|
240
396
|
BigInt64Array: 'new BigInt64Array(0)',
|
|
241
397
|
BigUint64Array: 'new BigUint64Array(0)',
|
|
242
|
-
// Web
|
|
398
|
+
// Web APIs (constructible)
|
|
243
399
|
URL: "new URL('https://example.com')",
|
|
244
400
|
URLSearchParams: 'new URLSearchParams()',
|
|
245
401
|
Headers: 'new Headers()',
|
|
@@ -249,6 +405,7 @@ function typeToExample(
|
|
|
249
405
|
Response: 'new Response()',
|
|
250
406
|
Request: "new Request('https://example.com')",
|
|
251
407
|
AbortController: 'new AbortController()',
|
|
408
|
+
AbortSignal: 'AbortSignal.abort()',
|
|
252
409
|
// Streams
|
|
253
410
|
ReadableStream: 'new ReadableStream()',
|
|
254
411
|
WritableStream: 'new WritableStream()',
|
|
@@ -256,6 +413,8 @@ function typeToExample(
|
|
|
256
413
|
// Structured data
|
|
257
414
|
TextEncoder: 'new TextEncoder()',
|
|
258
415
|
TextDecoder: 'new TextDecoder()',
|
|
416
|
+
// Promises
|
|
417
|
+
Promise: 'Promise.resolve(null)',
|
|
259
418
|
}
|
|
260
419
|
|
|
261
420
|
if (typeName in builtinExamples) {
|
|
@@ -318,6 +477,11 @@ function typeToExample(
|
|
|
318
477
|
// No constraint or default — fall through to 'any'
|
|
319
478
|
}
|
|
320
479
|
|
|
480
|
+
// DOM interface types — opaque objects, keep params annotated
|
|
481
|
+
if (domInterfaceTypes.has(typeName)) {
|
|
482
|
+
return '{}'
|
|
483
|
+
}
|
|
484
|
+
|
|
321
485
|
// Single uppercase letter or common generic names — treat as any
|
|
322
486
|
if (
|
|
323
487
|
/^[A-Z]$/.test(typeName) ||
|
|
@@ -420,9 +584,24 @@ function typeToExample(
|
|
|
420
584
|
return typeToExample(parenType.type, checker)
|
|
421
585
|
}
|
|
422
586
|
|
|
423
|
-
case ts.SyntaxKind.FunctionType:
|
|
424
|
-
//
|
|
425
|
-
|
|
587
|
+
case ts.SyntaxKind.FunctionType: {
|
|
588
|
+
// Convert to inline FunctionPredicate expression
|
|
589
|
+
const funcType = type as ts.FunctionTypeNode
|
|
590
|
+
const fpParams: string[] = []
|
|
591
|
+
for (const param of funcType.parameters) {
|
|
592
|
+
const name = param.name?.getText() || '_'
|
|
593
|
+
if (name === 'this') continue
|
|
594
|
+
let paramExample = typeToExample(param.type, checker, warnings, ctx)
|
|
595
|
+
if (paramExample === 'any') paramExample = 'null'
|
|
596
|
+
fpParams.push(`${name}: ${paramExample}`)
|
|
597
|
+
}
|
|
598
|
+
let fpReturn = typeToExample(funcType.type, checker, warnings, ctx)
|
|
599
|
+
if (fpReturn === 'any') fpReturn = 'null'
|
|
600
|
+
const spec: string[] = []
|
|
601
|
+
if (fpParams.length > 0) spec.push(`params: { ${fpParams.join(', ')} }`)
|
|
602
|
+
if (fpReturn !== 'undefined') spec.push(`returns: ${fpReturn}`)
|
|
603
|
+
return `FunctionPredicate('function', { ${spec.join(', ')} })`
|
|
604
|
+
}
|
|
426
605
|
|
|
427
606
|
case ts.SyntaxKind.TupleType: {
|
|
428
607
|
const tupleType = type as ts.TupleTypeNode
|
|
@@ -669,6 +848,11 @@ function typeToInfo(
|
|
|
669
848
|
}
|
|
670
849
|
}
|
|
671
850
|
|
|
851
|
+
// DOM interface types — opaque objects
|
|
852
|
+
if (domInterfaceTypes.has(typeName)) {
|
|
853
|
+
return { kind: 'object' }
|
|
854
|
+
}
|
|
855
|
+
|
|
672
856
|
// Generics and unknown types become 'any'
|
|
673
857
|
return { kind: 'any' }
|
|
674
858
|
}
|
|
@@ -978,6 +1162,25 @@ function transformTypeAliasToType(
|
|
|
978
1162
|
return `Union ${typeName} '${typeName}' ${literalValues.join(' | ')}`
|
|
979
1163
|
}
|
|
980
1164
|
|
|
1165
|
+
// Function types → FunctionPredicate declaration
|
|
1166
|
+
if (node.type.kind === ts.SyntaxKind.FunctionType) {
|
|
1167
|
+
const funcType = node.type as ts.FunctionTypeNode
|
|
1168
|
+
const fpParams: string[] = []
|
|
1169
|
+
for (const param of funcType.parameters) {
|
|
1170
|
+
const name = param.name?.getText(sourceFile) || '_'
|
|
1171
|
+
if (name === 'this') continue
|
|
1172
|
+
let paramExample = typeToExample(param.type, undefined, warnings)
|
|
1173
|
+
if (paramExample === 'any') paramExample = 'null'
|
|
1174
|
+
fpParams.push(`${name}: ${paramExample}`)
|
|
1175
|
+
}
|
|
1176
|
+
let fpReturn = typeToExample(funcType.type, undefined, warnings)
|
|
1177
|
+
if (fpReturn === 'any') fpReturn = 'null'
|
|
1178
|
+
const spec: string[] = []
|
|
1179
|
+
if (fpParams.length > 0) spec.push(`params: { ${fpParams.join(', ')} }`)
|
|
1180
|
+
if (fpReturn !== 'undefined') spec.push(`returns: ${fpReturn}`)
|
|
1181
|
+
return `FunctionPredicate ${typeName} {\n ${spec.join('\n ')}\n}`
|
|
1182
|
+
}
|
|
1183
|
+
|
|
981
1184
|
const example = typeToExample(node.type, undefined, warnings)
|
|
982
1185
|
|
|
983
1186
|
// 'any' and 'undefined' — skip declaration (undeclared = any in TJS)
|
|
@@ -2054,7 +2257,11 @@ export function fromTS(
|
|
|
2054
2257
|
const isExported = statement.modifiers?.some(
|
|
2055
2258
|
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
2056
2259
|
)
|
|
2057
|
-
tjsFunctions.push(
|
|
2260
|
+
tjsFunctions.push(
|
|
2261
|
+
isExported
|
|
2262
|
+
? typeDecl.replace(/^(\/\*[\s\S]*?\*\/\s*)?/, '$1export ')
|
|
2263
|
+
: typeDecl
|
|
2264
|
+
)
|
|
2058
2265
|
}
|
|
2059
2266
|
}
|
|
2060
2267
|
}
|
|
@@ -2076,7 +2283,11 @@ export function fromTS(
|
|
|
2076
2283
|
const isExported = statement.modifiers?.some(
|
|
2077
2284
|
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
2078
2285
|
)
|
|
2079
|
-
tjsFunctions.push(
|
|
2286
|
+
tjsFunctions.push(
|
|
2287
|
+
isExported
|
|
2288
|
+
? typeDecl.replace(/^(\/\*[\s\S]*?\*\/\s*)?/, '$1export ')
|
|
2289
|
+
: typeDecl
|
|
2290
|
+
)
|
|
2080
2291
|
}
|
|
2081
2292
|
}
|
|
2082
2293
|
}
|