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
@@ -196,8 +196,8 @@ export function App() @{
196
196
  const { code } = compile(source, 'primitive-merge.tsrx', { mode: 'client' });
197
197
 
198
198
  // One merged text expression — no separate comment-anchored expression.
199
- expect(code).toContain('\'label:\' + _$_.with_scope');
200
- expect(code).toContain('String(void');
199
+ // The coercion itself needs no scope; only the user call inside it does.
200
+ expect(code).toContain('\'label:\' + String(void _$_.with_scope');
201
201
  expect(code).not.toContain('_$_.expression(');
202
202
  });
203
203
 
@@ -211,8 +211,10 @@ export function App() @{
211
211
 
212
212
  const { code } = compile(source, 'string-merge.tsrx', { mode: 'client' });
213
213
 
214
- // Already a string: no String(… ?? '') double wrap around it.
215
- expect(code).toContain('String(fetchLabel())');
214
+ // Already a string: no String(… ?? '') double wrap around it, and the
215
+ // scope wrapper sits on the user call, not on the coercion.
216
+ expect(code).toContain('String(_$_.with_scope(__prev.___block, fetchLabel))');
217
+ expect(code).not.toContain('_$_.with_scope(__prev.___block, () => String(');
216
218
  expect(code).not.toContain('String(String(');
217
219
  expect(code).not.toContain('?? \'\'');
218
220
  });
@@ -579,23 +581,6 @@ function App() @{
579
581
  expect(result).toMatch(/value:\s*value\.value/);
580
582
  });
581
583
 
582
- it('keeps lazy destructuring as plain destructuring in to_ts output', () => {
583
- const track_source = `
584
- import { track } from 'ripple';
585
- function App() @{
586
- let &[value, ...rest] = track(0);
587
- const x = value;
588
- }
589
- `;
590
- const track_result = compile_to_volar_mappings(track_source, 'test.tsrx').code;
591
- expect(track_result).toContain('let [value, ...rest] = track(0);');
592
- expect(track_result).toContain('const x = value;');
593
- expect(track_result).not.toContain('let lazy = track(0)');
594
- expect(track_result).not.toContain('.slice(');
595
- expect(track_result).not.toContain('_$_.get(');
596
- expect(track_result).not.toContain('lazy0');
597
- });
598
-
599
584
  it('lowers native expression values in to_ts output', () => {
600
585
  const source = `
601
586
  function App() @{
@@ -827,37 +812,6 @@ export function optionalFn(declRequired: string, declMaybe?: string) {
827
812
  expect_identifier_mapping('declMaybe', 'declMaybe?: string');
828
813
  });
829
814
 
830
- it('uses tracked fast path for nested lazy params typed as Tracked', () => {
831
- const source = `
832
- import type { Tracked } from 'ripple';
833
- function use_nested({ value: &[count, tracked] }: { value: Tracked<number> }) {
834
- count++;
835
- return tracked;
836
- }
837
- `;
838
- const { code } = compile(source, 'tracked-nested-lazy.tsrx', { mode: 'client' });
839
-
840
- // Nested lazy array should still use tracked tuple fast path from outer annotation.
841
- expect(code).toContain('_$_.update(');
842
- expect(code).not.toContain('[0]');
843
- expect(code).not.toContain('[1]');
844
- });
845
-
846
- it('uses tracked fast path for nested lazy params at tuple rest positions', () => {
847
- const source = `
848
- import type { Tracked } from 'ripple';
849
- function use_tuple_rest({ value: [head, &[count, tracked]] }: { value: [number, ...Tracked<number>[]] }) {
850
- count++;
851
- return tracked;
852
- }
853
- `;
854
- const { code } = compile(source, 'tracked-nested-lazy-tuple-rest.tsrx', { mode: 'client' });
855
-
856
- // Tuple rest element access should resolve to Tracked<number>, not Tracked<number>[].
857
- expect(code).toContain('_$_.update(');
858
- expect(code).not.toContain('[1]');
859
- });
860
-
861
815
  it('preserves generic type args in interface extends for Volar mappings', () => {
862
816
  const source = `
863
817
  interface PolymorphicProps<T extends keyof HTMLElementTagNameMap> {
@@ -882,13 +836,13 @@ export function App(props: Props) @{
882
836
  const source = `
883
837
  import { track } from 'ripple';
884
838
  export function App() @{
885
- let &[level] = track(1);
839
+ const level = track(1);
886
840
 
887
841
  <button
888
842
  onClick={() => {
889
- if (level === 1) level = 2;
890
- else if (level === 2) level = 3;
891
- else level = 1;
843
+ if (level.value === 1) level.value = 2;
844
+ else if (level.value === 2) level.value = 3;
845
+ else level.value = 1;
892
846
  }}
893
847
  >
894
848
  {'Toggle'}
@@ -1026,6 +980,7 @@ export function App() @{
1026
980
  const result = compile(source, 'test.tsrx', { mode: 'client' }).code;
1027
981
 
1028
982
  // Template children should take precedence - explicit children prop should be removed
983
+ // The template children win: one `children` key, holding the element.
1029
984
  expect((result.match(/children:/g) || []).length).toBe(1);
1030
985
  expect(result).toContain('children: _$_.tsrx_element(');
1031
986
  });
@@ -1076,10 +1031,10 @@ export function App() @{
1076
1031
  import { track, effect, untrack } from 'ripple';
1077
1032
 
1078
1033
  function App() @{
1079
- let &[count] = track(0);
1034
+ const count = track(0);
1080
1035
 
1081
1036
  effect(() => {
1082
- const snapshot = untrack(() => count);
1037
+ const snapshot = untrack(() => count.value);
1083
1038
  console.log(snapshot);
1084
1039
  });
1085
1040
  }
@@ -1275,7 +1230,7 @@ export function App() @{
1275
1230
  },
1276
1231
  );
1277
1232
 
1278
- it('uses spread_props for spreads that may contain children', () => {
1233
+ it('spreads props objects as plain object spreads', () => {
1279
1234
  const source = `
1280
1235
  function Card(props) @{
1281
1236
  <div>{props.children}</div>
@@ -1290,7 +1245,7 @@ export function App() @{
1290
1245
 
1291
1246
  const result = compile(source, 'test.tsrx', { mode: 'client' }).code;
1292
1247
 
1293
- expect(result).toContain('_$_.spread_props(');
1248
+ expect(result).toContain('_$_.render_component(Card, __anchor, { ...props })');
1294
1249
  });
1295
1250
 
1296
1251
  it('reads a less-than at line start in element children as literal text', () => {
@@ -1,7 +1,6 @@
1
1
  import { compile } from '@tsrx/ripple';
2
2
 
3
- const value_message = /Use \.value or &\[\] lazy destructuring/;
4
- const reference_message = /Use the tracked value directly instead/;
3
+ const value_message = /Accessing a tracked object directly is not allowed, use `\.value`/;
5
4
 
6
5
  describe('Compiler: Tracked Object Direct Access Checks', () => {
7
6
  it('should error on direct access to __v of a tracked object', () => {
@@ -74,18 +73,6 @@ import { track } from 'ripple';
74
73
  );
75
74
  });
76
75
 
77
- it('should compile successfully with correct @ syntax access', () => {
78
- const code = `
79
- import { track } from 'ripple';
80
- export default function App() @{
81
- let &[count] = track(0);
82
- console.log(count);
83
- <></>
84
- }
85
- `;
86
- expect(() => compile(code, 'test.tsrx')).not.toThrow();
87
- });
88
-
89
76
  it('should compile successfully with correct value access', () => {
90
77
  const code = `
91
78
  import { track } from 'ripple';
@@ -156,7 +143,7 @@ import { track } from 'ripple';
156
143
  <></>
157
144
  }
158
145
  `;
159
- expect(() => compile(code, 'test.tsrx')).toThrow(reference_message);
146
+ expect(() => compile(code, 'test.tsrx')).toThrow(value_message);
160
147
  });
161
148
 
162
149
  it('should error on indexed [0] write on a tracked object', () => {
@@ -183,63 +170,49 @@ import { track } from 'ripple';
183
170
  `;
184
171
  const result = compile(code, 'test.tsrx', { loose: true });
185
172
 
186
- expect(result.errors.map((error) => error.message)).toEqual(
187
- expect.arrayContaining([
188
- expect.stringMatching(value_message),
189
- expect.stringMatching(reference_message),
190
- ]),
191
- );
173
+ expect(result.errors.map((error) => error.message)).toEqual([
174
+ expect.stringMatching(value_message),
175
+ expect.stringMatching(value_message),
176
+ ]);
192
177
  });
193
178
 
194
- it('should error on indexed access through a known tracked lazy ref binding', () => {
195
- const code = `
196
- import { track } from 'ripple';
197
- export default function App() @{
198
- let &[value, tracked_ref] = track(0);
199
- tracked_ref[0]++;
200
- <></>
201
- }
202
- `;
203
- expect(() => compile(code, 'test.tsrx')).toThrow(value_message);
204
- });
205
-
206
- it('should allow lazy destructuring a tracked value', () => {
179
+ it('should allow .value read access on a tracked object', () => {
207
180
  const code = `
208
181
  import { track } from 'ripple';
209
182
  export default function App() @{
210
- let &[value, tracked_ref] = track(0);
211
- console.log(value, tracked_ref.value);
183
+ let count = track(0);
184
+ console.log(count.value);
212
185
  <></>
213
186
  }
214
187
  `;
215
188
  expect(() => compile(code, 'test.tsrx')).not.toThrow();
216
189
  });
217
190
 
218
- it('should allow .value read access on a tracked object', () => {
191
+ it('should allow .value assignment on a tracked object', () => {
219
192
  const code = `
220
193
  import { track } from 'ripple';
221
194
  export default function App() @{
222
195
  let count = track(0);
223
- console.log(count.value);
196
+ count.value = 5;
224
197
  <></>
225
198
  }
226
199
  `;
227
200
  expect(() => compile(code, 'test.tsrx')).not.toThrow();
228
201
  });
229
202
 
230
- it('should allow .value assignment on a tracked object', () => {
203
+ it('should allow .readOnly() on a tracked object', () => {
231
204
  const code = `
232
205
  import { track } from 'ripple';
233
206
  export default function App() @{
234
207
  let count = track(0);
235
- count.value = 5;
236
- <></>
208
+ const view = count.readOnly();
209
+ <>{view.value}</>
237
210
  }
238
211
  `;
239
212
  expect(() => compile(code, 'test.tsrx')).not.toThrow();
240
213
  });
241
214
 
242
- it('should allow .length read access on a tracked object', () => {
215
+ it('should error on .length access on a tracked object', () => {
243
216
  const code = `
244
217
  import { track } from 'ripple';
245
218
  export default function App() @{
@@ -248,6 +221,8 @@ import { track } from 'ripple';
248
221
  <></>
249
222
  }
250
223
  `;
251
- expect(() => compile(code, 'test.tsrx')).not.toThrow();
224
+ expect(() => compile(code, 'test.tsrx')).toThrow(
225
+ 'Accessing a tracked object directly is not allowed',
226
+ );
252
227
  });
253
228
  });
@@ -74,7 +74,7 @@ describe('compiler > Volar transform does not duplicate try blocks from function
74
74
  const source = `import { trackAsync } from 'ripple';
75
75
  export function App() @{
76
76
  try {
77
- let &[data] = trackAsync(() => fetch('/api'));
77
+ const data = trackAsync(() => fetch('/api'));
78
78
  } catch (e) {}
79
79
  <div>{"hi"}</div>
80
80
  }`;
@@ -88,7 +88,7 @@ export function App() @{
88
88
  const source = `import { trackAsync } from 'ripple';
89
89
  export function App() @{
90
90
  try {
91
- let &[data] = trackAsync(() => fetch('/init'));
91
+ const data = trackAsync(() => fetch('/init'));
92
92
  } catch (e) {}
93
93
 
94
94
  async function refresh() {
@@ -14,7 +14,6 @@ exports[`composite > render > correct handles passing through component props an
14
14
  <div>
15
15
  I am B
16
16
  </div>
17
- <!---->
18
17
  </div>
19
18
 
20
19
  </div>
@@ -1,4 +1,4 @@
1
- import { flushSync, track } from 'ripple';
1
+ import { type Component, flushSync, track } from 'ripple';
2
2
 
3
3
  describe('composite > dynamic components', () => {
4
4
  it('supports rendering composite components using dynamic tag syntax', () => {
@@ -47,8 +47,8 @@ describe('composite > dynamic components', () => {
47
47
  const obj = {
48
48
  tracked_basic,
49
49
  };
50
- let &[inner] = track(obj);
51
- const comp = inner.tracked_basic;
50
+ const inner = track(obj);
51
+ const comp = inner.value.tracked_basic;
52
52
  <{comp} />
53
53
  }
54
54
 
@@ -86,13 +86,13 @@ describe('composite > dynamic components', () => {
86
86
  }
87
87
 
88
88
  function App() @{
89
- let &[thing] = track(() => Child1);
89
+ const thing = track(() => Child1, undefined, true);
90
90
  <>
91
91
  <div id="container">
92
- <{thing} />
92
+ <{thing.value} />
93
93
  </div>
94
94
  <button
95
- onClick={() => (thing = thing === Child1 ? Child2 : Child1)}
95
+ onClick={() => (thing.value = thing.value === Child1 ? Child2 : Child1)}
96
96
  >{'Change Child'}</button>
97
97
  </>
98
98
  }
@@ -113,4 +113,75 @@ describe('composite > dynamic components', () => {
113
113
 
114
114
  expect(container.querySelector('#container').textContent).toBe('I am child 1');
115
115
  });
116
+
117
+ it('switches components held in a plain tracked assigned after creation', () => {
118
+ function Child1() @{
119
+ <div>{'I am child 1'}</div>
120
+ }
121
+
122
+ function Child2() @{
123
+ <div>{'I am child 2'}</div>
124
+ }
125
+
126
+ function App() @{
127
+ // track(Child1) would make a derived that calls the component, so an
128
+ // empty tracked receives the component through `.value` instead.
129
+ const thing = track<Component>();
130
+ thing.value = Child1;
131
+ <>
132
+ <div id="container">
133
+ <{thing.value} />
134
+ </div>
135
+ <button
136
+ onClick={() => (thing.value = thing.value === Child1 ? Child2 : Child1)}
137
+ >{'Change Child'}</button>
138
+ </>
139
+ }
140
+
141
+ render(App);
142
+ flushSync();
143
+
144
+ expect(container.querySelector('#container').textContent).toBe('I am child 1');
145
+
146
+ const button = container.querySelector('button');
147
+ button.click();
148
+ flushSync();
149
+
150
+ expect(container.querySelector('#container').textContent).toBe('I am child 2');
151
+
152
+ button.click();
153
+ flushSync();
154
+
155
+ expect(container.querySelector('#container').textContent).toBe('I am child 1');
156
+ });
157
+
158
+ it('switches plain functions held in a tracked assigned after creation', () => {
159
+ const first = () => 'first';
160
+ const second = () => 'second';
161
+
162
+ function App() @{
163
+ const fn = track<() => string>();
164
+ fn.value = first;
165
+ <>
166
+ <p>{fn.value()}</p>
167
+ <button onClick={() => (fn.value = fn.value === first ? second : first)}>{'Swap'}</button>
168
+ </>
169
+ }
170
+
171
+ render(App);
172
+ flushSync();
173
+
174
+ expect(container.querySelector('p').textContent).toBe('first');
175
+
176
+ const button = container.querySelector('button');
177
+ button.click();
178
+ flushSync();
179
+
180
+ expect(container.querySelector('p').textContent).toBe('second');
181
+
182
+ button.click();
183
+ flushSync();
184
+
185
+ expect(container.querySelector('p').textContent).toBe('first');
186
+ });
116
187
  });
@@ -1,17 +1,150 @@
1
- import type { Tracked, Props } from 'ripple';
1
+ import type { Derived, Tracked, Props } from 'ripple';
2
2
  import { effect, flushSync, track } from 'ripple';
3
3
 
4
4
  describe('composite > props', () => {
5
+ it('hands a child a read-only view through readOnly()', () => {
6
+ function Child({ count }: { count: Derived<number> }) @{
7
+ <span class="child">{count.value}</span>
8
+ }
9
+
10
+ function App() @{
11
+ const count = track(0);
12
+ <>
13
+ <Child count={count.readOnly()} />
14
+ <button onClick={() => count.value++}>{'inc'}</button>
15
+ </>
16
+ }
17
+
18
+ render(App);
19
+ flushSync();
20
+ expect(container.querySelector('.child').textContent).toBe('0');
21
+
22
+ container.querySelector('button').click();
23
+ flushSync();
24
+ expect(container.querySelector('.child').textContent).toBe('1');
25
+ });
26
+
27
+ it('returns a read-only derived from readOnly() as is and wraps a writable one', () => {
28
+ function App() @{
29
+ const count = track(1);
30
+ const double = track(() => count.value * 2);
31
+ const draft = track(() => count.value, undefined, true);
32
+ const double_view = double.readOnly();
33
+ const draft_view = draft.readOnly();
34
+ expect(double_view).toBe(double);
35
+ expect(draft_view).not.toBe(draft);
36
+ <>
37
+ <span class="draft">{draft.value}</span>
38
+ <span class="view">{draft_view.value}</span>
39
+ <button onClick={() => (draft.value = 42)}>{'write'}</button>
40
+ </>
41
+ }
42
+
43
+ render(App);
44
+ flushSync();
45
+ expect(container.querySelector('.view').textContent).toBe('1');
46
+
47
+ // A write to the writable derived is visible through its view.
48
+ container.querySelector('button').click();
49
+ flushSync();
50
+ expect(container.querySelector('.draft').textContent).toBe('42');
51
+ expect(container.querySelector('.view').textContent).toBe('42');
52
+ });
53
+
54
+ it('releases a readOnly() view with the block that created it', () => {
55
+ let set_rows: (rows: number[]) => void = () => {};
56
+ let bump: () => void = () => {};
57
+ let source: Tracked<number> | undefined;
58
+
59
+ function Child({ count }: { count: Derived<number> }) @{
60
+ <span class="child">{count.value}</span>
61
+ }
62
+
63
+ function App() @{
64
+ const count = track(0);
65
+ const rows = track<number[]>([]);
66
+ source = count;
67
+ set_rows = (next) => {
68
+ rows.value = next;
69
+ };
70
+ bump = () => {
71
+ count.value++;
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
+ // Each view is a derived subscribed to the source; the subscriber list is
83
+ // the runtime's own (`sb` / `sn`).
84
+ const subscribers = () => {
85
+ let n = 0;
86
+ for (let d = (source as any).sb; d !== null; d = d.sn) n++;
87
+ return n;
88
+ };
89
+
90
+ render(App);
91
+ flushSync();
92
+ expect(subscribers()).toBe(0);
93
+
94
+ set_rows([1, 2, 3]);
95
+ flushSync();
96
+ expect(container.querySelectorAll('.child').length).toBe(3);
97
+ expect(subscribers()).toBe(3);
98
+
99
+ // Removing the items destroys the blocks that created the views, so the
100
+ // next write to the source prunes them instead of keeping them alive.
101
+ set_rows([]);
102
+ flushSync();
103
+ bump();
104
+ flushSync();
105
+ expect(container.querySelectorAll('.child').length).toBe(0);
106
+ expect(subscribers()).toBe(0);
107
+ });
108
+
109
+ it('keeps a readOnly() view from writing through to its source', () => {
110
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
111
+ try {
112
+ function App() @{
113
+ const count = track(1);
114
+ const view = count.readOnly();
115
+ const write = () => {
116
+ view.value = 99;
117
+ };
118
+ <>
119
+ <span class="source">{count.value}</span>
120
+ <span class="view">{view.value}</span>
121
+ <button onClick={write}>{'write'}</button>
122
+ </>
123
+ }
124
+
125
+ render(App);
126
+ flushSync();
127
+ container.querySelector('button').click();
128
+ flushSync();
129
+
130
+ // The write warned in development and never reached the source.
131
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('read-only derived'));
132
+ expect(container.querySelector('.source').textContent).toBe('1');
133
+ } finally {
134
+ warn.mockRestore();
135
+ }
136
+ });
137
+
5
138
  it('correctly handles default prop values', () => {
6
139
  function Child({ foo = 456 }) @{
7
140
  <div>{foo}</div>
8
141
  }
9
142
 
10
143
  function App() @{
11
- let &[foo] = track(123);
144
+ const foo = track(123);
12
145
  <>
13
146
  <Child />
14
- <Child {foo} />
147
+ <Child foo={foo.value} />
15
148
  </>
16
149
  }
17
150
 
@@ -67,10 +200,10 @@ describe('composite > props', () => {
67
200
  }
68
201
 
69
202
  function App() @{
70
- let &[foo] = track(123);
203
+ const foo = track(123);
71
204
  <>
72
205
  <Child />
73
- <Child {foo} />
206
+ <Child foo={foo.value} />
74
207
  </>
75
208
  }
76
209
 
@@ -110,7 +243,7 @@ describe('composite > props', () => {
110
243
  });
111
244
 
112
245
  it('correctly retains prop accessors and reactivity when using rest props', () => {
113
- function Button(&{ children, ...rest }: Props) @{
246
+ function Button({ children, ...rest }: Props) @{
114
247
  <>
115
248
  <button {...rest}>{children}</button>
116
249
  <style>
@@ -124,11 +257,15 @@ describe('composite > props', () => {
124
257
  </>
125
258
  }
126
259
 
127
- function Toggle(&{ pressed, ...rest }: { pressed: Tracked<boolean> }) @{
260
+ function Toggle({ pressed, ...rest }: { pressed: Tracked<boolean> }) @{
128
261
  const onClick = () => (pressed.value = !pressed.value);
129
262
  <>
130
- <Button {...rest} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
131
- <Button class={pressed.value ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
263
+ <Button
264
+ {...rest}
265
+ class={track(() => (pressed.value ? 'on' : 'off'))}
266
+ {onClick}
267
+ >{'button 1'}</Button>
268
+ <Button class={track(() => (pressed.value ? 'on' : 'off'))} {onClick}>{'button 2'}</Button>
132
269
  </>
133
270
  }
134
271
 
@@ -215,16 +352,16 @@ describe('composite > props', () => {
215
352
  }
216
353
 
217
354
  function App() @{
218
- let &[ok] = track(true);
355
+ const ok = track(true);
219
356
  <>
220
357
  <button
221
358
  onClick={() => {
222
- ok = !ok;
359
+ ok.value = !ok.value;
223
360
  }}
224
361
  >{'toggle'}</button>
225
362
  <Child
226
363
  header={<h1>
227
- @if (ok) {
364
+ @if (ok.value) {
228
365
  <span>{'yes'}</span>
229
366
  } @else {
230
367
  <span>{'no'}</span>