ripple 0.2.98 → 0.2.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.98",
6
+ "version": "0.2.99",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -461,14 +461,20 @@ function RipplePlugin(config) {
461
461
 
462
462
  if (this.isContextual('index')) {
463
463
  this.next(); // consume 'index'
464
-
465
- if (this.type === tt.name) {
466
- node.index = this.parseIdent();
467
- } else {
464
+ node.index = this.parseExpression();
465
+ if (node.index.type !== 'Identifier') {
468
466
  this.raise(this.start, 'Expected identifier after "index" keyword');
469
467
  }
470
- } else {
471
- this.raise(this.start, 'Expected "index" keyword after semicolon in for-of loop');
468
+ this.eat(tt.semi);
469
+ }
470
+
471
+ if (this.isContextual('key')) {
472
+ this.next(); // consume 'key'
473
+ node.key = this.parseExpression();
474
+ }
475
+
476
+ if (this.isContextual('index')) {
477
+ this.raise(this.start, '"index" must come before "key" in for-of loop');
472
478
  }
473
479
  } else if (!isForIn) {
474
480
  // Set index to null for standard for-of loops
@@ -858,7 +864,7 @@ function RipplePlugin(config) {
858
864
  const element = this.startNode();
859
865
  element.start = position.index;
860
866
  element.loc.start = position;
861
- element.metadata = {};
867
+ element.metadata = {};
862
868
  element.type = 'Element';
863
869
  this.#path.push(element);
864
870
  element.children = [];
@@ -466,6 +466,14 @@ const visitors = {
466
466
  if (attr.name.type === 'Identifier') {
467
467
  attribute_names.add(attr.name);
468
468
 
469
+ if (attr.name.name === 'key') {
470
+ error(
471
+ 'The `key` attribute is not a thing in Ripple, and cannot be used on DOM elements. If you are using a for loop, then use the `for (let item of items; key item.id)` syntax.',
472
+ state.analysis.module.filename,
473
+ attr,
474
+ );
475
+ }
476
+
469
477
  if (is_event_attribute(attr.name.name)) {
470
478
  const event_name = attr.name.name.slice(2).toLowerCase();
471
479
  const handler = visit(attr.value, state);
@@ -11,6 +11,7 @@ import {
11
11
  TEMPLATE_FRAGMENT,
12
12
  TEMPLATE_SVG_NAMESPACE,
13
13
  TEMPLATE_MATHML_NAMESPACE,
14
+ IS_KEYED,
14
15
  } from '../../../../constants.js';
15
16
  import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
16
17
  import {
@@ -1020,11 +1021,15 @@ const visitors = {
1020
1021
  }
1021
1022
  const is_controlled = node.is_controlled;
1022
1023
  const index = node.index;
1024
+ const key = node.key;
1023
1025
  let flags = is_controlled ? IS_CONTROLLED : 0;
1024
1026
 
1025
- if (index !== null) {
1027
+ if (index != null) {
1026
1028
  flags |= IS_INDEXED;
1027
1029
  }
1030
+ if (key != null) {
1031
+ flags |= IS_KEYED;
1032
+ }
1028
1033
 
1029
1034
  // do only if not controller
1030
1035
  if (!is_controlled) {
@@ -1034,6 +1039,7 @@ const visitors = {
1034
1039
  const id = context.state.flush_node(is_controlled);
1035
1040
  const pattern = node.left.declarations[0].id;
1036
1041
  const body_scope = context.state.scopes.get(node.body);
1042
+ const is_keyed = key && (flags & IS_KEYED) !== 0;
1037
1043
 
1038
1044
  context.state.init.push(
1039
1045
  b.stmt(
@@ -1051,6 +1057,7 @@ const visitors = {
1051
1057
  ),
1052
1058
  ),
1053
1059
  b.literal(flags),
1060
+ is_keyed ? b.arrow(index ? [pattern, index] : [pattern], context.visit(key)) : undefined,
1054
1061
  ),
1055
1062
  ),
1056
1063
  );
package/src/constants.js CHANGED
@@ -2,6 +2,7 @@ export const TEMPLATE_FRAGMENT = 1;
2
2
  export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
3
3
  export const IS_CONTROLLED = 1 << 2;
4
4
  export const IS_INDEXED = 1 << 3;
5
- export const TEMPLATE_SVG_NAMESPACE = 1 << 3;
6
- export const TEMPLATE_MATHML_NAMESPACE = 1 << 4;
5
+ export const IS_KEYED = 1 << 4;
6
+ export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
7
+ export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
7
8
 
@@ -60,8 +60,6 @@ export { TrackedMap } from './map.js';
60
60
 
61
61
  export { TrackedDate } from './date.js';
62
62
 
63
- export { keyed } from './internal/client/for.js';
64
-
65
63
  export { user_effect as effect } from './internal/client/blocks.js';
66
64
 
67
65
  export { Portal } from './internal/client/portal.js';
@@ -1,6 +1,6 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { Block, Tracked } from '#client' */
2
2
 
3
- import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
3
+ import { IS_CONTROLLED, IS_INDEXED, IS_KEYED } from '../../../constants.js';
4
4
  import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
5
5
  import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
6
6
  import { create_text, next_sibling } from './operations.js';
@@ -85,15 +85,18 @@ function collection_to_array(collection) {
85
85
 
86
86
  /**
87
87
  * @template V
88
+ * @template K
88
89
  * @param {Element} node
89
90
  * @param {() => V[] | Iterable<V>} get_collection
90
91
  * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
91
92
  * @param {number} flags
93
+ * @param {(item: V) => K} [get_key]
92
94
  * @returns {void}
93
95
  */
94
- export function for_block(node, get_collection, render_fn, flags) {
96
+ export function for_block(node, get_collection, render_fn, flags, get_key) {
95
97
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
96
98
  var is_indexed = (flags & IS_INDEXED) !== 0;
99
+ var is_keyed = (flags & IS_KEYED) !== 0;
97
100
  var anchor = /** @type {Element | Text} */ (node);
98
101
 
99
102
  if (is_controlled) {
@@ -104,6 +107,9 @@ export function for_block(node, get_collection, render_fn, flags) {
104
107
  var block = /** @type {Block} */ (active_block);
105
108
  var collection = get_collection();
106
109
  var array = collection_to_array(collection);
110
+ if (is_keyed) {
111
+ array = keyed(block, array, /** @type {(item: V) => K} */ (get_key));
112
+ }
107
113
 
108
114
  untrack(() => {
109
115
  reconcile(anchor, block, array, render_fn, is_controlled, is_indexed);
@@ -321,9 +327,9 @@ function reconcile(anchor, block, b, render_fn, is_controlled, is_indexed) {
321
327
  if (j !== undefined) {
322
328
  if (fast_path_removal) {
323
329
  fast_path_removal = false;
324
- // while (i > a_start) {
325
- // destroy_block(a[a_start++]);
326
- // }
330
+ while (i > a_start) {
331
+ destroy_block(a[a_start++]);
332
+ }
327
333
  }
328
334
  sources[j - b_start] = i + 1;
329
335
  if (pos > j) {
@@ -396,7 +402,7 @@ function reconcile(anchor, block, b, render_fn, is_controlled, is_indexed) {
396
402
  let result;
397
403
  /** @type {Int32Array} */
398
404
  let p;
399
- let maxLen = 0;
405
+ let max_len = 0;
400
406
  // https://en.wikipedia.org/wiki/Longest_increasing_subsequence
401
407
  /**
402
408
  * @param {Int32Array} arr
@@ -412,8 +418,8 @@ function lis_algorithm(arr) {
412
418
  let c = 0;
413
419
  var len = arr.length;
414
420
 
415
- if (len > maxLen) {
416
- maxLen = len;
421
+ if (len > max_len) {
422
+ max_len = len;
417
423
  result = new Int32Array(len);
418
424
  p = new Int32Array(len);
419
425
  }
@@ -466,16 +472,12 @@ function lis_algorithm(arr) {
466
472
  /**
467
473
  * @template V
468
474
  * @template K
475
+ * @param {Block} block
469
476
  * @param {V[] | Iterable<V>} collection
470
477
  * @param {(item: V) => K} key_fn
471
478
  * @returns {V[]}
472
479
  */
473
- export function keyed(collection, key_fn) {
474
- var block = active_block;
475
- if (block === null || (block.f & FOR_BLOCK) === 0) {
476
- throw new Error('keyed() must be used inside a for block');
477
- }
478
-
480
+ function keyed(block, collection, key_fn) {
479
481
  var b_array = collection_to_array(collection);
480
482
  var b_keys = b_array.map(key_fn);
481
483
 
@@ -488,14 +490,16 @@ export function keyed(collection, key_fn) {
488
490
  var state = block.s;
489
491
 
490
492
  if (state === null) {
491
- return b_array;
493
+ // Make a clone of it so we don't mutate the original thereafter
494
+ return b_array.slice();
492
495
  }
493
496
 
494
497
  var a_array = state.array;
498
+ var a_blocks = state.blocks;
495
499
  var a_keys = a_array.map(key_fn);
496
500
  var a = new Map();
497
501
 
498
- for (let i = 0; i < a_keys.length; i++) {
502
+ for (var i = 0; i < a_keys.length; i++) {
499
503
  a.set(a_keys[i], i);
500
504
  }
501
505
 
@@ -503,8 +507,12 @@ export function keyed(collection, key_fn) {
503
507
  throw new Error('Duplicate keys are not allowed');
504
508
  }
505
509
 
506
- for (let i = 0; i < b_keys.length; i++) {
510
+ for (var i = 0; i < b_keys.length; i++) {
507
511
  var b_val = b_keys[i];
512
+ // if the index is the key, skip
513
+ if (b_val === i) {
514
+ continue;
515
+ }
508
516
  var index = a.get(b_val);
509
517
 
510
518
  if (index !== undefined) {
@@ -1,49 +1,98 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`for statements > correctly handle the index in a for...of loop 1`] = `
3
+ exports[`for statements > correctly handles intermediate statements in for block 1`] = `
4
4
  <div>
5
5
  <div>
6
6
  <div>
7
- 0 : a
7
+ <div>
8
+ 1
9
+ </div>
10
+ <div>
11
+ 1
12
+ </div>
8
13
  </div>
9
14
  <div>
10
- 1 : b
15
+ <div>
16
+ 2
17
+ </div>
18
+ <div>
19
+ 2
20
+ </div>
11
21
  </div>
12
22
  <div>
13
- 2 : c
23
+ <div>
24
+ 3
25
+ </div>
26
+ <div>
27
+ 3
28
+ </div>
14
29
  </div>
15
30
 
16
31
  </div>
17
32
  <button>
18
33
  Add Item
19
34
  </button>
20
- <button>
21
- Reverse
22
- </button>
23
35
 
24
36
  </div>
25
37
  `;
26
38
 
27
- exports[`for statements > correctly handle the index in a for...of loop 2`] = `
39
+ exports[`for statements > correctly handles intermediate statements in for block 2`] = `
28
40
  <div>
29
41
  <div>
30
42
  <div>
31
- 0 : a
43
+ <div>
44
+ 1
45
+ </div>
46
+ <div>
47
+ 1
48
+ </div>
32
49
  </div>
33
50
  <div>
34
- 1 : b
51
+ <div>
52
+ 2
53
+ </div>
54
+ <div>
55
+ 2
56
+ </div>
35
57
  </div>
36
58
  <div>
37
- 2 : c
59
+ <div>
60
+ 3
61
+ </div>
62
+ <div>
63
+ 3
64
+ </div>
38
65
  </div>
39
66
  <div>
40
- 3 : d
67
+ <div>
68
+ 4
69
+ </div>
70
+ <div>
71
+ 4
72
+ </div>
41
73
  </div>
42
74
 
43
75
  </div>
44
76
  <button>
45
77
  Add Item
46
78
  </button>
79
+
80
+ </div>
81
+ `;
82
+
83
+ exports[`for statements > correctly handles keyed for...of loops 1`] = `
84
+ <div>
85
+ <!---->
86
+ <div>
87
+ 0:Item 1
88
+ </div>
89
+ <div>
90
+ 1:Item 2
91
+ </div>
92
+ <div>
93
+ 2:Item 3
94
+ </div>
95
+ <!---->
47
96
  <button>
48
97
  Reverse
49
98
  </button>
@@ -51,20 +100,37 @@ exports[`for statements > correctly handle the index in a for...of loop 2`] = `
51
100
  </div>
52
101
  `;
53
102
 
54
- exports[`for statements > correctly handle the index in a for...of loop 3`] = `
103
+ exports[`for statements > correctly handles keyed for...of loops 2`] = `
104
+ <div>
105
+ <!---->
106
+ <div>
107
+ 0:Item 3
108
+ </div>
109
+ <div>
110
+ 1:Item 2
111
+ </div>
112
+ <div>
113
+ 2:Item 1
114
+ </div>
115
+ <!---->
116
+ <button>
117
+ Reverse
118
+ </button>
119
+
120
+ </div>
121
+ `;
122
+
123
+ exports[`for statements > correctly handles the index in a for...of loop 1`] = `
55
124
  <div>
56
125
  <div>
57
126
  <div>
58
- 0 : d
59
- </div>
60
- <div>
61
- 1 : c
127
+ 0 : a
62
128
  </div>
63
129
  <div>
64
- 2 : b
130
+ 1 : b
65
131
  </div>
66
132
  <div>
67
- 3 : a
133
+ 2 : c
68
134
  </div>
69
135
 
70
136
  </div>
@@ -78,82 +144,56 @@ exports[`for statements > correctly handle the index in a for...of loop 3`] = `
78
144
  </div>
79
145
  `;
80
146
 
81
- exports[`for statements > correctly handles intermediate statements in for block 1`] = `
147
+ exports[`for statements > correctly handles the index in a for...of loop 2`] = `
82
148
  <div>
83
149
  <div>
84
150
  <div>
85
- <div>
86
- 1
87
- </div>
88
- <div>
89
- 1
90
- </div>
151
+ 0 : a
91
152
  </div>
92
153
  <div>
93
- <div>
94
- 2
95
- </div>
96
- <div>
97
- 2
98
- </div>
154
+ 1 : b
99
155
  </div>
100
156
  <div>
101
- <div>
102
- 3
103
- </div>
104
- <div>
105
- 3
106
- </div>
157
+ 2 : c
158
+ </div>
159
+ <div>
160
+ 3 : d
107
161
  </div>
108
162
 
109
163
  </div>
110
164
  <button>
111
165
  Add Item
112
166
  </button>
167
+ <button>
168
+ Reverse
169
+ </button>
113
170
 
114
171
  </div>
115
172
  `;
116
173
 
117
- exports[`for statements > correctly handles intermediate statements in for block 2`] = `
174
+ exports[`for statements > correctly handles the index in a for...of loop 3`] = `
118
175
  <div>
119
176
  <div>
120
177
  <div>
121
- <div>
122
- 1
123
- </div>
124
- <div>
125
- 1
126
- </div>
178
+ 0 : d
127
179
  </div>
128
180
  <div>
129
- <div>
130
- 2
131
- </div>
132
- <div>
133
- 2
134
- </div>
181
+ 1 : c
135
182
  </div>
136
183
  <div>
137
- <div>
138
- 3
139
- </div>
140
- <div>
141
- 3
142
- </div>
184
+ 2 : b
143
185
  </div>
144
186
  <div>
145
- <div>
146
- 4
147
- </div>
148
- <div>
149
- 4
150
- </div>
187
+ 3 : a
151
188
  </div>
152
189
 
153
190
  </div>
154
191
  <button>
155
192
  Add Item
156
193
  </button>
194
+ <button>
195
+ Reverse
196
+ </button>
157
197
 
158
198
  </div>
159
199
  `;
@@ -341,50 +341,6 @@ describe('composite components', () => {
341
341
  component App() {
342
342
  // Ambiguous generics vs JSX / less-than parsing scenarios
343
343
 
344
- // 1. Simple "new" with generic (should NOT become an <Element>)
345
- const a = new TrackedArray<number>();
346
-
347
- // 2. Multi-line generic with newline after '<'
348
- const b = new TrackedArray<
349
- string
350
- >();
351
-
352
- // // 3. Member expression + generic
353
- // class List<T> {
354
- // items: T[];
355
- // constructor() {
356
- // this.items = [];
357
- // }
358
- // }
359
- // class Containers {
360
- // List<T>() {
361
- // return new List<T>();
362
- // }
363
- // }
364
-
365
- // const c = new Containers.List<string>();
366
-
367
- // 4. Chained call with generic method
368
- const someSource = new Array<number>(1, 2, 3);
369
- const d = someSource.map<number>((x) => x * 2).filter<boolean>((x) => !!x);
370
-
371
- // 5. Nested generics
372
- const e = new Map<string, Promise<number>>();
373
-
374
- // // 6. Generic after a call expression result
375
- // function getBuilder<T>() {
376
- // return {
377
- // build<U>() {
378
- // return {
379
- // build<V>() {
380
- // return 42;
381
- // }
382
- // };
383
- // }
384
- // };
385
- // }
386
- // const f = getBuilder()<ResultType>().build<number>();
387
-
388
344
  // // 7. Generic following optional chaining
389
345
  // const maybe = {};
390
346
  // const g = maybe?.factory<number>()?.make<boolean>();
@@ -408,7 +364,7 @@ describe('composite components', () => {
408
364
 
409
365
  // 10. JSX / Element should still work
410
366
  <div class="still-works">
411
- <span>{a.length}</span>
367
+ <span>{'Test'}</span>
412
368
  </div>
413
369
 
414
370
  // // 11. Generic function call vs Element: Identifier followed by generic args
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, TrackedArray } from 'ripple';
2
+ import { mount, flushSync, TrackedArray, track } from 'ripple';
3
3
 
4
4
  describe('for statements', () => {
5
5
  let container;
@@ -85,7 +85,7 @@ describe('for statements', () => {
85
85
  expect(container).toMatchSnapshot();
86
86
  });
87
87
 
88
- it('correctly handle the index in a for...of loop', () => {
88
+ it('correctly handles the index in a for...of loop', () => {
89
89
  component App() {
90
90
  const items = new TrackedArray('a', 'b', 'c');
91
91
 
@@ -115,4 +115,33 @@ describe('for statements', () => {
115
115
 
116
116
  expect(container).toMatchSnapshot();
117
117
  });
118
+
119
+ it('correctly handles keyed for...of loops', () => {
120
+ component App() {
121
+ let items = track([
122
+ { id: 1, text: 'Item 1' },
123
+ { id: 2, text: 'Item 2' },
124
+ { id: 3, text: 'Item 3' },
125
+ ]);
126
+
127
+ for (let item of @items; index i; key item.id) {
128
+ <div>{i + ':' + item.text}</div>
129
+ }
130
+
131
+ <button onClick={() => {
132
+ @items = @items.toReversed();
133
+ }}>{'Reverse'}</button>
134
+ }
135
+
136
+ render(App);
137
+
138
+ expect(container).toMatchSnapshot();
139
+
140
+ const button = container.querySelector('button');
141
+
142
+ button.click();
143
+ flushSync();
144
+
145
+ expect(container).toMatchSnapshot();
146
+ });
118
147
  });
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount } from 'ripple';
3
+
4
+ describe('generic patterns', () => {
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('tests simple generic function', () => {
24
+ component App() {
25
+ const e = new Map<string, Promise<number>>();
26
+ const a = new Array<number>();
27
+
28
+ const b = new Array<
29
+ string
30
+ >();
31
+
32
+ <div class="still-works">
33
+ <span>{'Test'}</span>
34
+ </div>
35
+
36
+ const someSource = new Array<number>(1, 2, 3);
37
+ const d = someSource.map<number>((x) => x * 2).filter<boolean>((x) => !!x);
38
+ }
39
+
40
+ render(App);
41
+ });
42
+
43
+ it('tests member expression)', () => {
44
+ component App() {
45
+ class List<T> {
46
+ items: T[];
47
+ constructor() {
48
+ this.items = [];
49
+ }
50
+ }
51
+
52
+ <div class="still-works">
53
+ <span>{'Test'}</span>
54
+ </div>
55
+
56
+ class Containers {
57
+ static List<T>() {
58
+ return new List<T>();
59
+ }
60
+ }
61
+
62
+ const c = Containers.List<string>();
63
+ }
64
+
65
+ render(App);
66
+ });
67
+
68
+ /**
69
+ * Complex generics tests
70
+ * These currently break the parser
71
+ * We can't just use skip and then the tests are still parsed and compiled
72
+ */
73
+ /*
74
+ describe('complex generics', () => {
75
+ it('tests after a call expression result', () => {
76
+ component App() {
77
+ function getBuilder() {
78
+ return <T>() => ({
79
+ build<U>(): { build<V>(): V; data: T; key: U } {
80
+ return {
81
+ build<V>(): V {
82
+ return 42 as V;
83
+ },
84
+ data: undefined as T,
85
+ key: undefined as U
86
+ };
87
+ }
88
+ });
89
+ }
90
+
91
+ <div class="still-works">
92
+ <span>{'Test'}</span>
93
+ </div>
94
+
95
+ type ResultType = string;
96
+
97
+ const f = getBuilder()<ResultType>().build<number>();
98
+ }
99
+ });
100
+ });
101
+ */
102
+ });