ripple 0.2.203 → 0.2.205

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 an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.203",
6
+ "version": "0.2.205",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -97,6 +97,6 @@
97
97
  "vscode-languageserver-types": "^3.17.5"
98
98
  },
99
99
  "peerDependencies": {
100
- "ripple": "0.2.203"
100
+ "ripple": "0.2.205"
101
101
  }
102
102
  }
@@ -1749,15 +1749,7 @@ function RipplePlugin(config) {
1749
1749
  jsx_parseAttributeValue() {
1750
1750
  switch (this.type) {
1751
1751
  case tt.braceL:
1752
- const t = this.jsx_parseExpressionContainer();
1753
- return (
1754
- t.expression.type === 'JSXEmptyExpression' &&
1755
- this.raise(
1756
- /** @type {AST.NodeWithLocation} */ (t).start,
1757
- 'attributes must only be assigned a non-empty expression',
1758
- ),
1759
- t
1760
- );
1752
+ return this.jsx_parseExpressionContainer();
1761
1753
  case tstt.jsxTagStart:
1762
1754
  case tt.string:
1763
1755
  return this.parseExprAtom();
@@ -10,7 +10,10 @@
10
10
  StyleClasses,
11
11
  } from '#compiler';
12
12
  */
13
- /** @import * as AST from 'estree' */
13
+ /**
14
+ @import * as AST from 'estree';
15
+ @import * as ESTreeJSX from 'estree-jsx';
16
+ */
14
17
 
15
18
  import * as b from '../../../utils/builders.js';
16
19
  import { walk } from 'zimmerframe';
@@ -959,6 +962,31 @@ const visitors = {
959
962
 
960
963
  for (const attr of node.attributes) {
961
964
  if (attr.type === 'Attribute') {
965
+ if (attr.value && attr.value.type === 'JSXEmptyExpression') {
966
+ const value = /** @type {ESTreeJSX.JSXEmptyExpression & AST.NodeWithLocation} */ (
967
+ attr.value
968
+ );
969
+ error(
970
+ 'attributes must only be assigned a non-empty expression',
971
+ state.analysis.module.filename,
972
+ {
973
+ ...value,
974
+ start: value.start - 1,
975
+ end: value.end + 1,
976
+ loc: {
977
+ start: {
978
+ line: value.loc.start.line,
979
+ column: value.loc.start.column - 1,
980
+ },
981
+ end: {
982
+ line: value.loc.end.line,
983
+ column: value.loc.end.column + 1,
984
+ },
985
+ },
986
+ },
987
+ context.state.loose ? context.state.analysis.errors : undefined,
988
+ );
989
+ }
962
990
  if (attr.name.type === 'Identifier') {
963
991
  attribute_names.add(attr.name);
964
992
 
@@ -2492,11 +2492,16 @@ function transform_ts_child(node, context) {
2492
2492
  if (attr.type === 'Attribute') {
2493
2493
  const metadata = { await: false };
2494
2494
  const name = visit(attr.name, { ...state, metadata });
2495
+ const attr_value = /** @type { AST.Expression & AST.NodeWithLocation | null} */ (
2496
+ attr.value
2497
+ );
2495
2498
  const value =
2496
- attr.value === null
2497
- ? b.literal(true)
2499
+ attr_value === null
2500
+ ? // <div attr>, not adding `name` for loc because `jsx_name` below
2501
+ // will take care of the mapping JSXAttribute's JSXIdentifier
2502
+ b.literal(true)
2498
2503
  : // reset init, update, final to avoid adding attr value to the component body
2499
- visit(attr.value, SetStateForOutsideComponent(state, { metadata }));
2504
+ visit(attr_value, SetStateForOutsideComponent(state, { metadata }));
2500
2505
 
2501
2506
  // Handle both regular identifiers and tracked identifiers
2502
2507
  /** @type {string} */
@@ -2524,7 +2529,23 @@ function transform_ts_child(node, context) {
2524
2529
  jsx_name,
2525
2530
  b.jsx_expression_container(
2526
2531
  /** @type {AST.Expression} */ (value),
2527
- /** @type {AST.NodeWithLocation} */ (attr.value),
2532
+ attr_value === null
2533
+ ? /** @type {AST.NodeWithLocation} */ (value)
2534
+ : // account location for opening and closing braces around the expression
2535
+ /** @type {AST.NodeWithLocation} */ ({
2536
+ start: attr_value.start - 1,
2537
+ end: attr_value.end + 1,
2538
+ loc: {
2539
+ start: {
2540
+ line: attr_value.loc.start.line,
2541
+ column: attr_value.loc.start.column - 1,
2542
+ },
2543
+ end: {
2544
+ line: attr_value.loc.end.line,
2545
+ column: attr_value.loc.end.column + 1,
2546
+ },
2547
+ },
2548
+ }),
2528
2549
  ),
2529
2550
  attr.shorthand ?? false,
2530
2551
  /** @type {AST.NodeWithLocation} */ (attr),
@@ -3587,6 +3608,16 @@ function create_tsx_with_typescript_support(comments) {
3587
3608
  context.write(node.name);
3588
3609
  context.location(loc.end.line, loc.end.column);
3589
3610
  },
3611
+ JSXExpressionContainer(node, context) {
3612
+ const loc = /** @type {AST.SourceLocation} */ (node.loc);
3613
+ if (!loc) {
3614
+ base_tsx.JSXExpressionContainer?.(node, context);
3615
+ return;
3616
+ }
3617
+ context.location(loc.start.line, loc.start.column);
3618
+ base_tsx.JSXExpressionContainer?.(node, context);
3619
+ context.location(loc.end.line, loc.end.column);
3620
+ },
3590
3621
  MethodDefinition(node, context) {
3591
3622
  node.value.metadata.is_method = true;
3592
3623
  /** @type {AST.Position | undefined} */
@@ -3684,6 +3715,16 @@ function create_tsx_with_typescript_support(comments) {
3684
3715
  context.visit(node.value.body);
3685
3716
  }
3686
3717
  },
3718
+ TSAsExpression(node, context) {
3719
+ if (!node.loc) {
3720
+ base_tsx.TSAsExpression?.(node, context);
3721
+ return;
3722
+ }
3723
+ const loc = /** @type {AST.SourceLocation} */ (node.loc);
3724
+ context.location(loc.start.line, loc.start.column);
3725
+ base_tsx.TSAsExpression?.(node, context);
3726
+ context.location(loc.end.line, loc.end.column);
3727
+ },
3687
3728
  TSObjectKeyword(node, context) {
3688
3729
  if (node.loc) {
3689
3730
  context.location(node.loc.start.line, node.loc.start.column);
@@ -652,6 +652,11 @@ export function convert_source_map_to_mappings(
652
652
  }
653
653
  return;
654
654
  } else if (node.type === 'JSXExpressionContainer') {
655
+ if (node.loc) {
656
+ mappings.push(
657
+ get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
658
+ );
659
+ }
655
660
  // Visit the expression inside {}
656
661
  if (node.expression) {
657
662
  visit(node.expression);
@@ -131,6 +131,7 @@ declare module 'estree' {
131
131
  StyleIdentifier: StyleIdentifier;
132
132
  ServerIdentifier: ServerIdentifier;
133
133
  Text: TextNode;
134
+ JSXEmptyExpression: ESTreeJSX.JSXEmptyExpression;
134
135
  }
135
136
 
136
137
  // Missing estree type
@@ -15,7 +15,7 @@ export class TrackedDate extends Date {
15
15
  // @ts-ignore
16
16
  super(...params);
17
17
 
18
- var block = this.#block = safe_scope();
18
+ var block = (this.#block = safe_scope());
19
19
  this.#time = tracked(super.getTime(), block);
20
20
 
21
21
  if (!init) this.#init();
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { effect, render } from './blocks.js';
11
11
  import { on } from './events.js';
12
- import { get, set, tick, untrack } from './runtime.js';
12
+ import { get, set } from './runtime.js';
13
13
  import { is_array, is_tracked_object } from './utils.js';
14
14
 
15
15
  /**
@@ -216,11 +216,12 @@ export function bindValue(maybe_tracked, set_func = undefined) {
216
216
  value = [].map.call(select.querySelectorAll(query), get_option_value);
217
217
  } else {
218
218
  /** @type {HTMLOptionElement | null} */
219
- // @ts-ignore
220
219
  var selected_option =
221
- select.querySelector(query) ??
220
+ /** @type {HTMLOptionElement | null} */ (select.querySelector(query)) ??
222
221
  // will fall back to first non-disabled option if no option is selected
223
- select.querySelector('option:not([disabled])');
222
+ /** @type {HTMLOptionElement | null} */ (
223
+ select.querySelector('option:not([disabled])')
224
+ );
224
225
  value = selected_option && get_option_value(selected_option);
225
226
  }
226
227
 
@@ -234,8 +235,9 @@ export function bindValue(maybe_tracked, set_func = undefined) {
234
235
  // Mounting and value undefined -> take selection from dom
235
236
  if (mounting && value === undefined) {
236
237
  /** @type {HTMLOptionElement | null} */
237
- // @ts-ignore
238
- var selected_option = select.querySelector(':checked');
238
+ var selected_option = /** @type {HTMLOptionElement | null} */ (
239
+ select.querySelector(':checked')
240
+ );
239
241
  if (selected_option !== null) {
240
242
  value = get_option_value(selected_option);
241
243
  setter(value);
@@ -246,26 +248,15 @@ export function bindValue(maybe_tracked, set_func = undefined) {
246
248
  });
247
249
  } else {
248
250
  var input = /** @type {HTMLInputElement} */ (node);
251
+ var selection_restore_needed = false;
249
252
 
250
- clear_event = on(input, 'input', async () => {
253
+ clear_event = on(input, 'input', () => {
254
+ selection_restore_needed = true;
251
255
  /** @type {any} */
252
256
  var value = input.value;
253
257
  value = is_numberlike_input(input) ? to_number(value) : value;
258
+ // the setter will schedule a microtask and the render block below will run
254
259
  setter(value);
255
-
256
- await tick();
257
-
258
- if (value !== getter()) {
259
- var start = input.selectionStart;
260
- var end = input.selectionEnd;
261
- input.value = value ?? '';
262
-
263
- // Restore selection
264
- if (end !== null) {
265
- input.selectionStart = start;
266
- input.selectionEnd = Math.min(end, input.value.length);
267
- }
268
- }
269
260
  });
270
261
 
271
262
  render(() => {
@@ -280,7 +271,23 @@ export function bindValue(maybe_tracked, set_func = undefined) {
280
271
  }
281
272
 
282
273
  if (value !== input.value) {
283
- input.value = value ?? '';
274
+ if (selection_restore_needed) {
275
+ var start = input.selectionStart;
276
+ var end = input.selectionEnd;
277
+
278
+ input.value = value ?? '';
279
+
280
+ // Restore selection
281
+ if (end !== null && start !== null) {
282
+ end = Math.min(end, input.value.length);
283
+ start = Math.min(start, end);
284
+ input.selectionStart = start;
285
+ input.selectionEnd = end;
286
+ }
287
+ selection_restore_needed = false;
288
+ } else {
289
+ input.value = value ?? '';
290
+ }
284
291
  }
285
292
  });
286
293
 
@@ -527,11 +534,9 @@ export function bind_content_editable(maybe_tracked, property, set_func = undefi
527
534
  var value = getter();
528
535
  if (element[property] !== value) {
529
536
  if (value == null) {
530
- // @ts-ignore
531
537
  var non_null_value = element[property];
532
538
  setter(non_null_value);
533
539
  } else {
534
- // @ts-ignore
535
540
  element[property] = value + '';
536
541
  }
537
542
  }
@@ -2,6 +2,7 @@ import {
2
2
  flushSync,
3
3
  track,
4
4
  effect,
5
+ untrack,
5
6
  TrackedObject,
6
7
  bindValue,
7
8
  bindChecked,
@@ -239,7 +240,7 @@ describe('use value()', () => {
239
240
  });
240
241
 
241
242
  it('should update checked on input', () => {
242
- const logs: string[] = [];
243
+ const logs: (string | boolean)[] = [];
243
244
 
244
245
  component App() {
245
246
  const value = track(false);
@@ -335,7 +336,7 @@ describe('use value()', () => {
335
336
  });
336
337
 
337
338
  it('should update indeterminate on input', () => {
338
- const logs: string[] = [];
339
+ const logs: (string | boolean)[] = [];
339
340
 
340
341
  component App() {
341
342
  const value = track(false);
@@ -2533,6 +2534,305 @@ describe('bindNode', () => {
2533
2534
 
2534
2535
  expect(input.value).toBe('Set by ref');
2535
2536
  });
2537
+
2538
+ it('should accurately reflect values mutated through a tracked setter', () => {
2539
+ component App() {
2540
+ let value = track(
2541
+ '',
2542
+ (val) => {
2543
+ return val;
2544
+ },
2545
+ (next) => {
2546
+ if (next.includes('c')) {
2547
+ next = next.replace(/c/g, '');
2548
+ }
2549
+ return next;
2550
+ },
2551
+ );
2552
+
2553
+ <input type="text" {ref bindValue(value)} />
2554
+ <div>{@value}</div>
2555
+ }
2556
+
2557
+ render(App);
2558
+ flushSync();
2559
+
2560
+ const input = container.querySelector('input') as HTMLInputElement;
2561
+ const div = container.querySelector('div') as HTMLDivElement;
2562
+
2563
+ expect(input.value).toBe('');
2564
+ expect(div.textContent).toBe('');
2565
+
2566
+ input.value = 'abc';
2567
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2568
+ flushSync();
2569
+
2570
+ expect(input.value).toBe('ab');
2571
+ expect(div.textContent).toBe('ab');
2572
+ });
2573
+
2574
+ it('should accurately reflect values when a getter modifies value', () => {
2575
+ component App() {
2576
+ let value = track(
2577
+ '',
2578
+ (val) => {
2579
+ if (val.includes('c')) {
2580
+ val = val.replace(/c/g, '');
2581
+ }
2582
+ return val;
2583
+ },
2584
+ (next) => {
2585
+ return next;
2586
+ },
2587
+ );
2588
+
2589
+ <input type="text" {ref bindValue(value)} />
2590
+ <div>{@value}</div>
2591
+ }
2592
+
2593
+ render(App);
2594
+ flushSync();
2595
+
2596
+ const input = container.querySelector('input') as HTMLInputElement;
2597
+ const div = container.querySelector('div') as HTMLDivElement;
2598
+
2599
+ expect(input.value).toBe('');
2600
+ expect(div.textContent).toBe('');
2601
+
2602
+ input.value = 'abc';
2603
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2604
+ flushSync();
2605
+
2606
+ expect(input.value).toBe('ab');
2607
+ expect(div.textContent).toBe('ab');
2608
+ });
2609
+
2610
+ it('should always prefer what getter returns even if setter mutates next', () => {
2611
+ component App() {
2612
+ let value = track(
2613
+ '',
2614
+ (val) => {
2615
+ return val.replace(/[c,b]+/g, '');
2616
+ },
2617
+ (next) => {
2618
+ if (next.includes('c')) {
2619
+ next = next.replace(/c/g, '');
2620
+ }
2621
+ return next;
2622
+ },
2623
+ );
2624
+
2625
+ <input type="text" {ref bindValue(value)} />
2626
+ <div>{@value}</div>
2627
+ }
2628
+
2629
+ render(App);
2630
+ flushSync();
2631
+
2632
+ const input = container.querySelector('input') as HTMLInputElement;
2633
+ const div = container.querySelector('div') as HTMLDivElement;
2634
+
2635
+ expect(input.value).toBe('');
2636
+ expect(div.textContent).toBe('');
2637
+
2638
+ input.value = 'abc';
2639
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2640
+ flushSync();
2641
+
2642
+ expect(input.value).toBe('a');
2643
+ expect(div.textContent).toBe('a');
2644
+ });
2645
+
2646
+ it(
2647
+ 'should accurately reflect values mutated through an effect even after a setter mutation',
2648
+ () => {
2649
+ component App() {
2650
+ let value = track(
2651
+ '',
2652
+ (val) => {
2653
+ return val;
2654
+ },
2655
+ (next) => {
2656
+ if (next.includes('c')) {
2657
+ next = next.replace(/c/g, '');
2658
+ }
2659
+ return next;
2660
+ },
2661
+ );
2662
+
2663
+ effect(() => {
2664
+ @value;
2665
+
2666
+ untrack(() => {
2667
+ if (@value.includes('a')) {
2668
+ @value = @value.replace(/a/g, '');
2669
+ }
2670
+ });
2671
+ });
2672
+ <input type="text" {ref bindValue(value)} />
2673
+ <div>{@value}</div>
2674
+ }
2675
+
2676
+ render(App);
2677
+ flushSync();
2678
+
2679
+ const input = container.querySelector('input') as HTMLInputElement;
2680
+ const div = container.querySelector('div') as HTMLDivElement;
2681
+
2682
+ expect(input.value).toBe('');
2683
+ expect(div.textContent).toBe('');
2684
+
2685
+ input.value = 'abc';
2686
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2687
+ flushSync();
2688
+
2689
+ expect(input.value).toBe('b');
2690
+ expect(div.textContent).toBe('b');
2691
+ },
2692
+ );
2693
+
2694
+ it('should accurately reflect values mutated through a tracked setter via bind accessors', () => {
2695
+ component App() {
2696
+ let value = track('');
2697
+ const value_accessors = [
2698
+ () => {
2699
+ return @value;
2700
+ },
2701
+ (v: string) => {
2702
+ if (v.includes('c')) {
2703
+ v = v.replace(/c/g, '');
2704
+ }
2705
+ @value = v;
2706
+ },
2707
+ ];
2708
+
2709
+ <input type="text" {ref bindValue(...value_accessors)} />
2710
+ }
2711
+
2712
+ render(App);
2713
+ flushSync();
2714
+
2715
+ const input = container.querySelector('input') as HTMLInputElement;
2716
+
2717
+ expect(input.value).toBe('');
2718
+
2719
+ input.value = 'abc';
2720
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2721
+ flushSync();
2722
+
2723
+ expect(input.value).toBe('ab');
2724
+ });
2725
+
2726
+ it('should prefer what getter returns via bind accessors', () => {
2727
+ component App() {
2728
+ let value = track('');
2729
+ const value_accessors = [
2730
+ () => {
2731
+ if (@value.includes('c')) {
2732
+ return @value.replace(/c/g, '');
2733
+ }
2734
+ return @value;
2735
+ },
2736
+ (v: string) => {
2737
+ @value = v;
2738
+ },
2739
+ ];
2740
+
2741
+ <input type="text" {ref bindValue(...value_accessors)} />
2742
+ }
2743
+
2744
+ render(App);
2745
+ flushSync();
2746
+
2747
+ const input = container.querySelector('input') as HTMLInputElement;
2748
+
2749
+ expect(input.value).toBe('');
2750
+
2751
+ input.value = 'abc';
2752
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2753
+ flushSync();
2754
+
2755
+ expect(input.value).toBe('ab');
2756
+ });
2757
+
2758
+ it(
2759
+ 'should always prefer what getter returns even if setter mutates next via bind accessors',
2760
+ () => {
2761
+ component App() {
2762
+ let value = track('');
2763
+ const value_accessors = [
2764
+ () => {
2765
+ return @value.replace(/[c,b]+/g, '');
2766
+ },
2767
+ (v: string) => {
2768
+ if (v.includes('c')) {
2769
+ v = v.replace(/c/g, '');
2770
+ }
2771
+ @value = v;
2772
+ },
2773
+ ];
2774
+
2775
+ <input type="text" {ref bindValue(...value_accessors)} />
2776
+ }
2777
+
2778
+ render(App);
2779
+ flushSync();
2780
+
2781
+ const input = container.querySelector('input') as HTMLInputElement;
2782
+
2783
+ expect(input.value).toBe('');
2784
+
2785
+ input.value = 'abc';
2786
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2787
+ flushSync();
2788
+
2789
+ expect(input.value).toBe('a');
2790
+ },
2791
+ );
2792
+
2793
+ it(
2794
+ 'should accurately reflect values mutated through an effect even after a setter mutation via bind accessors',
2795
+ () => {
2796
+ component App() {
2797
+ let value = track('');
2798
+ const value_accessors = [
2799
+ () => {
2800
+ return @value;
2801
+ },
2802
+ (v: string) => {
2803
+ if (v.includes('c')) {
2804
+ v = v.replace(/c/g, '');
2805
+ }
2806
+ @value = v;
2807
+ },
2808
+ ];
2809
+
2810
+ effect(() => {
2811
+ @value;
2812
+
2813
+ untrack(() => {
2814
+ if (@value.includes('a')) {
2815
+ @value = @value.replace(/a/g, '');
2816
+ }
2817
+ });
2818
+ });
2819
+ <input type="text" {ref bindValue(...value_accessors)} />
2820
+ }
2821
+
2822
+ render(App);
2823
+ flushSync();
2824
+
2825
+ const input = container.querySelector('input') as HTMLInputElement;
2826
+
2827
+ expect(input.value).toBe('');
2828
+
2829
+ input.value = 'abc';
2830
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2831
+ flushSync();
2832
+
2833
+ expect(input.value).toBe('b');
2834
+ },
2835
+ );
2536
2836
  });
2537
2837
 
2538
2838
  describe('bindFiles', () => {