ripple 0.2.33 → 0.2.35

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.33",
6
+ "version": "0.2.35",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -8,6 +8,7 @@ import {
8
8
  is_ripple_import,
9
9
  is_tracked_computed_property,
10
10
  is_tracked_name,
11
+ is_void_element,
11
12
  } from '../../utils.js';
12
13
  import { extract_paths } from '../../../utils/ast.js';
13
14
  import is_reference from 'is-reference';
@@ -432,6 +433,8 @@ const visitors = {
432
433
  const attribute_names = new Set();
433
434
 
434
435
  if (is_dom_element) {
436
+ const is_void = is_void_element(node.id.name);
437
+
435
438
  if (state.elements) {
436
439
  state.elements.push(node);
437
440
  }
@@ -468,6 +471,14 @@ const visitors = {
468
471
  );
469
472
  }
470
473
  }
474
+
475
+ if (is_void && node.children.length > 0) {
476
+ error(
477
+ `The <${node.id.name}> element is a void element and cannot have children`,
478
+ state.analysis.module.filename,
479
+ node,
480
+ );
481
+ }
471
482
  } else {
472
483
  for (const attr of node.attributes) {
473
484
  if (attr.type === 'Attribute') {
@@ -21,6 +21,7 @@ import {
21
21
  is_inside_call_expression,
22
22
  is_tracked_computed_property,
23
23
  is_value_static,
24
+ is_void_element,
24
25
  } from '../../utils.js';
25
26
  import is_reference from 'is-reference';
26
27
  import { extract_paths, object } from '../../../utils/ast.js';
@@ -148,6 +149,45 @@ const visitors = {
148
149
  return context.next();
149
150
  }
150
151
 
152
+ // Handle array methods that access the array
153
+ if (
154
+ callee.type === 'MemberExpression' &&
155
+ !callee.optional &&
156
+ callee.property.type === 'Identifier'
157
+ ) {
158
+ const name = callee.property.name;
159
+ if (
160
+ // TODO support the missing array methods
161
+ name === 'reduce' ||
162
+ name === 'map' ||
163
+ name === 'forEach' ||
164
+ name === 'join' ||
165
+ name === 'includes' ||
166
+ name === 'indexOf' ||
167
+ name === 'lastIndexOf' ||
168
+ name === 'filter' ||
169
+ name === 'every' ||
170
+ name === 'some' ||
171
+ name === 'toSpliced' ||
172
+ name === 'toSorted' ||
173
+ name === 'toString' ||
174
+ name === 'values' ||
175
+ name === 'entries'
176
+ ) {
177
+ return b.call(
178
+ '$.with_scope',
179
+ b.id('__block'),
180
+ b.thunk(
181
+ b.call(
182
+ '$.array_' + name,
183
+ context.visit(callee.object),
184
+ ...node.arguments.map((arg) => context.visit(arg)),
185
+ ),
186
+ ),
187
+ );
188
+ }
189
+ }
190
+
151
191
  return b.call(
152
192
  '$.with_scope',
153
193
  b.id('__block'),
@@ -399,6 +439,7 @@ const visitors = {
399
439
  if (is_dom_element) {
400
440
  let class_attribute = null;
401
441
  const local_updates = [];
442
+ const is_void = is_void_element(node.id.name);
402
443
 
403
444
  state.template.push(`<${node.id.name}`);
404
445
 
@@ -590,7 +631,14 @@ const visitors = {
590
631
  const init = [];
591
632
  const update = [];
592
633
 
593
- transform_children(node.children, { visit, state: { ...state, init, update }, root: false });
634
+ if (!is_void) {
635
+ transform_children(node.children, {
636
+ visit,
637
+ state: { ...state, init, update },
638
+ root: false,
639
+ });
640
+ state.template.push(`</${node.id.name}>`);
641
+ }
594
642
 
595
643
  update.push(...local_updates);
596
644
 
@@ -601,8 +649,6 @@ const visitors = {
601
649
  if (update.length > 0) {
602
650
  state.init.push(b.stmt(b.call('$.render', b.thunk(b.block(update)))));
603
651
  }
604
-
605
- state.template.push(`</${node.id.name}>`);
606
652
  } else {
607
653
  const id = state.flush_node();
608
654
 
@@ -975,43 +1021,37 @@ const visitors = {
975
1021
  },
976
1022
 
977
1023
  ArrayExpression(node, context) {
978
- // const elements = [];
979
- // const tracked = [];
980
- // let i = 0;
981
-
982
- // for (const element of node.elements) {
983
- // if (element === null) {
984
- // elements.push(null);
985
- // } else if (element.type === 'Element') {
986
- // const metadata = { tracking: false, await: false };
987
- // const tracked_element = context.visit(element, { ...context.state, metadata });
988
-
989
- // if (metadata.tracking) {
990
- // tracked.push(b.literal(i));
991
- // elements.push(tracked_element);
992
- // } else {
993
- // elements.push(tracked_element);
994
- // }
995
- // } else if (element.type === 'SpreadElement') {
996
- // const metadata = { tracking: false, await: false };
997
- // const tracked_element = context.visit(element, { ...context.state, metadata });
998
-
999
- // if (metadata.tracking) {
1000
- // tracked.push(b.spread(tracked_element.argument));
1001
- // elements.push(tracked_element);
1002
- // } else {
1003
- // elements.push(tracked_element);
1004
- // }
1005
- // } else {
1006
- // const metadata = { tracking: false, await: false };
1007
- // elements.push(context.visit(element, { ...context.state, metadata }));
1008
- // }
1009
- // i++;
1010
- // }
1011
-
1012
- // if (tracked.length > 0) {
1013
- // return b.call('$.tracked_object', { ...node, elements }, b.array(tracked), b.id('__block'));
1014
- // }
1024
+ // TODO we can bail out of all of this if we know we're inside a computed fn expression
1025
+ // as the reactivity will hold from the reference of the $ binding itself
1026
+ const elements = [];
1027
+ const tracked = [];
1028
+ let i = 0;
1029
+
1030
+ for (const element of node.elements) {
1031
+ if (element === null) {
1032
+ elements.push(null);
1033
+ } else if (element.type === 'Identifier' && is_tracked_name(element.name)) {
1034
+ const metadata = { tracking: false, await: false };
1035
+ const tracked_identifier = context.visit(element, { ...context.state, metadata });
1036
+
1037
+ if (metadata.tracking) {
1038
+ tracked.push(b.literal(i));
1039
+ elements.push(
1040
+ b.call('$.computed_property', b.thunk(tracked_identifier), b.id('__block')),
1041
+ );
1042
+ } else {
1043
+ elements.push(tracked_identifier);
1044
+ }
1045
+ } else {
1046
+ const metadata = { tracking: false, await: false };
1047
+ elements.push(context.visit(element, { ...context.state, metadata }));
1048
+ }
1049
+ i++;
1050
+ }
1051
+
1052
+ if (tracked.length > 0) {
1053
+ return b.call('$.tracked_object', { ...node, elements }, b.array(tracked), b.id('__block'));
1054
+ }
1015
1055
 
1016
1056
  context.next();
1017
1057
  },
@@ -3,6 +3,33 @@ import * as b from '../utils/builders.js';
3
3
 
4
4
  const regex_return_characters = /\r/g;
5
5
 
6
+ const VOID_ELEMENT_NAMES = [
7
+ 'area',
8
+ 'base',
9
+ 'br',
10
+ 'col',
11
+ 'command',
12
+ 'embed',
13
+ 'hr',
14
+ 'img',
15
+ 'input',
16
+ 'keygen',
17
+ 'link',
18
+ 'meta',
19
+ 'param',
20
+ 'source',
21
+ 'track',
22
+ 'wbr'
23
+ ];
24
+
25
+ /**
26
+ * Returns `true` if `name` is of a void element
27
+ * @param {string} name
28
+ */
29
+ export function is_void_element(name) {
30
+ return VOID_ELEMENT_NAMES.includes(name) || name.toLowerCase() === '!doctype';
31
+ }
32
+
6
33
  const RESERVED_WORDS = [
7
34
  'arguments',
8
35
  'await',
@@ -0,0 +1,352 @@
1
+ import { TRACKED_OBJECT } from './constants';
2
+ import { get_property } from './runtime';
3
+
4
+ const array_proto = Array.prototype;
5
+
6
+ /**
7
+ * @template T
8
+ * @param {Array<T>} array
9
+ * @param {(previousValue: T, currentValue: T, currentIndex: number, array: Array<T>) => T} callback
10
+ * @param {T} initial_value
11
+ * @returns {T}
12
+ */
13
+ export function array_reduce(array, callback, initial_value) {
14
+ // @ts-expect-error
15
+ var tracked_properties = array[TRACKED_OBJECT];
16
+
17
+ if (tracked_properties === undefined || array.reduce !== array_proto.reduce) {
18
+ return array.reduce(callback, initial_value);
19
+ }
20
+
21
+ let accumulator = initial_value;
22
+
23
+ for (let i = 0; i < array.length; i++) {
24
+ accumulator = callback(accumulator, get_property(array, i), i, array);
25
+ }
26
+
27
+ return accumulator;
28
+ }
29
+
30
+ /**
31
+ * @template T
32
+ * @param {Array<T>} array
33
+ * @param {string} [separator]
34
+ * @returns {string}
35
+ */
36
+ export function array_join(array, separator) {
37
+ // @ts-expect-error
38
+ var tracked_properties = array[TRACKED_OBJECT];
39
+ if (tracked_properties === undefined || array.join !== array_proto.join) {
40
+ return array.join(separator);
41
+ }
42
+
43
+ let result = '';
44
+ for (let i = 0; i < array.length; i++) {
45
+ if (i > 0 && separator !== undefined) {
46
+ result += separator;
47
+ }
48
+ result += String(get_property(array, i));
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * @template T
56
+ * @template U
57
+ * @param {Array<T>} array
58
+ * @param {(value: T, index: number, array: Array<T>) => U} callback
59
+ * @returns {Array<U>}
60
+ */
61
+ export function array_map(array, callback) {
62
+ // @ts-expect-error
63
+ var tracked_properties = array[TRACKED_OBJECT];
64
+ if (tracked_properties === undefined || array.map !== array_proto.map) {
65
+ return array.map(callback);
66
+ }
67
+
68
+ const result = [];
69
+ for (let i = 0; i < array.length; i++) {
70
+ if (i in array) {
71
+ result[i] = callback(get_property(array, i), i, array);
72
+ }
73
+ }
74
+
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * @template T
80
+ * @param {Array<T>} array
81
+ * @param {(value: T, index: number, array: Array<T>) => boolean} callback
82
+ * @returns {Array<T>}
83
+ */
84
+ export function array_filter(array, callback) {
85
+ // @ts-expect-error
86
+ var tracked_properties = array[TRACKED_OBJECT];
87
+ if (tracked_properties === undefined || array.filter !== array_proto.filter) {
88
+ return array.filter(callback);
89
+ }
90
+
91
+ const result = [];
92
+ for (let i = 0; i < array.length; i++) {
93
+ if (i in array) {
94
+ const value = get_property(array, i);
95
+ if (callback(value, i, array)) {
96
+ result.push(value);
97
+ }
98
+ }
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ /**
105
+ * @template T
106
+ * @param {Array<T>} array
107
+ * @param {(value: T, index: number, array: Array<T>) => boolean} callback
108
+ * @returns {void}
109
+ */
110
+ export function array_forEach(array, callback) {
111
+ // @ts-expect-error
112
+ var tracked_properties = array[TRACKED_OBJECT];
113
+ if (tracked_properties === undefined || array.forEach !== array_proto.forEach) {
114
+ return array.forEach(callback);
115
+ }
116
+
117
+ for (let i = 0; i < array.length; i++) {
118
+ if (i in array) {
119
+ callback(get_property(array, i), i, array);
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @template T
126
+ * @param {Array<T>} array
127
+ * @param {T} value
128
+ * @returns {boolean}
129
+ */
130
+ export function array_includes(array, value) {
131
+ // @ts-expect-error
132
+ var tracked_properties = array[TRACKED_OBJECT];
133
+ if (tracked_properties === undefined || array.includes !== array_proto.includes) {
134
+ return array.includes(value);
135
+ }
136
+
137
+ for (let i = 0; i < array.length; i++) {
138
+ if (i in array && get_property(array, i) === value) {
139
+ return true;
140
+ }
141
+ }
142
+
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * @template T
148
+ * @param {Array<T>} array
149
+ * @param {T} value
150
+ * @returns {number}
151
+ */
152
+ export function array_indexOf(array, value) {
153
+ // @ts-expect-error
154
+ var tracked_properties = array[TRACKED_OBJECT];
155
+ if (tracked_properties === undefined || array.indexOf !== array_proto.indexOf) {
156
+ return array.indexOf(value);
157
+ }
158
+
159
+ for (let i = 0; i < array.length; i++) {
160
+ if (i in array && get_property(array, i) === value) {
161
+ return i;
162
+ }
163
+ }
164
+
165
+ return -1;
166
+ }
167
+
168
+ /**
169
+ * @template T
170
+ * @param {Array<T>} array
171
+ * @param {T} value
172
+ * @returns {number}
173
+ */
174
+ export function array_lastIndexOf(array, value) {
175
+ // @ts-expect-error
176
+ var tracked_properties = array[TRACKED_OBJECT];
177
+ if (tracked_properties === undefined || array.lastIndexOf !== array_proto.lastIndexOf) {
178
+ return array.lastIndexOf(value);
179
+ }
180
+
181
+ for (let i = array.length - 1; i >= 0; i--) {
182
+ if (i in array && get_property(array, i) === value) {
183
+ return i;
184
+ }
185
+ }
186
+
187
+ return -1;
188
+ }
189
+
190
+ /**
191
+ * @template T
192
+ * @param {Array<T>} array
193
+ * @param {(value: T, index: number, array: Array<T>) => boolean} callback
194
+ * @returns {boolean}
195
+ */
196
+ export function array_every(array, callback) {
197
+ // @ts-expect-error
198
+ var tracked_properties = array[TRACKED_OBJECT];
199
+ if (tracked_properties === undefined || array.every !== array_proto.every) {
200
+ return array.every(callback);
201
+ }
202
+
203
+ for (let i = 0; i < array.length; i++) {
204
+ if (i in array && !callback(get_property(array, i), i, array)) {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ return true;
210
+ }
211
+
212
+ /**
213
+ * @template T
214
+ * @param {Array<T>} array
215
+ * @param {(value: T, index: number, array: Array<T>) => boolean} callback
216
+ * @returns {boolean}
217
+ */
218
+ export function array_some(array, callback) {
219
+ // @ts-expect-error
220
+ var tracked_properties = array[TRACKED_OBJECT];
221
+ if (tracked_properties === undefined || array.some !== array_proto.some) {
222
+ return array.some(callback);
223
+ }
224
+
225
+ for (let i = 0; i < array.length; i++) {
226
+ if (i in array && callback(get_property(array, i), i, array)) {
227
+ return true;
228
+ }
229
+ }
230
+
231
+ return false;
232
+ }
233
+
234
+ /**
235
+ * @template T
236
+ * @param {Array<T>} array
237
+ * @returns {string}
238
+ */
239
+ export function array_toString(array) {
240
+ // @ts-expect-error
241
+ var tracked_properties = array[TRACKED_OBJECT];
242
+ if (tracked_properties === undefined || array.toString !== array_proto.toString) {
243
+ return array.toString();
244
+ }
245
+
246
+ let result = '';
247
+ for (let i = 0; i < array.length; i++) {
248
+ if (i > 0) {
249
+ result += ',';
250
+ }
251
+ if (i in array) {
252
+ result += String(get_property(array, i));
253
+ }
254
+ }
255
+
256
+ return result;
257
+ }
258
+
259
+ /**
260
+ * @template T
261
+ * @param {Array<T>} array
262
+ * @param {((a: T, b: T) => number) | undefined} compare_fn
263
+ * @returns {Array<T>}
264
+ */
265
+ export function array_toSorted(array, compare_fn) {
266
+ // @ts-expect-error
267
+ var tracked_properties = array[TRACKED_OBJECT];
268
+ if (tracked_properties === undefined || array.toSorted !== array_proto.toSorted) {
269
+ return array.toSorted(compare_fn);
270
+ }
271
+
272
+ const result = [];
273
+ for (let i = 0; i < array.length; i++) {
274
+ if (i in array) {
275
+ result.push(get_property(array, i));
276
+ }
277
+ }
278
+
279
+ return result.sort(compare_fn);
280
+ }
281
+
282
+ /**
283
+ * @template T
284
+ * @param {Array<T>} array
285
+ * @param {number} start
286
+ * @param {number} delete_count
287
+ * @param {...T} items
288
+ * @returns {Array<T>}
289
+ */
290
+ export function array_toSpliced(array, start, delete_count, ...items) {
291
+ // @ts-expect-error
292
+ var tracked_properties = array[TRACKED_OBJECT];
293
+ if (tracked_properties === undefined || array.toSpliced !== array_proto.toSpliced) {
294
+ return array.toSpliced(start, delete_count, ...items);
295
+ }
296
+
297
+ const result = [];
298
+ for (let i = 0; i < array.length; i++) {
299
+ if (i in array) {
300
+ result.push(get_property(array, i));
301
+ }
302
+ }
303
+
304
+ result.splice(start, delete_count, ...items);
305
+
306
+ return result;
307
+ }
308
+
309
+ /**
310
+ * @template T
311
+ * @param {Array<T>} array
312
+ * @returns {IterableIterator<T>}
313
+ */
314
+ export function array_values(array) {
315
+ // @ts-expect-error
316
+ var tracked_properties = array[TRACKED_OBJECT];
317
+ if (tracked_properties === undefined || array.values !== array_proto.values) {
318
+ return array.values();
319
+ }
320
+
321
+ const result = [];
322
+ for (let i = 0; i < array.length; i++) {
323
+ if (i in array) {
324
+ result.push(get_property(array, i));
325
+ }
326
+ }
327
+
328
+ return result[Symbol.iterator]();
329
+ }
330
+
331
+ /**
332
+ * @template T
333
+ * @param {Array<T>} array
334
+ * @returns {IterableIterator<[number, T]>}
335
+ */
336
+ export function array_entries(array) {
337
+ // @ts-expect-error
338
+ var tracked_properties = array[TRACKED_OBJECT];
339
+ if (tracked_properties === undefined || array.entries !== array_proto.entries) {
340
+ return array.entries();
341
+ }
342
+
343
+ /** @type {Array<[number, T]>} */
344
+ const result = [];
345
+ for (let i = 0; i < array.length; i++) {
346
+ if (i in array) {
347
+ result.push([i, get_property(array, i)]);
348
+ }
349
+ }
350
+
351
+ return result[Symbol.iterator]();
352
+ }
@@ -46,6 +46,24 @@ export {
46
46
  exclude_from_object,
47
47
  } from './runtime.js';
48
48
 
49
+ export {
50
+ array_reduce,
51
+ array_join,
52
+ array_map,
53
+ array_filter,
54
+ array_forEach,
55
+ array_includes,
56
+ array_indexOf,
57
+ array_lastIndexOf,
58
+ array_every,
59
+ array_some,
60
+ array_toString,
61
+ array_toSorted,
62
+ array_toSpliced,
63
+ array_values,
64
+ array_entries,
65
+ } from './array.js';
66
+
49
67
  export { for_block as for } from './for.js';
50
68
 
51
69
  export { if_block as if } from './if.js';
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync } from 'ripple';
2
+ import { mount, flushSync, effect } from 'ripple';
3
3
 
4
4
  describe('basic', () => {
5
5
  let container;
@@ -832,4 +832,70 @@ describe('basic', () => {
832
832
  expect(paragraphs[0].className).toBe('parent-class');
833
833
  expect(paragraphs[1].className).toBe('nested-class');
834
834
  });
835
+
836
+ it('basic reactivity with standard arrays should work', () => {
837
+ let logs = [];
838
+
839
+ component App() {
840
+ let $first = 0;
841
+ let $second = 0;
842
+ const arr = [$first, $second];
843
+
844
+ const $total = arr.reduce((a, b) => a + b, 0);
845
+
846
+ <button onClick={() => { $first++; }}>{'first:' + $first}</button>
847
+ <button onClick={() => { $second++; }}>{'second: ' + $second}</button>
848
+
849
+ effect(() => {
850
+ let _arr = [];
851
+
852
+ arr.forEach((item) => {
853
+ _arr.push(item);
854
+ });
855
+
856
+ logs.push(_arr.join(', '));
857
+ });
858
+
859
+ effect(() => {
860
+ if (arr.includes(1)) {
861
+ logs.push('arr includes 1');
862
+ }
863
+ });
864
+
865
+ <div>{'Sum: ' + $total}</div>
866
+ <div>{'Comma Separated: ' + arr.join(', ')}</div>
867
+ <div>{'Number to string: ' + arr.map(a => String(a))}</div>
868
+ <div>{'Even numbers: ' + arr.filter(a => a % 2 === 0)}</div>
869
+ }
870
+
871
+ render(App);
872
+ flushSync();
873
+
874
+ const buttons = container.querySelectorAll('button');
875
+ const divs = container.querySelectorAll('div');
876
+
877
+ expect(divs[0].textContent).toBe('Sum: 0');
878
+ expect(divs[1].textContent).toBe('Comma Separated: 0, 0');
879
+ expect(divs[2].textContent).toBe('Number to string: 0,0');
880
+ expect(divs[3].textContent).toBe('Even numbers: 0,0');
881
+ expect(logs).toEqual(['0, 0']);
882
+
883
+ buttons[0].click();
884
+ flushSync();
885
+
886
+ expect(divs[0].textContent).toBe('Sum: 1');
887
+ expect(divs[1].textContent).toBe('Comma Separated: 1, 0');
888
+ expect(divs[2].textContent).toBe('Number to string: 1,0');
889
+ expect(divs[3].textContent).toBe('Even numbers: 0');
890
+ expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1']);
891
+
892
+ buttons[1].click();
893
+ flushSync();
894
+
895
+ expect(divs[0].textContent).toBe('Sum: 2');
896
+ expect(divs[1].textContent).toBe('Comma Separated: 1, 1');
897
+ expect(divs[2].textContent).toBe('Number to string: 1,1');
898
+ expect(divs[3].textContent).toBe('Even numbers: ');
899
+ expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1', '1, 1']);
900
+ })
835
901
  });