@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 +3 -0
- package/index.ts +4 -0
- package/jsr.json +8 -0
- package/package.json +24 -0
- package/src/babel/handlers/dom/derived.ts +32 -0
- package/src/babel/handlers/dom/effect.ts +15 -0
- package/src/babel/handlers/dom/state.ts +78 -0
- package/src/babel/handlers/ssr/derived.ts +26 -0
- package/src/babel/handlers/ssr/state.ts +10 -0
- package/src/babel/index.ts +239 -0
- package/src/babel/jsx/anchor-mount.ts +366 -0
- package/src/babel/jsx/children.ts +124 -0
- package/src/babel/jsx/element.ts +520 -0
- package/src/babel/jsx/fragment.ts +279 -0
- package/src/babel/jsx/index.ts +2 -0
- package/src/babel/jsx/keyed-list.ts +278 -0
- package/src/babel/jsx/ssr.ts +309 -0
- package/src/babel/jsx/text-node.ts +192 -0
- package/src/babel/jsx/utils.ts +124 -0
- package/src/babel/util/bind.ts +75 -0
- package/src/babel/util/css.ts +151 -0
- package/src/babel/util/helpers.ts +32 -0
- package/src/babel/util/magic.ts +5 -0
- package/src/bun-plugin.ts +45 -0
- package/src/vite/index.ts +53 -0
- package/test/components.test.ts +129 -0
- package/test/fragments.test.ts +90 -0
- package/test/helpers/transform.ts +26 -0
- package/test/hydration.test.ts +147 -0
- package/test/jsx.test.ts +120 -0
- package/test/keyed-lists.test.ts +88 -0
- package/test/reactivity.test.ts +79 -0
- package/test/scoped-css.test.ts +129 -0
- package/test/ssr.test.ts +130 -0
- package/tsconfig.json +30 -0
package/README.md
ADDED
package/index.ts
ADDED
package/jsr.json
ADDED
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|
+
}
|