ripple 0.2.29 → 0.2.32

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.29",
6
+ "version": "0.2.32",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -6,6 +6,7 @@ import {
6
6
  is_event_attribute,
7
7
  is_inside_component,
8
8
  is_ripple_import,
9
+ is_tracked_computed_property,
9
10
  is_tracked_name,
10
11
  } from '../../utils.js';
11
12
  import { extract_paths } from '../../../utils/ast.js';
@@ -101,15 +102,21 @@ const visitors = {
101
102
  MemberExpression(node, context) {
102
103
  const parent = context.path.at(-1);
103
104
 
104
- if (
105
- context.state.metadata?.tracking === false &&
106
- node.property.type === 'Identifier' &&
107
- !node.computed &&
108
- is_tracked_name(node.property.name) &&
109
- parent.type !== 'AssignmentExpression'
110
- ) {
111
- context.state.metadata.tracking = true;
105
+ if (context.state.metadata?.tracking === false && parent.type !== 'AssignmentExpression') {
106
+ if (
107
+ node.property.type === 'Identifier' &&
108
+ !node.computed &&
109
+ is_tracked_name(node.property.name)
110
+ ) {
111
+ context.state.metadata.tracking = true;
112
+ } else if (
113
+ node.computed &&
114
+ is_tracked_computed_property(node.object, node.property, context)
115
+ ) {
116
+ context.state.metadata.tracking = true;
117
+ }
112
118
  }
119
+
113
120
  context.next();
114
121
  },
115
122
 
@@ -212,7 +219,7 @@ const visitors = {
212
219
  if (!computed) {
213
220
  return node;
214
221
  }
215
- return b.call('$.set_property', node, visit(computed), value, b.id('__block'));
222
+ return b.call('$.set_property', node, computed, value, b.id('__block'));
216
223
  },
217
224
  };
218
225
  break;
@@ -19,6 +19,8 @@ import {
19
19
  is_ripple_import,
20
20
  is_declared_within_component,
21
21
  is_inside_call_expression,
22
+ is_tracked_computed_property,
23
+ is_value_static,
22
24
  } from '../../utils.js';
23
25
  import is_reference from 'is-reference';
24
26
  import { extract_paths, object } from '../../../utils/ast.js';
@@ -169,6 +171,10 @@ const visitors = {
169
171
  return context.next();
170
172
  }
171
173
 
174
+ if (is_value_static(node)) {
175
+ return context.next();
176
+ }
177
+
172
178
  return b.call(
173
179
  '$.with_scope',
174
180
  b.id('__block'),
@@ -192,7 +198,10 @@ const visitors = {
192
198
  : property.type === 'Literal' && is_tracked_name(property.value);
193
199
 
194
200
  // TODO should we enforce that the identifier is tracked too?
195
- if ((node.computed && property.type === 'Identifier') || tracked_name) {
201
+ if (
202
+ (node.computed && is_tracked_computed_property(node.object, node.property, context)) ||
203
+ tracked_name
204
+ ) {
196
205
  if (context.state.metadata?.tracking === false) {
197
206
  context.state.metadata.tracking = true;
198
207
  }
@@ -389,6 +398,7 @@ const visitors = {
389
398
 
390
399
  if (is_dom_element) {
391
400
  let class_attribute = null;
401
+ const local_updates = [];
392
402
 
393
403
  state.template.push(`<${node.id.name}`);
394
404
 
@@ -413,7 +423,7 @@ const visitors = {
413
423
  const expression = visit(attr.value, state);
414
424
 
415
425
  if (name === '$value') {
416
- state.update.push(b.stmt(b.call('$.set_value', id, expression)));
426
+ local_updates.push(b.stmt(b.call('$.set_value', id, expression)));
417
427
  } else {
418
428
  state.init.push(b.stmt(b.call('$.set_value', id, expression)));
419
429
  }
@@ -426,7 +436,7 @@ const visitors = {
426
436
  const expression = visit(attr.value, state);
427
437
 
428
438
  if (name === '$checked') {
429
- state.update.push(b.stmt(b.call('$.set_checked', id, expression)));
439
+ local_updates.push(b.stmt(b.call('$.set_checked', id, expression)));
430
440
  } else {
431
441
  state.init.push(b.stmt(b.call('$.set_checked', id, expression)));
432
442
  }
@@ -438,7 +448,7 @@ const visitors = {
438
448
  const expression = visit(attr.value, state);
439
449
 
440
450
  if (name === '$selected') {
441
- state.update.push(b.stmt(b.call('$.set_selected', id, expression)));
451
+ local_updates.push(b.stmt(b.call('$.set_selected', id, expression)));
442
452
  } else {
443
453
  state.init.push(b.stmt(b.call('$.set_selected', id, expression)));
444
454
  }
@@ -514,9 +524,9 @@ const visitors = {
514
524
  const expression = visit(attr.value, state);
515
525
 
516
526
  if (is_dom_property(attribute)) {
517
- state.update.push(b.stmt(b.assignment('=', b.member(id, attribute), expression)));
527
+ local_updates.push(b.stmt(b.assignment('=', b.member(id, attribute), expression)));
518
528
  } else {
519
- state.update.push(
529
+ local_updates.push(
520
530
  b.stmt(b.call('$.set_attribute', id, b.literal(attribute), expression)),
521
531
  );
522
532
  }
@@ -557,7 +567,7 @@ const visitors = {
557
567
  }
558
568
 
559
569
  if (class_attribute.name.name === '$class') {
560
- state.update.push(b.stmt(b.call('$.set_class', id, expression)));
570
+ local_updates.push(b.stmt(b.call('$.set_class', id, expression)));
561
571
  } else {
562
572
  state.init.push(b.stmt(b.call('$.set_class', id, expression)));
563
573
  }
@@ -582,6 +592,8 @@ const visitors = {
582
592
 
583
593
  transform_children(node.children, { visit, state: { ...state, init, update }, root: false });
584
594
 
595
+ update.push(...local_updates);
596
+
585
597
  if (init.length > 0) {
586
598
  state.init.push(b.block(init));
587
599
  }
@@ -861,19 +873,20 @@ const visitors = {
861
873
 
862
874
  const left = node.left;
863
875
 
864
- if (
865
- left.type === 'MemberExpression' &&
866
- left.property.type === 'Identifier' &&
867
- is_tracked_name(left.property.name) &&
868
- left.property.name !== '$length'
869
- ) {
870
- return b.call(
871
- '$.set_property',
872
- context.visit(left.object),
873
- left.computed ? context.visit(left.property) : b.literal(left.property.name),
874
- visit_assignment_expression(node, context, build_assignment) ?? context.next(),
875
- b.id('__block'),
876
- );
876
+ if (left.type === 'MemberExpression') {
877
+ if (left.property.type === 'Identifier' && is_tracked_name(left.property.name)) {
878
+ if (left.property.name !== '$length') {
879
+ return b.call(
880
+ '$.set_property',
881
+ context.visit(left.object),
882
+ left.computed ? context.visit(left.property) : b.literal(left.property.name),
883
+ visit_assignment_expression(node, context, build_assignment) ?? context.next(),
884
+ b.id('__block'),
885
+ );
886
+ }
887
+ } else if (!is_tracked_computed_property(left.object, left.property, context)) {
888
+ return context.next();
889
+ }
877
890
  }
878
891
 
879
892
  const visited = visit_assignment_expression(node, context, build_assignment) ?? context.next();
@@ -900,7 +913,8 @@ const visitors = {
900
913
  if (
901
914
  argument.type === 'MemberExpression' &&
902
915
  ((argument.property.type === 'Identifier' && is_tracked_name(argument.property.name)) ||
903
- argument.computed)
916
+ (argument.computed &&
917
+ is_tracked_computed_property(argument.object, argument.property, context)))
904
918
  ) {
905
919
  return b.call(
906
920
  node.prefix ? '$.update_pre_property' : '$.update_property',
@@ -1570,7 +1584,7 @@ function transform_children(children, { visit, state, root }) {
1570
1584
  state.update.push(b.stmt(b.call('$.set_text', id, expression)));
1571
1585
  } else if (normalized.length === 1) {
1572
1586
  if (expression.type === 'Literal') {
1573
- state.template.push(expression.value);
1587
+ state.template.push(escape_html(expression.value));
1574
1588
  } else {
1575
1589
  const id = state.flush_node();
1576
1590
  state.init.push(
@@ -370,6 +370,47 @@ export function is_tracked_name(name) {
370
370
  return typeof name === 'string' && name.startsWith('$') && name.length > 1 && name[1] !== '$';
371
371
  }
372
372
 
373
+ export function is_value_static(node) {
374
+ if (node.type === 'Literal') {
375
+ return true;
376
+ }
377
+ if (node.type === 'ArrayExpression') {
378
+ return true;
379
+ }
380
+ if (node.type === 'NewExpression') {
381
+ if (node.callee.type === 'Identifier' && node.callee.name === 'Array') {
382
+ return true;
383
+ }
384
+ return false;
385
+ }
386
+
387
+ return false;
388
+ }
389
+
390
+ export function is_tracked_computed_property(object, property, context) {
391
+ const binding = context.state.scope.get(object.name);
392
+
393
+ if (binding) {
394
+ const initial = binding.initial;
395
+ if (initial && is_value_static(initial)) {
396
+ return false;
397
+ }
398
+ }
399
+ if (property.type === 'Identifier') {
400
+ return true;
401
+ }
402
+ if (
403
+ property.type === 'Literal' &&
404
+ typeof property.value === 'string' &&
405
+ is_tracked_name(property.value)
406
+ ) {
407
+ return true;
408
+ }
409
+
410
+ // TODO: do we need to handle more logic here? default to false for now
411
+ return true;
412
+ }
413
+
373
414
  export function is_ripple_import(callee, context) {
374
415
  if (callee.type === 'Identifier') {
375
416
  const binding = context.state.scope.get(callee.name);
@@ -507,7 +548,7 @@ export function build_assignment(operator, left, right, context) {
507
548
  return transform.assign(
508
549
  object,
509
550
  value,
510
- left.type === 'MemberExpression' && left.computed ? left.property : undefined,
551
+ left.type === 'MemberExpression' && left.computed ? context.visit(left.property) : undefined,
511
552
  );
512
553
  }
513
554
 
@@ -529,7 +570,7 @@ export function build_assignment(operator, left, right, context) {
529
570
  const ATTR_REGEX = /[&"<]/g;
530
571
  const CONTENT_REGEX = /[&<]/g;
531
572
 
532
- export function escape_html(value, is_attr) {
573
+ export function escape_html(value, is_attr = false) {
533
574
  const str = String(value ?? '');
534
575
 
535
576
  const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
@@ -1,7 +1,7 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
3
  import { TRACKED_OBJECT } from './internal/client/constants.js';
4
- import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
4
+ import { get, increment, safe_scope, set, tracked } from './internal/client/runtime.js';
5
5
 
6
6
  var symbol_iterator = Symbol.iterator;
7
7
 
@@ -36,17 +36,6 @@ const introspect_methods = [
36
36
  ];
37
37
 
38
38
  let init = false;
39
-
40
- /**
41
- * @param {Block | null} block
42
- * @throws {Error}
43
- */
44
- function check_block(block) {
45
- if (block === null) {
46
- throw new Error('Cannot set $length outside of a reactive context');
47
- }
48
- }
49
-
50
39
  export class RippleArray extends Array {
51
40
  #tracked_elements = [];
52
41
  #tracked_index;
@@ -62,12 +51,11 @@ export class RippleArray extends Array {
62
51
  constructor(...elements) {
63
52
  super(...elements);
64
53
 
65
- var block = scope();
66
- check_block(block);
54
+ var block = safe_scope();
67
55
  var tracked_elements = this.#tracked_elements;
68
56
 
69
57
  for (var i = 0; i < this.length; i++) {
70
- tracked_elements[i] = tracked(0, block);
58
+ tracked_elements[i] = tracked(elements[i], block);
71
59
  }
72
60
  this.#tracked_index = tracked(this.length, block);
73
61
 
@@ -90,20 +78,18 @@ export class RippleArray extends Array {
90
78
  }
91
79
  }
92
80
 
93
- fill() {
94
- var block = scope();
95
- check_block(block);
81
+ fill(val, start, end) {
82
+ var block = safe_scope();
96
83
  var tracked_elements = this.#tracked_elements;
97
84
 
98
- super.fill();
85
+ super.fill(val, start, end);
99
86
  for (var i = 0; i < this.length; i++) {
100
87
  increment(tracked_elements[i], block);
101
88
  }
102
89
  }
103
90
 
104
91
  reverse() {
105
- var block = scope();
106
- check_block(block);
92
+ var block = safe_scope();
107
93
  var tracked_elements = this.#tracked_elements;
108
94
 
109
95
  super.reverse();
@@ -113,8 +99,7 @@ export class RippleArray extends Array {
113
99
  }
114
100
 
115
101
  sort(fn) {
116
- var block = scope();
117
- check_block(block);
102
+ var block = safe_scope();
118
103
  var tracked_elements = this.#tracked_elements;
119
104
 
120
105
  super.sort(fn);
@@ -128,8 +113,7 @@ export class RippleArray extends Array {
128
113
  * @returns {number}
129
114
  */
130
115
  unshift(...elements) {
131
- var block = scope();
132
- check_block(block);
116
+ var block = safe_scope();
133
117
  var tracked_elements = this.#tracked_elements;
134
118
 
135
119
  super.unshift(...elements);
@@ -144,15 +128,14 @@ export class RippleArray extends Array {
144
128
  }
145
129
 
146
130
  shift() {
147
- var block = scope();
148
- check_block(block);
131
+ var block = safe_scope();
149
132
  var tracked_elements = this.#tracked_elements;
150
133
 
151
134
  super.shift();
152
- for (var i = 0; i < tracked_elements.length; i++) {
153
- increment(tracked_elements[i], block);
135
+ for (var i = 0; i < tracked_elements.length - 1; i++) {
136
+ set(tracked_elements[i], tracked_elements[i + 1].v, block);
154
137
  }
155
- tracked_elements.shift();
138
+ tracked_elements.pop();
156
139
 
157
140
  set(this.#tracked_index, this.length, block);
158
141
  }
@@ -162,8 +145,7 @@ export class RippleArray extends Array {
162
145
  * @returns {number}
163
146
  */
164
147
  push(...elements) {
165
- var block = scope();
166
- check_block(block);
148
+ var block = safe_scope();
167
149
  var start_index = this.length;
168
150
  var tracked_elements = this.#tracked_elements;
169
151
 
@@ -178,8 +160,7 @@ export class RippleArray extends Array {
178
160
  }
179
161
 
180
162
  pop() {
181
- var block = scope();
182
- check_block(block);
163
+ var block = safe_scope();
183
164
  var tracked_elements = this.#tracked_elements;
184
165
 
185
166
  super.pop();
@@ -198,8 +179,7 @@ export class RippleArray extends Array {
198
179
  * @returns {any[]}
199
180
  */
200
181
  splice(start, delete_count, ...elements) {
201
- var block = scope();
202
- check_block(block);
182
+ var block = safe_scope();
203
183
  var tracked_elements = this.#tracked_elements;
204
184
 
205
185
  super.splice(start, delete_count, ...elements);
@@ -220,11 +200,10 @@ export class RippleArray extends Array {
220
200
  }
221
201
 
222
202
  set $length(length) {
223
- var block = scope();
224
- check_block(block);
203
+ var block = safe_scope();
225
204
  var tracked_elements = this.#tracked_elements;
226
205
 
227
- if (length !== this.$length) {
206
+ if (length !== this.length) {
228
207
  for (var i = 0; i < tracked_elements.length; i++) {
229
208
  increment(tracked_elements[i], block);
230
209
  }
@@ -260,4 +239,3 @@ export function get_all_elements(array) {
260
239
 
261
240
  return arr;
262
241
  }
263
-
@@ -16,6 +16,7 @@ export { event, delegate } from './events.js';
16
16
  export {
17
17
  active_block,
18
18
  scope,
19
+ safe_scope,
19
20
  with_scope,
20
21
  get_tracked,
21
22
  get_computed,
@@ -1,4 +1,5 @@
1
1
  /** @import { Block, Component, Dependency, Computed, Tracked } from '#client' */
2
+ /** @import { RippleArray } from 'ripple' */
2
3
 
3
4
  import {
4
5
  destroy_block,
@@ -777,9 +778,8 @@ export function flush_sync(fn) {
777
778
  }
778
779
 
779
780
  /**
780
- * @template T
781
- * @param {() => T} fn
782
- * @returns {T & { [SPREAD_OBJECT]: () => T }}
781
+ * @param {() => Object} fn
782
+ * @returns {Object}
783
783
  */
784
784
  export function tracked_spread_object(fn) {
785
785
  var obj = fn();
@@ -815,8 +815,7 @@ export function tracked_object(obj, properties, block) {
815
815
  /** @type {Tracked | Computed} */
816
816
  var tracked_property;
817
817
 
818
- // accessor passed in, to avoid an expensive get_descriptor call
819
- // in the fast path
818
+ // accessor passed in, to avoid an expensive get_descriptor call in the fast path
820
819
  if (property[0] === '#') {
821
820
  property = property.slice(1);
822
821
  var descriptor = /** @type {PropertyDescriptor} */ (get_descriptor(obj, property));
@@ -824,7 +823,10 @@ export function tracked_object(obj, properties, block) {
824
823
  tracked_property = computed(desc_get, block);
825
824
  /** @type {any} */
826
825
  var initial = run_computed(/** @type {Computed} */ (tracked_property));
827
- obj[property] = initial;
826
+ // If there's a setter, we need to set the initial value
827
+ if (descriptor.set !== undefined) {
828
+ obj[property] = initial;
829
+ }
828
830
  } else {
829
831
  var initial = obj[property];
830
832
 
@@ -856,6 +858,12 @@ export function computed_property(fn) {
856
858
  return fn;
857
859
  }
858
860
 
861
+ /**
862
+ * @param {any} obj
863
+ * @param {string | number | symbol} property
864
+ * @param {boolean} [chain=false]
865
+ * @returns {any}
866
+ */
859
867
  export function get_property(obj, property, chain = false) {
860
868
  if (chain && obj == null) {
861
869
  return undefined;
@@ -865,7 +873,10 @@ export function get_property(obj, property, chain = false) {
865
873
  var tracked_property = tracked_properties?.[property];
866
874
 
867
875
  if (tracked_property !== undefined) {
868
- value = obj[property] = get(tracked_property);
876
+ value = get(tracked_property);
877
+ if (obj[property] !== value) {
878
+ obj[property] = value;
879
+ }
869
880
  } else if (SPREAD_OBJECT in obj) {
870
881
  var spread_fn = obj[SPREAD_OBJECT];
871
882
  var properties = spread_fn();
@@ -875,6 +886,13 @@ export function get_property(obj, property, chain = false) {
875
886
  return value;
876
887
  }
877
888
 
889
+ /**
890
+ * @param {any} obj
891
+ * @param {string | number | symbol} property
892
+ * @param {any} value
893
+ * @param {Block} block
894
+ * @returns {any}
895
+ */
878
896
  export function set_property(obj, property, value, block) {
879
897
  var res = (obj[property] = value);
880
898
  var tracked_properties = obj[TRACKED_OBJECT];
@@ -882,9 +900,9 @@ export function set_property(obj, property, value, block) {
882
900
 
883
901
  if (tracked === undefined) {
884
902
  // Handle computed assignments to arrays
885
- if (obj.$length && tracked_properties !== undefined && is_array(obj)) {
903
+ if (is_array(obj) && obj.$length && tracked_properties !== undefined) {
886
904
  with_scope(block, () => {
887
- obj.splice(property, 1, value);
905
+ obj.splice(/** @type {number} */ (property), 1, value);
888
906
  });
889
907
  }
890
908
  return res;
@@ -893,6 +911,12 @@ export function set_property(obj, property, value, block) {
893
911
  set(tracked, value, block);
894
912
  }
895
913
 
914
+ /**
915
+ * @param {Tracked} tracked
916
+ * @param {Block} block
917
+ * @param {number} [d]
918
+ * @returns {number}
919
+ */
896
920
  export function update(tracked, block, d = 1) {
897
921
  var value = get(tracked);
898
922
  var result = d === 1 ? value++ : value--;
@@ -902,66 +926,110 @@ export function update(tracked, block, d = 1) {
902
926
  return result;
903
927
  }
904
928
 
929
+ /**
930
+ * @param {Tracked} tracked
931
+ * @param {Block} block
932
+ * @returns {void}
933
+ */
905
934
  export function increment(tracked, block) {
906
935
  set(tracked, tracked.v + 1, block);
907
936
  }
908
937
 
938
+ /**
939
+ * @param {Tracked} tracked
940
+ * @param {Block} block
941
+ * @returns {void}
942
+ */
909
943
  export function decrement(tracked, block) {
910
944
  set(tracked, tracked.v - 1, block);
911
945
  }
912
946
 
947
+ /**
948
+ * @param {Tracked} tracked
949
+ * @param {Block} block
950
+ * @param {number} [d]
951
+ * @returns {number}
952
+ */
913
953
  export function update_pre(tracked, block, d = 1) {
914
954
  var value = get(tracked);
955
+ var new_value = d === 1 ? ++value : --value;
915
956
 
916
- return set(tracked, d === 1 ? ++value : --value, block);
957
+ set(tracked, new_value, block);
958
+
959
+ return new_value;
917
960
  }
918
961
 
962
+ /**
963
+ * @param {any} obj
964
+ * @param {string | number | symbol} property
965
+ * @param {Block} block
966
+ * @param {number} [d]
967
+ * @returns {number}
968
+ */
919
969
  export function update_property(obj, property, block, d = 1) {
920
970
  var tracked_properties = obj[TRACKED_OBJECT];
921
971
  var tracked = tracked_properties?.[property];
922
-
923
- if (tracked === undefined) {
924
- return d === 1 ? obj[property]++ : obj[property]--;
925
- }
926
-
927
- var value = get(tracked);
928
- var result = d === 1 ? value++ : value--;
972
+ var tracked_exists = tracked !== undefined;
973
+ var value = tracked_exists ? get(tracked) : obj[property];
929
974
 
930
975
  if (d === 1) {
931
- increment(tracked, block);
976
+ value++;
977
+ if (tracked_exists) {
978
+ increment(tracked, block);
979
+ }
932
980
  } else {
933
- decrement(tracked, block);
981
+ value--;
982
+ if (tracked_exists) {
983
+ decrement(tracked, block);
984
+ }
934
985
  }
935
986
 
936
- return result;
987
+ obj[property] = value
988
+
989
+ return value;
937
990
  }
938
991
 
992
+ /**
993
+ * @param {any} obj
994
+ * @param {string | number | symbol} property
995
+ * @param {Block} block
996
+ * @param {number} [d]
997
+ * @returns {number}
998
+ */
939
999
  export function update_pre_property(obj, property, block, d = 1) {
940
1000
  var tracked_properties = obj[TRACKED_OBJECT];
941
1001
  var tracked = tracked_properties?.[property];
942
-
943
- if (tracked === undefined) {
944
- return d === 1 ? ++obj[property] : --obj[property];
945
- }
946
-
947
- var value = get(tracked);
948
- var result = d === 1 ? ++value : --value;
1002
+ var tracked_exists = tracked !== undefined;
1003
+ var value = tracked_exists ? get(tracked) : obj[property];
949
1004
 
950
1005
  if (d === 1) {
951
- increment(tracked, block);
1006
+ ++value
1007
+ if (tracked_exists) {
1008
+ increment(tracked, block);
1009
+ }
952
1010
  } else {
953
- decrement(tracked, block);
1011
+ --value
1012
+ if (tracked_exists) {
1013
+ decrement(tracked, block);
1014
+ }
954
1015
  }
955
1016
 
956
- return result;
1017
+ obj[property] = value;
1018
+
1019
+ return value;
957
1020
  }
958
1021
 
1022
+ /**
1023
+ * @param {any} val
1024
+ * @param {StructuredSerializeOptions} [options]
1025
+ * @returns {any}
1026
+ */
959
1027
  export function structured_clone(val, options) {
960
1028
  if (typeof val === 'object' && val !== null) {
961
1029
  var tracked_properties = val[TRACKED_OBJECT];
962
1030
  if (tracked_properties !== undefined) {
963
1031
  if (is_array(val)) {
964
- val.$length;
1032
+ /** @type {RippleArray<any>} */ (val).$length;
965
1033
  }
966
1034
  return structured_clone(object_values(val), options);
967
1035
  }
@@ -1048,10 +1116,25 @@ export function with_scope(block, fn) {
1048
1116
  }
1049
1117
  }
1050
1118
 
1119
+ /**
1120
+ * @returns {Block | null}
1121
+ */
1051
1122
  export function scope() {
1052
1123
  return active_scope;
1053
1124
  }
1054
1125
 
1126
+ /**
1127
+ * @param {string} [err]
1128
+ * @returns {Block | never}
1129
+ */
1130
+ export function safe_scope(err = 'Cannot access outside of a component context') {
1131
+ if (active_scope === null) {
1132
+ throw new Error(err);
1133
+ }
1134
+
1135
+ return /** @type {Block} */ (active_scope);
1136
+ }
1137
+
1055
1138
  export function push_component() {
1056
1139
  var component = {
1057
1140
  c: null,
@@ -1,4 +1,4 @@
1
- import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
1
+ import { get, increment, safe_scope, set, tracked } from './internal/client/runtime.js';
2
2
 
3
3
  const introspect_methods = ['entries', 'forEach', 'values', Symbol.iterator];
4
4
 
@@ -11,7 +11,7 @@ export class RippleMap extends Map {
11
11
  constructor(iterable) {
12
12
  super();
13
13
 
14
- var block = scope();
14
+ var block = safe_scope();
15
15
 
16
16
  if (iterable) {
17
17
  for (var [key, value] of iterable) {
@@ -75,7 +75,7 @@ export class RippleMap extends Map {
75
75
  }
76
76
 
77
77
  set(key, value) {
78
- var block = scope();
78
+ var block = safe_scope();
79
79
  var tracked_items = this.#tracked_items;
80
80
  var t = tracked_items.get(key);
81
81
  var prev_res = super.get(key);
@@ -93,7 +93,7 @@ export class RippleMap extends Map {
93
93
  }
94
94
 
95
95
  delete(key) {
96
- var block = scope();
96
+ var block = safe_scope();
97
97
  var tracked_items = this.#tracked_items;
98
98
  var t = tracked_items.get(key);
99
99
  var result = super.delete(key);
@@ -108,7 +108,7 @@ export class RippleMap extends Map {
108
108
  }
109
109
 
110
110
  clear() {
111
- var block = scope();
111
+ var block = safe_scope();
112
112
 
113
113
  if (super.size === 0) {
114
114
  return;
@@ -1,4 +1,4 @@
1
- import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
1
+ import { get, increment, safe_scope, set, tracked } from './internal/client/runtime.js';
2
2
 
3
3
  const introspect_methods = ['entries', 'forEach', 'keys', 'values', Symbol.iterator];
4
4
 
@@ -15,7 +15,7 @@ export class RippleSet extends Set {
15
15
  constructor(iterable) {
16
16
  super();
17
17
 
18
- var block = scope();
18
+ var block = safe_scope();
19
19
 
20
20
  if (iterable) {
21
21
  for (var item of iterable) {
@@ -82,7 +82,7 @@ export class RippleSet extends Set {
82
82
  }
83
83
 
84
84
  add(value) {
85
- var block = scope();
85
+ var block = safe_scope();
86
86
 
87
87
  if (!super.has(value)) {
88
88
  super.add(value);
@@ -94,7 +94,7 @@ export class RippleSet extends Set {
94
94
  }
95
95
 
96
96
  delete(value) {
97
- var block = scope();
97
+ var block = safe_scope();
98
98
 
99
99
  if (!super.delete(value)) {
100
100
  return false;
@@ -110,7 +110,7 @@ export class RippleSet extends Set {
110
110
  }
111
111
 
112
112
  has(value) {
113
- var block = scope();
113
+ var block = safe_scope();
114
114
  var has = super.has(value);
115
115
  var tracked_items = this.#tracked_items;
116
116
  var t = tracked_items.get(value);
@@ -129,7 +129,7 @@ export class RippleSet extends Set {
129
129
  }
130
130
 
131
131
  clear() {
132
- var block = scope();
132
+ var block = safe_scope();
133
133
 
134
134
  if (super.size === 0) {
135
135
  return;
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, effect } from 'ripple';
3
+
4
+ describe('prop accessors', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('should render a basic prop accessor on a composite component', () => {
24
+ const logs = [];
25
+
26
+ component Child(props) {
27
+ effect(() => {
28
+ logs.push('App effect', props.$foo);
29
+ });
30
+
31
+ <button onClick={() => props.$foo++}>{"Increment foo"}</button>
32
+ }
33
+
34
+ component App(props) {
35
+ let $foo = 0;
36
+
37
+ <Child $foo:={() => {
38
+ return $foo;
39
+ }, v => {
40
+ $foo = v;
41
+ }} />
42
+
43
+ <div>{"parent foo: " + $foo}</div>
44
+
45
+ <button onClick={() => $foo++}>{"Increment parent foo"}</button>
46
+ }
47
+
48
+ render(App);
49
+ flushSync();
50
+
51
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 0');
52
+ expect(logs).toEqual(['App effect', 0]);
53
+
54
+ const [button, button2] = container.querySelectorAll('button');
55
+
56
+ button.click();
57
+ flushSync();
58
+
59
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 1');
60
+ expect(logs).toEqual(['App effect', 0, 'App effect', 1]);
61
+
62
+ button2.click();
63
+ flushSync();
64
+
65
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 2');
66
+ expect(logs).toEqual(['App effect', 0, 'App effect', 1, 'App effect', 2]);
67
+ });
68
+
69
+ it('should render a basic prop accessor on a composite component #2', () => {
70
+ const logs = [];
71
+
72
+ component Child(props) {
73
+ effect(() => {
74
+ logs.push('App effect', props.$foo);
75
+ });
76
+
77
+ <button onClick={() => props.$foo++}>{"Increment foo"}</button>
78
+ }
79
+
80
+ component App(props) {
81
+ let $foo = 0;
82
+
83
+ <Child $foo:={() => {
84
+ return $foo;
85
+ }, v => {
86
+ // do not update parent
87
+ }} />
88
+
89
+ <div>{"parent foo: " + $foo}</div>
90
+
91
+ <button onClick={() => $foo++}>{"Increment parent foo"}</button>
92
+ }
93
+
94
+ render(App);
95
+ flushSync();
96
+
97
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 0');
98
+ expect(logs).toEqual(['App effect', 0]);
99
+
100
+ const [button, button2] = container.querySelectorAll('button');
101
+
102
+ button.click();
103
+ flushSync();
104
+
105
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 0');
106
+ expect(logs).toEqual(['App effect', 0, 'App effect', 1]);
107
+
108
+ button2.click();
109
+ flushSync();
110
+
111
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 1');
112
+ expect(logs).toEqual(['App effect', 0, 'App effect', 1, 'App effect', 1]);
113
+
114
+ button2.click();
115
+ flushSync();
116
+
117
+ expect(container.querySelectorAll('div')[0].textContent).toBe('parent foo: 2');
118
+ expect(logs).toEqual(['App effect', 0, 'App effect', 1, 'App effect', 1, 'App effect', 2]);
119
+ });
120
+
121
+
122
+ it('handles a simple getter prop accessor with no setter', () =>{
123
+ component Parent() {
124
+ let $value = 123;
125
+
126
+ <Child $value:={() => $value} />
127
+
128
+ <button onClick={() => $value++}>{"Increment value"}</button>
129
+ }
130
+
131
+ component Child({ $value }) {
132
+ <div>{$value}</div>
133
+ }
134
+
135
+ render(Parent);
136
+
137
+ expect(container.querySelector('div').textContent).toBe('123');
138
+
139
+ const button = container.querySelector('button');
140
+ button.click();
141
+ flushSync();
142
+
143
+ expect(container.querySelector('div').textContent).toBe('124');
144
+
145
+ button.click();
146
+ flushSync();
147
+
148
+ expect(container.querySelector('div').textContent).toBe('125');
149
+ });
150
+ });
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
2
  import { mount, flushSync } from 'ripple';
4
3
 
5
4
  describe('basic', () => {
@@ -812,4 +811,25 @@ describe('basic', () => {
812
811
  flushSync();
813
812
  expect(greetingP.textContent).toBe('Hello, User!');
814
813
  });
814
+
815
+ it('renders with reactive attributes with nested reactive attributes', () => {
816
+ component App() {
817
+ let $value = 'parent-class';
818
+
819
+ <p $class={$value}>{'Colored parent value'}</p>
820
+
821
+ <div>
822
+ let $nested = 'nested-class';
823
+
824
+ <p $class={$nested}>{'Colored nested value'}</p>
825
+ </div>
826
+ }
827
+
828
+ render(App);
829
+
830
+ const paragraphs = container.querySelectorAll('p');
831
+
832
+ expect(paragraphs[0].className).toBe('parent-class');
833
+ expect(paragraphs[1].className).toBe('nested-class');
834
+ });
815
835
  });
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
2
  import { mount, flushSync, effect } from 'ripple';
4
3
 
5
4
  describe('passing reactivity between boundaries tests', () => {
@@ -1,6 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
- import { mount } from 'ripple';
2
+ import { mount, RippleArray } from 'ripple';
4
3
 
5
4
  describe('compiler success tests', () => {
6
5
  let container;
@@ -78,5 +77,80 @@ describe('compiler success tests', () => {
78
77
  }
79
78
 
80
79
  render(App);
81
- })
80
+ });
81
+
82
+ it('properly handles JS assignments, reads and updates to array indices', () => {
83
+ const logs = [];
84
+
85
+ component App() {
86
+ let items = [];
87
+ let $items = [];
88
+ let items2 = new Array();
89
+ let items3 = new RippleArray();
90
+ let i = 0;
91
+
92
+ logs.push(items[0]);
93
+ logs.push(items[i]);
94
+ logs.push($items[0]);
95
+ logs.push($items[i]);
96
+ logs.push(items2[0]);
97
+ logs.push(items2[i]);
98
+ logs.push(items3[0]);
99
+ logs.push(items3[i]);
100
+
101
+ items[0] = 123;
102
+ items[i] = 123;
103
+ $items[0] = 123;
104
+ $items[i] = 123;
105
+ items2[0] = 123;
106
+ items2[i] = 123;
107
+ items3[0] = 123;
108
+ items3[i] = 123;
109
+
110
+ logs.push(items[0]);
111
+ logs.push(items[i]);
112
+ logs.push($items[0]);
113
+ logs.push($items[i]);
114
+ logs.push(items2[0]);
115
+ logs.push(items2[i]);
116
+ logs.push(items3[0]);
117
+ logs.push(items3[i]);
118
+
119
+ items[0]++;
120
+ items[i]++;
121
+ $items[0]++;
122
+ $items[i]++;
123
+ items2[0]++;
124
+ items2[i]++;
125
+ items3[0]++;
126
+ items3[i]++;
127
+
128
+ logs.push(items[0]);
129
+ logs.push(items[i]);
130
+ logs.push($items[0]);
131
+ logs.push($items[i]);
132
+ logs.push(items2[0]);
133
+ logs.push(items2[i]);
134
+ logs.push(items3[0]);
135
+ logs.push(items3[i]);
136
+
137
+ logs.push(--items[0]);
138
+ logs.push(--items[i]);
139
+ logs.push(--$items[0]);
140
+ logs.push(--$items[i]);
141
+ logs.push(--items2[0]);
142
+ logs.push(--items2[i]);
143
+ logs.push(--items3[0]);
144
+ logs.push(--items3[i]);
145
+ }
146
+
147
+ render(App);
148
+
149
+ expect(logs).toEqual([
150
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
151
+ 123, 123, 123, 123, 123, 123, 123, 123,
152
+ 125, 125, 125, 125, 125, 125, 125, 125,
153
+ 124, 123, 124, 123, 124, 123, 124, 123
154
+ ]);
155
+ });
82
156
  });
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
2
  import { mount, flushSync } from 'ripple';
4
3
 
5
4
  describe('composite components', () => {
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
2
  import { mount, createContext } from 'ripple';
4
3
 
5
4
  describe('context', () => {
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
2
  import { mount, flushSync, RippleArray } from 'ripple';
4
3
 
5
4
  describe('@use element decorators', () => {
@@ -128,7 +128,14 @@ describe('RippleMap', () => {
128
128
  });
129
129
 
130
130
  it('toJSON returns correct object', () => {
131
- let map = new RippleMap([['foo', 1], ['bar', 2]]);
132
- expect(JSON.stringify(map)).toEqual('[["foo",1],["bar",2]]');
131
+ component MapTest() {
132
+ let map = new RippleMap([['foo', 1], ['bar', 2]]);
133
+
134
+ <pre>{JSON.stringify(map)}</pre>
135
+ }
136
+
137
+ render(MapTest);
138
+
139
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[["foo",1],["bar",2]]');
133
140
  });
134
141
  });