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 +7 -0
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/compiler/transforms/inline-get.js +117 -11
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
815
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 (
|