@sigil-dev/compiler 0.9.4 → 0.9.6
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/index.ts +4 -4
- package/jsr.json +8 -8
- package/package.json +2 -1
- package/src/babel/handlers/dom/derived.ts +62 -62
- package/src/babel/handlers/dom/effect.ts +15 -15
- package/src/babel/handlers/dom/globals.ts +105 -105
- package/src/babel/handlers/dom/state.ts +107 -107
- package/src/babel/handlers/ssr/derived.ts +27 -27
- package/src/babel/handlers/ssr/state.ts +11 -11
- package/src/babel/index.ts +9 -0
- package/src/babel/jsx/anchor-mount.ts +24 -1
- package/src/babel/jsx/children.ts +53 -69
- package/src/babel/jsx/element.ts +76 -50
- package/src/babel/jsx/fragment.ts +8 -31
- package/src/babel/jsx/index.ts +2 -2
- package/src/babel/jsx/keyed-list.ts +309 -309
- package/src/babel/jsx/ssr.ts +351 -324
- package/src/babel/jsx/text-node.ts +192 -192
- package/src/babel/jsx/utils.ts +179 -179
- package/src/babel/util/bind.ts +139 -139
- package/src/babel/util/css.ts +151 -151
- package/src/babel/util/dead-code.ts +28 -28
- package/src/babel/util/helpers.ts +46 -46
- package/src/babel/util/magic.ts +11 -11
- package/src/bun-plugin.ts +92 -92
- package/src/vite/index.ts +58 -58
- package/test/async-components.test.ts +59 -59
- package/test/components.test.ts +129 -129
- package/test/containsSignal.test.ts +288 -288
- package/test/fragments.test.ts +90 -90
- package/test/helpers/transform.ts +45 -45
- package/test/hydration.test.ts +130 -1
- package/test/jsx.test.ts +227 -225
- package/test/keyed-lists.test.ts +126 -126
- package/test/reactivity.test.ts +225 -225
- package/test/scoped-css.test.ts +129 -129
- package/test/ssr.test.ts +130 -130
- package/test/style-object.test.ts +13 -0
- package/test/tree-shaking.test.ts +78 -78
- package/tsconfig.json +30 -30
package/test/fragments.test.ts
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { transform } from "./helpers/transform.ts";
|
|
3
|
-
|
|
4
|
-
describe("Fragments", () => {
|
|
5
|
-
test("empty fragment", () => {
|
|
6
|
-
const result = transform(`const el = <></>;`);
|
|
7
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("fragment with elements", () => {
|
|
11
|
-
const result = transform(`const el = <><span>A</span><span>B</span></>;`);
|
|
12
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
13
|
-
expect(result).toContain('document.createElement("span")');
|
|
14
|
-
expect(result).toContain(".append(");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("fragment in native parent appends all nodes", () => {
|
|
18
|
-
const result = transform(
|
|
19
|
-
`const el = <div><><span>A</span><span>B</span></></div>;`,
|
|
20
|
-
);
|
|
21
|
-
expect(result).toContain('document.createElement("div")');
|
|
22
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
23
|
-
expect(result).toContain(".append(");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("fragment with mixed children", () => {
|
|
27
|
-
const result = transform(`const el = <>Hello<span>World</span></>;`);
|
|
28
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
29
|
-
expect(result).toContain('"Hello"');
|
|
30
|
-
expect(result).toContain('document.createElement("span")');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("fragment with reactive children", () => {
|
|
34
|
-
const result = transform(`
|
|
35
|
-
let count = $state(0);
|
|
36
|
-
const el = <>{count}</>;
|
|
37
|
-
`);
|
|
38
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
39
|
-
expect(result).toContain("createEffect(");
|
|
40
|
-
expect(result).toContain("count()");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("fragment returns a single variable", () => {
|
|
44
|
-
const result = transform(`const el = <><div /></>;`);
|
|
45
|
-
// Should return a single fragment variable, not an array
|
|
46
|
-
expect(result).toMatch(/_el\d+ = document\.createDocumentFragment\(\)/);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("nested fragments", () => {
|
|
50
|
-
const result = transform(
|
|
51
|
-
`const el = <><><span>A</span></><span>B</span></>;`,
|
|
52
|
-
);
|
|
53
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("fragment with text only", () => {
|
|
57
|
-
const result = transform(`const el = <>Hello</>;`);
|
|
58
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
59
|
-
expect(result).toContain('"Hello"');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("DOM order: fragment children interleaved with siblings", () => {
|
|
63
|
-
const result = transform(`
|
|
64
|
-
const el = (
|
|
65
|
-
<div>
|
|
66
|
-
<><span>a</span><span>b</span></>
|
|
67
|
-
<span>c</span>
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
`);
|
|
71
|
-
// Compiled output:
|
|
72
|
-
// div = createElement("div")
|
|
73
|
-
// frag = createDocumentFragment()
|
|
74
|
-
// spanA = createElement("span") -> append("a") -> frag.append(spanA)
|
|
75
|
-
// spanB = createElement("span") -> append("b") -> frag.append(spanB)
|
|
76
|
-
// div.append(frag) <- browser inlines fragment children
|
|
77
|
-
// spanC = createElement("span") -> append("c") -> div.append(spanC)
|
|
78
|
-
// DOM order: a, b, c (no fragment marker)
|
|
79
|
-
expect(result).toContain("document.createDocumentFragment()");
|
|
80
|
-
// Fragment is appended to div (not span c before fragment)
|
|
81
|
-
// div.append(frag) should come before div.append(spanC)
|
|
82
|
-
const fragAppendIdx = result.indexOf(".append(_el1)");
|
|
83
|
-
const spanCAppendIdx = result.indexOf("_el0.append(_el4)");
|
|
84
|
-
expect(fragAppendIdx).toBeLessThan(spanCAppendIdx);
|
|
85
|
-
// Span a and b go into fragment, span c goes into div directly
|
|
86
|
-
expect(result).toContain('createTextNode("a")');
|
|
87
|
-
expect(result).toContain('createTextNode("b")');
|
|
88
|
-
expect(result).toContain('createTextNode("c")');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { transform } from "./helpers/transform.ts";
|
|
3
|
+
|
|
4
|
+
describe("Fragments", () => {
|
|
5
|
+
test("empty fragment", () => {
|
|
6
|
+
const result = transform(`const el = <></>;`);
|
|
7
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("fragment with elements", () => {
|
|
11
|
+
const result = transform(`const el = <><span>A</span><span>B</span></>;`);
|
|
12
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
13
|
+
expect(result).toContain('document.createElement("span")');
|
|
14
|
+
expect(result).toContain(".append(");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("fragment in native parent appends all nodes", () => {
|
|
18
|
+
const result = transform(
|
|
19
|
+
`const el = <div><><span>A</span><span>B</span></></div>;`,
|
|
20
|
+
);
|
|
21
|
+
expect(result).toContain('document.createElement("div")');
|
|
22
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
23
|
+
expect(result).toContain(".append(");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("fragment with mixed children", () => {
|
|
27
|
+
const result = transform(`const el = <>Hello<span>World</span></>;`);
|
|
28
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
29
|
+
expect(result).toContain('"Hello"');
|
|
30
|
+
expect(result).toContain('document.createElement("span")');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("fragment with reactive children", () => {
|
|
34
|
+
const result = transform(`
|
|
35
|
+
let count = $state(0);
|
|
36
|
+
const el = <>{count}</>;
|
|
37
|
+
`);
|
|
38
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
39
|
+
expect(result).toContain("createEffect(");
|
|
40
|
+
expect(result).toContain("count()");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("fragment returns a single variable", () => {
|
|
44
|
+
const result = transform(`const el = <><div /></>;`);
|
|
45
|
+
// Should return a single fragment variable, not an array
|
|
46
|
+
expect(result).toMatch(/_el\d+ = document\.createDocumentFragment\(\)/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("nested fragments", () => {
|
|
50
|
+
const result = transform(
|
|
51
|
+
`const el = <><><span>A</span></><span>B</span></>;`,
|
|
52
|
+
);
|
|
53
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("fragment with text only", () => {
|
|
57
|
+
const result = transform(`const el = <>Hello</>;`);
|
|
58
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
59
|
+
expect(result).toContain('"Hello"');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("DOM order: fragment children interleaved with siblings", () => {
|
|
63
|
+
const result = transform(`
|
|
64
|
+
const el = (
|
|
65
|
+
<div>
|
|
66
|
+
<><span>a</span><span>b</span></>
|
|
67
|
+
<span>c</span>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
`);
|
|
71
|
+
// Compiled output:
|
|
72
|
+
// div = createElement("div")
|
|
73
|
+
// frag = createDocumentFragment()
|
|
74
|
+
// spanA = createElement("span") -> append("a") -> frag.append(spanA)
|
|
75
|
+
// spanB = createElement("span") -> append("b") -> frag.append(spanB)
|
|
76
|
+
// div.append(frag) <- browser inlines fragment children
|
|
77
|
+
// spanC = createElement("span") -> append("c") -> div.append(spanC)
|
|
78
|
+
// DOM order: a, b, c (no fragment marker)
|
|
79
|
+
expect(result).toContain("document.createDocumentFragment()");
|
|
80
|
+
// Fragment is appended to div (not span c before fragment)
|
|
81
|
+
// div.append(frag) should come before div.append(spanC)
|
|
82
|
+
const fragAppendIdx = result.indexOf(".append(_el1)");
|
|
83
|
+
const spanCAppendIdx = result.indexOf("_el0.append(_el4)");
|
|
84
|
+
expect(fragAppendIdx).toBeLessThan(spanCAppendIdx);
|
|
85
|
+
// Span a and b go into fragment, span c goes into div directly
|
|
86
|
+
expect(result).toContain('createTextNode("a")');
|
|
87
|
+
expect(result).toContain('createTextNode("b")');
|
|
88
|
+
expect(result).toContain('createTextNode("c")');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { transformSync } from "@babel/core";
|
|
2
|
-
import sigilPlugin from "../../src/babel/index.ts";
|
|
3
|
-
|
|
4
|
-
export function transform(code: string, hash?: string) {
|
|
5
|
-
const plugins: any[] = ["@babel/plugin-syntax-jsx"];
|
|
6
|
-
if (hash) {
|
|
7
|
-
plugins.push([sigilPlugin, { hash }]);
|
|
8
|
-
} else {
|
|
9
|
-
plugins.push(sigilPlugin);
|
|
10
|
-
}
|
|
11
|
-
return (
|
|
12
|
-
transformSync(code, {
|
|
13
|
-
plugins,
|
|
14
|
-
filename: "test.tsx",
|
|
15
|
-
})?.code ?? ""
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function transformSSR(code: string) {
|
|
20
|
-
return (
|
|
21
|
-
transformSync(code, {
|
|
22
|
-
plugins: ["@babel/plugin-syntax-jsx", [sigilPlugin, { mode: "ssr" }]],
|
|
23
|
-
filename: "test.tsx",
|
|
24
|
-
})?.code ?? ""
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
export function transformDOM(code: string, hash?: string) {
|
|
28
|
-
return (
|
|
29
|
-
transformSync(code, {
|
|
30
|
-
plugins: [
|
|
31
|
-
"@babel/plugin-syntax-jsx",
|
|
32
|
-
[sigilPlugin, { mode: "dom", hash }],
|
|
33
|
-
],
|
|
34
|
-
filename: "test.tsx",
|
|
35
|
-
})?.code ?? ""
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
export function transformAsync(code: string) {
|
|
39
|
-
return (
|
|
40
|
-
transformSync(code, {
|
|
41
|
-
plugins: ["@babel/plugin-syntax-jsx", [sigilPlugin, { mode: "dom" }]],
|
|
42
|
-
filename: "test.tsx",
|
|
43
|
-
})?.code ?? ""
|
|
44
|
-
);
|
|
45
|
-
}
|
|
1
|
+
import { transformSync } from "@babel/core";
|
|
2
|
+
import sigilPlugin from "../../src/babel/index.ts";
|
|
3
|
+
|
|
4
|
+
export function transform(code: string, hash?: string) {
|
|
5
|
+
const plugins: any[] = ["@babel/plugin-syntax-jsx"];
|
|
6
|
+
if (hash) {
|
|
7
|
+
plugins.push([sigilPlugin, { hash }]);
|
|
8
|
+
} else {
|
|
9
|
+
plugins.push(sigilPlugin);
|
|
10
|
+
}
|
|
11
|
+
return (
|
|
12
|
+
transformSync(code, {
|
|
13
|
+
plugins,
|
|
14
|
+
filename: "test.tsx",
|
|
15
|
+
})?.code ?? ""
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function transformSSR(code: string) {
|
|
20
|
+
return (
|
|
21
|
+
transformSync(code, {
|
|
22
|
+
plugins: ["@babel/plugin-syntax-jsx", [sigilPlugin, { mode: "ssr" }]],
|
|
23
|
+
filename: "test.tsx",
|
|
24
|
+
})?.code ?? ""
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
export function transformDOM(code: string, hash?: string) {
|
|
28
|
+
return (
|
|
29
|
+
transformSync(code, {
|
|
30
|
+
plugins: [
|
|
31
|
+
"@babel/plugin-syntax-jsx",
|
|
32
|
+
[sigilPlugin, { mode: "dom", hash }],
|
|
33
|
+
],
|
|
34
|
+
filename: "test.tsx",
|
|
35
|
+
})?.code ?? ""
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
export function transformAsync(code: string) {
|
|
39
|
+
return (
|
|
40
|
+
transformSync(code, {
|
|
41
|
+
plugins: ["@babel/plugin-syntax-jsx", [sigilPlugin, { mode: "dom" }]],
|
|
42
|
+
filename: "test.tsx",
|
|
43
|
+
})?.code ?? ""
|
|
44
|
+
);
|
|
45
|
+
}
|
package/test/hydration.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { transformSync } from "@babel/core";
|
|
3
|
+
import * as runtime from "../../runtime/index.ts";
|
|
3
4
|
import sigilPlugin from "../src/babel/index.ts";
|
|
4
5
|
|
|
5
6
|
function transform(
|
|
@@ -16,6 +17,23 @@ function transform(
|
|
|
16
17
|
return result!.code!;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate compiled output and return its exported `App`. SSR output carries
|
|
22
|
+
* its own `__h`/`__e` helpers; hydrate output imports from the runtime, which
|
|
23
|
+
* is injected as plain parameters instead.
|
|
24
|
+
*/
|
|
25
|
+
function evalApp(code: string): () => unknown {
|
|
26
|
+
const body = code
|
|
27
|
+
.replace(/import\s*\{[^}]*\}\s*from\s*["']@sigil-dev\/runtime["'];?/g, "")
|
|
28
|
+
.replace(/^export\s+/gm, "");
|
|
29
|
+
const factory = new Function(
|
|
30
|
+
"document",
|
|
31
|
+
...Object.keys(runtime),
|
|
32
|
+
`${body};\nreturn App;`,
|
|
33
|
+
);
|
|
34
|
+
return factory(globalThis.document, ...Object.values(runtime));
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
describe("Hydration", () => {
|
|
20
38
|
test("element uses claim instead of createElement", () => {
|
|
21
39
|
const code = transform(`const el = <div></div>`, { mode: "hydrate" });
|
|
@@ -112,7 +130,7 @@ describe("Hydration", () => {
|
|
|
112
130
|
test("imports claim and claimText from @sigil-dev/runtime", () => {
|
|
113
131
|
const code = transform(`const el = <div></div>`, { mode: "hydrate" });
|
|
114
132
|
expect(code).toContain(
|
|
115
|
-
"createSignal, createRawSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, insert, applyDirective, getHydrationNodes, pushHydrationNodes, popHydrationNodes, snapshot, tracking, untrack, createRoot, createEffectPre, createInspectEffect, withEffectScope, mountTransition, removeAnimated",
|
|
133
|
+
"createSignal, createRawSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, hydrateSlot, insert, applyDirective, getHydrationNodes, pushHydrationNodes, popHydrationNodes, snapshot, tracking, untrack, createRoot, createEffectPre, createInspectEffect, withEffectScope, mountTransition, removeAnimated",
|
|
116
134
|
);
|
|
117
135
|
});
|
|
118
136
|
|
|
@@ -144,4 +162,115 @@ describe("Hydration", () => {
|
|
|
144
162
|
// Inner div uses claim
|
|
145
163
|
expect(code).toContain('claim(__nodes, "div")');
|
|
146
164
|
});
|
|
165
|
+
|
|
166
|
+
test("component children are deferred thunks in hydrate mode", () => {
|
|
167
|
+
const code = transform(
|
|
168
|
+
`const Wrap = ({ children }) => <div>{children}</div>;
|
|
169
|
+
const el = <Wrap><span>Hi</span></Wrap>`,
|
|
170
|
+
{ mode: "hydrate" },
|
|
171
|
+
);
|
|
172
|
+
// children must not be evaluated at the call site — they claim from the
|
|
173
|
+
// component's own slot, which the caller cannot see
|
|
174
|
+
expect(code).toMatch(/children:\s*\[\(\)\s*=>\s*\{/);
|
|
175
|
+
// the {children} site consumes the SSR slot and hydrates against it
|
|
176
|
+
expect(code).toContain("hydrateSlot(_el1, _el0, () => children)");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("dom mode still evaluates component children eagerly", () => {
|
|
180
|
+
const code = transform(
|
|
181
|
+
`const Wrap = ({ children }) => <div>{children}</div>;
|
|
182
|
+
const el = <Wrap><span>Hi</span></Wrap>`,
|
|
183
|
+
{ mode: "dom" },
|
|
184
|
+
);
|
|
185
|
+
expect(code).toMatch(/children:\s*\[\(\(\)\s*=>\s*\{/);
|
|
186
|
+
expect(code).not.toContain("hydrateSlot(");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("hydrating a component with children leaves the SSR DOM untouched", () => {
|
|
190
|
+
// Regression: children were claimed from the caller's pool, so a nested
|
|
191
|
+
// component found no match, built a fresh subtree, and the sibling
|
|
192
|
+
// element stole the wrapper's own SSR root — every child rendered twice.
|
|
193
|
+
const src = `
|
|
194
|
+
function Wrap({ children }) { return <div class="wrap">{children}</div>; }
|
|
195
|
+
function Item({ label }) { return <p class="item">{label}</p>; }
|
|
196
|
+
export const App = () => (
|
|
197
|
+
<section class="sec">
|
|
198
|
+
<Wrap>
|
|
199
|
+
<Item label="alpha" />
|
|
200
|
+
<div class="grid">grid</div>
|
|
201
|
+
</Wrap>
|
|
202
|
+
</section>
|
|
203
|
+
);
|
|
204
|
+
`;
|
|
205
|
+
const html = String(evalApp(transform(src, { mode: "ssr" }))());
|
|
206
|
+
const host = document.createElement("div");
|
|
207
|
+
host.innerHTML = html;
|
|
208
|
+
|
|
209
|
+
const app = evalApp(transform(src, { mode: "hydrate" }));
|
|
210
|
+
runtime.pushHydrationNodes(Array.from(host.childNodes));
|
|
211
|
+
app();
|
|
212
|
+
runtime.popHydrationNodes();
|
|
213
|
+
|
|
214
|
+
expect(host.querySelectorAll("div.wrap").length).toBe(1);
|
|
215
|
+
expect(host.querySelectorAll("p.item").length).toBe(1);
|
|
216
|
+
expect(host.querySelectorAll("div.grid").length).toBe(1);
|
|
217
|
+
// perfect hydration: claims only, no re-render
|
|
218
|
+
expect(host.innerHTML).toBe(html);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("component children render once on an empty pool (SPA navigation)", () => {
|
|
222
|
+
const src = `
|
|
223
|
+
function Wrap({ children }) { return <div class="wrap">{children}</div>; }
|
|
224
|
+
function Item({ label }) { return <p class="item">{label}</p>; }
|
|
225
|
+
export const App = () => (
|
|
226
|
+
<section class="sec">
|
|
227
|
+
<Wrap>
|
|
228
|
+
<Item label="alpha" />
|
|
229
|
+
</Wrap>
|
|
230
|
+
</section>
|
|
231
|
+
);
|
|
232
|
+
`;
|
|
233
|
+
const app = evalApp(transform(src, { mode: "hydrate" }));
|
|
234
|
+
const host = document.createElement("div");
|
|
235
|
+
runtime.pushHydrationNodes([]);
|
|
236
|
+
runtime.insert(host, app());
|
|
237
|
+
runtime.popHydrationNodes();
|
|
238
|
+
|
|
239
|
+
expect(host.querySelectorAll("div.wrap").length).toBe(1);
|
|
240
|
+
expect(host.querySelectorAll("p.item").length).toBe(1);
|
|
241
|
+
expect(host.querySelector("p.item")?.textContent).toBe("alpha");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a static list inside a slot hydrates its SSR items in place", () => {
|
|
245
|
+
// The slot value must stay lazy: evaluated as a call argument the map
|
|
246
|
+
// bodies claimed from the enclosing pool, missed the SSR items and
|
|
247
|
+
// appended fresh copies next to them.
|
|
248
|
+
const src = `
|
|
249
|
+
function Wrap({ children }) { return <div class="wrap">{children}</div>; }
|
|
250
|
+
export const App = () => (
|
|
251
|
+
<section class="sec">
|
|
252
|
+
<Wrap>
|
|
253
|
+
<div class="grid">
|
|
254
|
+
{["a", "b", "c"].map((c) => <span class="cell">{c}</span>)}
|
|
255
|
+
</div>
|
|
256
|
+
</Wrap>
|
|
257
|
+
</section>
|
|
258
|
+
);
|
|
259
|
+
`;
|
|
260
|
+
const html = String(evalApp(transform(src, { mode: "ssr" }))());
|
|
261
|
+
const host = document.createElement("div");
|
|
262
|
+
host.innerHTML = html;
|
|
263
|
+
|
|
264
|
+
const app = evalApp(transform(src, { mode: "hydrate" }));
|
|
265
|
+
runtime.pushHydrationNodes(Array.from(host.childNodes));
|
|
266
|
+
app();
|
|
267
|
+
runtime.popHydrationNodes();
|
|
268
|
+
|
|
269
|
+
expect(host.querySelectorAll("span.cell").length).toBe(3);
|
|
270
|
+
expect(
|
|
271
|
+
Array.from(host.querySelectorAll("span.cell")).map(
|
|
272
|
+
(el) => el.textContent,
|
|
273
|
+
),
|
|
274
|
+
).toEqual(["a", "b", "c"]);
|
|
275
|
+
});
|
|
147
276
|
});
|