roqa 0.0.5 → 0.0.7
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 +16 -0
- package/package.json +76 -76
- package/src/compiler/codegen.js +138 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.7] - 2026-03-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed broken npm package publish — dependencies now correctly resolved at publish time using `pnpm publish` (previously `catalog:` version references were published verbatim, making the package uninstallable via npm/yarn)
|
|
13
|
+
|
|
14
|
+
## [0.0.6] - 2026-03-31
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added compile-time selector optimization for list selection patterns
|
|
19
|
+
- The compiler now detects `get(cell) === loopItem` inside `<For>` attribute expressions and generates an O(1) selector instead of N per-row `bind()` subscriptions
|
|
20
|
+
- A single shared `Map` (keyed by row reference) and one `bind()` on the outer cell replaces N per-row `bind()` calls
|
|
21
|
+
- On selection change only 2 Map callbacks fire (deselect old, select new) regardless of list size
|
|
22
|
+
- Row cleanup removes its Map entry to prevent memory leaks
|
|
23
|
+
|
|
8
24
|
## [0.0.5] - 2026-03-31
|
|
9
25
|
|
|
10
26
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
2
|
+
"name": "roqa",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Roqa is a reactive UI framework",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"UI",
|
|
7
|
+
"framework",
|
|
8
|
+
"roqa",
|
|
9
|
+
"roqajs",
|
|
10
|
+
"web components",
|
|
11
|
+
"custom elements",
|
|
12
|
+
"jsx",
|
|
13
|
+
"compiler"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://roqa.dev",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/roqajs/roqa/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Hawk Ticehurst",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/roqajs/roqa.git",
|
|
25
|
+
"directory": "packages/roqa"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"types",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"CHANGELOG.md"
|
|
34
|
+
],
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./types/index.d.ts",
|
|
38
|
+
"import": "./src/runtime/index.js",
|
|
39
|
+
"browser": "./src/runtime/index.js",
|
|
40
|
+
"default": "./src/runtime/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json",
|
|
43
|
+
"./compiler": {
|
|
44
|
+
"types": "./types/compiler.d.ts",
|
|
45
|
+
"import": "./src/compiler/index.js",
|
|
46
|
+
"default": "./src/compiler/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./jsx-runtime": {
|
|
49
|
+
"types": "./src/jsx-runtime.d.ts",
|
|
50
|
+
"import": "./src/jsx-runtime.js",
|
|
51
|
+
"default": "./src/jsx-runtime.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@babel/generator": "^7.28.5",
|
|
56
|
+
"@babel/parser": "^7.28.5",
|
|
57
|
+
"@babel/traverse": "^7.28.5",
|
|
58
|
+
"@babel/types": "^7.28.5",
|
|
59
|
+
"magic-string": "^0.30.21"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@vitest/browser": "^3.0.0",
|
|
63
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
64
|
+
"playwright": "^1.49.0",
|
|
65
|
+
"vitest": "^3.0.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=20.0.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"test:watch": "vitest",
|
|
73
|
+
"test:unit": "vitest run --project unit",
|
|
74
|
+
"test:browser": "vitest run --project browser",
|
|
75
|
+
"test:coverage": "vitest run --coverage"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/compiler/codegen.js
CHANGED
|
@@ -595,6 +595,44 @@ function generateForBlock(
|
|
|
595
595
|
|
|
596
596
|
// Process inner bindings (these need to reference the item parameter)
|
|
597
597
|
const processedBindings = processBindings(bindings, code);
|
|
598
|
+
|
|
599
|
+
// === SELECTOR DETECTION ===
|
|
600
|
+
// Scan for the pattern: get(outerCell) === itemParam in attribute expressions.
|
|
601
|
+
// When found, replace N individual bind() subscriptions with one shared dispatch
|
|
602
|
+
// function + a keyed Map, reducing selection change cost from O(n) to O(1).
|
|
603
|
+
const selectorMap = new Map(); // cellCode -> { subsVar, prevVar }
|
|
604
|
+
let selectorCounter = 0;
|
|
605
|
+
for (const binding of processedBindings) {
|
|
606
|
+
if (binding.isStatic || !binding.cellArg || !binding.fullExpression) continue;
|
|
607
|
+
const cellCode = generateExpr(code, binding.cellArg);
|
|
608
|
+
if (selectorMap.has(cellCode)) continue;
|
|
609
|
+
if (detectSelectorPattern(binding, itemParam)) {
|
|
610
|
+
selectorCounter++;
|
|
611
|
+
selectorMap.set(cellCode, {
|
|
612
|
+
subsVar: `_sel_${selectorCounter}_subs`,
|
|
613
|
+
prevVar: `_sel_${selectorCounter}_prev`,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Generate selector setup lines (emitted BEFORE the forBlock call)
|
|
619
|
+
const selectorSetupLines = [];
|
|
620
|
+
for (const [cellCode, sel] of selectorMap) {
|
|
621
|
+
selectorSetupLines.push(` const ${sel.subsVar} = new Map();`);
|
|
622
|
+
selectorSetupLines.push(` let ${sel.prevVar} = get(${cellCode});`);
|
|
623
|
+
selectorSetupLines.push(` bind(${cellCode}, (next) => {`);
|
|
624
|
+
selectorSetupLines.push(` if (${sel.prevVar} === next) return;`);
|
|
625
|
+
selectorSetupLines.push(` ${sel.subsVar}.get(${sel.prevVar})?.(false);`);
|
|
626
|
+
selectorSetupLines.push(` ${sel.subsVar}.get(next)?.(true);`);
|
|
627
|
+
selectorSetupLines.push(` ${sel.prevVar} = next;`);
|
|
628
|
+
selectorSetupLines.push(` });`);
|
|
629
|
+
selectorSetupLines.push("");
|
|
630
|
+
usedImports.add("bind");
|
|
631
|
+
usedImports.add("get");
|
|
632
|
+
}
|
|
633
|
+
const selectorSetupCode =
|
|
634
|
+
selectorSetupLines.length > 0 ? selectorSetupLines.join("\n") + "\n" : "";
|
|
635
|
+
|
|
598
636
|
for (const b of processedBindings) {
|
|
599
637
|
if (!b.isStatic) {
|
|
600
638
|
usedImports.add("bind");
|
|
@@ -703,9 +741,38 @@ function generateForBlock(
|
|
|
703
741
|
|
|
704
742
|
// Track cleanup functions needed from bind() calls
|
|
705
743
|
const cleanupVars = [];
|
|
744
|
+
// Track inline cleanup expressions from selector subscriptions
|
|
745
|
+
const selectorCleanups = [];
|
|
706
746
|
|
|
707
747
|
// Bindings inside for block
|
|
708
748
|
for (const binding of processedBindings) {
|
|
749
|
+
// Check for selector pattern before falling back to standard binding generation
|
|
750
|
+
if (!binding.isStatic && binding.cellArg && binding.fullExpression) {
|
|
751
|
+
const cellCode = generateExpr(code, binding.cellArg);
|
|
752
|
+
const sel = selectorMap.get(cellCode);
|
|
753
|
+
if (sel) {
|
|
754
|
+
const match = detectSelectorPattern(binding, itemParam);
|
|
755
|
+
if (match) {
|
|
756
|
+
const { targetVar, targetProperty } = binding;
|
|
757
|
+
// Initial value: evaluates with get(cell), Phase 4 inlines to cell.v
|
|
758
|
+
const initialExprCode = generateExpr(code, binding.fullExpression);
|
|
759
|
+
// Callback: replace the entire `get(cell) === itemParam` comparison with `active`
|
|
760
|
+
const callbackExprCode = generateExprWithReplacement(
|
|
761
|
+
code,
|
|
762
|
+
binding.fullExpression,
|
|
763
|
+
match.comparisonNode,
|
|
764
|
+
"active",
|
|
765
|
+
);
|
|
766
|
+
lines.push(` ${targetVar}.${targetProperty} = ${initialExprCode};`);
|
|
767
|
+
lines.push(` ${sel.subsVar}.set(${itemParam}, (active) => {`);
|
|
768
|
+
lines.push(` ${targetVar}.${targetProperty} = ${callbackExprCode};`);
|
|
769
|
+
lines.push(` });`);
|
|
770
|
+
selectorCleanups.push(`${sel.subsVar}.delete(${itemParam})`);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
709
776
|
const { bindCode, cleanupVar } = generateBindingWithCleanup(
|
|
710
777
|
code,
|
|
711
778
|
binding,
|
|
@@ -725,17 +792,23 @@ function generateForBlock(
|
|
|
725
792
|
lines.push("");
|
|
726
793
|
lines.push(` anchor.before(${firstElementVar});`);
|
|
727
794
|
|
|
728
|
-
|
|
729
|
-
|
|
795
|
+
const allCleanupCalls = [
|
|
796
|
+
...cleanupVars.map((v) => `${v}()`),
|
|
797
|
+
...selectorCleanups,
|
|
798
|
+
]
|
|
799
|
+
.filter(Boolean)
|
|
800
|
+
.join("; ");
|
|
801
|
+
|
|
802
|
+
if (allCleanupCalls) {
|
|
730
803
|
lines.push(
|
|
731
|
-
` return { start: ${firstElementVar}, end: ${firstElementVar}, cleanup: () => { ${
|
|
804
|
+
` return { start: ${firstElementVar}, end: ${firstElementVar}, cleanup: () => { ${allCleanupCalls}; } };`,
|
|
732
805
|
);
|
|
733
806
|
} else {
|
|
734
807
|
lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
|
|
735
808
|
}
|
|
736
809
|
lines.push(` });`);
|
|
737
810
|
|
|
738
|
-
return lines.join("\n");
|
|
811
|
+
return selectorSetupCode + lines.join("\n");
|
|
739
812
|
}
|
|
740
813
|
|
|
741
814
|
/**
|
|
@@ -1301,6 +1374,67 @@ function generatePropBinding(code, binding, usedImports) {
|
|
|
1301
1374
|
return `setProp(${targetVar}, "${propName}", ${exprCode});`;
|
|
1302
1375
|
}
|
|
1303
1376
|
|
|
1377
|
+
/**
|
|
1378
|
+
* Detect the selector pattern in a binding: get(outerCell) === itemParam
|
|
1379
|
+
*
|
|
1380
|
+
* This pattern is common for list selection (e.g. class={get(selected) === row ? "danger" : ""}).
|
|
1381
|
+
* When detected, the compiler replaces N individual bind() subscriptions with a single shared
|
|
1382
|
+
* dispatch function (O(1) per selection change) backed by a keyed Map.
|
|
1383
|
+
*
|
|
1384
|
+
* @param {object} binding - ProcessedBinding from bind-detector
|
|
1385
|
+
* @param {string} itemParam - The loop item parameter name (e.g., "row")
|
|
1386
|
+
* @returns {{ comparisonNode: object } | null}
|
|
1387
|
+
*/
|
|
1388
|
+
function detectSelectorPattern(binding, itemParam) {
|
|
1389
|
+
const { fullExpression, getCallNode, needsTransform } = binding;
|
|
1390
|
+
// Must be a complex expression (needsTransform) so there's something beyond just get(cell)
|
|
1391
|
+
if (!fullExpression || !getCallNode || !needsTransform) return null;
|
|
1392
|
+
|
|
1393
|
+
function find(node) {
|
|
1394
|
+
if (!node || typeof node !== "object" || !node.type) return null;
|
|
1395
|
+
|
|
1396
|
+
if (node.type === "BinaryExpression" && node.operator === "===") {
|
|
1397
|
+
// get(cell) === itemParam
|
|
1398
|
+
if (
|
|
1399
|
+
node.left === getCallNode &&
|
|
1400
|
+
node.right.type === "Identifier" &&
|
|
1401
|
+
node.right.name === itemParam
|
|
1402
|
+
) {
|
|
1403
|
+
return { comparisonNode: node };
|
|
1404
|
+
}
|
|
1405
|
+
// itemParam === get(cell)
|
|
1406
|
+
if (
|
|
1407
|
+
node.right === getCallNode &&
|
|
1408
|
+
node.left.type === "Identifier" &&
|
|
1409
|
+
node.left.name === itemParam
|
|
1410
|
+
) {
|
|
1411
|
+
return { comparisonNode: node };
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Recurse into child nodes
|
|
1416
|
+
for (const key of Object.keys(node)) {
|
|
1417
|
+
if (key === "start" || key === "end" || key === "type" || key === "loc" || key === "extra")
|
|
1418
|
+
continue;
|
|
1419
|
+
const child = node[key];
|
|
1420
|
+
if (child && typeof child === "object") {
|
|
1421
|
+
if (Array.isArray(child)) {
|
|
1422
|
+
for (const c of child) {
|
|
1423
|
+
const result = find(c);
|
|
1424
|
+
if (result) return result;
|
|
1425
|
+
}
|
|
1426
|
+
} else if (child.type) {
|
|
1427
|
+
const result = find(child);
|
|
1428
|
+
if (result) return result;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return find(fullExpression);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1304
1438
|
/**
|
|
1305
1439
|
* Generate expression code from AST node using original source
|
|
1306
1440
|
*/
|