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,452 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync, track, trackAsync } from 'ripple';
|
|
3
|
+
|
|
4
|
+
// A plain `let` that is written after its declaration (reassigned in the
|
|
5
|
+
// source, or assigned by a `ref` setter) and read by template code is
|
|
6
|
+
// compiled to a `{ v }` box, so the module-level functions the compiler
|
|
7
|
+
// hoists (`@if` and `@switch` conditions and branches, render blocks) share
|
|
8
|
+
// the variable with the component exactly as a closure would. None of these
|
|
9
|
+
// variables is reactive: the update is always triggered by a tracked value,
|
|
10
|
+
// and the box only has to make sure the hoisted code sees the latest value.
|
|
11
|
+
|
|
12
|
+
describe('boxed locals', () => {
|
|
13
|
+
it('a reassigned let read by an @if condition is current on re-evaluation', () => {
|
|
14
|
+
let disable: () => void = () => {};
|
|
15
|
+
let bump: () => void = () => {};
|
|
16
|
+
|
|
17
|
+
function App() @{
|
|
18
|
+
const count = track(0);
|
|
19
|
+
let enabled = true;
|
|
20
|
+
disable = () => {
|
|
21
|
+
enabled = false;
|
|
22
|
+
};
|
|
23
|
+
bump = () => {
|
|
24
|
+
count.value++;
|
|
25
|
+
};
|
|
26
|
+
@if (enabled && count.value > 0) {
|
|
27
|
+
<span>on</span>
|
|
28
|
+
} @else {
|
|
29
|
+
<span>off</span>
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render(App);
|
|
34
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
35
|
+
expect(text()).toBe('off');
|
|
36
|
+
|
|
37
|
+
bump();
|
|
38
|
+
flushSync();
|
|
39
|
+
expect(text()).toBe('on');
|
|
40
|
+
|
|
41
|
+
disable();
|
|
42
|
+
bump();
|
|
43
|
+
flushSync();
|
|
44
|
+
expect(text()).toBe('off');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('a reassigned let read inside an @if branch is current when the branch renders', () => {
|
|
48
|
+
let rename: () => void = () => {};
|
|
49
|
+
let toggle: () => void = () => {};
|
|
50
|
+
|
|
51
|
+
function App() @{
|
|
52
|
+
const show = track(false);
|
|
53
|
+
let title = 'first';
|
|
54
|
+
rename = () => {
|
|
55
|
+
title = 'second';
|
|
56
|
+
};
|
|
57
|
+
toggle = () => {
|
|
58
|
+
show.value = !show.value;
|
|
59
|
+
};
|
|
60
|
+
@if (show.value) {
|
|
61
|
+
<p>{title}</p>
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(App);
|
|
66
|
+
expect(container.querySelector('p')).toBeNull();
|
|
67
|
+
|
|
68
|
+
rename();
|
|
69
|
+
toggle();
|
|
70
|
+
flushSync();
|
|
71
|
+
expect(container.querySelector('p')?.textContent).toBe('second');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('a reassigned let read by a text expression is current on re-evaluation', () => {
|
|
75
|
+
let rename: () => void = () => {};
|
|
76
|
+
let bump: () => void = () => {};
|
|
77
|
+
|
|
78
|
+
function App() @{
|
|
79
|
+
const count = track(0);
|
|
80
|
+
let unit = 'ms';
|
|
81
|
+
rename = () => {
|
|
82
|
+
unit = 's';
|
|
83
|
+
};
|
|
84
|
+
bump = () => {
|
|
85
|
+
count.value++;
|
|
86
|
+
};
|
|
87
|
+
<p>{count.value + unit}</p>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
render(App);
|
|
91
|
+
const text = () => container.querySelector('p')?.textContent;
|
|
92
|
+
expect(text()).toBe('0ms');
|
|
93
|
+
|
|
94
|
+
rename();
|
|
95
|
+
bump();
|
|
96
|
+
flushSync();
|
|
97
|
+
expect(text()).toBe('1s');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it(
|
|
101
|
+
'a let written by a handler inside a hoisted @if branch updates the component variable',
|
|
102
|
+
() => {
|
|
103
|
+
let read: () => number = () => -1;
|
|
104
|
+
|
|
105
|
+
function App() @{
|
|
106
|
+
const show = track(true);
|
|
107
|
+
const tick = track(0);
|
|
108
|
+
let clicks = 0;
|
|
109
|
+
read = () => clicks;
|
|
110
|
+
@if (show.value) {
|
|
111
|
+
<button onClick={() => clicks++}>{'hit ' + tick.value}</button>
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render(App);
|
|
116
|
+
expect(read()).toBe(0);
|
|
117
|
+
|
|
118
|
+
container.querySelector('button')?.click();
|
|
119
|
+
container.querySelector('button')?.click();
|
|
120
|
+
expect(read()).toBe(2);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
it('a let assigned by a ref inside a hoisted @if branch is the element, then null', () => {
|
|
125
|
+
let div: HTMLDivElement | null | undefined;
|
|
126
|
+
let hide: () => void = () => {};
|
|
127
|
+
|
|
128
|
+
function App() @{
|
|
129
|
+
const show = track(true);
|
|
130
|
+
const label = track('a');
|
|
131
|
+
hide = () => {
|
|
132
|
+
show.value = false;
|
|
133
|
+
};
|
|
134
|
+
@if (show.value) {
|
|
135
|
+
<div ref={div}>{label.value}</div>
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
render(App);
|
|
140
|
+
flushSync();
|
|
141
|
+
expect(div).toBeInstanceOf(HTMLDivElement);
|
|
142
|
+
|
|
143
|
+
hide();
|
|
144
|
+
flushSync();
|
|
145
|
+
expect(div).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('supports compound assignment and update expressions on a boxed let', () => {
|
|
149
|
+
let step: () => void = () => {};
|
|
150
|
+
let bump: () => void = () => {};
|
|
151
|
+
|
|
152
|
+
function App() @{
|
|
153
|
+
const count = track(0);
|
|
154
|
+
let base = 10;
|
|
155
|
+
step = () => {
|
|
156
|
+
base += 5;
|
|
157
|
+
base++;
|
|
158
|
+
base = base * 2;
|
|
159
|
+
};
|
|
160
|
+
bump = () => {
|
|
161
|
+
count.value++;
|
|
162
|
+
};
|
|
163
|
+
<span>{base + count.value}</span>
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
render(App);
|
|
167
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
168
|
+
expect(text()).toBe('10');
|
|
169
|
+
|
|
170
|
+
step();
|
|
171
|
+
bump();
|
|
172
|
+
flushSync();
|
|
173
|
+
expect(text()).toBe('33');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('boxes a rebound name from a flat destructuring pattern', () => {
|
|
177
|
+
let rename: () => void = () => {};
|
|
178
|
+
let bump: () => void = () => {};
|
|
179
|
+
|
|
180
|
+
function Names(props: { user: { first: string; last?: string } }) @{
|
|
181
|
+
const count = track(0);
|
|
182
|
+
let { first, last = 'none' } = props.user;
|
|
183
|
+
rename = () => {
|
|
184
|
+
first = 'renamed';
|
|
185
|
+
};
|
|
186
|
+
bump = () => {
|
|
187
|
+
count.value++;
|
|
188
|
+
};
|
|
189
|
+
<span>{first + ' ' + last + ' ' + count.value}</span>
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function App() @{
|
|
193
|
+
<Names user={{ first: 'ada' }} />
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
render(App);
|
|
197
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
198
|
+
expect(text()).toBe('ada none 0');
|
|
199
|
+
|
|
200
|
+
rename();
|
|
201
|
+
bump();
|
|
202
|
+
flushSync();
|
|
203
|
+
expect(text()).toBe('renamed none 1');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('boxes a rebound destructured parameter', () => {
|
|
207
|
+
let edit: () => void = () => {};
|
|
208
|
+
let bump: () => void = () => {};
|
|
209
|
+
|
|
210
|
+
function Panel({ mode = 'view' }: { mode?: string }) @{
|
|
211
|
+
const count = track(0);
|
|
212
|
+
edit = () => {
|
|
213
|
+
mode = 'edit';
|
|
214
|
+
};
|
|
215
|
+
bump = () => {
|
|
216
|
+
count.value++;
|
|
217
|
+
};
|
|
218
|
+
@if (count.value >= 0) {
|
|
219
|
+
<button>{mode + count.value}</button>
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
render(Panel);
|
|
224
|
+
const text = () => container.querySelector('button')?.textContent;
|
|
225
|
+
expect(text()).toBe('view0');
|
|
226
|
+
|
|
227
|
+
edit();
|
|
228
|
+
bump();
|
|
229
|
+
flushSync();
|
|
230
|
+
expect(text()).toBe('edit1');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('boxes a reassigned props parameter', () => {
|
|
234
|
+
let bump: () => void = () => {};
|
|
235
|
+
|
|
236
|
+
function Child(props: { label: string }) @{
|
|
237
|
+
const count = track(0);
|
|
238
|
+
bump = () => {
|
|
239
|
+
count.value++;
|
|
240
|
+
};
|
|
241
|
+
props = { ...props, label: props.label + '!' };
|
|
242
|
+
<span>{props.label + count.value}</span>
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function App() @{
|
|
246
|
+
<Child label="hi" />
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
render(App);
|
|
250
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
251
|
+
expect(text()).toBe('hi!0');
|
|
252
|
+
|
|
253
|
+
bump();
|
|
254
|
+
flushSync();
|
|
255
|
+
expect(text()).toBe('hi!1');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('boxes rebound names from nested object and array patterns', () => {
|
|
259
|
+
let rename: () => void = () => {};
|
|
260
|
+
let bump: () => void = () => {};
|
|
261
|
+
|
|
262
|
+
function Names(props: { user: { first: string; last?: string }; tags: string[] }) @{
|
|
263
|
+
const count = track(0);
|
|
264
|
+
let { user: { first, last = 'none' }, tags: [tag] } = props;
|
|
265
|
+
rename = () => {
|
|
266
|
+
first = 'renamed';
|
|
267
|
+
tag = 't2';
|
|
268
|
+
};
|
|
269
|
+
bump = () => {
|
|
270
|
+
count.value++;
|
|
271
|
+
};
|
|
272
|
+
<span>{first + ' ' + last + ' ' + tag + count.value}</span>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function App() @{
|
|
276
|
+
<Names user={{ first: 'ada' }} tags={['t1']} />
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
render(App);
|
|
280
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
281
|
+
expect(text()).toBe('ada none t10');
|
|
282
|
+
|
|
283
|
+
rename();
|
|
284
|
+
bump();
|
|
285
|
+
flushSync();
|
|
286
|
+
expect(text()).toBe('renamed none t21');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('boxes a rebound catch parameter', async () => {
|
|
290
|
+
let clear: () => void = () => {};
|
|
291
|
+
let bump: () => void = () => {};
|
|
292
|
+
|
|
293
|
+
function Loader() @{
|
|
294
|
+
const n = track(0);
|
|
295
|
+
bump = () => {
|
|
296
|
+
n.value++;
|
|
297
|
+
};
|
|
298
|
+
@try {
|
|
299
|
+
const data = trackAsync(() => Promise.reject(new Error('boom')));
|
|
300
|
+
<p>{data.value}</p>
|
|
301
|
+
} @catch (err) {
|
|
302
|
+
clear = () => {
|
|
303
|
+
err = null;
|
|
304
|
+
};
|
|
305
|
+
<button>{String(err instanceof Error ? err.message : err) + n.value}</button>
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
render(Loader);
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
311
|
+
flushSync();
|
|
312
|
+
const text = () => container.querySelector('button')?.textContent;
|
|
313
|
+
expect(text()).toBe('boom0');
|
|
314
|
+
|
|
315
|
+
clear();
|
|
316
|
+
bump();
|
|
317
|
+
flushSync();
|
|
318
|
+
expect(text()).toBe('null1');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('boxes lets written through destructuring and for-of assignment targets', () => {
|
|
322
|
+
let swap: () => void = () => {};
|
|
323
|
+
let bump: () => void = () => {};
|
|
324
|
+
|
|
325
|
+
function Pair() @{
|
|
326
|
+
const tick = track(0);
|
|
327
|
+
let label = 'a';
|
|
328
|
+
let other = 'x';
|
|
329
|
+
let last = '';
|
|
330
|
+
swap = () => {
|
|
331
|
+
[label] = ['b'];
|
|
332
|
+
({ other } = { other: 'y' });
|
|
333
|
+
for (last of ['c', 'd']) {
|
|
334
|
+
// the loop variable is the boxed let
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
bump = () => {
|
|
338
|
+
tick.value++;
|
|
339
|
+
};
|
|
340
|
+
<span>{label + other + last + tick.value}</span>
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
render(Pair);
|
|
344
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
345
|
+
expect(text()).toBe('ax0');
|
|
346
|
+
|
|
347
|
+
swap();
|
|
348
|
+
bump();
|
|
349
|
+
flushSync();
|
|
350
|
+
expect(text()).toBe('byd1');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('boxes lets written through rest and default destructuring targets', () => {
|
|
354
|
+
let swap: () => void = () => {};
|
|
355
|
+
let bump: () => void = () => {};
|
|
356
|
+
|
|
357
|
+
function App() @{
|
|
358
|
+
const tick = track(0);
|
|
359
|
+
let a = 0;
|
|
360
|
+
let rest: Record<string, number> = {};
|
|
361
|
+
let tail: number[] = [];
|
|
362
|
+
swap = () => {
|
|
363
|
+
({ a = 5, ...rest } = { rest_x: 7 } as { a?: number; rest_x: number });
|
|
364
|
+
[a, ...tail] = new Set([1, 2, 3]);
|
|
365
|
+
};
|
|
366
|
+
bump = () => {
|
|
367
|
+
tick.value++;
|
|
368
|
+
};
|
|
369
|
+
<span>{a + ':' + (rest.rest_x ?? '-') + ':' + tail.join(',') + ':' + tick.value}</span>
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
render(App);
|
|
373
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
374
|
+
expect(text()).toBe('0:-::0');
|
|
375
|
+
|
|
376
|
+
swap();
|
|
377
|
+
bump();
|
|
378
|
+
flushSync();
|
|
379
|
+
// The object default applied, the rest collected, and the array rest
|
|
380
|
+
// consumed the Set through the iterator protocol.
|
|
381
|
+
expect(text()).toBe('1:7:2,3:1');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('boxes a reassigned function declaration', () => {
|
|
385
|
+
let swap: () => void = () => {};
|
|
386
|
+
let bump: () => void = () => {};
|
|
387
|
+
|
|
388
|
+
function App() @{
|
|
389
|
+
const n = track(0);
|
|
390
|
+
function greet() {
|
|
391
|
+
return 'hi';
|
|
392
|
+
}
|
|
393
|
+
swap = () => {
|
|
394
|
+
greet = () => 'bye';
|
|
395
|
+
};
|
|
396
|
+
bump = () => {
|
|
397
|
+
n.value++;
|
|
398
|
+
};
|
|
399
|
+
<span>{greet() + n.value}</span>
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
render(App);
|
|
403
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
404
|
+
expect(text()).toBe('hi0');
|
|
405
|
+
|
|
406
|
+
swap();
|
|
407
|
+
bump();
|
|
408
|
+
flushSync();
|
|
409
|
+
expect(text()).toBe('bye1');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('a let declared without an initializer starts as undefined', () => {
|
|
413
|
+
let assign: () => void = () => {};
|
|
414
|
+
let bump: () => void = () => {};
|
|
415
|
+
|
|
416
|
+
function App() @{
|
|
417
|
+
const count = track(0);
|
|
418
|
+
let later: string | undefined;
|
|
419
|
+
assign = () => {
|
|
420
|
+
later = 'set';
|
|
421
|
+
};
|
|
422
|
+
bump = () => {
|
|
423
|
+
count.value++;
|
|
424
|
+
};
|
|
425
|
+
<span>{String(later) + '/' + count.value}</span>
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
render(App);
|
|
429
|
+
const text = () => container.querySelector('span')?.textContent;
|
|
430
|
+
expect(text()).toBe('undefined/0');
|
|
431
|
+
|
|
432
|
+
assign();
|
|
433
|
+
bump();
|
|
434
|
+
flushSync();
|
|
435
|
+
expect(text()).toBe('set/1');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('boxing keeps per-iteration bindings', () => {
|
|
440
|
+
it('does not box a for-loop variable, whose closures see their own iteration', () => {
|
|
441
|
+
function App() @{
|
|
442
|
+
const list = [];
|
|
443
|
+
for (let i = 0; i < 3; i++) {
|
|
444
|
+
list.push(<span>{i}</span>);
|
|
445
|
+
}
|
|
446
|
+
<div>{list}</div>
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
render(App);
|
|
450
|
+
expect(container.querySelector('div')?.textContent).toBe('012');
|
|
451
|
+
});
|
|
452
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count =
|
|
3
|
+
exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = count.value;"`;
|
|
4
4
|
|
|
5
5
|
exports[`compiler > assignments > compiles tracked values in effect with update expressions 1`] = `
|
|
6
6
|
"_$_.untrack(() => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
state.preIncrement = ++count.value;
|
|
8
|
+
state.postIncrement = count.value++;
|
|
9
|
+
state.preDecrement = --count.value;
|
|
10
|
+
state.postDecrement = count.value--;
|
|
11
|
+
});"
|
|
12
12
|
`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RippleArray, effect, track, untrack } from 'ripple';
|
|
2
2
|
import { compile } from '@tsrx/ripple';
|
|
3
3
|
|
|
4
|
-
const EFFECT_BODY_REGEX = /_\$\_\.effect\(\(\) => \{([\s\S]*?)\n\t\
|
|
4
|
+
const EFFECT_BODY_REGEX = /_\$\_\.effect\(\(\) => \{([\s\S]*?)\n\t\}\);/;
|
|
5
5
|
|
|
6
6
|
describe('compiler > assignments', () => {
|
|
7
7
|
it('properly handles JS assignments, reads and updates to array indices', () => {
|
|
@@ -105,10 +105,10 @@ describe('compiler > assignments', () => {
|
|
|
105
105
|
it('compiles tracked values in effect with assignment expression', () => {
|
|
106
106
|
const source = `import { track, effect } from 'ripple';
|
|
107
107
|
function App() @{
|
|
108
|
-
|
|
108
|
+
const count = track(0);
|
|
109
109
|
|
|
110
110
|
effect(() => {
|
|
111
|
-
state.count = count;
|
|
111
|
+
state.count = count.value;
|
|
112
112
|
});
|
|
113
113
|
<></>
|
|
114
114
|
}`;
|
|
@@ -120,14 +120,14 @@ function App() @{
|
|
|
120
120
|
it('compiles tracked values in effect with update expressions', () => {
|
|
121
121
|
const source = `import { track, effect, untrack } from 'ripple';
|
|
122
122
|
function App() @{
|
|
123
|
-
|
|
123
|
+
const count = track(5);
|
|
124
124
|
|
|
125
125
|
effect(() => {
|
|
126
126
|
untrack(() => {
|
|
127
|
-
state.preIncrement = ++count;
|
|
128
|
-
state.postIncrement = count++;
|
|
129
|
-
state.preDecrement = --count;
|
|
130
|
-
state.postDecrement = count--;
|
|
127
|
+
state.preIncrement = ++count.value;
|
|
128
|
+
state.postIncrement = count.value++;
|
|
129
|
+
state.preDecrement = --count.value;
|
|
130
|
+
state.postDecrement = count.value--;
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
<></>
|
|
@@ -137,70 +137,6 @@ function App() @{
|
|
|
137
137
|
expect(effect_match?.[1].trim()).toMatchSnapshot();
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
it('compiles unknown lazy array destructuring through lazy array helpers', () => {
|
|
141
|
-
const source = `import { track } from 'ripple';
|
|
142
|
-
function Child({ tr: &[count, tr] }) @{
|
|
143
|
-
count++;
|
|
144
|
-
tr[0]++;
|
|
145
|
-
<div>{count}</div>
|
|
146
|
-
}
|
|
147
|
-
function App() @{
|
|
148
|
-
let tracked = track(0);
|
|
149
|
-
<Child tr={tracked} />
|
|
150
|
-
}`;
|
|
151
|
-
const { code } = compile(source, 'test.tsrx', { mode: 'client' });
|
|
152
|
-
|
|
153
|
-
expect(code).toContain('_$_.lazy_array_get(lazy, 0)');
|
|
154
|
-
expect(code).toContain('_$_.lazy_array_update(lazy, 0)');
|
|
155
|
-
expect(code).toContain('_$_.lazy_array_get(lazy, 1)');
|
|
156
|
-
expect(code).toContain('_$_.lazy_array_update(_$_.lazy_array_get(lazy, 1), 0)');
|
|
157
|
-
expect(code).not.toContain('lazy[0]');
|
|
158
|
-
expect(code).not.toContain('lazy[1][0]');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('compiles writes to unknown lazy array index 1 through lazy array helpers', () => {
|
|
162
|
-
const source = `function Child({ pair: &[first, second] }) @{
|
|
163
|
-
second = 10;
|
|
164
|
-
<div>{first}</div>
|
|
165
|
-
}
|
|
166
|
-
function App() @{
|
|
167
|
-
<Child pair={[0, 1]} />
|
|
168
|
-
}`;
|
|
169
|
-
const { code } = compile(source, 'test.tsrx', { mode: 'client' });
|
|
170
|
-
|
|
171
|
-
expect(code).toContain('_$_.lazy_array_set(lazy, 10, 1)');
|
|
172
|
-
expect(code).not.toContain('lazy[1] =');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('does not double-wrap member access on lazy array value bindings', () => {
|
|
176
|
-
const source = `function Child({ pair: &[first] }) @{
|
|
177
|
-
let value = first[0];
|
|
178
|
-
<div>{value}</div>
|
|
179
|
-
}
|
|
180
|
-
function App() @{
|
|
181
|
-
<Child pair={[{ 0: 'x' }]} />
|
|
182
|
-
}`;
|
|
183
|
-
const { code } = compile(source, 'test.tsrx', { mode: 'client' });
|
|
184
|
-
|
|
185
|
-
expect(code).toContain('let value = _$_.lazy_array_get(lazy, 0)[0];');
|
|
186
|
-
expect(code).not.toContain('_$_.lazy_array_get(_$_.lazy_array_get(lazy, 0), 0)');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('throws on indexed access through known tracked lazy destructures', () => {
|
|
190
|
-
const source = `import { track } from 'ripple';
|
|
191
|
-
function App() @{
|
|
192
|
-
let &[value, tracked_ref] = track({ 0: 'x' });
|
|
193
|
-
let nested = value[0];
|
|
194
|
-
tracked_ref[0] = { 0: 'y' };
|
|
195
|
-
let next = value[0];
|
|
196
|
-
<div>{nested}{next}</div>
|
|
197
|
-
}`;
|
|
198
|
-
|
|
199
|
-
expect(() => compile(source, 'test.tsrx', { mode: 'client' })).toThrow(
|
|
200
|
-
/Use \.value or &\[\] lazy destructuring/,
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
140
|
it('throws on known tracked indexed access', () => {
|
|
205
141
|
const source = `import { track } from 'ripple';
|
|
206
142
|
function App() @{
|
|
@@ -213,7 +149,7 @@ function App() @{
|
|
|
213
149
|
}`;
|
|
214
150
|
|
|
215
151
|
expect(() => compile(source, 'test.tsrx', { mode: 'client' })).toThrow(
|
|
216
|
-
/
|
|
152
|
+
/Accessing a tracked object directly is not allowed/,
|
|
217
153
|
);
|
|
218
154
|
});
|
|
219
155
|
});
|
|
@@ -13,18 +13,18 @@ describe('compiler > attributes', () => {
|
|
|
13
13
|
|
|
14
14
|
const result = compile(source, 'test.tsrx', { mode: 'client' });
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
expect(result.code).
|
|
18
|
-
expect(result.code).
|
|
19
|
-
expect(result.code).
|
|
16
|
+
// Hyphenated prop names are quoted keys of the plain props literal.
|
|
17
|
+
expect(result.code).toContain(`'data-scope': "test"`);
|
|
18
|
+
expect(result.code).toContain(`'aria-label': "accessible"`);
|
|
19
|
+
expect(result.code).toContain('class: "valid"');
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('generates valid JavaScript for all types of hyphenated attributes', () => {
|
|
23
23
|
const testCases = [
|
|
24
|
-
{ attr: 'data-testid="value"', expected:
|
|
25
|
-
{ attr: 'aria-label="label"', expected:
|
|
26
|
-
{ attr: 'data-custom-attr="custom"', expected:
|
|
27
|
-
{ attr: 'ng-if="condition"', expected:
|
|
24
|
+
{ attr: 'data-testid="value"', expected: `'data-testid': "value"` },
|
|
25
|
+
{ attr: 'aria-label="label"', expected: `'aria-label': "label"` },
|
|
26
|
+
{ attr: 'data-custom-attr="custom"', expected: `'data-custom-attr': "custom"` },
|
|
27
|
+
{ attr: 'ng-if="condition"', expected: `'ng-if': "condition"` },
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
testCases.forEach(({ attr, expected }) => {
|
|
@@ -37,7 +37,7 @@ describe('compiler > attributes', () => {
|
|
|
37
37
|
}`;
|
|
38
38
|
|
|
39
39
|
const result = compile(source, 'test.tsrx', { mode: 'client' });
|
|
40
|
-
expect(result.code).
|
|
40
|
+
expect(result.code).toContain(expected);
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -60,15 +60,15 @@ describe('compiler > attributes', () => {
|
|
|
60
60
|
|
|
61
61
|
const result = compile(source, 'test.tsrx', { mode: 'client' });
|
|
62
62
|
|
|
63
|
-
// Valid identifiers
|
|
64
|
-
expect(result.code).
|
|
65
|
-
expect(result.code).
|
|
66
|
-
expect(result.code).
|
|
63
|
+
// Valid identifiers are plain keys
|
|
64
|
+
expect(result.code).toContain('validProp: "valid"');
|
|
65
|
+
expect(result.code).toContain('class: "valid"');
|
|
66
|
+
expect(result.code).toContain('id: "valid"');
|
|
67
67
|
|
|
68
|
-
// Invalid identifiers (with hyphens)
|
|
69
|
-
expect(result.code).
|
|
70
|
-
expect(result.code).
|
|
71
|
-
expect(result.code).
|
|
68
|
+
// Invalid identifiers (with hyphens) are quoted keys
|
|
69
|
+
expect(result.code).toContain(`'data-invalid': "invalid"`);
|
|
70
|
+
expect(result.code).toContain(`'aria-invalid': "invalid"`);
|
|
71
|
+
expect(result.code).toContain(`'custom-prop': "invalid"`);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
it('ensures generated code is syntactically valid JavaScript', () => {
|
|
@@ -83,17 +83,17 @@ describe('compiler > attributes', () => {
|
|
|
83
83
|
|
|
84
84
|
const result = compile(source, 'test.tsrx', { mode: 'client' });
|
|
85
85
|
|
|
86
|
-
// Extract the props
|
|
87
|
-
const match = result.code.match(/
|
|
86
|
+
// Extract the props literal from the generated code and test it's valid JavaScript
|
|
87
|
+
const match = result.code.match(/render_component\(Child, __anchor, (\{[\s\S]*?\})\)/);
|
|
88
88
|
expect(match).toBeTruthy();
|
|
89
89
|
|
|
90
|
-
const
|
|
90
|
+
const literal = match?.[1];
|
|
91
91
|
expect(() => {
|
|
92
92
|
// Test that the object literal is syntactically valid
|
|
93
|
-
new Function(`return ${
|
|
93
|
+
new Function(`return ${literal}`);
|
|
94
94
|
}).not.toThrow();
|
|
95
95
|
|
|
96
|
-
// Also verify it contains the expected quoted
|
|
97
|
-
expect(
|
|
96
|
+
// Also verify it contains the expected quoted key
|
|
97
|
+
expect(literal).toContain(`'data-scope': "test"`);
|
|
98
98
|
});
|
|
99
99
|
});
|