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.
- package/CHANGELOG.md +30 -0
- package/package.json +3 -3
- package/tests/client/basic/basic.rendering.test.tsrx +72 -0
- package/tests/client/compiler/compiler.basic.test.tsrx +36 -4
- package/tests/client/composite/composite.props.test.tsrx +46 -0
- package/tests/hydration/basic.test.js +39 -0
- package/tests/hydration/compiled/client/basic.js +133 -5
- package/tests/hydration/compiled/client/if-fragment-controlflow.js +166 -214
- package/tests/hydration/compiled/client/streaming.js +81 -105
- package/tests/hydration/compiled/server/basic.js +121 -2
- package/tests/hydration/compiled/server/events.js +61 -5
- package/tests/hydration/compiled/server/for.js +242 -26
- package/tests/hydration/compiled/server/head.js +45 -5
- package/tests/hydration/compiled/server/html.js +24 -8
- package/tests/hydration/compiled/server/if-fragment-controlflow.js +73 -18
- package/tests/hydration/compiled/server/mixed-control-flow.js +9 -1
- package/tests/hydration/compiled/server/nested-control-flow.js +9 -1
- package/tests/hydration/compiled/server/reactivity.js +43 -3
- package/tests/hydration/compiled/server/streaming.js +131 -38
- package/tests/hydration/compiled/server/track-async-serialization.js +106 -10
- package/tests/hydration/compiled/server/try.js +45 -5
- package/tests/hydration/components/basic.tsrx +43 -0
- package/tests/server/basic.test.tsrx +30 -0
- package/tests/server/compiler.test.tsrx +29 -4
- package/tests/server/composite.props.test.tsrx +26 -0
- package/tests/server/streaming-ssr.test.tsrx +9 -9
- package/tests/server/track-async-serialization.test.tsrx +4 -4
- package/tests/types/component-return.ts +48 -0
- package/tests/utils/runtime-imports.test.js +5 -1
- package/tsconfig.typecheck.json +1 -1
- 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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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">'
|
|
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>'
|
|
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">'
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
384
|
-
expect(chunk).toContain('<p class="inner"
|
|
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"
|
|
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"
|
|
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
|
|
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"
|
|
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
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
|
|
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'),
|
package/tsconfig.typecheck.json
CHANGED
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) =>
|
|
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
|
|