@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 +2 -1
- package/src/babel/handlers/ssr/derived.ts +1 -0
- package/src/babel/handlers/ssr/state.ts +2 -1
- package/src/babel/index.ts +7 -10
- package/src/babel/jsx/children.ts +1 -1
- package/src/babel/jsx/element.ts +3 -2
- package/src/babel/jsx/utils.ts +49 -2
- package/src/babel/util/bind.ts +1 -1
- package/src/babel/util/css.ts +4 -4
- package/src/babel/util/dead-code.ts +2 -2
- package/src/bun-plugin.ts +2 -3
- package/test/containsSignal.test.ts +288 -0
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.
|
|
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
|
-
|
|
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";
|
package/src/babel/index.ts
CHANGED
|
@@ -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
|
-
|
|
133
|
-
|
|
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), [
|
package/src/babel/jsx/element.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
305
|
+
_inputValue = a.value;
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
}
|
package/src/babel/jsx/utils.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/babel/util/bind.ts
CHANGED
package/src/babel/util/css.ts
CHANGED
|
@@ -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
|
|
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
|
|
147
|
+
return `${trimmed}.${hash}`;
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
return parts.join(", ");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NodePath, types as t
|
|
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 ? `[
|
|
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
|
-
}
|
|
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
|
+
});
|