@sigil-dev/compiler 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +162 -162
  2. package/index.ts +4 -4
  3. package/jsr.json +8 -8
  4. package/package.json +8 -8
  5. package/src/babel/handlers/dom/derived.ts +62 -62
  6. package/src/babel/handlers/dom/effect.ts +15 -15
  7. package/src/babel/handlers/dom/globals.ts +105 -105
  8. package/src/babel/handlers/dom/state.ts +107 -104
  9. package/src/babel/handlers/ssr/derived.ts +27 -27
  10. package/src/babel/handlers/ssr/state.ts +11 -11
  11. package/src/babel/index.ts +599 -589
  12. package/src/babel/jsx/anchor-mount.ts +385 -389
  13. package/src/babel/jsx/children.ts +124 -124
  14. package/src/babel/jsx/element.ts +850 -831
  15. package/src/babel/jsx/fragment.ts +282 -282
  16. package/src/babel/jsx/index.ts +2 -2
  17. package/src/babel/jsx/keyed-list.ts +309 -278
  18. package/src/babel/jsx/ssr.ts +324 -319
  19. package/src/babel/jsx/text-node.ts +192 -192
  20. package/src/babel/jsx/utils.ts +179 -179
  21. package/src/babel/util/bind.ts +139 -139
  22. package/src/babel/util/css.ts +151 -151
  23. package/src/babel/util/dead-code.ts +28 -28
  24. package/src/babel/util/helpers.ts +46 -46
  25. package/src/babel/util/magic.ts +11 -11
  26. package/src/bun-plugin.ts +92 -90
  27. package/src/vite/index.ts +58 -58
  28. package/test/async-components.test.ts +24 -24
  29. package/test/components.test.ts +129 -129
  30. package/test/containsSignal.test.ts +288 -288
  31. package/test/fragments.test.ts +90 -90
  32. package/test/helpers/transform.ts +45 -48
  33. package/test/hydration.test.ts +147 -147
  34. package/test/jsx.test.ts +225 -197
  35. package/test/keyed-lists.test.ts +126 -88
  36. package/test/reactivity.test.ts +225 -225
  37. package/test/scoped-css.test.ts +129 -129
  38. package/test/ssr.test.ts +130 -130
  39. package/test/tree-shaking.test.ts +78 -78
  40. package/tsconfig.json +30 -30
@@ -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,48 +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: [
42
- "@babel/plugin-syntax-jsx",
43
- [sigilPlugin, { mode: "dom" }],
44
- ],
45
- filename: "test.tsx",
46
- })?.code ?? ""
47
- );
48
- }
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,147 +1,147 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { transformSync } from "@babel/core";
3
- import sigilPlugin from "../src/babel/index.ts";
4
-
5
- function transform(
6
- source: string,
7
- opts?: { mode?: "dom" | "ssr" | "hydrate" },
8
- ) {
9
- const result = transformSync(source, {
10
- plugins: [
11
- "@babel/plugin-syntax-jsx",
12
- [sigilPlugin, { mode: opts?.mode ?? "dom" }],
13
- ],
14
- filename: "test.tsx",
15
- });
16
- return result!.code!;
17
- }
18
-
19
- describe("Hydration", () => {
20
- test("element uses claim instead of createElement", () => {
21
- const code = transform(`const el = <div></div>`, { mode: "hydrate" });
22
- expect(code).toContain("claim(");
23
- expect(code).not.toContain("createElement");
24
- });
25
-
26
- test("nested elements use claim with scoped pools", () => {
27
- const code = transform(`const el = <div><span></span></div>`, {
28
- mode: "hydrate",
29
- });
30
- // Should claim div from root __nodes pool (no parent for root)
31
- expect(code).toContain('claim(__nodes, "div")');
32
- // Should scope to div's children (genId produces _el1 for pool)
33
- expect(code).toContain("Array.from(_el0.childNodes)");
34
- // Should claim span from div's child pool with parent for SPA fallback
35
- expect(code).toContain('claim(_el1, "span", _el0)');
36
- });
37
-
38
- test("static text creates text node only when pool is empty (SPA nav)", () => {
39
- const code = transform(`const el = <div>Hello</div>`, { mode: "hydrate" });
40
- // Static text: SSR hydration leaves it in DOM; SPA nav creates it when pool is empty
41
- expect(code).not.toContain("claimText(");
42
- // Should emit a conditional createTextNode guarded by pool.length === 0
43
- expect(code).toContain("createTextNode");
44
- expect(code).toContain(".length === 0");
45
- });
46
-
47
- test("reactive text node uses claimComment anchor + Text check for hydration", () => {
48
- const code = transform(
49
- `let name = $state('Alice'); const el = <span>{name}</span>`,
50
- { mode: "hydrate" },
51
- );
52
- // Dynamic text: claims <!--g--> as anchor (fixes adjacent-text bug vs DOM walk)
53
- // then gets nextSibling; creates text node if it's not a Text (SPA navigation)
54
- expect(code).toContain("claimComment(");
55
- expect(code).toContain("createEffect");
56
- expect(code).toContain("instanceof Text");
57
- });
58
-
59
- test("attributes still work in hydrate mode", () => {
60
- const code = transform(`const el = <div class="foo" id="bar"></div>`, {
61
- mode: "hydrate",
62
- });
63
- expect(code).toContain("claim(");
64
- expect(code).toContain('className = "foo"');
65
- expect(code).toContain('id = "bar"');
66
- });
67
-
68
- test("event handlers still attached in hydrate mode", () => {
69
- const code = transform(
70
- `const el = <button onClick={() => {}}>Click</button>`,
71
- { mode: "hydrate" },
72
- );
73
- expect(code).toContain("claim(");
74
- expect(code).toContain("addEventListener");
75
- });
76
-
77
- test("no unconditional append calls in hydrate mode", () => {
78
- const code = transform(`const el = <div><span></span></div>`, {
79
- mode: "hydrate",
80
- });
81
- // Elements are claimed from pool — no unconditional appends for elements
82
- // (static text does emit a conditional .append guarded by pool.length === 0)
83
- expect(code).not.toContain('claim(__nodes, "div").append(');
84
- });
85
-
86
- test("dom mode still uses createElement (no regression)", () => {
87
- const code = transform(`const el = <div></div>`, { mode: "dom" });
88
- expect(code).toContain("createElement");
89
- expect(code).not.toContain("claim(");
90
- });
91
-
92
- test("dom mode still appends children (no regression)", () => {
93
- const code = transform(`const el = <div><span></span></div>`, {
94
- mode: "dom",
95
- });
96
- expect(code).toContain(".append(");
97
- });
98
-
99
- test("three levels deep: div > p > span", () => {
100
- const code = transform(`const el = <div><p><span></span></p></div>`, {
101
- mode: "hydrate",
102
- });
103
- // Three scopes (Array.from)
104
- const scopes = code.match(/Array\.from\(.*\.childNodes\)/g);
105
- expect(scopes?.length).toBe(3);
106
- // Root claims from __nodes (no parent), children use generated pool names with parent
107
- expect(code).toContain('claim(__nodes, "div")');
108
- expect(code).toContain('claim(_el1, "p", _el0)');
109
- expect(code).toContain('claim(_el3, "span", _el2)');
110
- });
111
-
112
- test("imports claim and claimText from @sigil-dev/runtime", () => {
113
- const code = transform(`const el = <div></div>`, { mode: "hydrate" });
114
- expect(code).toContain(
115
- "createSignal, createRawSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, insert, getHydrationNodes, pushHydrationNodes, popHydrationNodes, snapshot, tracking, createEffectPre, createInspectEffect, withEffectScope",
116
- );
117
- });
118
-
119
- test("dom mode does not import claim/claimText", () => {
120
- const code = transform(`const el = <div></div>`, { mode: "dom" });
121
- // claim/claimText are imported but unused — tree-shaking removes them
122
- // Actually, they're always imported. That's fine — bundlers tree-shake.
123
- // Just verify createElement is used
124
- expect(code).toContain("createElement");
125
- });
126
-
127
- test("reactive attribute still wraps in createEffect", () => {
128
- const code = transform(
129
- `let cls = $state('a'); const el = <div class={cls}></div>`,
130
- { mode: "hydrate" },
131
- );
132
- expect(code).toContain("claim(");
133
- expect(code).toContain("createEffect");
134
- });
135
-
136
- test("components in hydrate mode pass children as props", () => {
137
- const code = transform(
138
- `const App = (props) => <div>{props.children}</div>;
139
- const el = <App><span>Hi</span></App>`,
140
- { mode: "hydrate" },
141
- );
142
- // Component still calls function
143
- expect(code).toContain("App(");
144
- // Inner div uses claim
145
- expect(code).toContain('claim(__nodes, "div")');
146
- });
147
- });
1
+ import { describe, expect, test } from "bun:test";
2
+ import { transformSync } from "@babel/core";
3
+ import sigilPlugin from "../src/babel/index.ts";
4
+
5
+ function transform(
6
+ source: string,
7
+ opts?: { mode?: "dom" | "ssr" | "hydrate" },
8
+ ) {
9
+ const result = transformSync(source, {
10
+ plugins: [
11
+ "@babel/plugin-syntax-jsx",
12
+ [sigilPlugin, { mode: opts?.mode ?? "dom" }],
13
+ ],
14
+ filename: "test.tsx",
15
+ });
16
+ return result!.code!;
17
+ }
18
+
19
+ describe("Hydration", () => {
20
+ test("element uses claim instead of createElement", () => {
21
+ const code = transform(`const el = <div></div>`, { mode: "hydrate" });
22
+ expect(code).toContain("claim(");
23
+ expect(code).not.toContain("createElement");
24
+ });
25
+
26
+ test("nested elements use claim with scoped pools", () => {
27
+ const code = transform(`const el = <div><span></span></div>`, {
28
+ mode: "hydrate",
29
+ });
30
+ // Should claim div from root __nodes pool (no parent for root)
31
+ expect(code).toContain('claim(__nodes, "div")');
32
+ // Should scope to div's children (genId produces _el1 for pool)
33
+ expect(code).toContain("Array.from(_el0.childNodes)");
34
+ // Should claim span from div's child pool with parent for SPA fallback
35
+ expect(code).toContain('claim(_el1, "span", _el0)');
36
+ });
37
+
38
+ test("static text creates text node only when pool is empty (SPA nav)", () => {
39
+ const code = transform(`const el = <div>Hello</div>`, { mode: "hydrate" });
40
+ // Static text: SSR hydration leaves it in DOM; SPA nav creates it when pool is empty
41
+ expect(code).not.toContain("claimText(");
42
+ // Should emit a conditional createTextNode guarded by pool.length === 0
43
+ expect(code).toContain("createTextNode");
44
+ expect(code).toContain(".length === 0");
45
+ });
46
+
47
+ test("reactive text node uses claimComment anchor + Text check for hydration", () => {
48
+ const code = transform(
49
+ `let name = $state('Alice'); const el = <span>{name}</span>`,
50
+ { mode: "hydrate" },
51
+ );
52
+ // Dynamic text: claims <!--g--> as anchor (fixes adjacent-text bug vs DOM walk)
53
+ // then gets nextSibling; creates text node if it's not a Text (SPA navigation)
54
+ expect(code).toContain("claimComment(");
55
+ expect(code).toContain("createEffect");
56
+ expect(code).toContain("instanceof Text");
57
+ });
58
+
59
+ test("attributes still work in hydrate mode", () => {
60
+ const code = transform(`const el = <div class="foo" id="bar"></div>`, {
61
+ mode: "hydrate",
62
+ });
63
+ expect(code).toContain("claim(");
64
+ expect(code).toContain('className = "foo"');
65
+ expect(code).toContain('id = "bar"');
66
+ });
67
+
68
+ test("event handlers still attached in hydrate mode", () => {
69
+ const code = transform(
70
+ `const el = <button onClick={() => {}}>Click</button>`,
71
+ { mode: "hydrate" },
72
+ );
73
+ expect(code).toContain("claim(");
74
+ expect(code).toContain("addEventListener");
75
+ });
76
+
77
+ test("no unconditional append calls in hydrate mode", () => {
78
+ const code = transform(`const el = <div><span></span></div>`, {
79
+ mode: "hydrate",
80
+ });
81
+ // Elements are claimed from pool — no unconditional appends for elements
82
+ // (static text does emit a conditional .append guarded by pool.length === 0)
83
+ expect(code).not.toContain('claim(__nodes, "div").append(');
84
+ });
85
+
86
+ test("dom mode still uses createElement (no regression)", () => {
87
+ const code = transform(`const el = <div></div>`, { mode: "dom" });
88
+ expect(code).toContain("createElement");
89
+ expect(code).not.toContain("claim(");
90
+ });
91
+
92
+ test("dom mode still appends children (no regression)", () => {
93
+ const code = transform(`const el = <div><span></span></div>`, {
94
+ mode: "dom",
95
+ });
96
+ expect(code).toContain(".append(");
97
+ });
98
+
99
+ test("three levels deep: div > p > span", () => {
100
+ const code = transform(`const el = <div><p><span></span></p></div>`, {
101
+ mode: "hydrate",
102
+ });
103
+ // Three scopes (Array.from)
104
+ const scopes = code.match(/Array\.from\(.*\.childNodes\)/g);
105
+ expect(scopes?.length).toBe(3);
106
+ // Root claims from __nodes (no parent), children use generated pool names with parent
107
+ expect(code).toContain('claim(__nodes, "div")');
108
+ expect(code).toContain('claim(_el1, "p", _el0)');
109
+ expect(code).toContain('claim(_el3, "span", _el2)');
110
+ });
111
+
112
+ test("imports claim and claimText from @sigil-dev/runtime", () => {
113
+ const code = transform(`const el = <div></div>`, { mode: "hydrate" });
114
+ 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",
116
+ );
117
+ });
118
+
119
+ test("dom mode does not import claim/claimText", () => {
120
+ const code = transform(`const el = <div></div>`, { mode: "dom" });
121
+ // claim/claimText are imported but unused — tree-shaking removes them
122
+ // Actually, they're always imported. That's fine — bundlers tree-shake.
123
+ // Just verify createElement is used
124
+ expect(code).toContain("createElement");
125
+ });
126
+
127
+ test("reactive attribute still wraps in createEffect", () => {
128
+ const code = transform(
129
+ `let cls = $state('a'); const el = <div class={cls}></div>`,
130
+ { mode: "hydrate" },
131
+ );
132
+ expect(code).toContain("claim(");
133
+ expect(code).toContain("createEffect");
134
+ });
135
+
136
+ test("components in hydrate mode pass children as props", () => {
137
+ const code = transform(
138
+ `const App = (props) => <div>{props.children}</div>;
139
+ const el = <App><span>Hi</span></App>`,
140
+ { mode: "hydrate" },
141
+ );
142
+ // Component still calls function
143
+ expect(code).toContain("App(");
144
+ // Inner div uses claim
145
+ expect(code).toContain('claim(__nodes, "div")');
146
+ });
147
+ });