roqa 0.0.1 → 0.0.2

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.2] - 2026-01-10
9
+
10
+ ### Fixed
11
+
12
+ - Fixed reactive bindings in `<For>` loops not updating when the bound cell references closure variables from the loop callback (e.g., `class={get(selected) === row.id ? "danger" : ""}`)
13
+ - The compiler now correctly preserves `bind()` calls when the callback body contains closure variables that wouldn't be available at `set()` call sites
14
+
8
15
  ## [0.0.1] - 2026-01-10
9
16
 
10
17
  ### Added
package/README.md CHANGED
@@ -2,10 +2,16 @@
2
2
 
3
3
  Roqa is a compile-time reactive web framework for building user interfaces and applications.
4
4
 
5
- Learn more about it at https://roqa.dev.
6
-
7
5
  ## At a glance
8
6
 
7
+ Roqa ships a familiar API and syntax for writing component based web UIs, with a few unique differences.
8
+
9
+ Under the hood every Roqa component is transformed into unbelievably optimized and performant custom elements & vanilla JavaScript. This means a compiled Roqa component is extremely portable and can be used in any other web framework or web-based environment. Additionally, by default Roqa does not use Shadow DOM, so you won't have to have to fight the styling gods to create beautiful web pages and applications –– you can use any of your favorite styling solutions out of the box.
10
+
11
+ The reactive primitive of Roqa is a `cell`. It can roughly be thought of as a signal, but at compile time, this "signal" is compiled to an ultra-lightweight plain JavaScript object. A handful of functions (i.e. `get`, `set`, `put`) are provided to manipulate the data in this object and reactive updates are automatically applied after a change is made.
12
+
13
+ Continue to learn more about Roqa at https://roqa.dev.
14
+
9
15
  ```jsx
10
16
  import { defineComponent, cell, get, set } from "roqa";
11
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roqa",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Roqa is a reactive UI framework",
5
5
  "keywords": [
6
6
  "UI",
@@ -458,7 +458,7 @@ function findBlockInfo(ast, code) {
458
458
  * Returns a map from cell code -> array of callback info
459
459
  */
460
460
  function findBindCallbacks(ast, code) {
461
- // Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd }]
461
+ // Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd, hasClosureVars }]
462
462
  const bindCallbacks = new Map();
463
463
  // Track ref numbers per cell
464
464
  const refCounters = new Map();
@@ -495,7 +495,9 @@ function findBindCallbacks(ast, code) {
495
495
  }
496
496
 
497
497
  // Find element variables used in the callback (e.g., p_1_text, tr_1)
498
- const elementVars = findElementVariables(body);
498
+ // Also detect closure variables that would prevent inlining
499
+ const { elementVars, closureVars } = findElementVariables(body, code, paramName, cellCode);
500
+ const hasClosureVars = closureVars.size > 0;
499
501
 
500
502
  // Get or create ref number for this cell
501
503
  const currentRef = refCounters.get(cellCode) || 0;
@@ -514,6 +516,7 @@ function findBindCallbacks(ast, code) {
514
516
  paramName,
515
517
  statementStart: path.node.start,
516
518
  statementEnd: path.node.end,
519
+ hasClosureVars,
517
520
  });
518
521
  },
519
522
  noScope: true,
@@ -525,15 +528,44 @@ function findBindCallbacks(ast, code) {
525
528
  /**
526
529
  * Find element variables used in a callback body
527
530
  * Looks for element.property = ... patterns
531
+ * Also returns closure variables that would prevent inlining
528
532
  */
529
- function findElementVariables(body) {
533
+ function findElementVariables(body, code, paramName, cellCode) {
530
534
  const elementVars = [];
535
+ const closureVars = new Set();
531
536
  const seen = new Set();
532
537
 
538
+ // Extract the cell's base identifier (e.g., "selected" from "selected" or "row.label" from "row.label")
539
+ const cellBaseMatch = cellCode.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
540
+ const cellBase = cellBaseMatch ? cellBaseMatch[1] : cellCode;
541
+
542
+ // Track identifiers that are used as property names (not variables)
543
+ const propertyNames = new Set();
544
+
545
+ // First pass: identify property names in member expressions
546
+ function findPropertyNames(node) {
547
+ if (!node) return;
548
+ if (node.type === "MemberExpression" && node.property.type === "Identifier" && !node.computed) {
549
+ // The property is accessed with dot notation, so it's not a variable reference
550
+ propertyNames.add(node.property);
551
+ }
552
+ for (const key of Object.keys(node)) {
553
+ const child = node[key];
554
+ if (child && typeof child === "object") {
555
+ if (Array.isArray(child)) {
556
+ child.forEach(findPropertyNames);
557
+ } else if (child.type) {
558
+ findPropertyNames(child);
559
+ }
560
+ }
561
+ }
562
+ }
563
+ findPropertyNames(body);
564
+
533
565
  function visit(node) {
534
566
  if (!node) return;
535
567
 
536
- // Look for element.property = ... patterns
568
+ // Look for element.property = ... patterns (element variable assignments)
537
569
  if (
538
570
  node.type === "AssignmentExpression" &&
539
571
  node.left.type === "MemberExpression" &&
@@ -546,6 +578,57 @@ function findElementVariables(body) {
546
578
  }
547
579
  }
548
580
 
581
+ // Look for identifier.property patterns (potential closure variables)
582
+ // e.g., row.id, item.name - these are closure variables from forBlock
583
+ if (node.type === "MemberExpression" && node.object.type === "Identifier") {
584
+ const varName = node.object.name;
585
+ // Skip if it's the callback parameter, the cell, or an already identified element var
586
+ if (varName !== paramName && varName !== cellBase && !seen.has(varName)) {
587
+ // This could be a closure variable - mark it
588
+ closureVars.add(varName);
589
+ }
590
+ }
591
+
592
+ // Also check for standalone identifiers that could be closure variables
593
+ // But skip identifiers that are property names in member expressions
594
+ if (node.type === "Identifier" && !propertyNames.has(node)) {
595
+ const varName = node.name;
596
+ // Skip common globals and the callback parameter
597
+ const knownGlobals = new Set([
598
+ "undefined",
599
+ "null",
600
+ "true",
601
+ "false",
602
+ "NaN",
603
+ "Infinity",
604
+ "console",
605
+ "window",
606
+ "document",
607
+ "Math",
608
+ "JSON",
609
+ "Array",
610
+ "Object",
611
+ "String",
612
+ "Number",
613
+ "Boolean",
614
+ "Date",
615
+ "RegExp",
616
+ "Error",
617
+ "Promise",
618
+ "Map",
619
+ "Set",
620
+ ]);
621
+ if (
622
+ varName !== paramName &&
623
+ varName !== cellBase &&
624
+ !seen.has(varName) &&
625
+ !knownGlobals.has(varName)
626
+ ) {
627
+ // Could be a closure variable
628
+ closureVars.add(varName);
629
+ }
630
+ }
631
+
549
632
  // Recurse into child nodes
550
633
  for (const key of Object.keys(node)) {
551
634
  const child = node[key];
@@ -560,7 +643,13 @@ function findElementVariables(body) {
560
643
  }
561
644
 
562
645
  visit(body);
563
- return elementVars;
646
+
647
+ // Remove element vars from closure vars (they're defined in the same scope level)
648
+ for (const { varName } of elementVars) {
649
+ closureVars.delete(varName);
650
+ }
651
+
652
+ return { elementVars, closureVars };
564
653
  }
565
654
 
566
655
  /**
@@ -811,8 +900,12 @@ export function inlineGetCalls(code, filename) {
811
900
  }
812
901
 
813
902
  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);
903
+ // Filter to only include callbacks that:
904
+ // 1. Have element vars (can update DOM elements)
905
+ // 2. Don't have closure vars (can be inlined at set() call sites)
906
+ const inlinableCallbacks = callbacks.filter(
907
+ (cb) => cb.elementVars.length > 0 && !cb.hasClosureVars,
908
+ );
816
909
  return inlinableCallbacks
817
910
  .map((cb) => {
818
911
  let body = transformCallbackBody(
@@ -927,11 +1020,15 @@ export function inlineGetCalls(code, filename) {
927
1020
  const blockUpdate = call.blockVar ? `${call.blockVar}.update();` : "";
928
1021
 
929
1022
  // 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
1023
+ // These are callbacks that:
1024
+ // 1. Have no element vars (can't update DOM), OR
1025
+ // 2. Have closure vars (can't be inlined at set() call sites)
931
1026
  let hasNonInlinedBinds = false;
932
1027
  const cellCallbacks = bindCallbacks.get(call.cellCode);
933
1028
  if (cellCallbacks) {
934
- hasNonInlinedBinds = cellCallbacks.some((cb) => cb.elementVars.length === 0);
1029
+ hasNonInlinedBinds = cellCallbacks.some(
1030
+ (cb) => cb.elementVars.length === 0 || cb.hasClosureVars,
1031
+ );
935
1032
  }
936
1033
 
937
1034
  // Check if this cell is a source for forBlock/showBlock
@@ -978,6 +1075,13 @@ export function inlineGetCalls(code, filename) {
978
1075
  bindStatementsToRemove.sort((a, b) => b.start - a.start);
979
1076
 
980
1077
  for (const { start, end, cellCode, callback } of bindStatementsToRemove) {
1078
+ // Don't inline bind() calls that have closure variables (e.g., from forBlock item parameter)
1079
+ // These callbacks capture variables like `row` that won't be in scope at set() call sites
1080
+ if (callback.hasClosureVars) {
1081
+ // Keep the bind() call - runtime handles execution
1082
+ continue;
1083
+ }
1084
+
981
1085
  // Generate ref assignment for each element variable
982
1086
  let refAssignment = "";
983
1087
  for (const { varName } of callback.elementVars) {
@@ -997,8 +1101,10 @@ export function inlineGetCalls(code, filename) {
997
1101
 
998
1102
  // Remove imports that are no longer needed
999
1103
  // 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);
1104
+ // Only remove bind import if ALL bind calls were fully inlined (had element vars and no closure vars)
1105
+ const allBindsInlined = bindStatementsToRemove.every(
1106
+ (b) => b.callback.elementVars.length > 0 && !b.callback.hasClosureVars,
1107
+ );
1002
1108
  const shouldRemoveBind = bindStatementsToRemove.length > 0 && allBindsInlined;
1003
1109
  const importsToActuallyRemove = importsToRemove.filter(({ name }) => {
1004
1110
  return (