roqa 0.0.4 → 0.0.6
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 +22 -0
- package/package.json +76 -76
- package/src/compiler/codegen.js +138 -4
- package/src/compiler/transforms/inline-get.js +96 -3
- package/src/runtime/for-block.js +31 -20
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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.6] - 2026-03-31
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added compile-time selector optimization for list selection patterns
|
|
13
|
+
- The compiler now detects `get(cell) === loopItem` inside `<For>` attribute expressions and generates an O(1) selector instead of N per-row `bind()` subscriptions
|
|
14
|
+
- A single shared `Map` (keyed by row reference) and one `bind()` on the outer cell replaces N per-row `bind()` calls
|
|
15
|
+
- On selection change only 2 Map callbacks fire (deselect old, select new) regardless of list size
|
|
16
|
+
- Row cleanup removes its Map entry to prevent memory leaks
|
|
17
|
+
|
|
18
|
+
## [0.0.5] - 2026-03-31
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fixed a compiler bug where cleanup-captured `bind()` calls inside `<For>` and `<Show>` blocks could be fully inlined to ref assignments while leaving dangling `_cleanup_N()` calls in generated cleanup functions
|
|
23
|
+
- The inliner now tracks cleanup variable names for removable `bind()` subscriptions and removes stale cleanup calls when their underlying subscription has been optimized away
|
|
24
|
+
- Cleanup properties are omitted entirely when all generated cleanup calls were eliminated during inlining
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Restored the js benchmark example to the faster per-row selection strategy so compiled output uses direct row refs instead of per-row subscriptions to a shared selection cell
|
|
29
|
+
|
|
8
30
|
## [0.0.4] - 2026-01-10
|
|
9
31
|
|
|
10
32
|
### 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.6",
|
|
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
|
+
}
|
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
|
*/
|
|
@@ -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, hasClosureVars }]
|
|
461
|
+
// Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd, hasClosureVars, cleanupVarName }]
|
|
462
462
|
const bindCallbacks = new Map();
|
|
463
463
|
// Track ref numbers per cell
|
|
464
464
|
const refCounters = new Map();
|
|
@@ -469,7 +469,7 @@ function findBindCallbacks(ast, code) {
|
|
|
469
469
|
* @param {number} stmtStart - Start position of containing statement
|
|
470
470
|
* @param {number} stmtEnd - End position of containing statement
|
|
471
471
|
*/
|
|
472
|
-
function processBindCall(bindExpr, stmtStart, stmtEnd) {
|
|
472
|
+
function processBindCall(bindExpr, stmtStart, stmtEnd, cleanupVarName = null) {
|
|
473
473
|
const cellArg = bindExpr.arguments[0];
|
|
474
474
|
const callbackArg = bindExpr.arguments[1];
|
|
475
475
|
if (!cellArg || !callbackArg) return;
|
|
@@ -519,6 +519,7 @@ function findBindCallbacks(ast, code) {
|
|
|
519
519
|
statementStart: stmtStart,
|
|
520
520
|
statementEnd: stmtEnd,
|
|
521
521
|
hasClosureVars,
|
|
522
|
+
cleanupVarName,
|
|
522
523
|
});
|
|
523
524
|
}
|
|
524
525
|
|
|
@@ -533,7 +534,12 @@ function findBindCallbacks(ast, code) {
|
|
|
533
534
|
VariableDeclaration(path) {
|
|
534
535
|
for (const decl of path.node.declarations) {
|
|
535
536
|
if (decl.init && isBindCall(decl.init)) {
|
|
536
|
-
processBindCall(
|
|
537
|
+
processBindCall(
|
|
538
|
+
decl.init,
|
|
539
|
+
path.node.start,
|
|
540
|
+
path.node.end,
|
|
541
|
+
decl.id.type === "Identifier" ? decl.id.name : null,
|
|
542
|
+
);
|
|
537
543
|
}
|
|
538
544
|
}
|
|
539
545
|
},
|
|
@@ -543,6 +549,35 @@ function findBindCallbacks(ast, code) {
|
|
|
543
549
|
return bindCallbacks;
|
|
544
550
|
}
|
|
545
551
|
|
|
552
|
+
function isCleanupCallStatement(node, cleanupVarNames) {
|
|
553
|
+
return (
|
|
554
|
+
node?.type === "ExpressionStatement" &&
|
|
555
|
+
node.expression?.type === "CallExpression" &&
|
|
556
|
+
node.expression.callee?.type === "Identifier" &&
|
|
557
|
+
cleanupVarNames.has(node.expression.callee.name) &&
|
|
558
|
+
node.expression.arguments.length === 0
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function removeObjectProperty(s, code, propertyNode) {
|
|
563
|
+
let start = propertyNode.start;
|
|
564
|
+
let end = propertyNode.end;
|
|
565
|
+
|
|
566
|
+
let left = start - 1;
|
|
567
|
+
while (left >= 0 && /\s/.test(code[left])) left--;
|
|
568
|
+
if (left >= 0 && code[left] === ",") {
|
|
569
|
+
start = left;
|
|
570
|
+
} else {
|
|
571
|
+
let right = end;
|
|
572
|
+
while (right < code.length && /\s/.test(code[right])) right++;
|
|
573
|
+
if (right < code.length && code[right] === ",") {
|
|
574
|
+
end = right + 1;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
s.remove(start, end);
|
|
579
|
+
}
|
|
580
|
+
|
|
546
581
|
/**
|
|
547
582
|
* Find element variables used in a callback body
|
|
548
583
|
* Looks for element.property = ... patterns
|
|
@@ -740,6 +775,7 @@ export function inlineGetCalls(code, filename) {
|
|
|
740
775
|
|
|
741
776
|
// Track bind statements to remove
|
|
742
777
|
const bindStatementsToRemove = [];
|
|
778
|
+
const inlinedCleanupVars = new Set();
|
|
743
779
|
|
|
744
780
|
// Track roqa imports for removal
|
|
745
781
|
const importsToRemove = [];
|
|
@@ -1108,6 +1144,9 @@ export function inlineGetCalls(code, filename) {
|
|
|
1108
1144
|
|
|
1109
1145
|
// Replace bind() call with ref assignment(s)
|
|
1110
1146
|
if (refAssignment) {
|
|
1147
|
+
if (callback.cleanupVarName) {
|
|
1148
|
+
inlinedCleanupVars.add(callback.cleanupVarName);
|
|
1149
|
+
}
|
|
1111
1150
|
s.overwrite(start, end, refAssignment);
|
|
1112
1151
|
} else {
|
|
1113
1152
|
// No element vars found - bind() can't be fully inlined
|
|
@@ -1117,6 +1156,60 @@ export function inlineGetCalls(code, filename) {
|
|
|
1117
1156
|
}
|
|
1118
1157
|
}
|
|
1119
1158
|
|
|
1159
|
+
// Remove cleanup calls for bind() subscriptions that were fully inlined away.
|
|
1160
|
+
// If a cleanup block only contains now-removed cleanup calls, drop the cleanup
|
|
1161
|
+
// property entirely so runtime list items don't carry no-op cleanup functions.
|
|
1162
|
+
if (inlinedCleanupVars.size > 0) {
|
|
1163
|
+
const cleanupStatementsToRemove = [];
|
|
1164
|
+
const cleanupPropertiesToRemove = [];
|
|
1165
|
+
|
|
1166
|
+
traverse(ast, {
|
|
1167
|
+
ObjectProperty(path) {
|
|
1168
|
+
const node = path.node;
|
|
1169
|
+
const key = node.key;
|
|
1170
|
+
const isCleanupProperty =
|
|
1171
|
+
(key?.type === "Identifier" && key.name === "cleanup") ||
|
|
1172
|
+
(key?.type === "StringLiteral" && key.value === "cleanup");
|
|
1173
|
+
if (!isCleanupProperty) return;
|
|
1174
|
+
|
|
1175
|
+
const fn = node.value;
|
|
1176
|
+
if (
|
|
1177
|
+
!fn ||
|
|
1178
|
+
(fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") ||
|
|
1179
|
+
fn.body?.type !== "BlockStatement"
|
|
1180
|
+
) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const statements = fn.body.body;
|
|
1185
|
+
if (statements.length === 0) return;
|
|
1186
|
+
|
|
1187
|
+
const removableStatements = statements.filter((stmt) =>
|
|
1188
|
+
isCleanupCallStatement(stmt, inlinedCleanupVars),
|
|
1189
|
+
);
|
|
1190
|
+
if (removableStatements.length === 0) return;
|
|
1191
|
+
|
|
1192
|
+
if (removableStatements.length === statements.length) {
|
|
1193
|
+
cleanupPropertiesToRemove.push(node);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
cleanupStatementsToRemove.push(...removableStatements);
|
|
1198
|
+
},
|
|
1199
|
+
noScope: true,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
cleanupStatementsToRemove.sort((a, b) => b.start - a.start);
|
|
1203
|
+
for (const stmt of cleanupStatementsToRemove) {
|
|
1204
|
+
s.remove(stmt.start, stmt.end);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
cleanupPropertiesToRemove.sort((a, b) => b.start - a.start);
|
|
1208
|
+
for (const property of cleanupPropertiesToRemove) {
|
|
1209
|
+
removeObjectProperty(s, code, property);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1120
1213
|
// Remove imports that are no longer needed
|
|
1121
1214
|
// Collect all imports to remove first
|
|
1122
1215
|
// Only remove bind import if ALL bind calls were fully inlined (had element vars and no closure vars)
|
package/src/runtime/for-block.js
CHANGED
|
@@ -66,10 +66,13 @@ function lisAlgorithm(arr) {
|
|
|
66
66
|
* @param {*} value - The data item
|
|
67
67
|
* @param {number} index - Array index
|
|
68
68
|
* @param {Function} renderFn - (anchor, value, index) => { start, end } or just appends nodes
|
|
69
|
+
* @param {Object} forState - The for loop state (to track cleanup count)
|
|
69
70
|
*/
|
|
70
|
-
function createItem(anchor, value, index, renderFn) {
|
|
71
|
+
function createItem(anchor, value, index, renderFn, forState) {
|
|
71
72
|
// renderFn should return { start, end } nodes for the item
|
|
72
73
|
const item = renderFn(anchor, value, index);
|
|
74
|
+
// Track if this item has cleanup for fast-path optimization
|
|
75
|
+
if (item.cleanup) forState.cleanupCount++;
|
|
73
76
|
return {
|
|
74
77
|
s: item, // state: { start, end } - the DOM range for this item
|
|
75
78
|
v: value,
|
|
@@ -101,14 +104,18 @@ function moveItem(item, anchor) {
|
|
|
101
104
|
|
|
102
105
|
/**
|
|
103
106
|
* Destroy an item's DOM nodes and run cleanup if present
|
|
107
|
+
* @param {Object} forState - The for loop state (to track cleanup count)
|
|
104
108
|
*/
|
|
105
|
-
function destroyItem(item) {
|
|
109
|
+
function destroyItem(item, forState) {
|
|
106
110
|
const state = item.s;
|
|
107
111
|
let node = state.start;
|
|
108
112
|
const end = state.end;
|
|
109
113
|
|
|
110
114
|
// Run cleanup function if the render provided one
|
|
111
|
-
if (state.cleanup)
|
|
115
|
+
if (state.cleanup) {
|
|
116
|
+
state.cleanup();
|
|
117
|
+
forState.cleanupCount--;
|
|
118
|
+
}
|
|
112
119
|
|
|
113
120
|
while (node !== null) {
|
|
114
121
|
const next = node.nextSibling;
|
|
@@ -122,11 +129,14 @@ function destroyItem(item) {
|
|
|
122
129
|
* Fast path: clear all items when going from non-empty to empty
|
|
123
130
|
*/
|
|
124
131
|
function reconcileFastClear(anchor, forState, array) {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
// Only run cleanup loop if there are items with cleanup functions
|
|
133
|
+
if (forState.cleanupCount > 0) {
|
|
134
|
+
const items = forState.items;
|
|
135
|
+
for (let i = 0; i < items.length; i++) {
|
|
136
|
+
const state = items[i].s;
|
|
137
|
+
if (state.cleanup) state.cleanup();
|
|
138
|
+
}
|
|
139
|
+
forState.cleanupCount = 0;
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
const parent_node = anchor.parentNode;
|
|
@@ -161,7 +171,7 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
161
171
|
// Empty -> non-empty: create all
|
|
162
172
|
if (aLen === 0) {
|
|
163
173
|
for (; j < bLen; j++) {
|
|
164
|
-
bItems[j] = createItem(anchor, b[j], j, renderFn);
|
|
174
|
+
bItems[j] = createItem(anchor, b[j], j, renderFn, forState);
|
|
165
175
|
}
|
|
166
176
|
forState.array = b;
|
|
167
177
|
forState.items = bItems;
|
|
@@ -205,14 +215,14 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
205
215
|
while (j <= bEnd) {
|
|
206
216
|
bVal = b[j];
|
|
207
217
|
target = j >= aLen ? anchor : aItems[j].s.start;
|
|
208
|
-
bItems[j] = createItem(target, bVal, j, renderFn);
|
|
218
|
+
bItems[j] = createItem(target, bVal, j, renderFn, forState);
|
|
209
219
|
j++;
|
|
210
220
|
}
|
|
211
221
|
}
|
|
212
222
|
} else if (j > bEnd) {
|
|
213
223
|
// Only removals
|
|
214
224
|
while (j <= aEnd) {
|
|
215
|
-
destroyItem(aItems[j++]);
|
|
225
|
+
destroyItem(aItems[j++], forState);
|
|
216
226
|
}
|
|
217
227
|
} else {
|
|
218
228
|
// General case: need full reconciliation
|
|
@@ -237,7 +247,7 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
237
247
|
sources[j - bStart] = i + 1;
|
|
238
248
|
if (fastPathRemoval) {
|
|
239
249
|
fastPathRemoval = false;
|
|
240
|
-
while (aStart < i) destroyItem(aItems[aStart++]);
|
|
250
|
+
while (aStart < i) destroyItem(aItems[aStart++], forState);
|
|
241
251
|
}
|
|
242
252
|
if (pos > j) moved = true;
|
|
243
253
|
else pos = j;
|
|
@@ -246,9 +256,9 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
246
256
|
break;
|
|
247
257
|
}
|
|
248
258
|
}
|
|
249
|
-
if (!fastPathRemoval && j > bEnd) destroyItem(aItems[i]);
|
|
259
|
+
if (!fastPathRemoval && j > bEnd) destroyItem(aItems[i], forState);
|
|
250
260
|
} else if (!fastPathRemoval) {
|
|
251
|
-
destroyItem(aItems[i]);
|
|
261
|
+
destroyItem(aItems[i], forState);
|
|
252
262
|
}
|
|
253
263
|
}
|
|
254
264
|
} else {
|
|
@@ -262,7 +272,7 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
262
272
|
if (j !== undefined) {
|
|
263
273
|
if (fastPathRemoval) {
|
|
264
274
|
fastPathRemoval = false;
|
|
265
|
-
while (i > aStart) destroyItem(aItems[aStart++]);
|
|
275
|
+
while (i > aStart) destroyItem(aItems[aStart++], forState);
|
|
266
276
|
}
|
|
267
277
|
sources[j - bStart] = i + 1;
|
|
268
278
|
if (pos > j) moved = true;
|
|
@@ -270,10 +280,10 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
270
280
|
bItems[j] = aItems[i];
|
|
271
281
|
++patched;
|
|
272
282
|
} else if (!fastPathRemoval) {
|
|
273
|
-
destroyItem(aItems[i]);
|
|
283
|
+
destroyItem(aItems[i], forState);
|
|
274
284
|
}
|
|
275
285
|
} else if (!fastPathRemoval) {
|
|
276
|
-
destroyItem(aItems[i]);
|
|
286
|
+
destroyItem(aItems[i], forState);
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
}
|
|
@@ -295,7 +305,7 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
295
305
|
|
|
296
306
|
if (sources[i] === 0) {
|
|
297
307
|
bVal = b[pos];
|
|
298
|
-
bItems[pos] = createItem(target, bVal, pos, renderFn);
|
|
308
|
+
bItems[pos] = createItem(target, bVal, pos, renderFn, forState);
|
|
299
309
|
} else if (j < 0 || i !== seq[j]) {
|
|
300
310
|
moveItem(bItems[pos], target);
|
|
301
311
|
} else {
|
|
@@ -309,7 +319,7 @@ function reconcileByRef(anchor, forState, b, renderFn) {
|
|
|
309
319
|
bVal = b[pos];
|
|
310
320
|
const nextPos = pos + 1;
|
|
311
321
|
target = nextPos < bLen ? bItems[nextPos].s.start : anchor;
|
|
312
|
-
bItems[pos] = createItem(target, bVal, pos, renderFn);
|
|
322
|
+
bItems[pos] = createItem(target, bVal, pos, renderFn, forState);
|
|
313
323
|
}
|
|
314
324
|
}
|
|
315
325
|
}
|
|
@@ -339,6 +349,7 @@ export function forBlock(container, sourceCell, renderFn) {
|
|
|
339
349
|
const forState = {
|
|
340
350
|
array: [],
|
|
341
351
|
items: [],
|
|
352
|
+
cleanupCount: 0, // Track items with cleanup for fast-path optimization
|
|
342
353
|
};
|
|
343
354
|
|
|
344
355
|
const doUpdate = () => {
|
|
@@ -363,7 +374,7 @@ export function forBlock(container, sourceCell, renderFn) {
|
|
|
363
374
|
// Destroy all current items
|
|
364
375
|
const items = forState.items;
|
|
365
376
|
for (let i = 0; i < items.length; i++) {
|
|
366
|
-
destroyItem(items[i]);
|
|
377
|
+
destroyItem(items[i], forState);
|
|
367
378
|
}
|
|
368
379
|
forState.array = [];
|
|
369
380
|
forState.items = [];
|