@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.
@@ -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
  }
@@ -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
 
@@ -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
- varName: string,
10
- attrName: string,
11
- attr: t.JSXAttribute,
12
- statements: t.Statement[],
12
+ varName: string,
13
+ bindProp: string,
14
+ attr: t.JSXAttribute,
15
+ statements: t.Statement[],
13
16
  ): void {
14
- const signal = (attr.value as t.JSXExpressionContainer)
15
- .expression as t.CallExpression; // already text()
17
+ const signal = (attr.value as t.JSXExpressionContainer)
18
+ .expression as t.CallExpression; // already text()
16
19
 
17
- const signalId = signal.callee as t.Identifier; // text — for .set()
20
+ const signalId = signal.callee as t.Identifier; // text — for .set()
18
21
 
19
- if (attrName === "bindThis") {
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
- const prop = attrName.slice(4).toLowerCase();
32
- const event = BIND_EVENT[prop] ?? "input";
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
- // createEffect(() => _el.prop = text())
35
- statements.push(
36
- t.expressionStatement(
37
- t.callExpression(t.identifier("createEffect"), [
38
- t.arrowFunctionExpression(
39
- [],
40
- t.blockStatement([
41
- t.expressionStatement(
42
- t.assignmentExpression(
43
- "=",
44
- t.memberExpression(t.identifier(varName), t.identifier(prop)),
45
- signal, // text() — already correct
46
- ),
47
- ),
48
- ]),
49
- ),
50
- ]),
51
- ),
52
- );
53
-
54
- // addEventListener(event, e => text.set(e.target.prop))
55
- statements.push(
56
- t.expressionStatement(
57
- t.callExpression(
58
- t.memberExpression(t.identifier(varName), t.identifier("addEventListener")),
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
- varName: string,
81
- attrName: string,
82
- attr: t.JSXAttribute,
83
- statements: t.Statement[],
84
- signal: t.MemberExpression, // already form().name
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
- const prop = attrName.slice(4).toLowerCase();
87
- const event = BIND_EVENT[prop] ?? "input";
82
+ const event = BIND_EVENT[bindProp] ?? "input";
88
83
 
89
- // createEffect(() => el.prop = form().name)
90
- statements.push(
91
- t.expressionStatement(
92
- t.callExpression(t.identifier("createEffect"), [
93
- t.arrowFunctionExpression(
94
- [],
95
- t.blockStatement([
96
- t.expressionStatement(
97
- t.assignmentExpression(
98
- "=",
99
- t.memberExpression(t.identifier(varName), t.identifier(prop)),
100
- signal,
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
- // addEventListener(event, e => { form().name = e.target.prop })
110
- statements.push(
111
- t.expressionStatement(
112
- t.callExpression(
113
- t.memberExpression(t.identifier(varName), t.identifier("addEventListener")),
114
- [
115
- t.stringLiteral(event),
116
- t.arrowFunctionExpression(
117
- [t.identifier("e")],
118
- t.blockStatement([
119
- t.expressionStatement(
120
- t.assignmentExpression(
121
- "=",
122
- signal as unknown as t.LVal, // form().name — valid LVal
123
- t.memberExpression(
124
- t.memberExpression(t.identifier("e"), t.identifier("target")),
125
- t.identifier(prop),
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 { traverse, types as t, NodePath } 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
- }
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
- if (!callee.isIdentifier()) return null;
9
- return { name: callee.node.name, init };
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(
@@ -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 JSX_RE.test(normalized) || SIGIL_MACRO_RE.test(code) || STYLE_TEST_RE.test(code);
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?: { hydrate?: boolean }): Plugin {
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
  }`;
@@ -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