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.
@@ -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
+ }