tjs-lang 0.2.8 → 0.3.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.
@@ -21,6 +21,14 @@ import { fromTS } from '../../src/lang/emitters/from-ts'
21
21
  import { tjs } from '../../src/lang'
22
22
  import { extractImports, resolveImports } from './imports'
23
23
  import { generateDocsMarkdown } from './docs-utils'
24
+ import {
25
+ buildIframeDoc,
26
+ createIframeMessageHandler,
27
+ renderConsoleMessages,
28
+ renderTestResults,
29
+ formatExecTime,
30
+ sharedPlaygroundStyles,
31
+ } from './playground-shared'
24
32
 
25
33
  const { div, button, span, pre, input } = elements
26
34
 
@@ -292,42 +300,11 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
292
300
  }
293
301
 
294
302
  private renderConsole() {
295
- // Parse messages for line references and make them clickable
296
- // Patterns: "at line X", "line X:", "Line X", ":X:" (line:col)
297
- const linePattern =
298
- /(?:at line |line |Line )(\d+)(?:[:,]?\s*(?:column |col )?(\d+))?|:(\d+):(\d+)/g
299
-
300
- const html = this.consoleMessages
301
- .map((msg) => {
302
- // Escape HTML
303
- const escaped = msg
304
- .replace(/&/g, '&amp;')
305
- .replace(/</g, '&lt;')
306
- .replace(/>/g, '&gt;')
307
-
308
- // Replace line references with clickable spans
309
- return escaped.replace(linePattern, (match, l1, c1, l2, c2) => {
310
- const line = l1 || l2
311
- const col = c1 || c2 || '1'
312
- return `<span class="clickable-line" data-line="${line}" data-col="${col}">${match}</span>`
313
- })
314
- })
315
- .join('\n')
316
-
317
- this.parts.console.innerHTML = html
318
- this.parts.console.scrollTop = this.parts.console.scrollHeight
319
-
320
- // Add click handlers
321
- this.parts.console.querySelectorAll('.clickable-line').forEach((el) => {
322
- el.addEventListener('click', (e) => {
323
- const target = e.currentTarget as HTMLElement
324
- const line = parseInt(target.dataset.line || '0', 10)
325
- const col = parseInt(target.dataset.col || '1', 10)
326
- if (line > 0) {
327
- this.goToSourceLine(line, col)
328
- }
329
- })
330
- })
303
+ renderConsoleMessages(
304
+ this.consoleMessages,
305
+ this.parts.console,
306
+ (line, col) => this.goToSourceLine(line, col)
307
+ )
331
308
  }
332
309
 
333
310
  // Navigate to a specific line in the source editor
@@ -407,11 +384,9 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
407
384
  this.updateDocs(jsResult)
408
385
 
409
386
  // Show timing: TS->TJS + TJS->JS = total
410
- const formatTime = (t: number) =>
411
- t < 1 ? `${(t * 1000).toFixed(0)}μs` : `${t.toFixed(2)}ms`
412
- this.parts.statusBar.textContent = `TS→TJS ${formatTime(
387
+ this.parts.statusBar.textContent = `TS→TJS ${formatExecTime(
413
388
  this.lastTsToTjsTime
414
- )} + TJS→JS ${formatTime(this.lastTjsToJsTime)} = ${formatTime(
389
+ )} + TJS→JS ${formatExecTime(this.lastTjsToJsTime)} = ${formatExecTime(
415
390
  this.lastTranspileTime
416
391
  )}`
417
392
  this.parts.statusBar.classList.remove('error')
@@ -456,69 +431,12 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
456
431
  }
457
432
 
458
433
  private updateTestResults(tests: any[]) {
459
- if (!tests || tests.length === 0) {
460
- this.parts.testsOutput.textContent = 'No tests defined'
461
- this.parts.tsEditor.clearMarkers()
462
- return
463
- }
464
-
465
- const passed = tests.filter((t) => t.passed).length
466
- const failed = tests.filter((t) => !t.passed).length
467
-
468
- // Set gutter markers for failed tests
469
- const failedTests = tests.filter((t: any) => !t.passed && t.line)
470
- if (failedTests.length > 0) {
471
- this.parts.tsEditor.setMarkers(
472
- failedTests.map((t: any) => ({
473
- line: t.line,
474
- message: t.error || t.description,
475
- severity: 'error' as const,
476
- }))
477
- )
478
- } else {
479
- this.parts.tsEditor.clearMarkers()
480
- }
481
-
482
- let html = `<div class="test-summary">`
483
- html += `<strong>${passed} passed</strong>`
484
- if (failed > 0) {
485
- html += `, <strong class="test-failed">${failed} failed</strong>`
486
- }
487
- html += `</div><ul class="test-list">`
488
-
489
- for (const test of tests) {
490
- const icon = test.passed ? '✓' : '✗'
491
- const cls = test.passed ? 'test-pass' : 'test-fail'
492
- const sigBadge = test.isSignatureTest
493
- ? ' <span class="sig-badge">signature</span>'
494
- : ''
495
- const dataLine = test.line ? ` data-line="${test.line}"` : ''
496
- html += `<li class="${cls}"${dataLine}>${icon} ${test.description}${sigBadge}`
497
- if (!test.passed && test.error) {
498
- html += `<div class="test-error${
499
- test.line ? ' clickable-error' : ''
500
- }"${dataLine}>${test.error}</div>`
501
- }
502
- html += `</li>`
503
- }
504
- html += `</ul>`
505
-
506
- this.parts.testsOutput.innerHTML = html
507
-
508
- // Add click handlers for clickable errors
509
- this.parts.testsOutput
510
- .querySelectorAll('.clickable-error')
511
- .forEach((el) => {
512
- el.addEventListener('click', (e) => {
513
- const line = parseInt(
514
- (e.currentTarget as HTMLElement).dataset.line || '0',
515
- 10
516
- )
517
- if (line > 0) {
518
- this.goToSourceLine(line)
519
- }
520
- })
521
- })
434
+ renderTestResults(
435
+ tests,
436
+ this.parts.testsOutput,
437
+ this.parts.tsEditor,
438
+ (line) => this.goToSourceLine(line)
439
+ )
522
440
  }
523
441
 
524
442
  private updateDocs(result: any) {
@@ -557,6 +475,9 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
557
475
  return
558
476
  }
559
477
 
478
+ // Show JS output immediately after successful transpilation
479
+ this.parts.outputTabs.value = 1 // JS is second tab (index 1)
480
+
560
481
  this.parts.statusBar.textContent = 'Running...'
561
482
 
562
483
  try {
@@ -586,87 +507,30 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
586
507
  }
587
508
 
588
509
  // Create iframe document
589
- const iframeDoc = `<!DOCTYPE html>
590
- <html>
591
- <head>
592
- <style>${cssContent}</style>
593
- ${importMapScript}
594
- </head>
595
- <body>
596
- ${htmlContent}
597
- <script type="module">
598
- // TJS Runtime stub for iframe execution
599
- globalThis.__tjs = {
600
- version: '0.0.0',
601
- pushStack: () => {},
602
- popStack: () => {},
603
- getStack: () => [],
604
- typeError: (path, expected, value) => {
605
- const actual = value === null ? 'null' : typeof value;
606
- const err = new Error(\`Expected \${expected} for '\${path}', got \${actual}\`);
607
- err.name = 'MonadicError';
608
- err.path = path;
609
- err.expected = expected;
610
- err.actual = actual;
611
- return err;
612
- },
613
- createRuntime: function() { return this; },
614
- Is: (a, b) => {
615
- if (a === b) return true;
616
- if (a === null || b === null) return a === b;
617
- if (typeof a !== typeof b) return false;
618
- if (typeof a !== 'object') return false;
619
- if (Array.isArray(a) && Array.isArray(b)) {
620
- if (a.length !== b.length) return false;
621
- return a.every((v, i) => globalThis.__tjs.Is(v, b[i]));
622
- }
623
- const keysA = Object.keys(a);
624
- const keysB = Object.keys(b);
625
- if (keysA.length !== keysB.length) return false;
626
- return keysA.every(k => globalThis.__tjs.Is(a[k], b[k]));
627
- },
628
- IsNot: (a, b) => !globalThis.__tjs.Is(a, b),
629
- };
630
-
631
- // Capture console.log
632
- const _log = console.log;
633
- console.log = (...args) => {
634
- _log(...args);
635
- parent.postMessage({ type: 'console', message: args.map(a =>
636
- typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)
637
- ).join(' ') }, '*');
638
- };
639
-
640
- try {
641
- const __execStart = performance.now();
642
- ${jsCode}
643
- const __execTime = performance.now() - __execStart;
644
- parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
645
- } catch (e) {
646
- parent.postMessage({ type: 'error', message: e.message }, '*');
647
- }
648
- </script>
649
- </body>
650
- </html>`
510
+ const iframeDoc = buildIframeDoc({
511
+ cssContent,
512
+ htmlContent,
513
+ importMapScript,
514
+ jsCode,
515
+ })
651
516
 
652
517
  // Listen for messages from iframe
653
- const messageHandler = (event: MessageEvent) => {
654
- if (event.data?.type === 'console') {
655
- this.log(event.data.message)
656
- } else if (event.data?.type === 'timing') {
657
- // Update console header with execution time
658
- const execTime = event.data.execTime
659
- const execStr =
660
- execTime < 1
661
- ? `${(execTime * 1000).toFixed(0)}μs`
662
- : `${execTime.toFixed(2)}ms`
663
- this.parts.consoleHeader.textContent = `Console — executed in ${execStr}`
664
- } else if (event.data?.type === 'error') {
665
- this.log(`Error: ${event.data.message}`)
518
+ const messageHandler = createIframeMessageHandler({
519
+ onConsole: (message) => this.log(message),
520
+ onTiming: (execTime) => {
521
+ this.parts.consoleHeader.textContent = `Console executed in ${formatExecTime(
522
+ execTime
523
+ )}`
524
+ },
525
+ onPreviewContent: () => {
526
+ this.parts.outputTabs.value = 2 // Preview is third tab (index 2)
527
+ },
528
+ onError: (message) => {
529
+ this.log(`Error: ${message}`)
666
530
  this.parts.statusBar.textContent = 'Runtime error'
667
531
  this.parts.statusBar.classList.add('error')
668
- }
669
- }
532
+ },
533
+ })
670
534
  window.addEventListener('message', messageHandler)
671
535
 
672
536
  // Set iframe content
@@ -675,7 +539,6 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
675
539
  // Wait a bit for execution, then clean up listener
676
540
  setTimeout(() => {
677
541
  window.removeEventListener('message', messageHandler)
678
- // Don't overwrite status bar - keep showing transpile time
679
542
  }, 1000)
680
543
  } catch (e: any) {
681
544
  this.log(`Error: ${e.message}`)
@@ -728,16 +591,9 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
728
591
  export const tsPlayground = TSPlayground.elementCreator({
729
592
  tag: 'ts-playground',
730
593
  styleSpec: {
731
- ':host': {
732
- display: 'flex',
733
- flexDirection: 'column',
734
- height: '100%',
735
- flex: '1 1 auto',
736
- background: 'var(--background, #fff)',
737
- color: 'var(--text-color, #1f2937)',
738
- fontFamily: 'system-ui, sans-serif',
739
- },
594
+ ...sharedPlaygroundStyles,
740
595
 
596
+ // TS-specific: toolbar
741
597
  ':host .ts-toolbar': {
742
598
  display: 'flex',
743
599
  alignItems: 'center',
@@ -747,96 +603,14 @@ export const tsPlayground = TSPlayground.elementCreator({
747
603
  borderBottom: '1px solid var(--code-border, #e5e7eb)',
748
604
  },
749
605
 
750
- ':host .run-btn': {
751
- display: 'flex',
752
- alignItems: 'center',
753
- gap: '4px',
754
- padding: '6px 12px',
755
- background: 'var(--brand-color, #3178c6)', // TypeScript blue
756
- color: 'var(--brand-text-color, white)',
757
- border: 'none',
758
- borderRadius: '6px',
759
- cursor: 'pointer',
760
- fontWeight: '500',
761
- fontSize: '14px',
762
- },
763
-
764
- ':host .run-btn:hover': {
765
- filter: 'brightness(1.1)',
766
- },
767
-
768
- ':host .toolbar-separator': {
769
- width: '1px',
770
- height: '20px',
771
- background: 'var(--code-border, #d1d5db)',
772
- },
773
-
774
- ':host .build-flags': {
775
- display: 'flex',
776
- alignItems: 'center',
777
- gap: '12px',
778
- },
779
-
780
- ':host .flag-label': {
781
- display: 'flex',
782
- alignItems: 'center',
783
- gap: '4px',
784
- fontSize: '13px',
785
- color: 'var(--text-color, #6b7280)',
786
- cursor: 'pointer',
787
- userSelect: 'none',
788
- },
789
-
790
- ':host .flag-label:hover': {
791
- color: 'var(--text-color, #374151)',
792
- },
793
-
606
+ // TS-specific: TypeScript blue brand color for checkboxes
794
607
  ':host .flag-label input[type="checkbox"]': {
795
608
  margin: '0',
796
609
  cursor: 'pointer',
797
610
  accentColor: 'var(--brand-color, #3178c6)',
798
611
  },
799
612
 
800
- ':host .revert-btn': {
801
- display: 'flex',
802
- alignItems: 'center',
803
- gap: '4px',
804
- padding: '6px 12px',
805
- background: 'var(--code-background, #e5e7eb)',
806
- color: 'var(--text-color, #374151)',
807
- border: '1px solid var(--code-border, #d1d5db)',
808
- borderRadius: '6px',
809
- cursor: 'pointer',
810
- fontWeight: '500',
811
- fontSize: '14px',
812
- transition: 'opacity 0.2s',
813
- },
814
-
815
- ':host .revert-btn:hover:not(:disabled)': {
816
- background: '#fef3c7',
817
- borderColor: '#f59e0b',
818
- color: '#92400e',
819
- },
820
-
821
- ':host .revert-btn:disabled': {
822
- cursor: 'default',
823
- },
824
-
825
- ':host .elastic': {
826
- flex: '1',
827
- },
828
-
829
- ':host .status-bar': {
830
- fontSize: '13px',
831
- color: 'var(--text-color, #6b7280)',
832
- opacity: '0.7',
833
- },
834
-
835
- ':host .status-bar.error': {
836
- color: '#dc2626',
837
- opacity: '1',
838
- },
839
-
613
+ // TS-specific: layout
840
614
  ':host .ts-main': {
841
615
  display: 'flex',
842
616
  flex: '1 1 auto',
@@ -862,28 +636,7 @@ export const tsPlayground = TSPlayground.elementCreator({
862
636
  height: '100%',
863
637
  },
864
638
 
865
- ':host tosi-tabs > [name]': {
866
- background: 'var(--background, #fff)',
867
- color: 'var(--text-color, #1f2937)',
868
- },
869
-
870
- ':host .editor-wrapper': {
871
- flex: '1 1 auto',
872
- height: '100%',
873
- minHeight: '300px',
874
- position: 'relative',
875
- overflow: 'hidden',
876
- },
877
-
878
- ':host .editor-wrapper code-mirror': {
879
- display: 'block',
880
- position: 'absolute',
881
- top: '0',
882
- left: '0',
883
- right: '0',
884
- bottom: '0',
885
- },
886
-
639
+ // TS-specific: TJS output toolbar and buttons
887
640
  ':host .output-toolbar': {
888
641
  display: 'flex',
889
642
  gap: '8px',
@@ -926,129 +679,12 @@ export const tsPlayground = TSPlayground.elementCreator({
926
679
  background: 'var(--background, #fff)',
927
680
  },
928
681
 
929
- ':host .preview-frame': {
930
- width: '100%',
931
- height: '100%',
932
- border: 'none',
933
- },
934
-
935
- ':host .docs-output': {
936
- display: 'block',
937
- padding: '12px 16px',
938
- fontSize: '14px',
939
- fontFamily: 'system-ui, sans-serif',
940
- color: 'var(--text-color, inherit)',
941
- background: 'var(--background, #fff)',
942
- height: '100%',
943
- overflow: 'auto',
944
- },
945
-
946
- ':host .tests-output': {
947
- padding: '12px',
948
- fontSize: '14px',
949
- fontFamily: 'system-ui, sans-serif',
950
- color: 'var(--text-color, inherit)',
951
- background: 'var(--background, #fff)',
952
- height: '100%',
953
- overflow: 'auto',
954
- },
955
-
956
- ':host .test-summary': {
957
- marginBottom: '12px',
958
- paddingBottom: '8px',
959
- borderBottom: '1px solid var(--code-border, #e5e7eb)',
960
- },
961
-
962
- ':host .test-failed': {
963
- color: '#dc2626',
964
- },
965
-
966
- ':host .test-list': {
967
- listStyle: 'none',
968
- padding: 0,
969
- margin: 0,
970
- },
971
-
972
- ':host .test-list li': {
973
- padding: '4px 0',
974
- },
975
-
976
- ':host .test-pass': {
977
- color: '#16a34a',
978
- },
979
-
980
- ':host .test-fail': {
981
- color: '#dc2626',
982
- },
983
-
984
- ':host .test-error': {
985
- marginLeft: '20px',
986
- marginTop: '4px',
987
- padding: '8px',
988
- background: 'rgba(220, 38, 38, 0.1)',
989
- borderRadius: '4px',
990
- fontSize: '13px',
991
- fontFamily: 'var(--font-mono, monospace)',
992
- },
993
-
994
- ':host .clickable-error': {
995
- cursor: 'pointer',
996
- textDecoration: 'underline',
997
- textDecorationStyle: 'dotted',
998
- },
999
-
1000
- ':host .clickable-error:hover': {
1001
- background: 'rgba(220, 38, 38, 0.2)',
1002
- },
1003
-
1004
- ':host .sig-badge': {
1005
- fontSize: '11px',
1006
- padding: '2px 6px',
1007
- marginLeft: '8px',
1008
- background: 'rgba(99, 102, 241, 0.1)',
1009
- color: '#6366f1',
1010
- borderRadius: '4px',
1011
- },
1012
-
682
+ // TS-specific: console container class name
1013
683
  ':host .ts-console': {
1014
684
  height: '120px',
1015
685
  borderTop: '1px solid var(--code-border, #e5e7eb)',
1016
686
  display: 'flex',
1017
687
  flexDirection: 'column',
1018
688
  },
1019
-
1020
- ':host .console-header': {
1021
- padding: '4px 12px',
1022
- background: 'var(--code-background, #f3f4f6)',
1023
- fontSize: '12px',
1024
- fontWeight: '500',
1025
- color: 'var(--text-color, #6b7280)',
1026
- opacity: '0.7',
1027
- borderBottom: '1px solid var(--code-border, #e5e7eb)',
1028
- },
1029
-
1030
- ':host .console-output': {
1031
- flex: '1',
1032
- margin: '0',
1033
- padding: '8px 12px',
1034
- background: 'var(--code-background, #f3f4f6)',
1035
- color: 'var(--text-color, #1f2937)',
1036
- fontSize: '12px',
1037
- fontFamily: 'ui-monospace, monospace',
1038
- overflow: 'auto',
1039
- whiteSpace: 'pre-wrap',
1040
- },
1041
-
1042
- ':host .clickable-line': {
1043
- cursor: 'pointer',
1044
- color: '#2563eb',
1045
- textDecoration: 'underline',
1046
- textDecorationStyle: 'dotted',
1047
- },
1048
-
1049
- ':host .clickable-line:hover': {
1050
- color: '#1d4ed8',
1051
- background: 'rgba(37, 99, 235, 0.1)',
1052
- },
1053
689
  },
1054
690
  }) as ElementCreator<TSPlayground>