ripple 0.2.27 → 0.2.29

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.27",
6
+ "version": "0.2.29",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -90,6 +90,41 @@ function RipplePlugin(config) {
90
90
 
91
91
  jsx_parseAttribute() {
92
92
  let node = this.startNode();
93
+ const lookahead = this.lookahead();
94
+
95
+ if (lookahead.type?.label === ':') {
96
+ let id = this.startNode();
97
+ id.name = this.value;
98
+ node.name = id;
99
+ this.next();
100
+ this.finishNode(id, 'Identifier');
101
+
102
+ if (this.lookahead().value !== '=') {
103
+ this.unexpected();
104
+ }
105
+ this.next();
106
+ if (this.lookahead().type !== tt.braceL) {
107
+ this.unexpected();
108
+ }
109
+ this.next();
110
+ const value = this.jsx_parseAttributeValue();
111
+ const expression = value.expression;
112
+ node.get = null;
113
+ node.set = null;
114
+
115
+ if (expression.type == 'SequenceExpression') {
116
+ node.get = expression.expressions[0];
117
+ node.set = expression.expressions[1];
118
+ if (expression.expressions.length > 2) {
119
+ this.unexpected();
120
+ }
121
+ } else {
122
+ node.get = expression;
123
+ }
124
+
125
+ return this.finishNode(node, 'AccessorAttribute');
126
+ }
127
+
93
128
  if (this.eat(tt.braceL)) {
94
129
  if (this.type.label === '@') {
95
130
  this.next();
@@ -389,12 +424,22 @@ function RipplePlugin(config) {
389
424
  // This node is used for Prettier, we don't actually need
390
425
  // the node for Ripple's transform process
391
426
  element.children = [component.css];
427
+ // Ensure we escape JSX <tag></tag> context
428
+ const tokContexts = this.acornTypeScript.tokContexts;
429
+ const curContext = this.curContext();
430
+
431
+ if (curContext === tokContexts.tc_expr) {
432
+ this.context.pop();
433
+ }
434
+
392
435
  return element;
393
436
  } else {
437
+ this.enterScope(0);
394
438
  this.parseTemplateBody(element.children);
439
+ this.exitScope();
395
440
  }
441
+ // Ensure we escape JSX <tag></tag> context
396
442
  const tokContexts = this.acornTypeScript.tokContexts;
397
-
398
443
  const curContext = this.curContext();
399
444
 
400
445
  if (curContext === tokContexts.tc_expr) {
@@ -151,6 +151,13 @@ const visitors = {
151
151
  const { state, visit, path } = context;
152
152
 
153
153
  for (const declarator of node.declarations) {
154
+ if (is_inside_component(context) && node.kind === 'var') {
155
+ error(
156
+ 'var declarations are not allowed in components, use let or const instead',
157
+ state.analysis.module.filename,
158
+ declarator,
159
+ );
160
+ }
154
161
  const metadata = { tracking: false, await: false };
155
162
  const parent = path.at(-1);
156
163
  const init_is_untracked =
@@ -321,12 +328,7 @@ const visitors = {
321
328
  binding.transform = {
322
329
  read: (_) => b.call('$.get_computed', path.node),
323
330
  assign: (node, value) => {
324
- return b.call(
325
- '$.set',
326
- path.node,
327
- value,
328
- b.id('__block'),
329
- );
331
+ return b.call('$.set', path.node, value, b.id('__block'));
330
332
  },
331
333
  update: (_) =>
332
334
  b.call(
@@ -451,6 +453,12 @@ const visitors = {
451
453
  }
452
454
  }
453
455
  }
456
+ } else if (attr.type === 'AccessorAttribute') {
457
+ error(
458
+ 'Accessor props are not supported on DOM elements',
459
+ state.analysis.module.filename,
460
+ attr,
461
+ );
454
462
  }
455
463
  }
456
464
  } else {
@@ -459,6 +467,15 @@ const visitors = {
459
467
  if (attr.name.type === 'Identifier') {
460
468
  attribute_names.add(attr.name);
461
469
  }
470
+ } else if (attr.type === 'AccessorAttribute') {
471
+ if (!attr.name.name.startsWith('$')) {
472
+ error(
473
+ 'Accessor props must always be $ prefixed as they are reactive',
474
+ state.analysis.module.filename,
475
+ attr,
476
+ );
477
+ }
478
+ attribute_names.add(attr.name);
462
479
  }
463
480
  }
464
481
 
@@ -472,7 +489,7 @@ const visitors = {
472
489
  if (implicit_children) {
473
490
  error(
474
491
  'Cannot have both implicit and explicit children',
475
- context.state.analysis.module.filename,
492
+ state.analysis.module.filename,
476
493
  node,
477
494
  );
478
495
  }
@@ -482,7 +499,7 @@ const visitors = {
482
499
  if (explicit_children) {
483
500
  error(
484
501
  'Cannot have both implicit and explicit children',
485
- context.state.analysis.module.filename,
502
+ state.analysis.module.filename,
486
503
  node,
487
504
  );
488
505
  }
@@ -133,6 +133,7 @@ const visitors = {
133
133
  }
134
134
 
135
135
  if (
136
+ !is_inside_component(context, true) ||
136
137
  context.state.to_ts ||
137
138
  (parent?.type === 'MemberExpression' && parent.property === node) ||
138
139
  is_inside_call_expression(context) ||
@@ -164,6 +165,10 @@ const visitors = {
164
165
  context.state.metadata.tracking = true;
165
166
  }
166
167
 
168
+ if (!is_inside_component(context, true) || is_inside_call_expression(context)) {
169
+ return context.next();
170
+ }
171
+
167
172
  return b.call(
168
173
  '$.with_scope',
169
174
  b.id('__block'),
@@ -572,7 +577,18 @@ const visitors = {
572
577
  );
573
578
  }
574
579
 
575
- transform_children(node.children, { visit, state, root: false });
580
+ const init = [];
581
+ const update = [];
582
+
583
+ transform_children(node.children, { visit, state: { ...state, init, update }, root: false });
584
+
585
+ if (init.length > 0) {
586
+ state.init.push(b.block(init));
587
+ }
588
+
589
+ if (update.length > 0) {
590
+ state.init.push(b.stmt(b.call('$.render', b.thunk(b.block(update)))));
591
+ }
576
592
 
577
593
  state.template.push(`</${node.id.name}>`);
578
594
  } else {
@@ -617,6 +633,52 @@ const visitors = {
617
633
  );
618
634
  } else if (attr.type === 'UseAttribute') {
619
635
  props.push(b.prop('init', b.call('$.use_prop'), visit(attr.argument, state), true));
636
+ } else if (attr.type === 'AccessorAttribute') {
637
+ // # means it's an accessor to the runtime
638
+ tracked.push(b.literal('#' + attr.name.name));
639
+ let get_expr;
640
+
641
+ if (
642
+ attr.get.type === 'FunctionExpression' ||
643
+ attr.get.type === 'ArrowFunctionExpression'
644
+ ) {
645
+ get_expr = context.state.scope.generate(attr.name.name + '_get');
646
+
647
+ state.init.push(b.const(get_expr, visit(attr.get, state)));
648
+ } else {
649
+ get_expr = visit(attr.get, state);
650
+ }
651
+
652
+ props.push(
653
+ b.prop('get', attr.name, b.function(null, [], b.block([b.return(b.call(get_expr))]))),
654
+ );
655
+
656
+ if (attr.set) {
657
+ let set_expr;
658
+
659
+ if (
660
+ attr.set.type === 'FunctionExpression' ||
661
+ attr.set.type === 'ArrowFunctionExpression'
662
+ ) {
663
+ set_expr = context.state.scope.generate(attr.name.name + '_set');
664
+
665
+ state.init.push(b.const(set_expr, visit(attr.set, state)));
666
+ } else {
667
+ set_expr = visit(attr.set, state);
668
+ }
669
+
670
+ props.push(
671
+ b.prop(
672
+ 'set',
673
+ attr.name,
674
+ b.function(
675
+ null,
676
+ [b.id('__value')],
677
+ b.block([b.return(b.call(set_expr, b.id('__value')))]),
678
+ ),
679
+ ),
680
+ );
681
+ }
620
682
  } else {
621
683
  throw new Error('TODO');
622
684
  }
@@ -802,7 +864,8 @@ const visitors = {
802
864
  if (
803
865
  left.type === 'MemberExpression' &&
804
866
  left.property.type === 'Identifier' &&
805
- is_tracked_name(left.property.name)
867
+ is_tracked_name(left.property.name) &&
868
+ left.property.name !== '$length'
806
869
  ) {
807
870
  return b.call(
808
871
  '$.set_property',
@@ -1211,7 +1274,7 @@ function transform_ts_child(node, context) {
1211
1274
  .filter((attr) => {
1212
1275
  if (attr.type === 'UseAttribute') {
1213
1276
  use_attributes.push(attr);
1214
- return false; // Filter out from JSX attributes
1277
+ return false;
1215
1278
  }
1216
1279
  return true;
1217
1280
  })
@@ -1368,7 +1431,12 @@ function transform_ts_child(node, context) {
1368
1431
  ),
1369
1432
  ),
1370
1433
  );
1434
+ } else if (node.type === 'Component') {
1435
+ const component = visit(node, context.state);
1436
+
1437
+ state.init.push(component);
1371
1438
  } else {
1439
+ debugger;
1372
1440
  throw new Error('TODO');
1373
1441
  }
1374
1442
  }
@@ -327,15 +327,16 @@ export function build_hoisted_params(node, context) {
327
327
  return params;
328
328
  }
329
329
 
330
- export function is_inside_component(context) {
330
+ export function is_inside_component(context, includes_functions = false) {
331
331
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
332
332
  const context_node = context.path[i];
333
333
  const type = context_node.type;
334
334
 
335
335
  if (
336
- type === 'FunctionExpression' ||
337
- type === 'ArrowFunctionExpression' ||
338
- type === 'FunctionDeclaration'
336
+ !includes_functions &&
337
+ (type === 'FunctionExpression' ||
338
+ type === 'ArrowFunctionExpression' ||
339
+ type === 'FunctionDeclaration')
339
340
  ) {
340
341
  return false;
341
342
  }
@@ -1,3 +1,5 @@
1
+ /** @import { Block } from '#client' */
2
+
1
3
  import { TRACKED_OBJECT } from './internal/client/constants.js';
2
4
  import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
3
5
 
@@ -35,6 +37,16 @@ const introspect_methods = [
35
37
 
36
38
  let init = false;
37
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
+
38
50
  export class RippleArray extends Array {
39
51
  #tracked_elements = [];
40
52
  #tracked_index;
@@ -51,6 +63,7 @@ export class RippleArray extends Array {
51
63
  super(...elements);
52
64
 
53
65
  var block = scope();
66
+ check_block(block);
54
67
  var tracked_elements = this.#tracked_elements;
55
68
 
56
69
  for (var i = 0; i < this.length; i++) {
@@ -79,6 +92,7 @@ export class RippleArray extends Array {
79
92
 
80
93
  fill() {
81
94
  var block = scope();
95
+ check_block(block);
82
96
  var tracked_elements = this.#tracked_elements;
83
97
 
84
98
  super.fill();
@@ -89,6 +103,7 @@ export class RippleArray extends Array {
89
103
 
90
104
  reverse() {
91
105
  var block = scope();
106
+ check_block(block);
92
107
  var tracked_elements = this.#tracked_elements;
93
108
 
94
109
  super.reverse();
@@ -99,6 +114,7 @@ export class RippleArray extends Array {
99
114
 
100
115
  sort(fn) {
101
116
  var block = scope();
117
+ check_block(block);
102
118
  var tracked_elements = this.#tracked_elements;
103
119
 
104
120
  super.sort(fn);
@@ -107,8 +123,13 @@ export class RippleArray extends Array {
107
123
  }
108
124
  }
109
125
 
126
+ /**
127
+ * @param {...any} elements
128
+ * @returns {number}
129
+ */
110
130
  unshift(...elements) {
111
131
  var block = scope();
132
+ check_block(block);
112
133
  var tracked_elements = this.#tracked_elements;
113
134
 
114
135
  super.unshift(...elements);
@@ -117,11 +138,14 @@ export class RippleArray extends Array {
117
138
  }
118
139
  tracked_elements.unshift(...elements.map(() => tracked(0, block)));
119
140
 
120
- set(this.#tracked_index, this.length, block);
141
+ var length = this.length;
142
+ set(this.#tracked_index, length, block);
143
+ return length;
121
144
  }
122
145
 
123
146
  shift() {
124
147
  var block = scope();
148
+ check_block(block);
125
149
  var tracked_elements = this.#tracked_elements;
126
150
 
127
151
  super.shift();
@@ -133,8 +157,13 @@ export class RippleArray extends Array {
133
157
  set(this.#tracked_index, this.length, block);
134
158
  }
135
159
 
160
+ /**
161
+ * @param {...any} elements
162
+ * @returns {number}
163
+ */
136
164
  push(...elements) {
137
165
  var block = scope();
166
+ check_block(block);
138
167
  var start_index = this.length;
139
168
  var tracked_elements = this.#tracked_elements;
140
169
 
@@ -143,11 +172,14 @@ export class RippleArray extends Array {
143
172
  for (var i = 0; i < elements.length; i++) {
144
173
  tracked_elements[start_index + i] = tracked(0, block);
145
174
  }
175
+ var length = this.length;
146
176
  set(this.#tracked_index, this.length, block);
177
+ return length;
147
178
  }
148
179
 
149
180
  pop() {
150
181
  var block = scope();
182
+ check_block(block);
151
183
  var tracked_elements = this.#tracked_elements;
152
184
 
153
185
  super.pop();
@@ -159,8 +191,15 @@ export class RippleArray extends Array {
159
191
  set(this.#tracked_index, this.length, block);
160
192
  }
161
193
 
194
+ /**
195
+ * @param {number} start
196
+ * @param {number} [delete_count]
197
+ * @param {...any} elements
198
+ * @returns {any[]}
199
+ */
162
200
  splice(start, delete_count, ...elements) {
163
201
  var block = scope();
202
+ check_block(block);
164
203
  var tracked_elements = this.#tracked_elements;
165
204
 
166
205
  super.splice(start, delete_count, ...elements);
@@ -182,6 +221,7 @@ export class RippleArray extends Array {
182
221
 
183
222
  set $length(length) {
184
223
  var block = scope();
224
+ check_block(block);
185
225
  var tracked_elements = this.#tracked_elements;
186
226
 
187
227
  if (length !== this.$length) {
@@ -189,13 +229,12 @@ export class RippleArray extends Array {
189
229
  increment(tracked_elements[i], block);
190
230
  }
191
231
  this.length = length;
232
+ set(this.#tracked_index, length, block);
192
233
  tracked_elements.length = length;
193
-
194
- return true;
195
234
  }
196
- return false;
197
235
  }
198
236
 
237
+ /** @param {number} _ */
199
238
  set length(_) {
200
239
  throw new Error('Cannot set length on RippleArray, use $length instead');
201
240
  }
@@ -206,6 +245,10 @@ export class RippleArray extends Array {
206
245
  }
207
246
  }
208
247
 
248
+ /**
249
+ * @param {RippleArray} array
250
+ * @returns {any[]}
251
+ */
209
252
  export function get_all_elements(array) {
210
253
  var tracked_elements = array[TRACKED_OBJECT];
211
254
  var arr = [];
@@ -42,6 +42,8 @@ export { RippleArray } from './array.js';
42
42
 
43
43
  export { RippleSet } from './set.js';
44
44
 
45
+ export { RippleMap } from './map.js';
46
+
45
47
  export { keyed } from './internal/client/for.js';
46
48
 
47
49
  export { user_effect as effect } from './internal/client/blocks.js';
@@ -18,6 +18,7 @@ import {
18
18
  active_block,
19
19
  active_component,
20
20
  active_reaction,
21
+ is_block_dirty,
21
22
  run_block,
22
23
  run_teardown,
23
24
  schedule_update,
@@ -93,21 +94,28 @@ export function async(fn) {
93
94
  });
94
95
  }
95
96
 
97
+ /**
98
+ * @param {Element} element
99
+ * @param {() => (element: Element) => (void | (() => void))} get_fn
100
+ * @returns {Block}
101
+ */
96
102
  export function use(element, get_fn) {
97
- var use_obj = undefined;
103
+ /** @type {(element: Element) => (void | (() => void) | undefined)} */
104
+ var use_fn;
105
+ /** @type {Block | null} */
98
106
  var e;
99
107
 
100
108
  return block(RENDER_BLOCK, () => {
101
- if (use_obj !== (use_obj = get_fn())) {
109
+ if (use_fn !== (use_fn = get_fn())) {
102
110
  if (e) {
103
111
  destroy_block(e);
104
112
  e = null;
105
113
  }
106
114
 
107
- if (use_obj) {
115
+ if (use_fn) {
108
116
  e = branch(() => {
109
117
  effect(() => {
110
- return use_obj(element);
118
+ return use_fn(element);
111
119
  });
112
120
  });
113
121
  }
@@ -115,14 +123,27 @@ export function use(element, get_fn) {
115
123
  });
116
124
  }
117
125
 
126
+ /**
127
+ * @param {() => void} fn
128
+ * @returns {Block}
129
+ */
118
130
  export function root(fn) {
119
131
  return block(ROOT_BLOCK, fn);
120
132
  }
121
133
 
134
+ /**
135
+ * @param {() => void} fn
136
+ * @param {any} state
137
+ * @returns {Block}
138
+ */
122
139
  export function create_try_block(fn, state) {
123
140
  return block(TRY_BLOCK, fn, state);
124
141
  }
125
142
 
143
+ /**
144
+ * @param {Block} block
145
+ * @param {Block} parent_block
146
+ */
126
147
  function push_block(block, parent_block) {
127
148
  var parent_last = parent_block.last;
128
149
  if (parent_last === null) {
@@ -174,6 +195,10 @@ export function block(flags, fn, state = null) {
174
195
  return block;
175
196
  }
176
197
 
198
+ /**
199
+ * @param {Block} parent
200
+ * @param {boolean} [remove_dom]
201
+ */
177
202
  export function destroy_block_children(parent, remove_dom = false) {
178
203
  var block = parent.first;
179
204
  parent.first = parent.last = null;
@@ -187,6 +212,10 @@ export function destroy_block_children(parent, remove_dom = false) {
187
212
  }
188
213
  }
189
214
 
215
+ /**
216
+ * @param {Block} parent
217
+ * @param {boolean} [remove_dom]
218
+ */
190
219
  export function destroy_non_branch_children(parent, remove_dom = false) {
191
220
  var block = parent.first;
192
221
 
@@ -207,6 +236,9 @@ export function destroy_non_branch_children(parent, remove_dom = false) {
207
236
  }
208
237
  }
209
238
 
239
+ /**
240
+ * @param {Block} block
241
+ */
210
242
  export function unlink_block(block) {
211
243
  var parent = block.p;
212
244
  var prev = block.prev;
@@ -221,6 +253,9 @@ export function unlink_block(block) {
221
253
  }
222
254
  }
223
255
 
256
+ /**
257
+ * @param {Block} block
258
+ */
224
259
  export function pause_block(block) {
225
260
  if ((block.f & PAUSED) !== 0) {
226
261
  return;
@@ -238,6 +273,9 @@ export function pause_block(block) {
238
273
  run_teardown(block);
239
274
  }
240
275
 
276
+ /**
277
+ * @param {Block} block
278
+ */
241
279
  export function resume_block(block) {
242
280
  if ((block.f & PAUSED) === 0) {
243
281
  return;
@@ -257,7 +295,12 @@ export function resume_block(block) {
257
295
  }
258
296
  }
259
297
 
298
+ /**
299
+ * @param {Block} target_block
300
+ * @returns {boolean}
301
+ */
260
302
  export function is_destroyed(target_block) {
303
+ /** @type {Block | null} */
261
304
  var block = target_block;
262
305
 
263
306
  while (block !== null) {
@@ -274,6 +317,10 @@ export function is_destroyed(target_block) {
274
317
  return true;
275
318
  }
276
319
 
320
+ /**
321
+ * @param {Block} block
322
+ * @param {boolean} [remove_dom]
323
+ */
277
324
  export function destroy_block(block, remove_dom = true) {
278
325
  block.f ^= DESTROYED;
279
326
 
@@ -28,7 +28,7 @@ import {
28
28
  USE_PROP,
29
29
  } from './constants';
30
30
  import { capture, suspend } from './try.js';
31
- import { define_property, is_array } from './utils';
31
+ import { define_property, get_descriptor, is_array } from './utils';
32
32
  import {
33
33
  object_keys as original_object_keys,
34
34
  object_values as original_object_values,
@@ -192,6 +192,7 @@ function run_computed(computed) {
192
192
  * @param {Block} block
193
193
  */
194
194
  export function handle_error(error, block) {
195
+ /** @type {Block | null} */
195
196
  var current = block;
196
197
 
197
198
  while (current !== null) {
@@ -230,6 +231,7 @@ export function run_block(block) {
230
231
 
231
232
  if (typeof res === 'function') {
232
233
  block.t = res;
234
+ /** @type {Block | null} */
233
235
  let current = block;
234
236
 
235
237
  while (current !== null && (current.f & CONTAINS_TEARDOWN) === 0) {
@@ -332,7 +334,7 @@ function is_tracking_dirty(tracking) {
332
334
  /**
333
335
  * @param {Block} block
334
336
  */
335
- function is_block_dirty(block) {
337
+ export function is_block_dirty(block) {
336
338
  var flags = block.f;
337
339
 
338
340
  if ((flags & (ROOT_BLOCK | BRANCH_BLOCK)) !== 0) {
@@ -407,6 +409,7 @@ export function deferred(fn) {
407
409
  var parent = active_block;
408
410
  var block = active_scope;
409
411
  var res = [UNINITIALIZED];
412
+ // TODO implement DEFERRED flag on tracked
410
413
  var t = tracked(UNINITIALIZED, block, DEFERRED);
411
414
  var tracked_properties = [t];
412
415
  var prev_value = UNINITIALIZED;
@@ -535,7 +538,7 @@ function flush_updates(root_block) {
535
538
  }
536
539
  }
537
540
 
538
- /** @type {Block} */
541
+ /** @type {Block | null} */
539
542
  var parent = current.p;
540
543
  current = current.next;
541
544
 
@@ -626,7 +629,7 @@ export function schedule_update(block) {
626
629
  if ((flags & ROOT_BLOCK) !== 0) {
627
630
  break;
628
631
  }
629
- current = current.p;
632
+ current = /** @type {Block} */ (current.p);
630
633
  }
631
634
 
632
635
  queued_root_blocks.push(current);
@@ -773,6 +776,11 @@ export function flush_sync(fn) {
773
776
  }
774
777
  }
775
778
 
779
+ /**
780
+ * @template T
781
+ * @param {() => T} fn
782
+ * @returns {T & { [SPREAD_OBJECT]: () => T }}
783
+ */
776
784
  export function tracked_spread_object(fn) {
777
785
  var obj = fn();
778
786
 
@@ -784,7 +792,14 @@ export function tracked_spread_object(fn) {
784
792
  return obj;
785
793
  }
786
794
 
795
+ /**
796
+ * @param {any} obj
797
+ * @param {string[]} properties
798
+ * @param {Block} block
799
+ * @returns {object}
800
+ */
787
801
  export function tracked_object(obj, properties, block) {
802
+ /** @type {Record<string, Tracked | Computed>} */
788
803
  var tracked_properties = obj[TRACKED_OBJECT];
789
804
 
790
805
  if (tracked_properties === undefined) {
@@ -797,27 +812,42 @@ export function tracked_object(obj, properties, block) {
797
812
 
798
813
  for (var i = 0; i < properties.length; i++) {
799
814
  var property = properties[i];
800
- var initial = obj[property];
815
+ /** @type {Tracked | Computed} */
801
816
  var tracked_property;
802
817
 
803
- if (typeof initial === 'function' && initial[COMPUTED_PROPERTY] === true) {
804
- tracked_property = computed(initial, block);
805
- initial = run_computed(tracked_property);
818
+ // accessor passed in, to avoid an expensive get_descriptor call
819
+ // in the fast path
820
+ if (property[0] === '#') {
821
+ property = property.slice(1);
822
+ var descriptor = /** @type {PropertyDescriptor} */ (get_descriptor(obj, property));
823
+ var desc_get = descriptor.get;
824
+ tracked_property = computed(desc_get, block);
825
+ /** @type {any} */
826
+ var initial = run_computed(/** @type {Computed} */ (tracked_property));
806
827
  obj[property] = initial;
807
- // TODO If nothing is tracked in the computed function, we can make it a standard tracked
808
- // however this is more allocations, so we probably want to minimize this
809
- // if (tracked_property.d === null) {
810
- // tracked_property = tracked(initial, block);
811
- // }
812
828
  } else {
813
- tracked_property = tracked(initial, block);
829
+ var initial = obj[property];
830
+
831
+ if (typeof initial === 'function' && initial[COMPUTED_PROPERTY] === true) {
832
+ tracked_property = computed(initial, block);
833
+ initial = run_computed(/** @type {Computed} */ (tracked_property));
834
+ obj[property] = initial;
835
+ } else {
836
+ tracked_property = tracked(initial, block);
837
+ }
814
838
  }
839
+
815
840
  tracked_properties[property] = tracked_property;
816
841
  }
817
842
 
818
843
  return obj;
819
844
  }
820
845
 
846
+ /**
847
+ * @template T
848
+ * @param {() => T} fn
849
+ * @returns {() => T}
850
+ */
821
851
  export function computed_property(fn) {
822
852
  define_property(fn, COMPUTED_PROPERTY, {
823
853
  value: true,
@@ -1002,6 +1032,12 @@ export function spread_object(obj) {
1002
1032
  return values;
1003
1033
  }
1004
1034
 
1035
+ /**
1036
+ * @template T
1037
+ * @param {Block} block
1038
+ * @param {() => T} fn
1039
+ * @returns {T}
1040
+ */
1005
1041
  export function with_scope(block, fn) {
1006
1042
  var previous_scope = active_scope;
1007
1043
  try {
@@ -1054,6 +1090,12 @@ export function use_prop() {
1054
1090
  return Symbol(USE_PROP);
1055
1091
  }
1056
1092
 
1093
+ /**
1094
+ * @template T
1095
+ * @param {T | undefined} value
1096
+ * @param {T} fallback
1097
+ * @returns {T}
1098
+ */
1057
1099
  export function fallback(value, fallback) {
1058
1100
  return value === undefined ? fallback : value;
1059
- }
1101
+ }
@@ -5,7 +5,7 @@ export type Component = {
5
5
  e: null | Array<{
6
6
  b: Block;
7
7
  fn: Function;
8
- r: null | Block;
8
+ r: null | Block | Computed;
9
9
  }>;
10
10
  p: null | Component;
11
11
  m: boolean;
@@ -40,10 +40,11 @@ export type Block = {
40
40
  first: null | Block;
41
41
  f: number;
42
42
  fn: any;
43
- last: null;
44
- next: null;
43
+ last: null | Block;
44
+ next: null | Block;
45
45
  p: null | Block;
46
- prev: null;
46
+ prev: null | Block;
47
47
  s: any;
48
- t: Tracked | null;
48
+ // teardown function
49
+ t: (() => {}) | null;
49
50
  };
@@ -0,0 +1,147 @@
1
+ import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
2
+
3
+ const introspect_methods = ['entries', 'forEach', 'values', Symbol.iterator];
4
+
5
+ let init = false;
6
+
7
+ export class RippleMap extends Map {
8
+ #tracked_size;
9
+ #tracked_items = new Map();
10
+
11
+ constructor(iterable) {
12
+ super();
13
+
14
+ var block = scope();
15
+
16
+ if (iterable) {
17
+ for (var [key, value] of iterable) {
18
+ super.set(key, value);
19
+ this.#tracked_items.set(key, tracked(0, block));
20
+ }
21
+ }
22
+
23
+ this.#tracked_size = tracked(this.size, block);
24
+
25
+ if (!init) {
26
+ init = true;
27
+ this.#init();
28
+ }
29
+ }
30
+
31
+ #init() {
32
+ var proto = RippleMap.prototype;
33
+ var map_proto = Map.prototype;
34
+
35
+ for (const method of introspect_methods) {
36
+ proto[method] = function (...v) {
37
+ this.$size;
38
+ this.#read_all();
39
+
40
+ return map_proto[method].apply(this, v);
41
+ };
42
+ }
43
+ }
44
+
45
+ get(key) {
46
+ var tracked_items = this.#tracked_items;
47
+ var t = tracked_items.get(key);
48
+
49
+ if (t === undefined) {
50
+ // same logic as has
51
+ this.$size;
52
+ } else {
53
+ get(t);
54
+ }
55
+
56
+ return super.get(key);
57
+ }
58
+
59
+ has(key) {
60
+ var has = super.has(key);
61
+ var tracked_items = this.#tracked_items;
62
+ var t = tracked_items.get(key);
63
+
64
+ if (t === undefined) {
65
+ // if no tracked it also means super didn't have it
66
+ // It's not possible to have a disconnect, we tract each key
67
+ // If the key doesn't exist, track the size in case it's added later
68
+ // but don't create tracked entries willy-nilly to track all possible keys
69
+ this.$size;
70
+ } else {
71
+ get(t);
72
+ }
73
+
74
+ return has;
75
+ }
76
+
77
+ set(key, value) {
78
+ var block = scope();
79
+ var tracked_items = this.#tracked_items;
80
+ var t = tracked_items.get(key);
81
+ var prev_res = super.get(key);
82
+
83
+ super.set(key, value);
84
+
85
+ if (!t) {
86
+ tracked_items.set(key, tracked(0, block));
87
+ set(this.#tracked_size, this.size, block);
88
+ } else if (prev_res !== value) {
89
+ increment(t, block);
90
+ }
91
+
92
+ return this;
93
+ }
94
+
95
+ delete(key) {
96
+ var block = scope();
97
+ var tracked_items = this.#tracked_items;
98
+ var t = tracked_items.get(key);
99
+ var result = super.delete(key);
100
+
101
+ if (t) {
102
+ increment(t, block);
103
+ tracked_items.delete(key);
104
+ set(this.#tracked_size, this.size, block);
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ clear() {
111
+ var block = scope();
112
+
113
+ if (super.size === 0) {
114
+ return;
115
+ }
116
+
117
+ for (var [_, t] of this.#tracked_items) {
118
+ increment(t, block);
119
+ }
120
+
121
+ super.clear();
122
+ this.#tracked_items.clear();
123
+ set(this.#tracked_size, 0, block);
124
+ }
125
+
126
+ keys() {
127
+ this.$size;
128
+ return super.keys();
129
+ }
130
+
131
+ #read_all() {
132
+ for (const [, t] of this.#tracked_items) {
133
+ get(t);
134
+ }
135
+ }
136
+
137
+ get $size() {
138
+ return get(this.#tracked_size);
139
+ }
140
+
141
+ toJSON() {
142
+ this.$size;
143
+ this.#read_all();
144
+
145
+ return [...this];
146
+ }
147
+ }
@@ -1,25 +1,10 @@
1
1
  import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
2
2
 
3
- const introspect_methods = [
4
- 'entries',
5
- 'forEach',
6
- 'keys',
7
- 'values',
8
- Symbol.iterator
9
- ];
10
-
11
- const compare_other_methods = [
12
- 'isDisjointFrom',
13
- 'isSubsetOf',
14
- 'isSupersetOf',
15
- ];
16
-
17
- const new_other_methods = [
18
- 'difference',
19
- 'intersection',
20
- 'symmetricDifference',
21
- 'union',
22
- ];
3
+ const introspect_methods = ['entries', 'forEach', 'keys', 'values', Symbol.iterator];
4
+
5
+ const compare_other_methods = ['isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
6
+
7
+ const new_other_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
23
8
 
24
9
  let init = false;
25
10
 
@@ -91,9 +76,7 @@ export class RippleSet extends Set {
91
76
  other.$size;
92
77
  }
93
78
 
94
- return new RippleSet(
95
- set_proto[method].apply(this, [other, ...v])
96
- );
79
+ return new RippleSet(set_proto[method].apply(this, [other, ...v]));
97
80
  };
98
81
  }
99
82
  }
@@ -113,21 +96,17 @@ export class RippleSet extends Set {
113
96
  delete(value) {
114
97
  var block = scope();
115
98
 
116
- if (super.has(value)) {
117
- super.delete(value);
118
- var t = this.#tracked_items.get(value);
119
-
120
- if (t) {
121
- increment(t, block);
122
- }
99
+ if (!super.delete(value)) {
100
+ return false;
101
+ }
123
102
 
124
- this.#tracked_items.delete(value);
125
- set(this.#tracked_size, this.size, block);
103
+ var t = this.#tracked_items.get(value);
126
104
 
127
- return true;
128
- }
105
+ increment(t, block);
106
+ this.#tracked_items.delete(value);
107
+ set(this.#tracked_size, this.size, block);
129
108
 
130
- return false;
109
+ return true;
131
110
  }
132
111
 
133
112
  has(value) {
@@ -137,34 +116,32 @@ export class RippleSet extends Set {
137
116
  var t = tracked_items.get(value);
138
117
 
139
118
  if (t === undefined) {
140
- if (!has) {
141
- // If the value doesn't exist, track the size in case it's added later
142
- // but don't create tracked entries willy-nilly to track all possible values
143
- this.$size;
144
-
145
- return false;
146
- }
147
-
148
- t = tracked(0, block);
149
- tracked_items.set(value, t);
119
+ // if no tracked it also means super didn't have it
120
+ // It's not possible to have a disconnect, we track each value
121
+ // If the value doesn't exist, track the size in case it's added later
122
+ // but don't create tracked entries willy-nilly to track all possible values
123
+ this.$size;
124
+ } else {
125
+ get(t);
150
126
  }
151
127
 
152
- get(t);
153
128
  return has;
154
129
  }
155
130
 
156
131
  clear() {
157
132
  var block = scope();
158
133
 
159
- if (this.size > 0) {
160
- for (var [value, t] of this.#tracked_items) {
161
- increment(t, block);
162
- }
134
+ if (super.size === 0) {
135
+ return;
136
+ }
163
137
 
164
- super.clear();
165
- this.#tracked_items.clear();
166
- set(this.#tracked_size, 0, block);
138
+ for (var [_, t] of this.#tracked_items) {
139
+ increment(t, block);
167
140
  }
141
+
142
+ super.clear();
143
+ this.#tracked_items.clear();
144
+ set(this.#tracked_size, 0, block);
168
145
  }
169
146
 
170
147
  get $size() {
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+
3
+ import { mount, flushSync, effect } from 'ripple';
4
+
5
+ describe('passing reactivity between boundaries tests', () => {
6
+ let container;
7
+
8
+ function render(component) {
9
+ mount(component, {
10
+ target: container
11
+ });
12
+ }
13
+
14
+ beforeEach(() => {
15
+ container = document.createElement('div');
16
+ document.body.appendChild(container);
17
+ });
18
+
19
+ afterEach(() => {
20
+ document.body.removeChild(container);
21
+ container = null;
22
+ });
23
+
24
+ it('can pass reactivity between functions with simple arrays and destructuring', () => {
25
+ let log: string[] = [];
26
+
27
+ function createDouble([ $count ]) {
28
+ const $double = $count * 2;
29
+
30
+ effect(() => {
31
+ log.push('Count:' + $count);
32
+ });
33
+
34
+ return [ $double ];
35
+ }
36
+
37
+ component App() {
38
+ let $count = 0;
39
+
40
+ const [ $double ] = createDouble([ $count ]);
41
+
42
+ <div>{'Double: ' + $double}</div>
43
+ <button onClick={() => { $count++; }}>{'Increment'}</button>
44
+ }
45
+
46
+ render(App);
47
+ flushSync();
48
+
49
+ expect(container.querySelector('div').textContent).toBe('Double: 0');
50
+ expect(log).toEqual(['Count:0']);
51
+
52
+ const button = container.querySelector('button');
53
+
54
+ button.click();
55
+ flushSync();
56
+
57
+ expect(container.querySelector('div').textContent).toBe('Double: 2');
58
+ expect(log).toEqual(['Count:0', 'Count:1']);
59
+
60
+ button.click();
61
+ flushSync();
62
+
63
+ expect(container.querySelector('div').textContent).toBe('Double: 4');
64
+ expect(log).toEqual(['Count:0', 'Count:1', 'Count:2']);
65
+ });
66
+
67
+ it('can pass reactivity between functions with simple objects and destructuring', () => {
68
+ let log: string[] = [];
69
+
70
+ function createDouble({ $count }) {
71
+ const $double = $count * 2;
72
+
73
+ effect(() => {
74
+ log.push('Count:' + $count);
75
+ });
76
+
77
+ return { $double };
78
+ }
79
+
80
+ component App() {
81
+ let $count = 0;
82
+
83
+ const { $double } = createDouble({ $count });
84
+
85
+ <div>{'Double: ' + $double}</div>
86
+ <button onClick={() => { $count++; }}>{'Increment'}</button>
87
+ }
88
+
89
+ render(App);
90
+ flushSync();
91
+
92
+ expect(container.querySelector('div').textContent).toBe('Double: 0');
93
+ expect(log).toEqual(['Count:0']);
94
+
95
+ const button = container.querySelector('button');
96
+
97
+ button.click();
98
+ flushSync();
99
+
100
+ expect(container.querySelector('div').textContent).toBe('Double: 2');
101
+ expect(log).toEqual(['Count:0', 'Count:1']);
102
+
103
+ button.click();
104
+ flushSync();
105
+
106
+ expect(container.querySelector('div').textContent).toBe('Double: 4');
107
+ expect(log).toEqual(['Count:0', 'Count:1', 'Count:2']);
108
+ });
109
+ });
@@ -58,4 +58,25 @@ describe('compiler success tests', () => {
58
58
  render(App);
59
59
  });
60
60
 
61
+ it('render lexical blocks without crashing', () => {
62
+ component App() {
63
+ <div>
64
+ const a = 1;
65
+ <div>
66
+ const b = 1;
67
+ </div>
68
+ <div>
69
+ const b = 1;
70
+ </div>
71
+ </div>
72
+ <div>
73
+ const a = 2;
74
+ <div>
75
+ const b = 1;
76
+ </div>
77
+ </div>
78
+ }
79
+
80
+ render(App);
81
+ })
61
82
  });
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, RippleMap } from 'ripple';
3
+
4
+ describe('RippleMap', () => {
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('handles set with update and delete operations with a reactive $size var', () => {
24
+ component MapTest() {
25
+ let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
26
+ let $value = map.get('a');
27
+ let $size = map.$size;
28
+
29
+ <button onClick={() => map.set('d', 4)}>{'set'}</button>
30
+ <button onClick={() => map.delete('b')}>{'delete'}</button>
31
+ <button onClick={() => map.set('a', 5)}>{'update'}</button>
32
+
33
+ <pre>{map.get('d')}</pre>
34
+ <pre>{$size}</pre>
35
+ <pre>{$value}</pre>
36
+ }
37
+
38
+ render(MapTest);
39
+
40
+ const setButton = container.querySelectorAll('button')[0];
41
+ const deleteButton = container.querySelectorAll('button')[1];
42
+ const updateButton = container.querySelectorAll('button')[2];
43
+
44
+ setButton.click();
45
+ flushSync();
46
+
47
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('4');
48
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
49
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
50
+
51
+ deleteButton.click();
52
+ flushSync();
53
+
54
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
55
+
56
+ updateButton.click();
57
+ flushSync();
58
+
59
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('5');
60
+ });
61
+
62
+ it('handles clear operation', () => {
63
+ component MapTest() {
64
+ let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
65
+
66
+ <button onClick={() => map.clear()}>{'clear'}</button>
67
+ <pre>{map.$size}</pre>
68
+ }
69
+
70
+ render(MapTest);
71
+
72
+ const clearButton = container.querySelector('button');
73
+
74
+ clearButton.click();
75
+ flushSync();
76
+
77
+ expect(container.querySelector('pre').textContent).toBe('0');
78
+ });
79
+
80
+ it('handles has operation and tracks reactive $has', () => {
81
+ component MapTest() {
82
+ let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
83
+ let $has = map.has('b');
84
+
85
+ <button onClick={() => map.delete('b')}>{'delete'}</button>
86
+ <pre>{$has}</pre>
87
+ }
88
+
89
+ render(MapTest);
90
+
91
+ const deleteButton = container.querySelector('button');
92
+ expect(container.querySelector('pre').textContent).toBe('true');
93
+
94
+ deleteButton.click();
95
+ flushSync();
96
+
97
+ expect(container.querySelector('pre').textContent).toBe('false');
98
+ });
99
+
100
+ it('handles reactivity of keys, values, and entries', () => {
101
+ component MapTest() {
102
+ let map = new RippleMap([['x', 10], ['y', 20]]);
103
+ let $keys = Array.from(map.keys());
104
+ let $values = Array.from(map.values());
105
+ let $entries = Array.from(map.entries());
106
+
107
+ <button onClick={() => map.delete('x')}>{'delete'}</button>
108
+
109
+ <pre>{JSON.stringify($keys)}</pre>
110
+ <pre>{JSON.stringify($values)}</pre>
111
+ <pre>{JSON.stringify($entries)}</pre>
112
+ }
113
+
114
+ render(MapTest);
115
+
116
+ const deleteButton = container.querySelector('button');
117
+
118
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
119
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('[10,20]');
120
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('[["x",10],["y",20]]');
121
+
122
+ deleteButton.click();
123
+ flushSync();
124
+
125
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["y"]');
126
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('[20]');
127
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('[["y",20]]');
128
+ });
129
+
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]]');
133
+ });
134
+ });
@@ -82,7 +82,7 @@ describe('RippleSet', () => {
82
82
  let $hasValue = items.has(2);
83
83
 
84
84
  <button onClick={() => items.delete(2)}>{'delete'}</button>
85
- <pre>{JSON.stringify($hasValue)}</pre>
85
+ <pre>{$hasValue}</pre>
86
86
  }
87
87
 
88
88
  render(SetTest);
package/types/index.d.ts CHANGED
@@ -50,3 +50,8 @@ export class RippleSet<T> extends Set<T> {
50
50
  union(other: RippleSet<T> | Set<T>): RippleSet<T>;
51
51
  toJSON(): T[];
52
52
  }
53
+
54
+ export class RippleMap<K, V> extends Map<K, V> {
55
+ get $size(): number;
56
+ toJSON(): [K, V][];
57
+ }
File without changes