ripple 0.3.128 → 0.4.1

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 (197) hide show
  1. package/CHANGELOG.md +294 -0
  2. package/package.json +11 -4
  3. package/src/jsx-runtime.d.ts +12 -2
  4. package/src/runtime/element.js +23 -6
  5. package/src/runtime/index-client.js +81 -41
  6. package/src/runtime/index-server.js +1 -0
  7. package/src/runtime/internal/client/blocks.js +173 -91
  8. package/src/runtime/internal/client/component.js +4 -30
  9. package/src/runtime/internal/client/composite.js +12 -13
  10. package/src/runtime/internal/client/constants.js +9 -0
  11. package/src/runtime/internal/client/context.js +12 -19
  12. package/src/runtime/internal/client/css.js +7 -9
  13. package/src/runtime/internal/client/events.js +178 -90
  14. package/src/runtime/internal/client/expression.js +45 -25
  15. package/src/runtime/internal/client/for.js +409 -129
  16. package/src/runtime/internal/client/head.js +10 -5
  17. package/src/runtime/internal/client/html.js +1 -0
  18. package/src/runtime/internal/client/hydration.js +152 -10
  19. package/src/runtime/internal/client/if.js +239 -50
  20. package/src/runtime/internal/client/index.js +4 -12
  21. package/src/runtime/internal/client/operations.js +40 -61
  22. package/src/runtime/internal/client/portal.js +163 -85
  23. package/src/runtime/internal/client/render.js +44 -43
  24. package/src/runtime/internal/client/rpc.js +3 -3
  25. package/src/runtime/internal/client/runtime.js +592 -433
  26. package/src/runtime/internal/client/selector.js +5 -2
  27. package/src/runtime/internal/client/template.js +133 -158
  28. package/src/runtime/internal/client/transport.js +38 -0
  29. package/src/runtime/internal/client/try.js +465 -426
  30. package/src/runtime/internal/client/types.d.ts +56 -23
  31. package/src/runtime/internal/client/utils.js +1 -3
  32. package/src/runtime/internal/server/dynamic.js +20 -21
  33. package/src/runtime/internal/server/index.js +329 -277
  34. package/src/runtime/internal/server/rpc.js +3 -2
  35. package/src/runtime/internal/server/transport.js +16 -0
  36. package/src/runtime/transport.js +21 -0
  37. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
  38. package/tests/client/array/array.copy-within.test.tsrx +12 -12
  39. package/tests/client/array/array.derived.test.tsrx +46 -46
  40. package/tests/client/array/array.iteration.test.tsrx +10 -10
  41. package/tests/client/array/array.mutations.test.tsrx +20 -20
  42. package/tests/client/array/array.static.test.tsrx +11 -11
  43. package/tests/client/array/array.to-methods.test.tsrx +6 -6
  44. package/tests/client/async-suspend.test.tsrx +74 -75
  45. package/tests/client/basic/basic.attributes.test.tsrx +123 -83
  46. package/tests/client/basic/basic.collections.test.tsrx +12 -12
  47. package/tests/client/basic/basic.components.test.tsrx +99 -25
  48. package/tests/client/basic/basic.errors.test.tsrx +18 -18
  49. package/tests/client/basic/basic.events.test.tsrx +42 -42
  50. package/tests/client/basic/basic.get-set.test.tsrx +8 -8
  51. package/tests/client/basic/basic.reactivity.test.tsrx +73 -38
  52. package/tests/client/basic/basic.rendering.test.tsrx +25 -25
  53. package/tests/client/basic/basic.utilities.test.tsrx +3 -3
  54. package/tests/client/boundaries.test.tsrx +8 -8
  55. package/tests/client/boxed-locals.test.tsrx +452 -0
  56. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
  57. package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
  58. package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
  59. package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
  60. package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
  61. package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
  62. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
  63. package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
  64. package/tests/client/composite/composite.props.test.tsrx +149 -12
  65. package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
  66. package/tests/client/composite/composite.render.test.tsrx +26 -9
  67. package/tests/client/css/scoped-styles.test.tsrx +8 -8
  68. package/tests/client/date.test.tsrx +42 -42
  69. package/tests/client/derived-release.test.tsrx +370 -0
  70. package/tests/client/dynamic-elements.test.tsrx +45 -45
  71. package/tests/client/events.test.tsrx +97 -71
  72. package/tests/client/for-selector.test.tsrx +14 -14
  73. package/tests/client/for-single-root-items.test.tsrx +118 -3
  74. package/tests/client/for.test.tsrx +332 -31
  75. package/tests/client/head.test.tsrx +19 -19
  76. package/tests/client/html.test.tsrx +11 -9
  77. package/tests/client/if-block.test.tsrx +80 -0
  78. package/tests/client/map.test.tsrx +16 -16
  79. package/tests/client/media-query.test.tsrx +7 -7
  80. package/tests/client/portal.test.tsrx +224 -26
  81. package/tests/client/primitive-text.test.tsrx +50 -0
  82. package/tests/client/prop-kind-switch.test.tsrx +141 -0
  83. package/tests/client/ref.test.tsrx +17 -17
  84. package/tests/client/return.test.tsrx +26 -3
  85. package/tests/client/root-boundary.test.tsrx +42 -0
  86. package/tests/client/scoped-flush.test.tsrx +8 -8
  87. package/tests/client/set.test.tsrx +6 -6
  88. package/tests/client/svg.test.tsrx +11 -11
  89. package/tests/client/switch.test.tsrx +44 -44
  90. package/tests/client/track-async-hydration.test.tsrx +4 -4
  91. package/tests/client/try.test.tsrx +151 -83
  92. package/tests/client/tsx.test.tsrx +43 -40
  93. package/tests/client/url/url.derived.test.tsrx +6 -6
  94. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
  95. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
  96. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
  97. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
  98. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
  99. package/tests/hydration/basic.test.js +74 -1
  100. package/tests/hydration/build-components.js +22 -1
  101. package/tests/hydration/compiled/client/basic.js +1142 -713
  102. package/tests/hydration/compiled/client/composite.js +151 -100
  103. package/tests/hydration/compiled/client/events.js +184 -171
  104. package/tests/hydration/compiled/client/for.js +1472 -1132
  105. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  106. package/tests/hydration/compiled/client/head.js +254 -201
  107. package/tests/hydration/compiled/client/hmr.js +54 -41
  108. package/tests/hydration/compiled/client/html-in-template.js +54 -39
  109. package/tests/hydration/compiled/client/html.js +1633 -1263
  110. package/tests/hydration/compiled/client/if-children.js +420 -270
  111. package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
  112. package/tests/hydration/compiled/client/if.js +229 -219
  113. package/tests/hydration/compiled/client/mixed-control-flow.js +420 -404
  114. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
  115. package/tests/hydration/compiled/client/portal.js +87 -95
  116. package/tests/hydration/compiled/client/reactivity.js +135 -101
  117. package/tests/hydration/compiled/client/return.js +28 -19
  118. package/tests/hydration/compiled/client/streaming.js +390 -306
  119. package/tests/hydration/compiled/client/switch.js +258 -266
  120. package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
  121. package/tests/hydration/compiled/client/try.js +173 -129
  122. package/tests/hydration/compiled/client/typed-text.js +71 -0
  123. package/tests/hydration/compiled/fixtures/money.js +10 -0
  124. package/tests/hydration/compiled/server/basic.js +123 -6
  125. package/tests/hydration/compiled/server/composite.js +16 -30
  126. package/tests/hydration/compiled/server/events.js +66 -18
  127. package/tests/hydration/compiled/server/for.js +117 -41
  128. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  129. package/tests/hydration/compiled/server/head.js +20 -20
  130. package/tests/hydration/compiled/server/hmr.js +2 -2
  131. package/tests/hydration/compiled/server/html.js +70 -238
  132. package/tests/hydration/compiled/server/if-children.js +107 -15
  133. package/tests/hydration/compiled/server/if.js +11 -15
  134. package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
  135. package/tests/hydration/compiled/server/portal.js +14 -22
  136. package/tests/hydration/compiled/server/reactivity.js +14 -14
  137. package/tests/hydration/compiled/server/streaming.js +19 -19
  138. package/tests/hydration/compiled/server/switch.js +8 -8
  139. package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
  140. package/tests/hydration/compiled/server/try.js +8 -8
  141. package/tests/hydration/compiled/server/typed-text.js +17 -0
  142. package/tests/hydration/components/basic.tsrx +44 -8
  143. package/tests/hydration/components/composite.tsrx +2 -2
  144. package/tests/hydration/components/events.tsrx +25 -25
  145. package/tests/hydration/components/for.tsrx +115 -67
  146. package/tests/hydration/components/fragment-cursor.tsrx +650 -0
  147. package/tests/hydration/components/head.tsrx +16 -16
  148. package/tests/hydration/components/hmr.tsrx +2 -2
  149. package/tests/hydration/components/html.tsrx +7 -7
  150. package/tests/hydration/components/if-children.tsrx +72 -25
  151. package/tests/hydration/components/if.tsrx +18 -18
  152. package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
  153. package/tests/hydration/components/portal.tsrx +3 -3
  154. package/tests/hydration/components/reactivity.tsrx +16 -16
  155. package/tests/hydration/components/streaming.tsrx +20 -20
  156. package/tests/hydration/components/switch.tsrx +20 -20
  157. package/tests/hydration/components/text-types.ts +4 -0
  158. package/tests/hydration/components/track-async-serialization.tsrx +80 -26
  159. package/tests/hydration/components/try.tsrx +8 -8
  160. package/tests/hydration/components/typed-text.tsrx +22 -0
  161. package/tests/hydration/fixtures/money.js +10 -0
  162. package/tests/hydration/for.test.js +36 -0
  163. package/tests/hydration/fragment-cursor.test.js +111 -0
  164. package/tests/hydration/if-children.test.js +24 -0
  165. package/tests/hydration/streaming.test.js +19 -0
  166. package/tests/hydration/track-async-serialization.test.js +87 -2
  167. package/tests/hydration/tsconfig.text-types.json +10 -0
  168. package/tests/hydration/typed-text.test.js +34 -0
  169. package/tests/server/await.test.tsrx +3 -3
  170. package/tests/server/basic.attributes.test.tsrx +70 -42
  171. package/tests/server/basic.components.test.tsrx +64 -8
  172. package/tests/server/basic.test.tsrx +34 -32
  173. package/tests/server/compiler.test.tsrx +1 -70
  174. package/tests/server/composite.props.test.tsrx +6 -6
  175. package/tests/server/dynamic-elements.test.tsrx +31 -8
  176. package/tests/server/for.test.tsrx +94 -0
  177. package/tests/server/head.test.tsrx +11 -11
  178. package/tests/server/scoped-styles.test.tsrx +2 -2
  179. package/tests/server/streaming-ssr.test.tsrx +48 -43
  180. package/tests/server/track-async-serialization.test.tsrx +204 -21
  181. package/tests/server/try.test.tsrx +48 -48
  182. package/tests/types/transport.ts +40 -0
  183. package/tests/utils/jsx-runtime-types.test.js +67 -2
  184. package/tests/utils/tracked-types.test.js +117 -0
  185. package/tests/utils/vite-plugin-config.test.js +23 -0
  186. package/types/index.d.ts +49 -9
  187. package/types/server.d.ts +3 -0
  188. package/types/transport.d.ts +15 -0
  189. package/src/runtime/internal/client/switch.js +0 -113
  190. package/src/utils/errors.js +0 -13
  191. package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +0 -12
  192. package/tests/client/compiler/__snapshots__/compiler.typescript.test.rsrx.snap +0 -46
  193. package/tests/client/lazy-array.test.tsrx +0 -31
  194. package/tests/client/lazy-destructuring.test.tsrx +0 -438
  195. package/tests/client/tracked-index-access.test.tsrx +0 -113
  196. package/tests/server/lazy-destructuring.test.tsrx +0 -595
  197. package/tests/server/tracked-index-access.test.tsrx +0 -76
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "skipLibCheck": true
8
+ },
9
+ "include": ["components/typed-text.tsrx", "components/text-types.ts"]
10
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { flushSync as flush_sync, hydrate } from 'ripple';
3
+ import { render } from 'ripple/server';
4
+ import { container } from '../setup-hydration.js';
5
+ import { TypedText as server_typed_text } from './compiled/server/typed-text.js';
6
+ import { TypedText as client_typed_text } from './compiled/client/typed-text.js';
7
+
8
+ describe('typed text hydration', () => {
9
+ it('reuses server text nodes and updates imported primitive types', async () => {
10
+ const { body } = await render(server_typed_text);
11
+ expect(body).toContain('<first>');
12
+ container.innerHTML = body;
13
+ const run = container.querySelector('.typed-run');
14
+ const text = run.firstChild;
15
+ const number = container.querySelector('.typed-number').firstChild;
16
+ const tail = run.querySelector('span');
17
+ const unmount = hydrate(client_typed_text, { target: container });
18
+ try {
19
+ expect(container.querySelector('.typed-run')).toBe(run);
20
+ expect(run.firstChild).toBe(text);
21
+ expect(run.textContent).toBe('value: <first>2tail');
22
+ expect(container.querySelector('.typed-number').firstChild).toBe(number);
23
+ container.querySelector('button').click();
24
+ flush_sync();
25
+ expect(run.textContent).toBe('value: &next3tail');
26
+ expect(run.firstChild).toBe(text);
27
+ expect(run.querySelector('span')).toBe(tail);
28
+ expect(container.querySelector('.typed-number').firstChild).toBe(number);
29
+ expect(number.textContent).toBe('3');
30
+ } finally {
31
+ unmount();
32
+ }
33
+ });
34
+ });
@@ -8,16 +8,16 @@ describe('await in control flow', () => {
8
8
  // it('should handle await inside if statement', async () => {
9
9
  // function App() @{
10
10
  // let condition = true;
11
- // let &[data] = track('loading');
11
+ // const data = track('loading');
12
12
 
13
13
  // if (condition) {
14
14
  // await new Promise((resolve) => setTimeout(() => {
15
- // data = 'loaded';
15
+ // data.value = 'loaded';
16
16
  // resolve();
17
17
  // }, 10));
18
18
  // }
19
19
 
20
- // <div>{data}</div>
20
+ // <div>{data.value}</div>
21
21
  // }
22
22
 
23
23
  // const { body } = await render(App);
@@ -19,9 +19,9 @@ describe('basic server > attribute rendering', () => {
19
19
 
20
20
  it('render dynamic class attribute', async () => {
21
21
  function Basic() @{
22
- let &[active] = track(false);
22
+ const active = track(false);
23
23
  <>
24
- <div class={active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
24
+ <div class={active.value ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
25
25
  <style>
26
26
  .active {
27
27
  color: green;
@@ -135,9 +135,9 @@ describe('basic server > attribute rendering', () => {
135
135
 
136
136
  it('render dynamic class object', async () => {
137
137
  function Basic() @{
138
- let &[active] = track(false);
138
+ const active = track(false);
139
139
  <>
140
- <div class={{ active: active, inactive: !active }}>{'Dynamic Class'}</div>
140
+ <div class={{ active: active.value, inactive: !active.value }}>{'Dynamic Class'}</div>
141
141
  <style>
142
142
  .active {
143
143
  color: green;
@@ -162,10 +162,10 @@ describe('basic server > attribute rendering', () => {
162
162
  'applies scoped ripple class to multiple elements with dynamic class expressions',
163
163
  async () => {
164
164
  function Basic() @{
165
- let &[selected] = track(1);
165
+ const selected = track(1);
166
166
  <>
167
- <div class={selected === 0 ? 'selected' : ''}>{`div 1`}</div>
168
- <div class={selected === 0 ? 'selected' : ''}>{`div 2`}</div>
167
+ <div class={selected.value === 0 ? 'selected' : ''}>{`div 1`}</div>
168
+ <div class={selected.value === 0 ? 'selected' : ''}>{`div 2`}</div>
169
169
  <style>
170
170
  div {
171
171
  background: green;
@@ -193,8 +193,8 @@ describe('basic server > attribute rendering', () => {
193
193
 
194
194
  it('render dynamic id attribute', async () => {
195
195
  function Basic() @{
196
- let &[count] = track(0);
197
- <div id={`item-${count}`}>{'Dynamic ID'}</div>
196
+ const count = track(0);
197
+ <div id={`item-${count.value}`}>{'Dynamic ID'}</div>
198
198
  }
199
199
 
200
200
  const { body } = await render(Basic);
@@ -207,8 +207,8 @@ describe('basic server > attribute rendering', () => {
207
207
 
208
208
  it('render dynamic style attribute', async () => {
209
209
  function Basic() @{
210
- let &[color] = track('red');
211
- <div style={`color: ${color}; font-weight: bold;`}>{'Dynamic Style'}</div>
210
+ const color = track('red');
211
+ <div style={`color: ${color.value}; font-weight: bold;`}>{'Dynamic Style'}</div>
212
212
  }
213
213
 
214
214
  const { body } = await render(Basic);
@@ -222,8 +222,8 @@ describe('basic server > attribute rendering', () => {
222
222
 
223
223
  it('render style attribute as dynamic object', async () => {
224
224
  function Basic() @{
225
- let &[color] = track('red');
226
- <div style={{ color: color, fontWeight: 'bold' }}>{'Dynamic Style'}</div>
225
+ const color = track('red');
226
+ <div style={{ color: color.value, fontWeight: 'bold' }}>{'Dynamic Style'}</div>
227
227
  }
228
228
 
229
229
  const { body } = await render(Basic);
@@ -235,10 +235,38 @@ describe('basic server > attribute rendering', () => {
235
235
  expect(div.style.fontWeight).toBe('bold');
236
236
  });
237
237
 
238
+ it.each([undefined, null])('omits %s style values from HTML and SVG attributes', async (
239
+ empty,
240
+ ) => {
241
+ function Basic() @{
242
+ const styles = {
243
+ width: empty,
244
+ '--scale': empty,
245
+ margin: 0,
246
+ opacity: 0,
247
+ '--literal': 'undefined',
248
+ '--literal-null': 'null',
249
+ };
250
+ <>
251
+ <div style={styles} />
252
+ <svg {...{ style: styles }} />
253
+ </>
254
+ }
255
+
256
+ const { body } = await render(Basic);
257
+ const { document } = parseHtml(body);
258
+
259
+ for (const element of document.querySelectorAll('div, svg')) {
260
+ expect(element.getAttribute('style')).toBe(
261
+ 'margin: 0; opacity: 0; --literal: undefined; --literal-null: null;',
262
+ );
263
+ }
264
+ });
265
+
238
266
  it('render tracked variable as style attribute', async () => {
239
267
  function Basic() @{
240
- let &[style] = track({ color: 'red', fontWeight: 'bold' });
241
- <div {style}>{'Dynamic Style'}</div>
268
+ const style = track({ color: 'red', fontWeight: 'bold' });
269
+ <div style={style.value}>{'Dynamic Style'}</div>
242
270
  }
243
271
 
244
272
  const { body } = await render(Basic);
@@ -363,9 +391,9 @@ describe('basic server > attribute rendering', () => {
363
391
 
364
392
  it('render dynamic boolean attributes as false', async () => {
365
393
  function Basic() @{
366
- let &[disabled] = track(false);
367
- let &[checked] = track(false);
368
- <input type="checkbox" {disabled} {checked} />
394
+ const disabled = track(false);
395
+ const checked = track(false);
396
+ <input type="checkbox" disabled={disabled.value} checked={checked.value} />
369
397
  }
370
398
 
371
399
  const { body } = await render(Basic);
@@ -396,9 +424,9 @@ describe('basic server > attribute rendering', () => {
396
424
 
397
425
  it('render dynamic boolean attributes as true', async () => {
398
426
  function Basic() @{
399
- let &[disabled] = track(true);
400
- let &[checked] = track(true);
401
- <input type="checkbox" {disabled} {checked} />
427
+ const disabled = track(true);
428
+ const checked = track(true);
429
+ <input type="checkbox" disabled={disabled.value} checked={checked.value} />
402
430
  }
403
431
 
404
432
  const { body } = await render(Basic);
@@ -429,8 +457,8 @@ describe('basic server > attribute rendering', () => {
429
457
 
430
458
  it('renders formnovalidate as a boolean attribute', async () => {
431
459
  function Basic() @{
432
- let &[formnovalidate] = track(true);
433
- <button {formnovalidate}>{'Submit'}</button>
460
+ const formnovalidate = track(true);
461
+ <button formnovalidate={formnovalidate.value}>{'Submit'}</button>
434
462
  }
435
463
 
436
464
  const { body } = await render(Basic);
@@ -445,8 +473,8 @@ describe('basic server > attribute rendering', () => {
445
473
 
446
474
  it('renders hidden as a boolean attribute when true', async () => {
447
475
  function Basic() @{
448
- let &[hidden] = track(true);
449
- <div {hidden}>{'Hidden content'}</div>
476
+ const hidden = track(true);
477
+ <div hidden={hidden.value}>{'Hidden content'}</div>
450
478
  }
451
479
 
452
480
  const { body } = await render(Basic);
@@ -461,8 +489,8 @@ describe('basic server > attribute rendering', () => {
461
489
 
462
490
  it('does not render hidden when false', async () => {
463
491
  function Basic() @{
464
- let &[hidden] = track(false);
465
- <div {hidden}>{'Visible content'}</div>
492
+ const hidden = track(false);
493
+ <div hidden={hidden.value}>{'Visible content'}</div>
466
494
  }
467
495
 
468
496
  const { body } = await render(Basic);
@@ -476,12 +504,12 @@ describe('basic server > attribute rendering', () => {
476
504
 
477
505
  it('render multiple dynamic attributes', async () => {
478
506
  function Basic() @{
479
- let &[theme] = track('light');
480
- let &[size] = track('medium');
507
+ const theme = track('light');
508
+ const size = track('medium');
481
509
  <div
482
- class={`theme-${theme} size-${size}`}
483
- data-theme={theme}
484
- data-size={size}
510
+ class={`theme-${theme.value} size-${size.value}`}
511
+ data-theme={theme.value}
512
+ data-size={size.value}
485
513
  >{'Multiple Dynamic Attributes'}</div>
486
514
  }
487
515
 
@@ -497,11 +525,11 @@ describe('basic server > attribute rendering', () => {
497
525
 
498
526
  it('render conditional attributes', async () => {
499
527
  function Basic() @{
500
- let &[showTitle] = track(false);
501
- let &[showAria] = track(false);
528
+ const showTitle = track(false);
529
+ const showAria = track(false);
502
530
  <div
503
- title={showTitle ? 'This is a title' : undefined}
504
- aria-label={showAria ? 'Accessible label' : undefined}
531
+ title={showTitle.value ? 'This is a title' : undefined}
532
+ aria-label={showAria.value ? 'Accessible label' : undefined}
505
533
  >{'Conditional Attributes'}</div>
506
534
  }
507
535
 
@@ -516,11 +544,11 @@ describe('basic server > attribute rendering', () => {
516
544
 
517
545
  it('render spread attributes', async () => {
518
546
  function Basic() @{
519
- let &[attrs] = track<TestAttributes>({
547
+ const attrs = track<TestAttributes>({
520
548
  class: 'initial',
521
549
  id: 'test-1',
522
550
  });
523
- <div {...attrs}>{'Spread Attributes'}</div>
551
+ <div {...attrs.value}>{'Spread Attributes'}</div>
524
552
  }
525
553
 
526
554
  const { body } = await render(Basic);
@@ -535,12 +563,12 @@ describe('basic server > attribute rendering', () => {
535
563
 
536
564
  it('renders with reactive attributes with nested reactive attributes', async () => {
537
565
  function Basic() @{
538
- let &[value] = track('parent-class');
539
- let &[nested] = track('nested-class');
566
+ const value = track('parent-class');
567
+ const nested = track('nested-class');
540
568
  <>
541
- <p class={value}>{'Colored parent value'}</p>
569
+ <p class={value.value}>{'Colored parent value'}</p>
542
570
  <div>
543
- <p class={nested}>{'Colored nested value'}</p>
571
+ <p class={nested.value}>{'Colored nested value'}</p>
544
572
  </div>
545
573
  </>
546
574
  }
@@ -1,5 +1,6 @@
1
1
  import { track } from 'ripple';
2
2
  import type {
3
+ Derived,
3
4
  Tracked,
4
5
  PropsWithChildren,
5
6
  PropsWithExtras,
@@ -8,6 +9,61 @@ import type {
8
9
  } from 'ripple';
9
10
 
10
11
  describe('basic server > components & composition', () => {
12
+ it('renders a read-only view passed through readOnly()', async () => {
13
+ function Child({ count }: { count: Derived<number> }) @{
14
+ <span>{count.value}</span>
15
+ }
16
+
17
+ function App() @{
18
+ const count = track(5);
19
+ <div>
20
+ <Child count={count.readOnly()} />
21
+ </div>
22
+ }
23
+
24
+ const { body } = await render(App);
25
+
26
+ expect(body).toBeHtml('<div><span>5</span></div>');
27
+ });
28
+
29
+ it('renders rest and default pattern names in a keyed loop', async () => {
30
+ function App() @{
31
+ const rows = [
32
+ { id: 1, name: 'one', tags: ['a', 'b'] },
33
+ ];
34
+ <ul>
35
+ @for (const { id, ...rest } of rows; key id) {
36
+ <li>{rest.name}</li>
37
+ }
38
+ @for (const [head, second = 'none', ...others] of rows.map((row) => row.tags); key head) {
39
+ <li>{head + ':' + second + ':' + others.length}</li>
40
+ }
41
+ </ul>
42
+ }
43
+
44
+ const { body } = await render(App);
45
+
46
+ expect(body).toBeHtml('<ul><li>one</li><li>a:b:0</li></ul>');
47
+ });
48
+
49
+ it('renders nothing for a component prop that is undefined', async () => {
50
+ function Child({ Comp }: { Comp?: Component }) @{
51
+ <div>
52
+ {'before'}
53
+ <Comp />
54
+ {'after'}
55
+ </div>
56
+ }
57
+
58
+ function App() @{
59
+ <Child />
60
+ }
61
+
62
+ const { body } = await render(App);
63
+
64
+ expect(body).toBeHtml('<div>beforeafter</div>');
65
+ });
66
+
11
67
  async function render_expected_invalid_component(App: Component) {
12
68
  const console_error_spy = vi.spyOn(console, 'error').mockImplementation(() => {});
13
69
  try {
@@ -186,16 +242,16 @@ describe('basic server > components & composition', () => {
186
242
  }
187
243
 
188
244
  function Basic() @{
189
- let &[clicked] = track(false);
245
+ const clicked = track(false);
190
246
  <>
191
247
  <Card
192
248
  title="Test Card"
193
249
  content="This is a test card"
194
250
  buttonText="Click me"
195
- onAction={() => (clicked = true)}
251
+ onAction={() => (clicked.value = true)}
196
252
  />
197
253
  <div class="status">
198
- {clicked ? 'Clicked' : 'Not clicked'}
254
+ {clicked.value ? 'Clicked' : 'Not clicked'}
199
255
  </div>
200
256
  </>
201
257
  }
@@ -228,14 +284,14 @@ describe('basic server > components & composition', () => {
228
284
  }
229
285
 
230
286
  function Basic() @{
231
- let &[message, messageTracked] = track('Hello');
232
- let &[number, numberTracked] = track(1);
287
+ const message = track('Hello');
288
+ const number = track(1);
233
289
  <>
234
- <ChildComponent text={messageTracked} count={numberTracked} />
290
+ <ChildComponent text={message} count={number} />
235
291
  <button
236
292
  onClick={() => {
237
- message = message === 'Hello' ? 'Goodbye' : 'Hello';
238
- number++;
293
+ message.value = message.value === 'Hello' ? 'Goodbye' : 'Hello';
294
+ number.value++;
239
295
  }}
240
296
  >{'Update Props'}</button>
241
297
  </>
@@ -397,10 +397,10 @@ second
397
397
 
398
398
  it('renders tracked state updates', async () => {
399
399
  function Counter() @{
400
- let &[count] = track(0);
401
- count++;
402
- count = count + 5;
403
- <div>{count}</div>
400
+ const count = track(0);
401
+ count.value++;
402
+ count.value = count.value + 5;
403
+ <div>{count.value}</div>
404
404
  }
405
405
 
406
406
  const { body } = await render(Counter);
@@ -445,10 +445,12 @@ second
445
445
 
446
446
  it('renders spread props with tracked values', async () => {
447
447
  function SpreadProps() @{
448
- let &[id] = track('unique-id');
449
- let &[isActive] = track(true);
450
- let &[styles] = track({ color: 'red', fontSize: '16px' });
451
- <div {...{ id: id, class: { active: isActive }, style: styles }}>{'Spread Props'}</div>
448
+ const id = track('unique-id');
449
+ const isActive = track(true);
450
+ const styles = track({ color: 'red', fontSize: '16px' });
451
+ <div
452
+ {...{ id: id.value, class: { active: isActive.value }, style: styles.value }}
453
+ >{'Spread Props'}</div>
452
454
  }
453
455
 
454
456
  const { body } = await render(SpreadProps);
@@ -460,12 +462,12 @@ second
460
462
 
461
463
  it('handles AssignExpressions with tracked values or properties correctly', async () => {
462
464
  function Assignments() @{
463
- let &[count] = track(0);
465
+ const count = track(0);
464
466
  const obj = { value: track(5) };
465
- count += 10;
467
+ count.value += 10;
466
468
  obj.value.value *= 2;
467
469
  <>
468
- <div>{count}</div>
470
+ <div>{count.value}</div>
469
471
  <div>{obj.value.value}</div>
470
472
  </>
471
473
  }
@@ -477,12 +479,12 @@ second
477
479
 
478
480
  it(`handles derived changes via tracked dependencies' changes`, async () => {
479
481
  function Derived() @{
480
- let &[base] = track(5);
481
- let &[multiplier] = track(3);
482
- let &[derived] = track(() => base * multiplier);
483
- const before = derived;
484
- base += 2;
485
- const after = derived;
482
+ const base = track(5);
483
+ const multiplier = track(3);
484
+ const derived = track(() => base.value * multiplier.value);
485
+ const before = derived.value;
486
+ base.value += 2;
487
+ const after = derived.value;
486
488
  <>
487
489
  <div>{before}</div>
488
490
  <div>{after}</div>
@@ -496,13 +498,13 @@ second
496
498
 
497
499
  it(`handles derived changes based on another derived's dependencies' changes`, async () => {
498
500
  function NestedDerived() @{
499
- let &[a] = track(2);
500
- let &[b] = track(3);
501
- let &[sum] = track(() => a + b);
502
- let &[product] = track(() => sum * 2);
503
- const before = product;
504
- a = 4;
505
- const after = product;
501
+ const a = track(2);
502
+ const b = track(3);
503
+ const sum = track(() => a.value + b.value);
504
+ const product = track(() => sum.value * 2);
505
+ const before = product.value;
506
+ a.value = 4;
507
+ const after = product.value;
506
508
  <>
507
509
  <div>{before}</div>
508
510
  <div>{after}</div>
@@ -516,24 +518,24 @@ second
516
518
 
517
519
  it('handles lexical scopes correctly', async () => {
518
520
  function LexicalScopes() @{
519
- let &[x] = track(1);
521
+ const x = track(1);
520
522
  let nested = '';
521
523
  let deep = '';
522
524
  let deeper = '';
523
525
  {
524
- let &[x] = track(10);
525
- nested = String(x);
526
+ const x = track(10);
527
+ nested = String(x.value);
526
528
  }
527
529
  {
528
- let &[x] = track(12);
529
- deep = String(x);
530
+ const x = track(12);
531
+ deep = String(x.value);
530
532
  {
531
- let &[x] = track(15);
532
- deeper = String(x);
533
+ const x = track(15);
534
+ deeper = String(x.value);
533
535
  }
534
536
  }
535
537
  <>
536
- <div>{x}</div>
538
+ <div>{x.value}</div>
537
539
  <div>{nested}</div>
538
540
  <div>
539
541
  {deep}
@@ -47,52 +47,6 @@ beforeAll(() => {
47
47
  expect(code).not.toContain('!');
48
48
  });
49
49
 
50
- it(
51
- 'compiles writes to unknown lazy array index 1 through lazy array helpers in SSR output',
52
- () => {
53
- const source = `function Child({ pair: &[first, second] }) @{
54
- second = 10;
55
- <div>{first}</div>
56
- }
57
- function App() @{
58
- <Child pair={[0, 1]} />
59
- }`;
60
- const { code } = compile(source, 'test.tsrx', { mode: 'server' });
61
-
62
- expect(code).toContain('_$_.lazy_array_set(lazy, 10, 1)');
63
- expect(code).not.toContain('lazy[1] =');
64
- },
65
- );
66
-
67
- it('does not double-wrap member access on lazy array value bindings in SSR output', () => {
68
- const source = `function Child({ pair: &[first] }) @{
69
- let value = first[0];
70
- <div>{value}</div>
71
- }
72
- function App() @{
73
- <Child pair={[{ 0: 'x' }]} />
74
- }`;
75
- const { code } = compile(source, 'test.tsrx', { mode: 'server' });
76
-
77
- expect(code).toContain('let value = _$_.lazy_array_get(lazy, 0)[0];');
78
- expect(code).not.toContain('_$_.lazy_array_get(_$_.lazy_array_get(lazy, 0), 0)');
79
- });
80
-
81
- it('throws on indexed access through known tracked lazy destructures in SSR output', () => {
82
- const source = `import { track } from 'ripple';
83
- function App() @{
84
- let &[value, tracked_ref] = track({ 0: 'x' });
85
- let nested = value[0];
86
- tracked_ref[0] = { 0: 'y' };
87
- let next = value[0];
88
- <div>{nested}{next}</div>
89
- }`;
90
-
91
- expect(() => compile(source, 'test.tsrx', { mode: 'server' })).toThrow(
92
- /Use \.value or &\[\] lazy destructuring/,
93
- );
94
- });
95
-
96
50
  it('throws on known tracked indexed access in SSR output', () => {
97
51
  const source = `import { track } from 'ripple';
98
52
  function App() @{
@@ -106,33 +60,10 @@ function App() @{
106
60
  }`;
107
61
 
108
62
  expect(() => compile(source, 'test.tsrx', { mode: 'server' })).toThrow(
109
- /Use \.value or &\[\] lazy destructuring/,
63
+ /Accessing a tracked object directly is not allowed, use `\.value`/,
110
64
  );
111
65
  });
112
66
 
113
- it(
114
- 'compiles indexed access on unknown lazy tracked refs through lazy array helpers in SSR output',
115
- () => {
116
- const source = `import { track } from 'ripple';
117
- function Child({ pair: &[value, tracked_ref] }) @{
118
- ++tracked_ref[0];
119
- tracked_ref[0]++;
120
- <div>{value}</div>
121
- }
122
- function App() @{
123
- let tracked = track(0);
124
- <Child pair={tracked} />
125
- }`;
126
- const { code } = compile(source, 'test.tsrx', { mode: 'server' });
127
-
128
- expect(code).toContain('_$_.lazy_array_get(lazy, 1)');
129
- expect(code).toContain('_$_.lazy_array_update_pre(_$_.lazy_array_get(lazy, 1), 0)');
130
- expect(code).toContain('_$_.lazy_array_update(_$_.lazy_array_get(lazy, 1), 0)');
131
- expect(code).not.toContain('lazy[1][0]');
132
- expect(code).not.toContain('lazy_array_get(lazy, 1)[0]');
133
- },
134
- );
135
-
136
67
  it('removes class TypeScript syntax from JS output', () => {
137
68
  const source = `interface BaseEvent {}
138
69
 
@@ -8,10 +8,10 @@ describe('composite > props', () => {
8
8
  }
9
9
 
10
10
  function App() @{
11
- let &[foo] = track(123);
11
+ const foo = track(123);
12
12
  <>
13
13
  <Child />
14
- <Child {foo} />
14
+ <Child foo={foo.value} />
15
15
  </>
16
16
  }
17
17
 
@@ -70,10 +70,10 @@ describe('composite > props', () => {
70
70
  }
71
71
 
72
72
  function App() @{
73
- let &[foo] = track(123);
73
+ const foo = track(123);
74
74
  <>
75
75
  <Child />
76
- <Child {foo} />
76
+ <Child foo={foo.value} />
77
77
  </>
78
78
  }
79
79
 
@@ -85,7 +85,7 @@ describe('composite > props', () => {
85
85
  });
86
86
 
87
87
  it('correctly retains prop accessors and reactivity when using rest props', async () => {
88
- function Button(&{ children, ...rest }: Props) @{
88
+ function Button({ children, ...rest }: Props) @{
89
89
  <>
90
90
  <button {...rest}>{children}</button>
91
91
  <style>
@@ -99,7 +99,7 @@ describe('composite > props', () => {
99
99
  </>
100
100
  }
101
101
 
102
- function Toggle(&{ pressed, ...rest }: { pressed: Tracked<boolean> }) @{
102
+ function Toggle({ pressed, ...rest }: { pressed: Tracked<boolean> }) @{
103
103
  const onClick = () => (pressed.value = !pressed.value);
104
104
  <>
105
105
  <Button {...rest} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>