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.
Files changed (63) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +3 -3
  3. package/src/jsx-runtime.d.ts +17 -3
  4. package/src/runtime/index-client.js +47 -8
  5. package/src/runtime/index-server.js +0 -2
  6. package/src/runtime/internal/client/index.js +5 -0
  7. package/src/runtime/internal/client/runtime.js +111 -11
  8. package/src/runtime/internal/client/types.d.ts +5 -0
  9. package/src/runtime/internal/server/blocks.js +1 -0
  10. package/src/runtime/internal/server/index.js +175 -20
  11. package/src/utils/errors.js +13 -0
  12. package/tests/client/async-suspend.test.tsrx +2 -2
  13. package/tests/client/basic/basic.get-set.test.tsrx +26 -26
  14. package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +1 -1
  15. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +1 -1
  16. package/tests/client/compiler/compiler.assignments.test.tsrx +80 -0
  17. package/tests/client/compiler/compiler.tracked-access.test.tsrx +52 -8
  18. package/tests/client/compiler/compiler.typescript.test.tsrx +23 -0
  19. package/tests/client/lazy-array.test.tsrx +34 -0
  20. package/tests/client/lazy-destructuring.test.tsrx +79 -8
  21. package/tests/client/tracked-index-access.test.tsrx +113 -0
  22. package/tests/client/tsx.test.tsrx +66 -21
  23. package/tests/hydration/compiled/client/basic.js +2 -2
  24. package/tests/hydration/compiled/client/events.js +9 -9
  25. package/tests/hydration/compiled/client/for.js +50 -54
  26. package/tests/hydration/compiled/client/head.js +9 -9
  27. package/tests/hydration/compiled/client/hmr.js +1 -1
  28. package/tests/hydration/compiled/client/html.js +2 -2
  29. package/tests/hydration/compiled/client/if-children.js +14 -14
  30. package/tests/hydration/compiled/client/if.js +10 -10
  31. package/tests/hydration/compiled/client/mixed-control-flow.js +7 -7
  32. package/tests/hydration/compiled/client/portal.js +2 -2
  33. package/tests/hydration/compiled/client/reactivity.js +7 -7
  34. package/tests/hydration/compiled/client/return.js +37 -37
  35. package/tests/hydration/compiled/client/switch.js +8 -8
  36. package/tests/hydration/compiled/client/track-async-serialization.js +12 -12
  37. package/tests/hydration/compiled/client/try.js +116 -33
  38. package/tests/hydration/compiled/server/basic.js +2 -2
  39. package/tests/hydration/compiled/server/events.js +8 -8
  40. package/tests/hydration/compiled/server/for.js +21 -21
  41. package/tests/hydration/compiled/server/head.js +10 -10
  42. package/tests/hydration/compiled/server/hmr.js +1 -1
  43. package/tests/hydration/compiled/server/html.js +1 -1
  44. package/tests/hydration/compiled/server/if-children.js +9 -9
  45. package/tests/hydration/compiled/server/if.js +6 -6
  46. package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
  47. package/tests/hydration/compiled/server/portal.js +1 -1
  48. package/tests/hydration/compiled/server/reactivity.js +7 -7
  49. package/tests/hydration/compiled/server/return.js +14 -14
  50. package/tests/hydration/compiled/server/switch.js +4 -4
  51. package/tests/hydration/compiled/server/track-async-serialization.js +12 -12
  52. package/tests/hydration/compiled/server/try.js +116 -4
  53. package/tests/hydration/components/basic.tsrx +3 -1
  54. package/tests/hydration/components/try.tsrx +26 -0
  55. package/tests/hydration/try.test.js +100 -1
  56. package/tests/server/await.test.tsrx +1 -1
  57. package/tests/server/basic.test.tsrx +3 -1
  58. package/tests/server/compiler.test.tsrx +109 -0
  59. package/tests/server/lazy-destructuring.test.tsrx +62 -0
  60. package/tests/server/tracked-index-access.test.tsrx +76 -0
  61. package/tests/setup-hydration.js +31 -0
  62. package/types/index.d.ts +11 -9
  63. 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 { hydrateComponent, container } from '../setup-hydration.js';
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(
@@ -1,4 +1,4 @@
1
- import { get, set, track } from 'ripple';
1
+ import { track } from 'ripple';
2
2
 
3
3
  describe('await in control flow', () => {
4
4
  it('all tests are commented out for now as await in control flow is not yet supported', () => {
@@ -73,7 +73,9 @@ second"</pre>
73
73
  }
74
74
 
75
75
  component Basic() {
76
- {<tsx>{[1, 2, 3].map((item) => <div class="app-item">{item}</div>)}</tsx>}
76
+ {<tsx>
77
+ {[1, 2, 3].map((item) => <div class="app-item">{item}</div>)}
78
+ </tsx>}
77
79
  {makeFragment('from helper')}
78
80
  }
79
81
 
@@ -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
+ });
@@ -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(): symbol;
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 indexed access: track(0)[0] value, track(0)[1] → Tracked<V>
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 {