@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.
@@ -0,0 +1,309 @@
1
+ import { types as t } from "@babel/core";
2
+ import { getTagName } from "../util/helpers";
3
+
4
+ const VOID_ELEMENTS = new Set([
5
+ "area",
6
+ "base",
7
+ "br",
8
+ "col",
9
+ "embed",
10
+ "hr",
11
+ "img",
12
+ "input",
13
+ "link",
14
+ "meta",
15
+ "param",
16
+ "source",
17
+ "track",
18
+ "wbr",
19
+ ]);
20
+
21
+ const ATTR_MAP_SSR: Record<string, string> = {
22
+ className: "class",
23
+ htmlFor: "for",
24
+ };
25
+ //biome-ignore lint/suspicious/noShadowRestrictedNames: shut up
26
+ function escape(expr: t.Expression): t.CallExpression {
27
+ return t.callExpression(t.identifier("__e"), [expr]);
28
+ }
29
+
30
+ // builds a template literal from alternating string parts and expressions
31
+ function buildTemplate(parts: Array<string | t.Expression>): t.TemplateLiteral {
32
+ const quasis: t.TemplateElement[] = [];
33
+ const exprs: t.Expression[] = [];
34
+ let current = "";
35
+
36
+ for (const part of parts) {
37
+ if (typeof part === "string") {
38
+ current += part;
39
+ } else {
40
+ quasis.push(t.templateElement({ raw: current, cooked: current }));
41
+ current = "";
42
+ exprs.push(part);
43
+ }
44
+ }
45
+ quasis.push(t.templateElement({ raw: current, cooked: current }, true));
46
+ return t.templateLiteral(quasis, exprs);
47
+ }
48
+
49
+ /**
50
+ * <Component /> is always JSX so its always safe to render as is
51
+ * but the compiler can also register its own safe variables, like:
52
+ * ```ts
53
+ * const rendered = <Component foo="bar" />
54
+ * return <div>{rendered}</div>
55
+ * ```
56
+ * @param expr
57
+ * @param safeIdents
58
+ * @returns
59
+ */
60
+ /*
61
+ function isSSRSafe(expr: t.Expression, safeIdents: Set<string>): boolean {
62
+ // direct JSX - compiled to HTML string by this very plugin
63
+ if (t.isJSXElement(expr) || t.isJSXFragment(expr)) return true;
64
+
65
+ // component call - returns HTML string by convention
66
+ if (
67
+ t.isCallExpression(expr) &&
68
+ t.isIdentifier(expr.callee) &&
69
+ /^[A-Z]/.test(expr.callee.name)
70
+ ) return true;
71
+
72
+ // registered by compiler (e.g. children)
73
+ if (t.isIdentifier(expr)) {
74
+ // if sigil as a compiler doesnt have an insane CVE
75
+ // this will always be true
76
+ if (expr.name === "children") return true;
77
+ return safeIdents.has(expr.name);
78
+ }
79
+
80
+ // condition && <JSX> — right side produces HTML
81
+ if (t.isLogicalExpression(expr))
82
+ return isSSRSafe(expr.right as t.Expression, safeIdents);
83
+
84
+ // condition ? <JSX> : <JSX> — both branches must be checked
85
+ if (t.isConditionalExpression(expr)) {
86
+ return (
87
+ isSSRSafe(expr.consequent as t.Expression, safeIdents) ||
88
+ isSSRSafe(expr.alternate as t.Expression, safeIdents)
89
+ );
90
+ }
91
+ if (
92
+ t.isCallExpression(expr) &&
93
+ t.isMemberExpression(expr.callee) &&
94
+ t.isIdentifier(expr.callee.property) &&
95
+ expr.callee.property.name === "map"
96
+ ) {
97
+ const cb = expr.arguments[0];
98
+ if (t.isArrowFunctionExpression(cb) || t.isFunctionExpression(cb)) {
99
+ const body = cb.body;
100
+ if (t.isJSXElement(body) || t.isJSXFragment(body)) return true;
101
+ if (t.isBlockStatement(body)) {
102
+ for (const stmt of body.body) {
103
+ if (
104
+ t.isReturnStatement(stmt) &&
105
+ stmt.argument &&
106
+ (t.isJSXElement(stmt.argument) || t.isJSXFragment(stmt.argument))
107
+ ) return true;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ return false;
113
+ }
114
+ */
115
+ export function processElementSSR(
116
+ node: t.JSXElement,
117
+ signals: Set<string>,
118
+ ): t.TemplateLiteral {
119
+ const tagName = getTagName(node.openingElement.name);
120
+ const isComponent = /^[A-Z]/.test(tagName);
121
+
122
+ if (isComponent) {
123
+ return processComponentSSR(node, tagName, signals);
124
+ }
125
+ let dangerousHTML: t.Expression | null = null;
126
+ const parts: Array<string | t.Expression> = [];
127
+ parts.push(`<${tagName}`);
128
+
129
+ // attributes
130
+ for (const attr of node.openingElement.attributes) {
131
+ if (!t.isJSXAttribute(attr)) continue;
132
+ const attrName = (attr.name as t.JSXIdentifier).name;
133
+
134
+ if (attrName === "innerHTML") {
135
+ if (t.isJSXExpressionContainer(attr.value)) {
136
+ dangerousHTML = attr.value.expression as t.Expression;
137
+ }
138
+ continue; // don't output as attribute
139
+ }
140
+ // drop event handlers
141
+ if (attrName.startsWith("on")) continue;
142
+ // drop bind:* - no two-way binding on server
143
+ if (attrName.startsWith("bind")) continue;
144
+
145
+ const domAttr = ATTR_MAP_SSR[attrName] ?? attrName;
146
+
147
+ if (t.isStringLiteral(attr.value)) {
148
+ parts.push(` ${domAttr}="${attr.value.value}"`);
149
+ } else if (t.isJSXExpressionContainer(attr.value)) {
150
+ const expr = attr.value.expression as t.Expression;
151
+ parts.push(` ${domAttr}="`);
152
+ parts.push(escape(expr));
153
+ parts.push(`"`);
154
+ }
155
+ }
156
+
157
+ if (VOID_ELEMENTS.has(tagName)) {
158
+ parts.push(">");
159
+ return buildTemplate(parts);
160
+ }
161
+
162
+ parts.push(">");
163
+
164
+ if (dangerousHTML) {
165
+ parts.push(dangerousHTML);
166
+ parts.push(`</${tagName}>`);
167
+ return buildTemplate(parts);
168
+ }
169
+
170
+ // children
171
+ for (const child of node.children) {
172
+ if (t.isJSXText(child)) {
173
+ const text = child.value;
174
+ if (!text.trim()) continue;
175
+ const normalized = text.replace(/\s*\n\s*/g, " ");
176
+ parts.push(normalized);
177
+ } else if (t.isJSXElement(child)) {
178
+ parts.push(processElementSSR(child, signals));
179
+ } else if (t.isJSXFragment(child)) {
180
+ parts.push(processFragmentSSR(child, signals));
181
+ } else if (t.isJSXExpressionContainer(child)) {
182
+ if (t.isJSXEmptyExpression(child.expression)) continue;
183
+ const expr = child.expression as t.Expression;
184
+ if (t.isJSXElement(expr)) {
185
+ parts.push(processElementSSR(expr, signals));
186
+ } else if (t.isJSXFragment(expr)) {
187
+ parts.push(processFragmentSSR(expr, signals));
188
+ } else {
189
+ // Delimit dynamic text segments so SSR produces separate text
190
+ // nodes that the hydrate compiler can claim individually.
191
+ parts.push("<!--g-->");
192
+ parts.push(escape(expr));
193
+ parts.push("<!--/g-->");
194
+ }
195
+ }
196
+ }
197
+
198
+ parts.push(`</${tagName}>`);
199
+ return buildTemplate(parts);
200
+ }
201
+
202
+ function processComponentSSR(
203
+ node: t.JSXElement,
204
+ tagName: string,
205
+ signals: Set<string>,
206
+ ): t.TemplateLiteral {
207
+ const props = t.objectExpression([]);
208
+
209
+ for (const attr of node.openingElement.attributes) {
210
+ if (!t.isJSXAttribute(attr)) continue;
211
+ const attrName = (attr.name as t.JSXIdentifier).name;
212
+ if (t.isStringLiteral(attr.value)) {
213
+ props.properties.push(
214
+ t.objectProperty(t.identifier(attrName), attr.value),
215
+ );
216
+ } else if (t.isJSXExpressionContainer(attr.value)) {
217
+ props.properties.push(
218
+ t.objectProperty(
219
+ t.identifier(attrName),
220
+ attr.value.expression as t.Expression,
221
+ ),
222
+ );
223
+ }
224
+ }
225
+
226
+ // children as string
227
+ const childParts: Array<string | t.Expression> = [];
228
+ for (const child of node.children) {
229
+ if (t.isJSXText(child)) {
230
+ const text = child.value;
231
+ if (!text.trim()) continue;
232
+ const normalized = text.replace(/\s*\n\s*/g, " ");
233
+ childParts.push(normalized);
234
+ } else if (t.isJSXElement(child)) {
235
+ childParts.push(processElementSSR(child, signals));
236
+ } else if (t.isJSXFragment(child)) {
237
+ childParts.push(processFragmentSSR(child, signals));
238
+ } else if (t.isJSXExpressionContainer(child)) {
239
+ if (t.isJSXEmptyExpression(child.expression)) continue;
240
+ const expr = child.expression as t.Expression;
241
+ if (t.isJSXElement(expr)) {
242
+ childParts.push(processElementSSR(expr, signals));
243
+ } else if (t.isJSXFragment(expr)) {
244
+ childParts.push(processFragmentSSR(expr, signals));
245
+ } else {
246
+ childParts.push("<!--g-->");
247
+ childParts.push(escape(expr));
248
+ childParts.push("<!--/g-->");
249
+ }
250
+ }
251
+ }
252
+
253
+ if (childParts.length > 0) {
254
+ props.properties.push(
255
+ t.objectProperty(
256
+ t.identifier("children"),
257
+ t.callExpression(t.identifier("__h"), [buildTemplate(childParts)]),
258
+ ),
259
+ );
260
+ }
261
+
262
+ // Component({ ...props }) - result is already a string
263
+ const call = t.callExpression(t.identifier("__h"), [
264
+ t.callExpression(t.identifier(tagName), [props]),
265
+ ]);
266
+
267
+ // wrap in template so return type is consistent
268
+ return t.templateLiteral(
269
+ [
270
+ t.templateElement({ raw: "", cooked: "" }),
271
+ t.templateElement({ raw: "", cooked: "" }, true),
272
+ ],
273
+ [call],
274
+ );
275
+ }
276
+
277
+ export function processFragmentSSR(
278
+ node: t.JSXFragment,
279
+ signals: Set<string>,
280
+ ): t.TemplateLiteral {
281
+ const parts: Array<string | t.Expression> = [];
282
+
283
+ for (const child of node.children) {
284
+ if (t.isJSXText(child)) {
285
+ const text = child.value;
286
+ if (!text.trim()) continue;
287
+ const normalized = text.replace(/\s*\n\s*/g, " ");
288
+ parts.push(normalized);
289
+ } else if (t.isJSXElement(child)) {
290
+ parts.push(processElementSSR(child, signals));
291
+ } else if (t.isJSXFragment(child)) {
292
+ parts.push(processFragmentSSR(child, signals));
293
+ } else if (t.isJSXExpressionContainer(child)) {
294
+ if (t.isJSXEmptyExpression(child.expression)) continue;
295
+ const expr = child.expression as t.Expression;
296
+ if (t.isJSXElement(expr)) {
297
+ parts.push(processElementSSR(expr, signals));
298
+ } else if (t.isJSXFragment(expr)) {
299
+ parts.push(processFragmentSSR(expr, signals));
300
+ } else {
301
+ parts.push("<!--g-->");
302
+ parts.push(escape(expr));
303
+ parts.push("<!--/g-->");
304
+ }
305
+ }
306
+ }
307
+
308
+ return buildTemplate(parts);
309
+ }
@@ -0,0 +1,192 @@
1
+ import { types as t } from "@babel/core";
2
+
3
+ /**
4
+ * Create a reactive text node: `createEffect(() => node.textContent = String(expr))`
5
+ * In hydrate mode:
6
+ * - Claims <!--g--> anchor via claimComment (fixes adjacent-text bug vs DOM walk)
7
+ * - Consumes <!--/g--> closing delimiter
8
+ * - Gets nextSibling as the text node; if it's not a Text (SPA navigation), creates one
9
+ * - Effect updates that text node's content
10
+ * In dom mode: creates a new text node and appends it.
11
+ */
12
+ export function buildTextNode(
13
+ varName: string,
14
+ expr: t.Expression,
15
+ statements: t.Statement[],
16
+ genId: () => string,
17
+ hydrate?: boolean,
18
+ nodesVar?: string,
19
+ parentVar?: string,
20
+ ): void {
21
+ if (hydrate) {
22
+ const nodesId = t.identifier(nodesVar ?? "__nodes");
23
+ const parentIdent = parentVar ? t.identifier(parentVar) : undefined;
24
+
25
+ // const _anchor = claimComment(nodes, "g", parent) — claim/create <!--g-->
26
+ const anchorVar = genId();
27
+ const claimGArgs: t.Expression[] = [nodesId, t.stringLiteral("g")];
28
+ if (parentIdent) claimGArgs.push(parentIdent);
29
+ statements.push(
30
+ t.variableDeclaration("const", [
31
+ t.variableDeclarator(
32
+ t.identifier(anchorVar),
33
+ t.callExpression(t.identifier("claimComment"), claimGArgs),
34
+ ),
35
+ ]),
36
+ );
37
+
38
+ // claimComment(nodes, "/g", parent) — consume <!--/g--> from pool
39
+ const claimSlashGArgs: t.Expression[] = [nodesId, t.stringLiteral("/g")];
40
+ if (parentIdent) claimSlashGArgs.push(parentIdent);
41
+ statements.push(
42
+ t.expressionStatement(
43
+ t.callExpression(t.identifier("claimComment"), claimSlashGArgs),
44
+ ),
45
+ );
46
+
47
+ // let _t = _anchor?.nextSibling
48
+ // In SSR hydration, this is the text node between the comment delimiters.
49
+ // In SPA navigation, this is the closing <!--/g--> (not a Text), so we create one.
50
+ const textVar = genId();
51
+ statements.push(
52
+ t.variableDeclaration("let", [
53
+ t.variableDeclarator(
54
+ t.identifier(textVar),
55
+ t.optionalMemberExpression(
56
+ t.identifier(anchorVar),
57
+ t.identifier("nextSibling"),
58
+ false,
59
+ true,
60
+ ),
61
+ ),
62
+ ]),
63
+ );
64
+
65
+ // if (!(_t instanceof Text)) { const _nt = createTextNode(""); if (_anchor) _anchor.after(_nt); _t = _nt; }
66
+ const newTextVar = genId();
67
+ statements.push(
68
+ t.ifStatement(
69
+ t.unaryExpression(
70
+ "!",
71
+ t.parenthesizedExpression(
72
+ t.binaryExpression(
73
+ "instanceof",
74
+ t.identifier(textVar),
75
+ t.identifier("Text"),
76
+ ),
77
+ ),
78
+ ),
79
+ t.blockStatement([
80
+ t.variableDeclaration("const", [
81
+ t.variableDeclarator(
82
+ t.identifier(newTextVar),
83
+ t.callExpression(
84
+ t.memberExpression(
85
+ t.identifier("document"),
86
+ t.identifier("createTextNode"),
87
+ ),
88
+ [t.stringLiteral("")],
89
+ ),
90
+ ),
91
+ ]),
92
+ t.ifStatement(
93
+ t.identifier(anchorVar),
94
+ t.expressionStatement(
95
+ t.callExpression(
96
+ t.memberExpression(
97
+ t.identifier(anchorVar),
98
+ t.identifier("after"),
99
+ ),
100
+ [t.identifier(newTextVar)],
101
+ ),
102
+ ),
103
+ ),
104
+ t.expressionStatement(
105
+ t.assignmentExpression(
106
+ "=",
107
+ t.identifier(textVar),
108
+ t.identifier(newTextVar),
109
+ ),
110
+ ),
111
+ ]),
112
+ ),
113
+ );
114
+
115
+ // createEffect(() => { if (_t) _t.textContent = String(expr); })
116
+ statements.push(
117
+ t.expressionStatement(
118
+ t.callExpression(t.identifier("createEffect"), [
119
+ t.arrowFunctionExpression(
120
+ [],
121
+ t.blockStatement([
122
+ t.ifStatement(
123
+ t.identifier(textVar),
124
+ t.expressionStatement(
125
+ t.assignmentExpression(
126
+ "=",
127
+ t.memberExpression(
128
+ t.identifier(textVar),
129
+ t.identifier("textContent"),
130
+ ),
131
+ t.callExpression(t.identifier("String"), [expr]),
132
+ ),
133
+ ),
134
+ ),
135
+ ]),
136
+ ),
137
+ ]),
138
+ ),
139
+ );
140
+ return;
141
+ }
142
+
143
+ // DOM mode: create text node and append
144
+ const textVar = genId();
145
+ statements.push(
146
+ t.variableDeclaration("const", [
147
+ t.variableDeclarator(
148
+ t.identifier(textVar),
149
+ t.callExpression(
150
+ t.memberExpression(
151
+ t.identifier("document"),
152
+ t.identifier("createTextNode"),
153
+ ),
154
+ [t.stringLiteral("")],
155
+ ),
156
+ ),
157
+ ]),
158
+ );
159
+
160
+ // createEffect(() => _t.textContent = String(expr))
161
+ statements.push(
162
+ t.expressionStatement(
163
+ t.callExpression(t.identifier("createEffect"), [
164
+ t.arrowFunctionExpression(
165
+ [],
166
+ t.blockStatement([
167
+ t.expressionStatement(
168
+ t.assignmentExpression(
169
+ "=",
170
+ t.memberExpression(
171
+ t.identifier(textVar),
172
+ t.identifier("textContent"),
173
+ ),
174
+ t.callExpression(t.identifier("String"), [expr]),
175
+ ),
176
+ ),
177
+ ]),
178
+ ),
179
+ ]),
180
+ ),
181
+ );
182
+
183
+ // varName.append(_t)
184
+ statements.push(
185
+ t.expressionStatement(
186
+ t.callExpression(
187
+ t.memberExpression(t.identifier(varName), t.identifier("append")),
188
+ [t.identifier(textVar)],
189
+ ),
190
+ ),
191
+ );
192
+ }
@@ -0,0 +1,124 @@
1
+ import { types as t } from "@babel/core";
2
+
3
+ export const ATTR_MAP: Record<string, string> = {
4
+ class: "className",
5
+ for: "htmlFor",
6
+ };
7
+
8
+ export function containsSignal(
9
+ expr: t.Expression,
10
+ signals: Set<string>,
11
+ ): boolean {
12
+ if (t.isIdentifier(expr)) return signals.has(expr.name);
13
+ if (t.isCallExpression(expr)) {
14
+ return containsSignal(expr.callee as t.Expression, signals);
15
+ }
16
+ if (t.isBinaryExpression(expr)) {
17
+ return (
18
+ containsSignal(expr.left as t.Expression, signals) ||
19
+ containsSignal(expr.right, signals)
20
+ );
21
+ }
22
+ if (t.isMemberExpression(expr)) {
23
+ return containsSignal(expr.object as t.Expression, signals);
24
+ }
25
+ if (t.isLogicalExpression(expr)) {
26
+ return (
27
+ containsSignal(expr.left as t.Expression, signals) ||
28
+ containsSignal(expr.right as t.Expression, signals)
29
+ );
30
+ }
31
+ if (t.isConditionalExpression(expr)) {
32
+ return (
33
+ containsSignal(expr.test as t.Expression, signals) ||
34
+ containsSignal(expr.consequent as t.Expression, signals) ||
35
+ containsSignal(expr.alternate as t.Expression, signals)
36
+ );
37
+ }
38
+ return false;
39
+ }
40
+
41
+ export function isPrimitive(expr: t.Expression): boolean {
42
+ if (t.isCallExpression(expr) && t.isIdentifier(expr.callee)) return true;
43
+ if (t.isBinaryExpression(expr)) return true;
44
+ if (t.isTemplateLiteral(expr)) return true;
45
+ if (t.isIdentifier(expr)) return true;
46
+ // user().name is a MemberExpression after state handler rewrites user → user()
47
+ if (t.isMemberExpression(expr)) return true;
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Generate createElement call. In hydrate mode, claims from node pool.
53
+ * When parentVar is provided, passes it to claim() for SPA fallback (append on create).
54
+ */
55
+ export function getCreateElement(
56
+ tag: string,
57
+ hydrate: boolean,
58
+ nodesVar = "__nodes",
59
+ parentVar?: string,
60
+ ): t.Expression {
61
+ if (hydrate) {
62
+ const args: t.Expression[] = [t.identifier(nodesVar), t.stringLiteral(tag)];
63
+ if (parentVar) {
64
+ args.push(t.identifier(parentVar));
65
+ }
66
+ return t.callExpression(t.identifier("claim"), args);
67
+ }
68
+ return t.callExpression(
69
+ t.memberExpression(t.identifier("document"), t.identifier("createElement")),
70
+ [t.stringLiteral(tag)],
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Generate createTextNode call. In hydrate mode, claims from node pool.
76
+ * When parentVar is provided, passes it to claimText() for SPA fallback.
77
+ */
78
+ export function getCreateText(
79
+ hydrate: boolean,
80
+ nodesVar = "__nodes",
81
+ parentVar?: string,
82
+ ): t.Expression {
83
+ if (hydrate) {
84
+ const args: t.Expression[] = [t.identifier(nodesVar)];
85
+ if (parentVar) {
86
+ args.push(t.identifier(parentVar));
87
+ }
88
+ return t.callExpression(t.identifier("claimText"), args);
89
+ }
90
+ return t.callExpression(
91
+ t.memberExpression(
92
+ t.identifier("document"),
93
+ t.identifier("createTextNode"),
94
+ ),
95
+ [t.stringLiteral("")],
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Generate `const <nodesVar> = Array.from(el.childNodes)` for hydration scope.
101
+ * Returns the generated variable name for use by children.
102
+ */
103
+ export function buildHydrationScope(
104
+ parentVar: string,
105
+ statements: t.Statement[],
106
+ nodesVar: string,
107
+ ): void {
108
+ statements.push(
109
+ t.variableDeclaration("const", [
110
+ t.variableDeclarator(
111
+ t.identifier(nodesVar),
112
+ t.callExpression(
113
+ t.memberExpression(t.identifier("Array"), t.identifier("from")),
114
+ [
115
+ t.memberExpression(
116
+ t.identifier(parentVar),
117
+ t.identifier("childNodes"),
118
+ ),
119
+ ],
120
+ ),
121
+ ),
122
+ ]),
123
+ );
124
+ }
@@ -0,0 +1,75 @@
1
+ import { types as t } from "@babel/core";
2
+
3
+ const BIND_EVENT: Record<string, string> = {
4
+ value: "input",
5
+ checked: "change",
6
+ };
7
+
8
+ export function buildBind(
9
+ varName: string,
10
+ attrName: string,
11
+ attr: t.JSXAttribute,
12
+ statements: t.Statement[],
13
+ ): void {
14
+ const signal = (attr.value as t.JSXExpressionContainer)
15
+ .expression as t.Expression;
16
+
17
+ // bind:this — just assign the element to the signal once
18
+ if (attrName === "bindThis") {
19
+ statements.push(
20
+ t.expressionStatement(
21
+ t.callExpression(t.memberExpression(signal, t.identifier("set")), [
22
+ t.identifier(varName),
23
+ ]),
24
+ ),
25
+ );
26
+ return;
27
+ }
28
+
29
+ const prop = attrName.slice(4).toLowerCase(); // bindValue → value
30
+ const event = BIND_EVENT[prop] ?? "input";
31
+
32
+ // createEffect(() => _el.prop = signal())
33
+ statements.push(
34
+ t.expressionStatement(
35
+ t.callExpression(t.identifier("createEffect"), [
36
+ t.arrowFunctionExpression(
37
+ [],
38
+ t.blockStatement([
39
+ t.expressionStatement(
40
+ t.assignmentExpression(
41
+ "=",
42
+ t.memberExpression(t.identifier(varName), t.identifier(prop)),
43
+ t.callExpression(signal, []),
44
+ ),
45
+ ),
46
+ ]),
47
+ ),
48
+ ]),
49
+ ),
50
+ );
51
+
52
+ // _el.addEventListener(event, e => signal.set(e.target.prop))
53
+ statements.push(
54
+ t.expressionStatement(
55
+ t.callExpression(
56
+ t.memberExpression(
57
+ t.identifier(varName),
58
+ t.identifier("addEventListener"),
59
+ ),
60
+ [
61
+ t.stringLiteral(event),
62
+ t.arrowFunctionExpression(
63
+ [t.identifier("e")],
64
+ t.callExpression(t.memberExpression(signal, t.identifier("set")), [
65
+ t.memberExpression(
66
+ t.memberExpression(t.identifier("e"), t.identifier("target")),
67
+ t.identifier(prop),
68
+ ),
69
+ ]),
70
+ ),
71
+ ],
72
+ ),
73
+ ),
74
+ );
75
+ }