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
@@ -24,7 +24,15 @@ function ServerCallResult({ count }) {
24
24
  _$_.regular_block(() => {
25
25
  let __out = '';
26
26
 
27
- __out += '<p class="result">' + _$_.escape(lazy.value) + '</p>';
27
+ __out += '<p class="result">';
28
+
29
+ {
30
+ _$_.output_push(__out);
31
+ __out = '';
32
+ _$_.render_expression(lazy.value);
33
+ }
34
+
35
+ __out += '</p>';
28
36
  _$_.output_push(__out);
29
37
  });
30
38
  });
@@ -89,7 +97,15 @@ export function AsyncSimpleValue() {
89
97
  _$_.regular_block(() => {
90
98
  let __out = '';
91
99
 
92
- __out += '<p class="result">' + _$_.escape(lazy_2.value) + '</p>';
100
+ __out += '<p class="result">';
101
+
102
+ {
103
+ _$_.output_push(__out);
104
+ __out = '';
105
+ _$_.render_expression(lazy_2.value);
106
+ }
107
+
108
+ __out += '</p>';
93
109
  _$_.output_push(__out);
94
110
  });
95
111
 
@@ -134,7 +150,15 @@ export function AsyncNumericValue() {
134
150
  _$_.regular_block(() => {
135
151
  let __out = '';
136
152
 
137
- __out += '<span class="count">' + _$_.escape(lazy_3.value) + '</span>';
153
+ __out += '<span class="count">';
154
+
155
+ {
156
+ _$_.output_push(__out);
157
+ __out = '';
158
+ _$_.render_expression(lazy_3.value);
159
+ }
160
+
161
+ __out += '</span>';
138
162
  _$_.output_push(__out);
139
163
  });
140
164
 
@@ -179,7 +203,23 @@ export function AsyncObjectValue() {
179
203
  _$_.regular_block(() => {
180
204
  let __out = '';
181
205
 
182
- __out += '<div class="user"><span class="name">' + _$_.escape(lazy_4.value.name) + '</span><span class="age">' + _$_.escape(lazy_4.value.age) + '</span></div>';
206
+ __out += '<div class="user"><span class="name">';
207
+
208
+ {
209
+ _$_.output_push(__out);
210
+ __out = '';
211
+ _$_.render_expression(lazy_4.value.name);
212
+ }
213
+
214
+ __out += '</span><span class="age">';
215
+
216
+ {
217
+ _$_.output_push(__out);
218
+ __out = '';
219
+ _$_.render_expression(lazy_4.value.age);
220
+ }
221
+
222
+ __out += '</span></div>';
183
223
  _$_.output_push(__out);
184
224
  });
185
225
 
@@ -225,7 +265,23 @@ export function AsyncMultipleValues() {
225
265
  _$_.regular_block(() => {
226
266
  let __out = '';
227
267
 
228
- __out += '<div class="multi"><span class="first">' + _$_.escape(lazy_5.value) + '</span><span class="second">' + _$_.escape(lazy_6.value) + '</span></div>';
268
+ __out += '<div class="multi"><span class="first">';
269
+
270
+ {
271
+ _$_.output_push(__out);
272
+ __out = '';
273
+ _$_.render_expression(lazy_5.value);
274
+ }
275
+
276
+ __out += '</span><span class="second">';
277
+
278
+ {
279
+ _$_.output_push(__out);
280
+ __out = '';
281
+ _$_.render_expression(lazy_6.value);
282
+ }
283
+
284
+ __out += '</span></div>';
229
285
  _$_.output_push(__out);
230
286
  });
231
287
 
@@ -270,7 +326,15 @@ export function AsyncWithCatch() {
270
326
  _$_.regular_block(() => {
271
327
  let __out = '';
272
328
 
273
- __out += '<p class="result">' + _$_.escape(lazy_7.value) + '</p>';
329
+ __out += '<p class="result">';
330
+
331
+ {
332
+ _$_.output_push(__out);
333
+ __out = '';
334
+ _$_.render_expression(lazy_7.value);
335
+ }
336
+
337
+ __out += '</p>';
274
338
  _$_.output_push(__out);
275
339
  });
276
340
 
@@ -287,7 +351,15 @@ export function AsyncWithCatch() {
287
351
  _$_.regular_block(() => {
288
352
  let __out = '';
289
353
 
290
- __out += '<p class="error">' + _$_.escape(e.message) + '</p>';
354
+ __out += '<p class="error">';
355
+
356
+ {
357
+ _$_.output_push(__out);
358
+ __out = '';
359
+ _$_.render_expression(e.message);
360
+ }
361
+
362
+ __out += '</p>';
291
363
  _$_.output_push(__out);
292
364
  });
293
365
 
@@ -331,7 +403,15 @@ export function ChildWithError() {
331
403
  _$_.regular_block(() => {
332
404
  let __out = '';
333
405
 
334
- __out += '<p class="result">' + _$_.escape(lazy_8.value) + '</p>';
406
+ __out += '<p class="result">';
407
+
408
+ {
409
+ _$_.output_push(__out);
410
+ __out = '';
411
+ _$_.render_expression(lazy_8.value);
412
+ }
413
+
414
+ __out += '</p>';
335
415
  _$_.output_push(__out);
336
416
  });
337
417
 
@@ -392,7 +472,15 @@ export function ParentWithCatch() {
392
472
  _$_.regular_block(() => {
393
473
  let __out = '';
394
474
 
395
- __out += '<p class="parent-error">' + _$_.escape(e.message) + '</p>';
475
+ __out += '<p class="parent-error">';
476
+
477
+ {
478
+ _$_.output_push(__out);
479
+ __out = '';
480
+ _$_.render_expression(e.message);
481
+ }
482
+
483
+ __out += '</p>';
396
484
  _$_.output_push(__out);
397
485
  });
398
486
 
@@ -411,7 +499,15 @@ function ReactiveDependencyResult({ count }) {
411
499
  _$_.regular_block(() => {
412
500
  let __out = '';
413
501
 
414
- __out += '<p class="result">' + _$_.escape(lazy_9.value) + '</p>';
502
+ __out += '<p class="result">';
503
+
504
+ {
505
+ _$_.output_push(__out);
506
+ __out = '';
507
+ _$_.render_expression(lazy_9.value);
508
+ }
509
+
510
+ __out += '</p>';
415
511
  _$_.output_push(__out);
416
512
  });
417
513
  });
@@ -19,7 +19,15 @@ export function RootCatch({ error, reset }) {
19
19
  _$_.regular_block(() => {
20
20
  let __out = '';
21
21
 
22
- __out += '<section class="root-catch"><p class="root-error">' + _$_.escape(error.message) + '</p><button class="root-reset">retry</button></section>';
22
+ __out += '<section class="root-catch"><p class="root-error">';
23
+
24
+ {
25
+ _$_.output_push(__out);
26
+ __out = '';
27
+ _$_.render_expression(error.message);
28
+ }
29
+
30
+ __out += '</p><button class="root-reset">retry</button></section>';
23
31
  _$_.output_push(__out);
24
32
  });
25
33
  });
@@ -45,7 +53,15 @@ export function RootAsyncDirect() {
45
53
  _$_.regular_block(() => {
46
54
  let __out = '';
47
55
 
48
- __out += '<p class="root-async-value">' + _$_.escape(lazy.value) + '</p>';
56
+ __out += '<p class="root-async-value">';
57
+
58
+ {
59
+ _$_.output_push(__out);
60
+ __out = '';
61
+ _$_.render_expression(lazy.value);
62
+ }
63
+
64
+ __out += '</p>';
49
65
  _$_.output_push(__out);
50
66
  });
51
67
  });
@@ -58,7 +74,15 @@ export function RootAsyncRejects() {
58
74
  _$_.regular_block(() => {
59
75
  let __out = '';
60
76
 
61
- __out += '<p class="root-async-value">' + _$_.escape(lazy_1.value) + '</p>';
77
+ __out += '<p class="root-async-value">';
78
+
79
+ {
80
+ _$_.output_push(__out);
81
+ __out = '';
82
+ _$_.render_expression(lazy_1.value);
83
+ }
84
+
85
+ __out += '</p>';
62
86
  _$_.output_push(__out);
63
87
  });
64
88
  });
@@ -118,7 +142,15 @@ function AsyncList() {
118
142
  __out += '<ul class="items"><!--[-->';
119
143
 
120
144
  for (let item of lazy_2.value) {
121
- __out += '<li>' + _$_.escape(item) + '</li>';
145
+ __out += '<li>';
146
+
147
+ {
148
+ _$_.output_push(__out);
149
+ __out = '';
150
+ _$_.render_expression(item);
151
+ }
152
+
153
+ __out += '</li>';
122
154
  }
123
155
 
124
156
  __out += '<!--]--></ul>';
@@ -175,7 +207,15 @@ function AsyncContent() {
175
207
  _$_.regular_block(() => {
176
208
  let __out = '';
177
209
 
178
- __out += '<div class="resolved">' + _$_.escape(lazy_3.value) + '</div>';
210
+ __out += '<div class="resolved">';
211
+
212
+ {
213
+ _$_.output_push(__out);
214
+ __out = '';
215
+ _$_.render_expression(lazy_3.value);
216
+ }
217
+
218
+ __out += '</div>';
179
219
  _$_.output_push(__out);
180
220
  });
181
221
  });
@@ -326,3 +326,46 @@ export function NestedComponentAsLastSibling() @{
326
326
  <InnerContent />
327
327
  </section>
328
328
  }
329
+
330
+ // A call-containing but provably-text-primitive expression following inlined
331
+ // text: client and server must agree on the DOM shape for the text run.
332
+ function fetchLabel() {
333
+ return 'fetched';
334
+ }
335
+
336
+ export function TextTailExpression() @{
337
+ <div>label: {String(fetchLabel())}</div>
338
+ }
339
+
340
+ export function FragmentTailExpression() @{
341
+ // prettier-ignore
342
+ <div><>{'frag-'}</>{String(fetchLabel())}</div>
343
+ }
344
+
345
+ export function FragmentChildOnly() @{
346
+ // prettier-ignore
347
+ <div><>{'frag-'}</><span>{'tail'}</span></div>
348
+ }
349
+
350
+ // Fragment-boundary marker prediction: the fragment brackets itself only when
351
+ // its first renderable child emits its own markers — the prediction must match
352
+ // what the expression visitor actually emits.
353
+ function OpaqueLead(props: { header: unknown }) @{
354
+ // prettier-ignore
355
+ <>{props.header}<p>{'after-opaque'}</p></>
356
+ }
357
+
358
+ export function FragmentLeadsWithOpaqueValue() @{
359
+ // prettier-ignore
360
+ <div><OpaqueLead header={'H'} /></div>
361
+ }
362
+
363
+ function PrimitiveCallLead() @{
364
+ // prettier-ignore
365
+ <>{String(fetchLabel())}<p>{'after-call'}</p></>
366
+ }
367
+
368
+ export function FragmentLeadsWithPrimitiveCall() @{
369
+ // prettier-ignore
370
+ <div><PrimitiveCallLead /></div>
371
+ }
@@ -598,4 +598,34 @@ second
598
598
  '<div class="block-function">function inner / function outer</div><div class="block-arrow">22</div><div class="block-method">method inner / method outer</div>',
599
599
  );
600
600
  });
601
+
602
+ it('renders a template element inside an expression container child', async () => {
603
+ function App() @{
604
+ <div>
605
+ {<h1>
606
+ @if (true) {
607
+ <span>{'yes'}</span>
608
+ } @else {
609
+ <span>{'no'}</span>
610
+ }
611
+ </h1>}
612
+ </div>
613
+ }
614
+
615
+ const { body } = await render(App);
616
+ expect(body).toContain('<h1>');
617
+ expect(body).toContain('<span>yes</span>');
618
+ expect(body).not.toContain('<span>no</span>');
619
+ });
620
+
621
+ it('renders a bigint merged into a text run after a string', async () => {
622
+ function App() @{
623
+ const label = 'big:';
624
+ // prettier-ignore
625
+ <div>{label}{1n}</div>
626
+ }
627
+
628
+ const { body } = await render(App);
629
+ expect(body).toContain('<div>big:1</div>');
630
+ });
601
631
  });
@@ -226,6 +226,10 @@ function tag() {
226
226
  const values = { member: 0 };`;
227
227
 
228
228
  const cases = [
229
+ // Call-containing but provably-text-primitive expressions — `empty() + ''`,
230
+ // `\`${empty()}\``, `void empty()`, `values[getKey()]++` — are NOT in this
231
+ // list: they cannot hold an element, so they DO merge into the text run
232
+ // (see 'merges provably-primitive call-containing children').
229
233
  { name: 'call expression', expression: 'child(\'call\')' },
230
234
  { name: 'new expression', expression: 'new Constructed(\'new\')' },
231
235
  { name: 'chain expression', expression: 'factory?.(\'chain\')' },
@@ -239,7 +243,6 @@ const values = { member: 0 };`;
239
243
  },
240
244
  { name: 'array expression', expression: '[child(\'array\')]' },
241
245
  { name: 'assignment expression', expression: 'assigned = child(\'assignment\')' },
242
- { name: 'binary expression', expression: 'empty() + \'\'' },
243
246
  { name: 'logical expression', expression: 'true && child(\'logical\')' },
244
247
  { name: 'conditional expression', expression: 'true ? child(\'conditional\') : \'\'' },
245
248
  { name: 'member expression', expression: 'lookup[getKey()]' },
@@ -249,9 +252,6 @@ const values = { member: 0 };`;
249
252
  },
250
253
  { name: 'sequence expression', expression: '(touch(), child(\'sequence\'))' },
251
254
  { name: 'tagged template expression', expression: 'tag`tagged`' },
252
- { name: 'template literal', expression: '`${empty()}`' },
253
- { name: 'unary expression', expression: 'void empty()' },
254
- { name: 'update expression', expression: 'values[getKey()]++' },
255
255
  ];
256
256
 
257
257
  for (const test_case of cases) {
@@ -268,6 +268,31 @@ export function App() @{
268
268
  }
269
269
  });
270
270
 
271
+ it(
272
+ 'merges provably-primitive call-containing children into one escaped text run in SSR output',
273
+ () => {
274
+ const source = `function empty() {
275
+ return null;
276
+ }
277
+ function fetchLabel() {
278
+ return 'fetched';
279
+ }
280
+ export function App() @{
281
+ <div>{'label:'}{void empty()}{String(fetchLabel())}</div>
282
+ }`;
283
+
284
+ const { code } = compile(source, 'primitive-merge.tsrx', { mode: 'server' });
285
+
286
+ // One escaped text run — no render_expression, no hydration markers; a
287
+ // provably-string child concatenates bare (no String(… ?? '') wrap).
288
+ expect(code).toContain(
289
+ '_$_.escape(\'label:\' + (String(void empty() ?? \'\') + String(fetchLabel())))',
290
+ );
291
+ expect(code).not.toContain('render_expression');
292
+ expect(code).not.toContain('String(String(');
293
+ },
294
+ );
295
+
271
296
  it('decodes JSX-style entities before server text escaping', () => {
272
297
  const result = compile(
273
298
  `function App() @{
@@ -164,4 +164,30 @@ describe('composite > props', () => {
164
164
  expect(document.querySelector('p').textContent).toBe('Description 1');
165
165
  expect(document.querySelector('price').textContent).toBe('15');
166
166
  });
167
+
168
+ it('renders a template element passed inline as a prop', async () => {
169
+ // `props.header` is not provably a string, so it renders through
170
+ // `render_expression`, which recognizes template-element values —
171
+ // inline escaping would stringify the element to `[object Object]`.
172
+ function Child(props: { header: unknown }) @{
173
+ <div>{props.header}</div>
174
+ }
175
+
176
+ function App() @{
177
+ <Child
178
+ header={<h1>
179
+ @if (true) {
180
+ <span>{'yes'}</span>
181
+ } @else {
182
+ <span>{'no'}</span>
183
+ }
184
+ </h1>}
185
+ />
186
+ }
187
+
188
+ const { body } = await render(App);
189
+ expect(body).toContain('<h1>');
190
+ expect(body).toContain('<span>yes</span>');
191
+ expect(body).not.toContain('<span>no</span>');
192
+ });
167
193
  });
@@ -203,7 +203,7 @@ describe('streaming try/pending boundaries', () => {
203
203
  const id = slot_match![1];
204
204
  const chunk = chunks[1];
205
205
  expect(chunk).toContain(`<template data-ripple-chunk="${id}">`);
206
- expect(chunk).toContain('<p>async-data</p>');
206
+ expect(chunk).toContain('<p><!--[-->async-data<!--]--></p>');
207
207
  // static markup after the suspension point belongs to the chunk
208
208
  // (regression: it used to be lost when the boundary cleared its buffer)
209
209
  expect(chunk).toContain('<footer>static-after</footer>');
@@ -242,7 +242,7 @@ describe('streaming try/pending boundaries', () => {
242
242
  await result;
243
243
 
244
244
  const chunk = chunks[chunks.length - 1];
245
- expect(chunk).toContain('<p>body</p>');
245
+ expect(chunk).toContain('<p><!--[-->body<!--]--></p>');
246
246
  expect(chunk).not.toContain('failed');
247
247
  });
248
248
 
@@ -272,7 +272,7 @@ describe('streaming try/pending boundaries', () => {
272
272
 
273
273
  const all = chunks.join('');
274
274
  const chunk = chunks[chunks.length - 1];
275
- expect(chunk).toContain('<em>boom</em>');
275
+ expect(chunk).toContain('<em><!--[-->boom<!--]--></em>');
276
276
  expect(chunk).toContain('<template data-ripple-chunk=');
277
277
  // the rejected trackAsync envelope reaches the wire before the chunk
278
278
  // that hydrates the catch branch from it
@@ -380,8 +380,8 @@ describe('streaming nested and sibling boundaries', () => {
380
380
 
381
381
  expect(chunks).toHaveLength(2);
382
382
  const chunk = chunks[1];
383
- expect(chunk).toContain('<p class="outer">outer-done</p>');
384
- expect(chunk).toContain('<p class="inner">inner-done</p>');
383
+ expect(chunk).toContain('<p class="outer"><!--[-->outer-done<!--]--></p>');
384
+ expect(chunk).toContain('<p class="inner"><!--[-->inner-done<!--]--></p>');
385
385
  // coalesced: the inner boundary is inlined, no open slot, no fallback
386
386
  expect(chunk).not.toContain('inner-loading');
387
387
  expect(chunk).not.toMatch(SLOT_OPEN_RE);
@@ -402,7 +402,7 @@ describe('streaming nested and sibling boundaries', () => {
402
402
 
403
403
  expect(chunks).toHaveLength(2);
404
404
  const parent_chunk = chunks[1];
405
- expect(parent_chunk).toContain('<p class="outer">outer-done</p>');
405
+ expect(parent_chunk).toContain('<p class="outer"><!--[-->outer-done<!--]--></p>');
406
406
  // the child is still pending: its slot (wrapper + fallback) ships
407
407
  // inside the parent chunk
408
408
  expect(parent_chunk).toContain('inner-loading');
@@ -412,7 +412,7 @@ describe('streaming nested and sibling boundaries', () => {
412
412
  await result;
413
413
 
414
414
  expect(chunks).toHaveLength(3);
415
- expect(chunks[2]).toContain('<p class="inner">inner-done</p>');
415
+ expect(chunks[2]).toContain('<p class="inner"><!--[-->inner-done<!--]--></p>');
416
416
  });
417
417
 
418
418
  it('streams sibling boundaries out of order, as each settles', async () => {
@@ -603,7 +603,7 @@ describe('streaming with a document template', () => {
603
603
  expect(isClosed()).toBe(true);
604
604
  // the suffix is the final push, after the last chunk
605
605
  expect(chunks[chunks.length - 1]).toBe('</div></body></html>');
606
- expect(chunks[chunks.length - 2]).toContain('<p>done</p>');
606
+ expect(chunks[chunks.length - 2]).toContain('<p><!--[-->done<!--]--></p>');
607
607
  });
608
608
  });
609
609
 
@@ -647,7 +647,7 @@ describe('streaming head content', () => {
647
647
  expect(chunk).toMatch(
648
648
  /<template data-ripple-head="\d+">[\s\S]*?<title>title:late<\/title>[\s\S]*?<\/template>/,
649
649
  );
650
- expect(chunk).toContain('<p class="has-head">late</p>');
650
+ expect(chunk).toContain('<p class="has-head"><!--[-->late<!--]--></p>');
651
651
  // the head template precedes the content template
652
652
  expect(chunk.indexOf('data-ripple-head')).toBeLessThan(chunk.indexOf('data-ripple-chunk'));
653
653
  });
@@ -66,7 +66,7 @@ describe('trackAsync serialization (server)', () => {
66
66
  }
67
67
 
68
68
  const { body } = await render(App);
69
- expect(body).toContain('<li>a</li>');
69
+ expect(body).toContain('<li><!--[-->a<!--]--></li>');
70
70
  expect(body).toContain('__ripple_ta_');
71
71
  });
72
72
 
@@ -84,7 +84,7 @@ describe('trackAsync serialization (server)', () => {
84
84
 
85
85
  const { body } = await render(App);
86
86
  const public_message = DEV ? 'fetch failed' : TRACK_ASYNC_PUBLIC_ERROR_MESSAGE;
87
- expect(body).toContain(`<p class="error">${public_message}</p>`);
87
+ expect(body).toContain(`<p class="error"><!--[-->${public_message}<!--]--></p>`);
88
88
  expect(body).toContain('__ripple_ta_');
89
89
  expect(body).toContain('"ok":false');
90
90
  expect(body).toContain(`"message":"${public_message}"`);
@@ -110,7 +110,7 @@ describe('trackAsync serialization (server)', () => {
110
110
 
111
111
  const { body } = await render(Parent);
112
112
  const public_message = DEV ? 'child error' : TRACK_ASYNC_PUBLIC_ERROR_MESSAGE;
113
- expect(body).toContain(`<p class="parent-error">${public_message}</p>`);
113
+ expect(body).toContain(`<p class="parent-error"><!--[-->${public_message}<!--]--></p>`);
114
114
  expect(body).toContain('__ripple_ta_');
115
115
  expect(body).toContain('"ok":false');
116
116
  expect(body).toContain(`"message":"${public_message}"`);
@@ -132,7 +132,7 @@ describe('trackAsync serialization (server)', () => {
132
132
 
133
133
  const { body } = await render(App);
134
134
  const public_message = DEV ? 'sync failure' : TRACK_ASYNC_PUBLIC_ERROR_MESSAGE;
135
- expect(body).toContain(`<p class="error">${public_message}</p>`);
135
+ expect(body).toContain(`<p class="error"><!--[-->${public_message}<!--]--></p>`);
136
136
  expect(body).toContain('__ripple_ta_');
137
137
  expect(body).toContain('"ok":false');
138
138
  expect(body).toContain(`"message":"${public_message}"`);
@@ -0,0 +1,48 @@
1
+ // Compile-only assertions for `Component`'s return type. `mount`, `hydrate`,
2
+ // and the server `render` all take a `Component`, so this type decides which
3
+ // components typecheck at their call sites. The accepted values below are the
4
+ // ones both runtimes render (see `render_value` in
5
+ // src/runtime/internal/client/expression.js and `render_expression` in
6
+ // src/runtime/internal/server/index.js); the rejected ones fall through to
7
+ // stringification or throw.
8
+ import type { Component } from 'ripple';
9
+
10
+ const returns_nothing: Component = () => {};
11
+ const returns_string: Component = () => 'text';
12
+ const returns_number: Component = () => 42;
13
+ const returns_bigint: Component = () => 1n;
14
+ const returns_boolean: Component = () => true;
15
+ const returns_null: Component = () => null;
16
+ const returns_undefined: Component = () => undefined;
17
+ const returns_array: Component = () => ['text', 42, null, ['nested']];
18
+
19
+ // A component that only sometimes returns early still has to typecheck.
20
+ const returns_union: Component<{ ready: boolean }> = (props) => {
21
+ if (!props.ready) {
22
+ return 'Waiting';
23
+ }
24
+ };
25
+
26
+ // @ts-expect-error promises are not awaited by either runtime
27
+ const returns_promise: Component = async () => 'text';
28
+
29
+ // @ts-expect-error a function is stringified, not called
30
+ const returns_function: Component = () => () => 'text';
31
+
32
+ // @ts-expect-error the client throws on `symbol + ''`
33
+ const returns_symbol: Component = () => Symbol('nope');
34
+
35
+ export type {};
36
+
37
+ void returns_nothing;
38
+ void returns_string;
39
+ void returns_number;
40
+ void returns_bigint;
41
+ void returns_boolean;
42
+ void returns_null;
43
+ void returns_undefined;
44
+ void returns_array;
45
+ void returns_union;
46
+ void returns_promise;
47
+ void returns_function;
48
+ void returns_symbol;
@@ -36,7 +36,11 @@ describe('runtime imports', () => {
36
36
  expect(barrel_imports).toEqual([]);
37
37
  });
38
38
 
39
- it('exports Fragment from both public runtimes', async () => {
39
+ // Pulls both entire runtime module graphs through vite's transform pipeline
40
+ // on a cold cache, which can take well past the 5s default when the rest of
41
+ // the suite is competing for workers. The assertion is about exports, not
42
+ // speed, so give the imports room rather than letting load decide.
43
+ it('exports Fragment from both public runtimes', { timeout: 30_000 }, async () => {
40
44
  const [client, server] = await Promise.all([
41
45
  import('../../src/runtime/index-client.js'),
42
46
  import('../../src/runtime/index-server.js'),
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
- "include": ["./src/"]
3
+ "include": ["./src/", "./tests/types/"]
4
4
  }
package/types/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExtendedEventOptions } from '@tsrx/core/types';
2
2
  export type { RefValue } from '@tsrx/core/runtime/ref';
3
3
  export type { AddEventOptions, AddEventObject, ExtendedEventOptions } from '@tsrx/core/types';
4
4
 
5
- export type Component<T = Record<string, any>> = (props: T) => void | TSRXElement;
5
+ export type Component<T = Record<string, any>> = (props: T) => Renderable | void;
6
6
 
7
7
  declare const TSRX_ELEMENT: unique symbol;
8
8
  declare const REF_KEY: unique symbol;
@@ -14,6 +14,15 @@ export type TSRXElement<Tag = any> = {
14
14
  readonly __tag?: Tag;
15
15
  };
16
16
 
17
+ /**
18
+ * Every value a component may return and have rendered. Both runtimes dispatch
19
+ * the same way: elements render, arrays flatten recursively, `null`/`undefined`
20
+ * render nothing, and everything else is stringified. Promises, functions, and
21
+ * symbols are not renderable.
22
+ */
23
+ export type Renderable =
24
+ TSRXElement | string | number | bigint | boolean | null | undefined | readonly Renderable[];
25
+
17
26
  /** Type for implicit children fragments rendered with `{children}`. */
18
27
  export type Children = TSRXElement | Component | string | number | boolean | null | undefined;
19
28