roqa 0.0.2 → 0.0.3
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 +9 -0
- package/package.json +1 -1
- package/src/compiler/codegen.js +188 -21
- package/src/runtime/for-block.js +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.3] - 2026-01-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed critical memory leak when using `<For>` with reactive bindings to cells outside the loop (e.g., `class={get(selected) === row.id ? "danger" : ""}`)
|
|
13
|
+
- Subscriptions to external cells are now properly cleaned up when items are removed or the array is cleared
|
|
14
|
+
- `reconcileFastClear` now calls cleanup functions before clearing DOM
|
|
15
|
+
- The compiler now generates cleanup functions that capture `bind()` unsubscribe calls inside `<For>` and `<Show>` blocks
|
|
16
|
+
|
|
8
17
|
## [0.0.2] - 2026-01-10
|
|
9
18
|
|
|
10
19
|
### Fixed
|
package/package.json
CHANGED
package/src/compiler/codegen.js
CHANGED
|
@@ -701,16 +701,38 @@ function generateForBlock(
|
|
|
701
701
|
// Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
|
|
702
702
|
const cellRefCounts = new Map();
|
|
703
703
|
|
|
704
|
+
// Track cleanup functions needed from bind() calls
|
|
705
|
+
const cleanupVars = [];
|
|
706
|
+
|
|
704
707
|
// Bindings inside for block
|
|
705
708
|
for (const binding of processedBindings) {
|
|
706
|
-
const bindCode
|
|
709
|
+
const { bindCode, cleanupVar } = generateBindingWithCleanup(
|
|
710
|
+
code,
|
|
711
|
+
binding,
|
|
712
|
+
usedImports,
|
|
713
|
+
itemParam,
|
|
714
|
+
true,
|
|
715
|
+
cellRefCounts,
|
|
716
|
+
cleanupVars.length,
|
|
717
|
+
);
|
|
707
718
|
lines.push(` ${bindCode}`);
|
|
719
|
+
if (cleanupVar) {
|
|
720
|
+
cleanupVars.push(cleanupVar);
|
|
721
|
+
}
|
|
708
722
|
}
|
|
709
723
|
|
|
710
|
-
// Insert before anchor and return range
|
|
724
|
+
// Insert before anchor and return range (with cleanup if needed)
|
|
711
725
|
lines.push("");
|
|
712
726
|
lines.push(` anchor.before(${firstElementVar});`);
|
|
713
|
-
|
|
727
|
+
|
|
728
|
+
if (cleanupVars.length > 0) {
|
|
729
|
+
const cleanupCalls = cleanupVars.map((v) => `${v}()`).join("; ");
|
|
730
|
+
lines.push(
|
|
731
|
+
` return { start: ${firstElementVar}, end: ${firstElementVar}, cleanup: () => { ${cleanupCalls}; } };`,
|
|
732
|
+
);
|
|
733
|
+
} else {
|
|
734
|
+
lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
|
|
735
|
+
}
|
|
714
736
|
lines.push(` });`);
|
|
715
737
|
|
|
716
738
|
return lines.join("\n");
|
|
@@ -861,16 +883,38 @@ function generateShowBlock(code, showBlock, nameGen, templateRegistry, usedImpor
|
|
|
861
883
|
// Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
|
|
862
884
|
const cellRefCounts = new Map();
|
|
863
885
|
|
|
886
|
+
// Track cleanup functions needed from bind() calls
|
|
887
|
+
const cleanupVars = [];
|
|
888
|
+
|
|
864
889
|
// Bindings inside show block
|
|
865
890
|
for (const binding of processedBindings) {
|
|
866
|
-
const bindCode
|
|
891
|
+
const { bindCode, cleanupVar } = generateBindingWithCleanup(
|
|
892
|
+
code,
|
|
893
|
+
binding,
|
|
894
|
+
usedImports,
|
|
895
|
+
null,
|
|
896
|
+
true,
|
|
897
|
+
cellRefCounts,
|
|
898
|
+
cleanupVars.length,
|
|
899
|
+
);
|
|
867
900
|
lines.push(` ${bindCode}`);
|
|
901
|
+
if (cleanupVar) {
|
|
902
|
+
cleanupVars.push(cleanupVar);
|
|
903
|
+
}
|
|
868
904
|
}
|
|
869
905
|
|
|
870
|
-
// Insert before anchor and return range
|
|
906
|
+
// Insert before anchor and return range (with cleanup if needed)
|
|
871
907
|
lines.push("");
|
|
872
908
|
lines.push(` anchor.before(${firstElementVar});`);
|
|
873
|
-
|
|
909
|
+
|
|
910
|
+
if (cleanupVars.length > 0) {
|
|
911
|
+
const cleanupCalls = cleanupVars.map((v) => `${v}()`).join("; ");
|
|
912
|
+
lines.push(
|
|
913
|
+
` return { start: ${firstElementVar}, end: ${firstElementVar}, cleanup: () => { ${cleanupCalls}; } };`,
|
|
914
|
+
);
|
|
915
|
+
} else {
|
|
916
|
+
lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
|
|
917
|
+
}
|
|
874
918
|
lines.push(` }${depsCode});`);
|
|
875
919
|
|
|
876
920
|
return lines.join("\n");
|
|
@@ -919,10 +963,83 @@ function generateBinding(
|
|
|
919
963
|
itemParam = null,
|
|
920
964
|
insideForBlock = false,
|
|
921
965
|
cellRefCounts = null,
|
|
966
|
+
) {
|
|
967
|
+
// At component level (not inside forBlock), we don't need cleanup tracking
|
|
968
|
+
// because cleanup is handled by the component's disconnectedCallback
|
|
969
|
+
const result = generateBindingCore(
|
|
970
|
+
code,
|
|
971
|
+
binding,
|
|
972
|
+
usedImports,
|
|
973
|
+
itemParam,
|
|
974
|
+
insideForBlock,
|
|
975
|
+
cellRefCounts,
|
|
976
|
+
false, // Don't generate cleanup variables
|
|
977
|
+
0,
|
|
978
|
+
);
|
|
979
|
+
return result.bindCode;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Generate code for a binding, returning both the code and any cleanup variable
|
|
984
|
+
* Used inside forBlock/showBlock where cleanup is needed on item destruction
|
|
985
|
+
* @param {string} code - Original source code
|
|
986
|
+
* @param {object} binding - Binding info
|
|
987
|
+
* @param {Set} usedImports - Set of imports to track
|
|
988
|
+
* @param {string} itemParam - Item parameter name (for forBlock context)
|
|
989
|
+
* @param {boolean} insideForBlock - Whether we're inside a forBlock callback
|
|
990
|
+
* @param {Map} cellRefCounts - Map to track ref counts per cell (for numbered refs)
|
|
991
|
+
* @param {number} cleanupIndex - Index for naming cleanup variables
|
|
992
|
+
* @returns {{ bindCode: string, cleanupVar: string|null }}
|
|
993
|
+
*/
|
|
994
|
+
function generateBindingWithCleanup(
|
|
995
|
+
code,
|
|
996
|
+
binding,
|
|
997
|
+
usedImports,
|
|
998
|
+
itemParam = null,
|
|
999
|
+
insideForBlock = false,
|
|
1000
|
+
cellRefCounts = null,
|
|
1001
|
+
cleanupIndex = 0,
|
|
1002
|
+
) {
|
|
1003
|
+
return generateBindingCore(
|
|
1004
|
+
code,
|
|
1005
|
+
binding,
|
|
1006
|
+
usedImports,
|
|
1007
|
+
itemParam,
|
|
1008
|
+
insideForBlock,
|
|
1009
|
+
cellRefCounts,
|
|
1010
|
+
true, // Generate cleanup variables
|
|
1011
|
+
cleanupIndex,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Core binding generation logic
|
|
1017
|
+
* @param {string} code - Original source code
|
|
1018
|
+
* @param {object} binding - Binding info
|
|
1019
|
+
* @param {Set} usedImports - Set of imports to track
|
|
1020
|
+
* @param {string} itemParam - Item parameter name (for forBlock context)
|
|
1021
|
+
* @param {boolean} insideForBlock - Whether we're inside a forBlock callback
|
|
1022
|
+
* @param {Map} cellRefCounts - Map to track ref counts per cell (for numbered refs)
|
|
1023
|
+
* @param {boolean} generateCleanup - Whether to generate cleanup variables for bind() calls
|
|
1024
|
+
* @param {number} cleanupIndex - Index for naming cleanup variables
|
|
1025
|
+
* @returns {{ bindCode: string, cleanupVar: string|null }}
|
|
1026
|
+
*/
|
|
1027
|
+
function generateBindingCore(
|
|
1028
|
+
code,
|
|
1029
|
+
binding,
|
|
1030
|
+
usedImports,
|
|
1031
|
+
itemParam = null,
|
|
1032
|
+
insideForBlock = false,
|
|
1033
|
+
cellRefCounts = null,
|
|
1034
|
+
generateCleanup = false,
|
|
1035
|
+
cleanupIndex = 0,
|
|
922
1036
|
) {
|
|
923
1037
|
// Handle prop bindings (for custom elements)
|
|
924
1038
|
if (binding.type === "prop") {
|
|
925
|
-
return
|
|
1039
|
+
return {
|
|
1040
|
+
bindCode: generatePropBinding(code, binding, usedImports, insideForBlock),
|
|
1041
|
+
cleanupVar: null,
|
|
1042
|
+
};
|
|
926
1043
|
}
|
|
927
1044
|
|
|
928
1045
|
const {
|
|
@@ -940,7 +1057,16 @@ function generateBinding(
|
|
|
940
1057
|
|
|
941
1058
|
// Handle new contentParts format (concatenated text content)
|
|
942
1059
|
if (contentParts) {
|
|
943
|
-
return
|
|
1060
|
+
return {
|
|
1061
|
+
bindCode: generateContentPartsBinding(
|
|
1062
|
+
code,
|
|
1063
|
+
binding,
|
|
1064
|
+
usedImports,
|
|
1065
|
+
insideForBlock,
|
|
1066
|
+
cellRefCounts,
|
|
1067
|
+
),
|
|
1068
|
+
cleanupVar: null,
|
|
1069
|
+
};
|
|
944
1070
|
}
|
|
945
1071
|
|
|
946
1072
|
// Build prefix string if we have static text before the dynamic expression
|
|
@@ -957,9 +1083,15 @@ function generateBinding(
|
|
|
957
1083
|
// Static assignment
|
|
958
1084
|
const exprCode = generateExpr(code, fullExpression);
|
|
959
1085
|
if (needsSetAttribute) {
|
|
960
|
-
return
|
|
1086
|
+
return {
|
|
1087
|
+
bindCode: `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${exprCode});`,
|
|
1088
|
+
cleanupVar: null,
|
|
1089
|
+
};
|
|
961
1090
|
}
|
|
962
|
-
return
|
|
1091
|
+
return {
|
|
1092
|
+
bindCode: `${targetVar}.${targetProperty} = ${prefixCode}${exprCode};`,
|
|
1093
|
+
cleanupVar: null,
|
|
1094
|
+
};
|
|
963
1095
|
}
|
|
964
1096
|
|
|
965
1097
|
// Generate the cell argument code
|
|
@@ -982,8 +1114,11 @@ function generateBinding(
|
|
|
982
1114
|
|
|
983
1115
|
// Emit: initial value assignment + ref storage on cell
|
|
984
1116
|
// cell.ref_N = element;
|
|
985
|
-
return
|
|
986
|
-
|
|
1117
|
+
return {
|
|
1118
|
+
bindCode: `${targetVar}.${targetProperty} = ${initialExprCode};
|
|
1119
|
+
${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`,
|
|
1120
|
+
cleanupVar: null,
|
|
1121
|
+
};
|
|
987
1122
|
}
|
|
988
1123
|
|
|
989
1124
|
// Fall back to bind() for complex bindings (or SVG attributes)
|
|
@@ -1000,18 +1135,50 @@ ${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
|
|
|
1000
1135
|
bindExprCode = "v";
|
|
1001
1136
|
}
|
|
1002
1137
|
|
|
1003
|
-
//
|
|
1138
|
+
// Determine indentation based on context
|
|
1139
|
+
const indent = insideForBlock ? " " : " ";
|
|
1140
|
+
|
|
1141
|
+
// Generate with or without cleanup variable capture
|
|
1142
|
+
if (generateCleanup) {
|
|
1143
|
+
const cleanupVar = `_cleanup_${cleanupIndex}`;
|
|
1144
|
+
|
|
1145
|
+
if (needsSetAttribute) {
|
|
1146
|
+
return {
|
|
1147
|
+
bindCode: `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
|
|
1148
|
+
${indent}const ${cleanupVar} = bind(${cellCode}, (v) => {
|
|
1149
|
+
${indent} ${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
|
|
1150
|
+
${indent}});`,
|
|
1151
|
+
cleanupVar,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return {
|
|
1156
|
+
bindCode: `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
|
|
1157
|
+
${indent}const ${cleanupVar} = bind(${cellCode}, (v) => {
|
|
1158
|
+
${indent} ${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
|
|
1159
|
+
${indent}});`,
|
|
1160
|
+
cleanupVar,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// No cleanup needed - just generate the bind without capturing
|
|
1004
1165
|
if (needsSetAttribute) {
|
|
1005
|
-
return
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1166
|
+
return {
|
|
1167
|
+
bindCode: `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
|
|
1168
|
+
${indent}bind(${cellCode}, (v) => {
|
|
1169
|
+
${indent} ${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
|
|
1170
|
+
${indent}});`,
|
|
1171
|
+
cleanupVar: null,
|
|
1172
|
+
};
|
|
1009
1173
|
}
|
|
1010
1174
|
|
|
1011
|
-
return
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1175
|
+
return {
|
|
1176
|
+
bindCode: `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
|
|
1177
|
+
${indent}bind(${cellCode}, (v) => {
|
|
1178
|
+
${indent} ${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
|
|
1179
|
+
${indent}});`,
|
|
1180
|
+
cleanupVar: null,
|
|
1181
|
+
};
|
|
1015
1182
|
}
|
|
1016
1183
|
|
|
1017
1184
|
/**
|
package/src/runtime/for-block.js
CHANGED
|
@@ -122,6 +122,13 @@ function destroyItem(item) {
|
|
|
122
122
|
* Fast path: clear all items when going from non-empty to empty
|
|
123
123
|
*/
|
|
124
124
|
function reconcileFastClear(anchor, forState, array) {
|
|
125
|
+
// Run cleanup for all items before clearing DOM
|
|
126
|
+
const items = forState.items;
|
|
127
|
+
for (let i = 0; i < items.length; i++) {
|
|
128
|
+
const state = items[i].s;
|
|
129
|
+
if (state.cleanup) state.cleanup();
|
|
130
|
+
}
|
|
131
|
+
|
|
125
132
|
const parent_node = anchor.parentNode;
|
|
126
133
|
parent_node.textContent = "";
|
|
127
134
|
parent_node.append(anchor);
|