ripple 0.2.152 → 0.2.154

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.
Files changed (67) hide show
  1. package/README.md +3 -3
  2. package/package.json +5 -5
  3. package/src/compiler/phases/1-parse/index.js +1 -1
  4. package/src/compiler/phases/3-transform/client/index.js +37 -16
  5. package/src/compiler/phases/3-transform/server/index.js +43 -25
  6. package/src/runtime/internal/client/events.js +5 -1
  7. package/src/runtime/internal/client/index.js +2 -1
  8. package/src/runtime/internal/client/render.js +18 -15
  9. package/src/runtime/internal/client/runtime.js +75 -10
  10. package/src/runtime/internal/server/index.js +51 -11
  11. package/src/server/index.js +1 -1
  12. package/tests/client/array/array.derived.test.ripple +61 -33
  13. package/tests/client/array/array.iteration.test.ripple +3 -1
  14. package/tests/client/array/array.mutations.test.ripple +19 -15
  15. package/tests/client/array/array.static.test.ripple +115 -104
  16. package/tests/client/array/array.to-methods.test.ripple +3 -3
  17. package/tests/client/basic/basic.attributes.test.ripple +110 -57
  18. package/tests/client/basic/basic.collections.test.ripple +41 -22
  19. package/tests/client/basic/basic.errors.test.ripple +12 -6
  20. package/tests/client/basic/basic.events.test.ripple +51 -33
  21. package/tests/client/basic/basic.reactivity.test.ripple +120 -56
  22. package/tests/client/basic/basic.rendering.test.ripple +49 -19
  23. package/tests/client/basic/basic.styling.test.ripple +2 -2
  24. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  25. package/tests/client/boundaries.test.ripple +70 -58
  26. package/tests/client/compiler/compiler.assignments.test.ripple +32 -4
  27. package/tests/client/compiler/compiler.attributes.test.ripple +46 -46
  28. package/tests/client/compiler/compiler.basic.test.ripple +18 -15
  29. package/tests/client/compiler/compiler.tracked-access.test.ripple +53 -42
  30. package/tests/client/compiler/compiler.typescript.test.ripple +1 -2
  31. package/tests/client/composite/composite.dynamic-components.test.ripple +6 -6
  32. package/tests/client/composite/composite.generics.test.ripple +39 -36
  33. package/tests/client/composite/composite.props.test.ripple +4 -3
  34. package/tests/client/composite/composite.reactivity.test.ripple +112 -27
  35. package/tests/client/composite/composite.render.test.ripple +9 -8
  36. package/tests/client/computed-properties.test.ripple +24 -24
  37. package/tests/client/context.test.ripple +11 -9
  38. package/tests/client/date.test.ripple +3 -1
  39. package/tests/client/dynamic-elements.test.ripple +103 -78
  40. package/tests/client/for.test.ripple +27 -17
  41. package/tests/client/head.test.ripple +42 -6
  42. package/tests/client/html.test.ripple +42 -32
  43. package/tests/client/input-value.test.ripple +4 -4
  44. package/tests/client/map.test.ripple +140 -141
  45. package/tests/client/media-query.test.ripple +31 -31
  46. package/tests/client/object.test.ripple +148 -112
  47. package/tests/client/portal.test.ripple +29 -15
  48. package/tests/client/ref.test.ripple +9 -3
  49. package/tests/client/set.test.ripple +111 -111
  50. package/tests/client/tracked-expression.test.ripple +16 -17
  51. package/tests/client/url/url.derived.test.ripple +19 -9
  52. package/tests/client/url/url.parsing.test.ripple +24 -8
  53. package/tests/client/url/url.partial-removal.test.ripple +12 -4
  54. package/tests/client/url/url.reactivity.test.ripple +63 -25
  55. package/tests/client/url/url.serialization.test.ripple +18 -6
  56. package/tests/client/url-search-params/url-search-params.derived.test.ripple +10 -6
  57. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +3 -1
  58. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +26 -14
  59. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +3 -1
  60. package/tests/server/await.test.ripple +23 -22
  61. package/tests/server/basic.test.ripple +1 -1
  62. package/tests/server/compiler.test.ripple +3 -7
  63. package/tests/server/composite.test.ripple +38 -36
  64. package/tests/server/for.test.ripple +9 -5
  65. package/tests/server/if.test.ripple +1 -1
  66. package/tests/server/streaming-ssr.test.ripple +67 -0
  67. package/types/server.d.ts +5 -4
@@ -24,7 +24,7 @@ describe('use value()', () => {
24
24
  expect(logs).toEqual(['text changed', '', 'text changed', 'Hello']);
25
25
  });
26
26
 
27
- it('should update value on input with a predefined value', () => {
27
+ it('should update value on input with a predefined value', () => {
28
28
  const logs: string[] = [];
29
29
 
30
30
  component App() {
@@ -83,9 +83,9 @@ describe('use value()', () => {
83
83
  });
84
84
 
85
85
  <select {ref bindValue(select)}>
86
- <option value="1">{"One"}</option>
87
- <option value="2">{"Two"}</option>
88
- <option value="3">{"Three"}</option>
86
+ <option value="1">{'One'}</option>
87
+ <option value="2">{'Two'}</option>
88
+ <option value="3">{'Three'}</option>
89
89
  </select>
90
90
  }
91
91
 
@@ -1,203 +1,202 @@
1
1
  import { flushSync, TrackedMap, track } from 'ripple';
2
2
 
3
3
  describe('TrackedMap', () => {
4
- it('handles set with update and delete operations with a reactive size var', () => {
5
- component MapTest() {
6
- let map = new TrackedMap([['a', 1], ['b', 2], ['c', 3]]);
7
- let value = track(() => map.get('a'));
8
- let size = track(() => map.size);
4
+ it('handles set with update and delete operations with a reactive size var', () => {
5
+ component MapTest() {
6
+ let map = new TrackedMap([['a', 1], ['b', 2], ['c', 3]]);
7
+ let value = track(() => map.get('a'));
8
+ let size = track(() => map.size);
9
9
 
10
- <button onClick={() => map.set('d', 4)}>{'set'}</button>
11
- <button onClick={() => map.delete('b')}>{'delete'}</button>
12
- <button onClick={() => map.set('a', 5)}>{'update'}</button>
10
+ <button onClick={() => map.set('d', 4)}>{'set'}</button>
11
+ <button onClick={() => map.delete('b')}>{'delete'}</button>
12
+ <button onClick={() => map.set('a', 5)}>{'update'}</button>
13
13
 
14
- <pre>{map.get('d')}</pre>
15
- <pre>{@size}</pre>
16
- <pre>{@value}</pre>
17
- }
14
+ <pre>{map.get('d')}</pre>
15
+ <pre>{@size}</pre>
16
+ <pre>{@value}</pre>
17
+ }
18
18
 
19
- render(MapTest);
19
+ render(MapTest);
20
20
 
21
- const setButton = container.querySelectorAll('button')[0];
22
- const deleteButton = container.querySelectorAll('button')[1];
23
- const updateButton = container.querySelectorAll('button')[2];
21
+ const setButton = container.querySelectorAll('button')[0];
22
+ const deleteButton = container.querySelectorAll('button')[1];
23
+ const updateButton = container.querySelectorAll('button')[2];
24
24
 
25
- setButton.click();
26
- flushSync();
25
+ setButton.click();
26
+ flushSync();
27
27
 
28
- expect(container.querySelectorAll('pre')[0].textContent).toBe('4');
29
- expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
30
- expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
28
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('4');
29
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
30
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
31
31
 
32
- deleteButton.click();
33
- flushSync();
32
+ deleteButton.click();
33
+ flushSync();
34
34
 
35
- expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
35
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
36
36
 
37
- updateButton.click();
38
- flushSync();
37
+ updateButton.click();
38
+ flushSync();
39
39
 
40
- expect(container.querySelectorAll('pre')[2].textContent).toBe('5');
41
- });
40
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('5');
41
+ });
42
42
 
43
- it('handles clear operation', () => {
44
- component MapTest() {
45
- let map = new TrackedMap([['a', 1], ['b', 2], ['c', 3]]);
43
+ it('handles clear operation', () => {
44
+ component MapTest() {
45
+ let map = new TrackedMap([['a', 1], ['b', 2], ['c', 3]]);
46
46
 
47
- <button onClick={() => map.clear()}>{'clear'}</button>
48
- <pre>{map.size}</pre>
49
- }
47
+ <button onClick={() => map.clear()}>{'clear'}</button>
48
+ <pre>{map.size}</pre>
49
+ }
50
50
 
51
- render(MapTest);
51
+ render(MapTest);
52
52
 
53
- const clearButton = container.querySelector('button');
53
+ const clearButton = container.querySelector('button');
54
54
 
55
- clearButton.click();
56
- flushSync();
55
+ clearButton.click();
56
+ flushSync();
57
57
 
58
- expect(container.querySelector('pre').textContent).toBe('0');
59
- });
58
+ expect(container.querySelector('pre').textContent).toBe('0');
59
+ });
60
60
 
61
- it('handles has operation and tracks reactive $has', () => {
62
- component MapTest() {
63
- let map = new TrackedMap([['a', 1], ['b', 2], ['c', 3]]);
64
- let has = track(() => map.has('b'));
61
+ it('handles has operation and tracks reactive $has', () => {
62
+ component MapTest() {
63
+ let map = new TrackedMap([['a', 1], ['b', 2], ['c', 3]]);
64
+ let has = track(() => map.has('b'));
65
65
 
66
- <button onClick={() => map.delete('b')}>{'delete'}</button>
67
- <pre>{@has}</pre>
68
- }
66
+ <button onClick={() => map.delete('b')}>{'delete'}</button>
67
+ <pre>{@has}</pre>
68
+ }
69
69
 
70
- render(MapTest);
70
+ render(MapTest);
71
71
 
72
- const deleteButton = container.querySelector('button');
73
- expect(container.querySelector('pre').textContent).toBe('true');
72
+ const deleteButton = container.querySelector('button');
73
+ expect(container.querySelector('pre').textContent).toBe('true');
74
74
 
75
- deleteButton.click();
76
- flushSync();
75
+ deleteButton.click();
76
+ flushSync();
77
77
 
78
- expect(container.querySelector('pre').textContent).toBe('false');
79
- });
78
+ expect(container.querySelector('pre').textContent).toBe('false');
79
+ });
80
80
 
81
- it('handles reactivity of keys, values, and entries', () => {
82
- component MapTest() {
83
- let map = new TrackedMap([['x', 10], ['y', 20]]);
84
- let keys = track(() => Array.from(map.keys()));
85
- let values = track(() => Array.from(map.values()));
86
- let entries = track(() => Array.from(map.entries()));
81
+ it('handles reactivity of keys, values, and entries', () => {
82
+ component MapTest() {
83
+ let map = new TrackedMap([['x', 10], ['y', 20]]);
84
+ let keys = track(() => Array.from(map.keys()));
85
+ let values = track(() => Array.from(map.values()));
86
+ let entries = track(() => Array.from(map.entries()));
87
87
 
88
- <button onClick={() => map.delete('x')}>{'delete'}</button>
88
+ <button onClick={() => map.delete('x')}>{'delete'}</button>
89
89
 
90
- <pre>{JSON.stringify(@keys)}</pre>
91
- <pre>{JSON.stringify(@values)}</pre>
92
- <pre>{JSON.stringify(@entries)}</pre>
93
- }
90
+ <pre>{JSON.stringify(@keys)}</pre>
91
+ <pre>{JSON.stringify(@values)}</pre>
92
+ <pre>{JSON.stringify(@entries)}</pre>
93
+ }
94
94
 
95
- render(MapTest);
95
+ render(MapTest);
96
96
 
97
- const deleteButton = container.querySelector('button');
97
+ const deleteButton = container.querySelector('button');
98
98
 
99
- expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
100
- expect(container.querySelectorAll('pre')[1].textContent).toBe('[10,20]');
101
- expect(container.querySelectorAll('pre')[2].textContent).toBe('[["x",10],["y",20]]');
99
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
100
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('[10,20]');
101
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('[["x",10],["y",20]]');
102
102
 
103
- deleteButton.click();
104
- flushSync();
103
+ deleteButton.click();
104
+ flushSync();
105
105
 
106
- expect(container.querySelectorAll('pre')[0].textContent).toBe('["y"]');
107
- expect(container.querySelectorAll('pre')[1].textContent).toBe('[20]');
108
- expect(container.querySelectorAll('pre')[2].textContent).toBe('[["y",20]]');
109
- });
106
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["y"]');
107
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('[20]');
108
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('[["y",20]]');
109
+ });
110
110
 
111
- it('toJSON returns correct object', () => {
112
- component MapTest() {
113
- let map = new TrackedMap([['foo', 1], ['bar', 2]]);
111
+ it('toJSON returns correct object', () => {
112
+ component MapTest() {
113
+ let map = new TrackedMap([['foo', 1], ['bar', 2]]);
114
114
 
115
- <pre>{JSON.stringify(map)}</pre>
116
- }
115
+ <pre>{JSON.stringify(map)}</pre>
116
+ }
117
117
 
118
- render(MapTest);
118
+ render(MapTest);
119
119
 
120
- expect(container.querySelectorAll('pre')[0].textContent).toBe('[["foo",1],["bar",2]]');
121
- });
120
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[["foo",1],["bar",2]]');
121
+ });
122
122
 
123
- it('creates empty TrackedMap using #Map() shorthand syntax', () => {
124
- component MapTest() {
125
- let map = #Map();
123
+ it('creates empty TrackedMap using #Map() shorthand syntax', () => {
124
+ component MapTest() {
125
+ let map = #Map();
126
126
 
127
- <button onClick={() => map.set('a', 1)}>{'add'}</button>
128
- <pre>{map.size}</pre>
129
- }
127
+ <button onClick={() => map.set('a', 1)}>{'add'}</button>
128
+ <pre>{map.size}</pre>
129
+ }
130
130
 
131
- render(MapTest);
131
+ render(MapTest);
132
132
 
133
- expect(container.querySelector('pre').textContent).toBe('0');
133
+ expect(container.querySelector('pre').textContent).toBe('0');
134
134
 
135
- const addButton = container.querySelector('button');
136
- addButton.click();
137
- flushSync();
135
+ const addButton = container.querySelector('button');
136
+ addButton.click();
137
+ flushSync();
138
138
 
139
- expect(container.querySelector('pre').textContent).toBe('1');
140
- });
139
+ expect(container.querySelector('pre').textContent).toBe('1');
140
+ });
141
141
 
142
+ it('creates TrackedMap with initial entries using #Map() shorthand syntax', () => {
143
+ component MapTest() {
144
+ let map = #Map([['a', 1], ['b', 2], ['c', 3]]);
145
+ let value = track(() => map.get('b'));
142
146
 
143
- it('creates TrackedMap with initial entries using #Map() shorthand syntax', () => {
144
- component MapTest() {
145
- let map = #Map([['a', 1], ['b', 2], ['c', 3]]);
146
- let value = track(() => map.get('b'));
147
+ <button onClick={() => map.set('b', 10)}>{'update'}</button>
148
+ <pre>{map.size}</pre>
149
+ <pre>{@value}</pre>
150
+ }
147
151
 
148
- <button onClick={() => map.set('b', 10)}>{'update'}</button>
149
- <pre>{map.size}</pre>
150
- <pre>{@value}</pre>
151
- }
152
+ render(MapTest);
152
153
 
153
- render(MapTest);
154
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('3');
155
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
154
156
 
155
- expect(container.querySelectorAll('pre')[0].textContent).toBe('3');
156
- expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
157
+ const updateButton = container.querySelector('button');
158
+ updateButton.click();
159
+ flushSync();
157
160
 
158
- const updateButton = container.querySelector('button');
159
- updateButton.click();
160
- flushSync();
161
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('10');
162
+ });
161
163
 
162
- expect(container.querySelectorAll('pre')[1].textContent).toBe('10');
163
- });
164
+ it('handles all operations with #Map() shorthand syntax', () => {
165
+ component MapTest() {
166
+ let map = #Map([['x', 100], ['y', 200]]);
167
+ let keys = track(() => Array.from(map.keys()));
164
168
 
165
- it('handles all operations with #Map() shorthand syntax', () => {
166
- component MapTest() {
167
- let map = #Map([['x', 100], ['y', 200]]);
168
- let keys = track(() => Array.from(map.keys()));
169
+ <button onClick={() => map.set('z', 300)}>{'add'}</button>
170
+ <button onClick={() => map.delete('x')}>{'delete'}</button>
171
+ <button onClick={() => map.clear()}>{'clear'}</button>
169
172
 
170
- <button onClick={() => map.set('z', 300)}>{'add'}</button>
171
- <button onClick={() => map.delete('x')}>{'delete'}</button>
172
- <button onClick={() => map.clear()}>{'clear'}</button>
173
+ <pre>{JSON.stringify(@keys)}</pre>
174
+ <pre>{map.size}</pre>
175
+ }
173
176
 
174
- <pre>{JSON.stringify(@keys)}</pre>
175
- <pre>{map.size}</pre>
176
- }
177
+ render(MapTest);
177
178
 
178
- render(MapTest);
179
+ const [addButton, deleteButton, clearButton] = container.querySelectorAll('button');
179
180
 
180
- const [addButton, deleteButton, clearButton] = container.querySelectorAll('button');
181
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
182
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
181
183
 
182
- expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
183
- expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
184
+ addButton.click();
185
+ flushSync();
184
186
 
185
- addButton.click();
186
- flushSync();
187
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y","z"]');
188
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
187
189
 
188
- expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y","z"]');
189
- expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
190
+ deleteButton.click();
191
+ flushSync();
190
192
 
191
- deleteButton.click();
192
- flushSync();
193
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["y","z"]');
194
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
193
195
 
194
- expect(container.querySelectorAll('pre')[0].textContent).toBe('["y","z"]');
195
- expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
196
+ clearButton.click();
197
+ flushSync();
196
198
 
197
- clearButton.click();
198
- flushSync();
199
-
200
- expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
201
- expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
202
- });
199
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
200
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
201
+ });
203
202
  });
@@ -3,43 +3,45 @@ import { flushSync, MediaQuery, track } from 'ripple';
3
3
  type Callback = (event: MediaQueryListEvent) => void;
4
4
 
5
5
  function setupMatchMedia() {
6
- let listeners = new Set<Callback>();
7
-
8
- // A mock implementation of matchMedia
9
- const mockMatchMedia = vi.fn().mockImplementation(query => {
10
- return {
11
- media: query,
12
- matches: false, // default value
13
- addEventListener: (type: string, cb: Callback) => {
14
- if (type === 'change') listeners.add(cb);
15
- },
16
- removeEventListener: (type: string, cb: Callback) => {
17
- if (type === 'change') listeners.delete(cb);
18
- },
19
- /** @param {function(MediaQueryListEvent): void} cb */
20
- addListener: (cb: Callback) => listeners.add(cb),
21
- /** @param {function(MediaQueryListEvent): void} cb */
22
- removeListener: (cb: Callback) => listeners.delete(cb),
23
- dispatch: (event: MediaQueryListEvent) => {
24
- listeners.forEach((cb) => cb(event));
25
- },
6
+ let listeners = new Set<Callback>();
7
+
8
+ // A mock implementation of matchMedia
9
+ const mockMatchMedia = vi.fn().mockImplementation((query) => {
10
+ return {
11
+ media: query,
12
+ matches: false, // default value
13
+ addEventListener: (type: string, cb: Callback) => {
14
+ if (type === 'change') listeners.add(cb);
15
+ },
16
+ removeEventListener: (type: string, cb: Callback) => {
17
+ if (type === 'change') listeners.delete(cb);
18
+ },
19
+
20
+ /** @param {function(MediaQueryListEvent): void} cb */
21
+ addListener: (cb: Callback) => listeners.add(cb),
22
+
23
+ /** @param {function(MediaQueryListEvent): void} cb */
24
+ removeListener: (cb: Callback) => listeners.delete(cb),
25
+ dispatch: (event: MediaQueryListEvent) => {
26
+ listeners.forEach((cb) => cb(event));
27
+ },
26
28
  listenersCount: () => listeners.size,
27
- };
28
- });
29
+ };
30
+ });
29
31
 
30
- Object.defineProperty(window, 'matchMedia', {
31
- writable: true,
32
- value: mockMatchMedia
33
- });
32
+ Object.defineProperty(window, 'matchMedia', {
33
+ writable: true,
34
+ value: mockMatchMedia,
35
+ });
34
36
 
35
- return { mockMatchMedia, listeners };
37
+ return { mockMatchMedia, listeners };
36
38
  }
37
39
 
38
40
  describe('MediaQuery', () => {
39
41
  let mm: ReturnType<typeof setupMatchMedia>;
40
42
 
41
43
  beforeEach(() => {
42
- mm = setupMatchMedia();
44
+ mm = setupMatchMedia();
43
45
  });
44
46
 
45
47
  it('should be reactive if matchMedia changes', () => {
@@ -62,7 +64,6 @@ describe('MediaQuery', () => {
62
64
  const p = container.querySelector('p');
63
65
  expect(p.textContent).toBe('false');
64
66
 
65
-
66
67
  mm.mockMatchMedia.mock.results[0].value.matches = true;
67
68
  mm.mockMatchMedia.mock.results[0].value.dispatch(event);
68
69
  flushSync();
@@ -78,7 +79,6 @@ describe('MediaQuery', () => {
78
79
  expect(p.textContent).toBe('false');
79
80
  });
80
81
 
81
-
82
82
  it('should have cleared event listeners after unmount', async () => {
83
83
  const media = '(min-width: 600px)';
84
84
 
@@ -110,7 +110,7 @@ describe('MediaQuery', () => {
110
110
  flushSync();
111
111
 
112
112
  // wait for microtask queue to flush
113
- await new Promise(resolve => setTimeout(resolve, 0));
113
+ await new Promise((resolve) => setTimeout(resolve, 0));
114
114
 
115
115
  expect(mm.mockMatchMedia.mock.results[0].value.listenersCount()).toBe(0);
116
116
  });