ripple 0.2.20 → 0.2.22

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.20",
6
+ "version": "0.2.22",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -77,20 +77,6 @@ function RipplePlugin(config) {
77
77
  return this.finishNode(node, 'JSXExpressionContainer');
78
78
  }
79
79
 
80
- jsx_parseBindingExpression() {
81
- let node = this.startNode();
82
- this.next();
83
-
84
- if (this.type === tt.bracketR) {
85
- this.raise(node.start, 'bindings must only be assigned a non-empty expression');
86
- }
87
-
88
- node.expression = this.parseExpression();
89
-
90
- this.expect(tt.bracketR);
91
- return this.finishNode(node, 'BindingExpression');
92
- }
93
-
94
80
  jsx_parseTupleContainer() {
95
81
  var t = this.startNode();
96
82
  return (
@@ -104,7 +90,6 @@ function RipplePlugin(config) {
104
90
 
105
91
  jsx_parseAttribute() {
106
92
  let node = this.startNode();
107
-
108
93
  if (this.eat(tt.braceL)) {
109
94
  if (this.type.label === '@') {
110
95
  this.next();
@@ -144,9 +129,6 @@ function RipplePlugin(config) {
144
129
  const tok = this.acornTypeScript.tokTypes;
145
130
 
146
131
  switch (this.type) {
147
- case tt.bracketL:
148
- var t = this.jsx_parseBindingExpression();
149
- return t;
150
132
  case tt.braceL:
151
133
  var t = this.jsx_parseExpressionContainer();
152
134
  return (
@@ -196,21 +178,6 @@ function RipplePlugin(config) {
196
178
  }
197
179
  return this.finishNode(node, 'TryStatement');
198
180
  }
199
-
200
- // updateContext(e) {
201
- // const s = this.type;
202
- // const context = this.curContext();
203
- // const tokContexts = this.acornTypeScript.tokContexts;
204
-
205
- // if (s == tt.bracketL && context == tokContexts.tc_oTag) {
206
- // this.context.push(tc.b_expr);
207
- // } else if (s == tt.bracketR && context == tc.b_expr) {
208
- // this.context.pop();
209
- // }
210
-
211
- // return super.updateContext(e);
212
- // }
213
-
214
181
  jsx_readToken() {
215
182
  let out = '',
216
183
  chunkStart = this.pos;
@@ -787,8 +787,8 @@ const visitors = {
787
787
 
788
788
  if (
789
789
  left.type === 'MemberExpression' &&
790
- ((left.property.type === 'Identifier' && is_tracked_name(left.property.name)) ||
791
- left.computed)
790
+ left.property.type === 'Identifier' &&
791
+ is_tracked_name(left.property.name)
792
792
  ) {
793
793
  return b.call(
794
794
  '$.set_property',
@@ -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) {
@@ -30,3 +30,15 @@ exports[`basic > render static text 1`] = `
30
30
 
31
31
  </div>
32
32
  `;
33
+
34
+ exports[`basic > renders simple JS expression logic correctly 1`] = `
35
+ <div>
36
+ <div>
37
+ {"0":"Test"}
38
+ </div>
39
+ <div>
40
+ 1
41
+ </div>
42
+
43
+ </div>
44
+ `;
@@ -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,472 @@ 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 simple JS expression logic correctly', () => {
514
+ component Example() {
515
+ let test = {}
516
+ let counter = 0;
517
+ test[counter++] = 'Test';
518
+
519
+ <div>{JSON.stringify(test)}</div>
520
+ <div>{JSON.stringify(counter)}</div>
521
+ }
522
+ render(Example);
523
+
524
+ expect(container).toMatchSnapshot();
525
+ });
526
+
527
+ it('renders with simple reactive objects', () => {
528
+ component Basic() {
529
+ let $user = {
530
+ $name: 'John',
531
+ $age: 25
532
+ };
533
+
534
+ <div class='name'>{$user.$name}</div>
535
+ <div class='age'>{$user.$age}</div>
536
+ <button onClick={() => {
537
+ $user.$name = 'Jane';
538
+ $user.$age = 30;
539
+ }}>{'Update User'}</button>
540
+ }
541
+
542
+ render(Basic);
543
+
544
+ const nameDiv = container.querySelector('.name');
545
+ const ageDiv = container.querySelector('.age');
546
+ const button = container.querySelector('button');
547
+
548
+ expect(nameDiv.textContent).toBe('John');
549
+ expect(ageDiv.textContent).toBe('25');
550
+
551
+ button.click();
552
+ flushSync();
553
+
554
+ expect(nameDiv.textContent).toBe('Jane');
555
+ expect(ageDiv.textContent).toBe('30');
556
+ });
557
+
558
+ it('renders with conditional rendering using if statements', () => {
559
+ component Basic() {
560
+ let $showContent = false;
561
+ let $userRole = 'guest';
562
+
563
+ <button onClick={() => $showContent = !$showContent}>{'Toggle Content'}</button>
564
+ <button onClick={() => $userRole = $userRole === 'guest' ? 'admin' : 'guest'}>{'Toggle Role'}</button>
565
+
566
+ <div class='content'>
567
+ if ($showContent) {
568
+ if ($userRole === 'admin') {
569
+ <div class='admin-content'>{'Admin content'}</div>
570
+ } else {
571
+ <div class='user-content'>{'User content'}</div>
572
+ }
573
+ } else {
574
+ <div class='no-content'>{'No content'}</div>
575
+ }
576
+ </div>
577
+ }
578
+
579
+ render(Basic);
580
+
581
+ const buttons = container.querySelectorAll('button');
582
+ const contentDiv = container.querySelector('.content');
583
+
584
+ expect(contentDiv.querySelector('.no-content')).toBeTruthy();
585
+ expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
586
+ expect(contentDiv.querySelector('.user-content')).toBeFalsy();
587
+
588
+ buttons[0].click();
589
+ flushSync();
590
+
591
+ expect(contentDiv.querySelector('.no-content')).toBeFalsy();
592
+ expect(contentDiv.querySelector('.user-content')).toBeTruthy();
593
+ expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
594
+
595
+ buttons[1].click();
596
+ flushSync();
597
+
598
+ expect(contentDiv.querySelector('.no-content')).toBeFalsy();
599
+ expect(contentDiv.querySelector('.user-content')).toBeFalsy();
600
+ expect(contentDiv.querySelector('.admin-content')).toBeTruthy();
601
+ });
602
+
603
+ it('renders with nested components and prop passing', () => {
604
+ component Button(props) {
605
+ <button class={props.variant} onClick={props.onClick}>
606
+ {props.label}
607
+ </button>
608
+ }
609
+
610
+ component Card(props) {
611
+ <div class='card'>
612
+ <h3>{props.title}</h3>
613
+ <p>{props.content}</p>
614
+ <Button variant='primary' label={props.buttonText} onClick={props.onAction} />
615
+ </div>
616
+ }
617
+
618
+ component Basic() {
619
+ let $clicked = false;
620
+
621
+ <Card
622
+ title='Test Card'
623
+ content='This is a test card'
624
+ buttonText='Click me'
625
+ onAction={() => $clicked = true}
626
+ />
627
+ <div class='status'>{$clicked ? 'Clicked' : 'Not clicked'}</div>
628
+ }
629
+
630
+ render(Basic);
631
+
632
+ const card = container.querySelector('.card');
633
+ const title = card.querySelector('h3');
634
+ const content = card.querySelector('p');
635
+ const button = card.querySelector('button');
636
+ const status = container.querySelector('.status');
637
+
638
+ expect(title.textContent).toBe('Test Card');
639
+ expect(content.textContent).toBe('This is a test card');
640
+ expect(button.textContent).toBe('Click me');
641
+ expect(button.className).toBe('primary');
642
+ expect(status.textContent).toBe('Not clicked');
643
+
644
+ button.click();
645
+ flushSync();
646
+
647
+ expect(status.textContent).toBe('Clicked');
648
+ });
649
+
650
+ it('renders with complex event handling and state updates', () => {
651
+ component Basic() {
652
+ let $counter = 0;
653
+ let $history = [];
654
+ let $isEven = true;
655
+
656
+ const handleIncrement = () => {
657
+ $counter++;
658
+ $history = [...$history, `Inc to ${$counter}`];
659
+ $isEven = $counter % 2 === 0;
660
+ };
661
+
662
+ const handleDecrement = () => {
663
+ $counter--;
664
+ $history = [...$history, `Dec to ${$counter}`];
665
+ $isEven = $counter % 2 === 0;
666
+ };
667
+
668
+ const handleReset = () => {
669
+ $counter = 0;
670
+ $history = [...$history, 'Reset'];
671
+ $isEven = true;
672
+ };
673
+
674
+ <div class='counter'>{$counter}</div>
675
+ <div class='parity'>{$isEven ? 'Even' : 'Odd'}</div>
676
+ <div class='history-count'>{$history.length}</div>
677
+
678
+ <button class='inc-btn' onClick={handleIncrement}>{'+'}</button>
679
+ <button class='dec-btn' onClick={handleDecrement}>{'-'}</button>
680
+ <button class='reset-btn' onClick={handleReset}>{'Reset'}</button>
681
+ }
682
+
683
+ render(Basic);
684
+
685
+ const counterDiv = container.querySelector('.counter');
686
+ const parityDiv = container.querySelector('.parity');
687
+ const historyDiv = container.querySelector('.history-count');
688
+ const incBtn = container.querySelector('.inc-btn');
689
+ const decBtn = container.querySelector('.dec-btn');
690
+ const resetBtn = container.querySelector('.reset-btn');
691
+
692
+ expect(counterDiv.textContent).toBe('0');
693
+ expect(parityDiv.textContent).toBe('Even');
694
+ expect(historyDiv.textContent).toBe('0');
695
+
696
+ incBtn.click();
697
+ flushSync();
698
+
699
+ expect(counterDiv.textContent).toBe('1');
700
+ expect(parityDiv.textContent).toBe('Odd');
701
+ expect(historyDiv.textContent).toBe('1');
702
+
703
+ incBtn.click();
704
+ flushSync();
705
+
706
+ expect(counterDiv.textContent).toBe('2');
707
+ expect(parityDiv.textContent).toBe('Even');
708
+ expect(historyDiv.textContent).toBe('2');
709
+
710
+ decBtn.click();
711
+ flushSync();
712
+
713
+ expect(counterDiv.textContent).toBe('1');
714
+ expect(parityDiv.textContent).toBe('Odd');
715
+ expect(historyDiv.textContent).toBe('3');
716
+
717
+ resetBtn.click();
718
+ flushSync();
719
+
720
+ expect(counterDiv.textContent).toBe('0');
721
+ expect(parityDiv.textContent).toBe('Even');
722
+ expect(historyDiv.textContent).toBe('4');
723
+ });
724
+
725
+ it('renders with reactive component props', () => {
726
+ component ChildComponent(props) {
727
+ <div class='child-content'>{props.$text}</div>
728
+ <div class='child-count'>{props.$count}</div>
729
+ }
730
+
731
+ component Basic() {
732
+ let $message = 'Hello';
733
+ let $number = 1;
734
+
735
+ <ChildComponent $text={$message} $count={$number} />
736
+ <button onClick={() => {
737
+ $message = $message === 'Hello' ? 'Goodbye' : 'Hello';
738
+ $number++;
739
+ }}>{'Update Props'}</button>
740
+ }
741
+
742
+ render(Basic);
743
+
744
+ const contentDiv = container.querySelector('.child-content');
745
+ const countDiv = container.querySelector('.child-count');
746
+ const button = container.querySelector('button');
747
+
748
+ expect(contentDiv.textContent).toBe('Hello');
749
+ expect(countDiv.textContent).toBe('1');
750
+
751
+ button.click();
752
+ flushSync();
753
+
754
+ expect(contentDiv.textContent).toBe('Goodbye');
755
+ expect(countDiv.textContent).toBe('2');
756
+
757
+ button.click();
758
+ flushSync();
759
+
760
+ expect(contentDiv.textContent).toBe('Hello');
761
+ expect(countDiv.textContent).toBe('3');
762
+ });
763
+
764
+ it('renders with styling scoped to component', () => {
765
+ component Basic() {
766
+ <div class='styled-container'>
767
+ <h1>{'Styled heading'}</h1>
768
+ <p class='text'>{'Styled paragraph'}</p>
769
+ </div>
770
+
771
+ <style>
772
+ .styled-container {
773
+ background-color: rgb(0, 0, 255);
774
+ padding: 16px;
775
+ }
776
+
777
+ h1 {
778
+ color: rgb(255, 255, 255);
779
+ font-size: 32px;
780
+ }
781
+
782
+ .text {
783
+ color: rgb(200, 200, 200);
784
+ font-size: 14px;
785
+ }
786
+ </style>
787
+ }
788
+
789
+ render(Basic);
790
+
791
+ const styledContainer = container.querySelector('.styled-container');
792
+ const heading = styledContainer.querySelector('h1');
793
+ const paragraph = styledContainer.querySelector('.text');
794
+
795
+ expect(styledContainer).toBeTruthy();
796
+ expect(heading.textContent).toBe('Styled heading');
797
+ expect(paragraph.textContent).toBe('Styled paragraph');
798
+ });
799
+
800
+ it('renders with mixed static and dynamic content', () => {
801
+ component Basic() {
802
+ let $name = 'World';
803
+ let $count = 0;
804
+ const staticMessage = 'Welcome to Ripple!';
805
+
806
+ <div class='mixed-content'>
807
+ <h1>{staticMessage}</h1>
808
+ <p class='greeting'>{'Hello, ' + $name + '!'}</p>
809
+ <p class='notifications'>{'You have ' + $count + ' notifications'}</p>
810
+ <button onClick={() => $count++}>{'Add Notification'}</button>
811
+ <button onClick={() => $name = $name === 'World' ? 'User' : 'World'}>{'Toggle Name'}</button>
812
+ </div>
813
+ }
814
+
815
+ render(Basic);
816
+
817
+ const heading = container.querySelector('h1');
818
+ const greetingP = container.querySelector('.greeting');
819
+ const notificationsP = container.querySelector('.notifications');
820
+ const buttons = container.querySelectorAll('button');
821
+
822
+ expect(heading.textContent).toBe('Welcome to Ripple!');
823
+ expect(greetingP.textContent).toBe('Hello, World!');
824
+ expect(notificationsP.textContent).toBe('You have 0 notifications');
825
+
826
+ buttons[0].click();
827
+ flushSync();
828
+ expect(notificationsP.textContent).toBe('You have 1 notifications');
829
+
830
+ buttons[1].click();
831
+ flushSync();
832
+ expect(greetingP.textContent).toBe('Hello, User!');
833
+ });
347
834
  });
@@ -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 />