ripple 0.2.19 → 0.2.21

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is a TypeScript UI framework for the web",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.19",
6
+ "version": "0.2.21",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -178,7 +178,6 @@ function RipplePlugin(config) {
178
178
  }
179
179
  return this.finishNode(node, 'TryStatement');
180
180
  }
181
-
182
181
  jsx_readToken() {
183
182
  let out = '',
184
183
  chunkStart = this.pos;
@@ -120,7 +120,7 @@ const visitors = {
120
120
  ...node,
121
121
  specifiers: node.specifiers
122
122
  .filter((spec) => spec.importKind !== 'type')
123
- .map((spec) => context.visit(spec.local)),
123
+ .map((spec) => context.visit(spec)),
124
124
  };
125
125
  },
126
126
 
@@ -466,7 +466,13 @@ export function visit_assignment_expression(node, context, build_assignment) {
466
466
  throw new Error(`Unexpected assignment type ${node.left.type}`);
467
467
  }
468
468
 
469
- return build_assignment(node.operator, node.left, node.right, context);
469
+ const transformed = build_assignment(node.operator, node.left, node.right, context);
470
+
471
+ if (transformed === node.left) {
472
+ return node;
473
+ }
474
+
475
+ return transformed;
470
476
  }
471
477
 
472
478
  export function build_assignment(operator, left, right, context) {
@@ -275,6 +275,25 @@ describe('basic', () => {
275
275
  expect(div.getAttribute('data-extra')).toBe('value');
276
276
  });
277
277
 
278
+ it('renders without crashing', () => {
279
+ component App() {
280
+ let foo;
281
+ let bar;
282
+ let baz;
283
+
284
+ foo = {};
285
+ foo = {'test': 0};
286
+ foo['abc'] = 123;
287
+
288
+ bar = { 'def': 456 };
289
+
290
+ baz = { 'ghi': 789 };
291
+ baz['jkl'] = 987;
292
+ }
293
+
294
+ render(App);
295
+ });
296
+
278
297
  it('renders multiple reactive lexical blocks', () => {
279
298
  component Basic() {
280
299
  <div>
@@ -344,4 +363,458 @@ describe('basic', () => {
344
363
 
345
364
  expect(container.querySelector('.count').textContent).toBe('0');
346
365
  });
366
+
367
+ it('renders with different event types', () => {
368
+ component Basic() {
369
+ let $focusCount = 0;
370
+ let $clickCount = 0;
371
+
372
+ <button
373
+ onFocus={() => $focusCount++}
374
+ onClick={() => $clickCount++}
375
+ >{'Test Button'}</button>
376
+ <div class='focus-count'>{$focusCount}</div>
377
+ <div class='click-count'>{$clickCount}</div>
378
+ }
379
+
380
+ render(Basic);
381
+
382
+ const button = container.querySelector('button');
383
+ const focusDiv = container.querySelector('.focus-count');
384
+ const clickDiv = container.querySelector('.click-count');
385
+
386
+ button.dispatchEvent(new Event('focus'));
387
+ flushSync();
388
+ expect(focusDiv.textContent).toBe('1');
389
+
390
+ button.click();
391
+ flushSync();
392
+ expect(clickDiv.textContent).toBe('1');
393
+ });
394
+
395
+ it('renders with capture events', () => {
396
+ component Basic() {
397
+ let $captureClicks = 0;
398
+ let $bubbleClicks = 0;
399
+
400
+ <div onClickCapture={() => $captureClicks++}>
401
+ <button onClick={() => $bubbleClicks++}>{'Click me'}</button>
402
+ <div class='capture-count'>{$captureClicks}</div>
403
+ <div class='bubble-count'>{$bubbleClicks}</div>
404
+ </div>
405
+ }
406
+
407
+ render(Basic);
408
+
409
+ const button = container.querySelector('button');
410
+ const captureDiv = container.querySelector('.capture-count');
411
+ const bubbleDiv = container.querySelector('.bubble-count');
412
+
413
+ button.click();
414
+ flushSync();
415
+
416
+ expect(captureDiv.textContent).toBe('1');
417
+ expect(bubbleDiv.textContent).toBe('1');
418
+ });
419
+
420
+ it('renders with component composition and children', () => {
421
+ component Card(props) {
422
+ <div class='card'>
423
+ <props.$children />
424
+ </div>
425
+ }
426
+
427
+ component Basic() {
428
+ <Card>
429
+ component $children() {
430
+ <p>{'Card content here'}</p>
431
+ }
432
+ </Card>
433
+ }
434
+
435
+ render(Basic);
436
+
437
+ const card = container.querySelector('.card');
438
+ const paragraph = card.querySelector('p');
439
+
440
+ expect(card).toBeTruthy();
441
+ expect(paragraph.textContent).toBe('Card content here');
442
+ });
443
+
444
+ it('renders with error handling simulation', () => {
445
+ component Basic() {
446
+ let $hasError = false;
447
+ let $errorMessage = '';
448
+
449
+ const triggerError = () => {
450
+ try {
451
+ throw new Error('Test error');
452
+ } catch (e) {
453
+ $hasError = true;
454
+ $errorMessage = e.message;
455
+ }
456
+ };
457
+
458
+ <div>
459
+ <button onClick={triggerError}>{'Trigger Error'}</button>
460
+ if ($hasError) {
461
+ <div class='error'>{'Error caught: ' + $errorMessage}</div>
462
+ } else {
463
+ <div class='success'>{'No error'}</div>
464
+ }
465
+ </div>
466
+ }
467
+
468
+ render(Basic);
469
+
470
+ const button = container.querySelector('button');
471
+ const successDiv = container.querySelector('.success');
472
+
473
+ expect(successDiv).toBeTruthy();
474
+ expect(successDiv.textContent).toBe('No error');
475
+
476
+ button.click();
477
+ flushSync();
478
+
479
+ const errorDiv = container.querySelector('.error');
480
+ expect(errorDiv).toBeTruthy();
481
+ expect(errorDiv.textContent).toBe('Error caught: Test error');
482
+ });
483
+
484
+ it('renders with computed reactive state', () => {
485
+ component Basic() {
486
+ let $count = 5;
487
+
488
+ <div class='count'>{$count}</div>
489
+ <div class='doubled'>{$count * 2}</div>
490
+ <div class='is-even'>{$count % 2 === 0 ? 'Even' : 'Odd'}</div>
491
+ <button onClick={() => $count++}>{'Increment'}</button>
492
+ }
493
+
494
+ render(Basic);
495
+
496
+ const countDiv = container.querySelector('.count');
497
+ const doubledDiv = container.querySelector('.doubled');
498
+ const evenDiv = container.querySelector('.is-even');
499
+ const button = container.querySelector('button');
500
+
501
+ expect(countDiv.textContent).toBe('5');
502
+ expect(doubledDiv.textContent).toBe('10');
503
+ expect(evenDiv.textContent).toBe('Odd');
504
+
505
+ button.click();
506
+ flushSync();
507
+
508
+ expect(countDiv.textContent).toBe('6');
509
+ expect(doubledDiv.textContent).toBe('12');
510
+ expect(evenDiv.textContent).toBe('Even');
511
+ });
512
+
513
+ it('renders with simple reactive objects', () => {
514
+ component Basic() {
515
+ let $user = {
516
+ $name: 'John',
517
+ $age: 25
518
+ };
519
+
520
+ <div class='name'>{$user.$name}</div>
521
+ <div class='age'>{$user.$age}</div>
522
+ <button onClick={() => {
523
+ $user.$name = 'Jane';
524
+ $user.$age = 30;
525
+ }}>{'Update User'}</button>
526
+ }
527
+
528
+ render(Basic);
529
+
530
+ const nameDiv = container.querySelector('.name');
531
+ const ageDiv = container.querySelector('.age');
532
+ const button = container.querySelector('button');
533
+
534
+ expect(nameDiv.textContent).toBe('John');
535
+ expect(ageDiv.textContent).toBe('25');
536
+
537
+ button.click();
538
+ flushSync();
539
+
540
+ expect(nameDiv.textContent).toBe('Jane');
541
+ expect(ageDiv.textContent).toBe('30');
542
+ });
543
+
544
+ it('renders with conditional rendering using if statements', () => {
545
+ component Basic() {
546
+ let $showContent = false;
547
+ let $userRole = 'guest';
548
+
549
+ <button onClick={() => $showContent = !$showContent}>{'Toggle Content'}</button>
550
+ <button onClick={() => $userRole = $userRole === 'guest' ? 'admin' : 'guest'}>{'Toggle Role'}</button>
551
+
552
+ <div class='content'>
553
+ if ($showContent) {
554
+ if ($userRole === 'admin') {
555
+ <div class='admin-content'>{'Admin content'}</div>
556
+ } else {
557
+ <div class='user-content'>{'User content'}</div>
558
+ }
559
+ } else {
560
+ <div class='no-content'>{'No content'}</div>
561
+ }
562
+ </div>
563
+ }
564
+
565
+ render(Basic);
566
+
567
+ const buttons = container.querySelectorAll('button');
568
+ const contentDiv = container.querySelector('.content');
569
+
570
+ expect(contentDiv.querySelector('.no-content')).toBeTruthy();
571
+ expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
572
+ expect(contentDiv.querySelector('.user-content')).toBeFalsy();
573
+
574
+ buttons[0].click();
575
+ flushSync();
576
+
577
+ expect(contentDiv.querySelector('.no-content')).toBeFalsy();
578
+ expect(contentDiv.querySelector('.user-content')).toBeTruthy();
579
+ expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
580
+
581
+ buttons[1].click();
582
+ flushSync();
583
+
584
+ expect(contentDiv.querySelector('.no-content')).toBeFalsy();
585
+ expect(contentDiv.querySelector('.user-content')).toBeFalsy();
586
+ expect(contentDiv.querySelector('.admin-content')).toBeTruthy();
587
+ });
588
+
589
+ it('renders with nested components and prop passing', () => {
590
+ component Button(props) {
591
+ <button class={props.variant} onClick={props.onClick}>
592
+ {props.label}
593
+ </button>
594
+ }
595
+
596
+ component Card(props) {
597
+ <div class='card'>
598
+ <h3>{props.title}</h3>
599
+ <p>{props.content}</p>
600
+ <Button variant='primary' label={props.buttonText} onClick={props.onAction} />
601
+ </div>
602
+ }
603
+
604
+ component Basic() {
605
+ let $clicked = false;
606
+
607
+ <Card
608
+ title='Test Card'
609
+ content='This is a test card'
610
+ buttonText='Click me'
611
+ onAction={() => $clicked = true}
612
+ />
613
+ <div class='status'>{$clicked ? 'Clicked' : 'Not clicked'}</div>
614
+ }
615
+
616
+ render(Basic);
617
+
618
+ const card = container.querySelector('.card');
619
+ const title = card.querySelector('h3');
620
+ const content = card.querySelector('p');
621
+ const button = card.querySelector('button');
622
+ const status = container.querySelector('.status');
623
+
624
+ expect(title.textContent).toBe('Test Card');
625
+ expect(content.textContent).toBe('This is a test card');
626
+ expect(button.textContent).toBe('Click me');
627
+ expect(button.className).toBe('primary');
628
+ expect(status.textContent).toBe('Not clicked');
629
+
630
+ button.click();
631
+ flushSync();
632
+
633
+ expect(status.textContent).toBe('Clicked');
634
+ });
635
+
636
+ it('renders with complex event handling and state updates', () => {
637
+ component Basic() {
638
+ let $counter = 0;
639
+ let $history = [];
640
+ let $isEven = true;
641
+
642
+ const handleIncrement = () => {
643
+ $counter++;
644
+ $history = [...$history, `Inc to ${$counter}`];
645
+ $isEven = $counter % 2 === 0;
646
+ };
647
+
648
+ const handleDecrement = () => {
649
+ $counter--;
650
+ $history = [...$history, `Dec to ${$counter}`];
651
+ $isEven = $counter % 2 === 0;
652
+ };
653
+
654
+ const handleReset = () => {
655
+ $counter = 0;
656
+ $history = [...$history, 'Reset'];
657
+ $isEven = true;
658
+ };
659
+
660
+ <div class='counter'>{$counter}</div>
661
+ <div class='parity'>{$isEven ? 'Even' : 'Odd'}</div>
662
+ <div class='history-count'>{$history.length}</div>
663
+
664
+ <button class='inc-btn' onClick={handleIncrement}>{'+'}</button>
665
+ <button class='dec-btn' onClick={handleDecrement}>{'-'}</button>
666
+ <button class='reset-btn' onClick={handleReset}>{'Reset'}</button>
667
+ }
668
+
669
+ render(Basic);
670
+
671
+ const counterDiv = container.querySelector('.counter');
672
+ const parityDiv = container.querySelector('.parity');
673
+ const historyDiv = container.querySelector('.history-count');
674
+ const incBtn = container.querySelector('.inc-btn');
675
+ const decBtn = container.querySelector('.dec-btn');
676
+ const resetBtn = container.querySelector('.reset-btn');
677
+
678
+ expect(counterDiv.textContent).toBe('0');
679
+ expect(parityDiv.textContent).toBe('Even');
680
+ expect(historyDiv.textContent).toBe('0');
681
+
682
+ incBtn.click();
683
+ flushSync();
684
+
685
+ expect(counterDiv.textContent).toBe('1');
686
+ expect(parityDiv.textContent).toBe('Odd');
687
+ expect(historyDiv.textContent).toBe('1');
688
+
689
+ incBtn.click();
690
+ flushSync();
691
+
692
+ expect(counterDiv.textContent).toBe('2');
693
+ expect(parityDiv.textContent).toBe('Even');
694
+ expect(historyDiv.textContent).toBe('2');
695
+
696
+ decBtn.click();
697
+ flushSync();
698
+
699
+ expect(counterDiv.textContent).toBe('1');
700
+ expect(parityDiv.textContent).toBe('Odd');
701
+ expect(historyDiv.textContent).toBe('3');
702
+
703
+ resetBtn.click();
704
+ flushSync();
705
+
706
+ expect(counterDiv.textContent).toBe('0');
707
+ expect(parityDiv.textContent).toBe('Even');
708
+ expect(historyDiv.textContent).toBe('4');
709
+ });
710
+
711
+ it('renders with reactive component props', () => {
712
+ component ChildComponent(props) {
713
+ <div class='child-content'>{props.$text}</div>
714
+ <div class='child-count'>{props.$count}</div>
715
+ }
716
+
717
+ component Basic() {
718
+ let $message = 'Hello';
719
+ let $number = 1;
720
+
721
+ <ChildComponent $text={$message} $count={$number} />
722
+ <button onClick={() => {
723
+ $message = $message === 'Hello' ? 'Goodbye' : 'Hello';
724
+ $number++;
725
+ }}>{'Update Props'}</button>
726
+ }
727
+
728
+ render(Basic);
729
+
730
+ const contentDiv = container.querySelector('.child-content');
731
+ const countDiv = container.querySelector('.child-count');
732
+ const button = container.querySelector('button');
733
+
734
+ expect(contentDiv.textContent).toBe('Hello');
735
+ expect(countDiv.textContent).toBe('1');
736
+
737
+ button.click();
738
+ flushSync();
739
+
740
+ expect(contentDiv.textContent).toBe('Goodbye');
741
+ expect(countDiv.textContent).toBe('2');
742
+
743
+ button.click();
744
+ flushSync();
745
+
746
+ expect(contentDiv.textContent).toBe('Hello');
747
+ expect(countDiv.textContent).toBe('3');
748
+ });
749
+
750
+ it('renders with styling scoped to component', () => {
751
+ component Basic() {
752
+ <div class='styled-container'>
753
+ <h1>{'Styled heading'}</h1>
754
+ <p class='text'>{'Styled paragraph'}</p>
755
+ </div>
756
+
757
+ <style>
758
+ .styled-container {
759
+ background-color: rgb(0, 0, 255);
760
+ padding: 16px;
761
+ }
762
+
763
+ h1 {
764
+ color: rgb(255, 255, 255);
765
+ font-size: 32px;
766
+ }
767
+
768
+ .text {
769
+ color: rgb(200, 200, 200);
770
+ font-size: 14px;
771
+ }
772
+ </style>
773
+ }
774
+
775
+ render(Basic);
776
+
777
+ const styledContainer = container.querySelector('.styled-container');
778
+ const heading = styledContainer.querySelector('h1');
779
+ const paragraph = styledContainer.querySelector('.text');
780
+
781
+ expect(styledContainer).toBeTruthy();
782
+ expect(heading.textContent).toBe('Styled heading');
783
+ expect(paragraph.textContent).toBe('Styled paragraph');
784
+ });
785
+
786
+ it('renders with mixed static and dynamic content', () => {
787
+ component Basic() {
788
+ let $name = 'World';
789
+ let $count = 0;
790
+ const staticMessage = 'Welcome to Ripple!';
791
+
792
+ <div class='mixed-content'>
793
+ <h1>{staticMessage}</h1>
794
+ <p class='greeting'>{'Hello, ' + $name + '!'}</p>
795
+ <p class='notifications'>{'You have ' + $count + ' notifications'}</p>
796
+ <button onClick={() => $count++}>{'Add Notification'}</button>
797
+ <button onClick={() => $name = $name === 'World' ? 'User' : 'World'}>{'Toggle Name'}</button>
798
+ </div>
799
+ }
800
+
801
+ render(Basic);
802
+
803
+ const heading = container.querySelector('h1');
804
+ const greetingP = container.querySelector('.greeting');
805
+ const notificationsP = container.querySelector('.notifications');
806
+ const buttons = container.querySelectorAll('button');
807
+
808
+ expect(heading.textContent).toBe('Welcome to Ripple!');
809
+ expect(greetingP.textContent).toBe('Hello, World!');
810
+ expect(notificationsP.textContent).toBe('You have 0 notifications');
811
+
812
+ buttons[0].click();
813
+ flushSync();
814
+ expect(notificationsP.textContent).toBe('You have 1 notifications');
815
+
816
+ buttons[1].click();
817
+ flushSync();
818
+ expect(greetingP.textContent).toBe('Hello, User!');
819
+ });
347
820
  });
@@ -32,8 +32,6 @@ describe('context', () => {
32
32
  component TestContext() {
33
33
  const value = MyContext.get();
34
34
 
35
- console.log(value);
36
-
37
35
  MyContext.set("Hello from context!");
38
36
 
39
37
  <Child />