@sigil-dev/compiler 0.7.7 → 0.8.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@sigil-dev/compiler",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.7.7",
5
+ "version": "0.8.2",
6
6
  "private": false,
7
7
  "description": "Compiler for the Sigil framework",
8
8
  "peerDependencies": {
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@babel/generator": "^7.29.7",
26
+ "@babel/parser": "^7.29.7",
26
27
  "@babel/plugin-transform-typescript": "^8.0.0-rc.6",
27
28
  "@babel/preset-typescript": "^8.0.0-rc.6"
28
29
  }
@@ -4,6 +4,7 @@ export function handleDerivedSSR(
4
4
  declaration: NodePath<t.VariableDeclaration>,
5
5
  declarator: NodePath<t.VariableDeclarator>,
6
6
  init: NodePath<t.CallExpression>,
7
+ _signals?: Set<string>, // SSR has no reactivity, intentionally unused
7
8
  ): void {
8
9
  const { id } = declarator.node;
9
10
  if (!t.isIdentifier(id))
@@ -2,8 +2,9 @@ import type { NodePath, types as t } from "@babel/core";
2
2
 
3
3
  export function handleStateSSR(
4
4
  declaration: NodePath<t.VariableDeclaration>,
5
- declarator: NodePath<t.VariableDeclarator>,
5
+ _declarator: NodePath<t.VariableDeclarator>,
6
6
  init: NodePath<t.CallExpression>,
7
+ _signals?: Set<string>, // SSR has no reactivity. Left in for future reference
7
8
  ): void {
8
9
  init.replaceWith(init.node.arguments[0] as t.Expression);
9
10
  declaration.node.kind = "let";
@@ -115,12 +115,6 @@ export default function sigilPlugin(): PluginObject {
115
115
  "createInspectEffect",
116
116
  "withEffectScope",
117
117
  ];
118
- console.log(
119
- "[sigil exit] didTransform:",
120
- didTransform,
121
- "withEffectScope in required:",
122
- requiredSpecifiers.includes("withEffectScope"),
123
- );
124
118
  const runtimeImport = path.node.body.find(
125
119
  (n): n is t.ImportDeclaration =>
126
120
  t.isImportDeclaration(n) &&
@@ -128,10 +122,10 @@ export default function sigilPlugin(): PluginObject {
128
122
  );
129
123
 
130
124
  if (runtimeImport) {
131
- console.log(
132
- "[sigil exit] found existing import, specifier count:",
133
- runtimeImport.specifiers.length,
134
- );
125
+ // console.log(
126
+ // "[sigil exit] found existing import, specifier count:",
127
+ // runtimeImport.specifiers.length,
128
+ // );
135
129
  const existingNames = new Set(
136
130
  runtimeImport.specifiers
137
131
  .filter(t.isImportSpecifier)
@@ -299,11 +293,13 @@ export default function sigilPlugin(): PluginObject {
299
293
  const innerCallee = object.get("callee");
300
294
  if (innerCallee.isIdentifier({ name: "$inspect" })) {
301
295
  didTransform = true;
296
+ //@ts-expect-error Bizzare but it works if you read LSP
302
297
  const args = object.node.arguments.map(wrapSignal);
303
298
  const cb = path.node.arguments[0];
304
299
  path.replaceWith(
305
300
  t.callExpression(t.identifier(MAGIC.createInspectEffect), [
306
301
  t.arrowFunctionExpression([], t.arrayExpression(args)),
302
+ //@ts-expect-error Bizzare but it works if you read LSP
307
303
  cb,
308
304
  ]),
309
305
  );
@@ -348,6 +344,7 @@ export default function sigilPlugin(): PluginObject {
348
344
  // Handle standalone $inspect(...)
349
345
  if (callee.isIdentifier({ name: "$inspect" })) {
350
346
  didTransform = true;
347
+ //@ts-expect-error Bizzare but it works if you read LSP
351
348
  const args = path.node.arguments.map(wrapSignal);
352
349
  path.replaceWith(
353
350
  t.callExpression(t.identifier(MAGIC.createInspectEffect), [
@@ -8,7 +8,7 @@ import { processFragment } from "./fragment";
8
8
  */
9
9
  export function collectChildren(
10
10
  node: t.JSXElement,
11
- statements: t.Statement[],
11
+ _statements: t.Statement[],
12
12
  genId: () => string,
13
13
  signals: Set<string>,
14
14
  hash?: string,
@@ -1,4 +1,5 @@
1
1
  import { types as t } from "@babel/core";
2
+ //@ts-expect-error Not our problem babel forgot tp export types.
2
3
  import { generate } from "@babel/generator";
3
4
  import {
4
5
  handleGlobalComponent,
@@ -294,14 +295,14 @@ export function processElement(
294
295
  if (bindProp === "group") {
295
296
  // Scan element attributes for type and value
296
297
  let inputType = "";
297
- let inputValue: t.StringLiteral | undefined;
298
+ let _inputValue: t.StringLiteral | undefined;
298
299
  for (const a of node.openingElement.attributes) {
299
300
  if (t.isJSXAttribute(a) && t.isJSXIdentifier(a.name)) {
300
301
  if (a.name.name === "type" && t.isStringLiteral(a.value)) {
301
302
  inputType = a.value.value;
302
303
  }
303
304
  if (a.name.name === "value" && t.isStringLiteral(a.value)) {
304
- inputValue = a.value;
305
+ _inputValue = a.value;
305
306
  }
306
307
  }
307
308
  }
@@ -6,12 +6,16 @@ export const ATTR_MAP: Record<string, string> = {
6
6
  };
7
7
 
8
8
  export function containsSignal(
9
- expr: t.Expression,
9
+ expr: t.Expression | t.Node,
10
10
  signals: Set<string>,
11
11
  ): boolean {
12
+ if (!expr) return false;
12
13
  if (t.isIdentifier(expr)) return signals.has(expr.name);
13
14
  if (t.isCallExpression(expr)) {
14
- return containsSignal(expr.callee as t.Expression, signals);
15
+ return (
16
+ containsSignal(expr.callee as t.Expression, signals) ||
17
+ expr.arguments.some((a) => containsSignal(a as t.Expression, signals))
18
+ );
15
19
  }
16
20
  if (t.isBinaryExpression(expr)) {
17
21
  return (
@@ -43,6 +47,49 @@ export function containsSignal(
43
47
  containsSignal(e as t.Expression, signals),
44
48
  );
45
49
  }
50
+ // ── additions ──
51
+ if (t.isArrayExpression(expr)) {
52
+ return expr.elements.some(
53
+ (e) => e !== null && containsSignal(e as t.Expression, signals),
54
+ );
55
+ }
56
+ if (t.isObjectExpression(expr)) {
57
+ return expr.properties.some((p) => {
58
+ if (t.isObjectProperty(p)) {
59
+ const computedKey = p.computed
60
+ ? containsSignal(p.key as t.Expression, signals)
61
+ : false;
62
+ return computedKey || containsSignal(p.value as t.Expression, signals);
63
+ }
64
+ if (t.isSpreadElement(p)) return containsSignal(p.argument, signals);
65
+ return false;
66
+ });
67
+ }
68
+ if (t.isSequenceExpression(expr)) {
69
+ return expr.expressions.some((e) => containsSignal(e, signals));
70
+ }
71
+ if (t.isAssignmentExpression(expr)) {
72
+ return containsSignal(expr.right, signals);
73
+ }
74
+ if (t.isSpreadElement(expr)) {
75
+ return containsSignal(expr.argument, signals);
76
+ }
77
+ // ArrowFunction/Function — don't traverse, signals inside are captured
78
+ // not reactive at the call site (onclick={() => count()} is fine as-is)
79
+ if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
80
+ return false;
81
+ }
82
+ // Babel represents ?. as distinct node types not in t.is* guards
83
+ if (t.isOptionalMemberExpression(expr)) {
84
+ return containsSignal((expr as any).object, signals);
85
+ }
86
+ if (t.isOptionalCallExpression(expr)) {
87
+ const n = expr as any;
88
+ return (
89
+ containsSignal(n.callee, signals) ||
90
+ n.arguments.some((a: t.Node) => containsSignal(a, signals))
91
+ );
92
+ }
46
93
  return false;
47
94
  }
48
95
 
@@ -75,7 +75,7 @@ export function buildBind(
75
75
  export function buildProxyBind(
76
76
  varName: string,
77
77
  bindProp: string,
78
- attr: t.JSXAttribute,
78
+ _attr: t.JSXAttribute,
79
79
  statements: t.Statement[],
80
80
  signal: t.MemberExpression, // already form().name
81
81
  ): void {
@@ -1,4 +1,4 @@
1
- import { createHash } from "crypto";
1
+ import { createHash } from "node:crypto";
2
2
 
3
3
  /**
4
4
  * Compute a deterministic hash from a file path.
@@ -6,7 +6,7 @@ import { createHash } from "crypto";
6
6
  * 's' prefix for Sigil.
7
7
  */
8
8
  export function computeHash(filePath: string): string {
9
- return "s" + createHash("md5").update(filePath).digest("hex").slice(0, 8);
9
+ return `s${createHash("md5").update(filePath).digest("hex").slice(0, 8)}`;
10
10
  }
11
11
 
12
12
  /**
@@ -141,10 +141,10 @@ function rewriteSelector(selector: string, hash: string): string {
141
141
  // Check for placeholder
142
142
  const globalMatch = trimmed.match(/^__GLOBAL_(\d+)__$/);
143
143
  if (globalMatch) {
144
- return globals[parseInt(globalMatch[1]!)];
144
+ return globals[parseInt(globalMatch[1]!, 10)];
145
145
  }
146
146
 
147
- return trimmed + "." + hash;
147
+ return `${trimmed}.${hash}`;
148
148
  });
149
149
 
150
150
  return parts.join(", ");
@@ -1,4 +1,4 @@
1
- import { type NodePath, types as t, traverse } from "@babel/core";
1
+ import { type NodePath, types as t } from "@babel/core";
2
2
 
3
3
  export function warnDeadReactivity(
4
4
  path: NodePath,
@@ -19,7 +19,7 @@ export function warnDeadReactivity(
19
19
  for (const name of signals) {
20
20
  if (storeSignals.has(name)) continue; // exported, used across files
21
21
  if (!used.has(name)) {
22
- const tag = filename ? `[sigil:${filename}]` : "[sigil]";
22
+ const tag = filename ? `[${filename}]` : "[sigil]";
23
23
  console.warn(
24
24
  `${tag} unused reactive declaration: "${name}" is declared with $state or $derived but never read.`,
25
25
  );
package/src/bun-plugin.ts CHANGED
@@ -73,14 +73,13 @@ export function sigil(options?: {
73
73
  });
74
74
  let out = transpiler.transformSync(res?.code ?? "");
75
75
  if (scopedCSS && options?.mode !== "ssr") {
76
- out =
77
- `
76
+ out = `
78
77
  if (typeof document !== 'undefined' && !document.getElementById('sigil-${hash}')) {
79
78
  const __style = document.createElement('style');
80
79
  __style.id = 'sigil-${hash}';
81
80
  __style.textContent = ${JSON.stringify(scopedCSS)};
82
81
  document.head.appendChild(__style);
83
- }` + out;
82
+ }${out}`;
84
83
  }
85
84
  return { contents: out, loader: "js" };
86
85
  });
@@ -0,0 +1,288 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseExpression } from "@babel/parser";
3
+ import type { Expression } from "@babel/types";
4
+ import { containsSignal } from "../src/babel/jsx/utils";
5
+
6
+ // Our containsSignal is so jank it needs a dedicated test suite.
7
+
8
+ // ── helpers ──────────────────────────────────────────────────────────────────
9
+
10
+ function parse(code: string): Expression {
11
+ return parseExpression(code, {
12
+ plugins: ["typescript", "jsx"],
13
+ strictMode: false,
14
+ }) as Expression;
15
+ }
16
+
17
+ /** Signals in scope for all tests */
18
+ const SIG = new Set(["count", "name", "items", "user", "flag"]);
19
+
20
+ function yes(code: string, msg?: string) {
21
+ it(msg ?? `contains signal: ${code}`, () => {
22
+ expect(containsSignal(parse(code), SIG)).toBe(true);
23
+ });
24
+ }
25
+
26
+ function no(code: string, msg?: string) {
27
+ it(msg ?? `no signal: ${code}`, () => {
28
+ expect(containsSignal(parse(code), SIG)).toBe(false);
29
+ });
30
+ }
31
+
32
+ // ── Identifier ───────────────────────────────────────────────────────────────
33
+
34
+ describe("Identifier", () => {
35
+ yes("count");
36
+ yes("name");
37
+ no("foo");
38
+ no("bar");
39
+ no("unrelated");
40
+ });
41
+
42
+ // ── CallExpression ───────────────────────────────────────────────────────────
43
+
44
+ describe("CallExpression — callee", () => {
45
+ yes("count()");
46
+ yes("name()");
47
+ no("foo()");
48
+ no("Math.random()");
49
+ });
50
+
51
+ describe("CallExpression — arguments", () => {
52
+ yes("foo(count())");
53
+ yes("foo(bar, count())");
54
+ yes("foo(1, 2, count())");
55
+ yes("foo(name(), bar)");
56
+ no("foo(bar, baz)");
57
+ no("foo(1, 2, 3)");
58
+ });
59
+
60
+ describe("CallExpression — nested callee", () => {
61
+ yes("count()()"); // count is in SIG, callee of outer call
62
+ yes("foo(bar(count()))"); // signal buried in nested argument
63
+ no("foo(bar(baz()))");
64
+ no("getSignal()()"); // getSignal not in SIG — correctly false
65
+ });
66
+
67
+ // ── MemberExpression ─────────────────────────────────────────────────────────
68
+
69
+ describe("MemberExpression", () => {
70
+ yes("count().value");
71
+ yes("count().nested.deep");
72
+ yes("name().length");
73
+ no("foo.bar");
74
+ no("obj.prop");
75
+ no("Math.PI");
76
+ no("foo.bar.baz");
77
+ });
78
+
79
+ // ── OptionalMemberExpression ─────────────────────────────────────────────────
80
+
81
+ describe("OptionalMemberExpression", () => {
82
+ yes("count()?.value");
83
+ yes("count()?.foo?.bar");
84
+ yes("name()?.length");
85
+ no("foo?.bar");
86
+ no("obj?.prop");
87
+ no("foo?.bar?.baz");
88
+ });
89
+
90
+ // ── OptionalCallExpression ───────────────────────────────────────────────────
91
+
92
+ describe("OptionalCallExpression", () => {
93
+ yes("count?.()");
94
+ yes("foo?.(count())");
95
+ yes("foo?.(bar, count())");
96
+ no("foo?.()");
97
+ no("foo?.(bar, baz)");
98
+ });
99
+
100
+ // ── BinaryExpression ─────────────────────────────────────────────────────────
101
+
102
+ describe("BinaryExpression", () => {
103
+ yes("count() + 1");
104
+ yes("1 + count()");
105
+ yes("count() > 0");
106
+ yes("count() === name()");
107
+ yes("foo + count()");
108
+ no("1 + 2");
109
+ no("foo + bar");
110
+ no("'a' + 'b'");
111
+ });
112
+
113
+ // ── LogicalExpression ────────────────────────────────────────────────────────
114
+
115
+ describe("LogicalExpression", () => {
116
+ yes("count() && foo");
117
+ yes("foo && count()");
118
+ yes("count() || name()");
119
+ yes("foo ?? count()");
120
+ no("foo && bar");
121
+ no("a || b");
122
+ no("null ?? 'default'");
123
+ });
124
+
125
+ // ── ConditionalExpression ────────────────────────────────────────────────────
126
+
127
+ describe("ConditionalExpression", () => {
128
+ yes("count() ? 'a' : 'b'", "signal in test");
129
+ yes("foo ? count() : 'b'", "signal in consequent");
130
+ yes("foo ? 'a' : count()", "signal in alternate");
131
+ yes("count() ? name() : 'b'", "signal in test and consequent");
132
+ no("foo ? 'a' : 'b'");
133
+ no("true ? 1 : 0");
134
+ });
135
+
136
+ // ── UnaryExpression ──────────────────────────────────────────────────────────
137
+
138
+ describe("UnaryExpression", () => {
139
+ yes("!count()");
140
+ yes("-count()");
141
+ yes("typeof count()");
142
+ no("!foo");
143
+ no("-1");
144
+ no("typeof foo");
145
+ });
146
+
147
+ // ── TemplateLiteral ──────────────────────────────────────────────────────────
148
+
149
+ describe("TemplateLiteral", () => {
150
+ yes("`${count()}`");
151
+ yes("`hello ${name()}`");
152
+ yes("`${foo} ${count()}`");
153
+ yes("`${count()} and ${name()}`");
154
+ no("`hello world`");
155
+ no("`${foo}`");
156
+ no("`${foo} ${bar}`");
157
+ });
158
+
159
+ // ── ArrayExpression ──────────────────────────────────────────────────────────
160
+
161
+ describe("ArrayExpression", () => {
162
+ yes("[count()]");
163
+ yes("[foo, count()]");
164
+ yes("[1, 2, count()]");
165
+ yes("[...count()]");
166
+ no("[]");
167
+ no("[1, 2, 3]");
168
+ no("[foo, bar]");
169
+ // sparse arrays have null holes
170
+ no("[,,,]", "sparse array holes");
171
+ });
172
+
173
+ // ── ObjectExpression ─────────────────────────────────────────────────────────
174
+
175
+ describe("ObjectExpression", () => {
176
+ yes("({ a: count() })");
177
+ yes("({ a: foo, b: count() })");
178
+ yes("({ ...count() })");
179
+ yes("({ [name()]: 'val' })", "computed key with signal");
180
+ no("({})");
181
+ no("({ a: 1 })");
182
+ no("({ a: foo, b: bar })");
183
+ no("({ ...foo })");
184
+ });
185
+
186
+ // ── SequenceExpression ───────────────────────────────────────────────────────
187
+
188
+ describe("SequenceExpression", () => {
189
+ yes("(foo, count())");
190
+ yes("(count(), foo)");
191
+ yes("(a, b, count())");
192
+ no("(a, b)");
193
+ no("(1, 2, 3)");
194
+ });
195
+
196
+ // ── AssignmentExpression ─────────────────────────────────────────────────────
197
+
198
+ describe("AssignmentExpression", () => {
199
+ // only RHS matters for reactivity — LHS is where you're writing to
200
+ yes("x = count()");
201
+ yes("x += count()");
202
+ no("x = foo");
203
+ no("x = 1");
204
+ });
205
+
206
+ // ── SpreadElement ────────────────────────────────────────────────────────────
207
+
208
+ describe("SpreadElement — inside array/call", () => {
209
+ yes("[...count()]");
210
+ yes("foo(...count())");
211
+ no("[...foo]");
212
+ no("foo(...bar)");
213
+ });
214
+
215
+ // ── ArrowFunctionExpression ──────────────────────────────────────────────────
216
+ // signals inside a function body are captured at call time, not bind time —
217
+ // making the attribute reactive would re-assign the handler on every signal change
218
+
219
+ describe("ArrowFunctionExpression — must NOT traverse body", () => {
220
+ no("() => count()");
221
+ no("(x) => count() + x");
222
+ no("() => { return count(); }");
223
+ no("(e) => { count(); }");
224
+ });
225
+
226
+ // ── FunctionExpression ───────────────────────────────────────────────────────
227
+
228
+ describe("FunctionExpression — must NOT traverse body", () => {
229
+ no("function() { return count(); }");
230
+ no("function handler() { count(); }");
231
+ });
232
+
233
+ // ── Literals (always false) ──────────────────────────────────────────────────
234
+
235
+ describe("Literals", () => {
236
+ no("1");
237
+ no("'hello'");
238
+ no("true");
239
+ no("null");
240
+ no("/regex/");
241
+ });
242
+
243
+ // ── Deep nesting ─────────────────────────────────────────────────────────────
244
+
245
+ describe("Deep nesting", () => {
246
+ yes("foo(bar(baz(count())))");
247
+ yes("a && b ? c + count() : d");
248
+ yes("`prefix_${foo ? count() : bar}`");
249
+ yes("[{ a: count() }]");
250
+ yes("({ arr: [1, count()] })");
251
+ yes("foo?.bar?.(count())");
252
+ no("foo(bar(baz(qux())))");
253
+ no("a && b ? c + d : e");
254
+ });
255
+
256
+ // ── Multiple signals ─────────────────────────────────────────────────────────
257
+
258
+ describe("Multiple signals", () => {
259
+ yes("count() + name()");
260
+ yes("[count(), name(), items()]");
261
+ yes("user()?.profile ?? name()");
262
+ yes("flag() ? count() : name()");
263
+ });
264
+
265
+ // ── Non-signal identifiers that look similar ─────────────────────────────────
266
+
267
+ describe("Near-miss identifiers", () => {
268
+ no("counter"); // not "count"
269
+ no("counted");
270
+ no("names"); // not "name"
271
+ no("userCount");
272
+ no("flagged");
273
+ no("itemsList");
274
+ });
275
+
276
+ // ── null/undefined safety ────────────────────────────────────────────────────
277
+
278
+ describe("null/undefined guard", () => {
279
+ it("returns false for null input", () => {
280
+ expect(containsSignal(null as any, SIG)).toBe(false);
281
+ });
282
+ it("returns false for undefined input", () => {
283
+ expect(containsSignal(undefined as any, SIG)).toBe(false);
284
+ });
285
+ it("empty signal set never matches", () => {
286
+ expect(containsSignal(parse("count()"), new Set())).toBe(false);
287
+ });
288
+ });