@sigil-dev/compiler 0.9.2 → 0.9.3

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 +1 -1
  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 +594 -589
  12. package/src/babel/jsx/anchor-mount.ts +389 -389
  13. package/src/babel/jsx/children.ts +124 -124
  14. package/src/babel/jsx/element.ts +831 -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 +278 -278
  18. package/src/babel/jsx/ssr.ts +319 -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 +197 -197
  35. package/test/keyed-lists.test.ts +88 -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
package/src/vite/index.ts CHANGED
@@ -1,58 +1,58 @@
1
- import { transformSync } from "@babel/core";
2
- import type { Plugin } from "vite";
3
- import sigilPlugin from "../babel/index.ts";
4
- import { computeHash, scopeCSS } from "../babel/util/css.ts";
5
-
6
- const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
7
-
8
- export function sigil(options?: {
9
- hydrate?: boolean;
10
- styleNonce?: string;
11
- }): Plugin {
12
- // Track injected CSS per component to avoid duplicates
13
- const injected = new Set<string>();
14
- const styleNonce = options?.styleNonce ?? "";
15
-
16
- return {
17
- name: "sigil",
18
- transform(code, id) {
19
- if (!id.match(/\.[jt]sx$/)) return;
20
-
21
- // 1. Extract <style> from raw source BEFORE Babel
22
- let scopedCSS = "";
23
- const codeWithoutStyle = code.replace(STYLE_RE, (_, cssContent) => {
24
- const hash = computeHash(id);
25
- scopedCSS = scopeCSS(cssContent.trim(), hash);
26
- return ""; // Remove <style> from source
27
- });
28
-
29
- // 2. Transform JSX with Babel, passing hash + mode
30
- const hash = computeHash(id);
31
- const mode = options?.hydrate ? ("hydrate" as const) : undefined;
32
- const res = transformSync(codeWithoutStyle, {
33
- plugins: ["@babel/plugin-syntax-jsx", [sigilPlugin, { hash, mode }]],
34
- filename: id,
35
- });
36
- if (!res) return;
37
-
38
- // 3. Inject scoped CSS into <head> (once per component)
39
- let cssInjection = "";
40
- if (scopedCSS && !injected.has(id)) {
41
- injected.add(id);
42
- const nonceAttr = styleNonce ? ` nonce="${styleNonce}"` : "";
43
- cssInjection = `
44
- if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
45
- const __style = document.createElement('style');
46
- __style.id = 'sigil-${hash}';${nonceAttr ? `\n __style.setAttribute('nonce', '${styleNonce}');` : ""}
47
- __style.textContent = ${JSON.stringify(scopedCSS)};
48
- document.head.appendChild(__style);
49
- }`;
50
- }
51
-
52
- return {
53
- code: cssInjection + (res.code ?? ""),
54
- map: res.map as any,
55
- };
56
- },
57
- };
58
- }
1
+ import { transformSync } from "@babel/core";
2
+ import type { Plugin } from "vite";
3
+ import sigilPlugin from "../babel/index.ts";
4
+ import { computeHash, scopeCSS } from "../babel/util/css.ts";
5
+
6
+ const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
7
+
8
+ export function sigil(options?: {
9
+ hydrate?: boolean;
10
+ styleNonce?: string;
11
+ }): Plugin {
12
+ // Track injected CSS per component to avoid duplicates
13
+ const injected = new Set<string>();
14
+ const styleNonce = options?.styleNonce ?? "";
15
+
16
+ return {
17
+ name: "sigil",
18
+ transform(code, id) {
19
+ if (!id.match(/\.[jt]sx$/)) return;
20
+
21
+ // 1. Extract <style> from raw source BEFORE Babel
22
+ let scopedCSS = "";
23
+ const codeWithoutStyle = code.replace(STYLE_RE, (_, cssContent) => {
24
+ const hash = computeHash(id);
25
+ scopedCSS = scopeCSS(cssContent.trim(), hash);
26
+ return ""; // Remove <style> from source
27
+ });
28
+
29
+ // 2. Transform JSX with Babel, passing hash + mode
30
+ const hash = computeHash(id);
31
+ const mode = options?.hydrate ? ("hydrate" as const) : undefined;
32
+ const res = transformSync(codeWithoutStyle, {
33
+ plugins: ["@babel/plugin-syntax-jsx", [sigilPlugin, { hash, mode }]],
34
+ filename: id,
35
+ });
36
+ if (!res) return;
37
+
38
+ // 3. Inject scoped CSS into <head> (once per component)
39
+ let cssInjection = "";
40
+ if (scopedCSS && !injected.has(id)) {
41
+ injected.add(id);
42
+ const nonceAttr = styleNonce ? ` nonce="${styleNonce}"` : "";
43
+ cssInjection = `
44
+ if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
45
+ const __style = document.createElement('style');
46
+ __style.id = 'sigil-${hash}';${nonceAttr ? `\n __style.setAttribute('nonce', '${styleNonce}');` : ""}
47
+ __style.textContent = ${JSON.stringify(scopedCSS)};
48
+ document.head.appendChild(__style);
49
+ }`;
50
+ }
51
+
52
+ return {
53
+ code: cssInjection + (res.code ?? ""),
54
+ map: res.map as any,
55
+ };
56
+ },
57
+ };
58
+ }
@@ -2,19 +2,19 @@
2
2
  import { transformAsync, transformSSR } from "./helpers/transform.ts";
3
3
 
4
4
  describe("Async Components", () => {
5
- test("async default export wraps in __asyncBoundary in DOM mode", () => {
6
- const out = transformAsync(`
5
+ test("async default export wraps in __asyncBoundary in DOM mode", () => {
6
+ const out = transformAsync(`
7
7
  export async function Page({ data }) {
8
8
  return <div>{data}</div>;
9
9
  }
10
10
  export default Page;
11
11
  `);
12
- expect(out).toContain("__asyncBoundary");
13
- expect(out).toContain("__Page");
14
- });
12
+ expect(out).toContain("__asyncBoundary");
13
+ expect(out).toContain("__Page");
14
+ });
15
15
 
16
- test("loading export passed to __asyncBoundary", () => {
17
- const out = transformAsync(`
16
+ test("loading export passed to __asyncBoundary", () => {
17
+ const out = transformAsync(`
18
18
  export async function Page({ data }) {
19
19
  return <div>{data}</div>;
20
20
  }
@@ -23,37 +23,37 @@ describe("Async Components", () => {
23
23
  return <div />;
24
24
  }
25
25
  `);
26
- expect(out).toContain("loading");
27
- expect(out).not.toContain("undefined, undefined");
28
- });
26
+ expect(out).toContain("loading");
27
+ expect(out).not.toContain("undefined, undefined");
28
+ });
29
29
 
30
- test("no loading or error yields undefined args", () => {
31
- const out = transformAsync(`
30
+ test("no loading or error yields undefined args", () => {
31
+ const out = transformAsync(`
32
32
  export async function Page({ data }) {
33
33
  return <div>{data}</div>;
34
34
  }
35
35
  export default Page;
36
36
  `);
37
- expect(out).toContain("undefined, undefined");
38
- });
37
+ expect(out).toContain("undefined, undefined");
38
+ });
39
39
 
40
- test("SSR mode leaves async function untouched", () => {
41
- const out = transformSSR(`
40
+ test("SSR mode leaves async function untouched", () => {
41
+ const out = transformSSR(`
42
42
  export async function Page({ data }) {
43
43
  return <div>{data}</div>;
44
44
  }
45
45
  export default Page;
46
46
  `);
47
- expect(out).not.toContain("__asyncBoundary");
48
- expect(out).not.toContain("__Page");
49
- });
47
+ expect(out).not.toContain("__asyncBoundary");
48
+ expect(out).not.toContain("__Page");
49
+ });
50
50
 
51
- test("non-async default is unchanged", () => {
52
- const out = transformAsync(`
51
+ test("non-async default is unchanged", () => {
52
+ const out = transformAsync(`
53
53
  export default function Page({ data }) {
54
54
  return <div>{data}</div>;
55
55
  }
56
56
  `);
57
- expect(out).not.toContain("__asyncBoundary");
58
- });
59
- });
57
+ expect(out).not.toContain("__asyncBoundary");
58
+ });
59
+ });
@@ -1,129 +1,129 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { transform } from "./helpers/transform.ts";
3
-
4
- describe("Components", () => {
5
- test("component call compiles to function invocation", () => {
6
- const result = transform(`const el = <Greeting />;`);
7
- expect(result).toContain("Greeting(");
8
- expect(result).not.toContain('document.createElement("Greeting")');
9
- });
10
-
11
- test("component with empty props", () => {
12
- const result = transform(`const el = <Greeting />;`);
13
- expect(result).toContain("Greeting({})");
14
- });
15
-
16
- test("component with static props", () => {
17
- const result = transform(`const el = <Greeting name="World" />;`);
18
- expect(result).toContain('name: "World"');
19
- });
20
-
21
- test("component with dynamic props", () => {
22
- const result = transform(`
23
- let userName = $state("World");
24
- const el = <Greeting name={userName} />;
25
- `);
26
- expect(result).toContain("userName()");
27
- });
28
-
29
- test("component with event handler prop", () => {
30
- const result = transform(`const el = <Button onClick={() => {}} />;`);
31
- expect(result).toContain("onClick:");
32
- });
33
-
34
- test("nested component in native element", () => {
35
- const result = transform(`
36
- const el = <div><Greeting /></div>;
37
- `);
38
- expect(result).toContain('document.createElement("div")');
39
- expect(result).toContain("Greeting(");
40
- expect(result).toContain(".append(");
41
- });
42
-
43
- test("component return value is used", () => {
44
- const result = transform(`const el = <Greeting />;`);
45
- // The component call should be assigned to a variable
46
- expect(result).toMatch(/_el\d+ = Greeting\(/);
47
- });
48
-
49
- test("native element tags are lowercase", () => {
50
- const result = transform(`const el = <div />;`);
51
- expect(result).toContain('document.createElement("div")');
52
- });
53
-
54
- test("custom native-like tags with lowercase are native", () => {
55
- const result = transform(`const el = <my-component />;`);
56
- expect(result).toContain('document.createElement("my-component")');
57
- });
58
-
59
- test("$state inside component function", () => {
60
- const result = transform(`
61
- function Counter() {
62
- let count = $state(0);
63
- count++;
64
- return count;
65
- }
66
- `);
67
- expect(result).toContain("createSignal(0)");
68
- expect(result).toContain("count.set(count() + 1)");
69
- expect(result).toContain("count()");
70
- });
71
- });
72
-
73
- describe("Component Children", () => {
74
- test("text children passed as string", () => {
75
- const result = transform(`const el = <Button>Click me</Button>;`);
76
- expect(result).toContain('children: "Click me"');
77
- });
78
-
79
- test("element children passed in array", () => {
80
- const result = transform(`const el = <Card><h2>Title</h2></Card>;`);
81
- expect(result).toMatch(/children:\s*\[/);
82
- });
83
-
84
- test("multiple children passed in array", () => {
85
- const result = transform(
86
- `const el = <Card><h2>Title</h2><p>Body</p></Card>;`,
87
- );
88
- expect(result).toMatch(/children:\s*\[/);
89
- expect(result).toContain('createElement("h2")');
90
- expect(result).toContain('createElement("p")');
91
- });
92
-
93
- test("mixed text and element children", () => {
94
- const result = transform(`const el = <Card>Hello<p>World</p></Card>;`);
95
- expect(result).toContain('"Hello"');
96
- expect(result).toContain('createElement("p")');
97
- });
98
-
99
- test("dynamic expression children", () => {
100
- const result = transform(`
101
- let count = $state(0);
102
- const el = <Display>{count}</Display>;
103
- `);
104
- expect(result).toContain("count()");
105
- expect(result).toMatch(/children:\s*\[/);
106
- });
107
-
108
- test("component children with reactivity", () => {
109
- const result = transform(`
110
- let count = $state(0);
111
- const el = <Card><span>{count}</span></Card>;
112
- `);
113
- expect(result).toContain("createEffect(");
114
- expect(result).toMatch(/children:\s*\[/);
115
- });
116
-
117
- test("no children prop when empty", () => {
118
- const result = transform(`const el = <Greeting />;`);
119
- expect(result).not.toContain("children:");
120
- });
121
-
122
- test("component with props and children", () => {
123
- const result = transform(
124
- `const el = <Card title="Hello"><p>Content</p></Card>;`,
125
- );
126
- expect(result).toContain('title: "Hello"');
127
- expect(result).toMatch(/children:\s*\[/);
128
- });
129
- });
1
+ import { describe, expect, test } from "bun:test";
2
+ import { transform } from "./helpers/transform.ts";
3
+
4
+ describe("Components", () => {
5
+ test("component call compiles to function invocation", () => {
6
+ const result = transform(`const el = <Greeting />;`);
7
+ expect(result).toContain("Greeting(");
8
+ expect(result).not.toContain('document.createElement("Greeting")');
9
+ });
10
+
11
+ test("component with empty props", () => {
12
+ const result = transform(`const el = <Greeting />;`);
13
+ expect(result).toContain("Greeting({})");
14
+ });
15
+
16
+ test("component with static props", () => {
17
+ const result = transform(`const el = <Greeting name="World" />;`);
18
+ expect(result).toContain('name: "World"');
19
+ });
20
+
21
+ test("component with dynamic props", () => {
22
+ const result = transform(`
23
+ let userName = $state("World");
24
+ const el = <Greeting name={userName} />;
25
+ `);
26
+ expect(result).toContain("userName()");
27
+ });
28
+
29
+ test("component with event handler prop", () => {
30
+ const result = transform(`const el = <Button onClick={() => {}} />;`);
31
+ expect(result).toContain("onClick:");
32
+ });
33
+
34
+ test("nested component in native element", () => {
35
+ const result = transform(`
36
+ const el = <div><Greeting /></div>;
37
+ `);
38
+ expect(result).toContain('document.createElement("div")');
39
+ expect(result).toContain("Greeting(");
40
+ expect(result).toContain(".append(");
41
+ });
42
+
43
+ test("component return value is used", () => {
44
+ const result = transform(`const el = <Greeting />;`);
45
+ // The component call should be assigned to a variable
46
+ expect(result).toMatch(/_el\d+ = Greeting\(/);
47
+ });
48
+
49
+ test("native element tags are lowercase", () => {
50
+ const result = transform(`const el = <div />;`);
51
+ expect(result).toContain('document.createElement("div")');
52
+ });
53
+
54
+ test("custom native-like tags with lowercase are native", () => {
55
+ const result = transform(`const el = <my-component />;`);
56
+ expect(result).toContain('document.createElement("my-component")');
57
+ });
58
+
59
+ test("$state inside component function", () => {
60
+ const result = transform(`
61
+ function Counter() {
62
+ let count = $state(0);
63
+ count++;
64
+ return count;
65
+ }
66
+ `);
67
+ expect(result).toContain("createSignal(0)");
68
+ expect(result).toContain("count.set(count() + 1)");
69
+ expect(result).toContain("count()");
70
+ });
71
+ });
72
+
73
+ describe("Component Children", () => {
74
+ test("text children passed as string", () => {
75
+ const result = transform(`const el = <Button>Click me</Button>;`);
76
+ expect(result).toContain('children: "Click me"');
77
+ });
78
+
79
+ test("element children passed in array", () => {
80
+ const result = transform(`const el = <Card><h2>Title</h2></Card>;`);
81
+ expect(result).toMatch(/children:\s*\[/);
82
+ });
83
+
84
+ test("multiple children passed in array", () => {
85
+ const result = transform(
86
+ `const el = <Card><h2>Title</h2><p>Body</p></Card>;`,
87
+ );
88
+ expect(result).toMatch(/children:\s*\[/);
89
+ expect(result).toContain('createElement("h2")');
90
+ expect(result).toContain('createElement("p")');
91
+ });
92
+
93
+ test("mixed text and element children", () => {
94
+ const result = transform(`const el = <Card>Hello<p>World</p></Card>;`);
95
+ expect(result).toContain('"Hello"');
96
+ expect(result).toContain('createElement("p")');
97
+ });
98
+
99
+ test("dynamic expression children", () => {
100
+ const result = transform(`
101
+ let count = $state(0);
102
+ const el = <Display>{count}</Display>;
103
+ `);
104
+ expect(result).toContain("count()");
105
+ expect(result).toMatch(/children:\s*\[/);
106
+ });
107
+
108
+ test("component children with reactivity", () => {
109
+ const result = transform(`
110
+ let count = $state(0);
111
+ const el = <Card><span>{count}</span></Card>;
112
+ `);
113
+ expect(result).toContain("createEffect(");
114
+ expect(result).toMatch(/children:\s*\[/);
115
+ });
116
+
117
+ test("no children prop when empty", () => {
118
+ const result = transform(`const el = <Greeting />;`);
119
+ expect(result).not.toContain("children:");
120
+ });
121
+
122
+ test("component with props and children", () => {
123
+ const result = transform(
124
+ `const el = <Card title="Hello"><p>Content</p></Card>;`,
125
+ );
126
+ expect(result).toContain('title: "Hello"');
127
+ expect(result).toMatch(/children:\s*\[/);
128
+ });
129
+ });