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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync, track, type Derived, type Tracked } from 'ripple';
|
|
3
|
+
|
|
4
|
+
// What happens when a value that reaches a sink (attribute, text, child
|
|
5
|
+
// props) changes kind at runtime: plain value first, tracked or derived later.
|
|
6
|
+
|
|
7
|
+
describe('prop kind switches at runtime', () => {
|
|
8
|
+
it('element spread: a plain attribute value replaced by a derived is followed', () => {
|
|
9
|
+
let swap: () => void = () => {};
|
|
10
|
+
let bump: () => void = () => {};
|
|
11
|
+
|
|
12
|
+
function App() @{
|
|
13
|
+
const n = track(0);
|
|
14
|
+
const attrs = track<{ class: string | Derived<string> }>({ class: 'plain' });
|
|
15
|
+
swap = () => {
|
|
16
|
+
attrs.value = { class: track(() => 'live-' + n.value) };
|
|
17
|
+
};
|
|
18
|
+
bump = () => {
|
|
19
|
+
n.value++;
|
|
20
|
+
};
|
|
21
|
+
<div {...attrs.value}>{'x'}</div>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render(App);
|
|
25
|
+
const div = () => container.querySelector('div');
|
|
26
|
+
expect(div()?.className).toBe('plain');
|
|
27
|
+
|
|
28
|
+
swap();
|
|
29
|
+
flushSync();
|
|
30
|
+
expect(div()?.className).toBe('live-0');
|
|
31
|
+
|
|
32
|
+
// The spread subscribed to the derived when it unwrapped it.
|
|
33
|
+
bump();
|
|
34
|
+
flushSync();
|
|
35
|
+
expect(div()?.className).toBe('live-1');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('plain attribute and text: a tracked prop is read through .value', () => {
|
|
39
|
+
let bump: () => void = () => {};
|
|
40
|
+
|
|
41
|
+
function Child(props: { cls: Tracked<string>; title: Tracked<string> }) @{
|
|
42
|
+
<div class={props.cls.value}>{props.title.value}</div>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function App() @{
|
|
46
|
+
const cls = track('a');
|
|
47
|
+
const title = track('t');
|
|
48
|
+
bump = () => {
|
|
49
|
+
cls.value = 'b';
|
|
50
|
+
title.value = 'u';
|
|
51
|
+
};
|
|
52
|
+
<Child {cls} {title} />
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(App);
|
|
56
|
+
const div = container.querySelector('div');
|
|
57
|
+
expect(div?.className).toBe('a');
|
|
58
|
+
expect(div?.textContent).toBe('t');
|
|
59
|
+
|
|
60
|
+
bump();
|
|
61
|
+
flushSync();
|
|
62
|
+
expect(div?.className).toBe('b');
|
|
63
|
+
expect(div?.textContent).toBe('u');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it(
|
|
67
|
+
'dynamic component: props are re-evaluated when the component changes, not when their inputs do',
|
|
68
|
+
() => {
|
|
69
|
+
let switchComponent: () => void = () => {};
|
|
70
|
+
let bump: () => void = () => {};
|
|
71
|
+
|
|
72
|
+
function A(props: { n: number }) @{
|
|
73
|
+
<span class="a">{props.n}</span>
|
|
74
|
+
}
|
|
75
|
+
function B(props: { n: number }) @{
|
|
76
|
+
<span class="b">{props.n}</span>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function App() @{
|
|
80
|
+
// A function value is a derived: hold the component in a writable one.
|
|
81
|
+
const Comp = track(() => A, undefined, true);
|
|
82
|
+
const n = track(0);
|
|
83
|
+
switchComponent = () => {
|
|
84
|
+
Comp.value = Comp.value === A ? B : A;
|
|
85
|
+
};
|
|
86
|
+
bump = () => {
|
|
87
|
+
n.value++;
|
|
88
|
+
};
|
|
89
|
+
<{Comp.value} n={n.value} />
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
render(App);
|
|
93
|
+
expect(container.querySelector('span.a')?.textContent).toBe('0');
|
|
94
|
+
|
|
95
|
+
// A prop is a value: the existing instance keeps the number it got.
|
|
96
|
+
bump();
|
|
97
|
+
flushSync();
|
|
98
|
+
expect(container.querySelector('span.a')?.textContent).toBe('0');
|
|
99
|
+
|
|
100
|
+
// A new instance reads the literal again.
|
|
101
|
+
switchComponent();
|
|
102
|
+
flushSync();
|
|
103
|
+
expect(container.querySelector('span.b')?.textContent).toBe('1');
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
it('a tracked prop whose value becomes a derived: the child must unwrap both levels', () => {
|
|
108
|
+
let swap: () => void = () => {};
|
|
109
|
+
let bump: () => void = () => {};
|
|
110
|
+
|
|
111
|
+
function Child(props: { v: Tracked<string | Derived<string>> }) @{
|
|
112
|
+
<span>
|
|
113
|
+
{typeof props.v.value === 'string' ? props.v.value : props.v.value.value}
|
|
114
|
+
</span>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function App() @{
|
|
118
|
+
const n = track(0);
|
|
119
|
+
const v = track<string | Derived<string>>('plain');
|
|
120
|
+
swap = () => {
|
|
121
|
+
v.value = track(() => 'live-' + n.value);
|
|
122
|
+
};
|
|
123
|
+
bump = () => {
|
|
124
|
+
n.value++;
|
|
125
|
+
};
|
|
126
|
+
<Child {v} />
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(App);
|
|
130
|
+
const span = () => container.querySelector('span');
|
|
131
|
+
expect(span()?.textContent).toBe('plain');
|
|
132
|
+
|
|
133
|
+
swap();
|
|
134
|
+
flushSync();
|
|
135
|
+
expect(span()?.textContent).toBe('live-0');
|
|
136
|
+
|
|
137
|
+
bump();
|
|
138
|
+
flushSync();
|
|
139
|
+
expect(span()?.textContent).toBe('live-1');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -119,7 +119,7 @@ describe('refs', () => {
|
|
|
119
119
|
let logs: string[] = [];
|
|
120
120
|
|
|
121
121
|
function App() @{
|
|
122
|
-
|
|
122
|
+
const value = 'test';
|
|
123
123
|
function inputRef(node: HTMLInputElement) {
|
|
124
124
|
logs.push('ref called');
|
|
125
125
|
}
|
|
@@ -219,12 +219,12 @@ describe('refs', () => {
|
|
|
219
219
|
let div: HTMLDivElement | null | undefined;
|
|
220
220
|
|
|
221
221
|
function App() @{
|
|
222
|
-
|
|
222
|
+
const show = track(true);
|
|
223
223
|
<>
|
|
224
|
-
@if (show) {
|
|
224
|
+
@if (show.value) {
|
|
225
225
|
<div ref={div}>{'Hello cleanup'}</div>
|
|
226
226
|
}
|
|
227
|
-
<button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
|
|
227
|
+
<button class="toggle" onClick={() => (show.value = false)}>{'hide'}</button>
|
|
228
228
|
</>
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -262,12 +262,12 @@ describe('refs', () => {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
function App() @{
|
|
265
|
-
|
|
265
|
+
const show = track(true);
|
|
266
266
|
<>
|
|
267
|
-
@if (show) {
|
|
267
|
+
@if (show.value) {
|
|
268
268
|
<Child ref={input} />
|
|
269
269
|
}
|
|
270
|
-
<button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
|
|
270
|
+
<button class="toggle" onClick={() => (show.value = false)}>{'hide'}</button>
|
|
271
271
|
</>
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -352,9 +352,9 @@ describe('refs', () => {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
function App() @{
|
|
355
|
-
|
|
355
|
+
const show = track(true);
|
|
356
356
|
<>
|
|
357
|
-
@if (show) {
|
|
357
|
+
@if (show.value) {
|
|
358
358
|
<Child
|
|
359
359
|
ref={(node: HTMLInputElement | null) => {
|
|
360
360
|
input = node;
|
|
@@ -364,7 +364,7 @@ describe('refs', () => {
|
|
|
364
364
|
}}
|
|
365
365
|
/>
|
|
366
366
|
}
|
|
367
|
-
<button class="toggle" onClick={() => (show = false)}>{'hide'}</button>
|
|
367
|
+
<button class="toggle" onClick={() => (show.value = false)}>{'hide'}</button>
|
|
368
368
|
</>
|
|
369
369
|
}
|
|
370
370
|
|
|
@@ -386,14 +386,14 @@ describe('refs', () => {
|
|
|
386
386
|
let previous: HTMLInputElement | undefined;
|
|
387
387
|
|
|
388
388
|
function Child(props: PropsWithExtras<{ ref: RefValue<HTMLInputElement> }>) @{
|
|
389
|
-
|
|
389
|
+
const as_ref = track(true);
|
|
390
390
|
<>
|
|
391
391
|
<input
|
|
392
392
|
type="text"
|
|
393
393
|
value="keep"
|
|
394
|
-
{...(as_ref ? { ref: props.ref } : { input_ref: 'regular prop' })}
|
|
394
|
+
{...(as_ref.value ? { ref: props.ref } : { input_ref: 'regular prop' })}
|
|
395
395
|
/>
|
|
396
|
-
<button class="toggle" onClick={() => (as_ref = false)}>{'toggle'}</button>
|
|
396
|
+
<button class="toggle" onClick={() => (as_ref.value = false)}>{'toggle'}</button>
|
|
397
397
|
</>
|
|
398
398
|
}
|
|
399
399
|
|
|
@@ -427,14 +427,14 @@ describe('refs', () => {
|
|
|
427
427
|
let previous: HTMLInputElement | undefined;
|
|
428
428
|
|
|
429
429
|
function Child(props: PropsWithExtras<{ ref: RefValue<HTMLInputElement> }>) @{
|
|
430
|
-
|
|
430
|
+
const as_ref = track(false);
|
|
431
431
|
<>
|
|
432
432
|
<input
|
|
433
433
|
type="text"
|
|
434
434
|
value="keep"
|
|
435
|
-
{...(as_ref ? { ref: props.ref } : { input_ref: 'regular prop' })}
|
|
435
|
+
{...(as_ref.value ? { ref: props.ref } : { input_ref: 'regular prop' })}
|
|
436
436
|
/>
|
|
437
|
-
<button class="toggle" onClick={() => (as_ref = true)}>{'toggle'}</button>
|
|
437
|
+
<button class="toggle" onClick={() => (as_ref.value = true)}>{'toggle'}</button>
|
|
438
438
|
</>
|
|
439
439
|
}
|
|
440
440
|
|
|
@@ -490,7 +490,7 @@ describe('refs', () => {
|
|
|
490
490
|
let logs: string[] = [];
|
|
491
491
|
|
|
492
492
|
function App() @{
|
|
493
|
-
|
|
493
|
+
const value = 'test';
|
|
494
494
|
function inputRef(node: HTMLInputElement) {
|
|
495
495
|
logs.push('ref called');
|
|
496
496
|
}
|
|
@@ -2,6 +2,15 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { track, flushSync, type Tracked } from 'ripple';
|
|
3
3
|
import { compile } from '@tsrx/ripple';
|
|
4
4
|
|
|
5
|
+
// Declared at module scope on purpose: a module-level component takes the
|
|
6
|
+
// hoisted `_render` lowering, which an in-suite declaration never exercises.
|
|
7
|
+
function ModuleLevelDashboard({ user }) {
|
|
8
|
+
if (!user) {
|
|
9
|
+
return <p class="ml-empty">No user found</p>;
|
|
10
|
+
}
|
|
11
|
+
return <p class="ml-user">{user}</p>;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
describe('returns in restricted scopes', () => {
|
|
6
15
|
it('throws error when return is used in module scope', () => {
|
|
7
16
|
expect(
|
|
@@ -216,10 +225,10 @@ describe('function returns in client components', () => {
|
|
|
216
225
|
}
|
|
217
226
|
|
|
218
227
|
function App() {
|
|
219
|
-
|
|
228
|
+
const user = track<string | null>(null);
|
|
220
229
|
return <>
|
|
221
|
-
<Dashboard user
|
|
222
|
-
<button onClick={() => (user = user === 'Adam' ? null : 'Adam')}>Toggle</button>
|
|
230
|
+
<Dashboard {user} />
|
|
231
|
+
<button onClick={() => (user.value = user.value === 'Adam' ? null : 'Adam')}>Toggle</button>
|
|
223
232
|
</>;
|
|
224
233
|
}
|
|
225
234
|
|
|
@@ -236,4 +245,18 @@ describe('function returns in client components', () => {
|
|
|
236
245
|
flushSync();
|
|
237
246
|
expect(container.querySelector('.empty')?.textContent).toBe('No user found');
|
|
238
247
|
});
|
|
248
|
+
|
|
249
|
+
it('renders every branch of a module-level component that returns templates', () => {
|
|
250
|
+
function App() @{
|
|
251
|
+
<>
|
|
252
|
+
<ModuleLevelDashboard user={null} />
|
|
253
|
+
<ModuleLevelDashboard user="Adam" />
|
|
254
|
+
</>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
render(App);
|
|
258
|
+
|
|
259
|
+
expect(container.querySelector('.ml-empty')?.textContent).toBe('No user found');
|
|
260
|
+
expect(container.querySelector('.ml-user')?.textContent).toBe('Adam');
|
|
261
|
+
});
|
|
239
262
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { flushSync, mount, trackAsync } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('mount rootBoundary', () => {
|
|
4
|
+
it('renders under a default root boundary, so trackAsync works outside @try', async () => {
|
|
5
|
+
function App() @{
|
|
6
|
+
const data = trackAsync(() => Promise.resolve('loaded'));
|
|
7
|
+
<div class="value">{data.value}</div>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const unmount = mount(App, { target: container });
|
|
11
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
12
|
+
flushSync();
|
|
13
|
+
|
|
14
|
+
expect(container.querySelector('.value')?.textContent).toBe('loaded');
|
|
15
|
+
unmount();
|
|
16
|
+
expect(container.innerHTML).toBe('');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders without any root boundary when rootBoundary is false', () => {
|
|
20
|
+
function App() @{
|
|
21
|
+
<div class="plain">{'no boundary'}</div>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const unmount = mount(App, { target: container, rootBoundary: false });
|
|
25
|
+
flushSync();
|
|
26
|
+
|
|
27
|
+
expect(container.querySelector('.plain')?.textContent).toBe('no boundary');
|
|
28
|
+
unmount();
|
|
29
|
+
expect(container.innerHTML).toBe('');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('requires a user @try for trackAsync when rootBoundary is false', () => {
|
|
33
|
+
function App() @{
|
|
34
|
+
const data = trackAsync(() => Promise.resolve('loaded'));
|
|
35
|
+
<div class="value">{data.value}</div>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
expect(() => mount(App, { target: container, rootBoundary: false })).toThrow(
|
|
39
|
+
/Missing parent `try/,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -9,10 +9,10 @@ describe('scoped flush', () => {
|
|
|
9
9
|
let bump = null;
|
|
10
10
|
|
|
11
11
|
function Producer() @{
|
|
12
|
-
|
|
13
|
-
shared =
|
|
12
|
+
const v = track(0);
|
|
13
|
+
shared = v;
|
|
14
14
|
bump = () => {
|
|
15
|
-
v += 1;
|
|
15
|
+
v.value += 1;
|
|
16
16
|
};
|
|
17
17
|
<div class="producer">{'producer'}</div>
|
|
18
18
|
}
|
|
@@ -59,10 +59,10 @@ describe('scoped flush', () => {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function Branch(props) @{
|
|
62
|
-
|
|
62
|
+
const local = track(0);
|
|
63
63
|
if (props.side === 'L') {
|
|
64
64
|
bumpLeftLocal = () => {
|
|
65
|
-
local += 1;
|
|
65
|
+
local.value += 1;
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
<div class={'branch-' + props.side}>
|
|
@@ -72,10 +72,10 @@ describe('scoped flush', () => {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function App() @{
|
|
75
|
-
|
|
76
|
-
Ctx.set(
|
|
75
|
+
const root = track(0);
|
|
76
|
+
Ctx.set(root);
|
|
77
77
|
bumpRoot = () => {
|
|
78
|
-
root += 1;
|
|
78
|
+
root.value += 1;
|
|
79
79
|
};
|
|
80
80
|
<div>
|
|
81
81
|
<Branch side="L" />
|
|
@@ -66,10 +66,10 @@ describe('RippleSet', () => {
|
|
|
66
66
|
it('handles has operation', () => {
|
|
67
67
|
function SetTest() @{
|
|
68
68
|
let items = new RippleSet([1, 2, 3]);
|
|
69
|
-
|
|
69
|
+
const hasValue = track(() => items.has(2));
|
|
70
70
|
<>
|
|
71
71
|
<button onClick={() => items.delete(2)}>{'delete'}</button>
|
|
72
|
-
<pre>{hasValue}</pre>
|
|
72
|
+
<pre>{hasValue.value}</pre>
|
|
73
73
|
</>
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -108,11 +108,11 @@ describe('RippleSet', () => {
|
|
|
108
108
|
it('creates RippleSet with initial values using RippleSet() shorthand syntax', () => {
|
|
109
109
|
function SetTest() @{
|
|
110
110
|
let items = new RippleSet([1, 2, 3, 4]);
|
|
111
|
-
|
|
111
|
+
const hasValue = track(() => items.has(3));
|
|
112
112
|
<>
|
|
113
113
|
<button onClick={() => items.delete(3)}>{'delete'}</button>
|
|
114
114
|
<pre>{items.size}</pre>
|
|
115
|
-
<pre>{hasValue}</pre>
|
|
115
|
+
<pre>{hasValue.value}</pre>
|
|
116
116
|
</>
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -132,12 +132,12 @@ describe('RippleSet', () => {
|
|
|
132
132
|
it('handles all operations with RippleSet() shorthand syntax', () => {
|
|
133
133
|
function SetTest() @{
|
|
134
134
|
let items = new RippleSet([10, 20, 30]);
|
|
135
|
-
|
|
135
|
+
const values = track(() => Array.from(items.values()));
|
|
136
136
|
<>
|
|
137
137
|
<button onClick={() => items.add(40)}>{'add'}</button>
|
|
138
138
|
<button onClick={() => items.delete(20)}>{'delete'}</button>
|
|
139
139
|
<button onClick={() => items.clear()}>{'clear'}</button>
|
|
140
|
-
<pre>{JSON.stringify(values)}</pre>
|
|
140
|
+
<pre>{JSON.stringify(values.value)}</pre>
|
|
141
141
|
<pre>{items.size}</pre>
|
|
142
142
|
</>
|
|
143
143
|
}
|
|
@@ -294,9 +294,9 @@ describe('SVG namespace handling', () => {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
function App() @{
|
|
297
|
-
|
|
297
|
+
const dynTag = track('polygon');
|
|
298
298
|
<SVG>
|
|
299
|
-
<{dynTag} points="0,0 30,0 15,10" />
|
|
299
|
+
<{dynTag.value} points="0,0 30,0 15,10" />
|
|
300
300
|
</SVG>
|
|
301
301
|
}
|
|
302
302
|
|
|
@@ -325,9 +325,9 @@ describe('SVG namespace handling', () => {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
function App() @{
|
|
328
|
-
|
|
328
|
+
const Component = track(() => Polygon);
|
|
329
329
|
<SVG>
|
|
330
|
-
<{Component} points="0,0 30,0 15,10" />
|
|
330
|
+
<{Component.value} points="0,0 30,0 15,10" />
|
|
331
331
|
</SVG>
|
|
332
332
|
}
|
|
333
333
|
|
|
@@ -342,25 +342,25 @@ describe('SVG namespace handling', () => {
|
|
|
342
342
|
|
|
343
343
|
it('should render SVG as a dynamic top element with any dynamic children elements', () => {
|
|
344
344
|
function SVG({ children }: PropsWithChildren<{}>) @{
|
|
345
|
-
|
|
346
|
-
<{tag}
|
|
345
|
+
const tag = track('svg');
|
|
346
|
+
<{tag.value}
|
|
347
347
|
width={100}
|
|
348
348
|
height={50}
|
|
349
349
|
fill="red"
|
|
350
350
|
viewBox="0 0 30 10"
|
|
351
351
|
preserveAspectRatio="none"
|
|
352
|
-
>{children}</{tag}>
|
|
352
|
+
>{children}</{tag.value}>
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
function Polygon({ points }: PropsWithExtras<{ points: string }>) @{
|
|
356
|
-
|
|
357
|
-
<{dynTag} {points} />
|
|
356
|
+
const dynTag = track('polygon');
|
|
357
|
+
<{dynTag.value} {points} />
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
function App() @{
|
|
361
|
-
|
|
361
|
+
const Component = track(() => Polygon);
|
|
362
362
|
<SVG>
|
|
363
|
-
<{Component} points="0,0 30,0 15,10" />
|
|
363
|
+
<{Component.value} points="0,0 30,0 15,10" />
|
|
364
364
|
</SVG>
|
|
365
365
|
}
|
|
366
366
|
|
|
@@ -3,11 +3,11 @@ import { flushSync, track } from 'ripple';
|
|
|
3
3
|
describe('switch statements', () => {
|
|
4
4
|
it('renders simple switch with literal cases', () => {
|
|
5
5
|
function App() @{
|
|
6
|
-
|
|
6
|
+
const value = track('b');
|
|
7
7
|
<>
|
|
8
|
-
<button onClick={() => (value = 'c')}>{'Change to C'}</button>
|
|
9
|
-
<button onClick={() => (value = 'a')}>{'Change to A'}</button>
|
|
10
|
-
@switch (value) {
|
|
8
|
+
<button onClick={() => (value.value = 'c')}>{'Change to C'}</button>
|
|
9
|
+
<button onClick={() => (value.value = 'a')}>{'Change to A'}</button>
|
|
10
|
+
@switch (value.value) {
|
|
11
11
|
@case 'a': {
|
|
12
12
|
<div>Case A</div>
|
|
13
13
|
}
|
|
@@ -39,10 +39,10 @@ describe('switch statements', () => {
|
|
|
39
39
|
|
|
40
40
|
it('renders switch with reactive discriminant', () => {
|
|
41
41
|
function App() @{
|
|
42
|
-
|
|
42
|
+
const count = track(1);
|
|
43
43
|
<>
|
|
44
|
-
<button onClick={() => count++}>{'Increment'}</button>
|
|
45
|
-
@switch (count) {
|
|
44
|
+
<button onClick={() => count.value++}>{'Increment'}</button>
|
|
45
|
+
@switch (count.value) {
|
|
46
46
|
@case 1: {
|
|
47
47
|
<div>{'Count is 1'}</div>
|
|
48
48
|
}
|
|
@@ -72,12 +72,12 @@ describe('switch statements', () => {
|
|
|
72
72
|
|
|
73
73
|
it('renders switch with default clause only', () => {
|
|
74
74
|
function App() @{
|
|
75
|
-
|
|
75
|
+
const value = track('x');
|
|
76
76
|
<>
|
|
77
|
-
<button onClick={() => (value = 'y')}>{'Change Value'}</button>
|
|
78
|
-
@switch (value) {
|
|
77
|
+
<button onClick={() => (value.value = 'y')}>{'Change Value'}</button>
|
|
78
|
+
@switch (value.value) {
|
|
79
79
|
@default: {
|
|
80
|
-
<div>{'Default for ' + value}</div>
|
|
80
|
+
<div>{'Default for ' + value.value}</div>
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
</>
|
|
@@ -94,11 +94,11 @@ describe('switch statements', () => {
|
|
|
94
94
|
|
|
95
95
|
it('renders switch with isolated empty cases', () => {
|
|
96
96
|
function App() @{
|
|
97
|
-
|
|
97
|
+
const value = track('a');
|
|
98
98
|
<>
|
|
99
|
-
<button onClick={() => (value = 'b')}>{'Change to B'}</button>
|
|
100
|
-
<button onClick={() => (value = 'c')}>{'Change to C'}</button>
|
|
101
|
-
@switch (value) {
|
|
99
|
+
<button onClick={() => (value.value = 'b')}>{'Change to B'}</button>
|
|
100
|
+
<button onClick={() => (value.value = 'c')}>{'Change to C'}</button>
|
|
101
|
+
@switch (value.value) {
|
|
102
102
|
@case 'a': {
|
|
103
103
|
<div>Case A</div>
|
|
104
104
|
}
|
|
@@ -138,28 +138,28 @@ describe('switch statements', () => {
|
|
|
138
138
|
|
|
139
139
|
it('renders switch with template content and reacts to tracked changes', () => {
|
|
140
140
|
function App() @{
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
const status = track('active');
|
|
142
|
+
const message = track('');
|
|
143
143
|
<>
|
|
144
|
-
<button onClick={() => (status = 'pending')}>{'Pending'}</button>
|
|
145
|
-
<button onClick={() => (status = 'completed')}>{'Completed'}</button>
|
|
146
|
-
<button onClick={() => (status = 'error')}>{'Error'}</button>
|
|
147
|
-
@switch (status) {
|
|
144
|
+
<button onClick={() => (status.value = 'pending')}>{'Pending'}</button>
|
|
145
|
+
<button onClick={() => (status.value = 'completed')}>{'Completed'}</button>
|
|
146
|
+
<button onClick={() => (status.value = 'error')}>{'Error'}</button>
|
|
147
|
+
@switch (status.value) {
|
|
148
148
|
@case 'active': {
|
|
149
|
-
message = 'Currently active.';
|
|
150
|
-
<div>{'Status: ' + message}</div>
|
|
149
|
+
message.value = 'Currently active.';
|
|
150
|
+
<div>{'Status: ' + message.value}</div>
|
|
151
151
|
}
|
|
152
152
|
@case 'pending': {
|
|
153
|
-
message = 'Waiting for completion.';
|
|
154
|
-
<div>{'Status: ' + message}</div>
|
|
153
|
+
message.value = 'Waiting for completion.';
|
|
154
|
+
<div>{'Status: ' + message.value}</div>
|
|
155
155
|
}
|
|
156
156
|
@case 'completed': {
|
|
157
|
-
message = 'Task finished!';
|
|
158
|
-
<div class="success">{'Status: ' + message}</div>
|
|
157
|
+
message.value = 'Task finished!';
|
|
158
|
+
<div class="success">{'Status: ' + message.value}</div>
|
|
159
159
|
}
|
|
160
160
|
@default: {
|
|
161
|
-
message = 'An error occurred.';
|
|
162
|
-
<div class="error">{'Status: ' + message}</div>
|
|
161
|
+
message.value = 'An error occurred.';
|
|
162
|
+
<div class="error">{'Status: ' + message.value}</div>
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
</>
|
|
@@ -191,10 +191,10 @@ describe('switch statements', () => {
|
|
|
191
191
|
'renders switch with isolated cases and reacts to tracked changes without recreating DOM unnecessarily',
|
|
192
192
|
() => {
|
|
193
193
|
function App() @{
|
|
194
|
-
|
|
194
|
+
const status = track(0);
|
|
195
195
|
<>
|
|
196
196
|
<div>
|
|
197
|
-
@switch (status) {
|
|
197
|
+
@switch (status.value) {
|
|
198
198
|
@case -1: {
|
|
199
199
|
}
|
|
200
200
|
@case 0: {
|
|
@@ -223,7 +223,7 @@ describe('switch statements', () => {
|
|
|
223
223
|
</div>
|
|
224
224
|
<button
|
|
225
225
|
onClick={() => {
|
|
226
|
-
status = (status + 1) % 5;
|
|
226
|
+
status.value = (status.value + 1) % 5;
|
|
227
227
|
}}
|
|
228
228
|
>{'Next Status'}</button>
|
|
229
229
|
</>
|
|
@@ -263,19 +263,19 @@ describe('switch statements', () => {
|
|
|
263
263
|
|
|
264
264
|
it('renders a middle switch case without falling through to later cases', () => {
|
|
265
265
|
function App() @{
|
|
266
|
-
|
|
266
|
+
const value = track('x');
|
|
267
267
|
<>
|
|
268
|
-
<button onClick={() => (value = 'a')}>{'Set A'}</button>
|
|
269
|
-
<button onClick={() => (value = 'b')}>{'Set B'}</button>
|
|
270
|
-
<button onClick={() => (value = 'c')}>{'Set C'}</button>
|
|
271
|
-
<button onClick={() => (value = 'x')}>{'Set X'}</button>
|
|
268
|
+
<button onClick={() => (value.value = 'a')}>{'Set A'}</button>
|
|
269
|
+
<button onClick={() => (value.value = 'b')}>{'Set B'}</button>
|
|
270
|
+
<button onClick={() => (value.value = 'c')}>{'Set C'}</button>
|
|
271
|
+
<button onClick={() => (value.value = 'x')}>{'Set X'}</button>
|
|
272
272
|
<div>
|
|
273
|
-
@switch (value) {
|
|
273
|
+
@switch (value.value) {
|
|
274
274
|
@case 'a': {
|
|
275
275
|
<div>{' Case A'}</div>
|
|
276
276
|
}
|
|
277
277
|
@case 'x': {
|
|
278
|
-
<div>{' Default Case for ' + value}</div>
|
|
278
|
+
<div>{' Default Case for ' + value.value}</div>
|
|
279
279
|
}
|
|
280
280
|
@case 'b': {
|
|
281
281
|
<div>{' Case B'}</div>
|
|
@@ -363,16 +363,16 @@ describe('switch statements', () => {
|
|
|
363
363
|
|
|
364
364
|
it('renders switch with isolated block-scoped cases', () => {
|
|
365
365
|
function App() @{
|
|
366
|
-
|
|
366
|
+
const level = track(1);
|
|
367
367
|
<>
|
|
368
368
|
<button
|
|
369
369
|
onClick={() => {
|
|
370
|
-
if (level === 1) level = 2;
|
|
371
|
-
else if (level === 2) level = 3;
|
|
372
|
-
else level = 1;
|
|
370
|
+
if (level.value === 1) level.value = 2;
|
|
371
|
+
else if (level.value === 2) level.value = 3;
|
|
372
|
+
else level.value = 1;
|
|
373
373
|
}}
|
|
374
374
|
>{'Toggle'}</button>
|
|
375
|
-
@switch (level) {
|
|
375
|
+
@switch (level.value) {
|
|
376
376
|
@case 1: {
|
|
377
377
|
<div class="level">{'Level 1'}</div>
|
|
378
378
|
}
|
|
@@ -7,8 +7,8 @@ describe('trackAsync hydration from serialized data (client)', () => {
|
|
|
7
7
|
|
|
8
8
|
function App() @{
|
|
9
9
|
@try {
|
|
10
|
-
|
|
11
|
-
<p class="result">{data}</p>
|
|
10
|
+
const data = trackAsync(() => Promise.resolve('fallback'));
|
|
11
|
+
<p class="result">{data.value}</p>
|
|
12
12
|
} @pending {
|
|
13
13
|
<p class="loading">{'loading...'}</p>
|
|
14
14
|
}
|
|
@@ -31,8 +31,8 @@ describe('trackAsync hydration from serialized data (client)', () => {
|
|
|
31
31
|
|
|
32
32
|
function App() @{
|
|
33
33
|
@try {
|
|
34
|
-
|
|
35
|
-
<span class="value">{value}</span>
|
|
34
|
+
const value = trackAsync(() => promise);
|
|
35
|
+
<span class="value">{value.value}</span>
|
|
36
36
|
} @pending {
|
|
37
37
|
<span class="pending">{'waiting'}</span>
|
|
38
38
|
}
|