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,1217 @@
1
+ import MagicString from "magic-string";
2
+ import { isJSXElement, isJSXFragment } from "./parser.js";
3
+ import { processBindings, findGetCalls } from "./transforms/bind-detector.js";
4
+ import { processEvents, generateEventAssignment } from "./transforms/events.js";
5
+ import { extractForInfo, getCallbackPreamble } from "./transforms/for-transform.js";
6
+ import {
7
+ TemplateRegistry,
8
+ VariableNameGenerator,
9
+ extractTemplate,
10
+ } from "./transforms/jsx-to-template.js";
11
+ import { extractShowInfo } from "./transforms/show-transform.js";
12
+ import { CONSTANTS, traverse, escapeStringLiteral } from "./utils.js";
13
+
14
+ /**
15
+ * Code generator using magic-string for efficient string manipulation
16
+ * and source map generation
17
+ */
18
+
19
+ /**
20
+ * Generate compiled output from analyzed JSX
21
+ * @param {string} code - Original source code
22
+ * @param {import("@babel/types").File} ast - Babel AST
23
+ * @param {string} filename - Source filename
24
+ * @returns {{ code: string, map: object }}
25
+ */
26
+ export function generateOutput(code, ast, filename) {
27
+ const s = new MagicString(code);
28
+ const templateRegistry = new TemplateRegistry();
29
+ const nameGen = new VariableNameGenerator();
30
+ const allEventTypes = new Set();
31
+
32
+ // Track what framework imports are needed
33
+ const usedImports = new Set(["defineComponent"]);
34
+
35
+ // Find all Roqa-defined component tag names (from defineComponent calls)
36
+ const roqaComponentTags = findRoqaComponentTags(ast);
37
+
38
+ // Find all component functions with JSX returns
39
+ const componentInfos = [];
40
+
41
+ traverse(ast, {
42
+ FunctionDeclaration(path) {
43
+ const jsxReturn = findJSXReturn(path.node.body);
44
+ if (jsxReturn) {
45
+ componentInfos.push({
46
+ type: "declaration",
47
+ node: path.node,
48
+ name: path.node.id.name,
49
+ jsxReturn,
50
+ bodyStart: path.node.body.start,
51
+ bodyEnd: path.node.body.end,
52
+ params: path.node.params,
53
+ });
54
+ }
55
+ },
56
+ FunctionExpression(path) {
57
+ const jsxReturn = findJSXReturn(path.node.body);
58
+ if (jsxReturn) {
59
+ componentInfos.push({
60
+ type: "expression",
61
+ node: path.node,
62
+ jsxReturn,
63
+ bodyStart: path.node.body.start,
64
+ bodyEnd: path.node.body.end,
65
+ params: path.node.params,
66
+ });
67
+ }
68
+ },
69
+ noScope: true,
70
+ });
71
+
72
+ if (componentInfos.length === 0) {
73
+ // No JSX found, return as-is
74
+ return {
75
+ code: s.toString(),
76
+ map: s.generateMap({ source: filename, includeContent: true }),
77
+ };
78
+ }
79
+
80
+ // Transform all components (process in reverse order to preserve positions)
81
+ for (const componentInfo of componentInfos.slice().reverse()) {
82
+ transformComponent(
83
+ code,
84
+ s,
85
+ ast,
86
+ componentInfo,
87
+ templateRegistry,
88
+ nameGen,
89
+ usedImports,
90
+ allEventTypes,
91
+ roqaComponentTags,
92
+ );
93
+ }
94
+
95
+ // Collect all needed imports BEFORE calling updateImports
96
+ // Add delegate if we have events
97
+ if (allEventTypes.size > 0) {
98
+ usedImports.add("delegate");
99
+ }
100
+
101
+ // Add template if we have templates
102
+ const templateDecls = templateRegistry.getDeclarations();
103
+ if (templateDecls.length > 0) {
104
+ usedImports.add("template");
105
+ // Also add svgTemplate if any SVG templates are used
106
+ if (templateRegistry.hasSvgTemplates()) {
107
+ usedImports.add("svgTemplate");
108
+ }
109
+ }
110
+
111
+ // NOW update imports with the complete set
112
+ updateImports(s, ast, usedImports);
113
+
114
+ // Add delegate call at the end if we have events
115
+ if (allEventTypes.size > 0) {
116
+ const eventTypesArray = Array.from(allEventTypes)
117
+ .map((e) => `"${e}"`)
118
+ .join(", ");
119
+ s.append(`\n\ndelegate([${eventTypesArray}]);`);
120
+ }
121
+
122
+ // Prepend template declarations after imports
123
+ if (templateDecls.length > 0) {
124
+ const importEndPos = findImportEndPosition(ast);
125
+ s.appendLeft(importEndPos, "\n\n" + templateDecls.join("\n"));
126
+ }
127
+
128
+ return {
129
+ code: s.toString(),
130
+ map: s.generateMap({
131
+ source: filename,
132
+ file: filename + ".map",
133
+ includeContent: true,
134
+ }),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Check if a node is JSX (element or fragment)
140
+ */
141
+ function isJSX(node) {
142
+ return isJSXElement(node) || isJSXFragment(node);
143
+ }
144
+
145
+ /**
146
+ * Find the JSX return statement in a function body
147
+ */
148
+ function findJSXReturn(body) {
149
+ if (body.type !== "BlockStatement") return null;
150
+
151
+ for (const stmt of body.body) {
152
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
153
+ if (isJSX(stmt.argument)) {
154
+ return stmt;
155
+ }
156
+ // Handle parenthesized: return (<div>...</div>)
157
+ if (stmt.argument.type === "ParenthesizedExpression") {
158
+ if (isJSX(stmt.argument.expression)) {
159
+ return stmt;
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ /**
169
+ * Find all Roqa-defined component tag names from defineComponent calls
170
+ * @param {import("@babel/types").File} ast - Babel AST
171
+ * @returns {Set<string>} Set of tag names defined via defineComponent
172
+ */
173
+ function findRoqaComponentTags(ast) {
174
+ const tags = new Set();
175
+
176
+ traverse(ast, {
177
+ CallExpression(path) {
178
+ const node = path.node;
179
+ // Check if this is a defineComponent call
180
+ if (
181
+ node.callee &&
182
+ node.callee.type === "Identifier" &&
183
+ node.callee.name === "defineComponent" &&
184
+ node.arguments.length >= 1
185
+ ) {
186
+ const firstArg = node.arguments[0];
187
+ // Extract the tag name from the first argument (should be a string literal)
188
+ if (firstArg.type === "StringLiteral") {
189
+ tags.add(firstArg.value);
190
+ }
191
+ }
192
+ },
193
+ noScope: true,
194
+ });
195
+
196
+ return tags;
197
+ }
198
+
199
+ /**
200
+ * Transform a component function
201
+ */
202
+ function transformComponent(
203
+ code,
204
+ s,
205
+ ast,
206
+ componentInfo,
207
+ templateRegistry,
208
+ nameGen,
209
+ usedImports,
210
+ allEventTypes,
211
+ roqaComponentTags,
212
+ ) {
213
+ const { jsxReturn, bodyStart, bodyEnd, name } = componentInfo;
214
+
215
+ // Get the JSX element or fragment
216
+ let jsxNode = isJSX(jsxReturn.argument) ? jsxReturn.argument : jsxReturn.argument.expression;
217
+
218
+ // Extract template info (isComponentRoot = true for main component)
219
+ // Pass the fragment flag so extractTemplate can handle it properly
220
+ const isFragment = isJSXFragment(jsxNode);
221
+ const templateResult = extractTemplate(
222
+ jsxNode,
223
+ templateRegistry,
224
+ nameGen,
225
+ true,
226
+ isFragment,
227
+ roqaComponentTags,
228
+ );
229
+ const { templateVar, rootVar, traversal, bindings, events, forBlocks, showBlocks } =
230
+ templateResult;
231
+
232
+ // Process bindings for bind() calls
233
+ const processedBindings = processBindings(bindings, code);
234
+ for (const b of processedBindings) {
235
+ if (!b.isStatic) {
236
+ usedImports.add("bind");
237
+ usedImports.add("get");
238
+ }
239
+ }
240
+
241
+ // Process events
242
+ const processedEvents = processEvents(events);
243
+ for (const e of processedEvents) {
244
+ allEventTypes.add(e.eventName);
245
+ }
246
+
247
+ // Extract forBlock cell names for variable hoisting
248
+ const forBlockVars = extractForBlockVars(code, forBlocks);
249
+
250
+ // Build the connected callback body
251
+ const connectedBody = buildConnectedBody(
252
+ code,
253
+ templateVar,
254
+ rootVar,
255
+ traversal,
256
+ processedBindings,
257
+ processedEvents,
258
+ forBlocks,
259
+ showBlocks,
260
+ nameGen,
261
+ templateRegistry,
262
+ usedImports,
263
+ allEventTypes,
264
+ forBlockVars,
265
+ );
266
+
267
+ // Replace the return statement with this.connected()
268
+ const returnStart = jsxReturn.start;
269
+ const returnEnd = jsxReturn.end;
270
+
271
+ // Generate variable declarations for forBlock captures (hoisted before connected)
272
+ const forBlockDecls = forBlockVars.map((fb) => `let ${fb.varName};`).join("\n ");
273
+ const forBlockDeclsCode = forBlockDecls ? `${forBlockDecls}\n ` : "";
274
+
275
+ const connectedCode = `${forBlockDeclsCode}this.connected(() => {
276
+ ${connectedBody}
277
+ });`;
278
+
279
+ // Ensure the generated this.connected (which appends the root template)
280
+ // is the first this.connected() call inside the component body. If the
281
+ // original source already contained one or more this.connected() calls
282
+ // earlier in the function, move those after our generated connected.
283
+ const existingConnected = [];
284
+
285
+ traverse(ast, {
286
+ CallExpression(path) {
287
+ const node = path.node;
288
+ if (
289
+ node.callee &&
290
+ node.callee.type === "MemberExpression" &&
291
+ node.callee.object &&
292
+ node.callee.object.type === "ThisExpression" &&
293
+ node.callee.property &&
294
+ node.callee.property.type === "Identifier" &&
295
+ node.callee.property.name === "connected" &&
296
+ node.start >= bodyStart &&
297
+ node.end <= bodyEnd
298
+ ) {
299
+ existingConnected.push({ start: node.start, end: node.end });
300
+ }
301
+ },
302
+ noScope: true,
303
+ });
304
+
305
+ existingConnected.sort((a, b) => a.start - b.start);
306
+
307
+ const leadingConnected = existingConnected.filter((c) => c.start < returnStart);
308
+
309
+ if (leadingConnected.length > 0) {
310
+ const snippets = leadingConnected.map((c) => code.slice(c.start, c.end));
311
+
312
+ for (let i = leadingConnected.length - 1; i >= 0; i--) {
313
+ const c = leadingConnected[i];
314
+ s.remove(c.start, c.end);
315
+ }
316
+
317
+ s.overwrite(returnStart, returnEnd, "");
318
+
319
+ const insertPos = leadingConnected[0].start;
320
+ const combined = `${connectedCode}\n\n${snippets.join("\n\n")}`;
321
+ s.appendLeft(insertPos, combined);
322
+ } else {
323
+ s.overwrite(returnStart, returnEnd, connectedCode);
324
+ }
325
+
326
+ return { componentName: name };
327
+ }
328
+
329
+ /**
330
+ * Extract forBlock cell names and generate variable names for capturing returns
331
+ * @param {string} code - Original source code
332
+ * @param {Array} forBlocks - For blocks from template extraction
333
+ * @returns {Array<{cellName: string, varName: string, cellCode: string}>}
334
+ */
335
+ function extractForBlockVars(code, forBlocks) {
336
+ const result = [];
337
+ for (const forBlock of forBlocks) {
338
+ const forInfo = extractForInfo(forBlock.node, forBlock.containerVarName);
339
+ const cellCode = code.slice(forInfo.itemsExpression.start, forInfo.itemsExpression.end);
340
+ // Use simple identifier if possible, otherwise generate a name
341
+ const cellName =
342
+ forInfo.itemsExpression.type === "Identifier"
343
+ ? forInfo.itemsExpression.name
344
+ : `forBlock_${result.length + 1}`;
345
+ result.push({
346
+ cellName,
347
+ varName: `${cellName}_forBlock`,
348
+ cellCode,
349
+ });
350
+ }
351
+ return result;
352
+ }
353
+
354
+ /**
355
+ * Collect all variable names that are actually used by bindings, events, for blocks, or show blocks
356
+ */
357
+ function collectUsedVars(bindings, events, forBlocks, showBlocks = []) {
358
+ const used = new Set();
359
+
360
+ for (const b of bindings) {
361
+ used.add(b.targetVar);
362
+ // If this binding uses a marker, also track the marker variable
363
+ if (b.usesMarker) {
364
+ used.add(`${b.targetVar}_marker`);
365
+ }
366
+ }
367
+
368
+ for (const e of events) {
369
+ used.add(e.varName);
370
+ }
371
+
372
+ for (const f of forBlocks) {
373
+ used.add(f.containerVarName);
374
+ }
375
+
376
+ for (const s of showBlocks) {
377
+ used.add(s.containerVarName);
378
+ }
379
+
380
+ return used;
381
+ }
382
+
383
+ /**
384
+ * Filter traversal steps to only include those that are needed
385
+ * A step is needed if:
386
+ * 1. Its variable is directly used by a binding/event/forBlock
387
+ * 2. Its variable is referenced by another needed step's traversal code
388
+ * @param {Array} traversal - Traversal steps
389
+ * @param {Set} usedVars - Variables that are directly used
390
+ * @param {Set} alreadyDeclared - Variables that have already been declared (to avoid duplicates)
391
+ */
392
+ function filterTraversalSteps(traversal, usedVars, alreadyDeclared = new Set()) {
393
+ // Start with directly used vars
394
+ const needed = new Set(usedVars);
395
+
396
+ // Build a map of varName -> step for quick lookup
397
+ const stepMap = new Map();
398
+ for (const step of traversal) {
399
+ stepMap.set(step.varName, step);
400
+ }
401
+
402
+ // Iteratively expand needed set based on dependencies
403
+ // A step depends on another if its code references that var
404
+ let changed = true;
405
+ while (changed) {
406
+ changed = false;
407
+ for (const step of traversal) {
408
+ if (needed.has(step.varName)) {
409
+ // Check what this step depends on
410
+ // e.g., "div_2.firstChild" depends on "div_2"
411
+ for (const [varName] of stepMap) {
412
+ if (step.code.startsWith(varName + ".") && !needed.has(varName)) {
413
+ needed.add(varName);
414
+ changed = true;
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ // Filter traversal to only needed steps, excluding already declared vars
422
+ return traversal.filter((step) => needed.has(step.varName) && !alreadyDeclared.has(step.varName));
423
+ }
424
+
425
+ /**
426
+ * Build the body of the connected() callback
427
+ */
428
+ function buildConnectedBody(
429
+ code,
430
+ templateVar,
431
+ rootVar,
432
+ traversal,
433
+ bindings,
434
+ events,
435
+ forBlocks,
436
+ showBlocks,
437
+ nameGen,
438
+ templateRegistry,
439
+ usedImports,
440
+ allEventTypes,
441
+ forBlockVars = [],
442
+ ) {
443
+ const lines = [];
444
+
445
+ // Separate prop bindings (need to be set before appendChild)
446
+ const propBindings = bindings.filter((b) => b.type === "prop");
447
+ const otherBindings = bindings.filter((b) => b.type !== "prop");
448
+
449
+ // Template instantiation
450
+ lines.push(` const ${rootVar} = ${templateVar}();`);
451
+
452
+ // If we have prop bindings, we need to set them BEFORE appendChild
453
+ // This requires getting element references from the template fragment
454
+ if (propBindings.length > 0) {
455
+ // Collect used vars for props
456
+ const propUsedVars = new Set(propBindings.map((b) => b.targetVar));
457
+ // Generate traversal that works on the template fragment (before appendChild)
458
+ const propTraversal = filterTraversalSteps(traversal, propUsedVars);
459
+
460
+ // Generate traversal code using rootVar instead of this.firstChild
461
+ for (const step of propTraversal) {
462
+ // Replace 'this.firstChild' with rootVar.firstChild for props
463
+ const propCode = step.code.replace("this.firstChild", `${rootVar}.firstChild`);
464
+ lines.push(` const ${step.varName} = ${propCode};`);
465
+ }
466
+
467
+ lines.push("");
468
+
469
+ // Set props before appendChild (using WeakMap-based setProp)
470
+ for (const binding of propBindings) {
471
+ const bindCode = generateBinding(code, binding, usedImports, null, false, null);
472
+ lines.push(` ${bindCode}`);
473
+ }
474
+
475
+ lines.push("");
476
+ }
477
+
478
+ lines.push(` this.appendChild(${rootVar});`);
479
+ lines.push("");
480
+
481
+ // Track which vars have already been declared (from prop bindings)
482
+ const alreadyDeclared = new Set();
483
+ if (propBindings.length > 0) {
484
+ // Collect all vars that were declared for prop bindings
485
+ const propUsedVars = new Set(propBindings.map((b) => b.targetVar));
486
+ const propTraversalVars = filterTraversalSteps(traversal, propUsedVars);
487
+ for (const step of propTraversalVars) {
488
+ alreadyDeclared.add(step.varName);
489
+ }
490
+ }
491
+
492
+ // Filter traversal to only include steps that are actually needed (excluding prop vars already declared)
493
+ const usedVars = collectUsedVars(otherBindings, events, forBlocks, showBlocks);
494
+ const filteredTraversal = filterTraversalSteps(traversal, usedVars, alreadyDeclared);
495
+
496
+ // DOM traversal - text nodes are now regular nodes (space placeholders), not markers
497
+ for (const step of filteredTraversal) {
498
+ lines.push(` const ${step.varName} = ${step.code};`);
499
+ }
500
+
501
+ if (filteredTraversal.length > 0) {
502
+ lines.push("");
503
+ }
504
+
505
+ // Event handler assignments
506
+ for (const event of events) {
507
+ const assignment = generateEventAssignment(event, (node) => generateExpr(code, node));
508
+ lines.push(` ${assignment}`);
509
+ }
510
+
511
+ if (events.length > 0) {
512
+ lines.push("");
513
+ }
514
+
515
+ // Process for blocks
516
+ for (let i = 0; i < forBlocks.length; i++) {
517
+ const forBlock = forBlocks[i];
518
+ const forBlockVar = forBlockVars[i];
519
+ usedImports.add("forBlock");
520
+ const forCode = generateForBlock(
521
+ code,
522
+ forBlock,
523
+ nameGen,
524
+ templateRegistry,
525
+ usedImports,
526
+ allEventTypes,
527
+ forBlockVar,
528
+ );
529
+ lines.push(forCode);
530
+ lines.push("");
531
+ }
532
+
533
+ // Process show blocks
534
+ for (const showBlock of showBlocks) {
535
+ usedImports.add("showBlock");
536
+ const showCode = generateShowBlock(
537
+ code,
538
+ showBlock,
539
+ nameGen,
540
+ templateRegistry,
541
+ usedImports,
542
+ allEventTypes,
543
+ );
544
+ lines.push(showCode);
545
+ lines.push("");
546
+ }
547
+
548
+ // Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
549
+ const cellRefCounts = new Map();
550
+
551
+ // Other bindings (non-prop)
552
+ for (const binding of otherBindings) {
553
+ const bindCode = generateBinding(code, binding, usedImports, null, false, cellRefCounts);
554
+ lines.push(` ${bindCode}`);
555
+ }
556
+
557
+ return lines.join("\n");
558
+ }
559
+
560
+ /**
561
+ * Generate code for a forBlock
562
+ */
563
+ function generateForBlock(
564
+ code,
565
+ forBlock,
566
+ nameGen,
567
+ templateRegistry,
568
+ usedImports,
569
+ allEventTypes,
570
+ forBlockVar = null,
571
+ ) {
572
+ const { containerVarName, node } = forBlock;
573
+
574
+ // Extract <For> component info
575
+ const forInfo = extractForInfo(node, containerVarName);
576
+ const { itemsExpression, itemParam, indexParam, bodyJSX, originalCallback } = forInfo;
577
+
578
+ // Get preamble code from original callback (variable declarations, etc.)
579
+ const preamble = getCallbackPreamble(originalCallback);
580
+ const preambleCode = preamble
581
+ .map((stmt) => " " + code.slice(stmt.start, stmt.end))
582
+ .join("\n");
583
+
584
+ // Extract template for the loop body (including nested forBlocks and showBlocks)
585
+ const innerTemplate = extractTemplate(bodyJSX, templateRegistry, nameGen);
586
+ const {
587
+ templateVar,
588
+ rootVar,
589
+ traversal,
590
+ bindings,
591
+ events,
592
+ forBlocks: innerForBlocks,
593
+ showBlocks: innerShowBlocks,
594
+ } = innerTemplate;
595
+
596
+ // Process inner bindings (these need to reference the item parameter)
597
+ const processedBindings = processBindings(bindings, code);
598
+ for (const b of processedBindings) {
599
+ if (!b.isStatic) {
600
+ usedImports.add("bind");
601
+ usedImports.add("get");
602
+ }
603
+ }
604
+
605
+ // Process inner events (may reference item)
606
+ const processedEvents = processEvents(events, itemParam);
607
+ for (const e of processedEvents) {
608
+ allEventTypes.add(e.eventName);
609
+ }
610
+
611
+ // Build the forBlock callback body
612
+ const indexParamName = indexParam || "index";
613
+ const lines = [];
614
+
615
+ // Capture the forBlock return value if we have a variable for it
616
+ const forBlockAssignment = forBlockVar ? `${forBlockVar.varName} = ` : "";
617
+
618
+ lines.push(
619
+ ` ${forBlockAssignment}forBlock(${containerVarName}, ${generateExpr(
620
+ code,
621
+ itemsExpression,
622
+ )}, (anchor, ${itemParam}, ${indexParamName}) => {`,
623
+ );
624
+ lines.push(` const ${rootVar} = ${templateVar}();`);
625
+ lines.push("");
626
+
627
+ // Filter traversal to only include steps that are actually needed
628
+ const usedVars = collectUsedVars(
629
+ processedBindings,
630
+ processedEvents,
631
+ innerForBlocks || [],
632
+ innerShowBlocks || [],
633
+ );
634
+ const filteredTraversal = filterTraversalSteps(traversal, usedVars);
635
+
636
+ // Traversal - text nodes are now regular nodes (space placeholders), not markers
637
+ for (const step of filteredTraversal) {
638
+ lines.push(` const ${step.varName} = ${step.code};`);
639
+ }
640
+
641
+ // Get the first element for start/end tracking
642
+ const firstElementVar = filteredTraversal.length > 0 ? filteredTraversal[0].varName : rootVar;
643
+
644
+ if (filteredTraversal.length > 0) {
645
+ lines.push("");
646
+ }
647
+
648
+ // Events inside for block
649
+ for (const event of processedEvents) {
650
+ const assignment = generateEventAssignment(event, (node) => generateExpr(code, node));
651
+ lines.push(` ${assignment}`);
652
+ }
653
+
654
+ if (processedEvents.length > 0) {
655
+ lines.push("");
656
+ }
657
+
658
+ // Process nested for blocks inside for block
659
+ if (innerForBlocks && innerForBlocks.length > 0) {
660
+ for (const nestedForBlock of innerForBlocks) {
661
+ usedImports.add("forBlock");
662
+ const nestedForCode = generateForBlock(
663
+ code,
664
+ nestedForBlock,
665
+ nameGen,
666
+ templateRegistry,
667
+ usedImports,
668
+ allEventTypes,
669
+ null, // no variable capture needed inside nested forBlock
670
+ );
671
+ // Indent the forBlock code by 2 extra spaces (it's inside another forBlock callback)
672
+ lines.push(nestedForCode.replace(/^ /gm, " "));
673
+ lines.push("");
674
+ }
675
+ }
676
+
677
+ // Process nested show blocks inside for block
678
+ if (innerShowBlocks && innerShowBlocks.length > 0) {
679
+ for (const nestedShowBlock of innerShowBlocks) {
680
+ usedImports.add("showBlock");
681
+ const showCode = generateShowBlock(
682
+ code,
683
+ nestedShowBlock,
684
+ nameGen,
685
+ templateRegistry,
686
+ usedImports,
687
+ allEventTypes,
688
+ );
689
+ // Indent the showBlock code by 2 extra spaces (it's inside forBlock callback)
690
+ lines.push(showCode.replace(/^ /gm, " "));
691
+ lines.push("");
692
+ }
693
+ }
694
+
695
+ // Preamble (local variables from original callback)
696
+ if (preambleCode) {
697
+ lines.push(preambleCode);
698
+ lines.push("");
699
+ }
700
+
701
+ // Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
702
+ const cellRefCounts = new Map();
703
+
704
+ // Bindings inside for block
705
+ for (const binding of processedBindings) {
706
+ const bindCode = generateBinding(code, binding, usedImports, itemParam, true, cellRefCounts);
707
+ lines.push(` ${bindCode}`);
708
+ }
709
+
710
+ // Insert before anchor and return range
711
+ lines.push("");
712
+ lines.push(` anchor.before(${firstElementVar});`);
713
+ lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
714
+ lines.push(` });`);
715
+
716
+ return lines.join("\n");
717
+ }
718
+
719
+ /**
720
+ * Generate code for a showBlock
721
+ */
722
+ function generateShowBlock(code, showBlock, nameGen, templateRegistry, usedImports, allEventTypes) {
723
+ const { containerVarName, node } = showBlock;
724
+
725
+ // Extract <Show> component info
726
+ const showInfo = extractShowInfo(node, containerVarName);
727
+ const { conditionExpression, bodyJSX } = showInfo;
728
+
729
+ // Detect get() calls in the condition to determine if it's simple or complex
730
+ const getCalls = findGetCalls(conditionExpression);
731
+ const isSimpleCell = getCalls.length === 1 && getCalls[0].isOnlyExpression;
732
+
733
+ // Extract template for the show body (including nested forBlocks and showBlocks)
734
+ const innerTemplate = extractTemplate(bodyJSX, templateRegistry, nameGen);
735
+ const {
736
+ templateVar,
737
+ rootVar,
738
+ traversal,
739
+ bindings,
740
+ events,
741
+ forBlocks: innerForBlocks,
742
+ showBlocks: innerShowBlocks,
743
+ } = innerTemplate;
744
+
745
+ // Process inner bindings
746
+ const processedBindings = processBindings(bindings, code);
747
+ for (const b of processedBindings) {
748
+ if (!b.isStatic) {
749
+ usedImports.add("bind");
750
+ usedImports.add("get");
751
+ }
752
+ }
753
+
754
+ // Process inner events
755
+ const processedEvents = processEvents(events);
756
+ for (const e of processedEvents) {
757
+ allEventTypes.add(e.eventName);
758
+ }
759
+
760
+ // Build the showBlock callback body
761
+ const lines = [];
762
+
763
+ // Generate the condition and dependencies based on complexity
764
+ let conditionCode;
765
+ let depsCode = "";
766
+
767
+ if (isSimpleCell) {
768
+ // Simple case: just pass the cell directly
769
+ conditionCode = generateExpr(code, getCalls[0].cellArg);
770
+ } else if (getCalls.length > 0) {
771
+ // Complex expression with get() calls - pass a getter function and deps array
772
+ conditionCode = `() => ${generateExpr(code, conditionExpression)}`;
773
+ const deps = getCalls.map((gc) => generateExpr(code, gc.cellArg));
774
+ depsCode = `, [${deps.join(", ")}]`;
775
+ usedImports.add("get");
776
+ } else {
777
+ // Static expression (no get() calls) - pass the value directly
778
+ conditionCode = generateExpr(code, conditionExpression);
779
+ }
780
+
781
+ lines.push(` showBlock(${containerVarName}, ${conditionCode}, (anchor) => {`);
782
+ lines.push(` const ${rootVar} = ${templateVar}();`);
783
+
784
+ // Filter traversal to only include steps that are actually needed
785
+ const usedVars = collectUsedVars(
786
+ processedBindings,
787
+ processedEvents,
788
+ innerForBlocks || [],
789
+ innerShowBlocks || [],
790
+ );
791
+ const filteredTraversal = filterTraversalSteps(traversal, usedVars);
792
+
793
+ // For start/end tracking, we need the actual first DOM element, not the fragment
794
+ // If there's no traversal, we need to grab firstChild before inserting
795
+ let firstElementVar;
796
+ if (filteredTraversal.length > 0) {
797
+ firstElementVar = filteredTraversal[0].varName;
798
+ } else {
799
+ // No traversal - get firstChild from the fragment before it's emptied by insertion
800
+ firstElementVar = `${rootVar}_first`;
801
+ lines.push(` const ${firstElementVar} = ${rootVar}.firstChild;`);
802
+ }
803
+ lines.push("");
804
+
805
+ // Traversal
806
+ for (const step of filteredTraversal) {
807
+ lines.push(` const ${step.varName} = ${step.code};`);
808
+ }
809
+
810
+ if (filteredTraversal.length > 0) {
811
+ lines.push("");
812
+ }
813
+
814
+ // Events inside show block
815
+ for (const event of processedEvents) {
816
+ const assignment = generateEventAssignment(event, (node) => generateExpr(code, node));
817
+ lines.push(` ${assignment}`);
818
+ }
819
+
820
+ if (processedEvents.length > 0) {
821
+ lines.push("");
822
+ }
823
+
824
+ // Process nested for blocks inside show block
825
+ if (innerForBlocks && innerForBlocks.length > 0) {
826
+ for (const forBlock of innerForBlocks) {
827
+ usedImports.add("forBlock");
828
+ const forCode = generateForBlock(
829
+ code,
830
+ forBlock,
831
+ nameGen,
832
+ templateRegistry,
833
+ usedImports,
834
+ allEventTypes,
835
+ null, // no variable capture needed inside showBlock
836
+ );
837
+ // Indent the forBlock code by 2 extra spaces (it's inside showBlock callback)
838
+ lines.push(forCode.replace(/^ /gm, " "));
839
+ lines.push("");
840
+ }
841
+ }
842
+
843
+ // Process nested show blocks inside show block
844
+ if (innerShowBlocks && innerShowBlocks.length > 0) {
845
+ for (const nestedShowBlock of innerShowBlocks) {
846
+ usedImports.add("showBlock");
847
+ const showCode = generateShowBlock(
848
+ code,
849
+ nestedShowBlock,
850
+ nameGen,
851
+ templateRegistry,
852
+ usedImports,
853
+ allEventTypes,
854
+ );
855
+ // Indent the showBlock code by 2 extra spaces (it's inside showBlock callback)
856
+ lines.push(showCode.replace(/^ /gm, " "));
857
+ lines.push("");
858
+ }
859
+ }
860
+
861
+ // Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
862
+ const cellRefCounts = new Map();
863
+
864
+ // Bindings inside show block
865
+ for (const binding of processedBindings) {
866
+ const bindCode = generateBinding(code, binding, usedImports, null, true, cellRefCounts);
867
+ lines.push(` ${bindCode}`);
868
+ }
869
+
870
+ // Insert before anchor and return range
871
+ lines.push("");
872
+ lines.push(` anchor.before(${firstElementVar});`);
873
+ lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
874
+ lines.push(` }${depsCode});`);
875
+
876
+ return lines.join("\n");
877
+ }
878
+
879
+ /**
880
+ * Check if a binding is a simple direct mapping (v maps directly to property)
881
+ * Simple: get(cell) with no transform -> element.property = v
882
+ * Not simple: get(cell) ? 'a' : 'b' -> requires transform
883
+ */
884
+ function isSimpleDirectBinding(binding) {
885
+ const { needsTransform, staticPrefix, fullExpression, getCallNode } = binding;
886
+
887
+ // If there's a static prefix, it's not simple
888
+ if (staticPrefix) return false;
889
+
890
+ // If no transform needed, the entire expression is get(cell)
891
+ if (!needsTransform) return true;
892
+
893
+ // If transform is needed, check if it's just get(cell) with no surrounding expression
894
+ // The fullExpression should be the same as the getCallNode
895
+ if (
896
+ getCallNode &&
897
+ fullExpression.start === getCallNode.start &&
898
+ fullExpression.end === getCallNode.end
899
+ ) {
900
+ return true;
901
+ }
902
+
903
+ return false;
904
+ }
905
+
906
+ /**
907
+ * Generate code for a binding
908
+ * @param {string} code - Original source code
909
+ * @param {object} binding - Binding info
910
+ * @param {Set} usedImports - Set of imports to track
911
+ * @param {string} itemParam - Item parameter name (for forBlock context)
912
+ * @param {boolean} insideForBlock - Whether we're inside a forBlock callback
913
+ * @param {Map} cellRefCounts - Map to track ref counts per cell (for numbered refs)
914
+ */
915
+ function generateBinding(
916
+ code,
917
+ binding,
918
+ usedImports,
919
+ itemParam = null,
920
+ insideForBlock = false,
921
+ cellRefCounts = null,
922
+ ) {
923
+ // Handle prop bindings (for custom elements)
924
+ if (binding.type === "prop") {
925
+ return generatePropBinding(code, binding, usedImports, insideForBlock);
926
+ }
927
+
928
+ const {
929
+ targetVar,
930
+ targetProperty,
931
+ cellArg,
932
+ fullExpression,
933
+ isStatic,
934
+ needsTransform,
935
+ getCallNode,
936
+ staticPrefix,
937
+ contentParts,
938
+ isSvg,
939
+ } = binding;
940
+
941
+ // Handle new contentParts format (concatenated text content)
942
+ if (contentParts) {
943
+ return generateContentPartsBinding(code, binding, usedImports, insideForBlock, cellRefCounts);
944
+ }
945
+
946
+ // Build prefix string if we have static text before the dynamic expression
947
+ const prefixCode = staticPrefix ? `"${escapeStringLiteral(staticPrefix)}" + ` : "";
948
+
949
+ // For SVG elements or attributes with hyphens, use setAttribute instead of property assignment
950
+ // (except for className and nodeValue which work as properties)
951
+ const needsSetAttribute =
952
+ (isSvg || targetProperty.includes("-")) &&
953
+ targetProperty !== "className" &&
954
+ targetProperty !== "nodeValue";
955
+
956
+ if (isStatic) {
957
+ // Static assignment
958
+ const exprCode = generateExpr(code, fullExpression);
959
+ if (needsSetAttribute) {
960
+ return `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${exprCode});`;
961
+ }
962
+ return `${targetVar}.${targetProperty} = ${prefixCode}${exprCode};`;
963
+ }
964
+
965
+ // Generate the cell argument code
966
+ const cellCode = generateExpr(code, cellArg);
967
+
968
+ // Generate the expression code for initial value (using original get() calls)
969
+ const initialExprCode = generateExpr(code, fullExpression);
970
+
971
+ // Check if this is a simple direct binding (works for both forBlock and component level)
972
+ // If so, we can use ref-based direct DOM updates instead of bind()
973
+ // Note: For SVG or hyphenated attributes we skip ref optimization since setAttribute needs different handling
974
+ if (isSimpleDirectBinding(binding) && cellRefCounts && !needsSetAttribute) {
975
+ // Get or initialize the ref count for this cell
976
+ const currentCount = cellRefCounts.get(cellCode) || 0;
977
+ const refNum = currentCount + 1;
978
+ cellRefCounts.set(cellCode, refNum);
979
+
980
+ // Determine indentation based on context
981
+ const indent = insideForBlock ? " " : " ";
982
+
983
+ // Emit: initial value assignment + ref storage on cell
984
+ // cell.ref_N = element;
985
+ return `${targetVar}.${targetProperty} = ${initialExprCode};
986
+ ${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
987
+ }
988
+
989
+ // Fall back to bind() for complex bindings (or SVG attributes)
990
+ usedImports.add("bind");
991
+ usedImports.add("get");
992
+
993
+ // Generate the expression code for bind callback, replacing get(cell) with v
994
+ let bindExprCode;
995
+ if (needsTransform && getCallNode) {
996
+ // Replace the specific get() call with 'v'
997
+ bindExprCode = generateExprWithReplacement(code, fullExpression, getCallNode, "v");
998
+ } else {
999
+ // Simple case: entire expression is get(cell), so just use v
1000
+ bindExprCode = "v";
1001
+ }
1002
+
1003
+ // Set initial value AND bind for updates
1004
+ if (needsSetAttribute) {
1005
+ return `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
1006
+ bind(${cellCode}, (v) => {
1007
+ ${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
1008
+ });`;
1009
+ }
1010
+
1011
+ return `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
1012
+ bind(${cellCode}, (v) => {
1013
+ ${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
1014
+ });`;
1015
+ }
1016
+
1017
+ /**
1018
+ * Generate binding code for contentParts format (concatenated text)
1019
+ */
1020
+ function generateContentPartsBinding(
1021
+ code,
1022
+ binding,
1023
+ usedImports,
1024
+ insideForBlock = false,
1025
+ cellRefCounts = null,
1026
+ ) {
1027
+ const { targetVar, targetProperty, cellArg, contentParts, isStatic } = binding;
1028
+
1029
+ // Build the concatenated expression from content parts
1030
+ const buildConcatExpr = (useOriginalCode = true) => {
1031
+ const parts = [];
1032
+ for (const part of contentParts) {
1033
+ if (part.type === "static") {
1034
+ // Keep whitespace as-is to preserve spacing between static/dynamic parts
1035
+ const text = part.value;
1036
+ // Only skip if completely empty
1037
+ if (text) {
1038
+ parts.push(`"${escapeStringLiteral(text)}"`);
1039
+ }
1040
+ } else if (part.type === "dynamic") {
1041
+ const exprCode = generateExpr(code, part.expression);
1042
+ // Wrap in parentheses if the expression contains binary operators that could
1043
+ // cause precedence issues when concatenated with strings (e.g., "a + b = " + (get(a) + get(b)))
1044
+ // We check for common arithmetic operators: +, -, *, /, %
1045
+ const needsParens =
1046
+ /[+\-*/%]/.test(exprCode) && !/^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*$/.test(exprCode);
1047
+ if (useOriginalCode) {
1048
+ parts.push(needsParens ? `(${exprCode})` : exprCode);
1049
+ } else {
1050
+ // For bind callback, we keep the original expression
1051
+ // (the bind will just re-evaluate the whole thing)
1052
+ parts.push(needsParens ? `(${exprCode})` : exprCode);
1053
+ }
1054
+ }
1055
+ }
1056
+ return parts.join(" + ");
1057
+ };
1058
+
1059
+ const initialExpr = buildConcatExpr(true);
1060
+
1061
+ if (isStatic) {
1062
+ return `${targetVar}.${targetProperty} = ${initialExpr};`;
1063
+ }
1064
+
1065
+ // For reactive bindings, use ref-based updates
1066
+ const cellCode = generateExpr(code, cellArg);
1067
+
1068
+ if (cellRefCounts) {
1069
+ // Get or initialize the ref count for this cell
1070
+ const currentCount = cellRefCounts.get(cellCode) || 0;
1071
+ const refNum = currentCount + 1;
1072
+ cellRefCounts.set(cellCode, refNum);
1073
+
1074
+ // Determine indentation based on context
1075
+ const indent = insideForBlock ? " " : " ";
1076
+
1077
+ // Store ref on cell for direct DOM updates
1078
+ // The setter will recompute the full expression using the ref
1079
+ return `${targetVar}.${targetProperty} = ${initialExpr};
1080
+ ${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
1081
+ }
1082
+
1083
+ // Fallback if no cellRefCounts provided (shouldn't happen in practice)
1084
+ usedImports.add("bind");
1085
+ usedImports.add("get");
1086
+
1087
+ const indent = insideForBlock ? " " : " ";
1088
+
1089
+ // Set initial value AND bind for updates
1090
+ return `${targetVar}.${targetProperty} = ${initialExpr};
1091
+ ${indent}bind(${cellCode}, (v) => {
1092
+ ${indent} ${targetVar}.${targetProperty} = ${initialExpr};
1093
+ ${indent}});`;
1094
+ }
1095
+
1096
+ /**
1097
+ * Generate code for a prop binding (for custom elements)
1098
+ */
1099
+ function generatePropBinding(code, binding, usedImports) {
1100
+ const { targetVar, propName, expression, fullExpression, isStatic, isThirdParty } = binding;
1101
+
1102
+ // For third-party web components, set property directly on the element
1103
+ // For Roqa components, use setProp() with WeakMap for pre-upgrade storage
1104
+ if (isThirdParty) {
1105
+ // Handle string literal props
1106
+ if (expression && expression.type === "StringLiteral") {
1107
+ return `${targetVar}.${propName} = "${escapeStringLiteral(expression.value)}";`;
1108
+ }
1109
+ const expr = fullExpression || expression;
1110
+ const exprCode = generateExpr(code, expr);
1111
+ return `${targetVar}.${propName} = ${exprCode};`;
1112
+ }
1113
+
1114
+ // Mark that we need setProp import
1115
+ usedImports.add("setProp");
1116
+
1117
+ // Handle string literal props
1118
+ if (expression && expression.type === "StringLiteral") {
1119
+ return `setProp(${targetVar}, "${propName}", "${escapeStringLiteral(expression.value)}");`;
1120
+ }
1121
+
1122
+ // Get the expression to use
1123
+ const expr = fullExpression || expression;
1124
+
1125
+ if (isStatic) {
1126
+ // Static expression (no get() calls)
1127
+ const exprCode = generateExpr(code, expr);
1128
+ return `setProp(${targetVar}, "${propName}", ${exprCode});`;
1129
+ }
1130
+
1131
+ // Reactive prop - set initial value
1132
+ // Note: Props are passed at connection time, so we just need the initial value
1133
+ const exprCode = generateExpr(code, expr);
1134
+ return `setProp(${targetVar}, "${propName}", ${exprCode});`;
1135
+ }
1136
+
1137
+ /**
1138
+ * Generate expression code from AST node using original source
1139
+ */
1140
+ function generateExpr(code, node) {
1141
+ if (!node) return "undefined";
1142
+ return code.slice(node.start, node.end);
1143
+ }
1144
+
1145
+ /**
1146
+ * Generate expression code with a specific node replaced
1147
+ */
1148
+ function generateExprWithReplacement(code, expr, nodeToReplace, replacement) {
1149
+ // Get the full expression code
1150
+ const fullCode = code.slice(expr.start, expr.end);
1151
+
1152
+ // Calculate relative positions
1153
+ const replaceStart = nodeToReplace.start - expr.start;
1154
+ const replaceEnd = nodeToReplace.end - expr.start;
1155
+
1156
+ // Replace
1157
+ return fullCode.slice(0, replaceStart) + replacement + fullCode.slice(replaceEnd);
1158
+ }
1159
+
1160
+ /**
1161
+ * Update the imports at the top of the file
1162
+ */
1163
+ function updateImports(s, ast, usedImports) {
1164
+ // Find existing import from roqa
1165
+ let roqaImport = null;
1166
+
1167
+ traverse(ast, {
1168
+ ImportDeclaration(path) {
1169
+ const source = path.node.source.value;
1170
+ if (source === "roqa") {
1171
+ roqaImport = path.node;
1172
+ path.stop();
1173
+ }
1174
+ },
1175
+ noScope: true,
1176
+ });
1177
+
1178
+ // Preserve existing imports from the original import statement
1179
+ if (roqaImport) {
1180
+ for (const specifier of roqaImport.specifiers) {
1181
+ if (specifier.type === "ImportSpecifier") {
1182
+ const importedName = specifier.imported.name;
1183
+ usedImports.add(importedName);
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ // Build the new import statement
1189
+ const imports = Array.from(usedImports).sort();
1190
+ const newImport = `import { ${imports.join(", ")} } from "roqa";`;
1191
+
1192
+ if (roqaImport) {
1193
+ // Replace existing import
1194
+ s.overwrite(roqaImport.start, roqaImport.end, newImport);
1195
+ } else {
1196
+ // No existing import found, prepend import from roqa
1197
+ s.prepend(`${newImport}\n\n`);
1198
+ }
1199
+ }
1200
+
1201
+ /**
1202
+ * Find the position after all imports
1203
+ */
1204
+ function findImportEndPosition(ast) {
1205
+ let lastImportEnd = 0;
1206
+
1207
+ traverse(ast, {
1208
+ ImportDeclaration(path) {
1209
+ if (path.node.end > lastImportEnd) {
1210
+ lastImportEnd = path.node.end;
1211
+ }
1212
+ },
1213
+ noScope: true,
1214
+ });
1215
+
1216
+ return lastImportEnd;
1217
+ }