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
@@ -166,18 +166,18 @@ describe('for statements', () => {
166
166
 
167
167
  it('correctly handles keyed for...of loops', () => {
168
168
  function App() @{
169
- let &[items] = track([
169
+ const items = track([
170
170
  { id: 1, text: 'Item 1' },
171
171
  { id: 2, text: 'Item 2' },
172
172
  { id: 3, text: 'Item 3' },
173
173
  ]);
174
174
  <>
175
- @for (let item of items; index i; key item.id) {
175
+ @for (let item of items.value; index i; key item.id) {
176
176
  <div>{i + ':' + item.text}</div>
177
177
  }
178
178
  <button
179
179
  onClick={() => {
180
- items = items.toReversed();
180
+ items.value = items.value.toReversed();
181
181
  }}
182
182
  >{'Reverse'}</button>
183
183
  </>
@@ -195,22 +195,63 @@ describe('for statements', () => {
195
195
  expect(container).toMatchSnapshot();
196
196
  });
197
197
 
198
+ it('keyed by identity re-reads a plain array mutated in place', () => {
199
+ const list = [{ id: 1 }, { id: 2 }, { id: 3 }];
200
+ let bump = () => {};
201
+
202
+ function App() @{
203
+ const version = track(0);
204
+ bump = () => {
205
+ version.value = version.value + 1;
206
+ };
207
+ <ul>
208
+ @for (const item of (version.value, list); key item) {
209
+ <li>{item.id}</li>
210
+ }
211
+ </ul>
212
+ }
213
+
214
+ render(App);
215
+ const texts = () => Array.from(container.querySelectorAll('li'), (el) => el.textContent);
216
+ expect(texts()).toEqual(['1', '2', '3']);
217
+
218
+ list.push({ id: 4 });
219
+ bump();
220
+ flushSync();
221
+ expect(texts()).toEqual(['1', '2', '3', '4']);
222
+
223
+ list.splice(0, 1);
224
+ bump();
225
+ flushSync();
226
+ expect(texts()).toEqual(['2', '3', '4']);
227
+
228
+ list.reverse();
229
+ bump();
230
+ flushSync();
231
+ expect(texts()).toEqual(['4', '3', '2']);
232
+
233
+ list.splice(1, 1, { id: 5 });
234
+ bump();
235
+ flushSync();
236
+ expect(texts()).toEqual(['4', '5', '2']);
237
+ });
238
+
198
239
  it('keyed for over derived updates sibling text nodes', () => {
199
240
  function App() @{
200
- let &[count] = track(0);
201
- let &[items] = track(
202
- () => Array.from({ length: count }).map((_, id) => ({ id, label: `Item ${id}` })),
241
+ const count = track(0);
242
+ const items = track(
243
+ () => Array.from({ length: count.value }).map((_, id) => ({ id, label: `Item ${id}` })),
203
244
  );
204
245
  <>
205
246
  <button
206
247
  onClick={() => {
207
- count++;
248
+ count.value++;
208
249
  }}
209
250
  >{'Add'}</button>
210
- @for (const item of items; key item.id) {
251
+ @for (const item of items.value; key item.id) {
211
252
  <div class="item">{item.label}</div>
212
253
  }
213
- <p class="count">{count}</p>
254
+ <p class="count">{count.value}</p>
214
255
  </>
215
256
  }
216
257
 
@@ -236,18 +277,18 @@ describe('for statements', () => {
236
277
 
237
278
  it('keyed for inserts multiple new items in the middle in correct order', () => {
238
279
  function App() @{
239
- let &[items] = track([
280
+ const items = track([
240
281
  { id: 'a', text: 'A' },
241
282
  { id: 'c', text: 'C' },
242
283
  { id: 'd', text: 'D' },
243
284
  ]);
244
285
  <>
245
- @for (const item of items; key item.id) {
286
+ @for (const item of items.value; key item.id) {
246
287
  <div class="item">{item.text}</div>
247
288
  }
248
289
  <button
249
290
  onClick={() => {
250
- items = [
291
+ items.value = [
251
292
  { id: 'a', text: 'A' },
252
293
  { id: 'b1', text: 'B1' },
253
294
  { id: 'b2', text: 'B2' },
@@ -279,14 +320,14 @@ describe('for statements', () => {
279
320
  const d = { text: 'D' };
280
321
 
281
322
  function App() @{
282
- let &[items] = track([a, c, d]);
323
+ const items = track([a, c, d]);
283
324
  <>
284
- @for (const item of items) {
325
+ @for (const item of items.value) {
285
326
  <div class="item">{item.text}</div>
286
327
  }
287
328
  <button
288
329
  onClick={() => {
289
- items = [a, { text: 'B1' }, { text: 'B2' }, c, d];
330
+ items.value = [a, { text: 'B1' }, { text: 'B2' }, c, d];
290
331
  }}
291
332
  >{'Insert'}</button>
292
333
  </>
@@ -308,16 +349,16 @@ describe('for statements', () => {
308
349
 
309
350
  it('keyed for with 32+ items: full reversal updates values via Map path', () => {
310
351
  function App() @{
311
- let &[items] = track(Array.from({ length: 40 }, (_, i) => ({ id: i, text: `Item ${i}` })));
352
+ const items = track(Array.from({ length: 40 }, (_, i) => ({ id: i, text: `Item ${i}` })));
312
353
  <>
313
354
  <div>
314
- @for (let item of items; index idx; key item.id) {
355
+ @for (let item of items.value; index idx; key item.id) {
315
356
  <span class="item">{idx + ':' + item.text}</span>
316
357
  }
317
358
  </div>
318
359
  <button
319
360
  onClick={() => {
320
- items = items.toReversed();
361
+ items.value = items.value.toReversed();
321
362
  }}
322
363
  >{'Reverse'}</button>
323
364
  </>
@@ -339,25 +380,25 @@ describe('for statements', () => {
339
380
 
340
381
  it('handles updating with new objects with same key', () => {
341
382
  function App() @{
342
- let &[items] = track([
383
+ const items = track([
343
384
  { id: 1, text: 'Item 1' },
344
385
  { id: 2, text: 'Item 2' },
345
386
  { id: 3, text: 'Item 3' },
346
387
  ]);
347
388
  <>
348
- @for (let item of items; index i; key item.id) {
389
+ @for (let item of items.value; index i; key item.id) {
349
390
  <div>{i + ':' + item.text}</div>
350
391
  }
351
392
  <button
352
393
  onClick={() => {
353
- items[0].id = 3;
354
- items[1].id = 2;
355
- items[2].id = 1;
356
-
357
- items = [
358
- { ...items[0], text: 'Item 1!' },
359
- { ...items[1], text: 'Item 2!' },
360
- { ...items[2], text: 'Item 3!' },
394
+ items.value[0].id = 3;
395
+ items.value[1].id = 2;
396
+ items.value[2].id = 1;
397
+
398
+ items.value = [
399
+ { ...items.value[0], text: 'Item 1!' },
400
+ { ...items.value[1], text: 'Item 2!' },
401
+ { ...items.value[2], text: 'Item 3!' },
361
402
  ];
362
403
  }}
363
404
  >{'Reverse'}</button>
@@ -378,16 +419,16 @@ describe('for statements', () => {
378
419
  const objects = Array.from({ length: 50 }, (_, i) => ({ id: i, text: `Obj ${i}` }));
379
420
 
380
421
  function App() @{
381
- let &[items] = track(objects.slice());
422
+ const items = track(objects.slice());
382
423
  <>
383
424
  <div>
384
- @for (const item of items) {
425
+ @for (const item of items.value) {
385
426
  <span class="item">{item.text}</span>
386
427
  }
387
428
  </div>
388
429
  <button
389
430
  onClick={() => {
390
- items = objects.slice(15).reverse();
431
+ items.value = objects.slice(15).reverse();
391
432
  }}
392
433
  >{'Trim and reverse'}</button>
393
434
  </>
@@ -406,4 +447,264 @@ describe('for statements', () => {
406
447
 
407
448
  expect(getTexts()).toEqual(objects.slice(15).reverse().map((o) => o.text));
408
449
  });
450
+
451
+ it('renders the index and the key from a function that returns its template', () => {
452
+ function Plain({ items }) {
453
+ return <ul>
454
+ @for (const item of items) {
455
+ <li class="plain">{item.text}</li>
456
+ }
457
+ </ul>;
458
+ }
459
+
460
+ function Indexed({ items }) {
461
+ return <ul>
462
+ @for (const item of items; index i) {
463
+ <li class="indexed">{i + ': ' + item.text}</li>
464
+ }
465
+ </ul>;
466
+ }
467
+
468
+ function Keyed({ items }) {
469
+ return <ul>
470
+ @for (const item of items; key item.id) {
471
+ <li class="keyed">{item.text}</li>
472
+ }
473
+ </ul>;
474
+ }
475
+
476
+ function IndexedAndKeyed({ items }) {
477
+ return <ul>
478
+ @for (const item of items; index i; key item.id) {
479
+ <li class="indexed-keyed">{i + ': ' + item.text}</li>
480
+ }
481
+ </ul>;
482
+ }
483
+
484
+ const Arrow = ({ items }) => <ul>
485
+ @for (const item of items; index i) {
486
+ <li class="arrow">{i + ': ' + item.text}</li>
487
+ }
488
+ </ul>;
489
+
490
+ function App() @{
491
+ const items = [
492
+ { id: 1, text: 'a' },
493
+ { id: 2, text: 'b' },
494
+ ];
495
+ <>
496
+ <Plain {items} />
497
+ <Indexed {items} />
498
+ <Keyed {items} />
499
+ <IndexedAndKeyed {items} />
500
+ <Arrow {items} />
501
+ </>
502
+ }
503
+
504
+ render(App);
505
+
506
+ const texts = (selector) => Array.from(container.querySelectorAll(selector)).map(
507
+ (el) => el.textContent,
508
+ );
509
+
510
+ expect(texts('.plain')).toEqual(['a', 'b']);
511
+ expect(texts('.indexed')).toEqual(['0: a', '1: b']);
512
+ expect(texts('.keyed')).toEqual(['a', 'b']);
513
+ expect(texts('.indexed-keyed')).toEqual(['0: a', '1: b']);
514
+ expect(texts('.arrow')).toEqual(['0: a', '1: b']);
515
+ });
516
+
517
+ it('renders the index and the key from a template held in a variable', () => {
518
+ function Indexed({ items }) {
519
+ const list = <ul>
520
+ @for (const item of items; index i) {
521
+ <li class="indexed">{i + ': ' + item.text}</li>
522
+ }
523
+ </ul>;
524
+ return list;
525
+ }
526
+
527
+ function Keyed({ items }) {
528
+ return [
529
+ <ul>
530
+ @for (const item of items; key item.id) {
531
+ <li class="keyed">{item.text}</li>
532
+ }
533
+ </ul>,
534
+ ];
535
+ }
536
+
537
+ function App() @{
538
+ const items = [
539
+ { id: 1, text: 'a' },
540
+ { id: 2, text: 'b' },
541
+ ];
542
+ <>
543
+ <Indexed {items} />
544
+ <Keyed {items} />
545
+ </>
546
+ }
547
+
548
+ render(App);
549
+
550
+ const texts = (selector) => Array.from(container.querySelectorAll(selector)).map(
551
+ (el) => el.textContent,
552
+ );
553
+
554
+ expect(texts('.indexed')).toEqual(['0: a', '1: b']);
555
+ expect(texts('.keyed')).toEqual(['a', 'b']);
556
+ });
557
+
558
+ it('renders a bare `@for` held in a variable through an expression', () => {
559
+ function Returned({ items }) {
560
+ const yo = @for (const item of items; index i; key item.id) {
561
+ <li class="returned">{i + ': ' + item.text}</li>
562
+ };
563
+ return <ul>{yo}</ul>;
564
+ }
565
+
566
+ function Held({ items }) {
567
+ const yo = @for (const item of items; index i) {
568
+ <li class="held">{i + ': ' + item.text}</li>
569
+ };
570
+ const list = <ul>{yo}</ul>;
571
+ return list;
572
+ }
573
+
574
+ function App() @{
575
+ const items = [
576
+ { id: 1, text: 'a' },
577
+ { id: 2, text: 'b' },
578
+ ];
579
+ <>
580
+ <Returned {items} />
581
+ <Held {items} />
582
+ </>
583
+ }
584
+
585
+ render(App);
586
+
587
+ const texts = (selector) => Array.from(container.querySelectorAll(selector)).map(
588
+ (el) => el.textContent,
589
+ );
590
+
591
+ expect(texts('.returned')).toEqual(['0: a', '1: b']);
592
+ expect(texts('.held')).toEqual(['0: a', '1: b']);
593
+ });
594
+
595
+ it('reads rest and default pattern names off the per-key item as it changes', () => {
596
+ type Row = { id: number; name: string; tags?: string[] };
597
+ let replace: (rows: Row[]) => void = () => {};
598
+
599
+ function App() @{
600
+ const rows = track<Row[]>([
601
+ { id: 1, name: 'one', tags: ['a', 'b'] },
602
+ ]);
603
+ replace = (next) => {
604
+ rows.value = next;
605
+ };
606
+ <ul>
607
+ @for (const { id, ...rest } of rows.value; key id) {
608
+ <li>{rest.name}</li>
609
+ }
610
+ @for (const [head, second = 'none', ...others] of rows.value.map(
611
+ (row) => row.tags ?? [],
612
+ ); key head) {
613
+ <li>{head + ':' + second + ':' + others.length}</li>
614
+ }
615
+ </ul>
616
+ }
617
+
618
+ render(App);
619
+ const items = () => [...container.querySelectorAll('li')].map((li) => li.textContent);
620
+ expect(items()).toEqual(['one', 'a:b:0']);
621
+
622
+ // A new object under the same key updates the existing block in place.
623
+ replace([
624
+ { id: 1, name: 'uno', tags: ['a'] },
625
+ ]);
626
+ flushSync();
627
+ expect(items()).toEqual(['uno', 'a:none:0']);
628
+ });
629
+
630
+ it('destructures a keyed item once per change, so defaults run once', () => {
631
+ type Row = { id: number; label?: string };
632
+ let replace: (rows: Row[]) => void = () => {};
633
+ let defaults = 0;
634
+ const next = () => `default ${++defaults}`;
635
+
636
+ function App() @{
637
+ const rows = track<Row[]>([{ id: 1 }]);
638
+ replace = (next) => {
639
+ rows.value = next;
640
+ };
641
+ <ul>
642
+ @for (const { id, label = next() } of rows.value; key id) {
643
+ <li>{label + '/' + label + '/' + id}</li>
644
+ }
645
+ </ul>
646
+ }
647
+
648
+ render(App);
649
+ const items = () => [...container.querySelectorAll('li')].map((li) => li.textContent);
650
+ // Two reads of `label`, one evaluation of its default.
651
+ expect(items()).toEqual(['default 1/default 1/1']);
652
+ expect(defaults).toBe(1);
653
+
654
+ replace([{ id: 1 }]);
655
+ flushSync();
656
+ expect(items()).toEqual(['default 2/default 2/1']);
657
+ expect(defaults).toBe(2);
658
+
659
+ replace([
660
+ { id: 1, label: 'given' },
661
+ ]);
662
+ flushSync();
663
+ expect(items()).toEqual(['given/given/1']);
664
+ expect(defaults).toBe(2);
665
+ });
666
+
667
+ it('reads rest pattern names inside a hoisted branch of a keyed loop', () => {
668
+ type Row = { id: number; name: string };
669
+ let replace: (rows: Row[]) => void = () => {};
670
+ let toggle: () => void = () => {};
671
+
672
+ function App() @{
673
+ const rows = track<Row[]>([
674
+ { id: 1, name: 'one' },
675
+ ]);
676
+ const show = track(true);
677
+ replace = (next) => {
678
+ rows.value = next;
679
+ };
680
+ toggle = () => {
681
+ show.value = !show.value;
682
+ };
683
+ <ul>
684
+ @for (const { id, ...rest } of rows.value; key id) {
685
+ @if (show.value) {
686
+ <li>{rest.name}</li>
687
+ }
688
+ }
689
+ </ul>
690
+ }
691
+
692
+ render(App);
693
+ const items = () => [...container.querySelectorAll('li')].map((li) => li.textContent);
694
+ expect(items()).toEqual(['one']);
695
+
696
+ replace([
697
+ { id: 1, name: 'uno' },
698
+ ]);
699
+ flushSync();
700
+ expect(items()).toEqual(['uno']);
701
+
702
+ toggle();
703
+ flushSync();
704
+ expect(items()).toEqual([]);
705
+
706
+ toggle();
707
+ flushSync();
708
+ expect(items()).toEqual(['uno']);
709
+ });
409
710
  });
@@ -31,18 +31,18 @@ describe('head elements', () => {
31
31
 
32
32
  it('renders reactive title element', () => {
33
33
  function App() @{
34
- let &[title] = track('Initial Title');
34
+ const title = track('Initial Title');
35
35
  <>
36
36
  <head>
37
- <title>{title}</title>
37
+ <title>{title.value}</title>
38
38
  </head>
39
39
  <div>
40
40
  <button
41
41
  onClick={() => {
42
- title = 'Updated Title';
42
+ title.value = 'Updated Title';
43
43
  }}
44
44
  >{'Update Title'}</button>
45
- <span>{title}</span>
45
+ <span>{title.value}</span>
46
46
  </div>
47
47
  </>
48
48
  }
@@ -62,15 +62,15 @@ describe('head elements', () => {
62
62
 
63
63
  it('renders title with template literal', () => {
64
64
  function App() @{
65
- let &[name] = track('World');
65
+ const name = track('World');
66
66
  <>
67
67
  <head>
68
- <title>{`Hello ${name}!`}</title>
68
+ <title>{`Hello ${name.value}!`}</title>
69
69
  </head>
70
70
  <div>
71
71
  <button
72
72
  onClick={() => {
73
- name = 'Ripple';
73
+ name.value = 'Ripple';
74
74
  }}
75
75
  >{'Change Name'}</button>
76
76
  </div>
@@ -90,19 +90,19 @@ describe('head elements', () => {
90
90
 
91
91
  it('renders title with computed value', () => {
92
92
  function App() @{
93
- let &[count] = track(0);
93
+ const count = track(0);
94
94
  let prefix = 'Count: ';
95
95
  <>
96
96
  <head>
97
- <title>{prefix + count}</title>
97
+ <title>{prefix + count.value}</title>
98
98
  </head>
99
99
  <div>
100
100
  <button
101
101
  onClick={() => {
102
- count++;
102
+ count.value++;
103
103
  }}
104
104
  >{'Increment'}</button>
105
- <span>{count}</span>
105
+ <span>{count.value}</span>
106
106
  </div>
107
107
  </>
108
108
  }
@@ -121,15 +121,15 @@ describe('head elements', () => {
121
121
 
122
122
  it('handles multiple title updates', () => {
123
123
  function App() @{
124
- let &[step] = track(1);
124
+ const step = track(1);
125
125
  <>
126
126
  <head>
127
- <title>{`Step ${step} of 3`}</title>
127
+ <title>{`Step ${step.value} of 3`}</title>
128
128
  </head>
129
129
  <div>
130
130
  <button
131
131
  onClick={() => {
132
- step = step % 3 + 1;
132
+ step.value = step.value % 3 + 1;
133
133
  }}
134
134
  >{'Next Step'}</button>
135
135
  </div>
@@ -244,23 +244,23 @@ describe('head elements', () => {
244
244
 
245
245
  it('renders title with conditional content', () => {
246
246
  function App() @{
247
- let &[showPrefix] = track(true);
248
- let &[title] = track('Main Page');
247
+ const showPrefix = track(true);
248
+ const title = track('Main Page');
249
249
  <>
250
250
  <head>
251
251
  <title>
252
- {showPrefix ? 'App - ' + title : title}
252
+ {showPrefix.value ? 'App - ' + title.value : title.value}
253
253
  </title>
254
254
  </head>
255
255
  <div>
256
256
  <button
257
257
  onClick={() => {
258
- showPrefix = !showPrefix;
258
+ showPrefix.value = !showPrefix.value;
259
259
  }}
260
260
  >{'Toggle Prefix'}</button>
261
261
  <button
262
262
  onClick={() => {
263
- title = title === 'Main Page' ? 'Settings' : 'Main Page';
263
+ title.value = title.value === 'Main Page' ? 'Settings' : 'Main Page';
264
264
  }}
265
265
  >{'Change Page'}</button>
266
266
  </div>
@@ -16,12 +16,14 @@ describe('Fragment innerHTML', () => {
16
16
 
17
17
  it('renders spread innerHTML as content instead of an attribute', () => {
18
18
  function App() @{
19
- let &[attrs] = track<{ innerHTML?: string }>({
19
+ const attrs = track<{ innerHTML?: string }>({
20
20
  innerHTML: '<span>Spread HTML</span>',
21
21
  });
22
22
  <>
23
- <code {...attrs} />
24
- <button onClick={() => (attrs = { innerHTML: '<em>Updated HTML</em>' })}>{'Update'}</button>
23
+ <code {...attrs.value} />
24
+ <button
25
+ onClick={() => (attrs.value = { innerHTML: '<em>Updated HTML</em>' })}
26
+ >{'Update'}</button>
25
27
  </>
26
28
  }
27
29
 
@@ -64,12 +66,12 @@ describe('Fragment innerHTML', () => {
64
66
 
65
67
  it('renders dynamic innerHTML fragments', () => {
66
68
  function App() @{
67
- let &[str] = track('<div>Test</div>');
69
+ const str = track('<div>Test</div>');
68
70
  <>
69
- <Fragment innerHTML={str} />
71
+ <Fragment innerHTML={str.value} />
70
72
  <button
71
73
  onClick={() => {
72
- str = '<div>Updated</div>';
74
+ str.value = '<div>Updated</div>';
73
75
  }}
74
76
  >{'Update'}</button>
75
77
  </>
@@ -99,10 +101,10 @@ describe('Fragment innerHTML', () => {
99
101
 
100
102
  it('updates dynamic Fragment innerHTML', () => {
101
103
  function App() @{
102
- let &[html] = track('<span>First</span>');
104
+ const html = track('<span>First</span>');
103
105
  <>
104
- <Fragment innerHTML={html} />
105
- <button onClick={() => (html = '<em>Second</em>')}>{'Update'}</button>
106
+ <Fragment innerHTML={html.value} />
107
+ <button onClick={() => (html.value = '<em>Second</em>')}>{'Update'}</button>
106
108
  </>
107
109
  }
108
110