tjs-lang 0.2.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.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
@@ -0,0 +1,1092 @@
1
+ /**
2
+ * TypeScript Playground - Interactive TypeScript to TJS to JS editor
3
+ *
4
+ * Demonstrates the full transpiler pipeline:
5
+ * 1. TypeScript input (user edits)
6
+ * 2. TJS intermediate output (read-only, with copy button)
7
+ * 3. JavaScript final output (with __tjs metadata)
8
+ * 4. Preview execution
9
+ */
10
+
11
+ import { Component, ElementCreator, PartsMap, elements, vars } from 'tosijs'
12
+ import {
13
+ tabSelector,
14
+ TabSelector,
15
+ icons,
16
+ markdownViewer,
17
+ MarkdownViewer,
18
+ } from 'tosijs-ui'
19
+ import { codeMirror, CodeMirror } from '../../editors/codemirror/component'
20
+ import { fromTS } from '../../src/lang/emitters/from-ts'
21
+ import { tjs } from '../../src/lang'
22
+ import { extractImports, resolveImports } from './imports'
23
+
24
+ const { div, button, span, pre, input } = elements
25
+
26
+ // Default TypeScript example
27
+ const DEFAULT_TS = `// TypeScript Example - Types become runtime validation
28
+ function greet(name: string): string {
29
+ return \`Hello, \${name}!\`
30
+ }
31
+
32
+ function add(a: number, b: number): number {
33
+ return a + b
34
+ }
35
+
36
+ // Try calling with wrong types - TJS catches it at runtime!
37
+ console.log(greet('World'))
38
+ console.log(add(2, 3))
39
+ `
40
+
41
+ const DEFAULT_HTML = `<div class="preview-content">
42
+ <h2>Preview</h2>
43
+ <div id="output"></div>
44
+ </div>`
45
+
46
+ const DEFAULT_CSS = `.preview-content {
47
+ padding: 1rem;
48
+ font-family: system-ui, sans-serif;
49
+ }
50
+
51
+ h2 {
52
+ color: #3d4a6b;
53
+ margin-top: 0;
54
+ }
55
+
56
+ #output {
57
+ padding: 0.5rem;
58
+ background: #f5f5f5;
59
+ border-radius: 4px;
60
+ }`
61
+
62
+ interface TSPlaygroundParts extends PartsMap {
63
+ tsEditor: CodeMirror
64
+ htmlEditor: CodeMirror
65
+ cssEditor: CodeMirror
66
+ inputTabs: TabSelector
67
+ outputTabs: TabSelector
68
+ tjsOutput: HTMLElement
69
+ jsOutput: HTMLElement
70
+ previewFrame: HTMLIFrameElement
71
+ docsOutput: MarkdownViewer
72
+ testsOutput: HTMLElement
73
+ consoleHeader: HTMLElement
74
+ console: HTMLElement
75
+ runBtn: HTMLButtonElement
76
+ revertBtn: HTMLButtonElement
77
+ copyTjsBtn: HTMLButtonElement
78
+ openTjsBtn: HTMLButtonElement
79
+ statusBar: HTMLElement
80
+ // Build flags
81
+ testsToggle: HTMLInputElement
82
+ debugToggle: HTMLInputElement
83
+ safetyToggle: HTMLInputElement
84
+ }
85
+
86
+ export class TSPlayground extends Component<TSPlaygroundParts> {
87
+ private lastTjsCode: string = ''
88
+ private lastJsCode: string = ''
89
+ private consoleMessages: string[] = []
90
+
91
+ // Editor state persistence
92
+ private currentExampleName: string | null = null
93
+ private originalCode: string = DEFAULT_TS
94
+ private editorCache: Map<string, string> = new Map()
95
+
96
+ // Build flags state
97
+ private buildFlags = {
98
+ tests: true, // Run tests at transpile time
99
+ debug: false, // Debug mode (call stack tracking)
100
+ safe: true, // Safe mode (validates inputs)
101
+ }
102
+
103
+ content = () => [
104
+ // Toolbar
105
+ div(
106
+ { class: 'ts-toolbar' },
107
+ button(
108
+ { part: 'runBtn', class: 'run-btn', onClick: this.run },
109
+ icons.play({ size: 16 }),
110
+ 'Run'
111
+ ),
112
+ span({ class: 'toolbar-separator' }),
113
+ // Build flags
114
+ div(
115
+ { class: 'build-flags' },
116
+ elements.label(
117
+ { class: 'flag-label', title: 'Run tests at transpile time' },
118
+ input({
119
+ part: 'testsToggle',
120
+ type: 'checkbox',
121
+ checked: true,
122
+ onChange: this.toggleTests,
123
+ }),
124
+ 'Tests'
125
+ ),
126
+ elements.label(
127
+ { class: 'flag-label', title: 'Debug mode (call stack tracking)' },
128
+ input({
129
+ part: 'debugToggle',
130
+ type: 'checkbox',
131
+ onChange: this.toggleDebug,
132
+ }),
133
+ 'Debug'
134
+ ),
135
+ elements.label(
136
+ { class: 'flag-label', title: 'Safe mode (validates inputs)' },
137
+ input({
138
+ part: 'safetyToggle',
139
+ type: 'checkbox',
140
+ checked: true,
141
+ onChange: this.toggleSafety,
142
+ }),
143
+ 'Safe'
144
+ )
145
+ ),
146
+ span({ class: 'toolbar-separator' }),
147
+ button(
148
+ {
149
+ part: 'revertBtn',
150
+ class: 'revert-btn',
151
+ onClick: this.revertToOriginal,
152
+ title: 'Revert to original example code',
153
+ },
154
+ icons.cornerUpLeft({ size: 16 }),
155
+ 'Revert'
156
+ ),
157
+ span({ class: 'elastic' }),
158
+ span({ part: 'statusBar', class: 'status-bar' }, 'Ready')
159
+ ),
160
+
161
+ // Main area - split into input (left) and output (right)
162
+ div(
163
+ { class: 'ts-main' },
164
+
165
+ // Input side - TS, HTML, CSS editors
166
+ div(
167
+ { class: 'ts-input' },
168
+ tabSelector(
169
+ { part: 'inputTabs' },
170
+ div(
171
+ { name: 'TS', class: 'editor-wrapper' },
172
+ codeMirror({
173
+ part: 'tsEditor',
174
+ mode: 'typescript',
175
+ })
176
+ ),
177
+ div(
178
+ { name: 'HTML', class: 'editor-wrapper' },
179
+ codeMirror({
180
+ part: 'htmlEditor',
181
+ mode: 'html',
182
+ })
183
+ ),
184
+ div(
185
+ { name: 'CSS', class: 'editor-wrapper' },
186
+ codeMirror({
187
+ part: 'cssEditor',
188
+ mode: 'css',
189
+ })
190
+ )
191
+ )
192
+ ),
193
+
194
+ // Output side - TJS, JS, Preview, Docs, Tests
195
+ div(
196
+ { class: 'ts-output' },
197
+ tabSelector(
198
+ { part: 'outputTabs' },
199
+ div(
200
+ { name: 'TJS' },
201
+ div(
202
+ { class: 'output-toolbar' },
203
+ button(
204
+ {
205
+ part: 'copyTjsBtn',
206
+ class: 'small-btn',
207
+ onClick: this.copyTjs,
208
+ title: 'Copy TJS to clipboard',
209
+ },
210
+ icons.copy({ size: 14 }),
211
+ 'Copy'
212
+ ),
213
+ button(
214
+ {
215
+ part: 'openTjsBtn',
216
+ class: 'small-btn',
217
+ onClick: this.openInTjsPlayground,
218
+ title: 'Open in TJS Playground',
219
+ },
220
+ icons.externalLink({ size: 14 }),
221
+ 'Open in TJS'
222
+ )
223
+ ),
224
+ pre(
225
+ { part: 'tjsOutput', class: 'code-output' },
226
+ '// TJS intermediate will appear here'
227
+ )
228
+ ),
229
+ div(
230
+ { name: 'JS' },
231
+ pre(
232
+ { part: 'jsOutput', class: 'code-output' },
233
+ '// Final JavaScript will appear here'
234
+ )
235
+ ),
236
+ div(
237
+ { name: 'Preview' },
238
+ div(
239
+ { class: 'preview-container' },
240
+ elements.iframe({
241
+ part: 'previewFrame',
242
+ class: 'preview-frame',
243
+ sandbox: 'allow-scripts',
244
+ })
245
+ )
246
+ ),
247
+ markdownViewer({
248
+ name: 'Docs',
249
+ part: 'docsOutput',
250
+ class: 'docs-output',
251
+ value: '*Documentation will appear here*',
252
+ }),
253
+ div(
254
+ { name: 'Tests' },
255
+ div(
256
+ { part: 'testsOutput', class: 'tests-output' },
257
+ 'Test results will appear here'
258
+ )
259
+ )
260
+ )
261
+ )
262
+ ),
263
+
264
+ // Console panel at bottom
265
+ div(
266
+ { class: 'ts-console' },
267
+ div({ part: 'consoleHeader', class: 'console-header' }, 'Console'),
268
+ pre({ part: 'console', class: 'console-output' })
269
+ ),
270
+ ]
271
+
272
+ connectedCallback(): void {
273
+ super.connectedCallback()
274
+
275
+ // Set default content
276
+ setTimeout(() => {
277
+ this.parts.tsEditor.value = DEFAULT_TS
278
+ this.parts.htmlEditor.value = DEFAULT_HTML
279
+ this.parts.cssEditor.value = DEFAULT_CSS
280
+
281
+ // Auto-transpile on load
282
+ this.transpile()
283
+ }, 0)
284
+
285
+ // Listen for changes (debounced)
286
+ let debounceTimer: ReturnType<typeof setTimeout>
287
+ this.parts.tsEditor.addEventListener('change', () => {
288
+ clearTimeout(debounceTimer)
289
+ debounceTimer = setTimeout(() => {
290
+ this.transpile()
291
+ this.updateRevertButton()
292
+ }, 300)
293
+ })
294
+ }
295
+
296
+ log = (message: string) => {
297
+ this.consoleMessages.push(message)
298
+ this.renderConsole()
299
+ }
300
+
301
+ clearConsole = () => {
302
+ this.consoleMessages = []
303
+ this.parts.console.innerHTML = ''
304
+ this.parts.consoleHeader.textContent = 'Console'
305
+ }
306
+
307
+ private renderConsole() {
308
+ // Parse messages for line references and make them clickable
309
+ // Patterns: "at line X", "line X:", "Line X", ":X:" (line:col)
310
+ const linePattern =
311
+ /(?:at line |line |Line )(\d+)(?:[:,]?\s*(?:column |col )?(\d+))?|:(\d+):(\d+)/g
312
+
313
+ const html = this.consoleMessages
314
+ .map((msg) => {
315
+ // Escape HTML
316
+ const escaped = msg
317
+ .replace(/&/g, '&amp;')
318
+ .replace(/</g, '&lt;')
319
+ .replace(/>/g, '&gt;')
320
+
321
+ // Replace line references with clickable spans
322
+ return escaped.replace(linePattern, (match, l1, c1, l2, c2) => {
323
+ const line = l1 || l2
324
+ const col = c1 || c2 || '1'
325
+ return `<span class="clickable-line" data-line="${line}" data-col="${col}">${match}</span>`
326
+ })
327
+ })
328
+ .join('\n')
329
+
330
+ this.parts.console.innerHTML = html
331
+ this.parts.console.scrollTop = this.parts.console.scrollHeight
332
+
333
+ // Add click handlers
334
+ this.parts.console.querySelectorAll('.clickable-line').forEach((el) => {
335
+ el.addEventListener('click', (e) => {
336
+ const target = e.currentTarget as HTMLElement
337
+ const line = parseInt(target.dataset.line || '0', 10)
338
+ const col = parseInt(target.dataset.col || '1', 10)
339
+ if (line > 0) {
340
+ this.goToSourceLine(line, col)
341
+ }
342
+ })
343
+ })
344
+ }
345
+
346
+ // Navigate to a specific line in the source editor
347
+ goToSourceLine(line: number, column: number = 1) {
348
+ this.parts.inputTabs.value = 0 // Switch to TS tab (first tab)
349
+ // Wait for tab switch and editor resize before scrolling
350
+ setTimeout(() => {
351
+ this.parts.tsEditor.goToLine(line, column)
352
+ }, 50)
353
+ }
354
+
355
+ // Build flag toggle handlers
356
+ toggleTests = () => {
357
+ this.buildFlags.tests = this.parts.testsToggle.checked
358
+ this.transpile()
359
+ }
360
+
361
+ toggleDebug = () => {
362
+ this.buildFlags.debug = this.parts.debugToggle.checked
363
+ this.transpile()
364
+ }
365
+
366
+ toggleSafety = () => {
367
+ this.buildFlags.safe = this.parts.safetyToggle.checked
368
+ this.transpile()
369
+ }
370
+
371
+ lastTranspileTime = 0
372
+ lastTsToTjsTime = 0
373
+ lastTjsToJsTime = 0
374
+
375
+ transpile = () => {
376
+ const tsSource = this.parts.tsEditor.value
377
+
378
+ try {
379
+ // Step 1: TS -> TJS (timed)
380
+ const tsStart = performance.now()
381
+ const tjsResult = fromTS(tsSource, { emitTJS: true })
382
+ this.lastTsToTjsTime = performance.now() - tsStart
383
+
384
+ this.lastTjsCode = tjsResult.code
385
+ this.parts.tjsOutput.textContent = tjsResult.code
386
+
387
+ // Show warnings if any
388
+ if (tjsResult.warnings && tjsResult.warnings.length > 0) {
389
+ this.parts.tjsOutput.textContent +=
390
+ '\n\n// Warnings:\n// ' + tjsResult.warnings.join('\n// ')
391
+ }
392
+
393
+ // Step 2: TJS -> JS (skip signature tests with -!) (timed)
394
+ // Replace -> with -! to avoid signature test failures during transpilation
395
+ let tjsCodeForJs = tjsResult.code.replace(/-> /g, '-! ')
396
+
397
+ // Inject safety directive if unsafe mode is enabled
398
+ if (!this.buildFlags.safe) {
399
+ tjsCodeForJs = 'safety none\n' + tjsCodeForJs
400
+ }
401
+
402
+ try {
403
+ const tjsStart = performance.now()
404
+ // Build transpiler options from flags
405
+ const options: { runTests: boolean | 'report'; debug?: boolean } = {
406
+ runTests: this.buildFlags.tests ? 'report' : false,
407
+ debug: this.buildFlags.debug,
408
+ }
409
+ const jsResult = tjs(tjsCodeForJs, options)
410
+ this.lastTjsToJsTime = performance.now() - tjsStart
411
+ this.lastTranspileTime = this.lastTsToTjsTime + this.lastTjsToJsTime
412
+
413
+ this.lastJsCode = jsResult.code
414
+ this.parts.jsOutput.textContent = jsResult.code
415
+
416
+ // Update test results
417
+ this.updateTestResults(jsResult.testResults || [])
418
+
419
+ // Update docs
420
+ this.updateDocs(jsResult)
421
+
422
+ // Show timing: TS->TJS + TJS->JS = total
423
+ const formatTime = (t: number) =>
424
+ t < 1 ? `${(t * 1000).toFixed(0)}μs` : `${t.toFixed(2)}ms`
425
+ this.parts.statusBar.textContent = `TS→TJS ${formatTime(
426
+ this.lastTsToTjsTime
427
+ )} + TJS→JS ${formatTime(this.lastTjsToJsTime)} = ${formatTime(
428
+ this.lastTranspileTime
429
+ )}`
430
+ this.parts.statusBar.classList.remove('error')
431
+ } catch (jsError: any) {
432
+ this.parts.jsOutput.textContent = `// TJS -> JS Error:\n// ${jsError.message}`
433
+ this.parts.statusBar.textContent = `TJS->JS error: ${jsError.message}`
434
+ this.parts.statusBar.classList.add('error')
435
+ this.lastJsCode = ''
436
+ }
437
+ } catch (tsError: any) {
438
+ // TS -> TJS failed
439
+ const errorInfo = this.formatError(tsError, tsSource)
440
+ this.parts.tjsOutput.textContent = errorInfo
441
+ this.parts.jsOutput.textContent =
442
+ '// Cannot generate JS - TS transpilation failed'
443
+ this.parts.statusBar.textContent = `TS error: ${tsError.message}`
444
+ this.parts.statusBar.classList.add('error')
445
+ this.lastTjsCode = ''
446
+ this.lastJsCode = ''
447
+
448
+ // Set error marker in gutter
449
+ if (tsError.line) {
450
+ this.parts.tsEditor.setMarkers([
451
+ {
452
+ line: tsError.line,
453
+ message: tsError.message || 'Transpilation error',
454
+ },
455
+ ])
456
+ } else {
457
+ this.parts.tsEditor.clearMarkers()
458
+ }
459
+ }
460
+ }
461
+
462
+ private formatError(e: any, source: string): string {
463
+ const lines = ['// Transpilation Error', '// ' + '='.repeat(50), '']
464
+ lines.push(`// ${e.message}`)
465
+ if (e.line) {
466
+ lines.push(`// at line ${e.line}, column ${e.column || 0}`)
467
+ }
468
+ return lines.join('\n')
469
+ }
470
+
471
+ private updateTestResults(tests: any[]) {
472
+ if (!tests || tests.length === 0) {
473
+ this.parts.testsOutput.textContent = 'No tests defined'
474
+ this.parts.tsEditor.clearMarkers()
475
+ return
476
+ }
477
+
478
+ const passed = tests.filter((t) => t.passed).length
479
+ const failed = tests.filter((t) => !t.passed).length
480
+
481
+ // Set gutter markers for failed tests
482
+ const failedTests = tests.filter((t: any) => !t.passed && t.line)
483
+ if (failedTests.length > 0) {
484
+ this.parts.tsEditor.setMarkers(
485
+ failedTests.map((t: any) => ({
486
+ line: t.line,
487
+ message: t.error || t.description,
488
+ severity: 'error' as const,
489
+ }))
490
+ )
491
+ } else {
492
+ this.parts.tsEditor.clearMarkers()
493
+ }
494
+
495
+ let html = `<div class="test-summary">`
496
+ html += `<strong>${passed} passed</strong>`
497
+ if (failed > 0) {
498
+ html += `, <strong class="test-failed">${failed} failed</strong>`
499
+ }
500
+ html += `</div><ul class="test-list">`
501
+
502
+ for (const test of tests) {
503
+ const icon = test.passed ? '✓' : '✗'
504
+ const cls = test.passed ? 'test-pass' : 'test-fail'
505
+ const sigBadge = test.isSignatureTest
506
+ ? ' <span class="sig-badge">signature</span>'
507
+ : ''
508
+ const dataLine = test.line ? ` data-line="${test.line}"` : ''
509
+ html += `<li class="${cls}"${dataLine}>${icon} ${test.description}${sigBadge}`
510
+ if (!test.passed && test.error) {
511
+ html += `<div class="test-error${
512
+ test.line ? ' clickable-error' : ''
513
+ }"${dataLine}>${test.error}</div>`
514
+ }
515
+ html += `</li>`
516
+ }
517
+ html += `</ul>`
518
+
519
+ this.parts.testsOutput.innerHTML = html
520
+
521
+ // Add click handlers for clickable errors
522
+ this.parts.testsOutput
523
+ .querySelectorAll('.clickable-error')
524
+ .forEach((el) => {
525
+ el.addEventListener('click', (e) => {
526
+ const line = parseInt(
527
+ (e.currentTarget as HTMLElement).dataset.line || '0',
528
+ 10
529
+ )
530
+ if (line > 0) {
531
+ this.goToSourceLine(line)
532
+ }
533
+ })
534
+ })
535
+ }
536
+
537
+ private updateDocs(result: any) {
538
+ if (!result?.types) {
539
+ this.parts.docsOutput.value = '*No documentation available*'
540
+ return
541
+ }
542
+
543
+ let docs = '# Generated Documentation\n\n'
544
+
545
+ for (const [name, info] of Object.entries(result.types) as any) {
546
+ docs += `## ${name}\n\n`
547
+
548
+ if (info.params) {
549
+ docs += '**Parameters:**\n'
550
+ for (const [paramName, paramInfo] of Object.entries(
551
+ info.params
552
+ ) as any) {
553
+ const required = paramInfo.required ? '' : ' *(optional)*'
554
+ const typeStr = paramInfo.type?.kind || 'any'
555
+ docs += `- \`${paramName}\`: ${typeStr}${required}\n`
556
+ }
557
+ docs += '\n'
558
+ }
559
+
560
+ if (info.returns) {
561
+ docs += `**Returns:** ${info.returns.kind || 'void'}\n\n`
562
+ }
563
+
564
+ docs += '---\n\n'
565
+ }
566
+
567
+ this.parts.docsOutput.value = docs
568
+ }
569
+
570
+ copyTjs = () => {
571
+ if (this.lastTjsCode) {
572
+ navigator.clipboard.writeText(this.lastTjsCode)
573
+ this.parts.statusBar.textContent = 'TJS copied to clipboard'
574
+ }
575
+ }
576
+
577
+ openInTjsPlayground = () => {
578
+ if (this.lastTjsCode) {
579
+ // Store the TJS code and navigate to TJS playground
580
+ // For now, we'll use a custom event that the parent can handle
581
+ this.dispatchEvent(
582
+ new CustomEvent('open-tjs', {
583
+ detail: { code: this.lastTjsCode },
584
+ bubbles: true,
585
+ })
586
+ )
587
+ }
588
+ }
589
+
590
+ run = async () => {
591
+ this.clearConsole()
592
+ this.transpile()
593
+
594
+ if (!this.lastJsCode) {
595
+ this.log('Cannot run - transpilation failed')
596
+ return
597
+ }
598
+
599
+ this.parts.statusBar.textContent = 'Running...'
600
+
601
+ try {
602
+ const htmlContent = this.parts.htmlEditor.value
603
+ const cssContent = this.parts.cssEditor.value
604
+ const jsCode = this.lastJsCode
605
+
606
+ // Resolve imports
607
+ const imports = extractImports(jsCode)
608
+ let importMapScript = ''
609
+
610
+ if (imports.length > 0) {
611
+ this.log(`Resolving imports: ${imports.join(', ')}`)
612
+ const { importMap, errors } = await resolveImports(jsCode)
613
+
614
+ if (errors.length > 0) {
615
+ for (const err of errors) {
616
+ this.log(`Import error: ${err}`)
617
+ }
618
+ }
619
+
620
+ if (Object.keys(importMap.imports).length > 0) {
621
+ importMapScript = `<script type="importmap">${JSON.stringify(
622
+ importMap
623
+ )}</script>`
624
+ }
625
+ }
626
+
627
+ // Create iframe document
628
+ const iframeDoc = `<!DOCTYPE html>
629
+ <html>
630
+ <head>
631
+ <style>${cssContent}</style>
632
+ ${importMapScript}
633
+ </head>
634
+ <body>
635
+ ${htmlContent}
636
+ <script type="module">
637
+ // TJS Runtime stub for iframe execution
638
+ globalThis.__tjs = {
639
+ version: '0.0.0',
640
+ pushStack: () => {},
641
+ popStack: () => {},
642
+ getStack: () => [],
643
+ typeError: (path, expected, value) => {
644
+ const actual = value === null ? 'null' : typeof value;
645
+ const err = new Error(\`Expected \${expected} for '\${path}', got \${actual}\`);
646
+ err.name = 'MonadicError';
647
+ err.path = path;
648
+ err.expected = expected;
649
+ err.actual = actual;
650
+ return err;
651
+ },
652
+ createRuntime: function() { return this; },
653
+ Is: (a, b) => {
654
+ if (a === b) return true;
655
+ if (a === null || b === null) return a === b;
656
+ if (typeof a !== typeof b) return false;
657
+ if (typeof a !== 'object') return false;
658
+ if (Array.isArray(a) && Array.isArray(b)) {
659
+ if (a.length !== b.length) return false;
660
+ return a.every((v, i) => globalThis.__tjs.Is(v, b[i]));
661
+ }
662
+ const keysA = Object.keys(a);
663
+ const keysB = Object.keys(b);
664
+ if (keysA.length !== keysB.length) return false;
665
+ return keysA.every(k => globalThis.__tjs.Is(a[k], b[k]));
666
+ },
667
+ IsNot: (a, b) => !globalThis.__tjs.Is(a, b),
668
+ };
669
+
670
+ // Capture console.log
671
+ const _log = console.log;
672
+ console.log = (...args) => {
673
+ _log(...args);
674
+ parent.postMessage({ type: 'console', message: args.map(a =>
675
+ typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)
676
+ ).join(' ') }, '*');
677
+ };
678
+
679
+ try {
680
+ const __execStart = performance.now();
681
+ ${jsCode}
682
+ const __execTime = performance.now() - __execStart;
683
+ parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
684
+ } catch (e) {
685
+ parent.postMessage({ type: 'error', message: e.message }, '*');
686
+ }
687
+ </script>
688
+ </body>
689
+ </html>`
690
+
691
+ // Listen for messages from iframe
692
+ const messageHandler = (event: MessageEvent) => {
693
+ if (event.data?.type === 'console') {
694
+ this.log(event.data.message)
695
+ } else if (event.data?.type === 'timing') {
696
+ // Update console header with execution time
697
+ const execTime = event.data.execTime
698
+ const execStr =
699
+ execTime < 1
700
+ ? `${(execTime * 1000).toFixed(0)}μs`
701
+ : `${execTime.toFixed(2)}ms`
702
+ this.parts.consoleHeader.textContent = `Console — executed in ${execStr}`
703
+ } else if (event.data?.type === 'error') {
704
+ this.log(`Error: ${event.data.message}`)
705
+ this.parts.statusBar.textContent = 'Runtime error'
706
+ this.parts.statusBar.classList.add('error')
707
+ }
708
+ }
709
+ window.addEventListener('message', messageHandler)
710
+
711
+ // Set iframe content
712
+ this.parts.previewFrame.srcdoc = iframeDoc
713
+
714
+ // Wait a bit for execution, then clean up listener
715
+ setTimeout(() => {
716
+ window.removeEventListener('message', messageHandler)
717
+ // Don't overwrite status bar - keep showing transpile time
718
+ }, 1000)
719
+ } catch (e: any) {
720
+ this.log(`Error: ${e.message}`)
721
+ this.parts.statusBar.textContent = 'Error'
722
+ this.parts.statusBar.classList.add('error')
723
+ }
724
+ }
725
+
726
+ // Public method to set source code (auto-runs when examples are loaded)
727
+ setSource(code: string, exampleName?: string) {
728
+ // Save current edits before switching
729
+ if (this.currentExampleName) {
730
+ this.editorCache.set(this.currentExampleName, this.parts.tsEditor.value)
731
+ }
732
+
733
+ // Update current example tracking
734
+ this.currentExampleName = exampleName || null
735
+ this.originalCode = code
736
+
737
+ // Check if we have cached edits for this example
738
+ const cachedCode = exampleName ? this.editorCache.get(exampleName) : null
739
+ this.parts.tsEditor.value = cachedCode || code
740
+
741
+ // Update revert button visibility
742
+ this.updateRevertButton()
743
+
744
+ this.transpile()
745
+ // Auto-run when source is loaded externally (e.g., from example selection)
746
+ this.run()
747
+ }
748
+
749
+ // Revert to the original example code
750
+ revertToOriginal = () => {
751
+ if (this.currentExampleName) {
752
+ this.editorCache.delete(this.currentExampleName)
753
+ }
754
+ this.parts.tsEditor.value = this.originalCode
755
+ this.updateRevertButton()
756
+ this.transpile()
757
+ }
758
+
759
+ // Update revert button state based on whether code has changed
760
+ private updateRevertButton() {
761
+ const hasChanges = this.parts.tsEditor.value !== this.originalCode
762
+ this.parts.revertBtn.disabled = !hasChanges
763
+ this.parts.revertBtn.style.opacity = hasChanges ? '1' : '0.5'
764
+ }
765
+ }
766
+
767
+ export const tsPlayground = TSPlayground.elementCreator({
768
+ tag: 'ts-playground',
769
+ styleSpec: {
770
+ ':host': {
771
+ display: 'flex',
772
+ flexDirection: 'column',
773
+ height: '100%',
774
+ flex: '1 1 auto',
775
+ background: 'var(--background, #fff)',
776
+ color: 'var(--text-color, #1f2937)',
777
+ fontFamily: 'system-ui, sans-serif',
778
+ },
779
+
780
+ ':host .ts-toolbar': {
781
+ display: 'flex',
782
+ alignItems: 'center',
783
+ gap: '10px',
784
+ padding: '8px 12px',
785
+ background: 'var(--code-background, #f3f4f6)',
786
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
787
+ },
788
+
789
+ ':host .run-btn': {
790
+ display: 'flex',
791
+ alignItems: 'center',
792
+ gap: '4px',
793
+ padding: '6px 12px',
794
+ background: 'var(--brand-color, #3178c6)', // TypeScript blue
795
+ color: 'var(--brand-text-color, white)',
796
+ border: 'none',
797
+ borderRadius: '6px',
798
+ cursor: 'pointer',
799
+ fontWeight: '500',
800
+ fontSize: '14px',
801
+ },
802
+
803
+ ':host .run-btn:hover': {
804
+ filter: 'brightness(1.1)',
805
+ },
806
+
807
+ ':host .toolbar-separator': {
808
+ width: '1px',
809
+ height: '20px',
810
+ background: 'var(--code-border, #d1d5db)',
811
+ },
812
+
813
+ ':host .build-flags': {
814
+ display: 'flex',
815
+ alignItems: 'center',
816
+ gap: '12px',
817
+ },
818
+
819
+ ':host .flag-label': {
820
+ display: 'flex',
821
+ alignItems: 'center',
822
+ gap: '4px',
823
+ fontSize: '13px',
824
+ color: 'var(--text-color, #6b7280)',
825
+ cursor: 'pointer',
826
+ userSelect: 'none',
827
+ },
828
+
829
+ ':host .flag-label:hover': {
830
+ color: 'var(--text-color, #374151)',
831
+ },
832
+
833
+ ':host .flag-label input[type="checkbox"]': {
834
+ margin: '0',
835
+ cursor: 'pointer',
836
+ accentColor: 'var(--brand-color, #3178c6)',
837
+ },
838
+
839
+ ':host .revert-btn': {
840
+ display: 'flex',
841
+ alignItems: 'center',
842
+ gap: '4px',
843
+ padding: '6px 12px',
844
+ background: 'var(--code-background, #e5e7eb)',
845
+ color: 'var(--text-color, #374151)',
846
+ border: '1px solid var(--code-border, #d1d5db)',
847
+ borderRadius: '6px',
848
+ cursor: 'pointer',
849
+ fontWeight: '500',
850
+ fontSize: '14px',
851
+ transition: 'opacity 0.2s',
852
+ },
853
+
854
+ ':host .revert-btn:hover:not(:disabled)': {
855
+ background: '#fef3c7',
856
+ borderColor: '#f59e0b',
857
+ color: '#92400e',
858
+ },
859
+
860
+ ':host .revert-btn:disabled': {
861
+ cursor: 'default',
862
+ },
863
+
864
+ ':host .elastic': {
865
+ flex: '1',
866
+ },
867
+
868
+ ':host .status-bar': {
869
+ fontSize: '13px',
870
+ color: 'var(--text-color, #6b7280)',
871
+ opacity: '0.7',
872
+ },
873
+
874
+ ':host .status-bar.error': {
875
+ color: '#dc2626',
876
+ opacity: '1',
877
+ },
878
+
879
+ ':host .ts-main': {
880
+ display: 'flex',
881
+ flex: '1 1 auto',
882
+ minHeight: '0',
883
+ gap: '1px',
884
+ background: 'var(--code-border, #e5e7eb)',
885
+ },
886
+
887
+ ':host .ts-input, :host .ts-output': {
888
+ flex: '1 1 50%',
889
+ minWidth: '0',
890
+ display: 'flex',
891
+ flexDirection: 'column',
892
+ background: 'var(--background, #fff)',
893
+ overflow: 'hidden',
894
+ },
895
+
896
+ ':host .ts-input xin-tabs, :host .ts-output xin-tabs': {
897
+ flex: '1 1 auto',
898
+ display: 'flex',
899
+ flexDirection: 'column',
900
+ minHeight: '0',
901
+ },
902
+
903
+ ':host xin-tabs > [name]': {
904
+ background: 'var(--background, #fff)',
905
+ color: 'var(--text-color, #1f2937)',
906
+ },
907
+
908
+ ':host .editor-wrapper': {
909
+ flex: '1 1 auto',
910
+ height: '100%',
911
+ minHeight: '300px',
912
+ position: 'relative',
913
+ overflow: 'hidden',
914
+ },
915
+
916
+ ':host .editor-wrapper code-mirror': {
917
+ display: 'block',
918
+ position: 'absolute',
919
+ top: '0',
920
+ left: '0',
921
+ right: '0',
922
+ bottom: '0',
923
+ },
924
+
925
+ ':host .output-toolbar': {
926
+ display: 'flex',
927
+ gap: '8px',
928
+ padding: '8px 12px',
929
+ background: 'var(--code-background, #f3f4f6)',
930
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
931
+ },
932
+
933
+ ':host .small-btn': {
934
+ display: 'flex',
935
+ alignItems: 'center',
936
+ gap: '4px',
937
+ padding: '4px 8px',
938
+ background: 'var(--background, #fff)',
939
+ color: 'var(--text-color, #374151)',
940
+ border: '1px solid var(--code-border, #d1d5db)',
941
+ borderRadius: '4px',
942
+ cursor: 'pointer',
943
+ fontSize: '12px',
944
+ },
945
+
946
+ ':host .small-btn:hover': {
947
+ background: 'var(--code-background, #f3f4f6)',
948
+ },
949
+
950
+ ':host .code-output': {
951
+ margin: '0',
952
+ padding: '12px',
953
+ background: 'var(--code-background, #f3f4f6)',
954
+ color: 'var(--text-color, #1f2937)',
955
+ fontSize: '13px',
956
+ fontFamily: 'ui-monospace, monospace',
957
+ overflow: 'auto',
958
+ height: '100%',
959
+ whiteSpace: 'pre-wrap',
960
+ },
961
+
962
+ ':host .preview-container': {
963
+ height: '100%',
964
+ background: 'var(--background, #fff)',
965
+ },
966
+
967
+ ':host .preview-frame': {
968
+ width: '100%',
969
+ height: '100%',
970
+ border: 'none',
971
+ },
972
+
973
+ ':host .docs-output': {
974
+ display: 'block',
975
+ padding: '12px 16px',
976
+ fontSize: '14px',
977
+ fontFamily: 'system-ui, sans-serif',
978
+ color: 'var(--text-color, inherit)',
979
+ background: 'var(--background, #fff)',
980
+ height: '100%',
981
+ overflow: 'auto',
982
+ },
983
+
984
+ ':host .tests-output': {
985
+ padding: '12px',
986
+ fontSize: '14px',
987
+ fontFamily: 'system-ui, sans-serif',
988
+ color: 'var(--text-color, inherit)',
989
+ background: 'var(--background, #fff)',
990
+ height: '100%',
991
+ overflow: 'auto',
992
+ },
993
+
994
+ ':host .test-summary': {
995
+ marginBottom: '12px',
996
+ paddingBottom: '8px',
997
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
998
+ },
999
+
1000
+ ':host .test-failed': {
1001
+ color: '#dc2626',
1002
+ },
1003
+
1004
+ ':host .test-list': {
1005
+ listStyle: 'none',
1006
+ padding: 0,
1007
+ margin: 0,
1008
+ },
1009
+
1010
+ ':host .test-list li': {
1011
+ padding: '4px 0',
1012
+ },
1013
+
1014
+ ':host .test-pass': {
1015
+ color: '#16a34a',
1016
+ },
1017
+
1018
+ ':host .test-fail': {
1019
+ color: '#dc2626',
1020
+ },
1021
+
1022
+ ':host .test-error': {
1023
+ marginLeft: '20px',
1024
+ marginTop: '4px',
1025
+ padding: '8px',
1026
+ background: 'rgba(220, 38, 38, 0.1)',
1027
+ borderRadius: '4px',
1028
+ fontSize: '13px',
1029
+ fontFamily: 'var(--font-mono, monospace)',
1030
+ },
1031
+
1032
+ ':host .clickable-error': {
1033
+ cursor: 'pointer',
1034
+ textDecoration: 'underline',
1035
+ textDecorationStyle: 'dotted',
1036
+ },
1037
+
1038
+ ':host .clickable-error:hover': {
1039
+ background: 'rgba(220, 38, 38, 0.2)',
1040
+ },
1041
+
1042
+ ':host .sig-badge': {
1043
+ fontSize: '11px',
1044
+ padding: '2px 6px',
1045
+ marginLeft: '8px',
1046
+ background: 'rgba(99, 102, 241, 0.1)',
1047
+ color: '#6366f1',
1048
+ borderRadius: '4px',
1049
+ },
1050
+
1051
+ ':host .ts-console': {
1052
+ height: '120px',
1053
+ borderTop: '1px solid var(--code-border, #e5e7eb)',
1054
+ display: 'flex',
1055
+ flexDirection: 'column',
1056
+ },
1057
+
1058
+ ':host .console-header': {
1059
+ padding: '4px 12px',
1060
+ background: 'var(--code-background, #f3f4f6)',
1061
+ fontSize: '12px',
1062
+ fontWeight: '500',
1063
+ color: 'var(--text-color, #6b7280)',
1064
+ opacity: '0.7',
1065
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
1066
+ },
1067
+
1068
+ ':host .console-output': {
1069
+ flex: '1',
1070
+ margin: '0',
1071
+ padding: '8px 12px',
1072
+ background: 'var(--code-background, #f3f4f6)',
1073
+ color: 'var(--text-color, #1f2937)',
1074
+ fontSize: '12px',
1075
+ fontFamily: 'ui-monospace, monospace',
1076
+ overflow: 'auto',
1077
+ whiteSpace: 'pre-wrap',
1078
+ },
1079
+
1080
+ ':host .clickable-line': {
1081
+ cursor: 'pointer',
1082
+ color: '#2563eb',
1083
+ textDecoration: 'underline',
1084
+ textDecorationStyle: 'dotted',
1085
+ },
1086
+
1087
+ ':host .clickable-line:hover': {
1088
+ color: '#1d4ed8',
1089
+ background: 'rgba(37, 99, 235, 0.1)',
1090
+ },
1091
+ },
1092
+ }) as ElementCreator<TSPlayground>