ripple 0.2.202 → 0.2.204

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.202",
6
+ "version": "0.2.204",
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.202"
100
+ "ripple": "0.2.204"
101
101
  }
102
102
  }
@@ -71,6 +71,10 @@ export interface CodeMapping extends VolarMapping<MappingData> {
71
71
  data: MappingData;
72
72
  }
73
73
 
74
+ export interface CodeMappingWithAll extends CodeMapping {
75
+ generatedLengths: number[];
76
+ }
77
+
74
78
  export interface VolarMappingsResult {
75
79
  code: string;
76
80
  mappings: CodeMapping[];
@@ -54,6 +54,7 @@ import {
54
54
  determine_namespace_for_children,
55
55
  index_to_key,
56
56
  is_element_dynamic,
57
+ is_inside_left_side_assignment,
57
58
  } from '../../../utils.js';
58
59
  import {
59
60
  CSS_HASH_IDENTIFIER,
@@ -413,9 +414,25 @@ const visitors = {
413
414
  is_capitalized: true,
414
415
  },
415
416
  };
416
- return b.member(capitalized_node, b.literal('#v'), true, true);
417
+ const member = b.member(
418
+ capitalized_node,
419
+ b.literal('#v'),
420
+ true,
421
+ !is_inside_left_side_assignment(node),
422
+ /** @type {AST.NodeWithLocation} */ (node),
423
+ );
424
+ member.tracked = true;
425
+ return member;
417
426
  }
418
- return b.member(node, b.literal('#v'), true, true);
427
+ const member = b.member(
428
+ node,
429
+ b.literal('#v'),
430
+ true,
431
+ !is_inside_left_side_assignment(node),
432
+ /** @type {AST.NodeWithLocation} */ (node),
433
+ );
434
+ member.tracked = true;
435
+ return member;
419
436
  }
420
437
  } else {
421
438
  const binding = context.state.scope.get(node.name);
@@ -754,16 +771,15 @@ const visitors = {
754
771
  );
755
772
 
756
773
  // Wrap with ['#v'] access
757
- return b.member(
774
+ const member_expanded = b.member(
758
775
  member,
759
776
  b.literal('#v'),
760
777
  true,
761
- // Always set optional just in case as we don't know if the user's
762
- // ts declaration had it as optional
763
- // It's safe to set it as ts won't report it as such unless the user's ts had it
764
- true,
778
+ !is_inside_left_side_assignment(node),
765
779
  /** @type {AST.NodeWithLocation} */ (node),
766
780
  );
781
+ member_expanded.tracked = true;
782
+ return member_expanded;
767
783
  } else {
768
784
  if (!context.state.to_ts) {
769
785
  return b.call(
@@ -3668,6 +3684,16 @@ function create_tsx_with_typescript_support(comments) {
3668
3684
  context.visit(node.value.body);
3669
3685
  }
3670
3686
  },
3687
+ TSAsExpression(node, context) {
3688
+ if (!node.loc) {
3689
+ base_tsx.TSAsExpression?.(node, context);
3690
+ return;
3691
+ }
3692
+ const loc = /** @type {AST.SourceLocation} */ (node.loc);
3693
+ context.location(loc.start.line, loc.start.column);
3694
+ base_tsx.TSAsExpression?.(node, context);
3695
+ context.location(loc.end.line, loc.end.column);
3696
+ },
3671
3697
  TSObjectKeyword(node, context) {
3672
3698
  if (node.loc) {
3673
3699
  context.location(node.loc.start.line, node.loc.start.column);
@@ -1031,9 +1031,22 @@ export function convert_source_map_to_mappings(
1031
1031
  return;
1032
1032
  } else if (node.type === 'MemberExpression') {
1033
1033
  if (node.loc) {
1034
- mappings.push(
1035
- get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
1034
+ const mapping = get_mapping_from_node(
1035
+ node,
1036
+ src_to_gen_map,
1037
+ gen_line_offsets,
1038
+ mapping_data_verify_only,
1036
1039
  );
1040
+
1041
+ if (node.tracked) {
1042
+ mapping.generatedLengths[0] = mapping.generatedLengths[0] + "['#v']".length;
1043
+ if (node.optional) {
1044
+ mapping.generatedLengths[0] = mapping.generatedLengths[0] + '.?'.length;
1045
+ }
1046
+ mapping.data.customData.generatedLengths = mapping.generatedLengths;
1047
+ }
1048
+
1049
+ mappings.push(mapping);
1037
1050
  }
1038
1051
 
1039
1052
  if (node.object) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js';
3
3
  @import * as AST from 'estree';
4
- @import { CodeMapping } from 'ripple/compiler';
4
+ @import { CodeMappingWithAll } from 'ripple/compiler';
5
5
  @import { CodeMapping as VolarCodeMapping } from '@volar/language-core';
6
6
  @import { RawSourceMap } from 'source-map';
7
7
  */
@@ -284,7 +284,7 @@ export function build_line_offsets(text) {
284
284
  * @param {Partial<VolarCodeMapping['data']>} [filtered_data]
285
285
  * @param {number} [src_max_len]
286
286
  * @param {number} [gen_max_len]
287
- * @returns {CodeMapping | Error}
287
+ * @returns {CodeMappingWithAll | Error}
288
288
  */
289
289
  function maybe_get_mapping_from_node(
290
290
  node,
@@ -333,7 +333,7 @@ function maybe_get_mapping_from_node(
333
333
  * @param {Partial<VolarCodeMapping['data']>} [filtered_data]
334
334
  * @param {number} [src_max_len]
335
335
  * @param {number} [gen_max_len]
336
- * @returns {CodeMapping}
336
+ * @returns {CodeMappingWithAll}
337
337
  */
338
338
  export function get_mapping_from_node(
339
339
  node,
@@ -833,3 +833,79 @@ export function is_inside_try_block(try_parent_stmt, context) {
833
833
 
834
834
  return block_node !== null && try_parent_stmt.block === block_node;
835
835
  }
836
+
837
+ /**
838
+ * Checks if a node is used as the left side of an assignment or update expression.
839
+ * @param {AST.Node} node
840
+ * @returns {boolean}
841
+ */
842
+ export function is_inside_left_side_assignment(node) {
843
+ const path = node.metadata?.path;
844
+ if (!path || path.length === 0) {
845
+ return false;
846
+ }
847
+
848
+ /** @type {AST.Node} */
849
+ let current = node;
850
+
851
+ for (let i = path.length - 1; i >= 0; i--) {
852
+ const parent = path[i];
853
+
854
+ switch (parent.type) {
855
+ case 'AssignmentExpression':
856
+ case 'AssignmentPattern':
857
+ if (parent.right === current) {
858
+ return false;
859
+ }
860
+
861
+ if (parent.left === current) {
862
+ return true;
863
+ }
864
+ current = parent;
865
+ continue;
866
+ case 'UpdateExpression':
867
+ return true;
868
+ case 'MemberExpression':
869
+ // In obj[computeKey()] = 10, computeKey() is evaluated to determine
870
+ // which property to assign to, but is not itself an assignment target
871
+ if (parent.computed && parent.property === current) {
872
+ return false;
873
+ }
874
+ current = parent;
875
+ continue;
876
+ case 'Property':
877
+ // exit here to stop promoting current to parent
878
+ // and thus reaching VariableDeclarator, causing an erroneous truthy result
879
+ // e.g. const { [computeKey()]: value } = obj; where node = computeKey:
880
+ if (parent.key === current) {
881
+ return false;
882
+ }
883
+ current = parent;
884
+ continue;
885
+ case 'VariableDeclarator':
886
+ return parent.id === current;
887
+ case 'ForInStatement':
888
+ case 'ForOfStatement':
889
+ return parent.left === current;
890
+
891
+ case 'Program':
892
+ case 'FunctionDeclaration':
893
+ case 'FunctionExpression':
894
+ case 'ArrowFunctionExpression':
895
+ case 'ClassDeclaration':
896
+ case 'ClassExpression':
897
+ case 'MethodDefinition':
898
+ case 'PropertyDefinition':
899
+ case 'StaticBlock':
900
+ case 'Component':
901
+ case 'Element':
902
+ return false;
903
+
904
+ default:
905
+ current = parent;
906
+ continue;
907
+ }
908
+ }
909
+
910
+ return false;
911
+ }
@@ -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
  /**
@@ -246,26 +246,15 @@ export function bindValue(maybe_tracked, set_func = undefined) {
246
246
  });
247
247
  } else {
248
248
  var input = /** @type {HTMLInputElement} */ (node);
249
+ var selection_restore_needed = false;
249
250
 
250
- clear_event = on(input, 'input', async () => {
251
+ clear_event = on(input, 'input', () => {
252
+ selection_restore_needed = true;
251
253
  /** @type {any} */
252
254
  var value = input.value;
253
255
  value = is_numberlike_input(input) ? to_number(value) : value;
256
+ // the setter will schedule a microtask and the render block below will run
254
257
  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
258
  });
270
259
 
271
260
  render(() => {
@@ -280,7 +269,23 @@ export function bindValue(maybe_tracked, set_func = undefined) {
280
269
  }
281
270
 
282
271
  if (value !== input.value) {
283
- input.value = value ?? '';
272
+ if (selection_restore_needed) {
273
+ var start = input.selectionStart;
274
+ var end = input.selectionEnd;
275
+
276
+ input.value = value ?? '';
277
+
278
+ // Restore selection
279
+ if (end !== null && start !== null) {
280
+ end = Math.min(end, input.value.length);
281
+ start = Math.min(start, end);
282
+ input.selectionStart = start;
283
+ input.selectionEnd = end;
284
+ }
285
+ selection_restore_needed = false;
286
+ } else {
287
+ input.value = value ?? '';
288
+ }
284
289
  }
285
290
  });
286
291
 
@@ -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', () => {
package/types/index.d.ts CHANGED
@@ -112,7 +112,11 @@ export type PropsNoChildren<T extends object = {}> = Expand<T>;
112
112
 
113
113
  type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
114
114
 
115
- type PickKeys<T, K extends readonly (keyof T)[]> = { [I in keyof K]: Tracked<T[K[I] & keyof T]> };
115
+ type WrapTracked<V> = V extends Tracked<any> ? V : Tracked<V>;
116
+
117
+ type PickKeys<T, K extends readonly (keyof T)[]> = {
118
+ [I in keyof K]: WrapTracked<T[K[I] & keyof T]>;
119
+ };
116
120
 
117
121
  type RestKeys<T, K extends readonly (keyof T)[]> = Expand<Omit<T, K[number]>>;
118
122