ripple 0.2.215 → 0.3.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 (157) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/package.json +16 -7
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/identifier-utils.js +2 -0
  5. package/src/compiler/index.d.ts +2 -6
  6. package/src/compiler/phases/1-parse/index.js +171 -233
  7. package/src/compiler/phases/2-analyze/index.js +216 -16
  8. package/src/compiler/phases/2-analyze/prune.js +2 -2
  9. package/src/compiler/phases/3-transform/client/index.js +326 -94
  10. package/src/compiler/phases/3-transform/segments.js +43 -15
  11. package/src/compiler/phases/3-transform/server/index.js +71 -21
  12. package/src/compiler/scope.js +31 -12
  13. package/src/compiler/source-map-utils.js +4 -6
  14. package/src/compiler/types/acorn.d.ts +11 -0
  15. package/src/compiler/types/estree-jsx.d.ts +11 -0
  16. package/src/compiler/types/estree.d.ts +11 -0
  17. package/src/compiler/types/import.d.ts +32 -18
  18. package/src/compiler/types/index.d.ts +75 -23
  19. package/src/compiler/types/parse.d.ts +7 -10
  20. package/src/compiler/utils.js +48 -0
  21. package/src/runtime/array.js +53 -22
  22. package/src/runtime/date.js +15 -5
  23. package/src/runtime/index-client.js +41 -7
  24. package/src/runtime/index-server.js +7 -7
  25. package/src/runtime/internal/client/bindings.js +2 -2
  26. package/src/runtime/internal/client/blocks.js +40 -1
  27. package/src/runtime/internal/client/context.js +8 -0
  28. package/src/runtime/internal/client/for.js +3 -3
  29. package/src/runtime/internal/client/index.js +32 -5
  30. package/src/runtime/internal/client/render.js +20 -8
  31. package/src/runtime/internal/client/runtime.js +9 -7
  32. package/src/runtime/internal/client/template.js +1 -1
  33. package/src/runtime/internal/client/try.js +15 -22
  34. package/src/runtime/internal/client/utils.js +1 -1
  35. package/src/runtime/internal/server/context.js +8 -0
  36. package/src/runtime/internal/server/index.js +99 -6
  37. package/src/runtime/map.js +7 -7
  38. package/src/runtime/media-query.js +10 -1
  39. package/src/runtime/object.js +6 -6
  40. package/src/runtime/proxy.js +6 -6
  41. package/src/runtime/set.js +11 -11
  42. package/src/runtime/url-search-params.js +13 -2
  43. package/src/runtime/url.js +15 -5
  44. package/src/utils/builders.js +13 -3
  45. package/tests/client/array/array.copy-within.test.ripple +11 -11
  46. package/tests/client/array/array.derived.test.ripple +42 -42
  47. package/tests/client/array/array.iteration.test.ripple +12 -12
  48. package/tests/client/array/array.mutations.test.ripple +25 -25
  49. package/tests/client/array/array.static.test.ripple +103 -106
  50. package/tests/client/array/array.to-methods.test.ripple +8 -8
  51. package/tests/client/async-suspend.test.ripple +94 -0
  52. package/tests/client/basic/basic.attributes.test.ripple +31 -31
  53. package/tests/client/basic/basic.collections.test.ripple +7 -7
  54. package/tests/client/basic/basic.components.test.ripple +48 -10
  55. package/tests/client/basic/basic.errors.test.ripple +111 -30
  56. package/tests/client/basic/basic.events.test.ripple +11 -11
  57. package/tests/client/basic/basic.get-set.test.ripple +18 -18
  58. package/tests/client/basic/basic.reactivity.test.ripple +47 -42
  59. package/tests/client/basic/basic.rendering.test.ripple +7 -7
  60. package/tests/client/basic/basic.utilities.test.ripple +4 -4
  61. package/tests/client/boundaries.test.ripple +7 -7
  62. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +2 -2
  63. package/tests/client/compiler/compiler.assignments.test.ripple +21 -21
  64. package/tests/client/compiler/compiler.basic.test.ripple +223 -82
  65. package/tests/client/compiler/compiler.tracked-access.test.ripple +8 -9
  66. package/tests/client/composite/composite.dynamic-components.test.ripple +8 -8
  67. package/tests/client/composite/composite.generics.test.ripple +4 -4
  68. package/tests/client/composite/composite.props.test.ripple +9 -9
  69. package/tests/client/composite/composite.reactivity.test.ripple +32 -26
  70. package/tests/client/composite/composite.render.test.ripple +13 -4
  71. package/tests/client/computed-properties.test.ripple +3 -3
  72. package/tests/client/context.test.ripple +3 -3
  73. package/tests/client/css/global-additional-cases.test.ripple +4 -4
  74. package/tests/client/css/style-identifier.test.ripple +49 -41
  75. package/tests/client/date.test.ripple +40 -40
  76. package/tests/client/dynamic-elements.test.ripple +165 -30
  77. package/tests/client/events.test.ripple +25 -25
  78. package/tests/client/for.test.ripple +76 -8
  79. package/tests/client/function-overload.test.ripple +0 -1
  80. package/tests/client/head.test.ripple +7 -7
  81. package/tests/client/html.test.ripple +2 -2
  82. package/tests/client/input-value.test.ripple +174 -176
  83. package/tests/client/map.test.ripple +21 -21
  84. package/tests/client/media-query.test.ripple +4 -4
  85. package/tests/client/object.test.ripple +12 -12
  86. package/tests/client/portal.test.ripple +4 -4
  87. package/tests/client/ref.test.ripple +5 -5
  88. package/tests/client/return.test.ripple +17 -17
  89. package/tests/client/set.test.ripple +16 -16
  90. package/tests/client/svg.test.ripple +6 -7
  91. package/tests/client/switch.test.ripple +10 -10
  92. package/tests/client/tracked-expression.test.ripple +1 -3
  93. package/tests/client/try.test.ripple +56 -4
  94. package/tests/client/url/url.derived.test.ripple +10 -9
  95. package/tests/client/url/url.parsing.test.ripple +10 -10
  96. package/tests/client/url/url.partial-removal.test.ripple +10 -10
  97. package/tests/client/url/url.reactivity.test.ripple +17 -17
  98. package/tests/client/url/url.serialization.test.ripple +4 -4
  99. package/tests/client/url-search-params/url-search-params.derived.test.ripple +11 -10
  100. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +5 -7
  101. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +13 -13
  102. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +19 -19
  103. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +17 -17
  104. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +5 -5
  105. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +5 -5
  106. package/tests/hydration/compiled/client/events.js +8 -11
  107. package/tests/hydration/compiled/client/for.js +20 -23
  108. package/tests/hydration/compiled/client/head.js +17 -19
  109. package/tests/hydration/compiled/client/hmr.js +84 -0
  110. package/tests/hydration/compiled/client/html.js +1 -15
  111. package/tests/hydration/compiled/client/if-children.js +7 -9
  112. package/tests/hydration/compiled/client/if.js +5 -7
  113. package/tests/hydration/compiled/client/mixed-control-flow.js +3 -5
  114. package/tests/hydration/compiled/client/portal.js +1 -1
  115. package/tests/hydration/compiled/client/reactivity.js +9 -11
  116. package/tests/hydration/compiled/client/return.js +11 -13
  117. package/tests/hydration/compiled/client/switch.js +4 -6
  118. package/tests/hydration/compiled/server/basic.js +0 -1
  119. package/tests/hydration/compiled/server/composite.js +0 -3
  120. package/tests/hydration/compiled/server/events.js +8 -12
  121. package/tests/hydration/compiled/server/for.js +20 -23
  122. package/tests/hydration/compiled/server/head.js +17 -19
  123. package/tests/hydration/compiled/server/hmr.js +107 -0
  124. package/tests/hydration/compiled/server/html.js +1 -35
  125. package/tests/hydration/compiled/server/if-children.js +7 -11
  126. package/tests/hydration/compiled/server/if.js +5 -7
  127. package/tests/hydration/compiled/server/mixed-control-flow.js +3 -5
  128. package/tests/hydration/compiled/server/portal.js +1 -9
  129. package/tests/hydration/compiled/server/reactivity.js +9 -11
  130. package/tests/hydration/compiled/server/return.js +11 -13
  131. package/tests/hydration/compiled/server/switch.js +4 -6
  132. package/tests/hydration/components/events.ripple +8 -9
  133. package/tests/hydration/components/for.ripple +20 -21
  134. package/tests/hydration/components/head.ripple +6 -8
  135. package/tests/hydration/components/hmr.ripple +34 -0
  136. package/tests/hydration/components/html.ripple +1 -3
  137. package/tests/hydration/components/if-children.ripple +7 -8
  138. package/tests/hydration/components/if.ripple +5 -6
  139. package/tests/hydration/components/mixed-control-flow.ripple +4 -6
  140. package/tests/hydration/components/portal.ripple +1 -1
  141. package/tests/hydration/components/reactivity.ripple +9 -10
  142. package/tests/hydration/components/return.ripple +11 -12
  143. package/tests/hydration/components/switch.ripple +6 -8
  144. package/tests/hydration/hmr.test.js +74 -0
  145. package/tests/server/await.test.ripple +2 -2
  146. package/tests/server/basic.attributes.test.ripple +19 -21
  147. package/tests/server/basic.components.test.ripple +13 -7
  148. package/tests/server/basic.test.ripple +20 -21
  149. package/tests/server/compiler.test.ripple +5 -5
  150. package/tests/server/composite.props.test.ripple +6 -7
  151. package/tests/server/composite.test.ripple +4 -4
  152. package/tests/server/context.test.ripple +1 -3
  153. package/tests/server/dynamic-elements.test.ripple +24 -24
  154. package/tests/server/head.test.ripple +5 -7
  155. package/tests/server/style-identifier.test.ripple +16 -17
  156. package/types/index.d.ts +266 -62
  157. package/types/server.d.ts +6 -6
@@ -1,10 +1,10 @@
1
- import { flushSync, TrackedDate, track } from 'ripple';
1
+ import { flushSync } from 'ripple';
2
2
 
3
- describe('TrackedDate', () => {
3
+ describe('RippleDate', () => {
4
4
  it('handles getTime() with reactive updates', () => {
5
5
  component DateTest() {
6
- let date = new TrackedDate(2025, 0, 1);
7
- let time = track(() => date.getTime());
6
+ let date = #ripple.date(2025, 0, 1);
7
+ let time = #ripple.track(() => date.getTime());
8
8
 
9
9
  <button onClick={() => date.setFullYear(2026)}>{'Change Year'}</button>
10
10
  <pre>{@time}</pre>
@@ -25,8 +25,8 @@ describe('TrackedDate', () => {
25
25
 
26
26
  it('handles getFullYear() with reactive updates', () => {
27
27
  component DateTest() {
28
- let date = new TrackedDate(2025, 5, 15);
29
- let year = track(() => date.getFullYear());
28
+ let date = #ripple.date(2025, 5, 15);
29
+ let year = #ripple.track(() => date.getFullYear());
30
30
 
31
31
  <button onClick={() => date.setFullYear(2030)}>{'Change Year'}</button>
32
32
  <pre>{@year}</pre>
@@ -46,8 +46,8 @@ describe('TrackedDate', () => {
46
46
 
47
47
  it('handles getMonth() with reactive updates', () => {
48
48
  component DateTest() {
49
- let date = new TrackedDate(2025, 0, 15);
50
- let month = track(() => date.getMonth());
49
+ let date = #ripple.date(2025, 0, 15);
50
+ let month = #ripple.track(() => date.getMonth());
51
51
 
52
52
  <button onClick={() => date.setMonth(11)}>{'Change to December'}</button>
53
53
  <pre>{@month}</pre>
@@ -67,8 +67,8 @@ describe('TrackedDate', () => {
67
67
 
68
68
  it('handles getDate() with reactive updates', () => {
69
69
  component DateTest() {
70
- let date = new TrackedDate(2025, 0, 1);
71
- let day = track(() => date.getDate());
70
+ let date = #ripple.date(2025, 0, 1);
71
+ let day = #ripple.track(() => date.getDate());
72
72
 
73
73
  <button onClick={() => date.setDate(15)}>{'Change Day'}</button>
74
74
  <pre>{@day}</pre>
@@ -88,8 +88,8 @@ describe('TrackedDate', () => {
88
88
 
89
89
  it('handles getDay() with reactive updates', () => {
90
90
  component DateTest() {
91
- let date = new TrackedDate(2025, 0, 1);
92
- let dayOfWeek = track(() => date.getDay());
91
+ let date = #ripple.date(2025, 0, 1);
92
+ let dayOfWeek = #ripple.track(() => date.getDay());
93
93
 
94
94
  <button onClick={() => date.setDate(2)}>{'Next Day'}</button>
95
95
  <pre>{@dayOfWeek}</pre>
@@ -109,8 +109,8 @@ describe('TrackedDate', () => {
109
109
 
110
110
  it('handles getHours() with reactive updates', () => {
111
111
  component DateTest() {
112
- let date = new TrackedDate(2025, 0, 1, 10, 30, 0);
113
- let hours = track(() => date.getHours());
112
+ let date = #ripple.date(2025, 0, 1, 10, 30, 0);
113
+ let hours = #ripple.track(() => date.getHours());
114
114
 
115
115
  <button onClick={() => date.setHours(15)}>{'Change to 3 PM'}</button>
116
116
  <pre>{@hours}</pre>
@@ -130,8 +130,8 @@ describe('TrackedDate', () => {
130
130
 
131
131
  it('handles getMinutes() with reactive updates', () => {
132
132
  component DateTest() {
133
- let date = new TrackedDate(2025, 0, 1, 10, 15, 0);
134
- let minutes = track(() => date.getMinutes());
133
+ let date = #ripple.date(2025, 0, 1, 10, 15, 0);
134
+ let minutes = #ripple.track(() => date.getMinutes());
135
135
 
136
136
  <button onClick={() => date.setMinutes(45)}>{'Change Minutes'}</button>
137
137
  <pre>{@minutes}</pre>
@@ -151,8 +151,8 @@ describe('TrackedDate', () => {
151
151
 
152
152
  it('handles getSeconds() with reactive updates', () => {
153
153
  component DateTest() {
154
- let date = new TrackedDate(2025, 0, 1, 10, 15, 30);
155
- let seconds = track(() => date.getSeconds());
154
+ let date = #ripple.date(2025, 0, 1, 10, 15, 30);
155
+ let seconds = #ripple.track(() => date.getSeconds());
156
156
 
157
157
  <button onClick={() => date.setSeconds(45)}>{'Change Seconds'}</button>
158
158
  <pre>{@seconds}</pre>
@@ -172,8 +172,8 @@ describe('TrackedDate', () => {
172
172
 
173
173
  it('handles toISOString() with reactive updates', () => {
174
174
  component DateTest() {
175
- let date = new TrackedDate(2025, 0, 1, 12, 0, 0);
176
- let isoString = track(() => date.toISOString());
175
+ let date = #ripple.date(2025, 0, 1, 12, 0, 0);
176
+ let isoString = #ripple.track(() => date.toISOString());
177
177
 
178
178
  <button onClick={() => date.setFullYear(2026)}>{'Change Year'}</button>
179
179
  <pre>{@isoString}</pre>
@@ -198,8 +198,8 @@ describe('TrackedDate', () => {
198
198
 
199
199
  it('handles toDateString() with reactive updates', () => {
200
200
  component DateTest() {
201
- let date = new TrackedDate(2025, 0, 1);
202
- let dateString = track(() => date.toDateString());
201
+ let date = #ripple.date(2025, 0, 1);
202
+ let dateString = #ripple.track(() => date.toDateString());
203
203
 
204
204
  <button onClick={() => date.setMonth(11)}>{'Change to December'}</button>
205
205
  <pre>{@dateString}</pre>
@@ -222,8 +222,8 @@ describe('TrackedDate', () => {
222
222
 
223
223
  it('handles valueOf() with reactive updates', () => {
224
224
  component DateTest() {
225
- let date = new TrackedDate(2025, 0, 1);
226
- let valueOf = track(() => date.valueOf());
225
+ let date = #ripple.date(2025, 0, 1);
226
+ let valueOf = #ripple.track(() => date.valueOf());
227
227
 
228
228
  <button onClick={() => date.setDate(2)}>{'Next Day'}</button>
229
229
  <pre>{@valueOf}</pre>
@@ -244,11 +244,11 @@ describe('TrackedDate', () => {
244
244
 
245
245
  it('handles multiple get methods reacting to same setTime change', () => {
246
246
  component DateTest() {
247
- let date = new TrackedDate(2025, 0, 1, 10, 30, 15);
248
- let year = track(() => date.getFullYear());
249
- let month = track(() => date.getMonth());
250
- let day = track(() => date.getDate());
251
- let hours = track(() => date.getHours());
247
+ let date = #ripple.date(2025, 0, 1, 10, 30, 15);
248
+ let year = #ripple.track(() => date.getFullYear());
249
+ let month = #ripple.track(() => date.getMonth());
250
+ let day = #ripple.track(() => date.getDate());
251
+ let hours = #ripple.track(() => date.getHours());
252
252
 
253
253
  <button onClick={() => date.setTime(new Date(2026, 5, 15, 14, 45, 30).getTime())}>
254
254
  {'Change All'}
@@ -292,15 +292,15 @@ describe('TrackedDate', () => {
292
292
 
293
293
  it('handles constructor with different parameter combinations', () => {
294
294
  component DateTest() {
295
- let dateNow = new TrackedDate();
296
- let dateFromString = new TrackedDate('2025-01-01');
297
- let dateFromNumbers = new TrackedDate(2025, 0, 1);
298
- let dateFromTimestamp = new TrackedDate(1735689600000);
295
+ let dateNow = #ripple.date();
296
+ let dateFromString = #ripple.date('2025-01-01');
297
+ let dateFromNumbers = #ripple.date(2025, 0, 1);
298
+ let dateFromTimestamp = #ripple.date(1735689600000);
299
299
 
300
- let nowYear = track(() => dateNow.getFullYear());
301
- let stringYear = track(() => dateFromString.getFullYear());
302
- let numbersYear = track(() => dateFromNumbers.getFullYear());
303
- let timestampYear = track(() => dateFromTimestamp.getFullYear());
300
+ let nowYear = #ripple.track(() => dateNow.getFullYear());
301
+ let stringYear = #ripple.track(() => dateFromString.getFullYear());
302
+ let numbersYear = #ripple.track(() => dateFromNumbers.getFullYear());
303
+ let timestampYear = #ripple.track(() => dateFromTimestamp.getFullYear());
304
304
 
305
305
  <div>
306
306
  {'Now: '}
@@ -341,9 +341,9 @@ describe('TrackedDate', () => {
341
341
 
342
342
  it('handles get methods with arguments non-memoized', () => {
343
343
  component DateTest() {
344
- let date = new TrackedDate();
345
- let localeDateString = track(() => date.toLocaleDateString('en-US'));
346
- let localeTimeString = track(() => date.toLocaleTimeString('en-US'));
344
+ let date = #ripple.date();
345
+ let localeDateString = #ripple.track(() => date.toLocaleDateString('en-US'));
346
+ let localeTimeString = #ripple.track(() => date.toLocaleTimeString('en-US'));
347
347
 
348
348
  <button onClick={() => date.setFullYear(date.getFullYear() + 1)}>{'Next Year'}</button>
349
349
  <div>
@@ -1,10 +1,10 @@
1
1
  import type { PropsWithExtras } from 'ripple';
2
- import { flushSync, track, createRefKey, trackSplit } from 'ripple';
2
+ import { flushSync, createRefKey } from 'ripple';
3
3
 
4
4
  describe('dynamic DOM elements', () => {
5
5
  it('renders static dynamic element', () => {
6
6
  component App() {
7
- let tag = track('div');
7
+ let tag = #ripple.track('div');
8
8
 
9
9
  <@tag>{'Hello World'}</@tag>
10
10
  }
@@ -19,7 +19,7 @@ describe('dynamic DOM elements', () => {
19
19
  // They can be ignored for now. But we'll fix them via jsx() vs <jsx>
20
20
  it('renders static dynamic element from a plain object with a tracked property', () => {
21
21
  component App() {
22
- let obj = { tag: track('div') };
22
+ let obj = { tag: #ripple.track('div') };
23
23
 
24
24
  <obj.@tag>{'Hello World'}</obj.@tag>
25
25
  }
@@ -32,7 +32,7 @@ describe('dynamic DOM elements', () => {
32
32
 
33
33
  it('renders static dynamic element from a tracked object with a tracked property', () => {
34
34
  component App() {
35
- let obj = track({ tag: track('div') });
35
+ let obj = #ripple.track({ tag: #ripple.track('div') });
36
36
 
37
37
  <@obj.@tag>{'Hello World'}</@obj.@tag>
38
38
  }
@@ -47,7 +47,7 @@ describe('dynamic DOM elements', () => {
47
47
  'renders static dynamic element from a tracked object with a computed tracked property',
48
48
  () => {
49
49
  component App() {
50
- let obj = track({ tag: track('div') });
50
+ let obj = #ripple.track({ tag: #ripple.track('div') });
51
51
 
52
52
  <@obj.@['tag']>{'Hello World'}</@obj.@['tag']>
53
53
  }
@@ -61,7 +61,7 @@ describe('dynamic DOM elements', () => {
61
61
 
62
62
  it('renders reactive dynamic element', () => {
63
63
  component App() {
64
- let tag = track('div');
64
+ let tag = #ripple.track('div');
65
65
 
66
66
  <button
67
67
  onClick={() => {
@@ -92,7 +92,7 @@ describe('dynamic DOM elements', () => {
92
92
 
93
93
  it('renders self-closing dynamic element', () => {
94
94
  component App() {
95
- let tag = track('input');
95
+ let tag = #ripple.track('input');
96
96
 
97
97
  <@tag type="text" value="test" />
98
98
  }
@@ -106,8 +106,8 @@ describe('dynamic DOM elements', () => {
106
106
 
107
107
  it('handles dynamic element with attributes', () => {
108
108
  component App() {
109
- let tag = track('div');
110
- let className = track('test-class');
109
+ let tag = #ripple.track('div');
110
+ let className = #ripple.track('test-class');
111
111
 
112
112
  <@tag class={@className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
113
113
  }
@@ -122,8 +122,8 @@ describe('dynamic DOM elements', () => {
122
122
 
123
123
  it('handles nested dynamic elements', () => {
124
124
  component App() {
125
- let outerTag = track('div');
126
- let innerTag = track('span');
125
+ let outerTag = #ripple.track('div');
126
+ let innerTag = #ripple.track('span');
127
127
 
128
128
  <@outerTag class="outer">
129
129
  <@innerTag class="inner">{'Nested content'}</@innerTag>
@@ -142,8 +142,8 @@ describe('dynamic DOM elements', () => {
142
142
 
143
143
  it('handles dynamic element with class object', () => {
144
144
  component App() {
145
- let tag = track('div');
146
- let active = track(true);
145
+ let tag = #ripple.track('div');
146
+ let active = #ripple.track(true);
147
147
 
148
148
  <@tag class={{ active: @active, 'dynamic-element': true }}>
149
149
  {'Element with class object'}
@@ -159,7 +159,7 @@ describe('dynamic DOM elements', () => {
159
159
 
160
160
  it('handles dynamic element with style object', () => {
161
161
  component App() {
162
- let tag = track('span');
162
+ let tag = #ripple.track('span');
163
163
 
164
164
  <@tag
165
165
  style={{
@@ -182,7 +182,7 @@ describe('dynamic DOM elements', () => {
182
182
 
183
183
  it('handles dynamic element with spread attributes', () => {
184
184
  component App() {
185
- let tag = track('section');
185
+ let tag = #ripple.track('section');
186
186
  const attrs = {
187
187
  id: 'spread-section',
188
188
  'data-testid': 'spread-test',
@@ -205,7 +205,7 @@ describe('dynamic DOM elements', () => {
205
205
  let capturedElement: HTMLElement | null = null;
206
206
 
207
207
  component App() {
208
- let tag = track('article');
208
+ let tag = #ripple.track('article');
209
209
 
210
210
  <@tag
211
211
  {ref (node: HTMLElement) => {
@@ -227,7 +227,7 @@ describe('dynamic DOM elements', () => {
227
227
 
228
228
  it('handles dynamic element with createRefKey in spread', () => {
229
229
  component App() {
230
- let tag = track('header');
230
+ let tag = #ripple.track('header');
231
231
 
232
232
  function elementRef(node: HTMLElement) {
233
233
  // Set an attribute on the element to prove ref was called
@@ -257,8 +257,8 @@ describe('dynamic DOM elements', () => {
257
257
 
258
258
  it('has reactive attributes on dynamic elements', () => {
259
259
  component App() {
260
- let tag = track('div');
261
- let count = track(0);
260
+ let tag = #ripple.track('div');
261
+ let count = #ripple.track(0);
262
262
 
263
263
  <button
264
264
  onClick={() => {
@@ -311,7 +311,7 @@ describe('dynamic DOM elements', () => {
311
311
 
312
312
  it('applies scoped CSS to dynamic elements', () => {
313
313
  component App() {
314
- let tag = track('div');
314
+ let tag = #ripple.track('div');
315
315
 
316
316
  <@tag class="test-class">{'Dynamic element'}</@tag>
317
317
 
@@ -336,8 +336,8 @@ describe('dynamic DOM elements', () => {
336
336
 
337
337
  it('applies scoped CSS to dynamic elements with reactive classes', () => {
338
338
  component App() {
339
- let tag = track('button');
340
- let count = track(0);
339
+ let tag = #ripple.track('button');
340
+ let count = #ripple.track(0);
341
341
 
342
342
  <@tag
343
343
  class={@count % 2 ? 'even' : 'odd'}
@@ -410,8 +410,8 @@ describe('dynamic DOM elements', () => {
410
410
  id: string;
411
411
  onClick: EventListener;
412
412
  }>) {
413
- const tag = track('button');
414
- const [rest] = trackSplit(props, []);
413
+ const tag = #ripple.track('button');
414
+ const [rest] = #ripple.trackSplit(props, []);
415
415
  <@tag {...@rest}>{@rest.class}</@tag>
416
416
 
417
417
  <style>
@@ -425,7 +425,7 @@ describe('dynamic DOM elements', () => {
425
425
  }
426
426
 
427
427
  component App() {
428
- const count = track(0);
428
+ const count = #ripple.track(0);
429
429
  <DynamicButton
430
430
  class={@count % 2 ? 'even' : 'odd'}
431
431
  id={@count % 2 ? 'even' : 'odd'}
@@ -468,7 +468,7 @@ describe('dynamic DOM elements', () => {
468
468
 
469
469
  it('adds scoping class to dynamic elements', () => {
470
470
  component App() {
471
- let tag = track('div');
471
+ let tag = #ripple.track('div');
472
472
 
473
473
  <@tag class="scoped">
474
474
  <p>{'Scoped dynamic element'}</p>
@@ -491,7 +491,7 @@ describe('dynamic DOM elements', () => {
491
491
 
492
492
  it('adds scoping class to dynamic elements when selector targets by tag name', () => {
493
493
  component App() {
494
- let tag = track('div');
494
+ let tag = #ripple.track('div');
495
495
 
496
496
  <@tag class="scoped">
497
497
  <p>{'Scoped dynamic element'}</p>
@@ -526,7 +526,7 @@ describe('dynamic DOM elements', () => {
526
526
  }
527
527
 
528
528
  component App() {
529
- let tag = track('div');
529
+ let tag = #ripple.track('div');
530
530
 
531
531
  <@tag class="scoped">
532
532
  <p>{'Scoped dynamic element'}</p>
@@ -571,7 +571,7 @@ describe('dynamic DOM elements', () => {
571
571
  }
572
572
 
573
573
  component App() {
574
- let tag = track(() => Child);
574
+ let tag = #ripple.track(() => Child);
575
575
 
576
576
  <@tag />
577
577
 
@@ -593,9 +593,144 @@ describe('dynamic DOM elements', () => {
593
593
  expect(innerScopes).toHaveLength(0);
594
594
  });
595
595
 
596
+ it('handles ref on dynamic element passed through component with reactive props', () => {
597
+ let capturedElement: HTMLElement | null = null;
598
+ let refCallCount = 0;
599
+
600
+ component Button(props: any) {
601
+ const el = #ripple.track('button');
602
+ <@el {...props} />
603
+ }
604
+
605
+ component App() {
606
+ let active = #ripple.track(false);
607
+
608
+ <Button
609
+ data-active={String(@active)}
610
+ onClick={() => {
611
+ @active = !@active;
612
+ }}
613
+ {ref (el: HTMLElement) => {
614
+ capturedElement = el;
615
+ refCallCount++;
616
+ }}
617
+ >
618
+ {'content'}
619
+ </Button>
620
+ }
621
+
622
+ render(App);
623
+ flushSync();
624
+
625
+ expect(capturedElement).toBeTruthy();
626
+ expect(capturedElement!.tagName).toBe('BUTTON');
627
+ expect(capturedElement!.getAttribute('data-active')).toBe('false');
628
+ const initialRefCount = refCallCount;
629
+
630
+ // Click the button to trigger reactive prop update
631
+ capturedElement!.click();
632
+ flushSync();
633
+
634
+ // After clicking, the reactive prop should update without error
635
+ expect(capturedElement!.getAttribute('data-active')).toBe('true');
636
+ // Ref block should not have been recreated on prop update
637
+ expect(refCallCount).toBe(initialRefCount);
638
+ });
639
+
640
+ it('handles ref on dynamic element with spread props containing reactive values', () => {
641
+ let capturedElement: HTMLElement | null = null;
642
+
643
+ component Button(props: any) {
644
+ const el = #ripple.track('button');
645
+ <@el {...props} />
646
+ }
647
+
648
+ component App() {
649
+ let active = #ripple.track(false);
650
+
651
+ let buttonProps = #ripple.track(
652
+ () => ({
653
+ 'data-active': @active,
654
+ }),
655
+ );
656
+
657
+ <Button
658
+ {...@buttonProps}
659
+ onClick={() => {
660
+ @active = !@active;
661
+ }}
662
+ {ref (el: HTMLElement) => {
663
+ capturedElement = el;
664
+ }}
665
+ >
666
+ {'content: '}
667
+ {@active}
668
+ </Button>
669
+ }
670
+
671
+ render(App);
672
+ flushSync();
673
+
674
+ expect(capturedElement).toBeTruthy();
675
+ expect(capturedElement!.tagName).toBe('BUTTON');
676
+
677
+ // Click the button to trigger reactive update
678
+ capturedElement!.click();
679
+ flushSync();
680
+
681
+ // Should not throw, and ref should still be valid
682
+ expect(capturedElement!.tagName).toBe('BUTTON');
683
+ });
684
+
685
+ it('re-establishes ref with cleanup after parent block re-runs', () => {
686
+ let cleanupCount = 0;
687
+ let refCallCount = 0;
688
+ let capturedElement: HTMLElement | null = null;
689
+
690
+ component Button(props: any) {
691
+ const el = #ripple.track('button');
692
+ <@el {...props} />
693
+ }
694
+
695
+ component App() {
696
+ let active = #ripple.track(false);
697
+
698
+ <Button
699
+ data-active={String(@active)}
700
+ onClick={() => {
701
+ @active = !@active;
702
+ }}
703
+ {ref (el: HTMLElement) => {
704
+ capturedElement = el;
705
+ refCallCount++;
706
+ return () => {
707
+ cleanupCount++;
708
+ };
709
+ }}
710
+ >
711
+ {'content'}
712
+ </Button>
713
+ }
714
+
715
+ render(App);
716
+ flushSync();
717
+
718
+ expect(capturedElement).toBeTruthy();
719
+ expect(refCallCount).toBe(1);
720
+ expect(cleanupCount).toBe(0);
721
+
722
+ // Click to trigger reactive prop update
723
+ capturedElement!.click();
724
+ flushSync();
725
+
726
+ // Ref with cleanup should be re-established after parent teardown cycle
727
+ expect(capturedElement!.getAttribute('data-active')).toBe('true');
728
+ expect(refCallCount).toBeGreaterThanOrEqual(1);
729
+ });
730
+
596
731
  it('should remove and add back a text node in a conditional statement with a tracked', () => {
597
732
  component App() {
598
- let b = track(true);
733
+ let b = #ripple.track(true);
599
734
  <div>
600
735
  if (@b) {
601
736
  {'Inside if'}
@@ -1,11 +1,11 @@
1
1
  import type { OnEventListenerRemover } from 'ripple';
2
- import { track, flushSync, on, effect } from 'ripple';
2
+ import { flushSync, on } from 'ripple';
3
3
 
4
4
  describe('on() event handler', () => {
5
5
  it('should attach multiple handlers via onClick attribute (delegated)', () => {
6
6
  component Basic() {
7
- let count1 = track(0);
8
- let count2 = track(0);
7
+ let count1 = #ripple.track(0);
8
+ let count2 = #ripple.track(0);
9
9
 
10
10
  <button
11
11
  onClick={() => {
@@ -36,7 +36,7 @@ describe('on() event handler', () => {
36
36
 
37
37
  it('should attach and remove a single event handler', () => {
38
38
  component Basic() {
39
- let count = track(0);
39
+ let count = #ripple.track(0);
40
40
 
41
41
  const setupListener = (node: HTMLButtonElement) => {
42
42
  const remove = on(node, 'click', () => {
@@ -67,8 +67,8 @@ describe('on() event handler', () => {
67
67
 
68
68
  it('should handle multiple different event types on same element', () => {
69
69
  component Basic() {
70
- let clickCount = track(0);
71
- let mousedownCount = track(0);
70
+ let clickCount = #ripple.track(0);
71
+ let mousedownCount = #ripple.track(0);
72
72
 
73
73
  const setupListeners = (node: HTMLButtonElement) => {
74
74
  const remove1 = on(node, 'click', () => {
@@ -116,7 +116,7 @@ describe('on() event handler', () => {
116
116
 
117
117
  it('should handle multiple handlers for same event type on same element', () => {
118
118
  component Basic() {
119
- let callOrder = track<number[]>([]);
119
+ let callOrder = #ripple.track<number[]>([]);
120
120
 
121
121
  const setupListeners = (node: HTMLButtonElement) => {
122
122
  const remove1 = on(node, 'click', () => {
@@ -158,9 +158,9 @@ describe('on() event handler', () => {
158
158
 
159
159
  it('should remove specific handler without affecting others', () => {
160
160
  component Basic() {
161
- let handler1Called = track(0);
162
- let handler2Called = track(0);
163
- let handler3Called = track(0);
161
+ let handler1Called = #ripple.track(0);
162
+ let handler2Called = #ripple.track(0);
163
+ let handler3Called = #ripple.track(0);
164
164
  let removeHandler2: OnEventListenerRemover | undefined;
165
165
 
166
166
  const setupListeners = (node: HTMLButtonElement) => {
@@ -235,8 +235,8 @@ describe('on() event handler', () => {
235
235
  'should handle change event with multiple handlers (like bindChecked and bindIndeterminate)',
236
236
  () => {
237
237
  component Basic() {
238
- let checked = track(false);
239
- let indeterminate = track(true);
238
+ let checked = #ripple.track(false);
239
+ let indeterminate = #ripple.track(true);
240
240
 
241
241
  const setupListeners = (node: HTMLInputElement) => {
242
242
  node.indeterminate = @indeterminate;
@@ -283,7 +283,7 @@ describe('on() event handler', () => {
283
283
 
284
284
  it('should support non-delegated events', () => {
285
285
  component Basic() {
286
- let focusCount = track(0);
286
+ let focusCount = #ripple.track(0);
287
287
 
288
288
  const setupListener = (node: HTMLInputElement) => {
289
289
  const remove = on(node, 'focus', () => {
@@ -315,7 +315,7 @@ describe('on() event handler', () => {
315
315
 
316
316
  it('should handle removal of all handlers for same event type', () => {
317
317
  component Basic() {
318
- let count = track(0);
318
+ let count = #ripple.track(0);
319
319
  let remove1: OnEventListenerRemover | undefined;
320
320
  let remove2: OnEventListenerRemover | undefined;
321
321
  let remove3: OnEventListenerRemover | undefined;
@@ -379,7 +379,7 @@ describe('on() event handler', () => {
379
379
 
380
380
  it('should not add duplicate handlers when same handler is attached multiple times', () => {
381
381
  component Basic() {
382
- let count = track(0);
382
+ let count = #ripple.track(0);
383
383
 
384
384
  const sharedHandler = () => {
385
385
  @count++;
@@ -422,7 +422,7 @@ describe('on() event handler', () => {
422
422
 
423
423
  it('should allow duplicate handlers when delegated is false (no deduplication)', () => {
424
424
  component Basic() {
425
- let count = track(0);
425
+ let count = #ripple.track(0);
426
426
 
427
427
  const sharedHandler = () => {
428
428
  @count++;
@@ -466,7 +466,7 @@ describe('on() event handler', () => {
466
466
 
467
467
  it('should fire capture event on parent before bubbling event on child', () => {
468
468
  component Basic() {
469
- let callOrder = track<string[]>([]);
469
+ let callOrder = #ripple.track<string[]>([]);
470
470
 
471
471
  const parentCaptureHandler = () => {
472
472
  @callOrder = [...@callOrder, 'parent-capture'];
@@ -511,8 +511,8 @@ describe('on() event handler', () => {
511
511
 
512
512
  it('should fire handler only once when once option is true', () => {
513
513
  component Basic() {
514
- let count = track(0);
515
- let permanentCount = track(0);
514
+ let count = #ripple.track(0);
515
+ let permanentCount = #ripple.track(0);
516
516
 
517
517
  const setupListeners = (node: HTMLButtonElement) => {
518
518
  const onceHandler = on(node, 'click', () => {
@@ -565,9 +565,9 @@ describe('on() event handler', () => {
565
565
 
566
566
  it('should handle click events on window', () => {
567
567
  component Basic() {
568
- let windowClickCount = track(0);
568
+ let windowClickCount = #ripple.track(0);
569
569
 
570
- effect(() => {
570
+ #ripple.effect(() => {
571
571
  const removeWindowListener = on(window, 'click', () => {
572
572
  @windowClickCount++;
573
573
  });
@@ -601,9 +601,9 @@ describe('on() event handler', () => {
601
601
 
602
602
  it('should handle click events on document', () => {
603
603
  component Basic() {
604
- let documentClickCount = track(0);
604
+ let documentClickCount = #ripple.track(0);
605
605
 
606
- effect(() => {
606
+ #ripple.effect(() => {
607
607
  const removeDocumentListener = on(document, 'click', () => {
608
608
  @documentClickCount++;
609
609
  });
@@ -637,9 +637,9 @@ describe('on() event handler', () => {
637
637
 
638
638
  it('should handle click events on body', () => {
639
639
  component Basic() {
640
- let bodyClickCount = track(0);
640
+ let bodyClickCount = #ripple.track(0);
641
641
 
642
- effect(() => {
642
+ #ripple.effect(() => {
643
643
  const removeBodyListener = on(document.body, 'click', () => {
644
644
  @bodyClickCount++;
645
645
  });