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 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
- "name": "roqa",
3
- "version": "0.0.5",
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
- "scripts": {
55
- "test": "vitest run",
56
- "test:watch": "vitest",
57
- "test:unit": "vitest run --project unit",
58
- "test:browser": "vitest run --project browser",
59
- "test:coverage": "vitest run --coverage"
60
- },
61
- "dependencies": {
62
- "@babel/generator": "catalog:default",
63
- "@babel/parser": "catalog:default",
64
- "@babel/traverse": "catalog:default",
65
- "@babel/types": "catalog:default",
66
- "magic-string": "catalog:default"
67
- },
68
- "devDependencies": {
69
- "@vitest/browser": "^3.0.0",
70
- "@vitest/coverage-v8": "^3.0.0",
71
- "playwright": "^1.49.0",
72
- "vitest": "^3.0.0"
73
- },
74
- "engines": {
75
- "node": ">=20.0.0"
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
+ }
@@ -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
- if (cleanupVars.length > 0) {
729
- const cleanupCalls = cleanupVars.map((v) => `${v}()`).join("; ");
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: () => { ${cleanupCalls}; } };`,
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
  */