ripple 0.3.128 → 0.4.0

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 (192) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/package.json +10 -4
  3. package/src/runtime/element.js +23 -6
  4. package/src/runtime/index-client.js +81 -41
  5. package/src/runtime/index-server.js +1 -0
  6. package/src/runtime/internal/client/blocks.js +173 -91
  7. package/src/runtime/internal/client/component.js +4 -30
  8. package/src/runtime/internal/client/composite.js +12 -13
  9. package/src/runtime/internal/client/constants.js +9 -0
  10. package/src/runtime/internal/client/context.js +12 -19
  11. package/src/runtime/internal/client/css.js +7 -9
  12. package/src/runtime/internal/client/events.js +178 -90
  13. package/src/runtime/internal/client/expression.js +45 -25
  14. package/src/runtime/internal/client/for.js +409 -129
  15. package/src/runtime/internal/client/head.js +10 -5
  16. package/src/runtime/internal/client/html.js +1 -0
  17. package/src/runtime/internal/client/hydration.js +152 -10
  18. package/src/runtime/internal/client/if.js +239 -50
  19. package/src/runtime/internal/client/index.js +4 -12
  20. package/src/runtime/internal/client/operations.js +40 -61
  21. package/src/runtime/internal/client/portal.js +163 -85
  22. package/src/runtime/internal/client/render.js +39 -39
  23. package/src/runtime/internal/client/rpc.js +3 -3
  24. package/src/runtime/internal/client/runtime.js +592 -433
  25. package/src/runtime/internal/client/selector.js +5 -2
  26. package/src/runtime/internal/client/template.js +133 -158
  27. package/src/runtime/internal/client/transport.js +38 -0
  28. package/src/runtime/internal/client/try.js +465 -426
  29. package/src/runtime/internal/client/types.d.ts +56 -23
  30. package/src/runtime/internal/client/utils.js +1 -3
  31. package/src/runtime/internal/server/dynamic.js +20 -21
  32. package/src/runtime/internal/server/index.js +325 -275
  33. package/src/runtime/internal/server/rpc.js +3 -2
  34. package/src/runtime/internal/server/transport.js +16 -0
  35. package/src/runtime/transport.js +21 -0
  36. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
  37. package/tests/client/array/array.copy-within.test.tsrx +12 -12
  38. package/tests/client/array/array.derived.test.tsrx +46 -46
  39. package/tests/client/array/array.iteration.test.tsrx +10 -10
  40. package/tests/client/array/array.mutations.test.tsrx +20 -20
  41. package/tests/client/array/array.static.test.tsrx +11 -11
  42. package/tests/client/array/array.to-methods.test.tsrx +6 -6
  43. package/tests/client/async-suspend.test.tsrx +74 -75
  44. package/tests/client/basic/basic.attributes.test.tsrx +86 -83
  45. package/tests/client/basic/basic.collections.test.tsrx +12 -12
  46. package/tests/client/basic/basic.components.test.tsrx +99 -25
  47. package/tests/client/basic/basic.errors.test.tsrx +18 -18
  48. package/tests/client/basic/basic.events.test.tsrx +42 -42
  49. package/tests/client/basic/basic.get-set.test.tsrx +8 -8
  50. package/tests/client/basic/basic.reactivity.test.tsrx +73 -38
  51. package/tests/client/basic/basic.rendering.test.tsrx +25 -25
  52. package/tests/client/basic/basic.utilities.test.tsrx +3 -3
  53. package/tests/client/boundaries.test.tsrx +8 -8
  54. package/tests/client/boxed-locals.test.tsrx +452 -0
  55. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
  56. package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
  57. package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
  58. package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
  59. package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
  60. package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
  61. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
  62. package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
  63. package/tests/client/composite/composite.props.test.tsrx +149 -12
  64. package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
  65. package/tests/client/composite/composite.render.test.tsrx +26 -9
  66. package/tests/client/css/scoped-styles.test.tsrx +8 -8
  67. package/tests/client/date.test.tsrx +42 -42
  68. package/tests/client/derived-release.test.tsrx +370 -0
  69. package/tests/client/dynamic-elements.test.tsrx +45 -45
  70. package/tests/client/events.test.tsrx +97 -71
  71. package/tests/client/for-selector.test.tsrx +14 -14
  72. package/tests/client/for-single-root-items.test.tsrx +118 -3
  73. package/tests/client/for.test.tsrx +332 -31
  74. package/tests/client/head.test.tsrx +19 -19
  75. package/tests/client/html.test.tsrx +11 -9
  76. package/tests/client/if-block.test.tsrx +80 -0
  77. package/tests/client/map.test.tsrx +16 -16
  78. package/tests/client/media-query.test.tsrx +7 -7
  79. package/tests/client/portal.test.tsrx +224 -26
  80. package/tests/client/primitive-text.test.tsrx +50 -0
  81. package/tests/client/prop-kind-switch.test.tsrx +141 -0
  82. package/tests/client/ref.test.tsrx +17 -17
  83. package/tests/client/return.test.tsrx +26 -3
  84. package/tests/client/root-boundary.test.tsrx +42 -0
  85. package/tests/client/scoped-flush.test.tsrx +8 -8
  86. package/tests/client/set.test.tsrx +6 -6
  87. package/tests/client/svg.test.tsrx +11 -11
  88. package/tests/client/switch.test.tsrx +44 -44
  89. package/tests/client/track-async-hydration.test.tsrx +4 -4
  90. package/tests/client/try.test.tsrx +151 -83
  91. package/tests/client/tsx.test.tsrx +43 -40
  92. package/tests/client/url/url.derived.test.tsrx +6 -6
  93. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
  95. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
  96. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
  97. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
  98. package/tests/hydration/basic.test.js +74 -1
  99. package/tests/hydration/build-components.js +22 -1
  100. package/tests/hydration/compiled/client/basic.js +1142 -713
  101. package/tests/hydration/compiled/client/composite.js +151 -100
  102. package/tests/hydration/compiled/client/events.js +184 -171
  103. package/tests/hydration/compiled/client/for.js +1472 -1132
  104. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  105. package/tests/hydration/compiled/client/head.js +254 -201
  106. package/tests/hydration/compiled/client/hmr.js +54 -41
  107. package/tests/hydration/compiled/client/html-in-template.js +54 -39
  108. package/tests/hydration/compiled/client/html.js +1633 -1263
  109. package/tests/hydration/compiled/client/if-children.js +420 -270
  110. package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
  111. package/tests/hydration/compiled/client/if.js +229 -219
  112. package/tests/hydration/compiled/client/mixed-control-flow.js +420 -404
  113. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
  114. package/tests/hydration/compiled/client/portal.js +87 -95
  115. package/tests/hydration/compiled/client/reactivity.js +135 -101
  116. package/tests/hydration/compiled/client/return.js +28 -19
  117. package/tests/hydration/compiled/client/streaming.js +390 -306
  118. package/tests/hydration/compiled/client/switch.js +258 -266
  119. package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
  120. package/tests/hydration/compiled/client/try.js +173 -129
  121. package/tests/hydration/compiled/client/typed-text.js +71 -0
  122. package/tests/hydration/compiled/fixtures/money.js +10 -0
  123. package/tests/hydration/compiled/server/basic.js +123 -6
  124. package/tests/hydration/compiled/server/composite.js +16 -30
  125. package/tests/hydration/compiled/server/events.js +66 -18
  126. package/tests/hydration/compiled/server/for.js +117 -41
  127. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  128. package/tests/hydration/compiled/server/head.js +20 -20
  129. package/tests/hydration/compiled/server/hmr.js +2 -2
  130. package/tests/hydration/compiled/server/html.js +70 -238
  131. package/tests/hydration/compiled/server/if-children.js +107 -15
  132. package/tests/hydration/compiled/server/if.js +11 -15
  133. package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
  134. package/tests/hydration/compiled/server/portal.js +14 -22
  135. package/tests/hydration/compiled/server/reactivity.js +14 -14
  136. package/tests/hydration/compiled/server/streaming.js +19 -19
  137. package/tests/hydration/compiled/server/switch.js +8 -8
  138. package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
  139. package/tests/hydration/compiled/server/try.js +8 -8
  140. package/tests/hydration/compiled/server/typed-text.js +17 -0
  141. package/tests/hydration/components/basic.tsrx +44 -8
  142. package/tests/hydration/components/composite.tsrx +2 -2
  143. package/tests/hydration/components/events.tsrx +25 -25
  144. package/tests/hydration/components/for.tsrx +115 -67
  145. package/tests/hydration/components/fragment-cursor.tsrx +650 -0
  146. package/tests/hydration/components/head.tsrx +16 -16
  147. package/tests/hydration/components/hmr.tsrx +2 -2
  148. package/tests/hydration/components/html.tsrx +7 -7
  149. package/tests/hydration/components/if-children.tsrx +72 -25
  150. package/tests/hydration/components/if.tsrx +18 -18
  151. package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
  152. package/tests/hydration/components/portal.tsrx +3 -3
  153. package/tests/hydration/components/reactivity.tsrx +16 -16
  154. package/tests/hydration/components/streaming.tsrx +20 -20
  155. package/tests/hydration/components/switch.tsrx +20 -20
  156. package/tests/hydration/components/text-types.ts +4 -0
  157. package/tests/hydration/components/track-async-serialization.tsrx +80 -26
  158. package/tests/hydration/components/try.tsrx +8 -8
  159. package/tests/hydration/components/typed-text.tsrx +22 -0
  160. package/tests/hydration/fixtures/money.js +10 -0
  161. package/tests/hydration/for.test.js +36 -0
  162. package/tests/hydration/fragment-cursor.test.js +111 -0
  163. package/tests/hydration/if-children.test.js +24 -0
  164. package/tests/hydration/streaming.test.js +19 -0
  165. package/tests/hydration/track-async-serialization.test.js +87 -2
  166. package/tests/hydration/tsconfig.text-types.json +10 -0
  167. package/tests/hydration/typed-text.test.js +34 -0
  168. package/tests/server/await.test.tsrx +3 -3
  169. package/tests/server/basic.attributes.test.tsrx +42 -42
  170. package/tests/server/basic.components.test.tsrx +64 -8
  171. package/tests/server/basic.test.tsrx +34 -32
  172. package/tests/server/compiler.test.tsrx +1 -70
  173. package/tests/server/composite.props.test.tsrx +6 -6
  174. package/tests/server/dynamic-elements.test.tsrx +31 -8
  175. package/tests/server/for.test.tsrx +94 -0
  176. package/tests/server/head.test.tsrx +11 -11
  177. package/tests/server/scoped-styles.test.tsrx +2 -2
  178. package/tests/server/streaming-ssr.test.tsrx +48 -43
  179. package/tests/server/track-async-serialization.test.tsrx +204 -21
  180. package/tests/server/try.test.tsrx +48 -48
  181. package/tests/types/transport.ts +40 -0
  182. package/tests/utils/vite-plugin-config.test.js +23 -0
  183. package/types/index.d.ts +48 -7
  184. package/types/server.d.ts +3 -0
  185. package/types/transport.d.ts +15 -0
  186. package/src/runtime/internal/client/switch.js +0 -113
  187. package/src/utils/errors.js +0 -13
  188. package/tests/client/lazy-array.test.tsrx +0 -31
  189. package/tests/client/lazy-destructuring.test.tsrx +0 -438
  190. package/tests/client/tracked-index-access.test.tsrx +0 -113
  191. package/tests/server/lazy-destructuring.test.tsrx +0 -595
  192. package/tests/server/tracked-index-access.test.tsrx +0 -76
@@ -0,0 +1,452 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flushSync, track, trackAsync } from 'ripple';
3
+
4
+ // A plain `let` that is written after its declaration (reassigned in the
5
+ // source, or assigned by a `ref` setter) and read by template code is
6
+ // compiled to a `{ v }` box, so the module-level functions the compiler
7
+ // hoists (`@if` and `@switch` conditions and branches, render blocks) share
8
+ // the variable with the component exactly as a closure would. None of these
9
+ // variables is reactive: the update is always triggered by a tracked value,
10
+ // and the box only has to make sure the hoisted code sees the latest value.
11
+
12
+ describe('boxed locals', () => {
13
+ it('a reassigned let read by an @if condition is current on re-evaluation', () => {
14
+ let disable: () => void = () => {};
15
+ let bump: () => void = () => {};
16
+
17
+ function App() @{
18
+ const count = track(0);
19
+ let enabled = true;
20
+ disable = () => {
21
+ enabled = false;
22
+ };
23
+ bump = () => {
24
+ count.value++;
25
+ };
26
+ @if (enabled && count.value > 0) {
27
+ <span>on</span>
28
+ } @else {
29
+ <span>off</span>
30
+ }
31
+ }
32
+
33
+ render(App);
34
+ const text = () => container.querySelector('span')?.textContent;
35
+ expect(text()).toBe('off');
36
+
37
+ bump();
38
+ flushSync();
39
+ expect(text()).toBe('on');
40
+
41
+ disable();
42
+ bump();
43
+ flushSync();
44
+ expect(text()).toBe('off');
45
+ });
46
+
47
+ it('a reassigned let read inside an @if branch is current when the branch renders', () => {
48
+ let rename: () => void = () => {};
49
+ let toggle: () => void = () => {};
50
+
51
+ function App() @{
52
+ const show = track(false);
53
+ let title = 'first';
54
+ rename = () => {
55
+ title = 'second';
56
+ };
57
+ toggle = () => {
58
+ show.value = !show.value;
59
+ };
60
+ @if (show.value) {
61
+ <p>{title}</p>
62
+ }
63
+ }
64
+
65
+ render(App);
66
+ expect(container.querySelector('p')).toBeNull();
67
+
68
+ rename();
69
+ toggle();
70
+ flushSync();
71
+ expect(container.querySelector('p')?.textContent).toBe('second');
72
+ });
73
+
74
+ it('a reassigned let read by a text expression is current on re-evaluation', () => {
75
+ let rename: () => void = () => {};
76
+ let bump: () => void = () => {};
77
+
78
+ function App() @{
79
+ const count = track(0);
80
+ let unit = 'ms';
81
+ rename = () => {
82
+ unit = 's';
83
+ };
84
+ bump = () => {
85
+ count.value++;
86
+ };
87
+ <p>{count.value + unit}</p>
88
+ }
89
+
90
+ render(App);
91
+ const text = () => container.querySelector('p')?.textContent;
92
+ expect(text()).toBe('0ms');
93
+
94
+ rename();
95
+ bump();
96
+ flushSync();
97
+ expect(text()).toBe('1s');
98
+ });
99
+
100
+ it(
101
+ 'a let written by a handler inside a hoisted @if branch updates the component variable',
102
+ () => {
103
+ let read: () => number = () => -1;
104
+
105
+ function App() @{
106
+ const show = track(true);
107
+ const tick = track(0);
108
+ let clicks = 0;
109
+ read = () => clicks;
110
+ @if (show.value) {
111
+ <button onClick={() => clicks++}>{'hit ' + tick.value}</button>
112
+ }
113
+ }
114
+
115
+ render(App);
116
+ expect(read()).toBe(0);
117
+
118
+ container.querySelector('button')?.click();
119
+ container.querySelector('button')?.click();
120
+ expect(read()).toBe(2);
121
+ },
122
+ );
123
+
124
+ it('a let assigned by a ref inside a hoisted @if branch is the element, then null', () => {
125
+ let div: HTMLDivElement | null | undefined;
126
+ let hide: () => void = () => {};
127
+
128
+ function App() @{
129
+ const show = track(true);
130
+ const label = track('a');
131
+ hide = () => {
132
+ show.value = false;
133
+ };
134
+ @if (show.value) {
135
+ <div ref={div}>{label.value}</div>
136
+ }
137
+ }
138
+
139
+ render(App);
140
+ flushSync();
141
+ expect(div).toBeInstanceOf(HTMLDivElement);
142
+
143
+ hide();
144
+ flushSync();
145
+ expect(div).toBeNull();
146
+ });
147
+
148
+ it('supports compound assignment and update expressions on a boxed let', () => {
149
+ let step: () => void = () => {};
150
+ let bump: () => void = () => {};
151
+
152
+ function App() @{
153
+ const count = track(0);
154
+ let base = 10;
155
+ step = () => {
156
+ base += 5;
157
+ base++;
158
+ base = base * 2;
159
+ };
160
+ bump = () => {
161
+ count.value++;
162
+ };
163
+ <span>{base + count.value}</span>
164
+ }
165
+
166
+ render(App);
167
+ const text = () => container.querySelector('span')?.textContent;
168
+ expect(text()).toBe('10');
169
+
170
+ step();
171
+ bump();
172
+ flushSync();
173
+ expect(text()).toBe('33');
174
+ });
175
+
176
+ it('boxes a rebound name from a flat destructuring pattern', () => {
177
+ let rename: () => void = () => {};
178
+ let bump: () => void = () => {};
179
+
180
+ function Names(props: { user: { first: string; last?: string } }) @{
181
+ const count = track(0);
182
+ let { first, last = 'none' } = props.user;
183
+ rename = () => {
184
+ first = 'renamed';
185
+ };
186
+ bump = () => {
187
+ count.value++;
188
+ };
189
+ <span>{first + ' ' + last + ' ' + count.value}</span>
190
+ }
191
+
192
+ function App() @{
193
+ <Names user={{ first: 'ada' }} />
194
+ }
195
+
196
+ render(App);
197
+ const text = () => container.querySelector('span')?.textContent;
198
+ expect(text()).toBe('ada none 0');
199
+
200
+ rename();
201
+ bump();
202
+ flushSync();
203
+ expect(text()).toBe('renamed none 1');
204
+ });
205
+
206
+ it('boxes a rebound destructured parameter', () => {
207
+ let edit: () => void = () => {};
208
+ let bump: () => void = () => {};
209
+
210
+ function Panel({ mode = 'view' }: { mode?: string }) @{
211
+ const count = track(0);
212
+ edit = () => {
213
+ mode = 'edit';
214
+ };
215
+ bump = () => {
216
+ count.value++;
217
+ };
218
+ @if (count.value >= 0) {
219
+ <button>{mode + count.value}</button>
220
+ }
221
+ }
222
+
223
+ render(Panel);
224
+ const text = () => container.querySelector('button')?.textContent;
225
+ expect(text()).toBe('view0');
226
+
227
+ edit();
228
+ bump();
229
+ flushSync();
230
+ expect(text()).toBe('edit1');
231
+ });
232
+
233
+ it('boxes a reassigned props parameter', () => {
234
+ let bump: () => void = () => {};
235
+
236
+ function Child(props: { label: string }) @{
237
+ const count = track(0);
238
+ bump = () => {
239
+ count.value++;
240
+ };
241
+ props = { ...props, label: props.label + '!' };
242
+ <span>{props.label + count.value}</span>
243
+ }
244
+
245
+ function App() @{
246
+ <Child label="hi" />
247
+ }
248
+
249
+ render(App);
250
+ const text = () => container.querySelector('span')?.textContent;
251
+ expect(text()).toBe('hi!0');
252
+
253
+ bump();
254
+ flushSync();
255
+ expect(text()).toBe('hi!1');
256
+ });
257
+
258
+ it('boxes rebound names from nested object and array patterns', () => {
259
+ let rename: () => void = () => {};
260
+ let bump: () => void = () => {};
261
+
262
+ function Names(props: { user: { first: string; last?: string }; tags: string[] }) @{
263
+ const count = track(0);
264
+ let { user: { first, last = 'none' }, tags: [tag] } = props;
265
+ rename = () => {
266
+ first = 'renamed';
267
+ tag = 't2';
268
+ };
269
+ bump = () => {
270
+ count.value++;
271
+ };
272
+ <span>{first + ' ' + last + ' ' + tag + count.value}</span>
273
+ }
274
+
275
+ function App() @{
276
+ <Names user={{ first: 'ada' }} tags={['t1']} />
277
+ }
278
+
279
+ render(App);
280
+ const text = () => container.querySelector('span')?.textContent;
281
+ expect(text()).toBe('ada none t10');
282
+
283
+ rename();
284
+ bump();
285
+ flushSync();
286
+ expect(text()).toBe('renamed none t21');
287
+ });
288
+
289
+ it('boxes a rebound catch parameter', async () => {
290
+ let clear: () => void = () => {};
291
+ let bump: () => void = () => {};
292
+
293
+ function Loader() @{
294
+ const n = track(0);
295
+ bump = () => {
296
+ n.value++;
297
+ };
298
+ @try {
299
+ const data = trackAsync(() => Promise.reject(new Error('boom')));
300
+ <p>{data.value}</p>
301
+ } @catch (err) {
302
+ clear = () => {
303
+ err = null;
304
+ };
305
+ <button>{String(err instanceof Error ? err.message : err) + n.value}</button>
306
+ }
307
+ }
308
+
309
+ render(Loader);
310
+ await new Promise((resolve) => setTimeout(resolve, 0));
311
+ flushSync();
312
+ const text = () => container.querySelector('button')?.textContent;
313
+ expect(text()).toBe('boom0');
314
+
315
+ clear();
316
+ bump();
317
+ flushSync();
318
+ expect(text()).toBe('null1');
319
+ });
320
+
321
+ it('boxes lets written through destructuring and for-of assignment targets', () => {
322
+ let swap: () => void = () => {};
323
+ let bump: () => void = () => {};
324
+
325
+ function Pair() @{
326
+ const tick = track(0);
327
+ let label = 'a';
328
+ let other = 'x';
329
+ let last = '';
330
+ swap = () => {
331
+ [label] = ['b'];
332
+ ({ other } = { other: 'y' });
333
+ for (last of ['c', 'd']) {
334
+ // the loop variable is the boxed let
335
+ }
336
+ };
337
+ bump = () => {
338
+ tick.value++;
339
+ };
340
+ <span>{label + other + last + tick.value}</span>
341
+ }
342
+
343
+ render(Pair);
344
+ const text = () => container.querySelector('span')?.textContent;
345
+ expect(text()).toBe('ax0');
346
+
347
+ swap();
348
+ bump();
349
+ flushSync();
350
+ expect(text()).toBe('byd1');
351
+ });
352
+
353
+ it('boxes lets written through rest and default destructuring targets', () => {
354
+ let swap: () => void = () => {};
355
+ let bump: () => void = () => {};
356
+
357
+ function App() @{
358
+ const tick = track(0);
359
+ let a = 0;
360
+ let rest: Record<string, number> = {};
361
+ let tail: number[] = [];
362
+ swap = () => {
363
+ ({ a = 5, ...rest } = { rest_x: 7 } as { a?: number; rest_x: number });
364
+ [a, ...tail] = new Set([1, 2, 3]);
365
+ };
366
+ bump = () => {
367
+ tick.value++;
368
+ };
369
+ <span>{a + ':' + (rest.rest_x ?? '-') + ':' + tail.join(',') + ':' + tick.value}</span>
370
+ }
371
+
372
+ render(App);
373
+ const text = () => container.querySelector('span')?.textContent;
374
+ expect(text()).toBe('0:-::0');
375
+
376
+ swap();
377
+ bump();
378
+ flushSync();
379
+ // The object default applied, the rest collected, and the array rest
380
+ // consumed the Set through the iterator protocol.
381
+ expect(text()).toBe('1:7:2,3:1');
382
+ });
383
+
384
+ it('boxes a reassigned function declaration', () => {
385
+ let swap: () => void = () => {};
386
+ let bump: () => void = () => {};
387
+
388
+ function App() @{
389
+ const n = track(0);
390
+ function greet() {
391
+ return 'hi';
392
+ }
393
+ swap = () => {
394
+ greet = () => 'bye';
395
+ };
396
+ bump = () => {
397
+ n.value++;
398
+ };
399
+ <span>{greet() + n.value}</span>
400
+ }
401
+
402
+ render(App);
403
+ const text = () => container.querySelector('span')?.textContent;
404
+ expect(text()).toBe('hi0');
405
+
406
+ swap();
407
+ bump();
408
+ flushSync();
409
+ expect(text()).toBe('bye1');
410
+ });
411
+
412
+ it('a let declared without an initializer starts as undefined', () => {
413
+ let assign: () => void = () => {};
414
+ let bump: () => void = () => {};
415
+
416
+ function App() @{
417
+ const count = track(0);
418
+ let later: string | undefined;
419
+ assign = () => {
420
+ later = 'set';
421
+ };
422
+ bump = () => {
423
+ count.value++;
424
+ };
425
+ <span>{String(later) + '/' + count.value}</span>
426
+ }
427
+
428
+ render(App);
429
+ const text = () => container.querySelector('span')?.textContent;
430
+ expect(text()).toBe('undefined/0');
431
+
432
+ assign();
433
+ bump();
434
+ flushSync();
435
+ expect(text()).toBe('set/1');
436
+ });
437
+ });
438
+
439
+ describe('boxing keeps per-iteration bindings', () => {
440
+ it('does not box a for-loop variable, whose closures see their own iteration', () => {
441
+ function App() @{
442
+ const list = [];
443
+ for (let i = 0; i < 3; i++) {
444
+ list.push(<span>{i}</span>);
445
+ }
446
+ <div>{list}</div>
447
+ }
448
+
449
+ render(App);
450
+ expect(container.querySelector('div')?.textContent).toBe('012');
451
+ });
452
+ });
@@ -1,12 +1,12 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = lazy.value;"`;
3
+ exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = count.value;"`;
4
4
 
5
5
  exports[`compiler > assignments > compiles tracked values in effect with update expressions 1`] = `
6
6
  "_$_.untrack(() => {
7
- state.preIncrement = _$_.update_pre(lazy);
8
- state.postIncrement = _$_.update(lazy);
9
- state.preDecrement = _$_.update_pre(lazy, -1);
10
- state.postDecrement = _$_.update(lazy, -1);
11
- });"
7
+ state.preIncrement = ++count.value;
8
+ state.postIncrement = count.value++;
9
+ state.preDecrement = --count.value;
10
+ state.postDecrement = count.value--;
11
+ });"
12
12
  `;
@@ -1,7 +1,7 @@
1
1
  import { RippleArray, effect, track, untrack } from 'ripple';
2
2
  import { compile } from '@tsrx/ripple';
3
3
 
4
- const EFFECT_BODY_REGEX = /_\$\_\.effect\(\(\) => \{([\s\S]*?)\n\t\t\}\);/;
4
+ const EFFECT_BODY_REGEX = /_\$\_\.effect\(\(\) => \{([\s\S]*?)\n\t\}\);/;
5
5
 
6
6
  describe('compiler > assignments', () => {
7
7
  it('properly handles JS assignments, reads and updates to array indices', () => {
@@ -105,10 +105,10 @@ describe('compiler > assignments', () => {
105
105
  it('compiles tracked values in effect with assignment expression', () => {
106
106
  const source = `import { track, effect } from 'ripple';
107
107
  function App() @{
108
- let &[count] = track(0);
108
+ const count = track(0);
109
109
 
110
110
  effect(() => {
111
- state.count = count;
111
+ state.count = count.value;
112
112
  });
113
113
  <></>
114
114
  }`;
@@ -120,14 +120,14 @@ function App() @{
120
120
  it('compiles tracked values in effect with update expressions', () => {
121
121
  const source = `import { track, effect, untrack } from 'ripple';
122
122
  function App() @{
123
- let &[count] = track(5);
123
+ const count = track(5);
124
124
 
125
125
  effect(() => {
126
126
  untrack(() => {
127
- state.preIncrement = ++count;
128
- state.postIncrement = count++;
129
- state.preDecrement = --count;
130
- state.postDecrement = count--;
127
+ state.preIncrement = ++count.value;
128
+ state.postIncrement = count.value++;
129
+ state.preDecrement = --count.value;
130
+ state.postDecrement = count.value--;
131
131
  });
132
132
  });
133
133
  <></>
@@ -137,70 +137,6 @@ function App() @{
137
137
  expect(effect_match?.[1].trim()).toMatchSnapshot();
138
138
  });
139
139
 
140
- it('compiles unknown lazy array destructuring through lazy array helpers', () => {
141
- const source = `import { track } from 'ripple';
142
- function Child({ tr: &[count, tr] }) @{
143
- count++;
144
- tr[0]++;
145
- <div>{count}</div>
146
- }
147
- function App() @{
148
- let tracked = track(0);
149
- <Child tr={tracked} />
150
- }`;
151
- const { code } = compile(source, 'test.tsrx', { mode: 'client' });
152
-
153
- expect(code).toContain('_$_.lazy_array_get(lazy, 0)');
154
- expect(code).toContain('_$_.lazy_array_update(lazy, 0)');
155
- expect(code).toContain('_$_.lazy_array_get(lazy, 1)');
156
- expect(code).toContain('_$_.lazy_array_update(_$_.lazy_array_get(lazy, 1), 0)');
157
- expect(code).not.toContain('lazy[0]');
158
- expect(code).not.toContain('lazy[1][0]');
159
- });
160
-
161
- it('compiles writes to unknown lazy array index 1 through lazy array helpers', () => {
162
- const source = `function Child({ pair: &[first, second] }) @{
163
- second = 10;
164
- <div>{first}</div>
165
- }
166
- function App() @{
167
- <Child pair={[0, 1]} />
168
- }`;
169
- const { code } = compile(source, 'test.tsrx', { mode: 'client' });
170
-
171
- expect(code).toContain('_$_.lazy_array_set(lazy, 10, 1)');
172
- expect(code).not.toContain('lazy[1] =');
173
- });
174
-
175
- it('does not double-wrap member access on lazy array value bindings', () => {
176
- const source = `function Child({ pair: &[first] }) @{
177
- let value = first[0];
178
- <div>{value}</div>
179
- }
180
- function App() @{
181
- <Child pair={[{ 0: 'x' }]} />
182
- }`;
183
- const { code } = compile(source, 'test.tsrx', { mode: 'client' });
184
-
185
- expect(code).toContain('let value = _$_.lazy_array_get(lazy, 0)[0];');
186
- expect(code).not.toContain('_$_.lazy_array_get(_$_.lazy_array_get(lazy, 0), 0)');
187
- });
188
-
189
- it('throws on indexed access through known tracked lazy destructures', () => {
190
- const source = `import { track } from 'ripple';
191
- function App() @{
192
- let &[value, tracked_ref] = track({ 0: 'x' });
193
- let nested = value[0];
194
- tracked_ref[0] = { 0: 'y' };
195
- let next = value[0];
196
- <div>{nested}{next}</div>
197
- }`;
198
-
199
- expect(() => compile(source, 'test.tsrx', { mode: 'client' })).toThrow(
200
- /Use \.value or &\[\] lazy destructuring/,
201
- );
202
- });
203
-
204
140
  it('throws on known tracked indexed access', () => {
205
141
  const source = `import { track } from 'ripple';
206
142
  function App() @{
@@ -213,7 +149,7 @@ function App() @{
213
149
  }`;
214
150
 
215
151
  expect(() => compile(source, 'test.tsrx', { mode: 'client' })).toThrow(
216
- /Use \.value or &\[\] lazy destructuring/,
152
+ /Accessing a tracked object directly is not allowed/,
217
153
  );
218
154
  });
219
155
  });
@@ -13,18 +13,18 @@ describe('compiler > attributes', () => {
13
13
 
14
14
  const result = compile(source, 'test.tsrx', { mode: 'client' });
15
15
 
16
- // Should contain properly quoted hyphenated properties and unquoted valid identifiers
17
- expect(result.code).toMatch(/'data-scope': "test"/);
18
- expect(result.code).toMatch(/'aria-label': "accessible"/);
19
- expect(result.code).toMatch(/class: "valid"/);
16
+ // Hyphenated prop names are quoted keys of the plain props literal.
17
+ expect(result.code).toContain(`'data-scope': "test"`);
18
+ expect(result.code).toContain(`'aria-label': "accessible"`);
19
+ expect(result.code).toContain('class: "valid"');
20
20
  });
21
21
 
22
22
  it('generates valid JavaScript for all types of hyphenated attributes', () => {
23
23
  const testCases = [
24
- { attr: 'data-testid="value"', expected: /'data-testid': "value"/ },
25
- { attr: 'aria-label="label"', expected: /'aria-label': "label"/ },
26
- { attr: 'data-custom-attr="custom"', expected: /'data-custom-attr': "custom"/ },
27
- { attr: 'ng-if="condition"', expected: /'ng-if': "condition"/ },
24
+ { attr: 'data-testid="value"', expected: `'data-testid': "value"` },
25
+ { attr: 'aria-label="label"', expected: `'aria-label': "label"` },
26
+ { attr: 'data-custom-attr="custom"', expected: `'data-custom-attr': "custom"` },
27
+ { attr: 'ng-if="condition"', expected: `'ng-if': "condition"` },
28
28
  ];
29
29
 
30
30
  testCases.forEach(({ attr, expected }) => {
@@ -37,7 +37,7 @@ describe('compiler > attributes', () => {
37
37
  }`;
38
38
 
39
39
  const result = compile(source, 'test.tsrx', { mode: 'client' });
40
- expect(result.code).toMatch(expected);
40
+ expect(result.code).toContain(expected);
41
41
  });
42
42
  });
43
43
 
@@ -60,15 +60,15 @@ describe('compiler > attributes', () => {
60
60
 
61
61
  const result = compile(source, 'test.tsrx', { mode: 'client' });
62
62
 
63
- // Valid identifiers should not be quoted
64
- expect(result.code).toMatch(/validProp: "valid"/);
65
- expect(result.code).toMatch(/class: "valid"/);
66
- expect(result.code).toMatch(/id: "valid"/);
63
+ // Valid identifiers are plain keys
64
+ expect(result.code).toContain('validProp: "valid"');
65
+ expect(result.code).toContain('class: "valid"');
66
+ expect(result.code).toContain('id: "valid"');
67
67
 
68
- // Invalid identifiers (with hyphens) should be quoted
69
- expect(result.code).toMatch(/'data-invalid': "invalid"/);
70
- expect(result.code).toMatch(/'aria-invalid': "invalid"/);
71
- expect(result.code).toMatch(/'custom-prop': "invalid"/);
68
+ // Invalid identifiers (with hyphens) are quoted keys
69
+ expect(result.code).toContain(`'data-invalid': "invalid"`);
70
+ expect(result.code).toContain(`'aria-invalid': "invalid"`);
71
+ expect(result.code).toContain(`'custom-prop': "invalid"`);
72
72
  });
73
73
 
74
74
  it('ensures generated code is syntactically valid JavaScript', () => {
@@ -83,17 +83,17 @@ describe('compiler > attributes', () => {
83
83
 
84
84
  const result = compile(source, 'test.tsrx', { mode: 'client' });
85
85
 
86
- // Extract the props object from the generated code and test it's valid JavaScript
87
- const match = result.code.match(/_\$_.render_component\(Child,\s*[^,]+,\s*(\{[^}]+\})/);
86
+ // Extract the props literal from the generated code and test it's valid JavaScript
87
+ const match = result.code.match(/render_component\(Child, __anchor, (\{[\s\S]*?\})\)/);
88
88
  expect(match).toBeTruthy();
89
89
 
90
- const propsObject = match?.[1];
90
+ const literal = match?.[1];
91
91
  expect(() => {
92
92
  // Test that the object literal is syntactically valid
93
- new Function(`return ${propsObject}`);
93
+ new Function(`return ${literal}`);
94
94
  }).not.toThrow();
95
95
 
96
- // Also verify it contains the expected quoted property
97
- expect(propsObject).toMatch(/'data-scope': "test"/);
96
+ // Also verify it contains the expected quoted key
97
+ expect(literal).toContain(`'data-scope': "test"`);
98
98
  });
99
99
  });