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
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
|
-
import { trackAsync } from 'ripple';
|
|
2
|
+
import { setTransport, track, trackAsync } from 'ripple';
|
|
3
|
+
import * as devalue from 'devalue';
|
|
3
4
|
|
|
4
5
|
const TRACK_ASYNC_PUBLIC_ERROR_MESSAGE = 'An error occurred during async rendering';
|
|
5
6
|
|
|
6
7
|
describe('trackAsync serialization (server)', () => {
|
|
8
|
+
afterEach(() => setTransport());
|
|
9
|
+
|
|
7
10
|
it('serializes resolved trackAsync data as a script tag in SSR output', async () => {
|
|
8
11
|
function App() @{
|
|
9
12
|
@try {
|
|
10
|
-
|
|
11
|
-
<p class="result">{data}</p>
|
|
13
|
+
const data = trackAsync(() => Promise.resolve('hello world'));
|
|
14
|
+
<p class="result">{data.value}</p>
|
|
12
15
|
} @pending {
|
|
13
16
|
<p class="loading">{'loading...'}</p>
|
|
14
17
|
}
|
|
@@ -24,8 +27,8 @@ describe('trackAsync serialization (server)', () => {
|
|
|
24
27
|
it('serializes numeric trackAsync data correctly', async () => {
|
|
25
28
|
function App() @{
|
|
26
29
|
@try {
|
|
27
|
-
|
|
28
|
-
<span class="count">{count}</span>
|
|
30
|
+
const count = trackAsync(() => Promise.resolve(42));
|
|
31
|
+
<span class="count">{count.value}</span>
|
|
29
32
|
} @pending {
|
|
30
33
|
<span>{'...'}</span>
|
|
31
34
|
}
|
|
@@ -39,8 +42,8 @@ describe('trackAsync serialization (server)', () => {
|
|
|
39
42
|
it('serializes object trackAsync data correctly', async () => {
|
|
40
43
|
function App() @{
|
|
41
44
|
@try {
|
|
42
|
-
|
|
43
|
-
<p>{data.name}</p>
|
|
45
|
+
const data = trackAsync(() => Promise.resolve({ name: 'Alice', age: 30 }));
|
|
46
|
+
<p>{data.value.name}</p>
|
|
44
47
|
} @pending {
|
|
45
48
|
<p>{'...'}</p>
|
|
46
49
|
}
|
|
@@ -54,9 +57,9 @@ describe('trackAsync serialization (server)', () => {
|
|
|
54
57
|
it('serializes array trackAsync data correctly', async () => {
|
|
55
58
|
function App() @{
|
|
56
59
|
@try {
|
|
57
|
-
|
|
60
|
+
const items = trackAsync(() => Promise.resolve(['a', 'b', 'c']));
|
|
58
61
|
<ul>
|
|
59
|
-
@for (let item of items) {
|
|
62
|
+
@for (let item of items.value) {
|
|
60
63
|
<li>{item}</li>
|
|
61
64
|
}
|
|
62
65
|
</ul>
|
|
@@ -73,8 +76,8 @@ describe('trackAsync serialization (server)', () => {
|
|
|
73
76
|
it('serializes rejected trackAsync as error envelope', async () => {
|
|
74
77
|
function App() @{
|
|
75
78
|
@try {
|
|
76
|
-
|
|
77
|
-
<p class="result">{data}</p>
|
|
79
|
+
const data = trackAsync(() => Promise.reject(new Error('fetch failed')));
|
|
80
|
+
<p class="result">{data.value}</p>
|
|
78
81
|
} @pending {
|
|
79
82
|
<p class="loading">{'loading...'}</p>
|
|
80
83
|
} @catch (e) {
|
|
@@ -93,8 +96,8 @@ describe('trackAsync serialization (server)', () => {
|
|
|
93
96
|
it('serializes error when child component error bubbles to parent catch', async () => {
|
|
94
97
|
function Child() @{
|
|
95
98
|
@try {
|
|
96
|
-
|
|
97
|
-
<p class="result">{data}</p>
|
|
99
|
+
const data = trackAsync(() => Promise.reject(new Error('child error')));
|
|
100
|
+
<p class="result">{data.value}</p>
|
|
98
101
|
} @pending {
|
|
99
102
|
<p class="pending">{'loading...'}</p>
|
|
100
103
|
}
|
|
@@ -119,10 +122,10 @@ describe('trackAsync serialization (server)', () => {
|
|
|
119
122
|
it('serializes sync trackAsync throw using original cause message', async () => {
|
|
120
123
|
function App() @{
|
|
121
124
|
@try {
|
|
122
|
-
|
|
125
|
+
const data = trackAsync(() => {
|
|
123
126
|
throw new Error('sync failure');
|
|
124
127
|
});
|
|
125
|
-
<p class="result">{data}</p>
|
|
128
|
+
<p class="result">{data.value}</p>
|
|
126
129
|
} @pending {
|
|
127
130
|
<p class="loading">{'loading...'}</p>
|
|
128
131
|
} @catch (e) {
|
|
@@ -139,13 +142,193 @@ describe('trackAsync serialization (server)', () => {
|
|
|
139
142
|
expect(body).not.toContain('Error thrown during trackAsync execution');
|
|
140
143
|
});
|
|
141
144
|
|
|
145
|
+
describe('hydration envelope dependencies', () => {
|
|
146
|
+
/** @param {'direct' | 'view'} read */
|
|
147
|
+
async function deps_for(read: 'direct' | 'view') {
|
|
148
|
+
function App() @{
|
|
149
|
+
const count = track(3);
|
|
150
|
+
const view = count.readOnly();
|
|
151
|
+
@try {
|
|
152
|
+
const data = trackAsync(
|
|
153
|
+
() => Promise.resolve(`count-${read === 'view' ? view.value : count.value}`),
|
|
154
|
+
);
|
|
155
|
+
<p>{data.value}</p>
|
|
156
|
+
} @pending {
|
|
157
|
+
<p>{'...'}</p>
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { body } = await render(App);
|
|
162
|
+
const match = body.match(
|
|
163
|
+
/<script id="__ripple_ta_[a-f0-9]+" type="application\/json">(.*?)<\/script>/s,
|
|
164
|
+
);
|
|
165
|
+
expect(match).not.toBeNull();
|
|
166
|
+
return JSON.parse(/** @type {RegExpMatchArray} */ (match)[1]).deps;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
it('records the tracked a trackAsync read directly', async () => {
|
|
170
|
+
expect(await deps_for('direct')).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('records the tracked behind a readOnly() view a trackAsync read', async () => {
|
|
174
|
+
// The view itself has no hash; the client must still re-subscribe to
|
|
175
|
+
// the source after hydration, so its hash is what gets recorded.
|
|
176
|
+
expect(await deps_for('view')).toEqual(await deps_for('direct'));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('hydration envelope encoding', () => {
|
|
181
|
+
async function envelope_for(value: unknown) {
|
|
182
|
+
function App() @{
|
|
183
|
+
@try {
|
|
184
|
+
const data = trackAsync(() => Promise.resolve(value));
|
|
185
|
+
<p>{String(data.value !== undefined)}</p>
|
|
186
|
+
} @pending {
|
|
187
|
+
<p>{'...'}</p>
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { body } = await render(App);
|
|
192
|
+
const match = body.match(
|
|
193
|
+
/<script id="__ripple_ta_[a-f0-9]+" type="application\/json">(.*?)<\/script>/s,
|
|
194
|
+
);
|
|
195
|
+
expect(match).not.toBeNull();
|
|
196
|
+
const text = /** @type {RegExpMatchArray} */ (match)[1];
|
|
197
|
+
expect(text).not.toContain('<');
|
|
198
|
+
return JSON.parse(text);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it('ships plain data trees as raw JSON', async () => {
|
|
202
|
+
const value = {
|
|
203
|
+
title: 'Card 0 — "alpha" \\ <b>&</b>',
|
|
204
|
+
count: 3,
|
|
205
|
+
ratio: 0.5,
|
|
206
|
+
ok: true,
|
|
207
|
+
none: null,
|
|
208
|
+
items: [{ label: 'a\nb', value: 'tab\there' }, ['x', 1, false], '😀 '],
|
|
209
|
+
'odd key': { nested: [] },
|
|
210
|
+
};
|
|
211
|
+
const envelope = await envelope_for(value);
|
|
212
|
+
expect(envelope.ok).toBe(true);
|
|
213
|
+
expect(envelope.payload).toBeUndefined();
|
|
214
|
+
expect(envelope.value).toEqual(value);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('ships primitives as raw JSON', async () => {
|
|
218
|
+
expect((await envelope_for('hello')).value).toBe('hello');
|
|
219
|
+
expect((await envelope_for('lone � surrogate')).value).toBe('lone � surrogate');
|
|
220
|
+
expect((await envelope_for(42)).value).toBe(42);
|
|
221
|
+
expect((await envelope_for(false)).value).toBe(false);
|
|
222
|
+
expect((await envelope_for(null)).value).toBe(null);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('keeps the devalue encoding for values JSON cannot represent', async () => {
|
|
226
|
+
const shared = { n: 1 };
|
|
227
|
+
const cyclic: { self?: unknown } = {};
|
|
228
|
+
cyclic.self = cyclic;
|
|
229
|
+
for (const value of [
|
|
230
|
+
undefined,
|
|
231
|
+
NaN,
|
|
232
|
+
Infinity,
|
|
233
|
+
-0,
|
|
234
|
+
10n,
|
|
235
|
+
new Date(0),
|
|
236
|
+
new Map([[1, 2]]),
|
|
237
|
+
new Set([1]),
|
|
238
|
+
/re/,
|
|
239
|
+
Object.create(null),
|
|
240
|
+
{ a: undefined },
|
|
241
|
+
{ a: shared, b: shared },
|
|
242
|
+
[shared, shared],
|
|
243
|
+
cyclic,
|
|
244
|
+
// eslint-disable-next-line no-sparse-arrays
|
|
245
|
+
[1, , 3],
|
|
246
|
+
]) {
|
|
247
|
+
const envelope = await envelope_for(value);
|
|
248
|
+
expect(envelope.value).toBeUndefined();
|
|
249
|
+
expect(typeof envelope.payload).toBe('string');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('keeps the raw JSON path for an empty transport', async () => {
|
|
254
|
+
setTransport({});
|
|
255
|
+
const value = { title: 'Card', items: [1, true, null] };
|
|
256
|
+
expect(await envelope_for(value)).toEqual({ ok: true, value });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('serializes custom classes and preserves shared references', async () => {
|
|
260
|
+
class Money {
|
|
261
|
+
amount: number;
|
|
262
|
+
currency: string;
|
|
263
|
+
constructor(amount: number, currency: string) {
|
|
264
|
+
this.amount = amount;
|
|
265
|
+
this.currency = currency;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const decode = ([amount, currency]: [number, string]) => new Money(amount, currency);
|
|
269
|
+
setTransport({
|
|
270
|
+
Money: {
|
|
271
|
+
encode: (value) => value instanceof Money && [value.amount, value.currency],
|
|
272
|
+
decode,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const money = new Money(12, '</script><script>injected</script>');
|
|
276
|
+
const envelope = await envelope_for({ first: money, second: money, date: new Date(0) });
|
|
277
|
+
expect(envelope.value).toBeUndefined();
|
|
278
|
+
const restored = devalue.unflatten(envelope.payload, { Money: decode });
|
|
279
|
+
expect(restored.first).toBeInstanceOf(Money);
|
|
280
|
+
expect(restored.first).toEqual(money);
|
|
281
|
+
expect(restored.second).toBe(restored.first);
|
|
282
|
+
expect(restored.date).toEqual(new Date(0));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('lets reducers claim plain objects without first walking the JSON path', async () => {
|
|
286
|
+
const read_amount = vi.fn(() => 12);
|
|
287
|
+
const encode = vi.fn((value: any) => value?.__type === 'Money' && [value.amount]);
|
|
288
|
+
const decode = ([amount]: [number]) => ({ amount, revived: true });
|
|
289
|
+
setTransport({ Money: { encode, decode } });
|
|
290
|
+
const value = {
|
|
291
|
+
nested: {
|
|
292
|
+
__type: 'Money',
|
|
293
|
+
get amount() {
|
|
294
|
+
return read_amount();
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
const envelope = await envelope_for(value);
|
|
299
|
+
expect(envelope.value).toBeUndefined();
|
|
300
|
+
expect(devalue.unflatten(envelope.payload, { Money: decode })).toEqual({
|
|
301
|
+
nested: { amount: 12, revived: true },
|
|
302
|
+
});
|
|
303
|
+
expect(encode.mock.calls.filter(([node]) => node === value.nested)).toHaveLength(1);
|
|
304
|
+
expect(read_amount).toHaveBeenCalledTimes(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it(
|
|
308
|
+
'uses devalue for plain data under an unused transport and restores JSON after reset',
|
|
309
|
+
async () => {
|
|
310
|
+
setTransport({ Unused: { encode: () => undefined, decode: (value) => value } });
|
|
311
|
+
const value = { title: 'Card', items: [1, true, null] };
|
|
312
|
+
const envelope = await envelope_for(value);
|
|
313
|
+
expect(envelope.value).toBeUndefined();
|
|
314
|
+
expect(devalue.unflatten(envelope.payload)).toEqual(value);
|
|
315
|
+
for (const special of [undefined, NaN, Infinity, -Infinity, -0, 10n, new Date(0)]) {
|
|
316
|
+
const encoded = await envelope_for(special);
|
|
317
|
+
expect(devalue.unflatten(encoded.payload)).toEqual(special);
|
|
318
|
+
}
|
|
319
|
+
setTransport();
|
|
320
|
+
expect(await envelope_for(value)).toEqual({ ok: true, value });
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
142
325
|
it('escapes serialized error payload to keep script tag safe', async () => {
|
|
143
326
|
function App() @{
|
|
144
327
|
@try {
|
|
145
|
-
|
|
328
|
+
const data = trackAsync(
|
|
146
329
|
() => Promise.reject(new Error('</script><div data-injected="1">boom</div>')),
|
|
147
330
|
);
|
|
148
|
-
<p class="result">{data}</p>
|
|
331
|
+
<p class="result">{data.value}</p>
|
|
149
332
|
} @pending {
|
|
150
333
|
<p class="loading">{'loading...'}</p>
|
|
151
334
|
} @catch (e) {
|
|
@@ -166,11 +349,11 @@ describe('trackAsync serialization (server)', () => {
|
|
|
166
349
|
it('generates unique hashes for multiple trackAsync calls', async () => {
|
|
167
350
|
function App() @{
|
|
168
351
|
@try {
|
|
169
|
-
|
|
170
|
-
|
|
352
|
+
const a = trackAsync(() => Promise.resolve('first'));
|
|
353
|
+
const b = trackAsync(() => Promise.resolve('second'));
|
|
171
354
|
<>
|
|
172
|
-
<p>{a}</p>
|
|
173
|
-
<p>{b}</p>
|
|
355
|
+
<p>{a.value}</p>
|
|
356
|
+
<p>{b.value}</p>
|
|
174
357
|
</>
|
|
175
358
|
} @pending {
|
|
176
359
|
<p>{'...'}</p>
|
|
@@ -3,8 +3,8 @@ import { trackAsync } from 'ripple';
|
|
|
3
3
|
describe('try block with catch and pending (server)', () => {
|
|
4
4
|
it('renders resolved content with an empty pending fallback', async () => {
|
|
5
5
|
function DataChild() @{
|
|
6
|
-
|
|
7
|
-
<p>{data}</p>
|
|
6
|
+
const data = trackAsync(() => Promise.resolve('resolved value'));
|
|
7
|
+
<p>{data.value}</p>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function App() @{
|
|
@@ -47,9 +47,9 @@ describe('try block with catch and pending (server)', () => {
|
|
|
47
47
|
|
|
48
48
|
it('catch block works when component throws after await with pending block', async () => {
|
|
49
49
|
function ThrowingAfterAwait() @{
|
|
50
|
-
|
|
50
|
+
const data = trackAsync(() => Promise.resolve('hello'));
|
|
51
51
|
throw new Error('error after await');
|
|
52
|
-
<p>{data}</p>
|
|
52
|
+
<p>{data.value}</p>
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function App() @{
|
|
@@ -70,8 +70,8 @@ describe('try block with catch and pending (server)', () => {
|
|
|
70
70
|
it('catch block works with try/catch/pending when async body rejects', async () => {
|
|
71
71
|
function App() @{
|
|
72
72
|
@try {
|
|
73
|
-
|
|
74
|
-
<p>{data}</p>
|
|
73
|
+
const data = trackAsync(() => Promise.reject(new Error('rejected')));
|
|
74
|
+
<p>{data.value}</p>
|
|
75
75
|
} @pending {
|
|
76
76
|
<p>{'loading...'}</p>
|
|
77
77
|
} @catch (err) {
|
|
@@ -88,8 +88,8 @@ describe('try block with catch and pending (server)', () => {
|
|
|
88
88
|
function App() @{
|
|
89
89
|
@try {
|
|
90
90
|
@try {
|
|
91
|
-
|
|
92
|
-
<p>{data}</p>
|
|
91
|
+
const data = trackAsync(() => Promise.resolve('resolved'));
|
|
92
|
+
<p>{data.value}</p>
|
|
93
93
|
} @pending {
|
|
94
94
|
<p>{'inner loading...'}</p>
|
|
95
95
|
}
|
|
@@ -116,8 +116,8 @@ describe('trackAsync directly in component body (server)', () => {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function DataChild() @{
|
|
119
|
-
|
|
120
|
-
<p>{data}</p>
|
|
119
|
+
const data = trackAsync(() => Promise.resolve('from child'));
|
|
120
|
+
<p>{data.value}</p>
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const { body } = await render(App);
|
|
@@ -128,12 +128,12 @@ describe('trackAsync directly in component body (server)', () => {
|
|
|
128
128
|
it('resolves multiple trackAsync values in the same component', async () => {
|
|
129
129
|
function App() @{
|
|
130
130
|
@try {
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
const a = trackAsync(() => Promise.resolve('hello'));
|
|
132
|
+
const b = trackAsync(() => Promise.resolve('world'));
|
|
133
133
|
<p>
|
|
134
|
-
{a}
|
|
134
|
+
{a.value}
|
|
135
135
|
{' '}
|
|
136
|
-
{b}
|
|
136
|
+
{b.value}
|
|
137
137
|
</p>
|
|
138
138
|
} @catch (err) {
|
|
139
139
|
<p>{'error'}</p>
|
|
@@ -147,8 +147,8 @@ describe('trackAsync directly in component body (server)', () => {
|
|
|
147
147
|
|
|
148
148
|
it('renders catch when trackAsync rejects in a child component without its own try', async () => {
|
|
149
149
|
function RejectChild() @{
|
|
150
|
-
|
|
151
|
-
<p>{data}</p>
|
|
150
|
+
const data = trackAsync(() => Promise.reject(new Error('child rejected')));
|
|
151
|
+
<p>{data.value}</p>
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function App() @{
|
|
@@ -166,12 +166,12 @@ describe('trackAsync directly in component body (server)', () => {
|
|
|
166
166
|
it('chained trackAsync resolves both values', async () => {
|
|
167
167
|
function App() @{
|
|
168
168
|
@try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const n = name;
|
|
169
|
+
const name = trackAsync(() => Promise.resolve('ripple'));
|
|
170
|
+
const upper = trackAsync(() => {
|
|
171
|
+
const n = name.value;
|
|
172
172
|
return Promise.resolve(n.toUpperCase());
|
|
173
173
|
});
|
|
174
|
-
<p>{upper}</p>
|
|
174
|
+
<p>{upper.value}</p>
|
|
175
175
|
} @catch (err) {
|
|
176
176
|
<p>{'error'}</p>
|
|
177
177
|
}
|
|
@@ -185,12 +185,12 @@ describe('trackAsync directly in component body (server)', () => {
|
|
|
185
185
|
it('chained trackAsync catches when first rejects', async () => {
|
|
186
186
|
function App() @{
|
|
187
187
|
@try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const n = name;
|
|
188
|
+
const name = trackAsync(() => Promise.reject<string>(new Error('first failed')));
|
|
189
|
+
const upper = trackAsync(() => {
|
|
190
|
+
const n = name.value;
|
|
191
191
|
return Promise.resolve(n.toUpperCase());
|
|
192
192
|
});
|
|
193
|
-
<p>{upper}</p>
|
|
193
|
+
<p>{upper.value}</p>
|
|
194
194
|
} @catch (err: Error) {
|
|
195
195
|
<p>{err.message}</p>
|
|
196
196
|
}
|
|
@@ -203,12 +203,12 @@ describe('trackAsync directly in component body (server)', () => {
|
|
|
203
203
|
it('chained trackAsync catches when second rejects', async () => {
|
|
204
204
|
function App() @{
|
|
205
205
|
@try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const n = name;
|
|
206
|
+
const name = trackAsync(() => Promise.resolve('ripple'));
|
|
207
|
+
const upper = trackAsync(() => {
|
|
208
|
+
const n = name.value;
|
|
209
209
|
return Promise.reject(new Error('second failed'));
|
|
210
210
|
});
|
|
211
|
-
<p>{upper}</p>
|
|
211
|
+
<p>{upper.value}</p>
|
|
212
212
|
} @catch (err: Error) {
|
|
213
213
|
<p>{err.message}</p>
|
|
214
214
|
}
|
|
@@ -248,8 +248,8 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
248
248
|
|
|
249
249
|
it('inner try/catch catches async rejection from its own child', async () => {
|
|
250
250
|
function AsyncChild() @{
|
|
251
|
-
|
|
252
|
-
<p>{data}</p>
|
|
251
|
+
const data = trackAsync(() => Promise.reject(new Error('async inner error')));
|
|
252
|
+
<p>{data.value}</p>
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
function Inner() @{
|
|
@@ -301,8 +301,8 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
301
301
|
|
|
302
302
|
it('async rejection propagates to outer catch when inner try has no catch', async () => {
|
|
303
303
|
function AsyncChild() @{
|
|
304
|
-
|
|
305
|
-
<p>{data}</p>
|
|
304
|
+
const data = trackAsync(() => Promise.reject(new Error('async propagated')));
|
|
305
|
+
<p>{data.value}</p>
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
function Inner() @{
|
|
@@ -364,13 +364,13 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
364
364
|
|
|
365
365
|
it('sibling components: one fails, the other does not affect catch', async () => {
|
|
366
366
|
function GoodChild() @{
|
|
367
|
-
|
|
368
|
-
<p>{data}</p>
|
|
367
|
+
const data = trackAsync(() => Promise.resolve('good'));
|
|
368
|
+
<p>{data.value}</p>
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
function BadChild() @{
|
|
372
|
-
|
|
373
|
-
<p>{data}</p>
|
|
372
|
+
const data = trackAsync(() => Promise.reject(new Error('bad child')));
|
|
373
|
+
<p>{data.value}</p>
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
function App() @{
|
|
@@ -391,13 +391,13 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
391
391
|
|
|
392
392
|
it('independent try/catch boundaries each handle their own errors', async () => {
|
|
393
393
|
function FailChild() @{
|
|
394
|
-
|
|
395
|
-
<p>{data}</p>
|
|
394
|
+
const data = trackAsync(() => Promise.reject(new Error('fail')));
|
|
395
|
+
<p>{data.value}</p>
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
function SuccessChild() @{
|
|
399
|
-
|
|
400
|
-
<p>{data}</p>
|
|
399
|
+
const data = trackAsync(() => Promise.resolve('success'));
|
|
400
|
+
<p>{data.value}</p>
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
function App() @{
|
|
@@ -423,8 +423,8 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
423
423
|
|
|
424
424
|
it('inner catch handles rejection, outer renders normally', async () => {
|
|
425
425
|
function AsyncChild() @{
|
|
426
|
-
|
|
427
|
-
<p>{data}</p>
|
|
426
|
+
const data = trackAsync(() => Promise.reject(new Error('handled inside')));
|
|
427
|
+
<p>{data.value}</p>
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
function Inner() @{
|
|
@@ -449,9 +449,9 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
449
449
|
|
|
450
450
|
it('sync error in child after trackAsync routes to catch boundary', async () => {
|
|
451
451
|
function BrokenChild() @{
|
|
452
|
-
|
|
452
|
+
const data = trackAsync(() => Promise.resolve('loaded'));
|
|
453
453
|
throw new Error('sync after async');
|
|
454
|
-
<p>{data}</p>
|
|
454
|
+
<p>{data.value}</p>
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
function Inner() @{
|
|
@@ -472,8 +472,8 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
472
472
|
|
|
473
473
|
it('outer try with pending, inner try with catch: rejection goes to inner catch', async () => {
|
|
474
474
|
function AsyncChild() @{
|
|
475
|
-
|
|
476
|
-
<p>{data}</p>
|
|
475
|
+
const data = trackAsync(() => Promise.reject(new Error('inner rejection')));
|
|
476
|
+
<p>{data.value}</p>
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
function Inner() @{
|
|
@@ -499,8 +499,8 @@ describe('nested child components with try/catch boundaries (server)', () => {
|
|
|
499
499
|
|
|
500
500
|
it('deeply nested: async resolves through multiple component layers', async () => {
|
|
501
501
|
function DataFetcher() @{
|
|
502
|
-
|
|
503
|
-
<span>{data}</span>
|
|
502
|
+
const data = trackAsync(() => Promise.resolve('deep data'));
|
|
503
|
+
<span>{data.value}</span>
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
function Level2() @{
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { setTransport, type Transport, type Transporter } from 'ripple';
|
|
2
|
+
import { setTransport as setServerTransport } from 'ripple/server';
|
|
3
|
+
import { setTransport as registerTransport } from 'ripple/transport';
|
|
4
|
+
import { defineConfig } from '@ripple-ts/vite-plugin';
|
|
5
|
+
|
|
6
|
+
class Money {
|
|
7
|
+
constructor(
|
|
8
|
+
public amount: number,
|
|
9
|
+
public currency: string,
|
|
10
|
+
) {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const money: Transporter<Money, [number, string]> = {
|
|
14
|
+
encode: (value) => value instanceof Money && [value.amount, value.currency],
|
|
15
|
+
decode: ([amount, currency]) => new Money(amount, currency),
|
|
16
|
+
};
|
|
17
|
+
const transport: Transport = { Money: money };
|
|
18
|
+
defineConfig({ transport });
|
|
19
|
+
defineConfig({
|
|
20
|
+
transport: {
|
|
21
|
+
Money: {
|
|
22
|
+
encode: (value) => value instanceof Money && [value.amount, value.currency],
|
|
23
|
+
decode: ([amount, currency]) => new Money(amount, currency),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
setTransport(transport);
|
|
28
|
+
setServerTransport(transport);
|
|
29
|
+
registerTransport(transport);
|
|
30
|
+
setTransport();
|
|
31
|
+
setServerTransport({});
|
|
32
|
+
|
|
33
|
+
// @ts-expect-error Every handler needs both directions.
|
|
34
|
+
defineConfig({ transport: { Money: { encode: () => false } } });
|
|
35
|
+
const invalid: Transporter<Money, [number, string]> = {
|
|
36
|
+
...money,
|
|
37
|
+
// @ts-expect-error A decoder must accept the encoded representation.
|
|
38
|
+
decode: (data: number) => new Money(data, 'USD'),
|
|
39
|
+
};
|
|
40
|
+
void invalid;
|
|
@@ -18,6 +18,7 @@ describe('vite-plugin-ripple config resolution', () => {
|
|
|
18
18
|
expect(config.platform.env).toEqual({});
|
|
19
19
|
expect(config.server.trustProxy).toBe(false);
|
|
20
20
|
expect(config.rootBoundary).toEqual({});
|
|
21
|
+
expect(config.transport).toEqual({});
|
|
21
22
|
expect(config.build.outDir).toBe('dist');
|
|
22
23
|
});
|
|
23
24
|
|
|
@@ -77,4 +78,26 @@ describe('vite-plugin-ripple config resolution', () => {
|
|
|
77
78
|
}),
|
|
78
79
|
).toThrow('router.routes must be an array');
|
|
79
80
|
});
|
|
81
|
+
|
|
82
|
+
it('preserves transport functions when resolving a config again', () => {
|
|
83
|
+
const transport = {
|
|
84
|
+
Money: { encode: () => false, decode: (data) => data },
|
|
85
|
+
};
|
|
86
|
+
const config = resolveRippleConfig({ transport });
|
|
87
|
+
expect(config.transport).toBe(transport);
|
|
88
|
+
expect(resolveRippleConfig(config).transport).toBe(transport);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it.each([null, false, [], 'Money', () => {}])('rejects invalid transport %s', (transport) => {
|
|
92
|
+
expect(() => resolveRippleConfig({ transport })).toThrow('transport must be an object');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it.each([null, [], false, {}, { encode() {} }, { decode() {} }, { encode: true, decode() {} }])(
|
|
96
|
+
'rejects invalid transport handler %s',
|
|
97
|
+
(Money) => {
|
|
98
|
+
expect(() => resolveRippleConfig({ transport: { Money } })).toThrow(
|
|
99
|
+
'transport.Money must be an object with encode and decode functions',
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
80
103
|
});
|