@sigil-dev/compiler 0.7.5 → 0.7.7
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/package.json +6 -1
- package/src/babel/handlers/dom/derived.ts +31 -1
- package/src/babel/handlers/dom/globals.ts +102 -0
- package/src/babel/index.ts +413 -202
- package/src/babel/jsx/anchor-mount.ts +28 -5
- package/src/babel/jsx/element.ts +794 -546
- package/src/babel/jsx/ssr.ts +16 -7
- package/src/babel/jsx/utils.ts +8 -0
- package/src/babel/util/bind.ts +119 -115
- package/src/babel/util/dead-code.ts +28 -28
- package/src/babel/util/helpers.ts +16 -2
- package/src/babel/util/magic.ts +6 -0
- package/src/bun-plugin.ts +20 -3
- package/src/vite/index.ts +7 -2
- package/test/hydration.test.ts +1 -1
- package/test/jsx.test.ts +144 -142
- package/test/reactivity.test.ts +147 -1
- package/test/tree-shaking.test.ts +78 -79
package/src/babel/jsx/ssr.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { types as t } from "@babel/core";
|
|
2
|
+
import { isGlobalComponent } from "../handlers/dom/globals";
|
|
2
3
|
import { getTagName } from "../util/helpers";
|
|
3
4
|
|
|
4
5
|
const VOID_ELEMENTS = new Set([
|
|
@@ -120,6 +121,13 @@ export function processElementSSR(
|
|
|
120
121
|
const isComponent = /^[A-Z]/.test(tagName);
|
|
121
122
|
|
|
122
123
|
if (isComponent) {
|
|
124
|
+
// Global components (Window/Document/Body) produce no SSR output
|
|
125
|
+
if (isGlobalComponent(tagName)) {
|
|
126
|
+
return t.templateLiteral(
|
|
127
|
+
[t.templateElement({ raw: "", cooked: "" }, true)],
|
|
128
|
+
[],
|
|
129
|
+
);
|
|
130
|
+
}
|
|
123
131
|
return processComponentSSR(node, tagName, signals);
|
|
124
132
|
}
|
|
125
133
|
let dangerousHTML: t.Expression | null = null;
|
|
@@ -210,16 +218,17 @@ function processComponentSSR(
|
|
|
210
218
|
for (const attr of node.openingElement.attributes) {
|
|
211
219
|
if (!t.isJSXAttribute(attr)) continue;
|
|
212
220
|
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
221
|
+
// data-key is a framework-internal attribute for HTML elements only; skip for components
|
|
222
|
+
if (attrName === "data-key" || attrName === "key") continue;
|
|
223
|
+
// use StringLiteral key for hyphenated names, Identifier otherwise
|
|
224
|
+
const propKey = /[^a-zA-Z_$0-9]/.test(attrName)
|
|
225
|
+
? t.stringLiteral(attrName)
|
|
226
|
+
: t.identifier(attrName);
|
|
213
227
|
if (t.isStringLiteral(attr.value)) {
|
|
214
|
-
props.properties.push(
|
|
215
|
-
t.objectProperty(t.identifier(attrName), attr.value),
|
|
216
|
-
);
|
|
228
|
+
props.properties.push(t.objectProperty(propKey, attr.value));
|
|
217
229
|
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
218
230
|
props.properties.push(
|
|
219
|
-
t.objectProperty(
|
|
220
|
-
t.identifier(attrName),
|
|
221
|
-
attr.value.expression as t.Expression,
|
|
222
|
-
),
|
|
231
|
+
t.objectProperty(propKey, attr.value.expression as t.Expression),
|
|
223
232
|
);
|
|
224
233
|
}
|
|
225
234
|
}
|
package/src/babel/jsx/utils.ts
CHANGED
|
@@ -35,6 +35,14 @@ export function containsSignal(
|
|
|
35
35
|
containsSignal(expr.alternate as t.Expression, signals)
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
|
+
if (t.isUnaryExpression(expr)) {
|
|
39
|
+
return containsSignal(expr.argument as t.Expression, signals);
|
|
40
|
+
}
|
|
41
|
+
if (t.isTemplateLiteral(expr)) {
|
|
42
|
+
return expr.expressions.some((e) =>
|
|
43
|
+
containsSignal(e as t.Expression, signals),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
38
46
|
return false;
|
|
39
47
|
}
|
|
40
48
|
|
package/src/babel/util/bind.ts
CHANGED
|
@@ -3,133 +3,137 @@ import { types as t } from "@babel/core";
|
|
|
3
3
|
const BIND_EVENT: Record<string, string> = {
|
|
4
4
|
value: "input",
|
|
5
5
|
checked: "change",
|
|
6
|
+
innerHTML: "input",
|
|
7
|
+
textContent: "input",
|
|
8
|
+
group: "change",
|
|
6
9
|
};
|
|
7
10
|
|
|
8
11
|
export function buildBind(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
varName: string,
|
|
13
|
+
bindProp: string,
|
|
14
|
+
attr: t.JSXAttribute,
|
|
15
|
+
statements: t.Statement[],
|
|
13
16
|
): void {
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const signal = (attr.value as t.JSXExpressionContainer)
|
|
18
|
+
.expression as t.CallExpression; // already text()
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
const signalId = signal.callee as t.Identifier; // text — for .set()
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
statements.push(
|
|
21
|
-
t.expressionStatement(
|
|
22
|
-
t.callExpression(
|
|
23
|
-
t.memberExpression(signalId, t.identifier("set")),
|
|
24
|
-
[t.identifier(varName)],
|
|
25
|
-
),
|
|
26
|
-
),
|
|
27
|
-
);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
22
|
+
const event = BIND_EVENT[bindProp] ?? "input";
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
// createEffect(() => _el.prop = text())
|
|
25
|
+
statements.push(
|
|
26
|
+
t.expressionStatement(
|
|
27
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
28
|
+
t.arrowFunctionExpression(
|
|
29
|
+
[],
|
|
30
|
+
t.blockStatement([
|
|
31
|
+
t.expressionStatement(
|
|
32
|
+
t.assignmentExpression(
|
|
33
|
+
"=",
|
|
34
|
+
t.memberExpression(
|
|
35
|
+
t.identifier(varName),
|
|
36
|
+
t.identifier(bindProp),
|
|
37
|
+
),
|
|
38
|
+
signal,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
]),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
t.stringLiteral(event),
|
|
61
|
-
t.arrowFunctionExpression(
|
|
62
|
-
[t.identifier("e")],
|
|
63
|
-
t.callExpression(
|
|
64
|
-
t.memberExpression(signalId, t.identifier("set")),
|
|
65
|
-
[
|
|
66
|
-
t.memberExpression(
|
|
67
|
-
t.memberExpression(t.identifier("e"), t.identifier("target")),
|
|
68
|
-
t.identifier(prop),
|
|
69
|
-
),
|
|
70
|
-
],
|
|
71
|
-
),
|
|
72
|
-
),
|
|
73
|
-
],
|
|
74
|
-
),
|
|
75
|
-
),
|
|
76
|
-
);
|
|
47
|
+
// addEventListener(event, e => text.set(e.target.prop))
|
|
48
|
+
statements.push(
|
|
49
|
+
t.expressionStatement(
|
|
50
|
+
t.callExpression(
|
|
51
|
+
t.memberExpression(
|
|
52
|
+
t.identifier(varName),
|
|
53
|
+
t.identifier("addEventListener"),
|
|
54
|
+
),
|
|
55
|
+
[
|
|
56
|
+
t.stringLiteral(event),
|
|
57
|
+
t.arrowFunctionExpression(
|
|
58
|
+
[t.identifier("e")],
|
|
59
|
+
t.callExpression(
|
|
60
|
+
t.memberExpression(signalId, t.identifier("set")),
|
|
61
|
+
[
|
|
62
|
+
t.memberExpression(
|
|
63
|
+
t.memberExpression(t.identifier("e"), t.identifier("target")),
|
|
64
|
+
t.identifier(bindProp),
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
],
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
export function buildProxyBind(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
varName: string,
|
|
77
|
+
bindProp: string,
|
|
78
|
+
attr: t.JSXAttribute,
|
|
79
|
+
statements: t.Statement[],
|
|
80
|
+
signal: t.MemberExpression, // already form().name
|
|
85
81
|
): void {
|
|
86
|
-
|
|
87
|
-
const event = BIND_EVENT[prop] ?? "input";
|
|
82
|
+
const event = BIND_EVENT[bindProp] ?? "input";
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
84
|
+
// createEffect(() => el.prop = form().name)
|
|
85
|
+
statements.push(
|
|
86
|
+
t.expressionStatement(
|
|
87
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
88
|
+
t.arrowFunctionExpression(
|
|
89
|
+
[],
|
|
90
|
+
t.blockStatement([
|
|
91
|
+
t.expressionStatement(
|
|
92
|
+
t.assignmentExpression(
|
|
93
|
+
"=",
|
|
94
|
+
t.memberExpression(
|
|
95
|
+
t.identifier(varName),
|
|
96
|
+
t.identifier(bindProp),
|
|
97
|
+
),
|
|
98
|
+
signal,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
]),
|
|
102
|
+
),
|
|
103
|
+
]),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
107
|
+
// addEventListener(event, e => { form().name = e.target.prop })
|
|
108
|
+
statements.push(
|
|
109
|
+
t.expressionStatement(
|
|
110
|
+
t.callExpression(
|
|
111
|
+
t.memberExpression(
|
|
112
|
+
t.identifier(varName),
|
|
113
|
+
t.identifier("addEventListener"),
|
|
114
|
+
),
|
|
115
|
+
[
|
|
116
|
+
t.stringLiteral(event),
|
|
117
|
+
t.arrowFunctionExpression(
|
|
118
|
+
[t.identifier("e")],
|
|
119
|
+
t.blockStatement([
|
|
120
|
+
t.expressionStatement(
|
|
121
|
+
t.assignmentExpression(
|
|
122
|
+
"=",
|
|
123
|
+
signal as unknown as t.LVal, // form().name — valid LVal
|
|
124
|
+
t.memberExpression(
|
|
125
|
+
t.memberExpression(
|
|
126
|
+
t.identifier("e"),
|
|
127
|
+
t.identifier("target"),
|
|
128
|
+
),
|
|
129
|
+
t.identifier(bindProp),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
]),
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
135
139
|
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export function warnDeadReactivity(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
): void {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
1
|
+
import { type NodePath, types as t, traverse } from "@babel/core";
|
|
2
|
+
|
|
3
|
+
export function warnDeadReactivity(
|
|
4
|
+
path: NodePath,
|
|
5
|
+
signals: Set<string>,
|
|
6
|
+
storeSignals: Set<string>,
|
|
7
|
+
filename?: string,
|
|
8
|
+
): void {
|
|
9
|
+
const used = new Set<string>();
|
|
10
|
+
|
|
11
|
+
path.traverse({
|
|
12
|
+
CallExpression(innerPath: any) {
|
|
13
|
+
if (t.isIdentifier(innerPath.node.callee)) {
|
|
14
|
+
used.add(innerPath.node.callee.name);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
for (const name of signals) {
|
|
20
|
+
if (storeSignals.has(name)) continue; // exported, used across files
|
|
21
|
+
if (!used.has(name)) {
|
|
22
|
+
const tag = filename ? `[sigil:${filename}]` : "[sigil]";
|
|
23
|
+
console.warn(
|
|
24
|
+
`${tag} unused reactive declaration: "${name}" is declared with $state or $derived but never read.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -5,8 +5,22 @@ export function getMacro(declarator: NodePath<t.VariableDeclarator>) {
|
|
|
5
5
|
const init = declarator.get("init");
|
|
6
6
|
if (!init.isCallExpression()) return null;
|
|
7
7
|
const callee = init.get("callee");
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
// Handle identifier: $state(x), $derived(x)
|
|
10
|
+
if (callee.isIdentifier()) {
|
|
11
|
+
return { name: callee.node.name, init };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Handle member expression: $state.raw(x), $state.snapshot(x)
|
|
15
|
+
if (callee.isMemberExpression()) {
|
|
16
|
+
const object = callee.get("object");
|
|
17
|
+
const property = callee.get("property");
|
|
18
|
+
if (object.isIdentifier() && property.isIdentifier()) {
|
|
19
|
+
return { name: `${object.node.name}.${property.node.name}`, init };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
export function getTagName(
|
package/src/babel/util/magic.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export const MAGIC = {
|
|
2
2
|
createSignal: "createSignal",
|
|
3
|
+
createRawSignal: "createRawSignal",
|
|
3
4
|
createMemo: "createMemo",
|
|
4
5
|
createEffect: "createEffect",
|
|
6
|
+
createEffectPre: "createEffectPre",
|
|
7
|
+
createInspectEffect: "createInspectEffect",
|
|
8
|
+
withEffectScope: "withEffectScope",
|
|
9
|
+
tracking: "tracking",
|
|
10
|
+
snapshot: "snapshot",
|
|
5
11
|
} as const;
|
package/src/bun-plugin.ts
CHANGED
|
@@ -26,7 +26,11 @@ function shouldTransform(path: string): boolean {
|
|
|
26
26
|
|
|
27
27
|
function shouldTransformSource(path: string, code: string): boolean {
|
|
28
28
|
const normalized = normalizePath(path);
|
|
29
|
-
return
|
|
29
|
+
return (
|
|
30
|
+
JSX_RE.test(normalized) ||
|
|
31
|
+
SIGIL_MACRO_RE.test(code) ||
|
|
32
|
+
STYLE_TEST_RE.test(code)
|
|
33
|
+
);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export function sigil(options?: {
|
|
@@ -36,10 +40,23 @@ export function sigil(options?: {
|
|
|
36
40
|
name: "sigil",
|
|
37
41
|
setup(build) {
|
|
38
42
|
const transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
43
|
+
|
|
44
|
+
// This is required for both linking errors,
|
|
45
|
+
// and for a bug in the bundler, which was reported in https://github.com/oven-sh/bun/issues/13897 (2 YEARSS AGO)
|
|
46
|
+
// and apparently fixed in https://github.com/oven-sh/bun/pull/15119. It wasnt
|
|
47
|
+
build.onResolve({ filter: /^@sigil-dev\/runtime($|\/)/ }, (args) => {
|
|
48
|
+
try {
|
|
49
|
+
const resolved = Bun.resolveSync(args.path, process.cwd());
|
|
50
|
+
return { path: resolved.replaceAll("\\", "/") };
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
build.onLoad({ filter: SOURCE_RE }, async ({ path }) => {
|
|
40
|
-
if (!shouldTransform(path)) return;
|
|
57
|
+
if (!shouldTransform(path)) return undefined;
|
|
41
58
|
let code = await Bun.file(path).text();
|
|
42
|
-
if (!shouldTransformSource(path, code)) return;
|
|
59
|
+
if (!shouldTransformSource(path, code)) return undefined;
|
|
43
60
|
// console.log("[bun-plugin] intercepting:", path);
|
|
44
61
|
const hash = computeHash(path);
|
|
45
62
|
let scopedCSS = "";
|
package/src/vite/index.ts
CHANGED
|
@@ -5,9 +5,13 @@ import { computeHash, scopeCSS } from "../babel/util/css.ts";
|
|
|
5
5
|
|
|
6
6
|
const STYLE_RE = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
7
7
|
|
|
8
|
-
export function sigil(options?: {
|
|
8
|
+
export function sigil(options?: {
|
|
9
|
+
hydrate?: boolean;
|
|
10
|
+
styleNonce?: string;
|
|
11
|
+
}): Plugin {
|
|
9
12
|
// Track injected CSS per component to avoid duplicates
|
|
10
13
|
const injected = new Set<string>();
|
|
14
|
+
const styleNonce = options?.styleNonce ?? "";
|
|
11
15
|
|
|
12
16
|
return {
|
|
13
17
|
name: "sigil",
|
|
@@ -35,10 +39,11 @@ export function sigil(options?: { hydrate?: boolean }): Plugin {
|
|
|
35
39
|
let cssInjection = "";
|
|
36
40
|
if (scopedCSS && !injected.has(id)) {
|
|
37
41
|
injected.add(id);
|
|
42
|
+
const nonceAttr = styleNonce ? ` nonce="${styleNonce}"` : "";
|
|
38
43
|
cssInjection = `
|
|
39
44
|
if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
|
|
40
45
|
const __style = document.createElement('style');
|
|
41
|
-
__style.id = 'sigil-${hash}'
|
|
46
|
+
__style.id = 'sigil-${hash}';${nonceAttr ? `\n __style.setAttribute('nonce', '${styleNonce}');` : ""}
|
|
42
47
|
__style.textContent = ${JSON.stringify(scopedCSS)};
|
|
43
48
|
document.head.appendChild(__style);
|
|
44
49
|
}`;
|
package/test/hydration.test.ts
CHANGED
|
@@ -112,7 +112,7 @@ describe("Hydration", () => {
|
|
|
112
112
|
test("imports claim and claimText from @sigil-dev/runtime", () => {
|
|
113
113
|
const code = transform(`const el = <div></div>`, { mode: "hydrate" });
|
|
114
114
|
expect(code).toContain(
|
|
115
|
-
"createSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, insert",
|
|
115
|
+
"createSignal, createRawSignal, createEffect, createMemo, reconcile, claim, claimText, claimComment, hydrateKeyedList, insert, getHydrationNodes, pushHydrationNodes, popHydrationNodes, snapshot, tracking, createEffectPre, createInspectEffect, withEffectScope",
|
|
116
116
|
);
|
|
117
117
|
});
|
|
118
118
|
|