@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
package/README.md CHANGED
@@ -1,162 +1,162 @@
1
- # @sigil-dev/compiler
2
-
3
- Compile-time JSX transform for Sigil.
4
- Turns reactive macros and JSX into direct DOM operations with a tiny runtime overhead.
5
-
6
- ```bash
7
- bun add -d @sigil-dev/compiler
8
- ```
9
-
10
- ## What it does
11
-
12
- The compiler runs at build time and does two things:
13
-
14
- **1. Transforms reactive macros into signal primitives**
15
- This helps you avoid footguns ~~which should hardly be possible, we're not react~~
16
-
17
- ```typescript
18
- // you write:
19
- let count = $state(0);
20
- let doubled = $derived(count * 2);
21
-
22
- $effect(() => {
23
- console.log(doubled);
24
- });
25
-
26
- // compiler outputs:
27
- const count = createSignal(0);
28
- const doubled = createMemo(() => count() * 2);
29
-
30
- createEffect(() => {
31
- console.log(doubled());
32
- });
33
- ```
34
-
35
- **2. Transforms JSX into imperative DOM**
36
-
37
- ```tsx
38
- // you write:
39
- const el = <div class="box">{count}</div>;
40
-
41
- // compiler outputs (dom mode):
42
- const el = document.createElement("div");
43
- el.className = "box";
44
- const t = document.createTextNode("");
45
- createEffect(() => { t.data = String(count()); });
46
- el.appendChild(t);
47
- ```
48
- There's no virtual DOM involved, and none of the nonsense involved with it. Output is either a string or DOM manipulation. You won't fight with the runtime over what actually changed and what didn't
49
-
50
- ## Modes
51
-
52
- Three output modes for the full SSR lifecycle:
53
-
54
- | Mode | Use | Output |
55
- |------|-----|--------|
56
- | `dom` | Client SPA navigation | Imperative DOM calls |
57
- | `ssr` | Server render | HTML string concatenation |
58
- | `hydrate` | Initial client load | Claims existing SSR nodes |
59
-
60
- The same source file compiles to different output depending on mode. In `ssr` mode, `$effect` is dropped entirely (effects don't make sense on the server). In `hydrate` mode, `claim()` calls replace `createElement` to reuse server-rendered nodes.
61
-
62
- ## Macros
63
-
64
- ### `$state`
65
-
66
- Declares a reactive variable. Primitive values use subscription sets. Objects and arrays use Proxy for deep reactivity.
67
-
68
- ```typescript
69
- let count = $state(0);
70
- count++; // compiled to count.set(count.peek() + 1)
71
- count = 10; // compiled to count.set(10)
72
-
73
- let user = $state({ name: "Rei" });
74
- user.name = "Asuka"; // proxy handles this — no rewrite needed
75
- ```
76
-
77
- ### `$derived`
78
-
79
- A memoized value derived from other reactive state. Cached until dependencies change.
80
-
81
- ```typescript
82
- let doubled = $derived(count * 2);
83
- // compiled to: const doubled = createMemo(() => count() * 2)
84
- // which is another tiny shim over createEffect()
85
- ```
86
-
87
- ### `$effect`
88
-
89
- Runs a side effect that re-runs when its reactive dependencies change.
90
-
91
- ```typescript
92
- $effect(() => {
93
- document.title = `Count: ${count}`;
94
- });
95
- // compiled to: createEffect(() => { document.title = `Count: ${count()}`; })
96
- ```
97
-
98
- ## Keyed lists
99
-
100
- `.map()` with a `key` prop compiles to a reconciled list that surgically updates only changed items:
101
-
102
- ```tsx
103
- const items = $state([{ id: 1, name: "Rei" }, { id: 2, name: "Asuka" }]);
104
-
105
- <ul>
106
- {items.map(item => (
107
- <li key={item.id}>{item.name}</li>
108
- ))}
109
- </ul>
110
- ```
111
-
112
- Compiled to `reconcile()` — moves, inserts, and removes DOM nodes by key without recreating unchanged elements.
113
-
114
- ## Scoped CSS
115
-
116
- CSS defined in a component file is automatically scoped with a deterministic hash:
117
-
118
- ```tsx
119
- // styles in your component file get a unique hash prefix
120
- // .box → .box.s-a3f9b2c1
121
- // :global(.reset) escapes scoping
122
- ```
123
-
124
- ## Setup
125
-
126
- ### Vite
127
-
128
- ```typescript
129
- // vite.config.ts
130
- import { sigil } from "@sigil-dev/compiler/vite";
131
-
132
- export default {
133
- plugins: [sigil()]
134
- };
135
- ```
136
-
137
- ### Bun (reccommended)
138
-
139
- ```typescript
140
- // build script
141
- import { sigil } from "@sigil-dev/compiler/bun";
142
-
143
- await Bun.build({
144
- entrypoints: ["src/index.tsx"],
145
- plugins: [sigil({ mode: "dom" })],
146
- });
147
- ```
148
-
149
- ### Babel
150
-
151
- ```typescript
152
- // babel.config.ts
153
- import { sigilBabelPlugin } from "@sigil-dev/compiler/babel";
154
-
155
- export default {
156
- plugins: [sigilBabelPlugin({ mode: "dom" })]
157
- };
158
- ```
159
-
160
- ## Used by
161
-
162
- `@sigil-dev/grimoire` uses this compiler internally to process route files in three passes (ssr, hydrate, dom) during the build step. You can use it standalone for SPA projects via Vite, or wire it into any Babel-compatible build pipeline.
1
+ # @sigil-dev/compiler
2
+
3
+ Compile-time JSX transform for Sigil.
4
+ Turns reactive macros and JSX into direct DOM operations with a tiny runtime overhead.
5
+
6
+ ```bash
7
+ bun add -d @sigil-dev/compiler
8
+ ```
9
+
10
+ ## What it does
11
+
12
+ The compiler runs at build time and does two things:
13
+
14
+ **1. Transforms reactive macros into signal primitives**
15
+ This helps you avoid footguns ~~which should hardly be possible, we're not react~~
16
+
17
+ ```typescript
18
+ // you write:
19
+ let count = $state(0);
20
+ let doubled = $derived(count * 2);
21
+
22
+ $effect(() => {
23
+ console.log(doubled);
24
+ });
25
+
26
+ // compiler outputs:
27
+ const count = createSignal(0);
28
+ const doubled = createMemo(() => count() * 2);
29
+
30
+ createEffect(() => {
31
+ console.log(doubled());
32
+ });
33
+ ```
34
+
35
+ **2. Transforms JSX into imperative DOM**
36
+
37
+ ```tsx
38
+ // you write:
39
+ const el = <div class="box">{count}</div>;
40
+
41
+ // compiler outputs (dom mode):
42
+ const el = document.createElement("div");
43
+ el.className = "box";
44
+ const t = document.createTextNode("");
45
+ createEffect(() => { t.data = String(count()); });
46
+ el.appendChild(t);
47
+ ```
48
+ There's no virtual DOM involved, and none of the nonsense involved with it. Output is either a string or DOM manipulation. You won't fight with the runtime over what actually changed and what didn't
49
+
50
+ ## Modes
51
+
52
+ Three output modes for the full SSR lifecycle:
53
+
54
+ | Mode | Use | Output |
55
+ |------|-----|--------|
56
+ | `dom` | Client SPA navigation | Imperative DOM calls |
57
+ | `ssr` | Server render | HTML string concatenation |
58
+ | `hydrate` | Initial client load | Claims existing SSR nodes |
59
+
60
+ The same source file compiles to different output depending on mode. In `ssr` mode, `$effect` is dropped entirely (effects don't make sense on the server). In `hydrate` mode, `claim()` calls replace `createElement` to reuse server-rendered nodes.
61
+
62
+ ## Macros
63
+
64
+ ### `$state`
65
+
66
+ Declares a reactive variable. Primitive values use subscription sets. Objects and arrays use Proxy for deep reactivity.
67
+
68
+ ```typescript
69
+ let count = $state(0);
70
+ count++; // compiled to count.set(count.peek() + 1)
71
+ count = 10; // compiled to count.set(10)
72
+
73
+ let user = $state({ name: "Rei" });
74
+ user.name = "Asuka"; // proxy handles this — no rewrite needed
75
+ ```
76
+
77
+ ### `$derived`
78
+
79
+ A memoized value derived from other reactive state. Cached until dependencies change.
80
+
81
+ ```typescript
82
+ let doubled = $derived(count * 2);
83
+ // compiled to: const doubled = createMemo(() => count() * 2)
84
+ // which is another tiny shim over createEffect()
85
+ ```
86
+
87
+ ### `$effect`
88
+
89
+ Runs a side effect that re-runs when its reactive dependencies change.
90
+
91
+ ```typescript
92
+ $effect(() => {
93
+ document.title = `Count: ${count}`;
94
+ });
95
+ // compiled to: createEffect(() => { document.title = `Count: ${count()}`; })
96
+ ```
97
+
98
+ ## Keyed lists
99
+
100
+ `.map()` with a `key` prop compiles to a reconciled list that surgically updates only changed items:
101
+
102
+ ```tsx
103
+ const items = $state([{ id: 1, name: "Rei" }, { id: 2, name: "Asuka" }]);
104
+
105
+ <ul>
106
+ {items.map(item => (
107
+ <li key={item.id}>{item.name}</li>
108
+ ))}
109
+ </ul>
110
+ ```
111
+
112
+ Compiled to `reconcile()` — moves, inserts, and removes DOM nodes by key without recreating unchanged elements.
113
+
114
+ ## Scoped CSS
115
+
116
+ CSS defined in a component file is automatically scoped with a deterministic hash:
117
+
118
+ ```tsx
119
+ // styles in your component file get a unique hash prefix
120
+ // .box → .box.s-a3f9b2c1
121
+ // :global(.reset) escapes scoping
122
+ ```
123
+
124
+ ## Setup
125
+
126
+ ### Vite
127
+
128
+ ```typescript
129
+ // vite.config.ts
130
+ import { sigil } from "@sigil-dev/compiler/vite";
131
+
132
+ export default {
133
+ plugins: [sigil()]
134
+ };
135
+ ```
136
+
137
+ ### Bun (reccommended)
138
+
139
+ ```typescript
140
+ // build script
141
+ import { sigil } from "@sigil-dev/compiler/bun";
142
+
143
+ await Bun.build({
144
+ entrypoints: ["src/index.tsx"],
145
+ plugins: [sigil({ mode: "dom" })],
146
+ });
147
+ ```
148
+
149
+ ### Babel
150
+
151
+ ```typescript
152
+ // babel.config.ts
153
+ import { sigilBabelPlugin } from "@sigil-dev/compiler/babel";
154
+
155
+ export default {
156
+ plugins: [sigilBabelPlugin({ mode: "dom" })]
157
+ };
158
+ ```
159
+
160
+ ## Used by
161
+
162
+ `@sigil-dev/grimoire` uses this compiler internally to process route files in three passes (ssr, hydrate, dom) during the build step. You can use it standalone for SPA projects via Vite, or wire it into any Babel-compatible build pipeline.
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // packages/compiler/index.ts
2
-
3
- export { default as sigilPlugin } from "./src/babel/index.ts";
4
- export { sigil } from "./src/vite/index.ts";
1
+ // packages/compiler/index.ts
2
+
3
+ export { default as sigilPlugin } from "./src/babel/index.ts";
4
+ export { sigil } from "./src/vite/index.ts";
package/jsr.json CHANGED
@@ -1,8 +1,8 @@
1
- {
2
- "name": "@sigil-dev/compiler",
3
- "version": "0.6.0",
4
- "exports": "./index.ts",
5
- "publish": {
6
- "exclude": ["src/**/*.test.ts"]
7
- }
8
- }
1
+ {
2
+ "name": "@sigil-dev/compiler",
3
+ "version": "0.6.0",
4
+ "exports": "./index.ts",
5
+ "publish": {
6
+ "exclude": ["src/**/*.test.ts"]
7
+ }
8
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@sigil-dev/compiler",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.9.2",
5
+ "version": "0.9.4",
6
6
  "private": false,
7
7
  "description": "Compiler for the Sigil framework",
8
8
  "peerDependencies": {
@@ -15,16 +15,16 @@
15
15
  "./bun": "./src/bun-plugin.ts"
16
16
  },
17
17
  "devDependencies": {
18
- "@babel/core": "next",
19
- "@babel/plugin-syntax-jsx": "next",
20
- "@babel/plugin-syntax-typescript": "next",
21
- "@babel/traverse": "next",
22
- "@babel/types": "next"
18
+ "@babel/core": "8.0.0-rc.6",
19
+ "@babel/plugin-syntax-jsx": "8.0.0-rc.6",
20
+ "@babel/plugin-syntax-typescript": "8.0.0-rc.6",
21
+ "@babel/traverse": "8.0.0-rc.6",
22
+ "@babel/types": "8.0.0-rc.6"
23
23
  },
24
24
  "dependencies": {
25
25
  "@babel/generator": "^7.29.7",
26
26
  "@babel/parser": "^7.29.7",
27
- "@babel/plugin-transform-typescript": "^8.0.0-rc.6",
28
- "@babel/preset-typescript": "^8.0.0-rc.6"
27
+ "@babel/plugin-transform-typescript": "8.0.0-rc.6",
28
+ "@babel/preset-typescript": "8.0.0-rc.6"
29
29
  }
30
30
  }
@@ -1,62 +1,62 @@
1
- import type { NodePath } from "@babel/core";
2
- import { types as t } from "@babel/core";
3
- import { MAGIC } from "../../util/magic.ts";
4
-
5
- const setCall = (name: string, value: t.Expression) =>
6
- t.callExpression(
7
- t.memberExpression(t.identifier(name), t.identifier("set")),
8
- [value],
9
- );
10
-
11
- export function handleDerived(
12
- declaration: NodePath<t.VariableDeclaration>,
13
- declarator: NodePath<t.VariableDeclarator>,
14
- init: NodePath<t.CallExpression>,
15
- signals: Set<string>,
16
- ): void {
17
- const { id } = declarator.node;
18
- if (!t.isIdentifier(id))
19
- throw new Error("Destructured $derived not supported");
20
- const name = id.name;
21
- signals.add(id.name);
22
-
23
- const binding = declaration.scope.getBinding(name);
24
- if (!binding) return;
25
- const refs = [...binding.referencePaths];
26
- const violations = [...binding.constantViolations];
27
-
28
- init.replaceWith(
29
- t.callExpression(t.identifier(MAGIC.createMemo), [
30
- t.arrowFunctionExpression([], init.node.arguments[0] as t.Expression),
31
- ]),
32
- );
33
- declaration.node.kind = "let";
34
-
35
- // A6: $derived reassign — convert assignments to .set() calls
36
- for (const violation of violations) {
37
- if (violation.isAssignmentExpression()) {
38
- const { operator, right } = violation.node;
39
- if (operator === "=") {
40
- violation.replaceWith(setCall(name, right));
41
- } else {
42
- const baseOp = operator.slice(0, -1);
43
- violation.replaceWith(
44
- setCall(
45
- name,
46
- t.binaryExpression(
47
- baseOp as t.BinaryExpression["operator"],
48
- t.callExpression(t.identifier(name), []),
49
- right,
50
- ),
51
- ),
52
- );
53
- }
54
- }
55
- }
56
-
57
- for (const ref of refs) {
58
- if (!ref.isIdentifier()) continue;
59
- if (ref.parentPath?.isUpdateExpression()) continue;
60
- ref.replaceWith(t.callExpression(ref.node, []));
61
- }
62
- }
1
+ import type { NodePath } from "@babel/core";
2
+ import { types as t } from "@babel/core";
3
+ import { MAGIC } from "../../util/magic.ts";
4
+
5
+ const setCall = (name: string, value: t.Expression) =>
6
+ t.callExpression(
7
+ t.memberExpression(t.identifier(name), t.identifier("set")),
8
+ [value],
9
+ );
10
+
11
+ export function handleDerived(
12
+ declaration: NodePath<t.VariableDeclaration>,
13
+ declarator: NodePath<t.VariableDeclarator>,
14
+ init: NodePath<t.CallExpression>,
15
+ signals: Set<string>,
16
+ ): void {
17
+ const { id } = declarator.node;
18
+ if (!t.isIdentifier(id))
19
+ throw new Error("Destructured $derived not supported");
20
+ const name = id.name;
21
+ signals.add(id.name);
22
+
23
+ const binding = declaration.scope.getBinding(name);
24
+ if (!binding) return;
25
+ const refs = [...binding.referencePaths];
26
+ const violations = [...binding.constantViolations];
27
+
28
+ init.replaceWith(
29
+ t.callExpression(t.identifier(MAGIC.createMemo), [
30
+ t.arrowFunctionExpression([], init.node.arguments[0] as t.Expression),
31
+ ]),
32
+ );
33
+ declaration.node.kind = "let";
34
+
35
+ // A6: $derived reassign — convert assignments to .set() calls
36
+ for (const violation of violations) {
37
+ if (violation.isAssignmentExpression()) {
38
+ const { operator, right } = violation.node;
39
+ if (operator === "=") {
40
+ violation.replaceWith(setCall(name, right));
41
+ } else {
42
+ const baseOp = operator.slice(0, -1);
43
+ violation.replaceWith(
44
+ setCall(
45
+ name,
46
+ t.binaryExpression(
47
+ baseOp as t.BinaryExpression["operator"],
48
+ t.callExpression(t.identifier(name), []),
49
+ right,
50
+ ),
51
+ ),
52
+ );
53
+ }
54
+ }
55
+ }
56
+
57
+ for (const ref of refs) {
58
+ if (!ref.isIdentifier()) continue;
59
+ if (ref.parentPath?.isUpdateExpression()) continue;
60
+ ref.replaceWith(t.callExpression(ref.node, []));
61
+ }
62
+ }
@@ -1,15 +1,15 @@
1
- import type { NodePath, types as t } from "@babel/core";
2
- import { MAGIC } from "../../util/magic.ts";
3
-
4
- export function handleEffect(statement: NodePath<t.ExpressionStatement>): void {
5
- const expr = statement.get("expression");
6
- if (!expr.isCallExpression()) return;
7
- const callee = expr.get("callee");
8
- if (!callee.isIdentifier()) return;
9
- if (callee.node.name === "$effect") callee.node.name = MAGIC.createEffect;
10
- else if (callee.node.name === "$derived") {
11
- throw statement.buildCodeFrameError(
12
- "$derived must be assigned to a variable: let doubled = $derived(count*2)",
13
- );
14
- }
15
- }
1
+ import type { NodePath, types as t } from "@babel/core";
2
+ import { MAGIC } from "../../util/magic.ts";
3
+
4
+ export function handleEffect(statement: NodePath<t.ExpressionStatement>): void {
5
+ const expr = statement.get("expression");
6
+ if (!expr.isCallExpression()) return;
7
+ const callee = expr.get("callee");
8
+ if (!callee.isIdentifier()) return;
9
+ if (callee.node.name === "$effect") callee.node.name = MAGIC.createEffect;
10
+ else if (callee.node.name === "$derived") {
11
+ throw statement.buildCodeFrameError(
12
+ "$derived must be assigned to a variable: let doubled = $derived(count*2)",
13
+ );
14
+ }
15
+ }