ripple 0.3.64 → 0.3.66
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 +46 -0
- package/package.json +3 -3
- package/src/jsx-runtime.d.ts +17 -3
- package/src/runtime/index-client.js +47 -8
- package/src/runtime/index-server.js +0 -2
- package/src/runtime/internal/client/index.js +5 -0
- package/src/runtime/internal/client/runtime.js +111 -11
- package/src/runtime/internal/client/types.d.ts +5 -0
- package/src/runtime/internal/server/blocks.js +1 -0
- package/src/runtime/internal/server/index.js +175 -20
- package/src/utils/errors.js +13 -0
- package/tests/client/async-suspend.test.tsrx +2 -2
- package/tests/client/basic/basic.get-set.test.tsrx +26 -26
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +1 -1
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +1 -1
- package/tests/client/compiler/compiler.assignments.test.tsrx +80 -0
- package/tests/client/compiler/compiler.tracked-access.test.tsrx +52 -8
- package/tests/client/compiler/compiler.typescript.test.tsrx +23 -0
- package/tests/client/lazy-array.test.tsrx +34 -0
- package/tests/client/lazy-destructuring.test.tsrx +79 -8
- package/tests/client/tracked-index-access.test.tsrx +113 -0
- package/tests/client/tsx.test.tsrx +66 -21
- package/tests/hydration/compiled/client/basic.js +2 -2
- package/tests/hydration/compiled/client/events.js +9 -9
- package/tests/hydration/compiled/client/for.js +50 -54
- package/tests/hydration/compiled/client/head.js +9 -9
- package/tests/hydration/compiled/client/hmr.js +1 -1
- package/tests/hydration/compiled/client/html.js +2 -2
- package/tests/hydration/compiled/client/if-children.js +14 -14
- package/tests/hydration/compiled/client/if.js +10 -10
- package/tests/hydration/compiled/client/mixed-control-flow.js +7 -7
- package/tests/hydration/compiled/client/portal.js +2 -2
- package/tests/hydration/compiled/client/reactivity.js +7 -7
- package/tests/hydration/compiled/client/return.js +37 -37
- package/tests/hydration/compiled/client/switch.js +8 -8
- package/tests/hydration/compiled/client/track-async-serialization.js +12 -12
- package/tests/hydration/compiled/client/try.js +116 -33
- package/tests/hydration/compiled/server/basic.js +2 -2
- package/tests/hydration/compiled/server/events.js +8 -8
- package/tests/hydration/compiled/server/for.js +21 -21
- package/tests/hydration/compiled/server/head.js +10 -10
- package/tests/hydration/compiled/server/hmr.js +1 -1
- package/tests/hydration/compiled/server/html.js +1 -1
- package/tests/hydration/compiled/server/if-children.js +9 -9
- package/tests/hydration/compiled/server/if.js +6 -6
- package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
- package/tests/hydration/compiled/server/portal.js +1 -1
- package/tests/hydration/compiled/server/reactivity.js +7 -7
- package/tests/hydration/compiled/server/return.js +14 -14
- package/tests/hydration/compiled/server/switch.js +4 -4
- package/tests/hydration/compiled/server/track-async-serialization.js +12 -12
- package/tests/hydration/compiled/server/try.js +116 -4
- package/tests/hydration/components/basic.tsrx +3 -1
- package/tests/hydration/components/try.tsrx +26 -0
- package/tests/hydration/try.test.js +100 -1
- package/tests/server/await.test.tsrx +1 -1
- package/tests/server/basic.test.tsrx +3 -1
- package/tests/server/compiler.test.tsrx +109 -0
- package/tests/server/lazy-destructuring.test.tsrx +62 -0
- package/tests/server/tracked-index-access.test.tsrx +76 -0
- package/tests/setup-hydration.js +31 -0
- package/types/index.d.ts +11 -9
- package/types/server.d.ts +2 -1
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { trackAsync } from 'ripple';
|
|
2
2
|
|
|
3
|
+
export component RootPending() {
|
|
4
|
+
<p class="root-pending">{'root loading...'}</p>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export component RootCatch({ error, reset }: { error: Error; reset: () => void }) {
|
|
8
|
+
<section class="root-catch">
|
|
9
|
+
<p class="root-error">{error.message}</p>
|
|
10
|
+
<button class="root-reset" onClick={reset}>{'retry'}</button>
|
|
11
|
+
</section>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export component RootThrows() {
|
|
15
|
+
throw new Error('root exploded');
|
|
16
|
+
<p>{'should not render'}</p>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export component RootAsyncDirect() {
|
|
20
|
+
let &[value] = trackAsync(() => Promise.resolve('root async value'));
|
|
21
|
+
<p class="root-async-value">{value}</p>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export component RootAsyncRejects() {
|
|
25
|
+
let &[value] = trackAsync(() => Promise.reject(new Error('root async failed')));
|
|
26
|
+
<p class="root-async-value">{value}</p>
|
|
27
|
+
}
|
|
28
|
+
|
|
3
29
|
export component AsyncListInTryPending() {
|
|
4
30
|
try {
|
|
5
31
|
<AsyncList />
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DEV } from 'esm-env';
|
|
2
3
|
import { flushSync } from 'ripple';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
hydrateComponent,
|
|
6
|
+
hydrateComponentWithRootBoundary,
|
|
7
|
+
container,
|
|
8
|
+
} from '../setup-hydration.js';
|
|
4
9
|
|
|
5
10
|
import * as ServerComponents from './compiled/server/try.js';
|
|
6
11
|
import * as ClientComponents from './compiled/client/try.js';
|
|
7
12
|
|
|
13
|
+
const TRACK_ASYNC_PUBLIC_ERROR_MESSAGE = 'An error occurred during async rendering';
|
|
14
|
+
const ROOT_TRACK_ASYNC_ERROR_MESSAGE = DEV ? 'root async failed' : TRACK_ASYNC_PUBLIC_ERROR_MESSAGE;
|
|
15
|
+
|
|
8
16
|
describe('hydration > try blocks (async)', () => {
|
|
9
17
|
it('hydrates async try with resolved server content', async () => {
|
|
10
18
|
await hydrateComponent(
|
|
@@ -30,6 +38,97 @@ describe('hydration > try blocks (async)', () => {
|
|
|
30
38
|
});
|
|
31
39
|
});
|
|
32
40
|
|
|
41
|
+
describe('hydration > root try boundary', () => {
|
|
42
|
+
it('hydrates root catch content rendered by the server root boundary', async () => {
|
|
43
|
+
/** @type {Element | null | undefined} */
|
|
44
|
+
let ssrCatch;
|
|
45
|
+
/** @type {Element | null | undefined} */
|
|
46
|
+
let ssrError;
|
|
47
|
+
/** @type {Element | null | undefined} */
|
|
48
|
+
let ssrReset;
|
|
49
|
+
|
|
50
|
+
await hydrateComponentWithRootBoundary(
|
|
51
|
+
ServerComponents.RootThrows,
|
|
52
|
+
ClientComponents.RootThrows,
|
|
53
|
+
{ catch: ServerComponents.RootCatch, pending: ServerComponents.RootPending },
|
|
54
|
+
{ catch: ClientComponents.RootCatch, pending: ClientComponents.RootPending },
|
|
55
|
+
({ container, body }) => {
|
|
56
|
+
expect(body).toContain('<!--[-->');
|
|
57
|
+
expect(body).toContain('<!--]-->');
|
|
58
|
+
|
|
59
|
+
ssrCatch = container.querySelector('.root-catch');
|
|
60
|
+
ssrError = container.querySelector('.root-error');
|
|
61
|
+
ssrReset = container.querySelector('.root-reset');
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(container.querySelector('.root-catch')).toBe(ssrCatch);
|
|
66
|
+
expect(container.querySelector('.root-error')).toBe(ssrError);
|
|
67
|
+
expect(container.querySelector('.root-reset')).toBe(ssrReset);
|
|
68
|
+
expect(ssrError?.textContent).toBe('root exploded');
|
|
69
|
+
expect(container.querySelector('.root-pending')).toBeNull();
|
|
70
|
+
expect(container.textContent).not.toContain('should not render');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('hydrates resolved root content from a server trackAsync result', async () => {
|
|
74
|
+
/** @type {Element | null | undefined} */
|
|
75
|
+
let ssrValue;
|
|
76
|
+
|
|
77
|
+
await hydrateComponentWithRootBoundary(
|
|
78
|
+
ServerComponents.RootAsyncDirect,
|
|
79
|
+
ClientComponents.RootAsyncDirect,
|
|
80
|
+
{ catch: ServerComponents.RootCatch, pending: ServerComponents.RootPending },
|
|
81
|
+
{ catch: ClientComponents.RootCatch, pending: ClientComponents.RootPending },
|
|
82
|
+
({ container, body }) => {
|
|
83
|
+
expect(body).toContain('<!--[-->');
|
|
84
|
+
expect(body).toContain('<!--]-->');
|
|
85
|
+
expect(body).toContain('__tsrx_ta_');
|
|
86
|
+
|
|
87
|
+
ssrValue = container.querySelector('.root-async-value');
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(container.querySelector('.root-async-value')).toBe(ssrValue);
|
|
92
|
+
expect(ssrValue?.textContent).toBe('root async value');
|
|
93
|
+
expect(container.querySelector('.root-pending')).toBeNull();
|
|
94
|
+
expect(container.querySelector('.root-catch')).toBeNull();
|
|
95
|
+
expect(container.querySelector('script[id^="__tsrx_ta_"]')).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('hydrates root catch content from a rejected server trackAsync result', async () => {
|
|
99
|
+
/** @type {Element | null | undefined} */
|
|
100
|
+
let ssrCatch;
|
|
101
|
+
/** @type {Element | null | undefined} */
|
|
102
|
+
let ssrError;
|
|
103
|
+
/** @type {Element | null | undefined} */
|
|
104
|
+
let ssrReset;
|
|
105
|
+
|
|
106
|
+
await hydrateComponentWithRootBoundary(
|
|
107
|
+
ServerComponents.RootAsyncRejects,
|
|
108
|
+
ClientComponents.RootAsyncRejects,
|
|
109
|
+
{ catch: ServerComponents.RootCatch, pending: ServerComponents.RootPending },
|
|
110
|
+
{ catch: ClientComponents.RootCatch, pending: ClientComponents.RootPending },
|
|
111
|
+
({ container, body }) => {
|
|
112
|
+
expect(body).toContain('<!--[-->');
|
|
113
|
+
expect(body).toContain('<!--]-->');
|
|
114
|
+
expect(body).toContain('__tsrx_ta_');
|
|
115
|
+
|
|
116
|
+
ssrCatch = container.querySelector('.root-catch');
|
|
117
|
+
ssrError = container.querySelector('.root-error');
|
|
118
|
+
ssrReset = container.querySelector('.root-reset');
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(container.querySelector('.root-catch')).toBe(ssrCatch);
|
|
123
|
+
expect(container.querySelector('.root-error')).toBe(ssrError);
|
|
124
|
+
expect(container.querySelector('.root-reset')).toBe(ssrReset);
|
|
125
|
+
expect(ssrError?.textContent).toBe(ROOT_TRACK_ASYNC_ERROR_MESSAGE);
|
|
126
|
+
expect(container.querySelector('.root-async-value')).toBeNull();
|
|
127
|
+
expect(container.querySelector('.root-pending')).toBeNull();
|
|
128
|
+
expect(container.querySelector('script[id^="__tsrx_ta_"]')).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
33
132
|
describe.skip('streaming ssr > try blocks (async pending)', () => {
|
|
34
133
|
it('hydrates async try/pending and retains pending fallback', async () => {
|
|
35
134
|
await hydrateComponent(
|
|
@@ -24,6 +24,115 @@ function getString(e: string = 'test') {
|
|
|
24
24
|
expect(result.code).toMatchSnapshot();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
it('removes TypeScript-only expression wrappers from assignment and update targets', () => {
|
|
28
|
+
const source = `
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
(global as any).ResizeObserver = createMockResizeObserver;
|
|
31
|
+
(global as any).count++;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
component Test() {
|
|
35
|
+
let toggle: { value: boolean } | undefined;
|
|
36
|
+
toggle!.value = false;
|
|
37
|
+
toggle!.value++;
|
|
38
|
+
}`;
|
|
39
|
+
|
|
40
|
+
const { code } = compile(source, 'test.tsrx', { mode: 'server' });
|
|
41
|
+
|
|
42
|
+
expect(code).toContain('global.ResizeObserver = createMockResizeObserver');
|
|
43
|
+
expect(code).toContain('global.count++');
|
|
44
|
+
expect(code).toContain('toggle.value = false');
|
|
45
|
+
expect(code).toContain('toggle.value++');
|
|
46
|
+
expect(code).not.toContain('as any');
|
|
47
|
+
expect(code).not.toContain('!');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it(
|
|
51
|
+
'compiles writes to unknown lazy array index 1 through lazy array helpers in SSR output',
|
|
52
|
+
() => {
|
|
53
|
+
const source = `component Child({ pair: &[first, second] }) {
|
|
54
|
+
second = 10;
|
|
55
|
+
<div>{first}</div>
|
|
56
|
+
}
|
|
57
|
+
component App() {
|
|
58
|
+
<Child pair={[0, 1]} />
|
|
59
|
+
}`;
|
|
60
|
+
const { code } = compile(source, 'test.tsrx', { mode: 'server' });
|
|
61
|
+
|
|
62
|
+
expect(code).toContain('_$_.lazy_array_set(lazy, 10, 1)');
|
|
63
|
+
expect(code).not.toContain('lazy[1] =');
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
it('does not double-wrap member access on lazy array value bindings in SSR output', () => {
|
|
68
|
+
const source = `component Child({ pair: &[first] }) {
|
|
69
|
+
let value = first[0];
|
|
70
|
+
<div>{value}</div>
|
|
71
|
+
}
|
|
72
|
+
component App() {
|
|
73
|
+
<Child pair={[{ 0: 'x' }]} />
|
|
74
|
+
}`;
|
|
75
|
+
const { code } = compile(source, 'test.tsrx', { mode: 'server' });
|
|
76
|
+
|
|
77
|
+
expect(code).toContain('let value = _$_.lazy_array_get(lazy, 0)[0];');
|
|
78
|
+
expect(code).not.toContain('_$_.lazy_array_get(_$_.lazy_array_get(lazy, 0), 0)');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('throws on indexed access through known tracked lazy destructures in SSR output', () => {
|
|
82
|
+
const source = `import { track } from 'ripple';
|
|
83
|
+
component App() {
|
|
84
|
+
let &[value, tracked_ref] = track({ 0: 'x' });
|
|
85
|
+
let nested = value[0];
|
|
86
|
+
tracked_ref[0] = { 0: 'y' };
|
|
87
|
+
let next = value[0];
|
|
88
|
+
<div>{nested}{next}</div>
|
|
89
|
+
}`;
|
|
90
|
+
|
|
91
|
+
expect(() => compile(source, 'test.tsrx', { mode: 'server' })).toThrow(
|
|
92
|
+
/Use \.value or &\[\] lazy destructuring/,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('throws on known tracked indexed access in SSR output', () => {
|
|
97
|
+
const source = `import { track } from 'ripple';
|
|
98
|
+
component App() {
|
|
99
|
+
let tracked = track(0);
|
|
100
|
+
++tracked[0];
|
|
101
|
+
tracked[0]++;
|
|
102
|
+
tracked[0] = tracked[0] + 1;
|
|
103
|
+
let value = tracked[0];
|
|
104
|
+
let ref = tracked[1];
|
|
105
|
+
<div>{value}</div>
|
|
106
|
+
}`;
|
|
107
|
+
|
|
108
|
+
expect(() => compile(source, 'test.tsrx', { mode: 'server' })).toThrow(
|
|
109
|
+
/Use \.value or &\[\] lazy destructuring/,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it(
|
|
114
|
+
'compiles indexed access on unknown lazy tracked refs through lazy array helpers in SSR output',
|
|
115
|
+
() => {
|
|
116
|
+
const source = `import { track } from 'ripple';
|
|
117
|
+
component Child({ pair: &[value, tracked_ref] }) {
|
|
118
|
+
++tracked_ref[0];
|
|
119
|
+
tracked_ref[0]++;
|
|
120
|
+
<div>{value}</div>
|
|
121
|
+
}
|
|
122
|
+
component App() {
|
|
123
|
+
let tracked = track(0);
|
|
124
|
+
<Child pair={tracked} />
|
|
125
|
+
}`;
|
|
126
|
+
const { code } = compile(source, 'test.tsrx', { mode: 'server' });
|
|
127
|
+
|
|
128
|
+
expect(code).toContain('_$_.lazy_array_get(lazy, 1)');
|
|
129
|
+
expect(code).toContain('_$_.lazy_array_update_pre(_$_.lazy_array_get(lazy, 1), 0)');
|
|
130
|
+
expect(code).toContain('_$_.lazy_array_update(_$_.lazy_array_get(lazy, 1), 0)');
|
|
131
|
+
expect(code).not.toContain('lazy[1][0]');
|
|
132
|
+
expect(code).not.toContain('lazy_array_get(lazy, 1)[0]');
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
27
136
|
it('removes class TypeScript syntax from JS output', () => {
|
|
28
137
|
const source = `interface BaseEvent {}
|
|
29
138
|
|
|
@@ -192,6 +192,19 @@ describe('lazy destructuring', () => {
|
|
|
192
192
|
expect(body).toBeHtml('<pre>Alice-30</pre>');
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
it('preserves numeric member access on lazy array value bindings', async () => {
|
|
196
|
+
component Child({ pair: &[first] }: { pair: [{ 0: string }] }) {
|
|
197
|
+
<pre>{first[0]}</pre>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
component Test() {
|
|
201
|
+
<Child pair={[{ 0: 'x' }]} />
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { body } = await render(Test);
|
|
205
|
+
expect(body).toBeHtml('<pre>x</pre>');
|
|
206
|
+
});
|
|
207
|
+
|
|
195
208
|
it('supports rest in lazy array destructuring for tracked tuples (iterable)', async () => {
|
|
196
209
|
component Test() {
|
|
197
210
|
let tracked_value = track(0);
|
|
@@ -214,6 +227,23 @@ describe('lazy destructuring', () => {
|
|
|
214
227
|
expect(body).toBeHtml('<pre>x-yz</pre>');
|
|
215
228
|
});
|
|
216
229
|
|
|
230
|
+
it('supports rest in lazy array destructuring for iterable values', async () => {
|
|
231
|
+
component Test() {
|
|
232
|
+
const source = {
|
|
233
|
+
*[Symbol.iterator]() {
|
|
234
|
+
yield 'x';
|
|
235
|
+
yield 'y';
|
|
236
|
+
yield 'z';
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
const &[first, ...rest] = source;
|
|
240
|
+
<pre>{`${first}-${rest.join('')}`}</pre>
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const { body } = await render(Test);
|
|
244
|
+
expect(body).toBeHtml('<pre>x-yz</pre>');
|
|
245
|
+
});
|
|
246
|
+
|
|
217
247
|
it('supports standalone lazy array destructuring with track()', async () => {
|
|
218
248
|
component Test() {
|
|
219
249
|
let count;
|
|
@@ -225,6 +255,38 @@ describe('lazy destructuring', () => {
|
|
|
225
255
|
expect(body).toBeHtml('<div>0</div>');
|
|
226
256
|
});
|
|
227
257
|
|
|
258
|
+
it('supports direct value access on tracked values during SSR', async () => {
|
|
259
|
+
component Test() {
|
|
260
|
+
let tracked = track(0);
|
|
261
|
+
++tracked.value;
|
|
262
|
+
tracked.value++;
|
|
263
|
+
tracked.value = tracked.value + 1;
|
|
264
|
+
let value = tracked.value;
|
|
265
|
+
let ref = tracked;
|
|
266
|
+
|
|
267
|
+
<pre>{`${value}-${ref === tracked}`}</pre>
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const { body } = await render(Test);
|
|
271
|
+
expect(body).toBeHtml('<pre>3-true</pre>');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('supports lazy destructured tracked ref value access during SSR', async () => {
|
|
275
|
+
component Child({ pair: &[value, tracked_ref] }: { pair: Tracked<number> }) {
|
|
276
|
+
++tracked_ref.value;
|
|
277
|
+
tracked_ref.value++;
|
|
278
|
+
<pre>{`${value}-${tracked_ref.value}`}</pre>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
component Test() {
|
|
282
|
+
let tracked = track(0);
|
|
283
|
+
<Child pair={tracked} />
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { body } = await render(Test);
|
|
287
|
+
expect(body).toBeHtml('<pre>2-2</pre>');
|
|
288
|
+
});
|
|
289
|
+
|
|
228
290
|
it('supports standalone lazy object destructuring', async () => {
|
|
229
291
|
component Test() {
|
|
230
292
|
let a;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { track } from 'ripple';
|
|
3
|
+
import {
|
|
4
|
+
lazy_array_get,
|
|
5
|
+
lazy_array_set,
|
|
6
|
+
lazy_array_update,
|
|
7
|
+
} from '../../src/runtime/internal/server/index.js';
|
|
8
|
+
|
|
9
|
+
const value_message = /Use \.value or &\[\] lazy destructuring/;
|
|
10
|
+
const reference_message = /Use the tracked value directly instead/;
|
|
11
|
+
const value_index = Number(0);
|
|
12
|
+
const reference_index = Number(1);
|
|
13
|
+
|
|
14
|
+
describe('server tracked numeric access', () => {
|
|
15
|
+
it('throws when tracked values are accessed through numeric properties', () => {
|
|
16
|
+
const values = [track(0)];
|
|
17
|
+
const value = values[0];
|
|
18
|
+
|
|
19
|
+
expect(() => value[value_index]).toThrow(value_message);
|
|
20
|
+
expect(() => {
|
|
21
|
+
value[value_index] = 1;
|
|
22
|
+
}).toThrow(value_message);
|
|
23
|
+
expect(() => value[reference_index]).toThrow(reference_message);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('throws when derived values are accessed through numeric properties', () => {
|
|
27
|
+
const values = [track(() => 1)];
|
|
28
|
+
const value = values[0];
|
|
29
|
+
|
|
30
|
+
expect(() => value[value_index]).toThrow(value_message);
|
|
31
|
+
expect(() => {
|
|
32
|
+
value[value_index] = 2;
|
|
33
|
+
}).toThrow(value_message);
|
|
34
|
+
expect(() => value[reference_index]).toThrow(reference_message);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('sets tracked lazy index 0 and rejects tracked lazy index 1', () => {
|
|
38
|
+
const value = track(0);
|
|
39
|
+
|
|
40
|
+
lazy_array_set(value, 2, 0);
|
|
41
|
+
expect(value.value).toBe(2);
|
|
42
|
+
expect(() => {
|
|
43
|
+
lazy_array_set(value, track(1), 1);
|
|
44
|
+
}).toThrow(reference_message);
|
|
45
|
+
expect(() => {
|
|
46
|
+
lazy_array_update(value, 1);
|
|
47
|
+
}).toThrow(reference_message);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns undefined for tracked lazy indexes past the tuple length', () => {
|
|
51
|
+
const value = track(0);
|
|
52
|
+
const derived = track(() => value.value + 1);
|
|
53
|
+
|
|
54
|
+
expect(lazy_array_get(value, 0)).toBe(0);
|
|
55
|
+
expect(lazy_array_get(value, 1)).toBe(value);
|
|
56
|
+
expect(lazy_array_get(value, 2)).toBeUndefined();
|
|
57
|
+
expect(lazy_array_get(derived, 0)).toBe(1);
|
|
58
|
+
expect(lazy_array_get(derived, 1)).toBe(derived);
|
|
59
|
+
expect(lazy_array_get(derived, 2)).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('ignores tracked lazy writes past the tuple length', () => {
|
|
63
|
+
const value = track(0);
|
|
64
|
+
const derived = track(() => value.value + 1);
|
|
65
|
+
|
|
66
|
+
lazy_array_set(value, 10, 2);
|
|
67
|
+
lazy_array_update(value, 2);
|
|
68
|
+
lazy_array_set(derived, 20, 2);
|
|
69
|
+
lazy_array_update(derived, 2);
|
|
70
|
+
|
|
71
|
+
expect(lazy_array_get(value, 2)).toBeUndefined();
|
|
72
|
+
expect(lazy_array_get(derived, 2)).toBeUndefined();
|
|
73
|
+
expect(Object.hasOwn(value, 2)).toBe(false);
|
|
74
|
+
expect(Object.hasOwn(derived, 2)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/tests/setup-hydration.js
CHANGED
|
@@ -26,6 +26,37 @@ export async function hydrateComponent(serverComponent, clientComponent) {
|
|
|
26
26
|
return { container, unmount };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Helper to server render and hydrate a component with explicit root boundary options.
|
|
31
|
+
* @param {() => void} serverComponent
|
|
32
|
+
* @param {() => void} clientComponent
|
|
33
|
+
* @param {import('ripple/server').BaseRenderOptions['rootBoundary']} serverRootBoundary
|
|
34
|
+
* @param {import('ripple').RootBoundaryOptions} clientRootBoundary
|
|
35
|
+
* @param {((details: { container: HTMLDivElement, body: string }) => void) | undefined} [beforeHydrate]
|
|
36
|
+
* @returns {Promise<{ container: HTMLDivElement, unmount: () => void, body: string }>}
|
|
37
|
+
*/
|
|
38
|
+
export async function hydrateComponentWithRootBoundary(
|
|
39
|
+
serverComponent,
|
|
40
|
+
clientComponent,
|
|
41
|
+
serverRootBoundary,
|
|
42
|
+
clientRootBoundary,
|
|
43
|
+
beforeHydrate,
|
|
44
|
+
) {
|
|
45
|
+
const { body } = await render(serverComponent, {
|
|
46
|
+
rootBoundary: serverRootBoundary,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
container.innerHTML = body;
|
|
50
|
+
beforeHydrate?.({ container, body });
|
|
51
|
+
|
|
52
|
+
const unmount = hydrate(clientComponent, {
|
|
53
|
+
target: container,
|
|
54
|
+
rootBoundary: clientRootBoundary,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { container, unmount, body };
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
/**
|
|
30
61
|
* Strips hydration markers from HTML for testing purposes.
|
|
31
62
|
* Hydration markers are: <!--[--> <!--[!--> <!--]-->
|
package/types/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { AddEventOptions, AddEventObject, ExtendedEventOptions } from '@tsr
|
|
|
4
4
|
export type Component<T = Record<string, any>> = (props: T) => void;
|
|
5
5
|
|
|
6
6
|
declare const TSRX_ELEMENT: unique symbol;
|
|
7
|
+
declare const REF_KEY: unique symbol;
|
|
8
|
+
export type RefKey = typeof REF_KEY;
|
|
7
9
|
|
|
8
10
|
export type TSRXElement = {
|
|
9
11
|
readonly render: Function;
|
|
@@ -17,14 +19,19 @@ export function tsrx_element(render: Function): TSRXElement;
|
|
|
17
19
|
|
|
18
20
|
export function mount(
|
|
19
21
|
component: Component,
|
|
20
|
-
options: { target: HTMLElement; props?: Record<string, any
|
|
22
|
+
options: { target: HTMLElement; props?: Record<string, any>; rootBoundary?: RootBoundaryOptions },
|
|
21
23
|
): () => void;
|
|
22
24
|
|
|
23
25
|
export function hydrate(
|
|
24
26
|
component: Component,
|
|
25
|
-
options: { target: HTMLElement; props?: Record<string, any
|
|
27
|
+
options: { target: HTMLElement; props?: Record<string, any>; rootBoundary?: RootBoundaryOptions },
|
|
26
28
|
): () => void;
|
|
27
29
|
|
|
30
|
+
export interface RootBoundaryOptions {
|
|
31
|
+
pending?: Component<Record<string, never>>;
|
|
32
|
+
catch?: Component<{ error: unknown; reset: () => void }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
export function tick(): Promise<void>;
|
|
29
36
|
|
|
30
37
|
export function untrack<T>(fn: () => T): T;
|
|
@@ -144,7 +151,7 @@ declare global {
|
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
export function createRefKey():
|
|
154
|
+
export function createRefKey(): RefKey;
|
|
148
155
|
|
|
149
156
|
export function isRefProp(value: unknown): boolean;
|
|
150
157
|
|
|
@@ -163,8 +170,7 @@ interface TrackedBase<V> {
|
|
|
163
170
|
interface TrackedCallable<V> {
|
|
164
171
|
(props: V extends Component<infer P> ? P : never): V extends Component ? void : never;
|
|
165
172
|
}
|
|
166
|
-
// Supports
|
|
167
|
-
// And destructuring `const [one, two] = track(0);`
|
|
173
|
+
// Supports destructuring `const [one, two] = track(0);`
|
|
168
174
|
export type Tracked<V> = [V, Tracked<V>] & TrackedBase<V> & TrackedCallable<V>;
|
|
169
175
|
|
|
170
176
|
// Helper type to infer component type from a function that returns a component
|
|
@@ -183,10 +189,6 @@ export type PropsNoChildren<T extends object = {}> = Expand<T>;
|
|
|
183
189
|
|
|
184
190
|
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
|
185
191
|
|
|
186
|
-
export function get<V>(tracked: Tracked<V>): V;
|
|
187
|
-
|
|
188
|
-
export function set<V>(tracked: Tracked<V>, value: V): void;
|
|
189
|
-
|
|
190
192
|
// Overload for tracked values - returns the original tracked value type
|
|
191
193
|
export function track<V>(value: Tracked<V>): Tracked<V>;
|
|
192
194
|
// Overload for function values - infers the return type of the function
|
package/types/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component } from '#public';
|
|
1
|
+
import type { Component, RootBoundaryOptions } from '#public';
|
|
2
2
|
|
|
3
3
|
// Re-export runtime types for server-compiled components
|
|
4
4
|
export {
|
|
@@ -48,6 +48,7 @@ export interface BaseRenderOptions {
|
|
|
48
48
|
// defaults to true
|
|
49
49
|
// set to false to add more content
|
|
50
50
|
closeStream?: boolean;
|
|
51
|
+
rootBoundary?: RootBoundaryOptions;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export interface StreamingRenderOptions extends BaseRenderOptions {
|