ripple 0.3.128 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +267 -0
- package/package.json +10 -4
- package/src/runtime/element.js +23 -6
- package/src/runtime/index-client.js +81 -41
- package/src/runtime/index-server.js +1 -0
- package/src/runtime/internal/client/blocks.js +173 -91
- package/src/runtime/internal/client/component.js +4 -30
- package/src/runtime/internal/client/composite.js +12 -13
- package/src/runtime/internal/client/constants.js +9 -0
- package/src/runtime/internal/client/context.js +12 -19
- package/src/runtime/internal/client/css.js +7 -9
- package/src/runtime/internal/client/events.js +178 -90
- package/src/runtime/internal/client/expression.js +45 -25
- package/src/runtime/internal/client/for.js +409 -129
- package/src/runtime/internal/client/head.js +10 -5
- package/src/runtime/internal/client/html.js +1 -0
- package/src/runtime/internal/client/hydration.js +152 -10
- package/src/runtime/internal/client/if.js +239 -50
- package/src/runtime/internal/client/index.js +4 -12
- package/src/runtime/internal/client/operations.js +40 -61
- package/src/runtime/internal/client/portal.js +163 -85
- package/src/runtime/internal/client/render.js +39 -39
- package/src/runtime/internal/client/rpc.js +3 -3
- package/src/runtime/internal/client/runtime.js +592 -433
- package/src/runtime/internal/client/selector.js +5 -2
- package/src/runtime/internal/client/template.js +133 -158
- package/src/runtime/internal/client/transport.js +38 -0
- package/src/runtime/internal/client/try.js +465 -426
- package/src/runtime/internal/client/types.d.ts +56 -23
- package/src/runtime/internal/client/utils.js +1 -3
- package/src/runtime/internal/server/dynamic.js +20 -21
- package/src/runtime/internal/server/index.js +325 -275
- package/src/runtime/internal/server/rpc.js +3 -2
- package/src/runtime/internal/server/transport.js +16 -0
- package/src/runtime/transport.js +21 -0
- package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
- package/tests/client/array/array.copy-within.test.tsrx +12 -12
- package/tests/client/array/array.derived.test.tsrx +46 -46
- package/tests/client/array/array.iteration.test.tsrx +10 -10
- package/tests/client/array/array.mutations.test.tsrx +20 -20
- package/tests/client/array/array.static.test.tsrx +11 -11
- package/tests/client/array/array.to-methods.test.tsrx +6 -6
- package/tests/client/async-suspend.test.tsrx +74 -75
- package/tests/client/basic/basic.attributes.test.tsrx +86 -83
- package/tests/client/basic/basic.collections.test.tsrx +12 -12
- package/tests/client/basic/basic.components.test.tsrx +99 -25
- package/tests/client/basic/basic.errors.test.tsrx +18 -18
- package/tests/client/basic/basic.events.test.tsrx +42 -42
- package/tests/client/basic/basic.get-set.test.tsrx +8 -8
- package/tests/client/basic/basic.reactivity.test.tsrx +73 -38
- package/tests/client/basic/basic.rendering.test.tsrx +25 -25
- package/tests/client/basic/basic.utilities.test.tsrx +3 -3
- package/tests/client/boundaries.test.tsrx +8 -8
- package/tests/client/boxed-locals.test.tsrx +452 -0
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
- package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
- package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
- package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
- package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
- package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
- package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
- package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
- package/tests/client/composite/composite.props.test.tsrx +149 -12
- package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
- package/tests/client/composite/composite.render.test.tsrx +26 -9
- package/tests/client/css/scoped-styles.test.tsrx +8 -8
- package/tests/client/date.test.tsrx +42 -42
- package/tests/client/derived-release.test.tsrx +370 -0
- package/tests/client/dynamic-elements.test.tsrx +45 -45
- package/tests/client/events.test.tsrx +97 -71
- package/tests/client/for-selector.test.tsrx +14 -14
- package/tests/client/for-single-root-items.test.tsrx +118 -3
- package/tests/client/for.test.tsrx +332 -31
- package/tests/client/head.test.tsrx +19 -19
- package/tests/client/html.test.tsrx +11 -9
- package/tests/client/if-block.test.tsrx +80 -0
- package/tests/client/map.test.tsrx +16 -16
- package/tests/client/media-query.test.tsrx +7 -7
- package/tests/client/portal.test.tsrx +224 -26
- package/tests/client/primitive-text.test.tsrx +50 -0
- package/tests/client/prop-kind-switch.test.tsrx +141 -0
- package/tests/client/ref.test.tsrx +17 -17
- package/tests/client/return.test.tsrx +26 -3
- package/tests/client/root-boundary.test.tsrx +42 -0
- package/tests/client/scoped-flush.test.tsrx +8 -8
- package/tests/client/set.test.tsrx +6 -6
- package/tests/client/svg.test.tsrx +11 -11
- package/tests/client/switch.test.tsrx +44 -44
- package/tests/client/track-async-hydration.test.tsrx +4 -4
- package/tests/client/try.test.tsrx +151 -83
- package/tests/client/tsx.test.tsrx +43 -40
- package/tests/client/url/url.derived.test.tsrx +6 -6
- package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
- package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
- package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
- package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
- package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
- package/tests/hydration/basic.test.js +74 -1
- package/tests/hydration/build-components.js +22 -1
- package/tests/hydration/compiled/client/basic.js +1142 -713
- package/tests/hydration/compiled/client/composite.js +151 -100
- package/tests/hydration/compiled/client/events.js +184 -171
- package/tests/hydration/compiled/client/for.js +1472 -1132
- package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
- package/tests/hydration/compiled/client/head.js +254 -201
- package/tests/hydration/compiled/client/hmr.js +54 -41
- package/tests/hydration/compiled/client/html-in-template.js +54 -39
- package/tests/hydration/compiled/client/html.js +1633 -1263
- package/tests/hydration/compiled/client/if-children.js +420 -270
- package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
- package/tests/hydration/compiled/client/if.js +229 -219
- package/tests/hydration/compiled/client/mixed-control-flow.js +420 -404
- package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
- package/tests/hydration/compiled/client/portal.js +87 -95
- package/tests/hydration/compiled/client/reactivity.js +135 -101
- package/tests/hydration/compiled/client/return.js +28 -19
- package/tests/hydration/compiled/client/streaming.js +390 -306
- package/tests/hydration/compiled/client/switch.js +258 -266
- package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
- package/tests/hydration/compiled/client/try.js +173 -129
- package/tests/hydration/compiled/client/typed-text.js +71 -0
- package/tests/hydration/compiled/fixtures/money.js +10 -0
- package/tests/hydration/compiled/server/basic.js +123 -6
- package/tests/hydration/compiled/server/composite.js +16 -30
- package/tests/hydration/compiled/server/events.js +66 -18
- package/tests/hydration/compiled/server/for.js +117 -41
- package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
- package/tests/hydration/compiled/server/head.js +20 -20
- package/tests/hydration/compiled/server/hmr.js +2 -2
- package/tests/hydration/compiled/server/html.js +70 -238
- package/tests/hydration/compiled/server/if-children.js +107 -15
- package/tests/hydration/compiled/server/if.js +11 -15
- package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
- package/tests/hydration/compiled/server/portal.js +14 -22
- package/tests/hydration/compiled/server/reactivity.js +14 -14
- package/tests/hydration/compiled/server/streaming.js +19 -19
- package/tests/hydration/compiled/server/switch.js +8 -8
- package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
- package/tests/hydration/compiled/server/try.js +8 -8
- package/tests/hydration/compiled/server/typed-text.js +17 -0
- package/tests/hydration/components/basic.tsrx +44 -8
- package/tests/hydration/components/composite.tsrx +2 -2
- package/tests/hydration/components/events.tsrx +25 -25
- package/tests/hydration/components/for.tsrx +115 -67
- package/tests/hydration/components/fragment-cursor.tsrx +650 -0
- package/tests/hydration/components/head.tsrx +16 -16
- package/tests/hydration/components/hmr.tsrx +2 -2
- package/tests/hydration/components/html.tsrx +7 -7
- package/tests/hydration/components/if-children.tsrx +72 -25
- package/tests/hydration/components/if.tsrx +18 -18
- package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
- package/tests/hydration/components/portal.tsrx +3 -3
- package/tests/hydration/components/reactivity.tsrx +16 -16
- package/tests/hydration/components/streaming.tsrx +20 -20
- package/tests/hydration/components/switch.tsrx +20 -20
- package/tests/hydration/components/text-types.ts +4 -0
- package/tests/hydration/components/track-async-serialization.tsrx +80 -26
- package/tests/hydration/components/try.tsrx +8 -8
- package/tests/hydration/components/typed-text.tsrx +22 -0
- package/tests/hydration/fixtures/money.js +10 -0
- package/tests/hydration/for.test.js +36 -0
- package/tests/hydration/fragment-cursor.test.js +111 -0
- package/tests/hydration/if-children.test.js +24 -0
- package/tests/hydration/streaming.test.js +19 -0
- package/tests/hydration/track-async-serialization.test.js +87 -2
- package/tests/hydration/tsconfig.text-types.json +10 -0
- package/tests/hydration/typed-text.test.js +34 -0
- package/tests/server/await.test.tsrx +3 -3
- package/tests/server/basic.attributes.test.tsrx +42 -42
- package/tests/server/basic.components.test.tsrx +64 -8
- package/tests/server/basic.test.tsrx +34 -32
- package/tests/server/compiler.test.tsrx +1 -70
- package/tests/server/composite.props.test.tsrx +6 -6
- package/tests/server/dynamic-elements.test.tsrx +31 -8
- package/tests/server/for.test.tsrx +94 -0
- package/tests/server/head.test.tsrx +11 -11
- package/tests/server/scoped-styles.test.tsrx +2 -2
- package/tests/server/streaming-ssr.test.tsrx +48 -43
- package/tests/server/track-async-serialization.test.tsrx +204 -21
- package/tests/server/try.test.tsrx +48 -48
- package/tests/types/transport.ts +40 -0
- package/tests/utils/vite-plugin-config.test.js +23 -0
- package/types/index.d.ts +48 -7
- package/types/server.d.ts +3 -0
- package/types/transport.d.ts +15 -0
- package/src/runtime/internal/client/switch.js +0 -113
- package/src/utils/errors.js +0 -13
- package/tests/client/lazy-array.test.tsrx +0 -31
- package/tests/client/lazy-destructuring.test.tsrx +0 -438
- package/tests/client/tracked-index-access.test.tsrx +0 -113
- package/tests/server/lazy-destructuring.test.tsrx +0 -595
- package/tests/server/tracked-index-access.test.tsrx +0 -76
|
@@ -16,17 +16,17 @@ export function SwitchStatic() @{
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function SwitchReactive() @{
|
|
19
|
-
|
|
19
|
+
const status = track<'a' | 'b' | 'c'>('a');
|
|
20
20
|
<>
|
|
21
21
|
<button
|
|
22
22
|
class="toggle"
|
|
23
23
|
onClick={() => {
|
|
24
|
-
if (status === 'a') status = 'b';
|
|
25
|
-
else if (status === 'b') status = 'c';
|
|
26
|
-
else status = 'a';
|
|
24
|
+
if (status.value === 'a') status.value = 'b';
|
|
25
|
+
else if (status.value === 'b') status.value = 'c';
|
|
26
|
+
else status.value = 'a';
|
|
27
27
|
}}
|
|
28
28
|
>{'Toggle'}</button>
|
|
29
|
-
@switch (status) {
|
|
29
|
+
@switch (status.value) {
|
|
30
30
|
@case 'a': {
|
|
31
31
|
<div class="case-a">Case A</div>
|
|
32
32
|
}
|
|
@@ -55,17 +55,17 @@ export function SwitchFallthrough() @{
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function SwitchNumericLevels() @{
|
|
58
|
-
|
|
58
|
+
const level = track<1 | 2 | 3>(1);
|
|
59
59
|
<>
|
|
60
60
|
<button
|
|
61
61
|
class="level-toggle"
|
|
62
62
|
onClick={() => {
|
|
63
|
-
if (level === 1) level = 2;
|
|
64
|
-
else if (level === 2) level = 3;
|
|
65
|
-
else level = 1;
|
|
63
|
+
if (level.value === 1) level.value = 2;
|
|
64
|
+
else if (level.value === 2) level.value = 3;
|
|
65
|
+
else level.value = 1;
|
|
66
66
|
}}
|
|
67
67
|
>{'Toggle Level'}</button>
|
|
68
|
-
@switch (level) {
|
|
68
|
+
@switch (level.value) {
|
|
69
69
|
@case 1: {
|
|
70
70
|
<div class="level-1">{'Level 1'}</div>
|
|
71
71
|
}
|
|
@@ -80,17 +80,17 @@ export function SwitchNumericLevels() @{
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function SwitchBlockScoped() @{
|
|
83
|
-
|
|
83
|
+
const level = track<1 | 2 | 3>(1);
|
|
84
84
|
<>
|
|
85
85
|
<button
|
|
86
86
|
class="block-toggle"
|
|
87
87
|
onClick={() => {
|
|
88
|
-
if (level === 1) level = 2;
|
|
89
|
-
else if (level === 2) level = 3;
|
|
90
|
-
else level = 1;
|
|
88
|
+
if (level.value === 1) level.value = 2;
|
|
89
|
+
else if (level.value === 2) level.value = 3;
|
|
90
|
+
else level.value = 1;
|
|
91
91
|
}}
|
|
92
92
|
>{'Toggle'}</button>
|
|
93
|
-
@switch (level) {
|
|
93
|
+
@switch (level.value) {
|
|
94
94
|
@case 1: {
|
|
95
95
|
<div class="block-1">{'Block 1'}</div>
|
|
96
96
|
}
|
|
@@ -105,17 +105,17 @@ export function SwitchBlockScoped() @{
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export function SwitchNoBreak() @{
|
|
108
|
-
|
|
108
|
+
const level = track<1 | 2 | 3>(1);
|
|
109
109
|
<>
|
|
110
110
|
<button
|
|
111
111
|
class="nobreak-toggle"
|
|
112
112
|
onClick={() => {
|
|
113
|
-
if (level === 1) level = 2;
|
|
114
|
-
else if (level === 2) level = 3;
|
|
115
|
-
else level = 1;
|
|
113
|
+
if (level.value === 1) level.value = 2;
|
|
114
|
+
else if (level.value === 2) level.value = 3;
|
|
115
|
+
else level.value = 1;
|
|
116
116
|
}}
|
|
117
117
|
>{'Toggle'}</button>
|
|
118
|
-
@switch (level) {
|
|
118
|
+
@switch (level.value) {
|
|
119
119
|
@case 1: {
|
|
120
120
|
<div class="nobreak-1">{'NoBreak 1'}</div>
|
|
121
121
|
}
|
|
@@ -1,29 +1,66 @@
|
|
|
1
1
|
import { track, trackAsync } from 'ripple';
|
|
2
|
+
import { Money } from '../fixtures/money.js';
|
|
3
|
+
|
|
4
|
+
export const transport = {
|
|
5
|
+
Money: {
|
|
6
|
+
encode: (value: unknown) => value instanceof Money && [value.amount, value.currency],
|
|
7
|
+
decode: ([amount, currency]: [number, string]) => new Money(amount, currency),
|
|
8
|
+
},
|
|
9
|
+
};
|
|
2
10
|
|
|
3
11
|
module server {
|
|
12
|
+
import { Money } from '../fixtures/money.js';
|
|
13
|
+
|
|
4
14
|
export async function formatValue(n: number): Promise<string> {
|
|
5
15
|
return `server-${n}`;
|
|
6
16
|
}
|
|
17
|
+
|
|
18
|
+
export async function doubleMoney(value: Money): Promise<Money> {
|
|
19
|
+
if (!(value instanceof Money)) throw new Error('RPC argument was not revived');
|
|
20
|
+
return new Money(value.amount * 2, value.currency);
|
|
21
|
+
}
|
|
7
22
|
}
|
|
8
23
|
|
|
9
|
-
import { formatValue } from server;
|
|
24
|
+
import { doubleMoney, formatValue } from server;
|
|
25
|
+
|
|
26
|
+
function MoneyResult({ count }: { count: { value: number } }) @{
|
|
27
|
+
const money = trackAsync(() => doubleMoney(new Money(count.value, 'USD')));
|
|
28
|
+
<p class="result">{money.value.format()}</p>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AsyncCustomType() @{
|
|
32
|
+
const count = track(6);
|
|
33
|
+
<>
|
|
34
|
+
<button
|
|
35
|
+
class="increment"
|
|
36
|
+
onClick={() => {
|
|
37
|
+
count.value++;
|
|
38
|
+
}}
|
|
39
|
+
>{'increment'}</button>
|
|
40
|
+
@try {
|
|
41
|
+
<MoneyResult {count} />
|
|
42
|
+
} @pending {
|
|
43
|
+
<p class="loading">{'loading...'}</p>
|
|
44
|
+
}
|
|
45
|
+
</>
|
|
46
|
+
}
|
|
10
47
|
|
|
11
48
|
function ServerCallResult({ count }: { count: { value: number } }) @{
|
|
12
|
-
|
|
13
|
-
<p class="result">{data}</p>
|
|
49
|
+
const data = trackAsync(() => formatValue(count.value));
|
|
50
|
+
<p class="result">{data.value}</p>
|
|
14
51
|
}
|
|
15
52
|
|
|
16
53
|
export function AsyncWithServerCall() @{
|
|
17
|
-
|
|
54
|
+
const count = track(0);
|
|
18
55
|
<>
|
|
19
56
|
<button
|
|
20
57
|
class="increment"
|
|
21
58
|
onClick={() => {
|
|
22
|
-
count++;
|
|
59
|
+
count.value++;
|
|
23
60
|
}}
|
|
24
61
|
>{'increment'}</button>
|
|
25
62
|
@try {
|
|
26
|
-
<ServerCallResult count
|
|
63
|
+
<ServerCallResult {count} />
|
|
27
64
|
} @pending {
|
|
28
65
|
<p class="loading">{'loading...'}</p>
|
|
29
66
|
}
|
|
@@ -32,8 +69,8 @@ export function AsyncWithServerCall() @{
|
|
|
32
69
|
|
|
33
70
|
export function AsyncSimpleValue() @{
|
|
34
71
|
@try {
|
|
35
|
-
|
|
36
|
-
<p class="result">{data}</p>
|
|
72
|
+
const data = trackAsync(() => Promise.resolve('hydrated value'));
|
|
73
|
+
<p class="result">{data.value}</p>
|
|
37
74
|
} @pending {
|
|
38
75
|
<p class="loading">{'loading...'}</p>
|
|
39
76
|
}
|
|
@@ -41,8 +78,8 @@ export function AsyncSimpleValue() @{
|
|
|
41
78
|
|
|
42
79
|
export function AsyncNumericValue() @{
|
|
43
80
|
@try {
|
|
44
|
-
|
|
45
|
-
<span class="count">{count}</span>
|
|
81
|
+
const count = trackAsync(() => Promise.resolve(42));
|
|
82
|
+
<span class="count">{count.value}</span>
|
|
46
83
|
} @pending {
|
|
47
84
|
<span class="pending">{'...'}</span>
|
|
48
85
|
}
|
|
@@ -50,10 +87,10 @@ export function AsyncNumericValue() @{
|
|
|
50
87
|
|
|
51
88
|
export function AsyncObjectValue() @{
|
|
52
89
|
@try {
|
|
53
|
-
|
|
90
|
+
const user = trackAsync(() => Promise.resolve({ name: 'Alice', age: 30 }));
|
|
54
91
|
<div class="user">
|
|
55
|
-
<span class="name">{user.name}</span>
|
|
56
|
-
<span class="age">{user.age}</span>
|
|
92
|
+
<span class="name">{user.value.name}</span>
|
|
93
|
+
<span class="age">{user.value.age}</span>
|
|
57
94
|
</div>
|
|
58
95
|
} @pending {
|
|
59
96
|
<div class="loading">{'loading user...'}</div>
|
|
@@ -62,11 +99,11 @@ export function AsyncObjectValue() @{
|
|
|
62
99
|
|
|
63
100
|
export function AsyncMultipleValues() @{
|
|
64
101
|
@try {
|
|
65
|
-
|
|
66
|
-
|
|
102
|
+
const first = trackAsync(() => Promise.resolve('alpha'));
|
|
103
|
+
const second = trackAsync(() => Promise.resolve('beta'));
|
|
67
104
|
<div class="multi">
|
|
68
|
-
<span class="first">{first}</span>
|
|
69
|
-
<span class="second">{second}</span>
|
|
105
|
+
<span class="first">{first.value}</span>
|
|
106
|
+
<span class="second">{second.value}</span>
|
|
70
107
|
</div>
|
|
71
108
|
} @pending {
|
|
72
109
|
<div class="loading">{'loading...'}</div>
|
|
@@ -75,8 +112,8 @@ export function AsyncMultipleValues() @{
|
|
|
75
112
|
|
|
76
113
|
export function AsyncWithCatch() @{
|
|
77
114
|
@try {
|
|
78
|
-
|
|
79
|
-
<p class="result">{data}</p>
|
|
115
|
+
const data = trackAsync(() => Promise.reject(new Error('fetch failed')));
|
|
116
|
+
<p class="result">{data.value}</p>
|
|
80
117
|
} @pending {
|
|
81
118
|
<p class="loading">{'loading...'}</p>
|
|
82
119
|
} @catch (e) {
|
|
@@ -86,8 +123,8 @@ export function AsyncWithCatch() @{
|
|
|
86
123
|
|
|
87
124
|
export function ChildWithError() @{
|
|
88
125
|
@try {
|
|
89
|
-
|
|
90
|
-
<p class="result">{data}</p>
|
|
126
|
+
const data = trackAsync(() => Promise.reject(new Error('child error')));
|
|
127
|
+
<p class="result">{data.value}</p>
|
|
91
128
|
} @pending {
|
|
92
129
|
<p class="pending">{'loading...'}</p>
|
|
93
130
|
}
|
|
@@ -102,21 +139,38 @@ export function ParentWithCatch() @{
|
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
function ReactiveDependencyResult({ count }: { count: { value: number } }) @{
|
|
105
|
-
|
|
106
|
-
<p class="result">{data}</p>
|
|
142
|
+
const data = trackAsync(() => Promise.resolve(`count-${count.value}`));
|
|
143
|
+
<p class="result">{data.value}</p>
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
export function AsyncWithReactiveDependency() @{
|
|
110
|
-
|
|
147
|
+
const count = track(0);
|
|
148
|
+
<>
|
|
149
|
+
<button
|
|
150
|
+
class="increment"
|
|
151
|
+
onClick={() => {
|
|
152
|
+
count.value++;
|
|
153
|
+
}}
|
|
154
|
+
>{'increment'}</button>
|
|
155
|
+
@try {
|
|
156
|
+
<ReactiveDependencyResult {count} />
|
|
157
|
+
} @pending {
|
|
158
|
+
<p class="loading">{'loading...'}</p>
|
|
159
|
+
}
|
|
160
|
+
</>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function AsyncWithReadOnlyDependency() @{
|
|
164
|
+
const count = track(0);
|
|
111
165
|
<>
|
|
112
166
|
<button
|
|
113
167
|
class="increment"
|
|
114
168
|
onClick={() => {
|
|
115
|
-
count++;
|
|
169
|
+
count.value++;
|
|
116
170
|
}}
|
|
117
171
|
>{'increment'}</button>
|
|
118
172
|
@try {
|
|
119
|
-
<ReactiveDependencyResult count={
|
|
173
|
+
<ReactiveDependencyResult count={count.readOnly()} />
|
|
120
174
|
} @pending {
|
|
121
175
|
<p class="loading">{'loading...'}</p>
|
|
122
176
|
}
|
|
@@ -17,13 +17,13 @@ export function RootThrows() @{
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function RootAsyncDirect() @{
|
|
20
|
-
|
|
21
|
-
<p class="root-async-value">{value}</p>
|
|
20
|
+
const value = trackAsync(() => Promise.resolve('root async value'));
|
|
21
|
+
<p class="root-async-value">{value.value}</p>
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function RootAsyncRejects() @{
|
|
25
|
-
|
|
26
|
-
<p class="root-async-value">{value}</p>
|
|
25
|
+
const value = trackAsync(() => Promise.reject(new Error('root async failed')));
|
|
26
|
+
<p class="root-async-value">{value.value}</p>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function AsyncListInTryPending() @{
|
|
@@ -35,9 +35,9 @@ export function AsyncListInTryPending() @{
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function AsyncList() @{
|
|
38
|
-
|
|
38
|
+
const items = trackAsync(() => Promise.resolve(['alpha', 'beta', 'gamma']));
|
|
39
39
|
<ul class="items">
|
|
40
|
-
@for (let item of items) {
|
|
40
|
+
@for (let item of items.value) {
|
|
41
41
|
<li>{item}</li>
|
|
42
42
|
}
|
|
43
43
|
</ul>
|
|
@@ -55,6 +55,6 @@ export function AsyncTryWithLeadingSibling() @{
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function AsyncContent() @{
|
|
58
|
-
|
|
59
|
-
<div class="resolved">{value}</div>
|
|
58
|
+
const value = trackAsync(() => Promise.resolve('ready'));
|
|
59
|
+
<div class="resolved">{value.value}</div>
|
|
60
60
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { track } from 'ripple';
|
|
2
|
+
import type { TextRow } from './text-types';
|
|
3
|
+
|
|
4
|
+
export function TypedText() @{
|
|
5
|
+
const row = track<TextRow>({ label: '<first>', count: 2 });
|
|
6
|
+
<>
|
|
7
|
+
<div class="typed-run">
|
|
8
|
+
{'value: '}
|
|
9
|
+
{row.value.label}
|
|
10
|
+
{row.value.count}
|
|
11
|
+
<span>tail</span>
|
|
12
|
+
</div>
|
|
13
|
+
<p class="typed-number">
|
|
14
|
+
{(0, row.value.count)}
|
|
15
|
+
</p>
|
|
16
|
+
<button
|
|
17
|
+
onClick={() => {
|
|
18
|
+
row.value = { label: '&next', count: 3n };
|
|
19
|
+
}}
|
|
20
|
+
>update</button>
|
|
21
|
+
</>
|
|
22
|
+
}
|
|
@@ -477,4 +477,40 @@ describe('hydration > for blocks', () => {
|
|
|
477
477
|
expect(listItems[2]?.textContent).toBe('B');
|
|
478
478
|
expect(listItems[3]?.textContent).toBe('A');
|
|
479
479
|
});
|
|
480
|
+
|
|
481
|
+
it('hydrates keyed for loop and appends and rotates before a trailing sibling', async () => {
|
|
482
|
+
await hydrateComponent(
|
|
483
|
+
ServerComponents.KeyedForLoopAppendAndRotate,
|
|
484
|
+
ClientComponents.KeyedForLoopAppendAndRotate,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const host = container.querySelector('.host');
|
|
488
|
+
expect(host?.textContent).toBe('123p');
|
|
489
|
+
|
|
490
|
+
container.querySelector('.push')?.click();
|
|
491
|
+
flushSync();
|
|
492
|
+
expect(host?.textContent).toBe('1234p');
|
|
493
|
+
|
|
494
|
+
container.querySelector('.rotate')?.click();
|
|
495
|
+
flushSync();
|
|
496
|
+
expect(host?.textContent).toBe('2341p');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('hydrates a root keyed for loop component and appends and rotates before a trailing sibling', async () => {
|
|
500
|
+
await hydrateComponent(
|
|
501
|
+
ServerComponents.RootKeyedForLoopAppendAndRotate,
|
|
502
|
+
ClientComponents.RootKeyedForLoopAppendAndRotate,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const host = container.querySelector('.host');
|
|
506
|
+
expect(host?.textContent).toBe('123p');
|
|
507
|
+
|
|
508
|
+
container.querySelector('.push')?.click();
|
|
509
|
+
flushSync();
|
|
510
|
+
expect(host?.textContent).toBe('1234p');
|
|
511
|
+
|
|
512
|
+
container.querySelector('.rotate')?.click();
|
|
513
|
+
flushSync();
|
|
514
|
+
expect(host?.textContent).toBe('2341p');
|
|
515
|
+
});
|
|
480
516
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync } from 'ripple';
|
|
3
|
+
import { render } from 'ripple/server';
|
|
4
|
+
import { hydrateComponent, container, stripHydrationMarkers } from '../setup-hydration.js';
|
|
5
|
+
|
|
6
|
+
import * as ServerComponents from './compiled/server/fragment-cursor.js';
|
|
7
|
+
import * as ClientComponents from './compiled/client/fragment-cursor.js';
|
|
8
|
+
|
|
9
|
+
// Component roots, mostly fragments, whose last node the hydration cursor
|
|
10
|
+
// must end on. Each is hydrated on its own and as `Wrap<Name>`: a child
|
|
11
|
+
// followed by siblings that hydrate wrongly if the cursor is off by a node.
|
|
12
|
+
const shapes = [
|
|
13
|
+
'TrailingNavigatedElements',
|
|
14
|
+
'TrailingStaticNavigatedElements',
|
|
15
|
+
'NavigatedThenStatic',
|
|
16
|
+
'LeadingNavigatedThenStatic',
|
|
17
|
+
'TrailingNestedNavigated',
|
|
18
|
+
'NestedNavigatedThenStatic',
|
|
19
|
+
'TrackedTextThenStatic',
|
|
20
|
+
'StaticThenTrackedText',
|
|
21
|
+
'StaticNestedThenStatic',
|
|
22
|
+
'AllStatic',
|
|
23
|
+
'TrailingDynamicChild',
|
|
24
|
+
'DynamicChildThenStatic',
|
|
25
|
+
'IfThenStatic',
|
|
26
|
+
'StaticThenIf',
|
|
27
|
+
'CompThenStatic',
|
|
28
|
+
'StaticThenComp',
|
|
29
|
+
'SiblingComps',
|
|
30
|
+
'UntrackedTextThenStatic',
|
|
31
|
+
'NestedFragmentThenStatic',
|
|
32
|
+
'TrailingNestedFragment',
|
|
33
|
+
'ForThenStatic',
|
|
34
|
+
'SwitchThenStatic',
|
|
35
|
+
'TryThenStatic',
|
|
36
|
+
'StyleThenStatic',
|
|
37
|
+
'CollectionThenStatic',
|
|
38
|
+
'InlineElementThenStatic',
|
|
39
|
+
'IfOnly',
|
|
40
|
+
'IfThenOne',
|
|
41
|
+
'SingleRootWithIf',
|
|
42
|
+
'IfTwoThenStatic',
|
|
43
|
+
'StaticCallThenStatic',
|
|
44
|
+
'StaticThenStyleThenStatic',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Single-root shapes whose children end on a slot the cursor must step past.
|
|
48
|
+
const rootShapes = ['ExprThenSiblingInDiv', 'IfTwoInDiv', 'ForTwoNodeItems'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Server renders and hydrates a component, asserting the hydrated DOM matches
|
|
52
|
+
* the server markup (no duplicated or dropped nodes).
|
|
53
|
+
* @param {string} name
|
|
54
|
+
*/
|
|
55
|
+
async function hydrateShape(name) {
|
|
56
|
+
const { body } = await render(ServerComponents[name]);
|
|
57
|
+
await hydrateComponent(ServerComponents[name], ClientComponents[name]);
|
|
58
|
+
expect(container.innerHTML).toBeHtml(stripHydrationMarkers(body));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clicks the shape's own `.inc` button (when it has one) and checks the
|
|
63
|
+
* hydrated `.count` text updates in place.
|
|
64
|
+
*/
|
|
65
|
+
function clickInner() {
|
|
66
|
+
const inc = container.querySelector('.inc');
|
|
67
|
+
const count = container.querySelector('.count');
|
|
68
|
+
if (inc && count) {
|
|
69
|
+
expect(count.textContent).toBe('0');
|
|
70
|
+
inc.click();
|
|
71
|
+
flushSync();
|
|
72
|
+
expect(count.textContent).toBe('1');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('hydration > fragment cursor', () => {
|
|
77
|
+
describe('fragment as the root', () => {
|
|
78
|
+
for (const name of shapes) {
|
|
79
|
+
it(`hydrates ${name}`, async () => {
|
|
80
|
+
await hydrateShape(name);
|
|
81
|
+
clickInner();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('slot followed by siblings inside an element', () => {
|
|
87
|
+
for (const name of rootShapes) {
|
|
88
|
+
it(`hydrates ${name}`, async () => {
|
|
89
|
+
await hydrateShape(name);
|
|
90
|
+
clickInner();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('fragment as a child with trailing siblings', () => {
|
|
96
|
+
for (const name of shapes) {
|
|
97
|
+
it(`hydrates ${name} followed by siblings`, async () => {
|
|
98
|
+
await hydrateShape(`Wrap${name}`);
|
|
99
|
+
|
|
100
|
+
const after = container.querySelector('.after');
|
|
101
|
+
expect(after?.textContent).toBe('0');
|
|
102
|
+
|
|
103
|
+
container.querySelector('.outer-inc')?.click();
|
|
104
|
+
flushSync();
|
|
105
|
+
expect(after?.textContent).toBe('1');
|
|
106
|
+
|
|
107
|
+
clickInner();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -269,4 +269,28 @@ describe('hydration > if blocks with children', () => {
|
|
|
269
269
|
expect(container.querySelector('.section-heading')?.textContent).toBe('Section Heading');
|
|
270
270
|
expect(container.querySelector('.section-content a')?.textContent).toBe('a link');
|
|
271
271
|
});
|
|
272
|
+
|
|
273
|
+
it('re-renders control-flow roots of all-component children at their own position', async () => {
|
|
274
|
+
await hydrateComponent(
|
|
275
|
+
ServerComponents.ComponentChildrenWithControlFlowRoots,
|
|
276
|
+
ClientComponents.ComponentChildrenWithControlFlowRoots,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const host = container.querySelector('.host');
|
|
280
|
+
expect(host?.textContent).toBe('on123end');
|
|
281
|
+
|
|
282
|
+
// The root `@if` child swaps its branch in place, before the siblings.
|
|
283
|
+
container.querySelector('.toggle')?.click();
|
|
284
|
+
flushSync();
|
|
285
|
+
expect(host?.textContent).toBe('off123end');
|
|
286
|
+
|
|
287
|
+
// The root `@for` child moves an item to its own end, not the host's.
|
|
288
|
+
container.querySelector('.rotate')?.click();
|
|
289
|
+
flushSync();
|
|
290
|
+
expect(host?.textContent).toBe('off231end');
|
|
291
|
+
|
|
292
|
+
container.querySelector('.toggle')?.click();
|
|
293
|
+
flushSync();
|
|
294
|
+
expect(host?.textContent).toBe('on231end');
|
|
295
|
+
});
|
|
272
296
|
});
|
|
@@ -269,6 +269,25 @@ describe('hydration > streamed boundaries (chunk after hydration)', () => {
|
|
|
269
269
|
expect(container.innerHTML).not.toContain('<!--[?');
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
it('hydrates a streamed root slot under a boundary even with rootBoundary: false', async () => {
|
|
273
|
+
const { chunks, done } = startStream(ServerComponents.StreamRootDirect, {
|
|
274
|
+
pending: ServerComponents.RootPending,
|
|
275
|
+
});
|
|
276
|
+
mountShell(chunks[0]);
|
|
277
|
+
|
|
278
|
+
// The shell starts with a slot marker: only a boundary can adopt the
|
|
279
|
+
// fallback and activate the chunk, so the opt-out does not apply here.
|
|
280
|
+
hydrate(ClientComponents.StreamRootDirect, { target: container, rootBoundary: false });
|
|
281
|
+
|
|
282
|
+
ServerComponents.controls.rootDirect.resolve('root data');
|
|
283
|
+
await done;
|
|
284
|
+
receiveChunk(chunks[1]);
|
|
285
|
+
flushSync();
|
|
286
|
+
|
|
287
|
+
expect(container.querySelector('.root-async')?.textContent).toBe('root data');
|
|
288
|
+
expect(container.innerHTML).not.toContain('<!--[?');
|
|
289
|
+
});
|
|
290
|
+
|
|
272
291
|
it('hydrates a root chunk that arrived before hydration through the normal path', async () => {
|
|
273
292
|
const { chunks, done } = startStream(ServerComponents.StreamRootDirect, {
|
|
274
293
|
pending: ServerComponents.RootPending,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
-
import { flushSync } from 'ripple';
|
|
2
|
+
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { flushSync, hydrate, setTransport } from 'ripple';
|
|
4
|
+
import { executeServerFunction, render, setTransport as setServerTransport } from 'ripple/server';
|
|
4
5
|
import * as devalue from 'devalue';
|
|
5
6
|
import { hydrateComponent, container } from '../setup-hydration.js';
|
|
6
7
|
|
|
@@ -12,6 +13,73 @@ const TRACK_ASYNC_ERROR_MESSAGE = DEV ? 'fetch failed' : TRACK_ASYNC_PUBLIC_ERRO
|
|
|
12
13
|
const TRACK_ASYNC_CHILD_ERROR_MESSAGE = DEV ? 'child error' : TRACK_ASYNC_PUBLIC_ERROR_MESSAGE;
|
|
13
14
|
|
|
14
15
|
describe('hydration > trackAsync serialization', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
setTransport();
|
|
18
|
+
setServerTransport();
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('also accepts string devalue payloads with a registered transport', async () => {
|
|
23
|
+
setServerTransport(ServerComponents.transport);
|
|
24
|
+
setTransport(ClientComponents.transport);
|
|
25
|
+
const { body } = await render(ServerComponents.AsyncCustomType);
|
|
26
|
+
container.innerHTML = body;
|
|
27
|
+
for (const script of container.querySelectorAll('script[id^="__ripple_ta_"]')) {
|
|
28
|
+
const envelope = JSON.parse(script.textContent);
|
|
29
|
+
envelope.payload = JSON.stringify(envelope.payload);
|
|
30
|
+
script.textContent = JSON.stringify(envelope);
|
|
31
|
+
}
|
|
32
|
+
const unmount = hydrate(ClientComponents.AsyncCustomType, { target: container });
|
|
33
|
+
try {
|
|
34
|
+
expect(container.querySelector('.result')?.textContent).toBe('12 USD');
|
|
35
|
+
expect(container.querySelector('script[id^="__ripple_ta_"]')).toBeNull();
|
|
36
|
+
} finally {
|
|
37
|
+
unmount();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('revives a custom class during hydration and sends it through RPC in both directions', async () => {
|
|
42
|
+
const serverDecode = vi.fn(ServerComponents.transport.Money.decode);
|
|
43
|
+
const clientDecode = vi.fn(ClientComponents.transport.Money.decode);
|
|
44
|
+
setServerTransport({ Money: { ...ServerComponents.transport.Money, decode: serverDecode } });
|
|
45
|
+
setTransport({ Money: { ...ClientComponents.transport.Money, decode: clientDecode } });
|
|
46
|
+
const fetchMock = vi.fn(async (_url, init) => {
|
|
47
|
+
const body = await executeServerFunction(
|
|
48
|
+
ServerComponents._$_server_$_.doubleMoney,
|
|
49
|
+
init.body,
|
|
50
|
+
);
|
|
51
|
+
return new Response(body, { status: 200 });
|
|
52
|
+
});
|
|
53
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
54
|
+
|
|
55
|
+
const { unmount } = await hydrateComponent(
|
|
56
|
+
ServerComponents.AsyncCustomType,
|
|
57
|
+
ClientComponents.AsyncCustomType,
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
// Calling the instance method requires the client decoder, and hydration
|
|
61
|
+
// must adopt the server result without refetching it.
|
|
62
|
+
expect(container.querySelector('.result')?.textContent).toBe('12 USD');
|
|
63
|
+
expect(container.querySelector('.loading')).toBeNull();
|
|
64
|
+
expect(container.querySelector('script[id^="__ripple_ta_"]')).toBeNull();
|
|
65
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
66
|
+
expect(clientDecode).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(serverDecode).not.toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
container.querySelector('.increment').click();
|
|
70
|
+
flushSync();
|
|
71
|
+
await vi.waitFor(() => {
|
|
72
|
+
expect(container.querySelector('.result')?.textContent).toBe('14 USD');
|
|
73
|
+
});
|
|
74
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(fetchMock.mock.calls[0][0]).toMatch(/\/_\$_ripple_rpc_\$_\//);
|
|
76
|
+
expect(clientDecode).toHaveBeenCalledTimes(2);
|
|
77
|
+
expect(serverDecode).toHaveBeenCalledTimes(1);
|
|
78
|
+
} finally {
|
|
79
|
+
unmount();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
15
83
|
it('hydrates simple string value from serialized trackAsync', async () => {
|
|
16
84
|
await hydrateComponent(ServerComponents.AsyncSimpleValue, ClientComponents.AsyncSimpleValue);
|
|
17
85
|
|
|
@@ -77,6 +145,23 @@ describe('hydration > trackAsync serialization', () => {
|
|
|
77
145
|
expect(container.querySelector('.result')?.textContent).toBe('count-1');
|
|
78
146
|
});
|
|
79
147
|
|
|
148
|
+
it('reruns trackAsync when a dependency read through readOnly() changes after hydration', async () => {
|
|
149
|
+
await hydrateComponent(
|
|
150
|
+
ServerComponents.AsyncWithReadOnlyDependency,
|
|
151
|
+
ClientComponents.AsyncWithReadOnlyDependency,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(container.querySelector('.result')?.textContent).toBe('count-0');
|
|
155
|
+
|
|
156
|
+
/** @type {any} */ (container.querySelector('.increment'))?.click();
|
|
157
|
+
flushSync();
|
|
158
|
+
await Promise.resolve();
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
flushSync();
|
|
161
|
+
|
|
162
|
+
expect(container.querySelector('.result')?.textContent).toBe('count-1');
|
|
163
|
+
});
|
|
164
|
+
|
|
80
165
|
it('reruns trackAsync via module server RPC call when a dependency changes', async () => {
|
|
81
166
|
const originalFetch = globalThis.fetch;
|
|
82
167
|
const fetchMock = vi.fn(async (_url, init) => {
|