ripple 0.2.215 → 0.2.216
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 +47 -0
- package/package.json +2 -2
- package/src/compiler/phases/2-analyze/index.js +24 -0
- package/src/compiler/phases/3-transform/client/index.js +18 -3
- package/src/runtime/internal/client/template.js +1 -1
- package/tests/client/basic/basic.errors.test.ripple +66 -0
- package/tests/client/try.test.ripple +23 -0
- package/tests/hydration/compiled/client/hmr.js +86 -0
- package/tests/hydration/compiled/server/hmr.js +110 -0
- package/tests/hydration/components/hmr.ripple +35 -0
- package/tests/hydration/hmr.test.js +74 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.2.216
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#757](https://github.com/Ripple-TS/ripple/pull/757)
|
|
8
|
+
[`9fb507d`](https://github.com/Ripple-TS/ripple/commit/9fb507d76af6fd6a5c636af1976d1e03d3e869ac)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - fixes compiler error that was
|
|
10
|
+
generating async functions for call expressions inside if conditions when inside
|
|
11
|
+
async context
|
|
12
|
+
|
|
13
|
+
- [#751](https://github.com/Ripple-TS/ripple/pull/751)
|
|
14
|
+
[`e1de4bb`](https://github.com/Ripple-TS/ripple/commit/e1de4bb9df75342a693cda24d0999a423db05ec4)
|
|
15
|
+
Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Fix
|
|
16
|
+
HMR "zoom" issue when a Ripple file is changed in the dev server.
|
|
17
|
+
|
|
18
|
+
When a layout component contained children with nested `if`/`for` blocks,
|
|
19
|
+
hydration would leave `hydrate_node` pointing deep inside the layout's root
|
|
20
|
+
element (e.g. a HYDRATION_END comment inside `<main>`). The `append()`
|
|
21
|
+
function's `parentNode === dom` check only handled direct children, so it missed
|
|
22
|
+
grandchild/deeper positions and incorrectly updated the branch block's `s.end`
|
|
23
|
+
to that deep internal node.
|
|
24
|
+
|
|
25
|
+
This caused two problems on HMR re-render:
|
|
26
|
+
1. `remove_block_dom(s.start, s.end)` removed wrong elements (the deep node was
|
|
27
|
+
treated as a sibling boundary, causing removal of unrelated content including
|
|
28
|
+
the root HYDRATION_END comment).
|
|
29
|
+
2. `target = hydrate_node` (set after the initial render) became `null` or
|
|
30
|
+
pointed outside the component's region, so new content was inserted at the
|
|
31
|
+
wrong DOM location — producing a layout that appeared "zoomed" because it
|
|
32
|
+
rendered outside its CSS container context.
|
|
33
|
+
|
|
34
|
+
The fix changes the `parentNode === dom` check to `dom.contains(hydrate_node)`,
|
|
35
|
+
consistent with the `anchor === dom` branch that already used `dom.contains()`.
|
|
36
|
+
This correctly resets `hydrate_node` to `dom`'s sibling level regardless of how
|
|
37
|
+
deeply nested it was inside `dom`.
|
|
38
|
+
|
|
39
|
+
- [#764](https://github.com/Ripple-TS/ripple/pull/764)
|
|
40
|
+
[`95ea864`](https://github.com/Ripple-TS/ripple/commit/95ea8645b2cb27e2610a4ace4c8fb238c92d441a)
|
|
41
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Fixes syntax color
|
|
42
|
+
highlighting for `pending`
|
|
43
|
+
|
|
44
|
+
- Updated dependencies
|
|
45
|
+
[[`9fb507d`](https://github.com/Ripple-TS/ripple/commit/9fb507d76af6fd6a5c636af1976d1e03d3e869ac),
|
|
46
|
+
[`e1de4bb`](https://github.com/Ripple-TS/ripple/commit/e1de4bb9df75342a693cda24d0999a423db05ec4),
|
|
47
|
+
[`95ea864`](https://github.com/Ripple-TS/ripple/commit/95ea8645b2cb27e2610a4ace4c8fb238c92d441a)]:
|
|
48
|
+
- ripple@0.2.216
|
|
49
|
+
|
|
3
50
|
## 0.2.215
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.216",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -96,6 +96,6 @@
|
|
|
96
96
|
"vscode-languageserver-types": "^3.17.5"
|
|
97
97
|
},
|
|
98
98
|
"peerDependencies": {
|
|
99
|
-
"ripple": "0.2.
|
|
99
|
+
"ripple": "0.2.216"
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -1031,6 +1031,30 @@ const visitors = {
|
|
|
1031
1031
|
context.next();
|
|
1032
1032
|
},
|
|
1033
1033
|
|
|
1034
|
+
WhileStatement(node, context) {
|
|
1035
|
+
if (is_inside_component(context)) {
|
|
1036
|
+
error(
|
|
1037
|
+
'While loops are not supported in components. Move the while loop into a function.',
|
|
1038
|
+
context.state.analysis.module.filename,
|
|
1039
|
+
node,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
context.next();
|
|
1044
|
+
},
|
|
1045
|
+
|
|
1046
|
+
DoWhileStatement(node, context) {
|
|
1047
|
+
if (is_inside_component(context)) {
|
|
1048
|
+
error(
|
|
1049
|
+
'Do...while loops are not supported in components. Move the do...while loop into a function.',
|
|
1050
|
+
context.state.analysis.module.filename,
|
|
1051
|
+
node,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
context.next();
|
|
1056
|
+
},
|
|
1057
|
+
|
|
1034
1058
|
JSXElement(node, context) {
|
|
1035
1059
|
const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat');
|
|
1036
1060
|
|
|
@@ -2162,7 +2162,12 @@ const visitors = {
|
|
|
2162
2162
|
callback_body.push(b.stmt(b.call('_$_.set', b.id(info.name), b.false)));
|
|
2163
2163
|
callback_body.push(
|
|
2164
2164
|
b.if(
|
|
2165
|
-
/** @type {AST.Expression} */ (
|
|
2165
|
+
/** @type {AST.Expression} */ (
|
|
2166
|
+
context.visit(node.test, {
|
|
2167
|
+
...context.state,
|
|
2168
|
+
metadata: { ...context.state.metadata, await: false },
|
|
2169
|
+
})
|
|
2170
|
+
),
|
|
2166
2171
|
b.stmt(b.call('_$_.set', b.id(info.name), b.true)),
|
|
2167
2172
|
),
|
|
2168
2173
|
);
|
|
@@ -2170,7 +2175,12 @@ const visitors = {
|
|
|
2170
2175
|
callback_body.push(b.stmt(b.assignment('=', b.id(info.name), b.false)));
|
|
2171
2176
|
callback_body.push(
|
|
2172
2177
|
b.if(
|
|
2173
|
-
/** @type {AST.Expression} */ (
|
|
2178
|
+
/** @type {AST.Expression} */ (
|
|
2179
|
+
context.visit(node.test, {
|
|
2180
|
+
...context.state,
|
|
2181
|
+
metadata: { ...context.state.metadata, await: false },
|
|
2182
|
+
})
|
|
2183
|
+
),
|
|
2174
2184
|
b.stmt(b.assignment('=', b.id(info.name), b.true)),
|
|
2175
2185
|
),
|
|
2176
2186
|
);
|
|
@@ -2237,7 +2247,12 @@ const visitors = {
|
|
|
2237
2247
|
|
|
2238
2248
|
callback_body.push(
|
|
2239
2249
|
b.if(
|
|
2240
|
-
/** @type {AST.Expression} */ (
|
|
2250
|
+
/** @type {AST.Expression} */ (
|
|
2251
|
+
context.visit(node.test, {
|
|
2252
|
+
...context.state,
|
|
2253
|
+
metadata: { ...context.state.metadata, await: false },
|
|
2254
|
+
})
|
|
2255
|
+
),
|
|
2241
2256
|
b.stmt(b.call(b.id('__render'), b.id(consequent_id))),
|
|
2242
2257
|
alternate_id
|
|
2243
2258
|
? b.stmt(
|
|
@@ -165,7 +165,7 @@ export function append(anchor, dom, skip_advance) {
|
|
|
165
165
|
// But if the cursor is already at dom's sibling level (e.g. because
|
|
166
166
|
// nested control flow blocks advanced it past dom via sibling traversal),
|
|
167
167
|
// pop() would incorrectly reset backwards — so we skip it.
|
|
168
|
-
if (hydrate_node
|
|
168
|
+
if (hydrate_node !== null && hydrate_node !== dom && dom.contains(hydrate_node)) {
|
|
169
169
|
pop(dom);
|
|
170
170
|
} else if (hydrate_node !== dom) {
|
|
171
171
|
// Cursor has advanced past dom via sibling traversal (due to nested
|
|
@@ -173,4 +173,70 @@ describe('basic client > errors', () => {
|
|
|
173
173
|
compile(code, 'test.ripple', { mode: 'client' });
|
|
174
174
|
}).toThrow('`await` is not allowed in client-side control-flow statements');
|
|
175
175
|
});
|
|
176
|
+
|
|
177
|
+
it('should throw error for while loop inside a component', () => {
|
|
178
|
+
const code = `
|
|
179
|
+
export default component App() {
|
|
180
|
+
let i = 0;
|
|
181
|
+
while (i < 10) {
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
<div>{i}</div>
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
expect(() => {
|
|
188
|
+
compile(code, 'test.ripple');
|
|
189
|
+
}).toThrow('While loops are not supported in components.');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should throw error for do...while loop inside a component', () => {
|
|
193
|
+
const code = `
|
|
194
|
+
export default component App() {
|
|
195
|
+
let i = 0;
|
|
196
|
+
do {
|
|
197
|
+
i++;
|
|
198
|
+
} while (i < 10);
|
|
199
|
+
<div>{i}</div>
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
expect(() => {
|
|
203
|
+
compile(code, 'test.ripple');
|
|
204
|
+
}).toThrow('Do...while loops are not supported in components.');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should not throw error for while loop inside a function within a component', () => {
|
|
208
|
+
const code = `
|
|
209
|
+
export default component App() {
|
|
210
|
+
const compute = () => {
|
|
211
|
+
let i = 0;
|
|
212
|
+
while (i < 10) {
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
return i;
|
|
216
|
+
};
|
|
217
|
+
<div>{compute()}</div>
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
expect(() => {
|
|
221
|
+
compile(code, 'test.ripple');
|
|
222
|
+
}).not.toThrow();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should not throw error for do...while loop inside a function within a component', () => {
|
|
226
|
+
const code = `
|
|
227
|
+
export default component App() {
|
|
228
|
+
const compute = () => {
|
|
229
|
+
let i = 0;
|
|
230
|
+
do {
|
|
231
|
+
i++;
|
|
232
|
+
} while (i < 10);
|
|
233
|
+
return i;
|
|
234
|
+
};
|
|
235
|
+
<div>{compute()}</div>
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
expect(() => {
|
|
239
|
+
compile(code, 'test.ripple');
|
|
240
|
+
}).not.toThrow();
|
|
241
|
+
});
|
|
176
242
|
});
|
|
@@ -141,4 +141,27 @@ describe('try block', () => {
|
|
|
141
141
|
expect(listItems.length).toBe(3);
|
|
142
142
|
},
|
|
143
143
|
);
|
|
144
|
+
|
|
145
|
+
it('if test condition does not become async within try/pending', async () => {
|
|
146
|
+
component App() {
|
|
147
|
+
try {
|
|
148
|
+
let items = await Promise.resolve(['apple', 'banana', 'cherry']);
|
|
149
|
+
|
|
150
|
+
if (items.includes('not-in-list')) {
|
|
151
|
+
<p>{'not-in-list is in the list!'}</p>
|
|
152
|
+
} else {
|
|
153
|
+
<p>{'not-in-list is not in the list.'}</p>
|
|
154
|
+
}
|
|
155
|
+
} pending {
|
|
156
|
+
<p>{'loading...'}</p>
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render(App);
|
|
161
|
+
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
163
|
+
flushSync();
|
|
164
|
+
|
|
165
|
+
expect(container.innerHTML).toContain('not-in-list is not in the list.');
|
|
166
|
+
});
|
|
144
167
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import * as _$_ from 'ripple/internal/client';
|
|
3
|
+
|
|
4
|
+
var root = _$_.template(`<div class="layout"><nav class="nav">Navigation</nav><main class="main"><!></main></div>`, 0);
|
|
5
|
+
var root_2 = _$_.template(`<p class="text">Hello world</p>`, 0);
|
|
6
|
+
var root_1 = _$_.template(`<div class="content"><!></div>`, 0);
|
|
7
|
+
var root_4 = _$_.template(`<!>`, 1, 1);
|
|
8
|
+
var root_3 = _$_.template(`<!>`, 1, 1);
|
|
9
|
+
|
|
10
|
+
import { track } from 'ripple';
|
|
11
|
+
|
|
12
|
+
export function Layout(__anchor, __props, __block) {
|
|
13
|
+
_$_.push_component();
|
|
14
|
+
|
|
15
|
+
var div_1 = root();
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
var nav_1 = _$_.child(div_1);
|
|
19
|
+
var main_1 = _$_.sibling(nav_1);
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
var node = _$_.child(main_1);
|
|
23
|
+
|
|
24
|
+
_$_.composite(() => __props.children, node, {});
|
|
25
|
+
_$_.pop(main_1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_$_.append(__anchor, div_1);
|
|
30
|
+
_$_.pop_component();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Content(__anchor, _, __block) {
|
|
34
|
+
_$_.push_component();
|
|
35
|
+
|
|
36
|
+
let visible = track(true, void 0, void 0, __block);
|
|
37
|
+
var div_2 = root_1();
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
var node_1 = _$_.child(div_2);
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
var consequent = (__anchor) => {
|
|
44
|
+
var p_1 = root_2();
|
|
45
|
+
|
|
46
|
+
_$_.append(__anchor, p_1);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
_$_.if(node_1, (__render) => {
|
|
50
|
+
if (_$_.get(visible)) __render(consequent);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_$_.pop(div_2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_$_.append(__anchor, div_2);
|
|
58
|
+
_$_.pop_component();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function LayoutWithContent(__anchor, _, __block) {
|
|
62
|
+
_$_.push_component();
|
|
63
|
+
|
|
64
|
+
var fragment = root_3();
|
|
65
|
+
var node_2 = _$_.first_child_frag(fragment);
|
|
66
|
+
|
|
67
|
+
Layout(
|
|
68
|
+
node_2,
|
|
69
|
+
{
|
|
70
|
+
children(__anchor, _, __block) {
|
|
71
|
+
_$_.push_component();
|
|
72
|
+
|
|
73
|
+
var fragment_1 = root_4();
|
|
74
|
+
var node_3 = _$_.first_child_frag(fragment_1);
|
|
75
|
+
|
|
76
|
+
Content(node_3, {}, _$_.active_block);
|
|
77
|
+
_$_.append(__anchor, fragment_1);
|
|
78
|
+
_$_.pop_component();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
_$_.active_block
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
_$_.append(__anchor, fragment);
|
|
85
|
+
_$_.pop_component();
|
|
86
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import * as _$_ from 'ripple/internal/server';
|
|
3
|
+
|
|
4
|
+
import { track } from 'ripple/server';
|
|
5
|
+
|
|
6
|
+
export async function Layout(__output, { children }) {
|
|
7
|
+
return _$_.async(async () => {
|
|
8
|
+
_$_.push_component();
|
|
9
|
+
__output.push('<div');
|
|
10
|
+
__output.push(' class="layout"');
|
|
11
|
+
__output.push('>');
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
__output.push('<nav');
|
|
15
|
+
__output.push(' class="nav"');
|
|
16
|
+
__output.push('>');
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
__output.push('Navigation');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
__output.push('</nav>');
|
|
23
|
+
__output.push('<main');
|
|
24
|
+
__output.push(' class="main"');
|
|
25
|
+
__output.push('>');
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
{
|
|
29
|
+
const comp = children;
|
|
30
|
+
const args = [__output, {}];
|
|
31
|
+
|
|
32
|
+
if (comp?.async) {
|
|
33
|
+
await comp(...args);
|
|
34
|
+
} else if (comp) {
|
|
35
|
+
comp(...args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
__output.push('</main>');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
__output.push('</div>');
|
|
44
|
+
_$_.pop_component();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Layout.async = true;
|
|
49
|
+
|
|
50
|
+
export function Content(__output) {
|
|
51
|
+
_$_.push_component();
|
|
52
|
+
|
|
53
|
+
let visible = track(true);
|
|
54
|
+
|
|
55
|
+
__output.push('<div');
|
|
56
|
+
__output.push(' class="content"');
|
|
57
|
+
__output.push('>');
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
__output.push('<!--[-->');
|
|
61
|
+
|
|
62
|
+
if (_$_.get(visible)) {
|
|
63
|
+
__output.push('<p');
|
|
64
|
+
__output.push(' class="text"');
|
|
65
|
+
__output.push('>');
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
__output.push('Hello world');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
__output.push('</p>');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
__output.push('<!--]-->');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
__output.push('</div>');
|
|
78
|
+
_$_.pop_component();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function LayoutWithContent(__output) {
|
|
82
|
+
_$_.push_component();
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
const comp = Layout;
|
|
86
|
+
|
|
87
|
+
const args = [
|
|
88
|
+
__output,
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
children: function children(__output) {
|
|
92
|
+
_$_.push_component();
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
const comp = Content;
|
|
96
|
+
const args = [__output, {}];
|
|
97
|
+
|
|
98
|
+
comp(...args);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_$_.pop_component();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
comp(...args);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_$_.pop_component();
|
|
110
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Components for testing HMR re-render after hydration.
|
|
2
|
+
// The key scenario: a layout wrapper whose root element contains children
|
|
3
|
+
// that use if/for blocks. After hydration, hydrate_node can be left pointing
|
|
4
|
+
// deep inside the layout's root element (from nested block processing), which
|
|
5
|
+
// previously caused branch.s.end to be set incorrectly and target to be null.
|
|
6
|
+
import { track } from 'ripple';
|
|
7
|
+
|
|
8
|
+
// A layout component similar to docs-layout: a root div wrapping child components
|
|
9
|
+
// where the children contain conditional content (if blocks)
|
|
10
|
+
export component Layout({ children }: { children: any }) {
|
|
11
|
+
<div class="layout">
|
|
12
|
+
<nav class="nav">{'Navigation'}</nav>
|
|
13
|
+
<main class="main">
|
|
14
|
+
<children />
|
|
15
|
+
</main>
|
|
16
|
+
</div>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A child component with an if block (the key source of deep hydrate_node)
|
|
20
|
+
export component Content() {
|
|
21
|
+
let visible = track(true);
|
|
22
|
+
|
|
23
|
+
<div class="content">
|
|
24
|
+
if (@visible) {
|
|
25
|
+
<p class="text">{'Hello world'}</p>
|
|
26
|
+
}
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The top-level component combining Layout + Content (mimics docs layout + page)
|
|
31
|
+
export component LayoutWithContent() {
|
|
32
|
+
<Layout>
|
|
33
|
+
<Content />
|
|
34
|
+
</Layout>
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync } from 'ripple';
|
|
3
|
+
import { hydrateComponent, container } from '../setup-hydration.js';
|
|
4
|
+
import { hmr } from '../../src/runtime/internal/client/hmr.js';
|
|
5
|
+
import { HMR } from '../../src/runtime/internal/client/constants.js';
|
|
6
|
+
|
|
7
|
+
// Import server-compiled components
|
|
8
|
+
import * as ServerComponents from './compiled/server/hmr.js';
|
|
9
|
+
// Import client-compiled components
|
|
10
|
+
import * as ClientComponents from './compiled/client/hmr.js';
|
|
11
|
+
|
|
12
|
+
describe('hydration > HMR re-render', () => {
|
|
13
|
+
it('re-renders layout component correctly after hydration (no zoom/displacement)', async () => {
|
|
14
|
+
// Hydrate the layout+content component
|
|
15
|
+
await hydrateComponent(ServerComponents.LayoutWithContent, ClientComponents.LayoutWithContent);
|
|
16
|
+
|
|
17
|
+
// Verify initial state
|
|
18
|
+
expect(container.querySelector('.layout')).not.toBeNull();
|
|
19
|
+
expect(container.querySelector('.nav')?.textContent).toBe('Navigation');
|
|
20
|
+
expect(container.querySelector('.main')).not.toBeNull();
|
|
21
|
+
expect(container.querySelector('.content')).not.toBeNull();
|
|
22
|
+
expect(container.querySelector('.text')?.textContent).toBe('Hello world');
|
|
23
|
+
|
|
24
|
+
// Wrap the layout component with HMR (simulates what the compiler does in dev mode)
|
|
25
|
+
const layout_hmr = hmr(ClientComponents.Layout);
|
|
26
|
+
|
|
27
|
+
// Create an "updated" version of the component (simulates saving the file)
|
|
28
|
+
function UpdatedLayout(anchor, props, block) {
|
|
29
|
+
return ClientComponents.Layout(anchor, props, block);
|
|
30
|
+
}
|
|
31
|
+
const incoming = hmr(UpdatedLayout);
|
|
32
|
+
|
|
33
|
+
// Simulate calling wrapper() to establish the HMR instance
|
|
34
|
+
// (In practice the component is already mounted via hydrateComponent above,
|
|
35
|
+
// but we test the HMR update mechanism directly)
|
|
36
|
+
const update_fn = layout_hmr[HMR].update;
|
|
37
|
+
|
|
38
|
+
// The update should not throw
|
|
39
|
+
expect(() => {
|
|
40
|
+
update_fn(incoming);
|
|
41
|
+
}).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('layout component remains inside container after hydration', async () => {
|
|
45
|
+
await hydrateComponent(ServerComponents.LayoutWithContent, ClientComponents.LayoutWithContent);
|
|
46
|
+
|
|
47
|
+
// The layout div must be inside the container, not displaced
|
|
48
|
+
const layout = container.querySelector('.layout');
|
|
49
|
+
expect(layout).not.toBeNull();
|
|
50
|
+
expect(container.contains(layout)).toBe(true);
|
|
51
|
+
|
|
52
|
+
// The main content must be inside the layout
|
|
53
|
+
const main = layout?.querySelector('.main');
|
|
54
|
+
expect(main).not.toBeNull();
|
|
55
|
+
|
|
56
|
+
// The text content must be present and correct
|
|
57
|
+
expect(container.querySelector('.text')?.textContent).toBe('Hello world');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('hydrates layout with nested if block without corrupting branch state', async () => {
|
|
61
|
+
await hydrateComponent(ServerComponents.LayoutWithContent, ClientComponents.LayoutWithContent);
|
|
62
|
+
|
|
63
|
+
// After hydration, the conditional content must still be visible
|
|
64
|
+
expect(container.querySelector('.text')).not.toBeNull();
|
|
65
|
+
expect(container.querySelector('.text')?.textContent).toBe('Hello world');
|
|
66
|
+
|
|
67
|
+
// All structural elements must be present in the correct hierarchy
|
|
68
|
+
const layout = container.querySelector('.layout');
|
|
69
|
+
expect(layout).not.toBeNull();
|
|
70
|
+
expect(layout?.querySelector('.nav')).not.toBeNull();
|
|
71
|
+
expect(layout?.querySelector('.main')).not.toBeNull();
|
|
72
|
+
expect(layout?.querySelector('.content')).not.toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|