ripple 0.3.112 → 0.3.113

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 (31) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +3 -3
  3. package/tests/client/basic/basic.rendering.test.tsrx +72 -0
  4. package/tests/client/compiler/compiler.basic.test.tsrx +36 -4
  5. package/tests/client/composite/composite.props.test.tsrx +46 -0
  6. package/tests/hydration/basic.test.js +39 -0
  7. package/tests/hydration/compiled/client/basic.js +133 -5
  8. package/tests/hydration/compiled/client/if-fragment-controlflow.js +166 -214
  9. package/tests/hydration/compiled/client/streaming.js +81 -105
  10. package/tests/hydration/compiled/server/basic.js +121 -2
  11. package/tests/hydration/compiled/server/events.js +61 -5
  12. package/tests/hydration/compiled/server/for.js +242 -26
  13. package/tests/hydration/compiled/server/head.js +45 -5
  14. package/tests/hydration/compiled/server/html.js +24 -8
  15. package/tests/hydration/compiled/server/if-fragment-controlflow.js +73 -18
  16. package/tests/hydration/compiled/server/mixed-control-flow.js +9 -1
  17. package/tests/hydration/compiled/server/nested-control-flow.js +9 -1
  18. package/tests/hydration/compiled/server/reactivity.js +43 -3
  19. package/tests/hydration/compiled/server/streaming.js +131 -38
  20. package/tests/hydration/compiled/server/track-async-serialization.js +106 -10
  21. package/tests/hydration/compiled/server/try.js +45 -5
  22. package/tests/hydration/components/basic.tsrx +43 -0
  23. package/tests/server/basic.test.tsrx +30 -0
  24. package/tests/server/compiler.test.tsrx +29 -4
  25. package/tests/server/composite.props.test.tsrx +26 -0
  26. package/tests/server/streaming-ssr.test.tsrx +9 -9
  27. package/tests/server/track-async-serialization.test.tsrx +4 -4
  28. package/tests/types/component-return.ts +48 -0
  29. package/tests/utils/runtime-imports.test.js +5 -1
  30. package/tsconfig.typecheck.json +1 -1
  31. package/types/index.d.ts +10 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.113
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1389](https://github.com/Ripple-TS/ripple/pull/1389)
8
+ [`7ad580e`](https://github.com/Ripple-TS/ripple/commit/7ad580efd24b338b4774add06afdcdd8876c954c)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - fix(types): widen
10
+ `Component`'s return type to everything the runtime renders
11
+
12
+ `Component` declared `(props: T) => void | TSRXElement`, so a component that
13
+ returned a string, number, bigint, boolean, `null`, or an array — all of which
14
+ both runtimes render — was rejected at `mount`, `hydrate`, and the server
15
+ `render`. The return type is now the new exported `Renderable` union, which
16
+ mirrors the shared dispatch in `render_value`/`render_expression`: elements
17
+ render, arrays flatten recursively, nullish renders nothing, everything else is
18
+ stringified. Promises, functions, and symbols stay rejected because neither
19
+ runtime renders them.
20
+
21
+ - Updated dependencies
22
+ [[`6404d3c`](https://github.com/Ripple-TS/ripple/commit/6404d3cc679fde2eb83ec85c9cd98b653f3f2fed),
23
+ [`6025176`](https://github.com/Ripple-TS/ripple/commit/6025176000cafa50d924add8e9a878fe37c0c22b),
24
+ [`6025176`](https://github.com/Ripple-TS/ripple/commit/6025176000cafa50d924add8e9a878fe37c0c22b),
25
+ [`6025176`](https://github.com/Ripple-TS/ripple/commit/6025176000cafa50d924add8e9a878fe37c0c22b),
26
+ [`7ad580e`](https://github.com/Ripple-TS/ripple/commit/7ad580efd24b338b4774add06afdcdd8876c954c),
27
+ [`6eaa2f3`](https://github.com/Ripple-TS/ripple/commit/6eaa2f3e6cd18973d57df06eae770313dd061a1a),
28
+ [`6025176`](https://github.com/Ripple-TS/ripple/commit/6025176000cafa50d924add8e9a878fe37c0c22b),
29
+ [`9ffd4ba`](https://github.com/Ripple-TS/ripple/commit/9ffd4ba3e5982acb79a02efe0379abdc14c092a1)]:
30
+ - @tsrx/core@0.1.51
31
+ - @tsrx/ripple@0.1.52
32
+
3
33
  ## 0.3.112
4
34
 
5
35
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.112",
6
+ "version": "0.3.113",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -73,8 +73,8 @@
73
73
  "clsx": "^2.1.1",
74
74
  "devalue": "^5.8.1",
75
75
  "esm-env": "^1.2.2",
76
- "@tsrx/core": "0.1.50",
77
- "@tsrx/ripple": "0.1.51"
76
+ "@tsrx/core": "0.1.51",
77
+ "@tsrx/ripple": "0.1.52"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/estree": "^1.0.8",
@@ -607,6 +607,78 @@ second
607
607
  expect(div.textContent).toEqual('a Hello');
608
608
  });
609
609
 
610
+ it('renders a template element inside an expression container child', () => {
611
+ function App() @{
612
+ <div>
613
+ {<h1>inline</h1>}
614
+ </div>
615
+ }
616
+
617
+ render(App);
618
+ expect(container.querySelector('div h1').textContent).toEqual('inline');
619
+ });
620
+
621
+ it('updates control flow inside an element in an expression container child', () => {
622
+ function App() @{
623
+ let &[ok] = track(true);
624
+ <>
625
+ <button
626
+ onClick={() => {
627
+ ok = !ok;
628
+ }}
629
+ >{'toggle'}</button>
630
+ <div>
631
+ {<h1>
632
+ @if (ok) {
633
+ <span>{'yes'}</span>
634
+ } @else {
635
+ <span>{'no'}</span>
636
+ }
637
+ </h1>}
638
+ </div>
639
+ </>
640
+ }
641
+
642
+ render(App);
643
+ expect(container.querySelector('div h1 span').textContent).toEqual('yes');
644
+
645
+ container.querySelector('button').click();
646
+ flushSync();
647
+ expect(container.querySelector('div h1 span').textContent).toEqual('no');
648
+ });
649
+
650
+ it('renders a call-containing text expression after inlined text', () => {
651
+ // The `' '` text anchor for the expression used to coalesce with the
652
+ // preceding inlined text into a single template text node, leaving
653
+ // `sibling(…, true)` nothing to anchor to — a runtime crash. (Only
654
+ // call-containing expressions hit this: call-free ones merge with the
655
+ // preceding text upstream instead.)
656
+ function fetchCount() {
657
+ return 42;
658
+ }
659
+
660
+ function App() @{
661
+ <div>count: {String(fetchCount())}</div>
662
+ }
663
+
664
+ render(App);
665
+ expect(container.querySelector('div').textContent).toEqual('count: 42');
666
+ });
667
+
668
+ it('renders a bigint merged into a text run after a string', () => {
669
+ // `string + bigint` is legal string concatenation in JS (the mixing
670
+ // TypeError only applies to numeric addition), so a bare bigint literal
671
+ // merged after a provably-string operand renders its decimal text.
672
+ function App() @{
673
+ const label = 'big:';
674
+ // prettier-ignore
675
+ <div>{label}{1n}</div>
676
+ }
677
+
678
+ render(App);
679
+ expect(container.querySelector('div').textContent).toEqual('big:1');
680
+ });
681
+
610
682
  it('should handle multiple consecutive text expressions', () => {
611
683
  function App() @{
612
684
  let name = 'World';
@@ -143,6 +143,10 @@ function tag() {
143
143
  const values = { member: 0 };`;
144
144
 
145
145
  const cases = [
146
+ // Call-containing but provably-text-primitive expressions — `empty() + ''`,
147
+ // `\`${empty()}\``, `void empty()`, `values[getKey()]++` — are NOT in this
148
+ // list: they cannot hold an element, so they DO merge into the text run
149
+ // (see 'merges provably-primitive call-containing children').
146
150
  { name: 'call expression', expression: 'child(\'call\')' },
147
151
  { name: 'new expression', expression: 'new Constructed(\'new\')' },
148
152
  { name: 'chain expression', expression: 'factory?.(\'chain\')' },
@@ -156,7 +160,6 @@ const values = { member: 0 };`;
156
160
  },
157
161
  { name: 'array expression', expression: '[child(\'array\')]' },
158
162
  { name: 'assignment expression', expression: 'assigned = child(\'assignment\')' },
159
- { name: 'binary expression', expression: 'empty() + \'\'' },
160
163
  { name: 'logical expression', expression: 'true && child(\'logical\')' },
161
164
  { name: 'conditional expression', expression: 'true ? child(\'conditional\') : \'\'' },
162
165
  { name: 'member expression', expression: 'lookup[getKey()]' },
@@ -166,9 +169,6 @@ const values = { member: 0 };`;
166
169
  },
167
170
  { name: 'sequence expression', expression: '(touch(), child(\'sequence\'))' },
168
171
  { name: 'tagged template expression', expression: 'tag`tagged`' },
169
- { name: 'template literal', expression: '`${empty()}`' },
170
- { name: 'unary expression', expression: 'void empty()' },
171
- { name: 'update expression', expression: 'values[getKey()]++' },
172
172
  ];
173
173
 
174
174
  for (const test_case of cases) {
@@ -185,6 +185,38 @@ export function App() @{
185
185
  }
186
186
  });
187
187
 
188
+ it('merges provably-primitive call-containing children into the text run', () => {
189
+ const source = `function empty() {
190
+ return null;
191
+ }
192
+ export function App() @{
193
+ <div>{'label:'}{void empty()}</div>
194
+ }`;
195
+
196
+ const { code } = compile(source, 'primitive-merge.tsrx', { mode: 'client' });
197
+
198
+ // One merged text expression — no separate comment-anchored expression.
199
+ expect(code).toContain('\'label:\' + _$_.with_scope');
200
+ expect(code).toContain('String(void');
201
+ expect(code).not.toContain('_$_.expression(');
202
+ });
203
+
204
+ it('concatenates provably-string merge children bare', () => {
205
+ const source = `function fetchLabel() {
206
+ return 'fetched';
207
+ }
208
+ export function App() @{
209
+ <div>{'label:'}{String(fetchLabel())}</div>
210
+ }`;
211
+
212
+ const { code } = compile(source, 'string-merge.tsrx', { mode: 'client' });
213
+
214
+ // Already a string: no String(… ?? '') double wrap around it.
215
+ expect(code).toContain('String(fetchLabel())');
216
+ expect(code).not.toContain('String(String(');
217
+ expect(code).not.toContain('?? \'\'');
218
+ });
219
+
188
220
  it('parses backtick expressions inside TSRX fragments as template literals', () => {
189
221
  const source = `let a = function() @{
190
222
  <>
@@ -195,4 +195,50 @@ describe('composite > props', () => {
195
195
  expect(container.querySelector('p').textContent).toBe('Description 1');
196
196
  expect(container.querySelector('price').textContent).toBe('15');
197
197
  });
198
+
199
+ it('renders a template element passed inline as a prop', () => {
200
+ function Child(props: { header: unknown }) @{
201
+ <div>{props.header}</div>
202
+ }
203
+
204
+ function App() @{
205
+ <Child header={<h1>{'hello'}</h1>} />
206
+ }
207
+
208
+ render(App);
209
+ expect(container.querySelector('div h1').textContent).toBe('hello');
210
+ });
211
+
212
+ it('updates control flow inside an element passed inline as a prop', () => {
213
+ function Child(props: { header: unknown }) @{
214
+ <div>{props.header}</div>
215
+ }
216
+
217
+ function App() @{
218
+ let &[ok] = track(true);
219
+ <>
220
+ <button
221
+ onClick={() => {
222
+ ok = !ok;
223
+ }}
224
+ >{'toggle'}</button>
225
+ <Child
226
+ header={<h1>
227
+ @if (ok) {
228
+ <span>{'yes'}</span>
229
+ } @else {
230
+ <span>{'no'}</span>
231
+ }
232
+ </h1>}
233
+ />
234
+ </>
235
+ }
236
+
237
+ render(App);
238
+ expect(container.querySelector('div h1 span').textContent).toBe('yes');
239
+
240
+ container.querySelector('button').click();
241
+ flushSync();
242
+ expect(container.querySelector('div h1 span').textContent).toBe('no');
243
+ });
198
244
  });
@@ -255,3 +255,42 @@ describe('hydration > basic', () => {
255
255
  expect(container.querySelector('.inner .last-child')?.textContent).toBe('I am the last child');
256
256
  });
257
257
  });
258
+
259
+ describe('hydration > text runs with call-containing primitives', () => {
260
+ it('hydrates a text expression that follows inlined text', async () => {
261
+ await hydrateComponent(
262
+ ServerComponents.TextTailExpression,
263
+ ClientComponents.TextTailExpression,
264
+ );
265
+ expect(container.textContent).toBe('label: fetched');
266
+ });
267
+
268
+ it('hydrates a text expression that follows a fragment ending in text', async () => {
269
+ await hydrateComponent(
270
+ ServerComponents.FragmentTailExpression,
271
+ ClientComponents.FragmentTailExpression,
272
+ );
273
+ expect(container.textContent).toBe('frag-fetched');
274
+ });
275
+
276
+ it('hydrates an authored fragment child', async () => {
277
+ await hydrateComponent(ServerComponents.FragmentChildOnly, ClientComponents.FragmentChildOnly);
278
+ expect(container.textContent).toBe('frag-tail');
279
+ });
280
+
281
+ it('hydrates a fragment leading with an opaque value', async () => {
282
+ await hydrateComponent(
283
+ ServerComponents.FragmentLeadsWithOpaqueValue,
284
+ ClientComponents.FragmentLeadsWithOpaqueValue,
285
+ );
286
+ expect(container.textContent).toBe('Hafter-opaque');
287
+ });
288
+
289
+ it('hydrates a fragment leading with a call-containing primitive', async () => {
290
+ await hydrateComponent(
291
+ ServerComponents.FragmentLeadsWithPrimitiveCall,
292
+ ClientComponents.FragmentLeadsWithPrimitiveCall,
293
+ );
294
+ expect(container.textContent).toBe('fetchedafter-call');
295
+ });
296
+ });
@@ -71,6 +71,15 @@ var root_66 = _$_.template(`<footer class="last-child">I am the last child</foot
71
71
  var root_67 = _$_.template(`<div class="wrapper"><h1>Header</h1><p>Some content</p><!></div>`, 0);
72
72
  var root_68 = _$_.template(`<div class="inner"><span>Inner text</span><!></div>`, 0);
73
73
  var root_69 = _$_.template(`<section class="outer"><h2>Section title</h2><!></section>`, 0);
74
+ var root_70 = _$_.template(`<div> </div>`, 0);
75
+ var root_71 = _$_.template(`<div> </div>`, 0);
76
+ var root_72 = _$_.template(`<div>frag-<span>tail</span></div>`, 0);
77
+ var root_74 = _$_.template(`<!><p>after-opaque</p>`, 1, 2);
78
+ var root_73 = _$_.template(`<!>`, 1, 1);
79
+ var root_75 = _$_.template(`<div></div>`, 0);
80
+ var root_77 = _$_.template(` <p>after-call</p>`, 1, 2);
81
+ var root_76 = _$_.template(`<!>`, 1, 1);
82
+ var root_78 = _$_.template(`<div></div>`, 0);
74
83
 
75
84
  import { track } from 'ripple';
76
85
 
@@ -193,7 +202,7 @@ export function Greeting(props) {
193
202
  }
194
203
 
195
204
  _$_.render(() => {
196
- _$_.set_text(expression, 'Hello ' + _$_.with_scope(__block, () => String(props.name ?? '')));
205
+ _$_.set_text(expression, 'Hello ' + props.name);
197
206
  });
198
207
 
199
208
  _$_.append(__anchor, div_5);
@@ -218,9 +227,9 @@ export function ExpressionContent() {
218
227
  var div_6 = _$_.first_child_frag(fragment_7);
219
228
 
220
229
  {
221
- var expression_1 = _$_.child(div_6);
230
+ var expression_1 = _$_.child(div_6, true);
222
231
 
223
- _$_.expression(expression_1, () => value);
232
+ expression_1.nodeValue = value;
224
233
  _$_.pop(div_6);
225
234
  }
226
235
 
@@ -246,9 +255,9 @@ function NestedHelperItem({ item }) {
246
255
  var div_7 = root_15();
247
256
 
248
257
  {
249
- var expression_3 = _$_.child(div_7);
258
+ var expression_3 = _$_.child(div_7, true);
250
259
 
251
- _$_.expression(expression_3, () => item);
260
+ expression_3.nodeValue = item;
252
261
  _$_.pop(div_7);
253
262
  }
254
263
 
@@ -917,4 +926,123 @@ export function NestedComponentAsLastSibling() {
917
926
  });
918
927
  }
919
928
 
929
+ function fetchLabel() {
930
+ return 'fetched';
931
+ }
932
+
933
+ export function TextTailExpression() {
934
+ return _$_.tsrx_element((__anchor, __block) => {
935
+ var div_27 = root_70();
936
+
937
+ {
938
+ var text = _$_.child(div_27, true);
939
+
940
+ _$_.pop(div_27);
941
+ }
942
+
943
+ _$_.render(() => {
944
+ _$_.set_text(text, "label: " + _$_.with_scope(__block, () => String(fetchLabel())));
945
+ });
946
+
947
+ _$_.append(__anchor, div_27);
948
+ });
949
+ }
950
+
951
+ export function FragmentTailExpression() {
952
+ return _$_.tsrx_element((__anchor, __block) => {
953
+ var div_28 = root_71();
954
+
955
+ {
956
+ var expression_29 = _$_.child(div_28, true);
957
+
958
+ _$_.pop(div_28);
959
+ }
960
+
961
+ _$_.render(() => {
962
+ _$_.set_text(expression_29, 'frag-' + _$_.with_scope(__block, () => String(fetchLabel())));
963
+ });
964
+
965
+ _$_.append(__anchor, div_28);
966
+ });
967
+ }
968
+
969
+ export function FragmentChildOnly() {
970
+ return _$_.tsrx_element((__anchor, __block) => {
971
+ var div_29 = root_72();
972
+
973
+ {
974
+ _$_.pop(div_29);
975
+ }
976
+
977
+ _$_.append(__anchor, div_29);
978
+ });
979
+ }
980
+
981
+ function OpaqueLead(props) {
982
+ return _$_.tsrx_element((__anchor, __block) => {
983
+ var fragment_30 = root_73();
984
+ var node_26 = _$_.first_child_frag(fragment_30);
985
+
986
+ _$_.expression(node_26, () => _$_.tsrx_element((__anchor, __block) => {
987
+ var fragment_31 = root_74();
988
+ var expression_30 = _$_.first_child_frag(fragment_31);
989
+
990
+ _$_.expression(expression_30, () => props.header);
991
+ _$_.append(__anchor, fragment_31);
992
+ }));
993
+
994
+ _$_.append(__anchor, fragment_30);
995
+ });
996
+ }
997
+
998
+ export function FragmentLeadsWithOpaqueValue() {
999
+ return _$_.tsrx_element((__anchor, __block) => {
1000
+ var div_30 = root_75();
1001
+
1002
+ {
1003
+ var append_anchor_1 = _$_.append_into(div_30);
1004
+
1005
+ _$_.render_component(OpaqueLead, append_anchor_1, { header: 'H' });
1006
+ _$_.pop(div_30);
1007
+ }
1008
+
1009
+ _$_.append(__anchor, div_30);
1010
+ });
1011
+ }
1012
+
1013
+ function PrimitiveCallLead() {
1014
+ return _$_.tsrx_element((__anchor, __block) => {
1015
+ var fragment_32 = root_76();
1016
+ var node_27 = _$_.first_child_frag(fragment_32);
1017
+
1018
+ _$_.expression(node_27, () => _$_.tsrx_element((__anchor, __block) => {
1019
+ var fragment_33 = root_77();
1020
+ var expression_31 = _$_.first_child_frag(fragment_33, true);
1021
+
1022
+ _$_.render(() => {
1023
+ _$_.set_text(expression_31, _$_.with_scope(__block, () => String(fetchLabel())));
1024
+ });
1025
+
1026
+ _$_.append(__anchor, fragment_33);
1027
+ }));
1028
+
1029
+ _$_.append(__anchor, fragment_32);
1030
+ });
1031
+ }
1032
+
1033
+ export function FragmentLeadsWithPrimitiveCall() {
1034
+ return _$_.tsrx_element((__anchor, __block) => {
1035
+ var div_31 = root_78();
1036
+
1037
+ {
1038
+ var append_anchor_2 = _$_.append_into(div_31);
1039
+
1040
+ _$_.render_component(PrimitiveCallLead, append_anchor_2, {});
1041
+ _$_.pop(div_31);
1042
+ }
1043
+
1044
+ _$_.append(__anchor, div_31);
1045
+ });
1046
+ }
1047
+
920
1048
  _$_.delegate(['click']);