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,141 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flushSync, track, type Derived, type Tracked } from 'ripple';
3
+
4
+ // What happens when a value that reaches a sink (attribute, text, child
5
+ // props) changes kind at runtime: plain value first, tracked or derived later.
6
+
7
+ describe('prop kind switches at runtime', () => {
8
+ it('element spread: a plain attribute value replaced by a derived is followed', () => {
9
+ let swap: () => void = () => {};
10
+ let bump: () => void = () => {};
11
+
12
+ function App() @{
13
+ const n = track(0);
14
+ const attrs = track<{ class: string | Derived<string> }>({ class: 'plain' });
15
+ swap = () => {
16
+ attrs.value = { class: track(() => 'live-' + n.value) };
17
+ };
18
+ bump = () => {
19
+ n.value++;
20
+ };
21
+ <div {...attrs.value}>{'x'}</div>
22
+ }
23
+
24
+ render(App);
25
+ const div = () => container.querySelector('div');
26
+ expect(div()?.className).toBe('plain');
27
+
28
+ swap();
29
+ flushSync();
30
+ expect(div()?.className).toBe('live-0');
31
+
32
+ // The spread subscribed to the derived when it unwrapped it.
33
+ bump();
34
+ flushSync();
35
+ expect(div()?.className).toBe('live-1');
36
+ });
37
+
38
+ it('plain attribute and text: a tracked prop is read through .value', () => {
39
+ let bump: () => void = () => {};
40
+
41
+ function Child(props: { cls: Tracked<string>; title: Tracked<string> }) @{
42
+ <div class={props.cls.value}>{props.title.value}</div>
43
+ }
44
+
45
+ function App() @{
46
+ const cls = track('a');
47
+ const title = track('t');
48
+ bump = () => {
49
+ cls.value = 'b';
50
+ title.value = 'u';
51
+ };
52
+ <Child {cls} {title} />
53
+ }
54
+
55
+ render(App);
56
+ const div = container.querySelector('div');
57
+ expect(div?.className).toBe('a');
58
+ expect(div?.textContent).toBe('t');
59
+
60
+ bump();
61
+ flushSync();
62
+ expect(div?.className).toBe('b');
63
+ expect(div?.textContent).toBe('u');
64
+ });
65
+
66
+ it(
67
+ 'dynamic component: props are re-evaluated when the component changes, not when their inputs do',
68
+ () => {
69
+ let switchComponent: () => void = () => {};
70
+ let bump: () => void = () => {};
71
+
72
+ function A(props: { n: number }) @{
73
+ <span class="a">{props.n}</span>
74
+ }
75
+ function B(props: { n: number }) @{
76
+ <span class="b">{props.n}</span>
77
+ }
78
+
79
+ function App() @{
80
+ // A function value is a derived: hold the component in a writable one.
81
+ const Comp = track(() => A, undefined, true);
82
+ const n = track(0);
83
+ switchComponent = () => {
84
+ Comp.value = Comp.value === A ? B : A;
85
+ };
86
+ bump = () => {
87
+ n.value++;
88
+ };
89
+ <{Comp.value} n={n.value} />
90
+ }
91
+
92
+ render(App);
93
+ expect(container.querySelector('span.a')?.textContent).toBe('0');
94
+
95
+ // A prop is a value: the existing instance keeps the number it got.
96
+ bump();
97
+ flushSync();
98
+ expect(container.querySelector('span.a')?.textContent).toBe('0');
99
+
100
+ // A new instance reads the literal again.
101
+ switchComponent();
102
+ flushSync();
103
+ expect(container.querySelector('span.b')?.textContent).toBe('1');
104
+ },
105
+ );
106
+
107
+ it('a tracked prop whose value becomes a derived: the child must unwrap both levels', () => {
108
+ let swap: () => void = () => {};
109
+ let bump: () => void = () => {};
110
+
111
+ function Child(props: { v: Tracked<string | Derived<string>> }) @{
112
+ <span>
113
+ {typeof props.v.value === 'string' ? props.v.value : props.v.value.value}
114
+ </span>
115
+ }
116
+
117
+ function App() @{
118
+ const n = track(0);
119
+ const v = track<string | Derived<string>>('plain');
120
+ swap = () => {
121
+ v.value = track(() => 'live-' + n.value);
122
+ };
123
+ bump = () => {
124
+ n.value++;
125
+ };
126
+ <Child {v} />
127
+ }
128
+
129
+ render(App);
130
+ const span = () => container.querySelector('span');
131
+ expect(span()?.textContent).toBe('plain');
132
+
133
+ swap();
134
+ flushSync();
135
+ expect(span()?.textContent).toBe('live-0');
136
+
137
+ bump();
138
+ flushSync();
139
+ expect(span()?.textContent).toBe('live-1');
140
+ });
141
+ });
@@ -119,7 +119,7 @@ describe('refs', () => {
119
119
  let logs: string[] = [];
120
120
 
121
121
  function App() @{
122
- let &[value] = track('test');
122
+ const value = 'test';
123
123
  function inputRef(node: HTMLInputElement) {
124
124
  logs.push('ref called');
125
125
  }
@@ -219,12 +219,12 @@ describe('refs', () => {
219
219
  let div: HTMLDivElement | null | undefined;
220
220
 
221
221
  function App() @{
222
- let &[show] = track(true);
222
+ const show = track(true);
223
223
  <>
224
- @if (show) {
224
+ @if (show.value) {
225
225
  <div ref={div}>{'Hello cleanup'}</div>
226
226
  }
227
- <button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
227
+ <button class="toggle" onClick={() => (show.value = false)}>{'hide'}</button>
228
228
  </>
229
229
  }
230
230
 
@@ -262,12 +262,12 @@ describe('refs', () => {
262
262
  }
263
263
 
264
264
  function App() @{
265
- let &[show] = track(true);
265
+ const show = track(true);
266
266
  <>
267
- @if (show) {
267
+ @if (show.value) {
268
268
  <Child ref={input} />
269
269
  }
270
- <button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
270
+ <button class="toggle" onClick={() => (show.value = false)}>{'hide'}</button>
271
271
  </>
272
272
  }
273
273
 
@@ -352,9 +352,9 @@ describe('refs', () => {
352
352
  }
353
353
 
354
354
  function App() @{
355
- let &[show] = track(true);
355
+ const show = track(true);
356
356
  <>
357
- @if (show) {
357
+ @if (show.value) {
358
358
  <Child
359
359
  ref={(node: HTMLInputElement | null) => {
360
360
  input = node;
@@ -364,7 +364,7 @@ describe('refs', () => {
364
364
  }}
365
365
  />
366
366
  }
367
- <button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
367
+ <button class="toggle" onClick={() => (show.value = false)}>{'hide'}</button>
368
368
  </>
369
369
  }
370
370
 
@@ -386,14 +386,14 @@ describe('refs', () => {
386
386
  let previous: HTMLInputElement | undefined;
387
387
 
388
388
  function Child(props: PropsWithExtras<{ ref: RefValue<HTMLInputElement> }>) @{
389
- let &[as_ref] = track(true);
389
+ const as_ref = track(true);
390
390
  <>
391
391
  <input
392
392
  type="text"
393
393
  value="keep"
394
- {...(as_ref ? { ref: props.ref } : { input_ref: 'regular prop' })}
394
+ {...(as_ref.value ? { ref: props.ref } : { input_ref: 'regular prop' })}
395
395
  />
396
- <button class="toggle" onClick={() => (as_ref = false)}>{'toggle'}</button>
396
+ <button class="toggle" onClick={() => (as_ref.value = false)}>{'toggle'}</button>
397
397
  </>
398
398
  }
399
399
 
@@ -427,14 +427,14 @@ describe('refs', () => {
427
427
  let previous: HTMLInputElement | undefined;
428
428
 
429
429
  function Child(props: PropsWithExtras<{ ref: RefValue<HTMLInputElement> }>) @{
430
- let &[as_ref] = track(false);
430
+ const as_ref = track(false);
431
431
  <>
432
432
  <input
433
433
  type="text"
434
434
  value="keep"
435
- {...(as_ref ? { ref: props.ref } : { input_ref: 'regular prop' })}
435
+ {...(as_ref.value ? { ref: props.ref } : { input_ref: 'regular prop' })}
436
436
  />
437
- <button class="toggle" onClick={() => (as_ref = true)}>{'toggle'}</button>
437
+ <button class="toggle" onClick={() => (as_ref.value = true)}>{'toggle'}</button>
438
438
  </>
439
439
  }
440
440
 
@@ -490,7 +490,7 @@ describe('refs', () => {
490
490
  let logs: string[] = [];
491
491
 
492
492
  function App() @{
493
- let &[value] = track('test');
493
+ const value = 'test';
494
494
  function inputRef(node: HTMLInputElement) {
495
495
  logs.push('ref called');
496
496
  }
@@ -2,6 +2,15 @@ import { describe, it, expect } from 'vitest';
2
2
  import { track, flushSync, type Tracked } from 'ripple';
3
3
  import { compile } from '@tsrx/ripple';
4
4
 
5
+ // Declared at module scope on purpose: a module-level component takes the
6
+ // hoisted `_render` lowering, which an in-suite declaration never exercises.
7
+ function ModuleLevelDashboard({ user }) {
8
+ if (!user) {
9
+ return <p class="ml-empty">No user found</p>;
10
+ }
11
+ return <p class="ml-user">{user}</p>;
12
+ }
13
+
5
14
  describe('returns in restricted scopes', () => {
6
15
  it('throws error when return is used in module scope', () => {
7
16
  expect(
@@ -216,10 +225,10 @@ describe('function returns in client components', () => {
216
225
  }
217
226
 
218
227
  function App() {
219
- let &[user, userTracked] = track<string | null>(null);
228
+ const user = track<string | null>(null);
220
229
  return <>
221
- <Dashboard user={userTracked} />
222
- <button onClick={() => (user = user === 'Adam' ? null : 'Adam')}>Toggle</button>
230
+ <Dashboard {user} />
231
+ <button onClick={() => (user.value = user.value === 'Adam' ? null : 'Adam')}>Toggle</button>
223
232
  </>;
224
233
  }
225
234
 
@@ -236,4 +245,18 @@ describe('function returns in client components', () => {
236
245
  flushSync();
237
246
  expect(container.querySelector('.empty')?.textContent).toBe('No user found');
238
247
  });
248
+
249
+ it('renders every branch of a module-level component that returns templates', () => {
250
+ function App() @{
251
+ <>
252
+ <ModuleLevelDashboard user={null} />
253
+ <ModuleLevelDashboard user="Adam" />
254
+ </>
255
+ }
256
+
257
+ render(App);
258
+
259
+ expect(container.querySelector('.ml-empty')?.textContent).toBe('No user found');
260
+ expect(container.querySelector('.ml-user')?.textContent).toBe('Adam');
261
+ });
239
262
  });
@@ -0,0 +1,42 @@
1
+ import { flushSync, mount, trackAsync } from 'ripple';
2
+
3
+ describe('mount rootBoundary', () => {
4
+ it('renders under a default root boundary, so trackAsync works outside @try', async () => {
5
+ function App() @{
6
+ const data = trackAsync(() => Promise.resolve('loaded'));
7
+ <div class="value">{data.value}</div>
8
+ }
9
+
10
+ const unmount = mount(App, { target: container });
11
+ await new Promise((resolve) => setTimeout(resolve, 0));
12
+ flushSync();
13
+
14
+ expect(container.querySelector('.value')?.textContent).toBe('loaded');
15
+ unmount();
16
+ expect(container.innerHTML).toBe('');
17
+ });
18
+
19
+ it('renders without any root boundary when rootBoundary is false', () => {
20
+ function App() @{
21
+ <div class="plain">{'no boundary'}</div>
22
+ }
23
+
24
+ const unmount = mount(App, { target: container, rootBoundary: false });
25
+ flushSync();
26
+
27
+ expect(container.querySelector('.plain')?.textContent).toBe('no boundary');
28
+ unmount();
29
+ expect(container.innerHTML).toBe('');
30
+ });
31
+
32
+ it('requires a user @try for trackAsync when rootBoundary is false', () => {
33
+ function App() @{
34
+ const data = trackAsync(() => Promise.resolve('loaded'));
35
+ <div class="value">{data.value}</div>
36
+ }
37
+
38
+ expect(() => mount(App, { target: container, rootBoundary: false })).toThrow(
39
+ /Missing parent `try/,
40
+ );
41
+ });
42
+ });
@@ -9,10 +9,10 @@ describe('scoped flush', () => {
9
9
  let bump = null;
10
10
 
11
11
  function Producer() @{
12
- let &[v, vT] = track(0);
13
- shared = vT;
12
+ const v = track(0);
13
+ shared = v;
14
14
  bump = () => {
15
- v += 1;
15
+ v.value += 1;
16
16
  };
17
17
  <div class="producer">{'producer'}</div>
18
18
  }
@@ -59,10 +59,10 @@ describe('scoped flush', () => {
59
59
  }
60
60
 
61
61
  function Branch(props) @{
62
- let &[local, localT] = track(0);
62
+ const local = track(0);
63
63
  if (props.side === 'L') {
64
64
  bumpLeftLocal = () => {
65
- local += 1;
65
+ local.value += 1;
66
66
  };
67
67
  }
68
68
  <div class={'branch-' + props.side}>
@@ -72,10 +72,10 @@ describe('scoped flush', () => {
72
72
  }
73
73
 
74
74
  function App() @{
75
- let &[root, rootT] = track(0);
76
- Ctx.set(rootT);
75
+ const root = track(0);
76
+ Ctx.set(root);
77
77
  bumpRoot = () => {
78
- root += 1;
78
+ root.value += 1;
79
79
  };
80
80
  <div>
81
81
  <Branch side="L" />
@@ -66,10 +66,10 @@ describe('RippleSet', () => {
66
66
  it('handles has operation', () => {
67
67
  function SetTest() @{
68
68
  let items = new RippleSet([1, 2, 3]);
69
- let &[hasValue] = track(() => items.has(2));
69
+ const hasValue = track(() => items.has(2));
70
70
  <>
71
71
  <button onClick={() => items.delete(2)}>{'delete'}</button>
72
- <pre>{hasValue}</pre>
72
+ <pre>{hasValue.value}</pre>
73
73
  </>
74
74
  }
75
75
 
@@ -108,11 +108,11 @@ describe('RippleSet', () => {
108
108
  it('creates RippleSet with initial values using RippleSet() shorthand syntax', () => {
109
109
  function SetTest() @{
110
110
  let items = new RippleSet([1, 2, 3, 4]);
111
- let &[hasValue] = track(() => items.has(3));
111
+ const hasValue = track(() => items.has(3));
112
112
  <>
113
113
  <button onClick={() => items.delete(3)}>{'delete'}</button>
114
114
  <pre>{items.size}</pre>
115
- <pre>{hasValue}</pre>
115
+ <pre>{hasValue.value}</pre>
116
116
  </>
117
117
  }
118
118
 
@@ -132,12 +132,12 @@ describe('RippleSet', () => {
132
132
  it('handles all operations with RippleSet() shorthand syntax', () => {
133
133
  function SetTest() @{
134
134
  let items = new RippleSet([10, 20, 30]);
135
- let &[values] = track(() => Array.from(items.values()));
135
+ const values = track(() => Array.from(items.values()));
136
136
  <>
137
137
  <button onClick={() => items.add(40)}>{'add'}</button>
138
138
  <button onClick={() => items.delete(20)}>{'delete'}</button>
139
139
  <button onClick={() => items.clear()}>{'clear'}</button>
140
- <pre>{JSON.stringify(values)}</pre>
140
+ <pre>{JSON.stringify(values.value)}</pre>
141
141
  <pre>{items.size}</pre>
142
142
  </>
143
143
  }
@@ -294,9 +294,9 @@ describe('SVG namespace handling', () => {
294
294
  }
295
295
 
296
296
  function App() @{
297
- let &[dynTag] = track('polygon');
297
+ const dynTag = track('polygon');
298
298
  <SVG>
299
- <{dynTag} points="0,0 30,0 15,10" />
299
+ <{dynTag.value} points="0,0 30,0 15,10" />
300
300
  </SVG>
301
301
  }
302
302
 
@@ -325,9 +325,9 @@ describe('SVG namespace handling', () => {
325
325
  }
326
326
 
327
327
  function App() @{
328
- let &[Component] = track(() => Polygon);
328
+ const Component = track(() => Polygon);
329
329
  <SVG>
330
- <{Component} points="0,0 30,0 15,10" />
330
+ <{Component.value} points="0,0 30,0 15,10" />
331
331
  </SVG>
332
332
  }
333
333
 
@@ -342,25 +342,25 @@ describe('SVG namespace handling', () => {
342
342
 
343
343
  it('should render SVG as a dynamic top element with any dynamic children elements', () => {
344
344
  function SVG({ children }: PropsWithChildren<{}>) @{
345
- let &[tag] = track('svg');
346
- <{tag}
345
+ const tag = track('svg');
346
+ <{tag.value}
347
347
  width={100}
348
348
  height={50}
349
349
  fill="red"
350
350
  viewBox="0 0 30 10"
351
351
  preserveAspectRatio="none"
352
- >{children}</{tag}>
352
+ >{children}</{tag.value}>
353
353
  }
354
354
 
355
355
  function Polygon({ points }: PropsWithExtras<{ points: string }>) @{
356
- let &[dynTag] = track('polygon');
357
- <{dynTag} {points} />
356
+ const dynTag = track('polygon');
357
+ <{dynTag.value} {points} />
358
358
  }
359
359
 
360
360
  function App() @{
361
- let &[Component] = track(() => Polygon);
361
+ const Component = track(() => Polygon);
362
362
  <SVG>
363
- <{Component} points="0,0 30,0 15,10" />
363
+ <{Component.value} points="0,0 30,0 15,10" />
364
364
  </SVG>
365
365
  }
366
366
 
@@ -3,11 +3,11 @@ import { flushSync, track } from 'ripple';
3
3
  describe('switch statements', () => {
4
4
  it('renders simple switch with literal cases', () => {
5
5
  function App() @{
6
- let &[value] = track('b');
6
+ const value = track('b');
7
7
  <>
8
- <button onClick={() => (value = 'c')}>{'Change to C'}</button>
9
- <button onClick={() => (value = 'a')}>{'Change to A'}</button>
10
- @switch (value) {
8
+ <button onClick={() => (value.value = 'c')}>{'Change to C'}</button>
9
+ <button onClick={() => (value.value = 'a')}>{'Change to A'}</button>
10
+ @switch (value.value) {
11
11
  @case 'a': {
12
12
  <div>Case A</div>
13
13
  }
@@ -39,10 +39,10 @@ describe('switch statements', () => {
39
39
 
40
40
  it('renders switch with reactive discriminant', () => {
41
41
  function App() @{
42
- let &[count] = track(1);
42
+ const count = track(1);
43
43
  <>
44
- <button onClick={() => count++}>{'Increment'}</button>
45
- @switch (count) {
44
+ <button onClick={() => count.value++}>{'Increment'}</button>
45
+ @switch (count.value) {
46
46
  @case 1: {
47
47
  <div>{'Count is 1'}</div>
48
48
  }
@@ -72,12 +72,12 @@ describe('switch statements', () => {
72
72
 
73
73
  it('renders switch with default clause only', () => {
74
74
  function App() @{
75
- let &[value] = track('x');
75
+ const value = track('x');
76
76
  <>
77
- <button onClick={() => (value = 'y')}>{'Change Value'}</button>
78
- @switch (value) {
77
+ <button onClick={() => (value.value = 'y')}>{'Change Value'}</button>
78
+ @switch (value.value) {
79
79
  @default: {
80
- <div>{'Default for ' + value}</div>
80
+ <div>{'Default for ' + value.value}</div>
81
81
  }
82
82
  }
83
83
  </>
@@ -94,11 +94,11 @@ describe('switch statements', () => {
94
94
 
95
95
  it('renders switch with isolated empty cases', () => {
96
96
  function App() @{
97
- let &[value] = track('a');
97
+ const value = track('a');
98
98
  <>
99
- <button onClick={() => (value = 'b')}>{'Change to B'}</button>
100
- <button onClick={() => (value = 'c')}>{'Change to C'}</button>
101
- @switch (value) {
99
+ <button onClick={() => (value.value = 'b')}>{'Change to B'}</button>
100
+ <button onClick={() => (value.value = 'c')}>{'Change to C'}</button>
101
+ @switch (value.value) {
102
102
  @case 'a': {
103
103
  <div>Case A</div>
104
104
  }
@@ -138,28 +138,28 @@ describe('switch statements', () => {
138
138
 
139
139
  it('renders switch with template content and reacts to tracked changes', () => {
140
140
  function App() @{
141
- let &[status] = track('active');
142
- let &[message] = track('');
141
+ const status = track('active');
142
+ const message = track('');
143
143
  <>
144
- <button onClick={() => (status = 'pending')}>{'Pending'}</button>
145
- <button onClick={() => (status = 'completed')}>{'Completed'}</button>
146
- <button onClick={() => (status = 'error')}>{'Error'}</button>
147
- @switch (status) {
144
+ <button onClick={() => (status.value = 'pending')}>{'Pending'}</button>
145
+ <button onClick={() => (status.value = 'completed')}>{'Completed'}</button>
146
+ <button onClick={() => (status.value = 'error')}>{'Error'}</button>
147
+ @switch (status.value) {
148
148
  @case 'active': {
149
- message = 'Currently active.';
150
- <div>{'Status: ' + message}</div>
149
+ message.value = 'Currently active.';
150
+ <div>{'Status: ' + message.value}</div>
151
151
  }
152
152
  @case 'pending': {
153
- message = 'Waiting for completion.';
154
- <div>{'Status: ' + message}</div>
153
+ message.value = 'Waiting for completion.';
154
+ <div>{'Status: ' + message.value}</div>
155
155
  }
156
156
  @case 'completed': {
157
- message = 'Task finished!';
158
- <div class="success">{'Status: ' + message}</div>
157
+ message.value = 'Task finished!';
158
+ <div class="success">{'Status: ' + message.value}</div>
159
159
  }
160
160
  @default: {
161
- message = 'An error occurred.';
162
- <div class="error">{'Status: ' + message}</div>
161
+ message.value = 'An error occurred.';
162
+ <div class="error">{'Status: ' + message.value}</div>
163
163
  }
164
164
  }
165
165
  </>
@@ -191,10 +191,10 @@ describe('switch statements', () => {
191
191
  'renders switch with isolated cases and reacts to tracked changes without recreating DOM unnecessarily',
192
192
  () => {
193
193
  function App() @{
194
- let &[status] = track(0);
194
+ const status = track(0);
195
195
  <>
196
196
  <div>
197
- @switch (status) {
197
+ @switch (status.value) {
198
198
  @case -1: {
199
199
  }
200
200
  @case 0: {
@@ -223,7 +223,7 @@ describe('switch statements', () => {
223
223
  </div>
224
224
  <button
225
225
  onClick={() => {
226
- status = (status + 1) % 5;
226
+ status.value = (status.value + 1) % 5;
227
227
  }}
228
228
  >{'Next Status'}</button>
229
229
  </>
@@ -263,19 +263,19 @@ describe('switch statements', () => {
263
263
 
264
264
  it('renders a middle switch case without falling through to later cases', () => {
265
265
  function App() @{
266
- let &[value] = track('x');
266
+ const value = track('x');
267
267
  <>
268
- <button onClick={() => (value = 'a')}>{'Set A'}</button>
269
- <button onClick={() => (value = 'b')}>{'Set B'}</button>
270
- <button onClick={() => (value = 'c')}>{'Set C'}</button>
271
- <button onClick={() => (value = 'x')}>{'Set X'}</button>
268
+ <button onClick={() => (value.value = 'a')}>{'Set A'}</button>
269
+ <button onClick={() => (value.value = 'b')}>{'Set B'}</button>
270
+ <button onClick={() => (value.value = 'c')}>{'Set C'}</button>
271
+ <button onClick={() => (value.value = 'x')}>{'Set X'}</button>
272
272
  <div>
273
- @switch (value) {
273
+ @switch (value.value) {
274
274
  @case 'a': {
275
275
  <div>{' Case A'}</div>
276
276
  }
277
277
  @case 'x': {
278
- <div>{' Default Case for ' + value}</div>
278
+ <div>{' Default Case for ' + value.value}</div>
279
279
  }
280
280
  @case 'b': {
281
281
  <div>{' Case B'}</div>
@@ -363,16 +363,16 @@ describe('switch statements', () => {
363
363
 
364
364
  it('renders switch with isolated block-scoped cases', () => {
365
365
  function App() @{
366
- let &[level] = track(1);
366
+ const level = track(1);
367
367
  <>
368
368
  <button
369
369
  onClick={() => {
370
- if (level === 1) level = 2;
371
- else if (level === 2) level = 3;
372
- else level = 1;
370
+ if (level.value === 1) level.value = 2;
371
+ else if (level.value === 2) level.value = 3;
372
+ else level.value = 1;
373
373
  }}
374
374
  >{'Toggle'}</button>
375
- @switch (level) {
375
+ @switch (level.value) {
376
376
  @case 1: {
377
377
  <div class="level">{'Level 1'}</div>
378
378
  }
@@ -7,8 +7,8 @@ describe('trackAsync hydration from serialized data (client)', () => {
7
7
 
8
8
  function App() @{
9
9
  @try {
10
- let &[data] = trackAsync(() => Promise.resolve('fallback'));
11
- <p class="result">{data}</p>
10
+ const data = trackAsync(() => Promise.resolve('fallback'));
11
+ <p class="result">{data.value}</p>
12
12
  } @pending {
13
13
  <p class="loading">{'loading...'}</p>
14
14
  }
@@ -31,8 +31,8 @@ describe('trackAsync hydration from serialized data (client)', () => {
31
31
 
32
32
  function App() @{
33
33
  @try {
34
- let &[value] = trackAsync(() => promise);
35
- <span class="value">{value}</span>
34
+ const value = trackAsync(() => promise);
35
+ <span class="value">{value.value}</span>
36
36
  } @pending {
37
37
  <span class="pending">{'waiting'}</span>
38
38
  }