@sigil-dev/compiler 0.3.0

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Sigil Compiler
2
+
3
+ Compiles JSX down to `document.createElement()` calls
package/index.ts ADDED
@@ -0,0 +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";
package/jsr.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@sigil-dev/compiler",
3
+ "version": "0.2.0",
4
+ "exports": "./index.ts",
5
+ "publish": {
6
+ "exclude": ["src/**/*.test.ts"]
7
+ }
8
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@sigil-dev/compiler",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "version": "0.3.0",
6
+ "private": false,
7
+ "description": "Compiler for the Sigil framework",
8
+ "peerDependencies": {
9
+ "typescript": "^5"
10
+ },
11
+ "exports": {
12
+ ".": "./index.ts",
13
+ "./babel": "./src/babel/index.ts",
14
+ "./vite": "./src/vite/index.ts",
15
+ "./bun": "./src/bun-plugin.ts"
16
+ },
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"
23
+ }
24
+ }
@@ -0,0 +1,32 @@
1
+ import type { NodePath } from "@babel/core";
2
+ import { types as t } from "@babel/core";
3
+ import { MAGIC } from "../../util/magic.ts";
4
+
5
+ export function handleDerived(
6
+ declaration: NodePath<t.VariableDeclaration>,
7
+ declarator: NodePath<t.VariableDeclarator>,
8
+ init: NodePath<t.CallExpression>,
9
+ signals: Set<string>,
10
+ ): void {
11
+ const { id } = declarator.node;
12
+ if (!t.isIdentifier(id))
13
+ throw new Error("Destructured $derived not supported");
14
+ const name = id.name;
15
+ signals.add(id.name);
16
+
17
+ const binding = declaration.scope.getBinding(name);
18
+ if (!binding) return;
19
+ const refs = [...binding.referencePaths];
20
+
21
+ init.replaceWith(
22
+ t.callExpression(t.identifier(MAGIC.createMemo), [
23
+ t.arrowFunctionExpression([], init.node.arguments[0] as t.Expression),
24
+ ]),
25
+ );
26
+ declaration.node.kind = "const";
27
+ for (const ref of refs) {
28
+ if (!ref.isIdentifier()) continue;
29
+ if (ref.parentPath?.isUpdateExpression()) continue;
30
+ ref.replaceWith(t.callExpression(ref.node, []));
31
+ }
32
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,78 @@
1
+ import type { NodePath } from "@babel/core";
2
+ import { types as t } from "@babel/core";
3
+ import { MAGIC } from "../../util/magic.ts";
4
+ export function handleState(
5
+ declaration: NodePath<t.VariableDeclaration>,
6
+ declarator: NodePath<t.VariableDeclarator>,
7
+ init: NodePath<t.CallExpression>,
8
+ signals: Set<string>,
9
+ ): void {
10
+ // AST has to be perfect so we're using type safe
11
+ // replacement instead of just swapping method name everywhere.
12
+
13
+ const { id } = declarator.node;
14
+ if (!t.isIdentifier(id)) throw new Error("Destructured $state not supported");
15
+ const name = id.name; // <-- "count"
16
+
17
+ const setCall = (
18
+ value: t.Expression, // <-- now captures "count"
19
+ ) =>
20
+ t.callExpression(
21
+ t.memberExpression(t.identifier(name), t.identifier("set")),
22
+ [value],
23
+ );
24
+
25
+ const binding = declaration.scope.getBinding(name);
26
+ if (!binding) return;
27
+ const refs = [...binding.referencePaths];
28
+ const violations = [...binding.constantViolations]; // snapshot
29
+
30
+ // rewrite declaration
31
+ init.replaceWith(
32
+ t.callExpression(t.identifier(MAGIC.createSignal), init.node.arguments),
33
+ );
34
+ declaration.node.kind = "const";
35
+
36
+ // rewrite writes
37
+ // violations go before refs becase UpdateExpression
38
+ // is both a read and a write and this prevents count()++
39
+ for (const violation of violations) {
40
+ if (violation.isUpdateExpression()) {
41
+ const op = violation.node.operator === "++" ? "+" : "-";
42
+ violation.replaceWith(
43
+ setCall(
44
+ t.binaryExpression(
45
+ op,
46
+ t.callExpression(t.identifier(name), []),
47
+ t.numericLiteral(1),
48
+ ),
49
+ ),
50
+ );
51
+ } else if (violation.isAssignmentExpression()) {
52
+ const { operator, right } = violation.node;
53
+ if (operator === "=") {
54
+ // count = 5 -> count.set(5)
55
+ violation.replaceWith(setCall(right));
56
+ } else {
57
+ // count += 2 -> count.set(count() + 2)
58
+ const baseOp = operator.slice(0, -1);
59
+ violation.replaceWith(
60
+ setCall(
61
+ t.binaryExpression(
62
+ baseOp as t.BinaryExpression["operator"],
63
+ t.callExpression(t.identifier(name), []),
64
+ right,
65
+ ),
66
+ ),
67
+ );
68
+ }
69
+ }
70
+ }
71
+
72
+ for (const ref of refs) {
73
+ if (!ref.isIdentifier()) continue;
74
+ // weird syntax, but just swaps the read with a call.
75
+ ref.replaceWith(t.callExpression(ref.node, []));
76
+ }
77
+ signals.add(name);
78
+ }
@@ -0,0 +1,26 @@
1
+ import { type NodePath, types as t } from "@babel/core";
2
+
3
+ export function handleDerivedSSR(
4
+ declaration: NodePath<t.VariableDeclaration>,
5
+ declarator: NodePath<t.VariableDeclarator>,
6
+ init: NodePath<t.CallExpression>,
7
+ ): void {
8
+ const { id } = declarator.node;
9
+ if (!t.isIdentifier(id))
10
+ throw new Error("Destructured $derived not supported");
11
+
12
+ // Replace $derived(expr) with () => expr (lazy evaluation)
13
+ init.replaceWith(
14
+ t.arrowFunctionExpression([], init.node.arguments[0] as t.Expression),
15
+ );
16
+ declaration.node.kind = "const";
17
+
18
+ // Replace all references to this variable with () calls
19
+ const binding = declaration.scope.getBinding(id.name);
20
+ if (!binding) return;
21
+ for (const ref of [...binding.referencePaths]) {
22
+ if (!ref.isIdentifier()) continue;
23
+ if (ref.parentPath?.isUpdateExpression()) continue;
24
+ ref.replaceWith(t.callExpression(ref.node, []));
25
+ }
26
+ }
@@ -0,0 +1,10 @@
1
+ import type { NodePath, types as t } from "@babel/core";
2
+
3
+ export function handleStateSSR(
4
+ declaration: NodePath<t.VariableDeclaration>,
5
+ declarator: NodePath<t.VariableDeclarator>,
6
+ init: NodePath<t.CallExpression>,
7
+ ): void {
8
+ init.replaceWith(init.node.arguments[0] as t.Expression);
9
+ declaration.node.kind = "let";
10
+ }
@@ -0,0 +1,239 @@
1
+ import { type PluginObject, types as t, template } from "@babel/core";
2
+ import { handleDerived } from "./handlers/dom/derived.ts";
3
+ import { handleEffect } from "./handlers/dom/effect.ts";
4
+ import { handleState } from "./handlers/dom/state.ts";
5
+ import { handleDerivedSSR } from "./handlers/ssr/derived.ts";
6
+ import { handleStateSSR } from "./handlers/ssr/state.ts";
7
+ import { processElement, processFragment } from "./jsx/index.ts";
8
+ import { processElementSSR, processFragmentSSR } from "./jsx/ssr.ts";
9
+ import { getMacro } from "./util/helpers.ts";
10
+
11
+ const SSR_HELPERS = template.statements.ast(`
12
+ const __SAFE = Symbol.for('sigil.safe');
13
+ class __H {
14
+ constructor(v) { this.v = v; this[__SAFE] = true; }
15
+ toString() { return this.v; }
16
+ }
17
+ const __h = (v) => {
18
+ if (v?.[__SAFE]) return v;
19
+ if (Array.isArray(v)) return new __H(v.map(x => x?.[__SAFE] ? x.v : __e(x)).join(''));
20
+ return new __H(String(v ?? ''));
21
+ };
22
+ const __e = (v) => {
23
+ if (v?.[__SAFE]) return v.v;
24
+ if (Array.isArray(v)) return v.map(x => x?.[__SAFE] ? x.v : __e(x)).join('');
25
+ if (v === false || v === null || v === undefined) return '';
26
+ return String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
27
+ };
28
+ `);
29
+
30
+ /**
31
+ * Recursively convert key={expr} to data-key={expr} in JSX tree.
32
+ * Needed for SSR output so hydration can map items by key.
33
+ */
34
+ function convertKeyToDataKey(node: t.JSXElement): void {
35
+ for (let i = 0; i < node.openingElement.attributes.length; i++) {
36
+ const attr = node.openingElement.attributes[i];
37
+ if (
38
+ t.isJSXAttribute(attr) &&
39
+ t.isJSXIdentifier(attr.name) &&
40
+ attr.name.name === "key"
41
+ ) {
42
+ node.openingElement.attributes[i] = t.jsxAttribute(
43
+ t.jsxIdentifier("data-key"),
44
+ attr.value,
45
+ );
46
+ }
47
+ }
48
+ for (const child of node.children) {
49
+ if (t.isJSXElement(child)) {
50
+ convertKeyToDataKey(child);
51
+ } else if (
52
+ t.isJSXExpressionContainer(child) &&
53
+ t.isJSXElement(child.expression)
54
+ ) {
55
+ convertKeyToDataKey(child.expression);
56
+ }
57
+ }
58
+ }
59
+
60
+ interface SigilOptions {
61
+ hash?: string;
62
+ mode?: "dom" | "ssr" | "hydrate";
63
+ }
64
+
65
+ export default function sigilPlugin(): PluginObject {
66
+ const signals = new Set<string>();
67
+ let scopedHash: string | undefined;
68
+ let isSSR = false;
69
+ let isHydrate = false;
70
+
71
+ return {
72
+ name: "sigil",
73
+ visitor: {
74
+ Program: {
75
+ enter(path, state) {
76
+ const opts = (state.opts || {}) as SigilOptions;
77
+ scopedHash = opts.hash;
78
+ isSSR = opts.mode === "ssr";
79
+ isHydrate = opts.mode === "hydrate";
80
+
81
+ // SSR doesn't need any runtime imports except the `escape` utility.
82
+ if (isSSR) {
83
+ path.unshiftContainer("body", SSR_HELPERS);
84
+ return;
85
+ }
86
+
87
+ path.unshiftContainer(
88
+ "body",
89
+ t.importDeclaration(
90
+ [
91
+ t.importSpecifier(
92
+ t.identifier("createSignal"),
93
+ t.identifier("createSignal"),
94
+ ),
95
+ t.importSpecifier(
96
+ t.identifier("createEffect"),
97
+ t.identifier("createEffect"),
98
+ ),
99
+ t.importSpecifier(
100
+ t.identifier("createMemo"),
101
+ t.identifier("createMemo"),
102
+ ),
103
+ t.importSpecifier(
104
+ t.identifier("reconcile"),
105
+ t.identifier("reconcile"),
106
+ ),
107
+ t.importSpecifier(t.identifier("claim"), t.identifier("claim")),
108
+ t.importSpecifier(
109
+ t.identifier("claimText"),
110
+ t.identifier("claimText"),
111
+ ),
112
+ t.importSpecifier(
113
+ t.identifier("claimComment"),
114
+ t.identifier("claimComment"),
115
+ ),
116
+ t.importSpecifier(
117
+ t.identifier("hydrateKeyedList"),
118
+ t.identifier("hydrateKeyedList"),
119
+ ),
120
+ t.importSpecifier(
121
+ t.identifier("insert"),
122
+ t.identifier("insert"),
123
+ ),
124
+ ],
125
+ t.stringLiteral("@sigil-dev/runtime"),
126
+ ),
127
+ );
128
+ },
129
+ },
130
+ VariableDeclaration(path) {
131
+ for (const declarator of path.get("declarations")) {
132
+ const macro = getMacro(declarator);
133
+ if (!macro) continue;
134
+ const { name, init } = macro;
135
+ if (name === "$state") {
136
+ isSSR
137
+ ? handleStateSSR(path, declarator, init)
138
+ : handleState(path, declarator, init, signals);
139
+ } else if (name === "$derived") {
140
+ isSSR
141
+ ? handleDerivedSSR(path, declarator, init)
142
+ : handleDerived(path, declarator, init, signals);
143
+ }
144
+ }
145
+ },
146
+ ExpressionStatement(path) {
147
+ if (isSSR) {
148
+ // drop $effect entirely in SSR
149
+ const expr = path.get("expression");
150
+ if (expr.isCallExpression()) {
151
+ const callee = expr.get("callee");
152
+ if (callee.isIdentifier() && callee.node.name === "$effect") {
153
+ path.remove();
154
+ }
155
+ }
156
+ return;
157
+ }
158
+ handleEffect(path);
159
+ },
160
+ JSXElement(path) {
161
+ if (path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment())
162
+ return;
163
+ if (isSSR) {
164
+ convertKeyToDataKey(path.node);
165
+ path.replaceWith(
166
+ t.callExpression(t.identifier("__h"), [
167
+ processElementSSR(path.node, signals),
168
+ ]),
169
+ );
170
+ return;
171
+ }
172
+ const statements: t.Statement[] = [];
173
+ const genId = (() => {
174
+ let i = 0;
175
+ return () => `_el${i++}`;
176
+ })();
177
+ const root = processElement(
178
+ path.node,
179
+ statements,
180
+ genId,
181
+ signals,
182
+ scopedHash,
183
+ isHydrate,
184
+ isHydrate ? "__nodes" : undefined,
185
+ );
186
+ path.replaceWith(
187
+ t.callExpression(
188
+ t.arrowFunctionExpression(
189
+ [],
190
+ t.blockStatement([
191
+ ...statements,
192
+ t.returnStatement(t.identifier(root)),
193
+ ]),
194
+ ),
195
+ [],
196
+ ),
197
+ );
198
+ },
199
+ JSXFragment(path) {
200
+ if (path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment())
201
+ return;
202
+ if (isSSR) {
203
+ path.replaceWith(
204
+ t.callExpression(t.identifier("__h"), [
205
+ processFragmentSSR(path.node, signals),
206
+ ]),
207
+ );
208
+ return;
209
+ }
210
+ const statements: t.Statement[] = [];
211
+ const genId = (() => {
212
+ let i = 0;
213
+ return () => `_el${i++}`;
214
+ })();
215
+ const root = processFragment(
216
+ path.node,
217
+ statements,
218
+ genId,
219
+ signals,
220
+ scopedHash,
221
+ isHydrate,
222
+ isHydrate ? "__nodes" : undefined,
223
+ );
224
+ path.replaceWith(
225
+ t.callExpression(
226
+ t.arrowFunctionExpression(
227
+ [],
228
+ t.blockStatement([
229
+ ...statements,
230
+ t.returnStatement(t.identifier(root)),
231
+ ]),
232
+ ),
233
+ [],
234
+ ),
235
+ );
236
+ },
237
+ },
238
+ };
239
+ }