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,370 @@
1
+ import type { Derived, Tracked } from 'ripple';
2
+ import { effect, flushSync, track } from 'ripple';
3
+
4
+ /** The runtime's own subscriber list of a tracked value (`sb` / `sn`). */
5
+ function subscribers(source: Tracked<number>) {
6
+ let n = 0;
7
+ for (let d = (source as any).sb; d !== null; d = d.sn) n++;
8
+ return n;
9
+ }
10
+
11
+ function Child({ count }: { count: Derived<number> }) @{
12
+ <span class="child">{count.value}</span>
13
+ }
14
+
15
+ describe('derived release', () => {
16
+ it('releases a derived created in an @if branch when the branch toggles off', () => {
17
+ let toggle: () => void = () => {};
18
+ let bump: () => void = () => {};
19
+ let source: Tracked<number> | undefined;
20
+
21
+ function App() @{
22
+ const count = track(0);
23
+ const show = track(false);
24
+ source = count;
25
+ toggle = () => {
26
+ show.value = !show.value;
27
+ };
28
+ bump = () => {
29
+ count.value++;
30
+ };
31
+ <>
32
+ <p>{count.value}</p>
33
+ @if (show.value) {
34
+ const label = track(() => 'n=' + count.value);
35
+ <>
36
+ <span class="label">{label.value}</span>
37
+ <Child count={count.readOnly()} />
38
+ </>
39
+ }
40
+ </>
41
+ }
42
+
43
+ render(App);
44
+ flushSync();
45
+ const before = subscribers(source!);
46
+
47
+ for (let i = 0; i < 5; i++) {
48
+ toggle();
49
+ flushSync();
50
+ expect(container.querySelector('.label')!.textContent).toBe(`n=${i}`);
51
+ expect(subscribers(source!)).toBe(before + 2);
52
+ toggle();
53
+ flushSync();
54
+ // Both deriveds of the old branch are gone before the next write.
55
+ expect(subscribers(source!)).toBe(before);
56
+ bump();
57
+ flushSync();
58
+ expect(subscribers(source!)).toBe(before);
59
+ }
60
+ });
61
+
62
+ it('releases deriveds created in keyed @for items when the items are removed', () => {
63
+ let set_rows: (rows: number[]) => void = () => {};
64
+ let source: Tracked<number> | undefined;
65
+
66
+ function App() @{
67
+ const count = track(0);
68
+ const rows = track<number[]>([]);
69
+ source = count;
70
+ set_rows = (next) => {
71
+ rows.value = next;
72
+ };
73
+ <ul>
74
+ @for (const row of rows.value; key row) {
75
+ <li>
76
+ <Child count={count.readOnly()} />
77
+ </li>
78
+ }
79
+ </ul>
80
+ }
81
+
82
+ render(App);
83
+ flushSync();
84
+ set_rows([1, 2, 3]);
85
+ flushSync();
86
+ expect(subscribers(source!)).toBe(3);
87
+
88
+ set_rows([2]);
89
+ flushSync();
90
+ expect(subscribers(source!)).toBe(1);
91
+
92
+ set_rows([]);
93
+ flushSync();
94
+ expect(subscribers(source!)).toBe(0);
95
+ });
96
+
97
+ it('releases a derived created in component setup when the component unmounts', () => {
98
+ let toggle: () => void = () => {};
99
+ let source: Tracked<number> | undefined;
100
+
101
+ function Doubled({ count }: { count: Tracked<number> }) @{
102
+ const double = track(() => count.value * 2);
103
+ <span class="double">{double.value}</span>
104
+ }
105
+
106
+ function App() @{
107
+ const count = track(1);
108
+ const show = track(true);
109
+ source = count;
110
+ toggle = () => {
111
+ show.value = !show.value;
112
+ };
113
+ <>
114
+ @if (show.value) {
115
+ <Doubled {count} />
116
+ }
117
+ </>
118
+ }
119
+
120
+ render(App);
121
+ flushSync();
122
+ expect(container.querySelector('.double')!.textContent).toBe('2');
123
+ expect(subscribers(source!)).toBe(1);
124
+
125
+ toggle();
126
+ flushSync();
127
+ expect(container.querySelector('.double')).toBeNull();
128
+ expect(subscribers(source!)).toBe(0);
129
+ });
130
+
131
+ it('keeps a derived that a longer-lived reader still holds', () => {
132
+ let toggle: () => void = () => {};
133
+ let bump: () => void = () => {};
134
+
135
+ function App() @{
136
+ const count = track(1);
137
+ const show = track(true);
138
+ const held = track<Derived<number> | null>(null);
139
+ toggle = () => {
140
+ show.value = !show.value;
141
+ };
142
+ bump = () => {
143
+ count.value++;
144
+ };
145
+ <>
146
+ <p class="outer">
147
+ {held.value === null ? '-' : held.value.value}
148
+ </p>
149
+ @if (show.value) {
150
+ const double = track(() => count.value * 2);
151
+ held.value = double;
152
+ <span class="inner">{double.value}</span>
153
+ }
154
+ </>
155
+ }
156
+
157
+ render(App);
158
+ flushSync();
159
+ expect(container.querySelector('.outer')!.textContent).toBe('2');
160
+
161
+ // The branch that created the derived is gone, but the outer text still
162
+ // reads it, so it stays subscribed and keeps following the source.
163
+ toggle();
164
+ flushSync();
165
+ expect(container.querySelector('.inner')).toBeNull();
166
+ bump();
167
+ flushSync();
168
+ expect(container.querySelector('.outer')!.textContent).toBe('4');
169
+ });
170
+ });
171
+
172
+ describe('derived release across reruns', () => {
173
+ it('keeps track of deriveds across reruns that stay on the same branch', () => {
174
+ let bump: () => void = () => {};
175
+ let reset: () => void = () => {};
176
+ let source: Tracked<number> | undefined;
177
+
178
+ function App() @{
179
+ const count = track(1);
180
+ source = count;
181
+ bump = () => {
182
+ count.value++;
183
+ };
184
+ reset = () => {
185
+ count.value = 0;
186
+ };
187
+ <>
188
+ @if (count.value > 0) {
189
+ const double = track(() => count.value * 2);
190
+ <p class="double">{double.value}</p>
191
+ }
192
+ </>
193
+ }
194
+
195
+ render(App);
196
+ flushSync();
197
+ // The if block reads `count` for its condition; the derived is the other subscriber.
198
+ const before = subscribers(source!) - 1;
199
+
200
+ // Same branch on every rerun: nothing is created, nothing accumulates.
201
+ for (let i = 0; i < 4; i++) {
202
+ bump();
203
+ flushSync();
204
+ expect(container.querySelector('.double')!.textContent).toBe(String((i + 2) * 2));
205
+ expect(subscribers(source!)).toBe(before + 1);
206
+ }
207
+
208
+ // The branch finally goes away: its derived goes with it.
209
+ reset();
210
+ flushSync();
211
+ expect(container.querySelector('.double')).toBeNull();
212
+ expect(subscribers(source!)).toBe(before);
213
+ });
214
+
215
+ it('releases a derived a render expression created and read on its previous run', () => {
216
+ let bump: () => void = () => {};
217
+ let source: Tracked<number> | undefined;
218
+
219
+ function App() @{
220
+ const count = track(0);
221
+ source = count;
222
+ bump = () => {
223
+ count.value++;
224
+ };
225
+ <p class="value">{track(() => count.value * 10).value}</p>
226
+ }
227
+
228
+ render(App);
229
+ flushSync();
230
+ expect(container.querySelector('.value')!.textContent).toBe('0');
231
+ expect(subscribers(source!)).toBe(1);
232
+
233
+ // Each rerun creates a new derived and the previous one is released, so
234
+ // the source never sees more than the current one.
235
+ for (let i = 1; i <= 5; i++) {
236
+ bump();
237
+ flushSync();
238
+ expect(container.querySelector('.value')!.textContent).toBe(String(i * 10));
239
+ expect(subscribers(source!)).toBe(1);
240
+ }
241
+ });
242
+
243
+ it('releases a chain of deriveds after reruns that kept them, whatever their count', () => {
244
+ let bump: () => void = () => {};
245
+ let reset: () => void = () => {};
246
+ let source: Tracked<number> | undefined;
247
+
248
+ function App() @{
249
+ const count = track(1);
250
+ source = count;
251
+ bump = () => {
252
+ count.value++;
253
+ };
254
+ reset = () => {
255
+ count.value = 0;
256
+ };
257
+ <>
258
+ @if (count.value > 0) {
259
+ // `second` reads `first`; a release must visit `second` before
260
+ // `first`, in every pass, or `first` looks alive and survives.
261
+ const first = track(() => count.value * 2);
262
+ const second = track(() => first.value + 1);
263
+ <p class="second">{second.value}</p>
264
+ }
265
+ </>
266
+ }
267
+
268
+ render(App);
269
+ flushSync();
270
+ const before = subscribers(source!) - 1;
271
+
272
+ for (const same_branch_reruns of [1, 3, 2]) {
273
+ for (let i = 0; i < same_branch_reruns; i++) {
274
+ bump();
275
+ flushSync();
276
+ }
277
+ expect(subscribers(source!)).toBe(before + 1);
278
+ reset();
279
+ flushSync();
280
+ expect(container.querySelector('.second')).toBeNull();
281
+ expect(subscribers(source!)).toBe(before);
282
+ bump();
283
+ flushSync();
284
+ expect(container.querySelector('.second')!.textContent).toBe('3');
285
+ }
286
+ });
287
+
288
+ it('releases a derived an effect created and read on its previous run', () => {
289
+ let bump: () => void = () => {};
290
+ let source: Tracked<number> | undefined;
291
+ const seen: number[] = [];
292
+
293
+ function App() @{
294
+ const count = track(0);
295
+ source = count;
296
+ bump = () => {
297
+ count.value++;
298
+ };
299
+ effect(() => {
300
+ // On even counts the run makes a derived and reads it; on odd counts
301
+ // it does not, so the block's stale link to the previous run's
302
+ // derived is still in place when that derived is released. It must
303
+ // not count as a live reader.
304
+ const n = count.value;
305
+ if (n % 2 === 0) {
306
+ const tenfold = track(() => count.value * 10);
307
+ seen.push(tenfold.value);
308
+ } else {
309
+ seen.push(-1);
310
+ }
311
+ });
312
+ <p>{count.value}</p>
313
+ }
314
+
315
+ render(App);
316
+ flushSync();
317
+ expect(seen).toEqual([0]);
318
+ const before = subscribers(source!);
319
+
320
+ for (let i = 1; i <= 6; i++) {
321
+ bump();
322
+ flushSync();
323
+ expect(seen.at(-1)).toBe(i % 2 === 0 ? i * 10 : -1);
324
+ // Only the current run's derived, if any, is subscribed.
325
+ expect(subscribers(source!)).toBe(before - i % 2);
326
+ }
327
+ });
328
+
329
+ it('still releases deriveds when the rerun throws into a catch boundary', () => {
330
+ let fail: () => void = () => {};
331
+ let source: Tracked<number> | undefined;
332
+
333
+ function App() @{
334
+ const count = track(1);
335
+ const broken = track(false);
336
+ source = count;
337
+ fail = () => {
338
+ broken.value = true;
339
+ };
340
+ <>
341
+ @try {
342
+ @if (broken.value ? boom() : count.value > 0) {
343
+ const double = track(() => count.value * 2);
344
+ <p class="double">{double.value}</p>
345
+ }
346
+ } @catch (error) {
347
+ <p class="error">{'failed'}</p>
348
+ }
349
+ </>
350
+ }
351
+
352
+ function boom(): boolean {
353
+ throw new Error('condition failed');
354
+ }
355
+
356
+ render(App);
357
+ flushSync();
358
+ expect(container.querySelector('.double')!.textContent).toBe('2');
359
+ const with_branch = subscribers(source!);
360
+
361
+ // The if block's rerun throws before it can release anything; the
362
+ // boundary then tears the if block down, which must still find them.
363
+ fail();
364
+ flushSync();
365
+ expect(container.querySelector('.error')).not.toBeNull();
366
+ expect(container.querySelector('.double')).toBeNull();
367
+ expect(subscribers(source!)).toBeLessThan(with_branch);
368
+ expect(subscribers(source!)).toBe(0);
369
+ });
370
+ });
@@ -1,4 +1,4 @@
1
- import type { PropsWithExtras } from 'ripple';
1
+ import type { Derived, PropsWithExtras } from 'ripple';
2
2
  import { createRefKey, effect, flushSync, track } from 'ripple';
3
3
 
4
4
  describe('dynamic DOM elements', () => {
@@ -57,14 +57,14 @@ describe('dynamic DOM elements', () => {
57
57
 
58
58
  it('renders reactive dynamic element', () => {
59
59
  function App() @{
60
- let &[tag] = track('div');
60
+ const tag = track('div');
61
61
  <>
62
62
  <button
63
63
  onClick={() => {
64
- tag = 'span';
64
+ tag.value = 'span';
65
65
  }}
66
66
  >{'Change Tag'}</button>
67
- <{tag} id="dynamic">{'Hello World'}</{tag}>
67
+ <{tag.value} id="dynamic">{'Hello World'}</{tag.value}>
68
68
  </>
69
69
  }
70
70
  render(App);
@@ -101,8 +101,8 @@ describe('dynamic DOM elements', () => {
101
101
  it('handles dynamic element with attributes', () => {
102
102
  function App() @{
103
103
  let tag = track('div');
104
- let &[className] = track('test-class');
105
- <{tag} class={className} id="test" data-testid="dynamic-element">{'Content'}</{tag}>
104
+ const className = track('test-class');
105
+ <{tag} class={className.value} id="test" data-testid="dynamic-element">{'Content'}</{tag}>
106
106
  }
107
107
  render(App);
108
108
 
@@ -135,9 +135,9 @@ describe('dynamic DOM elements', () => {
135
135
  it('handles dynamic element with class object', () => {
136
136
  function App() @{
137
137
  let tag = track('div');
138
- let &[active] = track(true);
138
+ const active = track(true);
139
139
  <{tag}
140
- class={{ active: active, 'dynamic-element': true }}
140
+ class={{ active: active.value, 'dynamic-element': true }}
141
141
  >{'Element with class object'}</{tag}>
142
142
  }
143
143
  render(App);
@@ -301,14 +301,14 @@ describe('dynamic DOM elements', () => {
301
301
  }
302
302
 
303
303
  function App() @{
304
- let &[dynamic] = track(() => TextInput);
304
+ const dynamic = track(() => TextInput);
305
305
  <>
306
306
  <button
307
307
  onClick={() => {
308
- dynamic = dynamic === TextInput ? SearchInput : TextInput;
308
+ dynamic.value = dynamic.value === TextInput ? SearchInput : TextInput;
309
309
  }}
310
310
  >{'Change Component'}</button>
311
- <{dynamic}
311
+ <{dynamic.value}
312
312
  ref={[
313
313
  (node: HTMLInputElement | null) => {
314
314
  refAttrElement = node;
@@ -375,20 +375,20 @@ describe('dynamic DOM elements', () => {
375
375
  it('has reactive attributes on dynamic elements', () => {
376
376
  function App() @{
377
377
  let tag = track('div');
378
- let &[count] = track(0);
378
+ const count = track(0);
379
379
  <>
380
380
  <button
381
381
  onClick={() => {
382
- count++;
382
+ count.value++;
383
383
  }}
384
384
  >{'Increment'}</button>
385
385
  <{tag}
386
- id={count % 2 ? 'even' : 'odd'}
387
- class={count % 2 ? 'even-class' : 'odd-class'}
388
- data-count={count}
386
+ id={count.value % 2 ? 'even' : 'odd'}
387
+ class={count.value % 2 ? 'even-class' : 'odd-class'}
388
+ data-count={count.value}
389
389
  >
390
390
  {'Count: '}
391
- {count}
391
+ {count.value}
392
392
  </{tag}>
393
393
  </>
394
394
  }
@@ -453,17 +453,17 @@ describe('dynamic DOM elements', () => {
453
453
  it('applies scoped CSS to dynamic elements with reactive classes', () => {
454
454
  function App() @{
455
455
  let tag = track('button');
456
- let &[count] = track(0);
456
+ const count = track(0);
457
457
  <>
458
458
  <{tag}
459
- class={count % 2 ? 'even' : 'odd'}
460
- id={count % 2 ? 'even' : 'odd'}
459
+ class={count.value % 2 ? 'even' : 'odd'}
460
+ id={count.value % 2 ? 'even' : 'odd'}
461
461
  onClick={() => {
462
- count++;
462
+ count.value++;
463
463
  }}
464
464
  >
465
465
  {'Count: '}
466
- {count}
466
+ {count.value}
467
467
  </{tag}>
468
468
  <style>
469
469
  .even {
@@ -521,14 +521,14 @@ describe('dynamic DOM elements', () => {
521
521
  });
522
522
 
523
523
  it('handles spread attributes with class and CSS scoping ', () => {
524
- function DynamicButton(&{ ...rest }: PropsWithExtras<{
525
- class: string;
526
- id: string;
524
+ function DynamicButton({ ...rest }: PropsWithExtras<{
525
+ class: Derived<string>;
526
+ id: Derived<string>;
527
527
  onClick: EventListener;
528
528
  }>) @{
529
529
  const tag = track('button');
530
530
  <>
531
- <{tag} {...rest}>{rest.class}</{tag}>
531
+ <{tag} {...rest}>{rest.class.value}</{tag}>
532
532
  <style>
533
533
  .even {
534
534
  background-color: green;
@@ -541,12 +541,12 @@ describe('dynamic DOM elements', () => {
541
541
  }
542
542
 
543
543
  function App() @{
544
- let &[count] = track(0);
544
+ const count = track(0);
545
545
  <DynamicButton
546
- class={count % 2 ? 'even' : 'odd'}
547
- id={count % 2 ? 'even' : 'odd'}
546
+ class={track(() => (count.value % 2 ? 'even' : 'odd'))}
547
+ id={track(() => (count.value % 2 ? 'even' : 'odd'))}
548
548
  onClick={() => {
549
- count++;
549
+ count.value++;
550
550
  }}
551
551
  />
552
552
  }
@@ -724,11 +724,11 @@ describe('dynamic DOM elements', () => {
724
724
  }
725
725
 
726
726
  function App() @{
727
- let &[active] = track(false);
727
+ const active = track(false);
728
728
  <Button
729
- data-active={String(active)}
729
+ data-active={track(() => String(active.value))}
730
730
  onClick={() => {
731
- active = !active;
731
+ active.value = !active.value;
732
732
  }}
733
733
  ref={(el: HTMLElement) => {
734
734
  capturedElement = el;
@@ -764,23 +764,23 @@ describe('dynamic DOM elements', () => {
764
764
  }
765
765
 
766
766
  function App() @{
767
- let &[active] = track(false);
768
- let &[buttonProps] = track(
767
+ const active = track(false);
768
+ const buttonProps = track(
769
769
  () => ({
770
- 'data-active': active,
770
+ 'data-active': active.value,
771
771
  }),
772
772
  );
773
773
  <Button
774
- {...buttonProps}
774
+ {...buttonProps.value}
775
775
  onClick={() => {
776
- active = !active;
776
+ active.value = !active.value;
777
777
  }}
778
778
  ref={(el: HTMLElement) => {
779
779
  capturedElement = el;
780
780
  }}
781
781
  >
782
782
  {'content: '}
783
- {active}
783
+ {active.value}
784
784
  </Button>
785
785
  }
786
786
 
@@ -809,11 +809,11 @@ describe('dynamic DOM elements', () => {
809
809
  }
810
810
 
811
811
  function App() @{
812
- let &[active] = track(false);
812
+ const active = track(false);
813
813
  <Button
814
- data-active={String(active)}
814
+ data-active={track(() => String(active.value))}
815
815
  onClick={() => {
816
- active = !active;
816
+ active.value = !active.value;
817
817
  }}
818
818
  ref={(el: HTMLElement) => {
819
819
  capturedElement = el;
@@ -843,14 +843,14 @@ describe('dynamic DOM elements', () => {
843
843
 
844
844
  it('should remove and add back a text node in a conditional statement with a tracked', () => {
845
845
  function App() @{
846
- let &[b] = track(true);
846
+ const b = track(true);
847
847
  <>
848
848
  <div>
849
- @if (b) {
849
+ @if (b.value) {
850
850
  <>Inside if</>
851
851
  }
852
852
  </div>
853
- <button onClick={() => (b = !b)}>{'Toggle b'}</button>
853
+ <button onClick={() => (b.value = !b.value)}>{'Toggle b'}</button>
854
854
  </>
855
855
  }
856
856