tjs-lang 0.5.4 → 0.6.0
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 +33 -13
- package/README.md +4 -4
- package/bin/dev.ts +5 -1
- package/demo/docs.json +14 -2
- package/demo/index.html +2 -2
- package/demo/src/capabilities.ts +109 -2
- package/demo/src/demo-nav.ts +137 -203
- package/demo/src/imports.ts +43 -9
- package/demo/src/index.ts +179 -29
- package/demo/src/playground-shared.ts +11 -4
- package/demo/src/playground.ts +2 -2
- package/demo/src/tjs-playground.ts +294 -11
- package/demo/src/ts-playground.ts +239 -0
- package/dist/index.js +135 -127
- package/dist/index.js.map +6 -5
- package/dist/src/cli/commands/emit.d.ts +3 -0
- package/dist/src/lang/emitters/dts.d.ts +48 -0
- package/dist/src/lang/emitters/from-ts.d.ts +2 -0
- package/dist/src/lang/index.d.ts +1 -0
- package/dist/tjs-batteries.js +3 -3
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-full.js +135 -127
- package/dist/tjs-full.js.map +6 -5
- package/dist/tjs-transpiler.js +2 -349
- package/dist/tjs-transpiler.js.map +4 -19
- package/package.json +1 -1
- package/src/cli/commands/emit.ts +26 -0
- package/src/cli/tjs.ts +4 -1
- package/src/lang/codegen.test.ts +55 -0
- package/src/lang/emitters/dts.test.ts +406 -0
- package/src/lang/emitters/dts.ts +588 -0
- package/src/lang/emitters/from-ts.ts +244 -20
- package/src/lang/index.ts +5 -0
- package/src/lang/typescript-syntax.test.ts +358 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TJS to .d.ts Emitter
|
|
3
|
+
*
|
|
4
|
+
* Generates TypeScript declaration files from TJS transpilation results.
|
|
5
|
+
* Allows TypeScript consumers to use TJS-authored libraries with full
|
|
6
|
+
* type information for functions, and helpful `any`-based stubs for
|
|
7
|
+
* classes, generics, and predicate types.
|
|
8
|
+
*
|
|
9
|
+
* Design principle: emit enough structure for autocomplete/tooltips
|
|
10
|
+
* (parameter names, object shapes) but lean on `any` where TJS types
|
|
11
|
+
* can't be faithfully expressed in TS (predicate types, generics,
|
|
12
|
+
* class instances). This gives developers IDE hints without false
|
|
13
|
+
* lint errors from types that don't fully match.
|
|
14
|
+
*
|
|
15
|
+
* Handles:
|
|
16
|
+
* - Exported functions → full param/return types from TJSTypeInfo
|
|
17
|
+
* - Exported classes → callable function stub with constructor params, returns any
|
|
18
|
+
* - Exported Type declarations → type guard function stubs
|
|
19
|
+
* - Exported Generic declarations → factory function stubs
|
|
20
|
+
* - Re-exports via `export { Name }` syntax
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { TypeDescriptor } from '../types'
|
|
24
|
+
import type { TJSTranspileResult, TJSTypeInfo } from './js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert a TypeDescriptor to a TypeScript type string.
|
|
28
|
+
*
|
|
29
|
+
* Maps TJS's example-inferred types to the closest TS equivalents:
|
|
30
|
+
* integer / non-negative-integer → number (TS has no integer type)
|
|
31
|
+
* string / number / boolean / null / undefined / any → themselves
|
|
32
|
+
* array + items → T[]
|
|
33
|
+
* object + shape → { key: Type; ... }
|
|
34
|
+
* union + members → T1 | T2
|
|
35
|
+
* nullable → T | null
|
|
36
|
+
*/
|
|
37
|
+
export function typeDescriptorToTS(td: TypeDescriptor): string {
|
|
38
|
+
let base: string
|
|
39
|
+
|
|
40
|
+
switch (td.kind) {
|
|
41
|
+
case 'string':
|
|
42
|
+
base = 'string'
|
|
43
|
+
break
|
|
44
|
+
case 'number':
|
|
45
|
+
case 'integer':
|
|
46
|
+
case 'non-negative-integer':
|
|
47
|
+
base = 'number'
|
|
48
|
+
break
|
|
49
|
+
case 'boolean':
|
|
50
|
+
base = 'boolean'
|
|
51
|
+
break
|
|
52
|
+
case 'null':
|
|
53
|
+
return 'null'
|
|
54
|
+
case 'undefined':
|
|
55
|
+
return 'undefined'
|
|
56
|
+
case 'any':
|
|
57
|
+
base = 'any'
|
|
58
|
+
break
|
|
59
|
+
case 'array':
|
|
60
|
+
if (td.items) {
|
|
61
|
+
const inner = typeDescriptorToTS(td.items)
|
|
62
|
+
// Wrap union types in parens for array: (A | B)[]
|
|
63
|
+
base = inner.includes('|') ? `(${inner})[]` : `${inner}[]`
|
|
64
|
+
} else {
|
|
65
|
+
base = 'any[]'
|
|
66
|
+
}
|
|
67
|
+
break
|
|
68
|
+
case 'object':
|
|
69
|
+
if (td.shape && Object.keys(td.shape).length > 0) {
|
|
70
|
+
const fields = Object.entries(td.shape)
|
|
71
|
+
.map(([k, v]) => `${k}: ${typeDescriptorToTS(v)}`)
|
|
72
|
+
.join('; ')
|
|
73
|
+
base = `{ ${fields} }`
|
|
74
|
+
} else {
|
|
75
|
+
base = 'Record<string, any>'
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
case 'union':
|
|
79
|
+
if (td.members && td.members.length > 0) {
|
|
80
|
+
return td.members.map(typeDescriptorToTS).join(' | ')
|
|
81
|
+
}
|
|
82
|
+
base = 'any'
|
|
83
|
+
break
|
|
84
|
+
default:
|
|
85
|
+
base = 'any'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (td.nullable) {
|
|
89
|
+
return `${base} | null`
|
|
90
|
+
}
|
|
91
|
+
return base
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate a function declaration line for .d.ts
|
|
96
|
+
*/
|
|
97
|
+
function functionDeclToTS(
|
|
98
|
+
name: string,
|
|
99
|
+
info: TJSTypeInfo,
|
|
100
|
+
exported: boolean,
|
|
101
|
+
isDefault: boolean
|
|
102
|
+
): string {
|
|
103
|
+
const params = Object.entries(info.params)
|
|
104
|
+
.map(([pName, p]) => {
|
|
105
|
+
const optional = !p.required
|
|
106
|
+
const tsType = typeDescriptorToTS(p.type)
|
|
107
|
+
return optional ? `${pName}?: ${tsType}` : `${pName}: ${tsType}`
|
|
108
|
+
})
|
|
109
|
+
.join(', ')
|
|
110
|
+
|
|
111
|
+
const returnType = info.returns ? typeDescriptorToTS(info.returns) : 'any'
|
|
112
|
+
const prefix = exported
|
|
113
|
+
? isDefault
|
|
114
|
+
? 'export default function'
|
|
115
|
+
: 'export declare function'
|
|
116
|
+
: 'declare function'
|
|
117
|
+
|
|
118
|
+
return `${prefix} ${name}(${params}): ${returnType};`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface GenerateDTSOptions {
|
|
122
|
+
/** Module name for ambient declarations (omit for module-mode .d.ts) */
|
|
123
|
+
moduleName?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Info about a name detected as exported */
|
|
127
|
+
interface ExportInfo {
|
|
128
|
+
exported: boolean
|
|
129
|
+
isDefault: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Detect which top-level names are exported in the source.
|
|
134
|
+
*
|
|
135
|
+
* Returns a map of name → { exported, isDefault }.
|
|
136
|
+
* Scans the original TJS source for export keywords.
|
|
137
|
+
*/
|
|
138
|
+
function detectExports(source: string): Map<string, ExportInfo> {
|
|
139
|
+
const result = new Map<string, ExportInfo>()
|
|
140
|
+
let m
|
|
141
|
+
|
|
142
|
+
// export function name / export default function name
|
|
143
|
+
const funcRe = /^[ \t]*export\s+(default\s+)?function\s+(\w+)/gm
|
|
144
|
+
while ((m = funcRe.exec(source)) !== null) {
|
|
145
|
+
result.set(m[2], { exported: true, isDefault: !!m[1] })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// export class name / export default class name
|
|
149
|
+
const classRe = /^[ \t]*export\s+(default\s+)?class\s+(\w+)/gm
|
|
150
|
+
while ((m = classRe.exec(source)) !== null) {
|
|
151
|
+
result.set(m[2], { exported: true, isDefault: !!m[1] })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// export const/let/var name
|
|
155
|
+
const varRe = /^[ \t]*export\s+(default\s+)?(?:const|let|var)\s+(\w+)/gm
|
|
156
|
+
while ((m = varRe.exec(source)) !== null) {
|
|
157
|
+
result.set(m[2], { exported: true, isDefault: !!m[1] })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// export Type Name
|
|
161
|
+
const typeRe = /^[ \t]*export\s+Type\s+(\w+)/gm
|
|
162
|
+
while ((m = typeRe.exec(source)) !== null) {
|
|
163
|
+
result.set(m[1], { exported: true, isDefault: false })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// export Generic Name<...>
|
|
167
|
+
const genericRe = /^[ \t]*export\s+Generic\s+(\w+)/gm
|
|
168
|
+
while ((m = genericRe.exec(source)) !== null) {
|
|
169
|
+
result.set(m[1], { exported: true, isDefault: false })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// export { Name, Name2, ... } — re-export form
|
|
173
|
+
const reExportRe = /^[ \t]*export\s*\{([^}]+)\}/gm
|
|
174
|
+
while ((m = reExportRe.exec(source)) !== null) {
|
|
175
|
+
const names = m[1].split(',').map((s) => s.trim().split(/\s+as\s+/))
|
|
176
|
+
for (const parts of names) {
|
|
177
|
+
const exportedName = parts.length > 1 ? parts[1] : parts[0]
|
|
178
|
+
if (exportedName && /^\w+$/.test(exportedName)) {
|
|
179
|
+
result.set(exportedName, { exported: true, isDefault: false })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Info about a class extracted from source */
|
|
188
|
+
interface ClassInfo {
|
|
189
|
+
name: string
|
|
190
|
+
constructorParams: string // raw param string, e.g. "x: 0.0, y: 0.0"
|
|
191
|
+
methods: { name: string; params: string; returnType: string | null }[]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detect class declarations and extract constructor param names/types.
|
|
196
|
+
* Scans original TJS source (before preprocessing).
|
|
197
|
+
*/
|
|
198
|
+
function detectClasses(source: string): Map<string, ClassInfo> {
|
|
199
|
+
const result = new Map<string, ClassInfo>()
|
|
200
|
+
|
|
201
|
+
// Find class declarations
|
|
202
|
+
const classRe =
|
|
203
|
+
/^[ \t]*(?:export\s+(?:default\s+)?)?class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/gm
|
|
204
|
+
let m
|
|
205
|
+
while ((m = classRe.exec(source)) !== null) {
|
|
206
|
+
const className = m[1]
|
|
207
|
+
const classBodyStart = m.index + m[0].length - 1
|
|
208
|
+
|
|
209
|
+
// Find matching closing brace
|
|
210
|
+
let depth = 1
|
|
211
|
+
let i = classBodyStart + 1
|
|
212
|
+
while (i < source.length && depth > 0) {
|
|
213
|
+
if (source[i] === '{') depth++
|
|
214
|
+
else if (source[i] === '}') depth--
|
|
215
|
+
i++
|
|
216
|
+
}
|
|
217
|
+
const classBody = source.slice(classBodyStart + 1, i - 1)
|
|
218
|
+
|
|
219
|
+
// Extract constructor params (handle nested parens/braces in param types)
|
|
220
|
+
const ctorStart = classBody.indexOf('constructor')
|
|
221
|
+
let ctorParams = ''
|
|
222
|
+
if (ctorStart !== -1) {
|
|
223
|
+
const parenStart = classBody.indexOf('(', ctorStart)
|
|
224
|
+
if (parenStart !== -1) {
|
|
225
|
+
let depth = 1
|
|
226
|
+
let j = parenStart + 1
|
|
227
|
+
while (j < classBody.length && depth > 0) {
|
|
228
|
+
if (classBody[j] === '(') depth++
|
|
229
|
+
else if (classBody[j] === ')') depth--
|
|
230
|
+
j++
|
|
231
|
+
}
|
|
232
|
+
ctorParams = classBody.slice(parenStart + 1, j - 1).trim()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Extract methods (name followed by parens, not constructor/get/set)
|
|
237
|
+
const methods: ClassInfo['methods'] = []
|
|
238
|
+
const methodStartRe = /^\s+(\w+)\s*\(/gm
|
|
239
|
+
let mm
|
|
240
|
+
while ((mm = methodStartRe.exec(classBody)) !== null) {
|
|
241
|
+
const name = mm[1]
|
|
242
|
+
if (name === 'constructor' || name === 'get' || name === 'set') continue
|
|
243
|
+
|
|
244
|
+
// Find matching close paren (handles nested braces in params)
|
|
245
|
+
const parenStart = mm.index + mm[0].length - 1
|
|
246
|
+
let depth = 1
|
|
247
|
+
let j = parenStart + 1
|
|
248
|
+
while (j < classBody.length && depth > 0) {
|
|
249
|
+
if (classBody[j] === '(') depth++
|
|
250
|
+
else if (classBody[j] === ')') depth--
|
|
251
|
+
j++
|
|
252
|
+
}
|
|
253
|
+
const params = classBody.slice(parenStart + 1, j - 1).trim()
|
|
254
|
+
|
|
255
|
+
// Check for return type annotation: -> Type
|
|
256
|
+
const afterParen = classBody.slice(j).match(/^\s*->\s*(.+?)\s*\{/)
|
|
257
|
+
const returnType = afterParen ? afterParen[1].trim() : null
|
|
258
|
+
|
|
259
|
+
methods.push({ name, params, returnType })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
result.set(className, {
|
|
263
|
+
name: className,
|
|
264
|
+
constructorParams: ctorParams,
|
|
265
|
+
methods,
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Split a param string on commas, respecting nested braces/brackets.
|
|
274
|
+
* "x: 0.0, y: { a: 1, b: 2 }" → ["x: 0.0", "y: { a: 1, b: 2 }"]
|
|
275
|
+
*/
|
|
276
|
+
function splitParams(paramStr: string): string[] {
|
|
277
|
+
const result: string[] = []
|
|
278
|
+
let depth = 0
|
|
279
|
+
let current = ''
|
|
280
|
+
for (const ch of paramStr) {
|
|
281
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++
|
|
282
|
+
else if (ch === '}' || ch === ']' || ch === ')') depth--
|
|
283
|
+
|
|
284
|
+
if (ch === ',' && depth === 0) {
|
|
285
|
+
result.push(current.trim())
|
|
286
|
+
current = ''
|
|
287
|
+
} else {
|
|
288
|
+
current += ch
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (current.trim()) result.push(current.trim())
|
|
292
|
+
return result
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse a TJS constructor/method param string into TS param declarations.
|
|
297
|
+
* Input: "x: 0.0, y: 0.0" or "name: '', age: 0"
|
|
298
|
+
* Output: "x: number, y: number" or "name: string, age: number"
|
|
299
|
+
*
|
|
300
|
+
* Uses `any` for anything we can't confidently parse.
|
|
301
|
+
*/
|
|
302
|
+
function tjsParamsToTS(paramStr: string): string {
|
|
303
|
+
if (!paramStr.trim()) return ''
|
|
304
|
+
|
|
305
|
+
return splitParams(paramStr)
|
|
306
|
+
.map((trimmed) => {
|
|
307
|
+
// name: value (required) or name = value (optional)
|
|
308
|
+
const colonMatch = trimmed.match(/^(\w+)\s*:\s*(.+)$/)
|
|
309
|
+
if (colonMatch) {
|
|
310
|
+
const name = colonMatch[1]
|
|
311
|
+
const tsType = inferTSTypeFromExample(colonMatch[2].trim())
|
|
312
|
+
return `${name}: ${tsType}`
|
|
313
|
+
}
|
|
314
|
+
const eqMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/)
|
|
315
|
+
if (eqMatch) {
|
|
316
|
+
const name = eqMatch[1]
|
|
317
|
+
const tsType = inferTSTypeFromExample(eqMatch[2].trim())
|
|
318
|
+
return `${name}?: ${tsType}`
|
|
319
|
+
}
|
|
320
|
+
// Destructured or complex — fall back to any
|
|
321
|
+
if (trimmed.startsWith('{')) return `options: any`
|
|
322
|
+
return `${trimmed}: any`
|
|
323
|
+
})
|
|
324
|
+
.join(', ')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Detect Type declarations and their example values */
|
|
328
|
+
function detectTypeDeclarations(source: string): Map<string, string> {
|
|
329
|
+
const result = new Map<string, string>()
|
|
330
|
+
let m
|
|
331
|
+
|
|
332
|
+
// Type Name = <value> (assignment form)
|
|
333
|
+
const assignRe = /^[ \t]*(?:export\s+)?Type\s+(\w+)\s*=\s*(.+)$/gm
|
|
334
|
+
while ((m = assignRe.exec(source)) !== null) {
|
|
335
|
+
result.set(m[1], m[2].trim())
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Type Name <value> (simple form, not block)
|
|
339
|
+
const simpleRe = /^[ \t]*(?:export\s+)?Type\s+(\w+)\s+([^{=].*)$/gm
|
|
340
|
+
while ((m = simpleRe.exec(source)) !== null) {
|
|
341
|
+
if (!result.has(m[1])) {
|
|
342
|
+
result.set(m[1], m[2].trim())
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Block: Type Name { ... example: <value> ... }
|
|
347
|
+
const blockRe =
|
|
348
|
+
/^[ \t]*(?:export\s+)?Type\s+(\w+)\s*\{[^}]*example\s*:\s*(.+?)(?:\n|\s*[,}])/gm
|
|
349
|
+
while ((m = blockRe.exec(source)) !== null) {
|
|
350
|
+
result.set(m[1], m[2].trim())
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Detect Generic declarations and their type parameter names */
|
|
357
|
+
function detectGenerics(source: string): Map<string, { typeParams: string[] }> {
|
|
358
|
+
const result = new Map<string, { typeParams: string[] }>()
|
|
359
|
+
const re = /^[ \t]*(?:export\s+)?Generic\s+(\w+)\s*<([^>]+)>/gm
|
|
360
|
+
let m
|
|
361
|
+
while ((m = re.exec(source)) !== null) {
|
|
362
|
+
const name = m[1]
|
|
363
|
+
const typeParams = m[2].split(',').map((tp) => {
|
|
364
|
+
// Strip defaults: "U = ''" → "U"
|
|
365
|
+
return tp.trim().split(/\s*=/)[0].trim()
|
|
366
|
+
})
|
|
367
|
+
result.set(name, { typeParams })
|
|
368
|
+
}
|
|
369
|
+
return result
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Generate a .d.ts string from TJS transpilation output.
|
|
374
|
+
*
|
|
375
|
+
* @param result - The TJSTranspileResult from tjs()
|
|
376
|
+
* @param source - The original TJS source (needed to detect exports)
|
|
377
|
+
* @param options - Generation options
|
|
378
|
+
* @returns The .d.ts file content as a string
|
|
379
|
+
*/
|
|
380
|
+
export function generateDTS(
|
|
381
|
+
result: TJSTranspileResult,
|
|
382
|
+
source: string,
|
|
383
|
+
options: GenerateDTSOptions = {}
|
|
384
|
+
): string {
|
|
385
|
+
const lines: string[] = []
|
|
386
|
+
const exports = detectExports(source)
|
|
387
|
+
const typeDecls = detectTypeDeclarations(source)
|
|
388
|
+
const classes = detectClasses(source)
|
|
389
|
+
const generics = detectGenerics(source)
|
|
390
|
+
|
|
391
|
+
// If no exports detected, treat all top-level declarations as exported
|
|
392
|
+
// (CommonJS / script-mode files)
|
|
393
|
+
const hasAnyExport = exports.size > 0
|
|
394
|
+
|
|
395
|
+
// Track names we've already emitted
|
|
396
|
+
const emitted = new Set<string>()
|
|
397
|
+
|
|
398
|
+
// Emit function declarations (from transpiler metadata — best type info)
|
|
399
|
+
for (const [name, info] of Object.entries(result.types)) {
|
|
400
|
+
// Skip polymorphic variants (name$0, name$1, etc.)
|
|
401
|
+
if (name.includes('$')) continue
|
|
402
|
+
|
|
403
|
+
const exportInfo = exports.get(name)
|
|
404
|
+
const isExported = hasAnyExport ? !!exportInfo?.exported : true
|
|
405
|
+
const isDefault = exportInfo?.isDefault ?? false
|
|
406
|
+
|
|
407
|
+
if (!isExported) continue
|
|
408
|
+
|
|
409
|
+
if (info.description) {
|
|
410
|
+
lines.push(`/** ${info.description} */`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
lines.push(functionDeclToTS(name, info, true, isDefault))
|
|
414
|
+
emitted.add(name)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Emit class declarations as callable functions returning any.
|
|
418
|
+
// TJS wraps classes to be callable without `new`, so this matches
|
|
419
|
+
// the actual runtime API. Returning `any` means TS won't fight
|
|
420
|
+
// the developer on instance property access.
|
|
421
|
+
for (const [name, classInfo] of classes) {
|
|
422
|
+
if (emitted.has(name)) continue
|
|
423
|
+
|
|
424
|
+
const exportInfo = exports.get(name)
|
|
425
|
+
const isExported = hasAnyExport ? !!exportInfo?.exported : true
|
|
426
|
+
if (!isExported) continue
|
|
427
|
+
|
|
428
|
+
const tsParams = classInfo.constructorParams
|
|
429
|
+
? tjsParamsToTS(classInfo.constructorParams)
|
|
430
|
+
: ''
|
|
431
|
+
|
|
432
|
+
// Emit as callable function (matches TJS wrapClass behavior)
|
|
433
|
+
lines.push(`export declare function ${name}(${tsParams}): any;`)
|
|
434
|
+
|
|
435
|
+
// Also emit as a class with `new` for the rare case someone uses it
|
|
436
|
+
if (tsParams || classInfo.methods.length > 0) {
|
|
437
|
+
lines.push(`export declare class ${name} {`)
|
|
438
|
+
if (classInfo.constructorParams) {
|
|
439
|
+
lines.push(` constructor(${tsParams});`)
|
|
440
|
+
}
|
|
441
|
+
for (const method of classInfo.methods) {
|
|
442
|
+
const mParams = method.params ? tjsParamsToTS(method.params) : ''
|
|
443
|
+
lines.push(` ${method.name}(${mParams}): any;`)
|
|
444
|
+
}
|
|
445
|
+
lines.push(`}`)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
emitted.add(name)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Emit Type declarations as type guard functions.
|
|
452
|
+
// Type('Name', example) returns an object with .check(), .default, etc.
|
|
453
|
+
// For TS consumers, the useful thing is knowing it's a callable type guard.
|
|
454
|
+
for (const [name, exampleStr] of typeDecls) {
|
|
455
|
+
if (emitted.has(name)) continue
|
|
456
|
+
|
|
457
|
+
const exportInfo = exports.get(name)
|
|
458
|
+
const isExported = hasAnyExport ? !!exportInfo?.exported : true
|
|
459
|
+
if (!isExported) continue
|
|
460
|
+
|
|
461
|
+
const tsType = inferTSTypeFromExample(exampleStr)
|
|
462
|
+
lines.push(
|
|
463
|
+
`export declare const ${name}: {` +
|
|
464
|
+
` check(value: any): boolean;` +
|
|
465
|
+
` default: ${tsType};` +
|
|
466
|
+
` (value: any): boolean;` +
|
|
467
|
+
` };`
|
|
468
|
+
)
|
|
469
|
+
emitted.add(name)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Emit Generic declarations as factory functions.
|
|
473
|
+
// Generic('Box', ['T'], predicate) → callable that creates type guards.
|
|
474
|
+
// In TS terms: a function that takes type args and returns a type guard.
|
|
475
|
+
for (const [name, info] of generics) {
|
|
476
|
+
if (emitted.has(name)) continue
|
|
477
|
+
|
|
478
|
+
const exportInfo = exports.get(name)
|
|
479
|
+
const isExported = hasAnyExport ? !!exportInfo?.exported : true
|
|
480
|
+
if (!isExported) continue
|
|
481
|
+
|
|
482
|
+
// Emit as a function that takes any args and returns a type guard object
|
|
483
|
+
// (same shape as Type — .check(), callable, etc.)
|
|
484
|
+
const anyParams = info.typeParams.map((_) => `...args: any[]`)
|
|
485
|
+
lines.push(
|
|
486
|
+
`export declare function ${name}(` +
|
|
487
|
+
`...args: any[]` +
|
|
488
|
+
`): { check(value: any): boolean; (value: any): boolean; };`
|
|
489
|
+
)
|
|
490
|
+
emitted.add(name)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (options.moduleName) {
|
|
494
|
+
const indented = lines.map((l) => ` ${l}`).join('\n')
|
|
495
|
+
return `declare module '${options.moduleName}' {\n${indented}\n}\n`
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return lines.join('\n') + '\n'
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Best-effort TS type inference from an example value string.
|
|
503
|
+
* Used for Type declarations and constructor params where we only
|
|
504
|
+
* have the raw source text, not a parsed TypeDescriptor.
|
|
505
|
+
*/
|
|
506
|
+
function inferTSTypeFromExample(example: string): string {
|
|
507
|
+
const s = example.trim()
|
|
508
|
+
|
|
509
|
+
// Unions first: "'' | 0 | null" → "string | number | null"
|
|
510
|
+
// Only split on | that's outside quotes/braces/brackets
|
|
511
|
+
if (hasTopLevelPipe(s)) {
|
|
512
|
+
const members = splitOnPipe(s).map((m) => inferTSTypeFromExample(m.trim()))
|
|
513
|
+
return [...new Set(members)].join(' | ')
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// String literals
|
|
517
|
+
if (/^['"]/.test(s)) return 'string'
|
|
518
|
+
|
|
519
|
+
// Boolean
|
|
520
|
+
if (s === 'true' || s === 'false') return 'boolean'
|
|
521
|
+
|
|
522
|
+
// Null / undefined
|
|
523
|
+
if (s === 'null') return 'null'
|
|
524
|
+
if (s === 'undefined') return 'undefined'
|
|
525
|
+
|
|
526
|
+
// Numbers
|
|
527
|
+
if (/^[+-]?\d+\.\d+$/.test(s)) return 'number'
|
|
528
|
+
if (/^[+-]?\d+$/.test(s)) return 'number'
|
|
529
|
+
|
|
530
|
+
// Arrays
|
|
531
|
+
if (s.startsWith('[')) return 'any[]'
|
|
532
|
+
|
|
533
|
+
// Objects
|
|
534
|
+
if (s.startsWith('{')) return 'Record<string, any>'
|
|
535
|
+
|
|
536
|
+
return 'any'
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Check if a string has a top-level | (not inside quotes/braces) */
|
|
540
|
+
function hasTopLevelPipe(s: string): boolean {
|
|
541
|
+
let depth = 0
|
|
542
|
+
let inStr: string | null = null
|
|
543
|
+
for (const ch of s) {
|
|
544
|
+
if (inStr) {
|
|
545
|
+
if (ch === inStr) inStr = null
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
549
|
+
inStr = ch
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++
|
|
553
|
+
else if (ch === '}' || ch === ']' || ch === ')') depth--
|
|
554
|
+
else if (ch === '|' && depth === 0) return true
|
|
555
|
+
}
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Split on top-level | characters */
|
|
560
|
+
function splitOnPipe(s: string): string[] {
|
|
561
|
+
const result: string[] = []
|
|
562
|
+
let depth = 0
|
|
563
|
+
let inStr: string | null = null
|
|
564
|
+
let current = ''
|
|
565
|
+
for (const ch of s) {
|
|
566
|
+
if (inStr) {
|
|
567
|
+
current += ch
|
|
568
|
+
if (ch === inStr) inStr = null
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
572
|
+
inStr = ch
|
|
573
|
+
current += ch
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
576
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++
|
|
577
|
+
else if (ch === '}' || ch === ']' || ch === ')') depth--
|
|
578
|
+
|
|
579
|
+
if (ch === '|' && depth === 0) {
|
|
580
|
+
result.push(current)
|
|
581
|
+
current = ''
|
|
582
|
+
} else {
|
|
583
|
+
current += ch
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (current) result.push(current)
|
|
587
|
+
return result
|
|
588
|
+
}
|