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,1049 @@
1
+ import { parse } from "@babel/parser";
2
+ import MagicString from "magic-string";
3
+ import { CONSTANTS, traverse } from "../utils.js";
4
+
5
+ /**
6
+ * Inline get(), cell(), put(), set(), and bind() calls
7
+ *
8
+ * The key optimization is wholesale inlining of bind() callback bodies at set() locations.
9
+ * Instead of using effect loops, we:
10
+ * 1. Find all bind(cell, callback) calls
11
+ * 2. Store refs to DOM elements: cell.ref_N = element
12
+ * 3. At set() locations, inline the callback body with element vars replaced by cell.ref_N
13
+ *
14
+ * Transforms:
15
+ * get(cell) -> cell.v (or inlined function body for derived cells)
16
+ * cell(value) -> { v: value, e: [] }
17
+ * cell(() => expr) -> { v: () => expr, e: [] } (derived cell - function stored)
18
+ * put(cell, value) -> cell.v = value
19
+ * set(cell, value) -> { cell.v = value; <inlined callback bodies> }
20
+ * bind(cell, callback) -> cell.ref_N = element; (callback inlined at set() locations)
21
+ */
22
+
23
+ /**
24
+ * Pre-compiled regex for property pattern extraction
25
+ * Matches patterns like "row.isSelected" -> prefix="row", pattern="isSelected"
26
+ */
27
+ const PROPERTY_PATTERN_REGEX = /^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$/;
28
+
29
+ /**
30
+ * Context class that encapsulates mutable state for a single compilation.
31
+ * This eliminates global state and makes the compiler safe for parallel execution.
32
+ */
33
+ class InlineContext {
34
+ constructor() {
35
+ /**
36
+ * Map to track derived cells: cellName -> { body: string, dependencies: string[] }
37
+ * body is the function body code (with get() already transformed to .v)
38
+ * dependencies is an array of cell names this derived cell depends on (direct dependencies only)
39
+ * @type {Map<string, {body: string, dependencies: string[]}>}
40
+ */
41
+ this.derivedCells = new Map();
42
+
43
+ /**
44
+ * Cache for extractPropertyPattern to avoid repeated regex matching
45
+ * @type {Map<string, {prefix: string, pattern: string} | null>}
46
+ */
47
+ this.propertyPatternCache = new Map();
48
+ }
49
+
50
+ /**
51
+ * Register a derived cell
52
+ * @param {string} cellName - The cell variable name
53
+ * @param {string} body - The transformed function body
54
+ * @param {string[]} dependencies - Direct dependencies
55
+ */
56
+ registerDerivedCell(cellName, body, dependencies) {
57
+ this.derivedCells.set(cellName, { body, dependencies });
58
+ }
59
+
60
+ /**
61
+ * Check if a cell is a derived cell
62
+ * @param {string} cellName
63
+ * @returns {boolean}
64
+ */
65
+ isDerivedCell(cellName) {
66
+ return this.derivedCells.has(cellName);
67
+ }
68
+
69
+ /**
70
+ * Get the body of a derived cell
71
+ * @param {string} cellName
72
+ * @returns {{body: string, dependencies: string[]} | undefined}
73
+ */
74
+ getDerivedCellInfo(cellName) {
75
+ return this.derivedCells.get(cellName);
76
+ }
77
+
78
+ /**
79
+ * Get the fully expanded body for a derived cell, recursively resolving any
80
+ * references to other derived cells.
81
+ * @param {string} cellName - The name of the derived cell
82
+ * @param {Set<string>} visited - Set of already visited cells (to prevent infinite loops)
83
+ * @returns {string|null} - The fully expanded body, or null if not a derived cell
84
+ */
85
+ getExpandedDerivedBody(cellName, visited = new Set()) {
86
+ const info = this.derivedCells.get(cellName);
87
+ if (!info) return null;
88
+
89
+ // Prevent infinite loops from circular dependencies
90
+ if (visited.has(cellName)) {
91
+ return info.body;
92
+ }
93
+ visited.add(cellName);
94
+
95
+ let expandedBody = info.body;
96
+
97
+ // Replace any references to other derived cells with their expanded bodies
98
+ for (const otherCellName of this.derivedCells.keys()) {
99
+ if (otherCellName === cellName) continue;
100
+
101
+ // Check if this body references the other derived cell
102
+ const cellRefRegex = new RegExp(`\\b${otherCellName}\\.v\\b`, "g");
103
+ if (cellRefRegex.test(expandedBody)) {
104
+ // Recursively get the expanded body for the other cell
105
+ const otherExpandedBody = this.getExpandedDerivedBody(otherCellName, visited);
106
+ if (otherExpandedBody) {
107
+ // Replace references with the expanded body (wrapped in parens for safety)
108
+ expandedBody = expandedBody.replace(
109
+ new RegExp(`\\b${otherCellName}\\.v\\b`, "g"),
110
+ `(${otherExpandedBody})`,
111
+ );
112
+ }
113
+ }
114
+ }
115
+
116
+ return expandedBody;
117
+ }
118
+
119
+ /**
120
+ * Get all cells that transitively depend on a given cell.
121
+ * This includes direct dependencies and dependencies of dependencies.
122
+ * @param {string} cellName - The source cell name
123
+ * @returns {Set<string>} - Set of all derived cell names that depend on this cell
124
+ */
125
+ getTransitiveDependents(cellName) {
126
+ const dependents = new Set();
127
+
128
+ // Find direct dependents
129
+ for (const [derivedCellName, derivedInfo] of this.derivedCells) {
130
+ if (derivedInfo.dependencies.includes(cellName)) {
131
+ dependents.add(derivedCellName);
132
+ }
133
+ }
134
+
135
+ // Find transitive dependents (cells that depend on our direct dependents)
136
+ let changed = true;
137
+ while (changed) {
138
+ changed = false;
139
+ for (const [derivedCellName, derivedInfo] of this.derivedCells) {
140
+ if (dependents.has(derivedCellName)) continue;
141
+
142
+ // Check if this cell depends on any of our current dependents
143
+ for (const dep of derivedInfo.dependencies) {
144
+ if (dependents.has(dep)) {
145
+ dependents.add(derivedCellName);
146
+ changed = true;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ return dependents;
154
+ }
155
+
156
+ /**
157
+ * Extract the property pattern from a cell code (with caching)
158
+ * e.g., "row.isSelected" -> { pattern: "isSelected", prefix: "row" }
159
+ * e.g., "count" -> null (simple identifier, no pattern)
160
+ * @param {string} cellCode
161
+ * @returns {{prefix: string, pattern: string} | null}
162
+ */
163
+ extractPropertyPattern(cellCode) {
164
+ if (this.propertyPatternCache.has(cellCode)) {
165
+ return this.propertyPatternCache.get(cellCode);
166
+ }
167
+ const match = cellCode.match(PROPERTY_PATTERN_REGEX);
168
+ const result = match ? { prefix: match[1], pattern: match[2] } : null;
169
+ this.propertyPatternCache.set(cellCode, result);
170
+ return result;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Check if a node is a get() call
176
+ */
177
+ function isGetCall(node) {
178
+ return (
179
+ node?.type === "CallExpression" &&
180
+ node.callee?.type === "Identifier" &&
181
+ node.callee.name === "get"
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Check if a node is a cell() call
187
+ */
188
+ function isCellCall(node) {
189
+ return (
190
+ node?.type === "CallExpression" &&
191
+ node.callee?.type === "Identifier" &&
192
+ node.callee.name === "cell"
193
+ );
194
+ }
195
+
196
+ /**
197
+ * Check if a node is a put() call
198
+ */
199
+ function isPutCall(node) {
200
+ return (
201
+ node?.type === "CallExpression" &&
202
+ node.callee?.type === "Identifier" &&
203
+ node.callee.name === "put"
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Check if a node is a set() call
209
+ */
210
+ function isSetCall(node) {
211
+ return (
212
+ node?.type === "CallExpression" &&
213
+ node.callee?.type === "Identifier" &&
214
+ node.callee.name === "set"
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Check if a node is a bind() call
220
+ */
221
+ function isBindCall(node) {
222
+ return (
223
+ node?.type === "CallExpression" &&
224
+ node.callee?.type === "Identifier" &&
225
+ node.callee.name === "bind"
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Find ref assignments at the component level (NOT inside bind() callbacks).
231
+ * These are generated by codegen for simple bindings without bind():
232
+ * element.property = expression; (where expression contains get(cell))
233
+ * cell.ref_N = element;
234
+ *
235
+ * Returns a map: cellCode -> [{ refNum, property, updateExpr }]
236
+ */
237
+ function findRefAssignmentsWithoutBind(ast, code) {
238
+ // Map: cellCode -> [{ refNum, property, updateExpr }]
239
+ const refMappings = new Map();
240
+
241
+ // Helper to process statements from a callback body
242
+ function processStatements(statements) {
243
+ // Build a map: elementVar -> { property, updateExpr, cellCodes }
244
+ // from element.property = expression assignments
245
+ const elementAssignments = new Map();
246
+
247
+ // First pass: find element.property = expression (with get(cell) calls)
248
+ for (const stmt of statements) {
249
+ if (
250
+ stmt.type === "ExpressionStatement" &&
251
+ stmt.expression.type === "AssignmentExpression" &&
252
+ stmt.expression.operator === "=" &&
253
+ stmt.expression.left.type === "MemberExpression" &&
254
+ stmt.expression.left.object.type === "Identifier" &&
255
+ stmt.expression.left.property.type === "Identifier"
256
+ ) {
257
+ const elementVar = stmt.expression.left.object.name;
258
+ const property = stmt.expression.left.property.name;
259
+ const rightExpr = stmt.expression.right;
260
+
261
+ // Check if the expression contains get() calls
262
+ const cellCodes = findGetCallCells(rightExpr, code);
263
+ if (cellCodes.length > 0) {
264
+ // Get the full expression and transform get(x) to x.v
265
+ let updateExpr = code.slice(rightExpr.start, rightExpr.end);
266
+ updateExpr = updateExpr.replace(/\bget\(([^)]+)\)/g, "$1.v");
267
+
268
+ elementAssignments.set(elementVar, {
269
+ property,
270
+ updateExpr,
271
+ cellCodes,
272
+ });
273
+ }
274
+ }
275
+ }
276
+
277
+ // Second pass: find cell.ref_N = element assignments
278
+ // Handle both simple identifiers (count.ref_1) and member expressions (row.label.ref_1)
279
+ for (const stmt of statements) {
280
+ if (
281
+ stmt.type === "ExpressionStatement" &&
282
+ stmt.expression.type === "AssignmentExpression" &&
283
+ stmt.expression.operator === "=" &&
284
+ stmt.expression.right.type === "Identifier"
285
+ ) {
286
+ const left = stmt.expression.left;
287
+ let cellCode = null;
288
+ let refProp = null;
289
+
290
+ // Pattern 1: cell.ref_N (simple identifier)
291
+ if (
292
+ left.type === "MemberExpression" &&
293
+ left.object.type === "Identifier" &&
294
+ left.property.type === "Identifier" &&
295
+ left.property.name.startsWith(CONSTANTS.REF_PREFIX)
296
+ ) {
297
+ cellCode = left.object.name;
298
+ refProp = left.property.name;
299
+ }
300
+ // Pattern 2: obj.cell.ref_N (member expression like row.label.ref_1)
301
+ else if (
302
+ left.type === "MemberExpression" &&
303
+ left.object.type === "MemberExpression" &&
304
+ left.property.type === "Identifier" &&
305
+ left.property.name.startsWith(CONSTANTS.REF_PREFIX)
306
+ ) {
307
+ cellCode = code.slice(left.object.start, left.object.end);
308
+ refProp = left.property.name;
309
+ }
310
+
311
+ if (cellCode && refProp) {
312
+ const refNum = parseInt(refProp.replace(CONSTANTS.REF_PREFIX, ""), 10);
313
+ const elementVar = stmt.expression.right.name;
314
+
315
+ // Look up the element assignment
316
+ const elemInfo = elementAssignments.get(elementVar);
317
+ if (elemInfo && elemInfo.cellCodes.includes(cellCode)) {
318
+ if (!refMappings.has(cellCode)) {
319
+ refMappings.set(cellCode, []);
320
+ }
321
+ refMappings.get(cellCode).push({
322
+ refNum,
323
+ property: elemInfo.property,
324
+ updateExpr: elemInfo.updateExpr,
325
+ });
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ traverse(ast, {
333
+ CallExpression(path) {
334
+ const node = path.node;
335
+
336
+ // Find this.connected() calls
337
+ if (
338
+ node.callee?.type === "MemberExpression" &&
339
+ node.callee.object?.type === "ThisExpression" &&
340
+ node.callee.property?.type === "Identifier" &&
341
+ node.callee.property.name === "connected" &&
342
+ node.arguments.length >= 1
343
+ ) {
344
+ const callbackArg = node.arguments[0];
345
+ if (
346
+ callbackArg.type !== "ArrowFunctionExpression" &&
347
+ callbackArg.type !== "FunctionExpression"
348
+ ) {
349
+ return;
350
+ }
351
+
352
+ const body = callbackArg.body;
353
+ const statements = body.type === "BlockStatement" ? body.body : [body];
354
+ processStatements(statements);
355
+ }
356
+
357
+ // Find forBlock() calls and process their render callbacks
358
+ if (
359
+ node.callee?.type === "Identifier" &&
360
+ node.callee.name === "forBlock" &&
361
+ node.arguments.length >= 3
362
+ ) {
363
+ const callbackArg = node.arguments[2];
364
+ if (
365
+ callbackArg.type !== "ArrowFunctionExpression" &&
366
+ callbackArg.type !== "FunctionExpression"
367
+ ) {
368
+ return;
369
+ }
370
+
371
+ const body = callbackArg.body;
372
+ const statements = body.type === "BlockStatement" ? body.body : [body];
373
+ processStatements(statements);
374
+ }
375
+ },
376
+ noScope: true,
377
+ });
378
+
379
+ return refMappings;
380
+ }
381
+
382
+ /**
383
+ * Find all cell codes referenced by get() calls in an expression
384
+ */
385
+ function findGetCallCells(node, code) {
386
+ const cellCodes = [];
387
+
388
+ function visit(n) {
389
+ if (!n) return;
390
+ if (isGetCall(n) && n.arguments[0]) {
391
+ cellCodes.push(code.slice(n.arguments[0].start, n.arguments[0].end));
392
+ return;
393
+ }
394
+ for (const key of Object.keys(n)) {
395
+ const child = n[key];
396
+ if (child && typeof child === "object") {
397
+ if (Array.isArray(child)) {
398
+ child.forEach(visit);
399
+ } else if (child.type) {
400
+ visit(child);
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ visit(node);
407
+ return cellCodes;
408
+ }
409
+
410
+ /**
411
+ * Find all forBlock and showBlock calls and collect:
412
+ * 1. Cells that are sources for forBlock/showBlock (need effect loop at set() if no explicit .update())
413
+ * 2. Variable assignments for forBlock/showBlock calls (for explicit .update() calls)
414
+ *
415
+ * Returns { sourceCells: Set<string>, variableMappings: Map<string, string> }
416
+ */
417
+ function findBlockInfo(ast, code) {
418
+ const sourceCells = new Set();
419
+ const variableMappings = new Map();
420
+
421
+ traverse(ast, {
422
+ CallExpression(path) {
423
+ const node = path.node;
424
+ const isForBlock =
425
+ node.callee?.type === "Identifier" &&
426
+ node.callee.name === "forBlock" &&
427
+ node.arguments.length >= 2;
428
+ const isShowBlock =
429
+ node.callee?.type === "Identifier" &&
430
+ node.callee.name === "showBlock" &&
431
+ node.arguments.length >= 2;
432
+
433
+ if (isForBlock || isShowBlock) {
434
+ // Always track the source cell (argument index 1 for both)
435
+ const cellArg = node.arguments[1];
436
+ const cellCode = code.slice(cellArg.start, cellArg.end);
437
+ sourceCells.add(cellCode);
438
+
439
+ // Check if this is an assignment: xxx_forBlock = forBlock(...) or xxx_showBlock = showBlock(...)
440
+ const parent = path.parent;
441
+ if (
442
+ parent?.type === "AssignmentExpression" &&
443
+ parent.left?.type === "Identifier" &&
444
+ (parent.left.name.endsWith("_forBlock") || parent.left.name.endsWith("_showBlock"))
445
+ ) {
446
+ variableMappings.set(cellCode, parent.left.name);
447
+ }
448
+ }
449
+ },
450
+ noScope: true,
451
+ });
452
+
453
+ return { sourceCells, variableMappings };
454
+ }
455
+
456
+ /**
457
+ * Find all bind() calls and extract callback info for inlining
458
+ * Returns a map from cell code -> array of callback info
459
+ */
460
+ function findBindCallbacks(ast, code) {
461
+ // Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd }]
462
+ const bindCallbacks = new Map();
463
+ // Track ref numbers per cell
464
+ const refCounters = new Map();
465
+
466
+ traverse(ast, {
467
+ ExpressionStatement(path) {
468
+ const expr = path.node.expression;
469
+ if (!isBindCall(expr)) return;
470
+
471
+ const cellArg = expr.arguments[0];
472
+ const callbackArg = expr.arguments[1];
473
+ if (!cellArg || !callbackArg) return;
474
+
475
+ // Get callback info
476
+ if (
477
+ callbackArg.type !== "ArrowFunctionExpression" &&
478
+ callbackArg.type !== "FunctionExpression"
479
+ ) {
480
+ return;
481
+ }
482
+
483
+ const cellCode = code.slice(cellArg.start, cellArg.end);
484
+ const paramName = callbackArg.params[0]?.name || "v";
485
+
486
+ // Get the callback body
487
+ const body = callbackArg.body;
488
+ let bodyCode;
489
+ if (body.type === "BlockStatement") {
490
+ // Extract statements from block, removing braces
491
+ bodyCode = code.slice(body.start + 1, body.end - 1).trim();
492
+ } else {
493
+ // Expression body
494
+ bodyCode = code.slice(body.start, body.end);
495
+ }
496
+
497
+ // Find element variables used in the callback (e.g., p_1_text, tr_1)
498
+ const elementVars = findElementVariables(body);
499
+
500
+ // Get or create ref number for this cell
501
+ const currentRef = refCounters.get(cellCode) || 0;
502
+ const refNum = currentRef + 1;
503
+ refCounters.set(cellCode, refNum);
504
+
505
+ // Store bind callback info
506
+ if (!bindCallbacks.has(cellCode)) {
507
+ bindCallbacks.set(cellCode, []);
508
+ }
509
+
510
+ bindCallbacks.get(cellCode).push({
511
+ callbackBody: bodyCode,
512
+ elementVars,
513
+ refNum,
514
+ paramName,
515
+ statementStart: path.node.start,
516
+ statementEnd: path.node.end,
517
+ });
518
+ },
519
+ noScope: true,
520
+ });
521
+
522
+ return bindCallbacks;
523
+ }
524
+
525
+ /**
526
+ * Find element variables used in a callback body
527
+ * Looks for element.property = ... patterns
528
+ */
529
+ function findElementVariables(body) {
530
+ const elementVars = [];
531
+ const seen = new Set();
532
+
533
+ function visit(node) {
534
+ if (!node) return;
535
+
536
+ // Look for element.property = ... patterns
537
+ if (
538
+ node.type === "AssignmentExpression" &&
539
+ node.left.type === "MemberExpression" &&
540
+ node.left.object.type === "Identifier"
541
+ ) {
542
+ const varName = node.left.object.name;
543
+ if (!seen.has(varName)) {
544
+ seen.add(varName);
545
+ elementVars.push({ varName });
546
+ }
547
+ }
548
+
549
+ // Recurse into child nodes
550
+ for (const key of Object.keys(node)) {
551
+ const child = node[key];
552
+ if (child && typeof child === "object") {
553
+ if (Array.isArray(child)) {
554
+ child.forEach(visit);
555
+ } else if (child.type) {
556
+ visit(child);
557
+ }
558
+ }
559
+ }
560
+ }
561
+
562
+ visit(body);
563
+ return elementVars;
564
+ }
565
+
566
+ /**
567
+ * Transform a callback body for inlining at a set() location
568
+ * - Replace element variables with cell.ref_N
569
+ * - Replace callback parameter (v) with cell.v
570
+ * - Transform get() calls to .v access
571
+ */
572
+ function transformCallbackBody(bodyCode, cellCode, elementVars, paramName, refNum) {
573
+ let transformed = bodyCode;
574
+
575
+ // Replace the callback parameter (v) with cell.v
576
+ const paramRegex = new RegExp(`\\b${paramName}\\b`, "g");
577
+ transformed = transformed.replace(paramRegex, `${cellCode}.v`);
578
+
579
+ // Replace element variables with cell.ref_N
580
+ for (const { varName } of elementVars) {
581
+ const varRegex = new RegExp(`\\b${varName}\\b`, "g");
582
+ transformed = transformed.replace(varRegex, `${cellCode}.${CONSTANTS.REF_PREFIX}${refNum}`);
583
+ }
584
+
585
+ // Transform any remaining get() calls to .v access
586
+ transformed = transformed.replace(/\bget\(([^)]+)\)/g, "$1.v");
587
+
588
+ return transformed;
589
+ }
590
+
591
+ /**
592
+ * Generate ref assignment code: cell.ref_N = elementVar
593
+ */
594
+ function generateRefAssignment(cellCode, elementVar, refNum) {
595
+ return `${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${elementVar};`;
596
+ }
597
+
598
+ /**
599
+ * Transform all get(), cell(), put(), set(), and bind() calls
600
+ */
601
+ export function inlineGetCalls(code, filename) {
602
+ // Create a fresh context for this compilation
603
+ const ctx = new InlineContext();
604
+
605
+ const isTypeScript = filename && (filename.endsWith(".tsx") || filename.endsWith(".ts"));
606
+ const plugins = isTypeScript ? ["jsx", "typescript"] : ["jsx"];
607
+
608
+ const ast = parse(code, {
609
+ sourceType: "module",
610
+ plugins,
611
+ });
612
+
613
+ const s = new MagicString(code);
614
+
615
+ // Find forBlock and showBlock info (source cells and variable mappings)
616
+ const { sourceCells: blockSourceCells, variableMappings: blockMappings } = findBlockInfo(
617
+ ast,
618
+ code,
619
+ ctx,
620
+ );
621
+
622
+ // Find all bind() callbacks for inlining
623
+ const bindCallbacks = findBindCallbacks(ast, code);
624
+
625
+ // Find ref assignments without bind() (from codegen's direct ref approach)
626
+ const refWithoutBind = findRefAssignmentsWithoutBind(ast, code, ctx);
627
+
628
+ // Track all calls to transform
629
+ const getCalls = [];
630
+ const cellCalls = [];
631
+ const putCalls = [];
632
+ const setCalls = [];
633
+
634
+ // Track bind statements to remove
635
+ const bindStatementsToRemove = [];
636
+
637
+ // Track roqa imports for removal
638
+ const importsToRemove = [];
639
+
640
+ // Collect bind statements to remove
641
+ for (const [cellCode, callbacks] of bindCallbacks) {
642
+ for (const cb of callbacks) {
643
+ bindStatementsToRemove.push({
644
+ start: cb.statementStart,
645
+ end: cb.statementEnd,
646
+ cellCode,
647
+ callback: cb,
648
+ });
649
+ }
650
+ }
651
+
652
+ // First pass: identify derived cells (cells with arrow function arguments)
653
+ traverse(ast, {
654
+ VariableDeclarator(path) {
655
+ if (path.node.id.type === "Identifier" && path.node.init && isCellCall(path.node.init)) {
656
+ const cellName = path.node.id.name;
657
+ const arg = path.node.init.arguments[0];
658
+
659
+ // Check if the argument is an arrow function or function expression
660
+ if (arg && (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression")) {
661
+ // Extract the function body and transform get() calls to .v
662
+ const body = arg.body;
663
+ let bodyCode;
664
+ if (body.type === "BlockStatement") {
665
+ // For block bodies, we'd need to handle return statements
666
+ // For now, skip these complex cases
667
+ return;
668
+ } else {
669
+ // Expression body - inline it directly
670
+ bodyCode = code.slice(body.start, body.end);
671
+ }
672
+
673
+ // Extract dependencies from get() calls
674
+ const dependencies = [];
675
+ const getCallRegex = /\bget\(([^)]+)\)/g;
676
+ let match;
677
+ while ((match = getCallRegex.exec(bodyCode)) !== null) {
678
+ dependencies.push(match[1].trim());
679
+ }
680
+
681
+ // Transform get() calls in the body to .v access
682
+ const transformedBody = bodyCode.replace(/\bget\(([^)]+)\)/g, "$1.v");
683
+ ctx.registerDerivedCell(cellName, transformedBody, dependencies);
684
+ }
685
+ }
686
+ },
687
+ noScope: true,
688
+ });
689
+
690
+ // Second pass: collect all calls to transform
691
+ traverse(ast, {
692
+ CallExpression(path) {
693
+ if (isGetCall(path.node)) {
694
+ const arg = path.node.arguments[0];
695
+ if (arg) {
696
+ const argCode = code.slice(arg.start, arg.end);
697
+ const derivedInfo = ctx.getDerivedCellInfo(argCode);
698
+ getCalls.push({
699
+ start: path.node.start,
700
+ end: path.node.end,
701
+ argCode,
702
+ // Check if this is a derived cell
703
+ isDerived: !!derivedInfo,
704
+ derivedBody: derivedInfo?.body || null,
705
+ });
706
+ }
707
+ } else if (isCellCall(path.node)) {
708
+ const arg = path.node.arguments[0];
709
+ cellCalls.push({
710
+ start: path.node.start,
711
+ end: path.node.end,
712
+ argStart: arg?.start,
713
+ argEnd: arg?.end,
714
+ });
715
+ } else if (isPutCall(path.node)) {
716
+ const cellArg = path.node.arguments[0];
717
+ const valueArg = path.node.arguments[1];
718
+ if (cellArg) {
719
+ putCalls.push({
720
+ start: path.node.start,
721
+ end: path.node.end,
722
+ cellStart: cellArg.start,
723
+ cellEnd: cellArg.end,
724
+ valueStart: valueArg?.start,
725
+ valueEnd: valueArg?.end,
726
+ });
727
+ }
728
+ } else if (isSetCall(path.node)) {
729
+ const cellArg = path.node.arguments[0];
730
+ const valueArg = path.node.arguments[1];
731
+ if (cellArg) {
732
+ const cellCode = code.slice(cellArg.start, cellArg.end);
733
+ setCalls.push({
734
+ start: path.node.start,
735
+ end: path.node.end,
736
+ cellStart: cellArg.start,
737
+ cellEnd: cellArg.end,
738
+ cellCode,
739
+ valueStart: valueArg?.start,
740
+ valueEnd: valueArg?.end,
741
+ blockVar: blockMappings.get(cellCode) || null,
742
+ });
743
+ }
744
+ }
745
+ },
746
+ ImportDeclaration(path) {
747
+ const source = path.node.source.value;
748
+ if (source === "roqa") {
749
+ for (const specifier of path.node.specifiers) {
750
+ if (specifier.type === "ImportSpecifier") {
751
+ const name = specifier.imported.name;
752
+ if (["get", "cell", "put", "set", "bind"].includes(name)) {
753
+ importsToRemove.push({ name, specifier });
754
+ }
755
+ }
756
+ }
757
+ }
758
+ },
759
+ noScope: true,
760
+ });
761
+
762
+ // Sort all calls by position descending (process from end to start)
763
+ const allCalls = [
764
+ ...getCalls.map((c) => ({ ...c, type: "get" })),
765
+ ...cellCalls.map((c) => ({ ...c, type: "cell" })),
766
+ ...putCalls.map((c) => ({ ...c, type: "put" })),
767
+ ...setCalls.map((c) => ({ ...c, type: "set" })),
768
+ ].sort((a, b) => b.start - a.start);
769
+
770
+ // Process calls
771
+ for (const call of allCalls) {
772
+ if (call.type === "get") {
773
+ // If this is a derived cell, inline the fully expanded function body
774
+ if (call.isDerived) {
775
+ const expandedBody = ctx.getExpandedDerivedBody(call.argCode);
776
+ s.overwrite(call.start, call.end, `(${expandedBody})`);
777
+ } else {
778
+ s.overwrite(call.start, call.end, `${call.argCode}.v`);
779
+ }
780
+ } else if (call.type === "cell") {
781
+ const argCode = call.argStart != null ? s.slice(call.argStart, call.argEnd) : "undefined";
782
+ s.overwrite(call.start, call.end, `{ v: ${argCode}, e: [] }`);
783
+ } else if (call.type === "put") {
784
+ const cellCode = s.slice(call.cellStart, call.cellEnd);
785
+ const valueCode =
786
+ call.valueStart != null ? s.slice(call.valueStart, call.valueEnd) : "undefined";
787
+ s.overwrite(call.start, call.end, `(${cellCode}.v = ${valueCode})`);
788
+ } else if (call.type === "set") {
789
+ const c = s.slice(call.cellStart, call.cellEnd);
790
+ const v = call.valueStart != null ? s.slice(call.valueStart, call.valueEnd) : "undefined";
791
+
792
+ // Helper to generate inlined updates from bind callbacks
793
+ // Only include callbacks that were fully inlined (have element vars)
794
+ const generateBindCallbackUpdates = () => {
795
+ let callbacks = bindCallbacks.get(call.cellCode);
796
+ let callbackCellCode = call.cellCode;
797
+
798
+ // If no exact match, try pattern match (e.g., "prev.isSelected" matches "row.isSelected")
799
+ if ((!callbacks || callbacks.length === 0) && call.cellCode.includes(".")) {
800
+ const patternInfo = ctx.extractPropertyPattern(call.cellCode);
801
+ if (patternInfo) {
802
+ for (const [existingCellCode, existingCallbacks] of bindCallbacks) {
803
+ const existingPattern = ctx.extractPropertyPattern(existingCellCode);
804
+ if (existingPattern && existingPattern.pattern === patternInfo.pattern) {
805
+ callbacks = existingCallbacks;
806
+ callbackCellCode = existingCellCode;
807
+ break;
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ if (callbacks && callbacks.length > 0) {
814
+ // Filter to only include callbacks that have element vars (were fully inlined)
815
+ const inlinableCallbacks = callbacks.filter((cb) => cb.elementVars.length > 0);
816
+ return inlinableCallbacks
817
+ .map((cb) => {
818
+ let body = transformCallbackBody(
819
+ cb.callbackBody,
820
+ callbackCellCode,
821
+ cb.elementVars,
822
+ cb.paramName,
823
+ cb.refNum,
824
+ );
825
+ if (callbackCellCode !== call.cellCode) {
826
+ const originalPattern = ctx.extractPropertyPattern(callbackCellCode);
827
+ const actualPattern = ctx.extractPropertyPattern(call.cellCode);
828
+ if (originalPattern && actualPattern) {
829
+ const regex = new RegExp(
830
+ `\\b${originalPattern.prefix}\\.${originalPattern.pattern}\\b`,
831
+ "g",
832
+ );
833
+ body = body.replace(regex, `${actualPattern.prefix}.${actualPattern.pattern}`);
834
+ }
835
+ }
836
+ return body;
837
+ })
838
+ .join(" ");
839
+ }
840
+ return "";
841
+ };
842
+
843
+ // Helper to generate updates from ref assignments without bind()
844
+ const generateRefUpdates = () => {
845
+ let refInfos = refWithoutBind.get(call.cellCode);
846
+ let refCellCode = call.cellCode;
847
+
848
+ if ((!refInfos || refInfos.length === 0) && call.cellCode.includes(".")) {
849
+ const patternInfo = ctx.extractPropertyPattern(call.cellCode);
850
+ if (patternInfo) {
851
+ for (const [existingCellCode, existingRefInfos] of refWithoutBind) {
852
+ const existingPattern = ctx.extractPropertyPattern(existingCellCode);
853
+ if (existingPattern && existingPattern.pattern === patternInfo.pattern) {
854
+ refInfos = existingRefInfos;
855
+ refCellCode = existingCellCode;
856
+ break;
857
+ }
858
+ }
859
+ }
860
+ }
861
+
862
+ if (refInfos && refInfos.length > 0) {
863
+ return refInfos
864
+ .map((info) => {
865
+ let update = `${refCellCode}.${CONSTANTS.REF_PREFIX}${info.refNum}.${info.property} = ${info.updateExpr};`;
866
+ if (refCellCode !== call.cellCode) {
867
+ const originalPattern = ctx.extractPropertyPattern(refCellCode);
868
+ const actualPattern = ctx.extractPropertyPattern(call.cellCode);
869
+ if (originalPattern && actualPattern) {
870
+ const regex = new RegExp(
871
+ `\\b${originalPattern.prefix}\\.${originalPattern.pattern}\\b`,
872
+ "g",
873
+ );
874
+ update = update.replace(
875
+ regex,
876
+ `${actualPattern.prefix}.${actualPattern.pattern}`,
877
+ );
878
+ }
879
+ }
880
+ return update;
881
+ })
882
+ .join(" ");
883
+ }
884
+ return "";
885
+ };
886
+
887
+ // Helper to generate updates for derived cells that depend on this cell (transitively)
888
+ const generateDerivedCellUpdates = () => {
889
+ const updates = [];
890
+ const seenUpdates = new Set(); // Deduplicate updates
891
+
892
+ // Get ALL cells that transitively depend on the cell being set
893
+ const transitiveDependents = ctx.getTransitiveDependents(call.cellCode);
894
+
895
+ for (const derivedCellName of transitiveDependents) {
896
+ // Find refs for the derived cell to update them
897
+ const derivedRefInfos = refWithoutBind.get(derivedCellName);
898
+ if (derivedRefInfos && derivedRefInfos.length > 0) {
899
+ // Get the fully expanded body for this derived cell
900
+ const expandedBody = ctx.getExpandedDerivedBody(derivedCellName);
901
+
902
+ for (const info of derivedRefInfos) {
903
+ // Replace references to the derived cell with its fully expanded body
904
+ // e.g., "Doubled: " + doubled.v -> "Doubled: " + count.v * 2
905
+ // e.g., "Quadrupled: " + quadrupled.v -> "Quadrupled: " + count.v * 2 * 2
906
+ let updateExpr = info.updateExpr.replace(
907
+ new RegExp(`\\b${derivedCellName}\\.v\\b`, "g"),
908
+ `(${expandedBody})`,
909
+ );
910
+ const updateCode = `${derivedCellName}.${CONSTANTS.REF_PREFIX}${info.refNum}.${info.property} = ${updateExpr};`;
911
+
912
+ // Deduplicate
913
+ if (!seenUpdates.has(updateCode)) {
914
+ seenUpdates.add(updateCode);
915
+ updates.push(updateCode);
916
+ }
917
+ }
918
+ }
919
+ }
920
+ return updates.join(" ");
921
+ };
922
+
923
+ // Collect all updates
924
+ const bindUpdates = generateBindCallbackUpdates();
925
+ const refUpdates = generateRefUpdates();
926
+ const derivedUpdates = generateDerivedCellUpdates();
927
+ const blockUpdate = call.blockVar ? `${call.blockVar}.update();` : "";
928
+
929
+ // Check if there are non-inlined bind callbacks for this cell
930
+ // These are callbacks without element vars that were kept as runtime bind() calls
931
+ let hasNonInlinedBinds = false;
932
+ const cellCallbacks = bindCallbacks.get(call.cellCode);
933
+ if (cellCallbacks) {
934
+ hasNonInlinedBinds = cellCallbacks.some((cb) => cb.elementVars.length === 0);
935
+ }
936
+
937
+ // Check if this cell is a source for forBlock/showBlock
938
+ // First check exact match
939
+ let isBlockSource = blockSourceCells.has(call.cellCode);
940
+
941
+ // If not an exact match and this is a member expression (foo.bar), check if any
942
+ // block source has the same property name (pattern matching)
943
+ // e.g., todoColumn.tasks should match column.tasks
944
+ if (!isBlockSource && call.cellCode.includes(".")) {
945
+ const callPattern = ctx.extractPropertyPattern(call.cellCode);
946
+ if (callPattern) {
947
+ for (const sourceCell of blockSourceCells) {
948
+ const sourcePattern = ctx.extractPropertyPattern(sourceCell);
949
+ if (sourcePattern && sourcePattern.pattern === callPattern.pattern) {
950
+ isBlockSource = true;
951
+ break;
952
+ }
953
+ }
954
+ }
955
+ }
956
+
957
+ // Effect loop needed for:
958
+ // 1. Non-inlined bind() callbacks
959
+ // 2. Cells that are sources for forBlock/showBlock WITHOUT an explicit .update() call
960
+ // (if we have blockVar, we call .update() directly, so no need for effect loop)
961
+ const needsEffectLoop = hasNonInlinedBinds || (isBlockSource && !call.blockVar);
962
+ const effectLoop = needsEffectLoop
963
+ ? `for (let i = 0; i < ${c}.e.length; i++) ${c}.e[i](${c}.v);`
964
+ : "";
965
+
966
+ // Combine all updates
967
+ const allUpdates = [blockUpdate, bindUpdates, refUpdates, derivedUpdates, effectLoop]
968
+ .filter(Boolean)
969
+ .join(" ");
970
+
971
+ // Always wrap in block with effect loop to support runtime bindings
972
+ s.overwrite(call.start, call.end, `{ ${c}.v = ${v}; ${allUpdates} }`);
973
+ }
974
+ }
975
+
976
+ // Remove bind statements and add ref assignments in their place
977
+ // Sort by position descending
978
+ bindStatementsToRemove.sort((a, b) => b.start - a.start);
979
+
980
+ for (const { start, end, cellCode, callback } of bindStatementsToRemove) {
981
+ // Generate ref assignment for each element variable
982
+ let refAssignment = "";
983
+ for (const { varName } of callback.elementVars) {
984
+ refAssignment += generateRefAssignment(cellCode, varName, callback.refNum);
985
+ }
986
+
987
+ // Replace bind() call with ref assignment(s)
988
+ if (refAssignment) {
989
+ s.overwrite(start, end, refAssignment);
990
+ } else {
991
+ // No element vars found - bind() can't be fully inlined
992
+ // Keep the bind() call but wrap it to run immediately AND register for updates
993
+ // This handles complex callbacks like d3.select().call() patterns
994
+ // Don't remove - leave bind() in place (runtime handles immediate execution)
995
+ }
996
+ }
997
+
998
+ // Remove imports that are no longer needed
999
+ // Collect all imports to remove first
1000
+ // Only remove bind import if ALL bind calls were fully inlined (had element vars)
1001
+ const allBindsInlined = bindStatementsToRemove.every((b) => b.callback.elementVars.length > 0);
1002
+ const shouldRemoveBind = bindStatementsToRemove.length > 0 && allBindsInlined;
1003
+ const importsToActuallyRemove = importsToRemove.filter(({ name }) => {
1004
+ return (
1005
+ (name === "get" && getCalls.length > 0) ||
1006
+ (name === "cell" && cellCalls.length > 0) ||
1007
+ (name === "put" && putCalls.length > 0) ||
1008
+ (name === "set" && setCalls.length > 0) ||
1009
+ (name === "bind" && shouldRemoveBind)
1010
+ );
1011
+ });
1012
+
1013
+ // Sort by position
1014
+ importsToActuallyRemove.sort((a, b) => a.specifier.start - b.specifier.start);
1015
+
1016
+ // Remove from right to left to preserve positions
1017
+ for (let i = importsToActuallyRemove.length - 1; i >= 0; i--) {
1018
+ const { specifier } = importsToActuallyRemove[i];
1019
+
1020
+ // Check if this is the first import (after opening brace)
1021
+ const beforeSpecifier = code.slice(Math.max(0, specifier.start - 2), specifier.start);
1022
+ const afterSpecifier = code.slice(specifier.end, specifier.end + 2);
1023
+
1024
+ let startPos = specifier.start;
1025
+ let endPos = specifier.end;
1026
+
1027
+ if (afterSpecifier.startsWith(", ")) {
1028
+ // Remove trailing ", "
1029
+ endPos = specifier.end + 2;
1030
+ } else if (afterSpecifier.startsWith(",")) {
1031
+ // Remove trailing ","
1032
+ endPos = specifier.end + 1;
1033
+ } else if (beforeSpecifier.endsWith(", ")) {
1034
+ // No trailing comma - remove leading ", "
1035
+ startPos = specifier.start - 2;
1036
+ }
1037
+
1038
+ s.remove(startPos, endPos);
1039
+ }
1040
+
1041
+ return {
1042
+ code: s.toString(),
1043
+ map: s.generateMap({
1044
+ source: filename,
1045
+ file: filename + ".map",
1046
+ includeContent: true,
1047
+ }),
1048
+ };
1049
+ }