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,47 @@
1
+ import { generateOutput } from "./codegen.js";
2
+ import { parse } from "./parser.js";
3
+ import { inlineGetCalls } from "./transforms/inline-get.js";
4
+ import { validateNoCustomComponents } from "./transforms/validate.js";
5
+
6
+ /**
7
+ * Main entry point for the Roqa JSX compiler
8
+ *
9
+ * Compilation pipeline:
10
+ * 1. Parse JSX source code into AST
11
+ * 2. Validate (no unsupported PascalCase components)
12
+ * 3. Generate output code with template extraction, event transforms, and bindings
13
+ * 4. Inline get() calls to direct property access (get(x) -> x.v)
14
+ *
15
+ * @param {string} code - Source code to compile
16
+ * @param {string} filename - Source filename for error messages and source maps
17
+ * @returns {{ code: string, map: object }} - Compiled code and source map
18
+ */
19
+ export function compile(code, filename) {
20
+ // Step 1: Parse
21
+ const ast = parse(code, filename);
22
+
23
+ // Step 2: Validate (after parsing, before transforms)
24
+ // This checks for unsupported PascalCase components
25
+ // Note: <For> is handled specially in codegen, so it won't trigger validation error
26
+ validateNoCustomComponents(ast);
27
+
28
+ // Step 3: Generate output
29
+ // This handles:
30
+ // - Template extraction and deduplication
31
+ // - <For> component transformation
32
+ // - Event handler transformation
33
+ // - Reactive binding detection (get() -> bind())
34
+ const result = generateOutput(code, ast, filename);
35
+
36
+ // Step 4: Inline get() calls to direct property access
37
+ // This optimizes get(cell) to cell.v, avoiding function call overhead
38
+ const inlined = inlineGetCalls(result.code, filename);
39
+
40
+ return inlined;
41
+ }
42
+
43
+ // Re-export for direct usage if needed
44
+ export { parse } from "./parser.js";
45
+ export { validateNoCustomComponents } from "./transforms/validate.js";
46
+ export { generateOutput } from "./codegen.js";
47
+ export { inlineGetCalls } from "./transforms/inline-get.js";
@@ -0,0 +1,197 @@
1
+ import * as parser from "@babel/parser";
2
+
3
+ /**
4
+ * Parse JSX source code into an AST
5
+ * @param {string} code - Source code to parse
6
+ * @param {string} filename - Source filename for error messages
7
+ * @returns {import("@babel/types").File} - Babel AST
8
+ */
9
+ export function parse(code, filename) {
10
+ const isTypeScript = filename && (filename.endsWith(".tsx") || filename.endsWith(".ts"));
11
+ const plugins = isTypeScript ? ["jsx", "typescript"] : ["jsx"];
12
+
13
+ return parser.parse(code, {
14
+ sourceType: "module",
15
+ plugins,
16
+ sourceFilename: filename,
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Check if a JSX element is a control flow component (<For>, <Show>)
22
+ * @param {import("@babel/types").JSXElement} node
23
+ * @returns {boolean}
24
+ */
25
+ export function isControlFlowComponent(node) {
26
+ const name = getJSXElementName(node);
27
+ return name === "For" || name === "Show";
28
+ }
29
+
30
+ /**
31
+ * Check if a JSX element is the <For> component
32
+ * @param {import("@babel/types").JSXElement} node
33
+ * @returns {boolean}
34
+ */
35
+ export function isForComponent(node) {
36
+ return getJSXElementName(node) === "For";
37
+ }
38
+
39
+ /**
40
+ * Check if a JSX element is the <Show> component
41
+ * @param {import("@babel/types").JSXElement} node
42
+ * @returns {boolean}
43
+ */
44
+ export function isShowComponent(node) {
45
+ return getJSXElementName(node) === "Show";
46
+ }
47
+
48
+ /**
49
+ * Check if a string is PascalCase (starts with uppercase)
50
+ * @param {string} name
51
+ * @returns {boolean}
52
+ */
53
+ export function isPascalCase(name) {
54
+ return /^[A-Z]/.test(name);
55
+ }
56
+
57
+ /**
58
+ * Get the name of a JSX element
59
+ * @param {import("@babel/types").JSXElement} node
60
+ * @returns {string|null}
61
+ */
62
+ export function getJSXElementName(node) {
63
+ const openingElement = node.openingElement;
64
+ if (!openingElement) return null;
65
+
66
+ const nameNode = openingElement.name;
67
+
68
+ // <div>, <For>, <my-component>
69
+ if (nameNode.type === "JSXIdentifier") {
70
+ return nameNode.name;
71
+ }
72
+
73
+ // <Foo.Bar> - member expression
74
+ if (nameNode.type === "JSXMemberExpression") {
75
+ return getMemberExpressionName(nameNode);
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Get the full name from a JSXMemberExpression (Foo.Bar.Baz)
83
+ * @param {import("@babel/types").JSXMemberExpression} node
84
+ * @returns {string}
85
+ */
86
+ function getMemberExpressionName(node) {
87
+ const parts = [];
88
+ let current = node;
89
+
90
+ while (current.type === "JSXMemberExpression") {
91
+ parts.unshift(current.property.name);
92
+ current = current.object;
93
+ }
94
+
95
+ if (current.type === "JSXIdentifier") {
96
+ parts.unshift(current.name);
97
+ }
98
+
99
+ return parts.join(".");
100
+ }
101
+
102
+ /**
103
+ * Extract attributes from a JSX opening element
104
+ * @param {import("@babel/types").JSXOpeningElement} openingElement
105
+ * @returns {Map<string, import("@babel/types").Node>}
106
+ */
107
+ export function extractJSXAttributes(openingElement) {
108
+ const attrs = new Map();
109
+
110
+ for (const attr of openingElement.attributes) {
111
+ if (attr.type === "JSXAttribute") {
112
+ const name = attr.name.name;
113
+ // Value can be: StringLiteral, JSXExpressionContainer, or null (boolean true)
114
+ attrs.set(name, attr.value);
115
+ } else if (attr.type === "JSXSpreadAttribute") {
116
+ // Mark spread attributes specially
117
+ attrs.set("...", attr.argument);
118
+ }
119
+ }
120
+
121
+ return attrs;
122
+ }
123
+
124
+ /**
125
+ * Check if a node is a JSX element
126
+ * @param {import("@babel/types").Node} node
127
+ * @returns {boolean}
128
+ */
129
+ export function isJSXElement(node) {
130
+ return node && node.type === "JSXElement";
131
+ }
132
+
133
+ /**
134
+ * Check if a node is a JSX fragment
135
+ * @param {import("@babel/types").Node} node
136
+ * @returns {boolean}
137
+ */
138
+ export function isJSXFragment(node) {
139
+ return node && node.type === "JSXFragment";
140
+ }
141
+
142
+ /**
143
+ * Check if a node is JSX text
144
+ * @param {import("@babel/types").Node} node
145
+ * @returns {boolean}
146
+ */
147
+ export function isJSXText(node) {
148
+ return node && node.type === "JSXText";
149
+ }
150
+
151
+ /**
152
+ * Check if a node is a JSX expression container
153
+ * @param {import("@babel/types").Node} node
154
+ * @returns {boolean}
155
+ */
156
+ export function isJSXExpressionContainer(node) {
157
+ return node && node.type === "JSXExpressionContainer";
158
+ }
159
+
160
+ /**
161
+ * Get children of a JSX element, filtering out empty text nodes
162
+ * @param {import("@babel/types").JSXElement} node
163
+ * @returns {import("@babel/types").Node[]}
164
+ */
165
+ export function getJSXChildren(node) {
166
+ return node.children.filter((child) => {
167
+ // Filter out whitespace-only text nodes
168
+ if (child.type === "JSXText") {
169
+ return child.value.trim().length > 0;
170
+ }
171
+ return true;
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Check if a CallExpression is a get() call
177
+ * @param {import("@babel/types").Node} node
178
+ * @returns {boolean}
179
+ */
180
+ export function isGetCall(node) {
181
+ return (
182
+ node &&
183
+ node.type === "CallExpression" &&
184
+ node.callee.type === "Identifier" &&
185
+ node.callee.name === "get"
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Extract the cell argument from a get() call
191
+ * @param {import("@babel/types").CallExpression} node
192
+ * @returns {import("@babel/types").Node|null}
193
+ */
194
+ export function extractGetCellArg(node) {
195
+ if (!isGetCall(node)) return null;
196
+ return node.arguments[0] || null;
197
+ }
@@ -0,0 +1,264 @@
1
+ import { isGetCall, extractGetCellArg } from "../parser.js";
2
+ import { traverse } from "../utils.js";
3
+
4
+ /**
5
+ * Auto-detect get() calls in JSX expressions and generate bind() wrappers
6
+ *
7
+ * Input JSX:
8
+ * <tr class={get(row.isSelected) ? "danger" : ""}>{get(row.label)}</tr>
9
+ *
10
+ * Output:
11
+ * bind(row.isSelected, (v) => { tr_1.className = v ? "danger" : ""; });
12
+ * bind(row.label, (v) => { a_1_text.nodeValue = v; });
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} BindingInfo
17
+ * @property {import("@babel/types").Node} cellArg - The cell argument (e.g., row.label)
18
+ * @property {import("@babel/types").Node} fullExpression - The complete expression containing get()
19
+ * @property {string} targetVar - Variable name of the DOM element/text node
20
+ * @property {string} targetProperty - Property to update (nodeValue, className, etc.)
21
+ * @property {"text"|"attribute"} bindingType - Type of binding
22
+ * @property {string} attrName - Original attribute name (for attribute bindings)
23
+ */
24
+
25
+ /**
26
+ * Analyze an expression for get() calls
27
+ * @param {import("@babel/types").Node} expression - The expression to analyze
28
+ * @returns {GetCallInfo[]} - All get() calls found in the expression
29
+ *
30
+ * @typedef {Object} GetCallInfo
31
+ * @property {import("@babel/types").Node} cellArg - The cell reference (e.g., row.label)
32
+ * @property {import("@babel/types").CallExpression} callNode - The get() call node
33
+ * @property {boolean} isOnlyExpression - Whether this get() is the entire expression
34
+ */
35
+ export function findGetCalls(expression) {
36
+ const getCalls = [];
37
+
38
+ // Simple case: expression IS a get() call
39
+ if (isGetCall(expression)) {
40
+ getCalls.push({
41
+ cellArg: extractGetCellArg(expression),
42
+ callNode: expression,
43
+ isOnlyExpression: true,
44
+ });
45
+ return getCalls;
46
+ }
47
+
48
+ // Complex case: get() is somewhere inside the expression
49
+ // Use Babel traverse to find all get() calls
50
+ const visitedNodes = new Set();
51
+
52
+ // Create a mini AST wrapper for traverse
53
+ const wrapper = {
54
+ type: "Program",
55
+ body: [
56
+ {
57
+ type: "ExpressionStatement",
58
+ expression: expression,
59
+ },
60
+ ],
61
+ };
62
+
63
+ traverse(wrapper, {
64
+ CallExpression(path) {
65
+ const node = path.node;
66
+ if (visitedNodes.has(node)) return;
67
+ visitedNodes.add(node);
68
+
69
+ if (isGetCall(node)) {
70
+ getCalls.push({
71
+ cellArg: extractGetCellArg(node),
72
+ callNode: node,
73
+ isOnlyExpression: false,
74
+ });
75
+ }
76
+ },
77
+ noScope: true,
78
+ });
79
+
80
+ return getCalls;
81
+ }
82
+
83
+ /**
84
+ * Process bindings from template extraction and generate bind() info
85
+ * @param {Array} bindings - Bindings from jsx-to-template
86
+ * @param {string} code - Original source code
87
+ * @returns {ProcessedBinding[]}
88
+ *
89
+ * @typedef {Object} ProcessedBinding
90
+ * @property {string} targetVar - Variable name of the target element
91
+ * @property {string} targetProperty - Property to update
92
+ * @property {import("@babel/types").Node} cellArg - Cell to bind to
93
+ * @property {import("@babel/types").Node} fullExpression - Full expression for the update callback
94
+ * @property {boolean} needsTransform - Whether to transform get(cell) to v in callback
95
+ * @property {Array} contentParts - Array of content parts (static/dynamic) for concatenated text
96
+ */
97
+ export function processBindings(bindings, code) {
98
+ const processed = [];
99
+
100
+ for (const binding of bindings) {
101
+ const { type, varName } = binding;
102
+
103
+ // Handle prop bindings (for custom elements)
104
+ if (type === "prop") {
105
+ const { propName, expression, isStatic } = binding;
106
+
107
+ // Check if expression is a string literal (static prop)
108
+ if (isStatic || expression.type === "StringLiteral") {
109
+ processed.push({
110
+ type: "prop",
111
+ targetVar: varName,
112
+ propName,
113
+ expression: expression,
114
+ isStatic: true,
115
+ });
116
+ continue;
117
+ }
118
+
119
+ // Find get() calls in the expression
120
+ const getCalls = findGetCalls(expression);
121
+
122
+ if (getCalls.length === 0) {
123
+ // No get() calls - static expression
124
+ processed.push({
125
+ type: "prop",
126
+ targetVar: varName,
127
+ propName,
128
+ fullExpression: expression,
129
+ isStatic: true,
130
+ });
131
+ continue;
132
+ }
133
+
134
+ // Reactive prop
135
+ for (const getCall of getCalls) {
136
+ processed.push({
137
+ type: "prop",
138
+ targetVar: varName,
139
+ propName,
140
+ cellArg: getCall.cellArg,
141
+ fullExpression: expression,
142
+ needsTransform: !getCall.isOnlyExpression,
143
+ isStatic: false,
144
+ getCallNode: getCall.callNode,
145
+ });
146
+ }
147
+ continue;
148
+ }
149
+
150
+ // Handle new contentParts format for text bindings
151
+ if (type === "text" && binding.contentParts) {
152
+ const { textVarName, contentParts } = binding;
153
+
154
+ // Collect all get() calls from all dynamic parts
155
+ const allGetCalls = [];
156
+ for (const part of contentParts) {
157
+ if (part.type === "dynamic") {
158
+ const getCalls = findGetCalls(part.expression);
159
+ for (const getCall of getCalls) {
160
+ allGetCalls.push({
161
+ ...getCall,
162
+ partExpression: part.expression,
163
+ });
164
+ }
165
+ }
166
+ }
167
+
168
+ if (allGetCalls.length === 0) {
169
+ // All static - shouldn't happen but handle it
170
+ processed.push({
171
+ targetVar: textVarName,
172
+ targetProperty: "nodeValue",
173
+ cellArg: null,
174
+ fullExpression: null,
175
+ contentParts,
176
+ needsTransform: false,
177
+ isStatic: true,
178
+ });
179
+ continue;
180
+ }
181
+
182
+ // Create a binding for each unique cell
183
+ // Track cells we've already created bindings for (by cell code, not position)
184
+ const seenCells = new Set();
185
+
186
+ for (const getCall of allGetCalls) {
187
+ // Create a unique key for this cell based on its code representation
188
+ // Use the actual cell code (e.g., "a", "row.label") not position,
189
+ // since the same cell may appear multiple times at different positions
190
+ const cellCode = code.slice(getCall.cellArg.start, getCall.cellArg.end);
191
+
192
+ if (seenCells.has(cellCode)) continue;
193
+ seenCells.add(cellCode);
194
+
195
+ processed.push({
196
+ targetVar: textVarName,
197
+ targetProperty: "nodeValue",
198
+ cellArg: getCall.cellArg,
199
+ fullExpression: null, // Not used with contentParts
200
+ contentParts,
201
+ needsTransform: true,
202
+ isStatic: false,
203
+ getCallNode: getCall.callNode,
204
+ });
205
+ }
206
+ continue;
207
+ }
208
+
209
+ // Handle legacy single expression format and attribute bindings
210
+ const { expression, staticPrefix, usesMarker } = binding;
211
+
212
+ // Find all get() calls in this expression
213
+ const getCalls = findGetCalls(expression);
214
+
215
+ if (getCalls.length === 0) {
216
+ // No get() calls - this is a static expression, no binding needed
217
+ // But we still need to set the initial value
218
+ processed.push({
219
+ targetVar: type === "text" ? binding.textVarName : varName,
220
+ targetProperty: type === "text" ? "nodeValue" : binding.attrName,
221
+ cellArg: null,
222
+ fullExpression: expression,
223
+ needsTransform: false,
224
+ isStatic: true,
225
+ staticPrefix: staticPrefix || "",
226
+ usesMarker: usesMarker || false,
227
+ // Pass through SVG flag for proper attribute setting
228
+ isSvg: binding.isSvg || false,
229
+ });
230
+ continue;
231
+ }
232
+
233
+ // For each get() call, create a binding
234
+ // Note: Multiple get() calls in one expression will create multiple bindings
235
+ // This might cause redundant updates, but ensures correctness
236
+ for (const getCall of getCalls) {
237
+ const targetProperty =
238
+ type === "text"
239
+ ? "nodeValue"
240
+ : binding.attrName === "class" || binding.attrName === "className"
241
+ ? "className"
242
+ : binding.attrName;
243
+
244
+ processed.push({
245
+ targetVar: type === "text" ? binding.textVarName : varName,
246
+ targetProperty,
247
+ cellArg: getCall.cellArg,
248
+ fullExpression: expression,
249
+ needsTransform: !getCall.isOnlyExpression,
250
+ isStatic: false,
251
+ // Store the get() call node for replacement in codegen
252
+ getCallNode: getCall.callNode,
253
+ // Include static prefix for text bindings
254
+ staticPrefix: staticPrefix || "",
255
+ // Pass through marker flag
256
+ usesMarker: usesMarker || false,
257
+ // Pass through SVG flag for proper attribute setting
258
+ isSvg: binding.isSvg || false,
259
+ });
260
+ }
261
+ }
262
+
263
+ return processed;
264
+ }