roqa 0.0.1

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,246 @@
1
+ import { isJSXExpressionContainer } from "../parser.js";
2
+ import { CONSTANTS } from "../utils.js";
3
+
4
+ /**
5
+ * Transform event handlers from JSX attributes to inline assignments
6
+ *
7
+ * Input JSX:
8
+ * <button onclick={run}>Click</button>
9
+ * <a onclick={() => select(row)}>Select</a>
10
+ *
11
+ * Output:
12
+ * button_1.__click = run;
13
+ * a_1.__click = [select, row]; // For parameterized handlers
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} EventBinding
18
+ * @property {string} varName - Variable name of the element
19
+ * @property {string} eventName - Event name (click, input, etc.)
20
+ * @property {import("@babel/types").Node} handler - The handler expression
21
+ * @property {number[]} path - Path in the JSX tree
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} ProcessedEvent
26
+ * @property {string} varName - Variable name of the element
27
+ * @property {string} eventName - Event name
28
+ * @property {import("@babel/types").Node} handlerExpression - The handler (identifier or array expression)
29
+ * @property {boolean} isParameterized - Whether handler needs item parameter
30
+ * @property {import("@babel/types").Node[]} params - Additional parameters for the handler
31
+ */
32
+
33
+ /**
34
+ * Process event bindings and determine handler type
35
+ * @param {EventBinding[]} events
36
+ * @param {string|null} itemParam - Current loop item parameter name (for parameterized handlers)
37
+ * @returns {ProcessedEvent[]}
38
+ */
39
+ export function processEvents(events, itemParam = null) {
40
+ const processed = [];
41
+
42
+ for (const event of events) {
43
+ const { varName, eventName, handler } = event;
44
+
45
+ if (!handler) {
46
+ // Boolean attribute like onclick without value - skip
47
+ continue;
48
+ }
49
+
50
+ // Extract the actual expression from JSXExpressionContainer
51
+ const expression = isJSXExpressionContainer(handler) ? handler.expression : handler;
52
+
53
+ const processedEvent = analyzeHandler(expression, varName, eventName, itemParam);
54
+ processed.push(processedEvent);
55
+ }
56
+
57
+ return processed;
58
+ }
59
+
60
+ /**
61
+ * Analyze a handler expression to determine its type
62
+ * @param {import("@babel/types").Node} expression
63
+ * @param {string} varName
64
+ * @param {string} eventName
65
+ * @param {string|null} itemParam
66
+ * @returns {ProcessedEvent}
67
+ */
68
+ function analyzeHandler(expression, varName, eventName, itemParam) {
69
+ // Case 1: Simple identifier - onclick={run}
70
+ if (expression.type === "Identifier") {
71
+ return {
72
+ varName,
73
+ eventName,
74
+ handlerExpression: expression,
75
+ isParameterized: false,
76
+ params: [],
77
+ };
78
+ }
79
+
80
+ // Case 2: Arrow function - onclick={() => select(row)}
81
+ if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression") {
82
+ return analyzeArrowHandler(expression, varName, eventName, itemParam);
83
+ }
84
+
85
+ // Case 3: Call expression directly - onclick={handler(item)}
86
+ // This is unusual but handle it
87
+ if (expression.type === "CallExpression") {
88
+ // Wrap in array format: [handler, ...args]
89
+ const args = expression.arguments;
90
+ return {
91
+ varName,
92
+ eventName,
93
+ handlerExpression: expression.callee,
94
+ isParameterized: true,
95
+ params: args,
96
+ };
97
+ }
98
+
99
+ // Default: treat as simple handler
100
+ return {
101
+ varName,
102
+ eventName,
103
+ handlerExpression: expression,
104
+ isParameterized: false,
105
+ params: [],
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Check if an expression is "static" - safe to extract as a parameter
111
+ * that can be evaluated at setup time rather than click time.
112
+ * @param {import("@babel/types").Node} node
113
+ * @param {Set<string>} boundParams - Parameter names bound by the arrow function
114
+ * @returns {boolean}
115
+ */
116
+ function isStaticExpression(node, boundParams = new Set()) {
117
+ if (!node) return true;
118
+
119
+ switch (node.type) {
120
+ // Identifiers are static ONLY if they are not bound parameters
121
+ case "Identifier":
122
+ return !boundParams.has(node.name);
123
+
124
+ // Literals are always static
125
+ case "NumericLiteral":
126
+ case "StringLiteral":
127
+ case "BooleanLiteral":
128
+ case "NullLiteral":
129
+ return true;
130
+
131
+ // Member expressions like obj.prop are static only if obj is static
132
+ case "MemberExpression":
133
+ return (
134
+ isStaticExpression(node.object, boundParams) &&
135
+ isStaticExpression(node.property, boundParams)
136
+ );
137
+
138
+ // Function calls are NOT static - they need to be evaluated at click time
139
+ case "CallExpression":
140
+ return false;
141
+
142
+ // Binary/unary expressions containing calls are not static
143
+ case "BinaryExpression":
144
+ case "LogicalExpression":
145
+ return (
146
+ isStaticExpression(node.left, boundParams) && isStaticExpression(node.right, boundParams)
147
+ );
148
+
149
+ case "UnaryExpression":
150
+ return isStaticExpression(node.argument, boundParams);
151
+
152
+ // Conditional expressions need all parts to be static
153
+ case "ConditionalExpression":
154
+ return (
155
+ isStaticExpression(node.test, boundParams) &&
156
+ isStaticExpression(node.consequent, boundParams) &&
157
+ isStaticExpression(node.alternate, boundParams)
158
+ );
159
+
160
+ // Array/object literals - check all elements
161
+ case "ArrayExpression":
162
+ return node.elements.every((el) => el === null || isStaticExpression(el, boundParams));
163
+
164
+ case "ObjectExpression":
165
+ return node.properties.every(
166
+ (prop) =>
167
+ prop.type === "ObjectProperty" &&
168
+ isStaticExpression(prop.key, boundParams) &&
169
+ isStaticExpression(prop.value, boundParams),
170
+ );
171
+
172
+ // Default: not static (be conservative)
173
+ default:
174
+ return false;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Analyze an arrow function handler
180
+ * @param {import("@babel/types").ArrowFunctionExpression} arrow
181
+ * @param {string} varName
182
+ * @param {string} eventName
183
+ * @param {string|null} _itemParam
184
+ * @returns {ProcessedEvent}
185
+ */
186
+ function analyzeArrowHandler(arrow, varName, eventName, _itemParam) {
187
+ const body = arrow.body;
188
+
189
+ // Collect parameter names that are bound by this arrow function
190
+ const boundParams = new Set();
191
+ for (const param of arrow.params) {
192
+ if (param.type === "Identifier") {
193
+ boundParams.add(param.name);
194
+ }
195
+ }
196
+
197
+ // Check if it's a simple call expression: () => fn(args)
198
+ // Transform to array format: [fn, ...args] ONLY if all args are static
199
+ // (don't reference arrow function parameters)
200
+ if (body.type === "CallExpression" && body.callee.type === "Identifier") {
201
+ const allArgsStatic = body.arguments.every((arg) => isStaticExpression(arg, boundParams));
202
+
203
+ if (allArgsStatic) {
204
+ return {
205
+ varName,
206
+ eventName,
207
+ handlerExpression: body.callee,
208
+ isParameterized: body.arguments.length > 0,
209
+ params: body.arguments,
210
+ };
211
+ }
212
+ }
213
+
214
+ // For more complex arrow functions, keep them as-is
215
+ // e.g., () => { multiple; statements; } or () => obj.method()
216
+ // or when arguments contain dynamic expressions like get(count) + 1
217
+ // or when arguments reference arrow function parameters like e.target.value
218
+ return {
219
+ varName,
220
+ eventName,
221
+ handlerExpression: arrow,
222
+ isParameterized: false,
223
+ params: [],
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Generate event handler assignment code
229
+ * @param {ProcessedEvent} event
230
+ * @param {(node: import("@babel/types").Node) => string} generateExpr - Function to generate expression code
231
+ * @returns {string}
232
+ */
233
+ export function generateEventAssignment(event, generateExpr) {
234
+ const { varName, eventName, handlerExpression, isParameterized, params } = event;
235
+
236
+ if (isParameterized) {
237
+ // Array format: element.__click = [handler, arg1, arg2]
238
+ const handlerCode = generateExpr(handlerExpression);
239
+ const paramsCode = params.map((p) => generateExpr(p)).join(", ");
240
+ return `${varName}.${CONSTANTS.EVENT_PREFIX}${eventName} = [${handlerCode}, ${paramsCode}];`;
241
+ }
242
+
243
+ // Simple assignment: element.__click = handler
244
+ const handlerCode = generateExpr(handlerExpression);
245
+ return `${varName}.${CONSTANTS.EVENT_PREFIX}${eventName} = ${handlerCode};`;
246
+ }
@@ -0,0 +1,164 @@
1
+ import { extractJSXAttributes, getJSXChildren, isJSXExpressionContainer } from "../parser.js";
2
+
3
+ /**
4
+ * Transform <For> components into forBlock() calls
5
+ *
6
+ * Input:
7
+ * <For each={items}>{(row) => <tr>...</tr>}</For>
8
+ *
9
+ * Output:
10
+ * forBlock(container, items, (anchor, item, index) => {
11
+ * // template creation and bindings
12
+ * return { start, end };
13
+ * });
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} ForTransformResult
18
+ * @property {string} containerVar - Variable name of the container element
19
+ * @property {import("@babel/types").Node} itemsExpression - The `each` prop expression
20
+ * @property {string} itemParam - The callback's item parameter name
21
+ * @property {string|null} indexParam - The callback's index parameter name (if present)
22
+ * @property {import("@babel/types").JSXElement} bodyJSX - The JSX returned by the callback
23
+ * @property {import("@babel/types").Node} originalCallback - The original callback for preserving other code
24
+ */
25
+
26
+ /**
27
+ * Extract information from a <For> component for transformation
28
+ * @param {import("@babel/types").JSXElement} node - The <For> JSX element
29
+ * @param {string} containerVar - Variable name of the parent container
30
+ * @returns {ForTransformResult}
31
+ */
32
+ export function extractForInfo(node, containerVar) {
33
+ const attrs = extractJSXAttributes(node.openingElement);
34
+
35
+ // Get the `each` prop
36
+ const eachValue = attrs.get("each");
37
+ if (!eachValue || !isJSXExpressionContainer(eachValue)) {
38
+ throw createForError(node, "Missing required 'each' prop on <For> component");
39
+ }
40
+ const itemsExpression = eachValue.expression;
41
+
42
+ // Get the children - should be a single expression container with arrow function
43
+ const children = getJSXChildren(node);
44
+ if (children.length !== 1) {
45
+ throw createForError(node, "<For> must have exactly one child (render callback)");
46
+ }
47
+
48
+ const child = children[0];
49
+ if (!isJSXExpressionContainer(child)) {
50
+ throw createForError(node, "<For> child must be an expression: {(item) => <element>}");
51
+ }
52
+
53
+ const callback = child.expression;
54
+ if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
55
+ throw createForError(node, "<For> child must be an arrow function or function expression");
56
+ }
57
+
58
+ // Extract callback parameters
59
+ const params = callback.params;
60
+ if (params.length < 1) {
61
+ throw createForError(node, "<For> callback must have at least one parameter (item)");
62
+ }
63
+
64
+ const itemParam = extractParamName(params[0]);
65
+ const indexParam = params.length > 1 ? extractParamName(params[1]) : null;
66
+
67
+ // Extract the JSX from the callback body
68
+ const bodyJSX = extractCallbackJSX(callback);
69
+ if (!bodyJSX) {
70
+ throw createForError(node, "<For> callback must return JSX");
71
+ }
72
+
73
+ return {
74
+ containerVar,
75
+ itemsExpression,
76
+ itemParam,
77
+ indexParam,
78
+ bodyJSX,
79
+ originalCallback: callback,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Extract parameter name from various parameter types
85
+ * @param {import("@babel/types").Node} param
86
+ * @returns {string}
87
+ */
88
+ function extractParamName(param) {
89
+ if (param.type === "Identifier") {
90
+ return param.name;
91
+ }
92
+ if (param.type === "AssignmentPattern" && param.left.type === "Identifier") {
93
+ return param.left.name;
94
+ }
95
+ throw new Error("Unsupported parameter type in <For> callback");
96
+ }
97
+
98
+ /**
99
+ * Extract JSX from callback body
100
+ * @param {import("@babel/types").ArrowFunctionExpression|import("@babel/types").FunctionExpression} callback
101
+ * @returns {import("@babel/types").JSXElement|null}
102
+ */
103
+ function extractCallbackJSX(callback) {
104
+ const body = callback.body;
105
+
106
+ // Arrow function with expression body: (item) => <tr>...</tr>
107
+ if (body.type === "JSXElement") {
108
+ return body;
109
+ }
110
+
111
+ // Block body: (item) => { return <tr>...</tr>; }
112
+ if (body.type === "BlockStatement") {
113
+ // Look for return statement with JSX
114
+ for (const stmt of body.body) {
115
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
116
+ if (stmt.argument.type === "JSXElement") {
117
+ return stmt.argument;
118
+ }
119
+ // Handle parenthesized JSX: return (<tr>...</tr>)
120
+ if (stmt.argument.type === "ParenthesizedExpression") {
121
+ if (stmt.argument.expression.type === "JSXElement") {
122
+ return stmt.argument.expression;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Get any statements before the return in a callback body
134
+ * These need to be preserved in the generated forBlock callback
135
+ * @param {import("@babel/types").ArrowFunctionExpression|import("@babel/types").FunctionExpression} callback
136
+ * @returns {import("@babel/types").Statement[]}
137
+ */
138
+ export function getCallbackPreamble(callback) {
139
+ const body = callback.body;
140
+
141
+ if (body.type !== "BlockStatement") {
142
+ return [];
143
+ }
144
+
145
+ const preamble = [];
146
+ for (const stmt of body.body) {
147
+ if (stmt.type === "ReturnStatement") {
148
+ break;
149
+ }
150
+ preamble.push(stmt);
151
+ }
152
+
153
+ return preamble;
154
+ }
155
+
156
+ /**
157
+ * Create a formatted error for <For> component issues
158
+ */
159
+ function createForError(node, message) {
160
+ const error = new Error(message);
161
+ error.code = "FOR_COMPONENT_ERROR";
162
+ error.loc = node.loc;
163
+ return error;
164
+ }