@llui/vite-plugin 0.0.3 → 0.0.5

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/dist/transform.js CHANGED
@@ -139,6 +139,7 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
139
139
  let usesElTemplate = false;
140
140
  let usesElSplit = false;
141
141
  let usesMemo = false;
142
+ let usesApplyBinding = false;
142
143
  const f = ts.factory;
143
144
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
144
145
  // Collect source positions of transformed nodes for source mapping
@@ -164,10 +165,27 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
164
165
  current = deduped;
165
166
  changed = true;
166
167
  }
168
+ // Inject __mask for Phase 1 gating
169
+ const masked = tryInjectStructuralMask(current, viewHelperNames, viewHelperAliases, fieldBits, f);
170
+ if (masked) {
171
+ current = masked;
172
+ changed = true;
173
+ }
167
174
  if (changed) {
168
175
  const result = ts.visitEachChild(current, visitor, undefined);
169
176
  if (hasPos)
170
177
  edits.push({ start: origStart, end: origEnd, replacement: '' });
178
+ // Row factory: try after children are transformed
179
+ if (ts.isCallExpression(result)) {
180
+ try {
181
+ const rf = tryEmitRowFactory(result, f, source);
182
+ if (rf)
183
+ return rf;
184
+ }
185
+ catch (err) {
186
+ console.warn('[llui] Row factory failed:', err.message, '\n', err.stack?.split('\n').slice(0, 5).join('\n'));
187
+ }
188
+ }
171
189
  return result;
172
190
  }
173
191
  }
@@ -192,10 +210,19 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
192
210
  edits.push({ start: origStart, end: origEnd, replacement: '' });
193
211
  return textTransformed;
194
212
  }
213
+ // Inject __mask into each()/branch()/show() options for Phase 1 gating
214
+ const structuralMasked = tryInjectStructuralMask(node, viewHelperNames, viewHelperAliases, fieldBits, f);
215
+ if (structuralMasked) {
216
+ if (hasPos)
217
+ edits.push({ start: origStart, end: origEnd, replacement: '' });
218
+ return ts.visitEachChild(structuralMasked, visitor, undefined);
219
+ }
195
220
  }
196
- // Pass 2: Inject __dirty and __msgSchema into component() calls
221
+ // Pass 2: Inject __dirty, __update, and __msgSchema into component() calls
197
222
  if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
198
223
  let result = tryInjectDirty(node, fieldBits, f);
224
+ if (result)
225
+ usesApplyBinding = true;
199
226
  if (devMode) {
200
227
  const schema = extractMsgSchema(source);
201
228
  if (schema) {
@@ -223,7 +250,7 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
223
250
  // Pass 3: Clean up imports — use the old cleanupImports approach
224
251
  // which operates on the transformed SourceFile safely
225
252
  const safeToRemove = new Set([...compiledHelpers].filter((h) => !bailedHelpers.has(h)));
226
- transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, f);
253
+ transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, f);
227
254
  if (edits.length === 0)
228
255
  return null;
229
256
  // Find component declarations for HMR
@@ -752,6 +779,61 @@ function tryInjectTextMask(node, lluiImport, viewHelperNames, viewHelperAliases,
752
779
  createMaskLiteral(f, mask === 0 ? 0xffffffff | 0 : mask),
753
780
  ]);
754
781
  }
782
+ /**
783
+ * Inject `__mask` into the options object of each()/branch()/show() calls.
784
+ *
785
+ * Analyzes the driving accessor (`items` for each, `on` for branch, `when`
786
+ * for show) and computes the bitmask of state fields it reads. The runtime
787
+ * uses this to skip Phase 1 reconciliation when irrelevant state changed
788
+ * (e.g., each() that reads `rows` is skipped when only `selected` changed).
789
+ */
790
+ function tryInjectStructuralMask(node, viewHelperNames, viewHelperAliases, fieldBits, f) {
791
+ if (fieldBits.size === 0)
792
+ return null;
793
+ // Match each(), branch(), show() — bare, aliased, or member-call
794
+ const isEach = isHelperCall(node.expression, 'each', viewHelperNames, viewHelperAliases);
795
+ const isBranch = isHelperCall(node.expression, 'branch', viewHelperNames, viewHelperAliases);
796
+ const isShow = isHelperCall(node.expression, 'show', viewHelperNames, viewHelperAliases);
797
+ if (!isEach && !isBranch && !isShow)
798
+ return null;
799
+ const optsArg = node.arguments[0];
800
+ if (!optsArg || !ts.isObjectLiteralExpression(optsArg))
801
+ return null;
802
+ // Already has __mask
803
+ for (const prop of optsArg.properties) {
804
+ if (ts.isPropertyAssignment(prop) &&
805
+ ts.isIdentifier(prop.name) &&
806
+ prop.name.text === '__mask') {
807
+ return null;
808
+ }
809
+ }
810
+ // Find the driving accessor property: items/on/when
811
+ const driverProp = isEach ? 'items' : isBranch ? 'on' : 'when';
812
+ let driverAccessor = null;
813
+ for (const prop of optsArg.properties) {
814
+ if (ts.isPropertyAssignment(prop) &&
815
+ ts.isIdentifier(prop.name) &&
816
+ prop.name.text === driverProp) {
817
+ if (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer)) {
818
+ driverAccessor = prop.initializer;
819
+ }
820
+ break;
821
+ }
822
+ }
823
+ if (!driverAccessor)
824
+ return null;
825
+ const { mask } = computeAccessorMask(driverAccessor, fieldBits);
826
+ if (mask === 0 || mask === (0xffffffff | 0))
827
+ return null; // no benefit
828
+ // Inject __mask into the options object
829
+ const maskProp = f.createPropertyAssignment('__mask', createMaskLiteral(f, mask));
830
+ const newProps = [...optsArg.properties, maskProp];
831
+ const newOpts = f.createObjectLiteralExpression(newProps, optsArg.properties.hasTrailingComma);
832
+ return f.createCallExpression(node.expression, node.typeArguments, [
833
+ newOpts,
834
+ ...node.arguments.slice(1),
835
+ ]);
836
+ }
755
837
  function tryInjectDirty(node, fieldBits, f) {
756
838
  if (fieldBits.size === 0)
757
839
  return null;
@@ -803,12 +885,908 @@ function tryInjectDirty(node, fieldBits, f) {
803
885
  legendProps.push(f.createPropertyAssignment(field, createMaskLiteral(f, bit)));
804
886
  }
805
887
  const legendProp = f.createPropertyAssignment('__maskLegend', f.createObjectLiteralExpression(legendProps, false));
806
- const newConfig = f.createObjectLiteralExpression([...configArg.properties, dirtyProp, legendProp], true);
888
+ // Structural mask used by both __update and __handlers
889
+ const structuralMask = computeStructuralMask(configArg, fieldBits);
890
+ const phase2Mask = computePhase2Mask(configArg, fieldBits);
891
+ const updateBody = buildUpdateBody(f, structuralMask, phase2Mask);
892
+ const updateFn = f.createArrowFunction(undefined, undefined, [
893
+ f.createParameterDeclaration(undefined, undefined, 's'),
894
+ f.createParameterDeclaration(undefined, undefined, 'd'),
895
+ f.createParameterDeclaration(undefined, undefined, 'b'),
896
+ f.createParameterDeclaration(undefined, undefined, 'bl'),
897
+ f.createParameterDeclaration(undefined, undefined, 'p'),
898
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), updateBody);
899
+ const updateProp = f.createPropertyAssignment('__update', updateFn);
900
+ // __handlers: per-message-type specialized update functions.
901
+ // Analyzes the update() switch/case and generates direct handlers
902
+ // that bypass the generic Phase 1/2 pipeline for single-message updates.
903
+ const handlersProp = tryBuildHandlers(configArg, topLevelBits, structuralMask, f);
904
+ // Keep __update even when __handlers is present — it provides Phase 1
905
+ // mask gating for multi-message batches that bypass __handlers.
906
+ const extraProps = [dirtyProp, legendProp, updateProp];
907
+ if (handlersProp)
908
+ extraProps.push(handlersProp);
909
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, ...extraProps], true);
807
910
  return f.createCallExpression(node.expression, node.typeArguments, [
808
911
  newConfig,
809
912
  ...node.arguments.slice(1),
810
913
  ]);
811
914
  }
915
+ /**
916
+ * Analyze update() switch/case and generate per-message-type handlers.
917
+ *
918
+ * Each handler receives (inst, msg) and returns [newState, effects].
919
+ * The handler calls update() to get the new state, then directly invokes
920
+ * the appropriate runtime primitives (reconcileItems, __directUpdate, etc.)
921
+ * instead of going through the generic Phase 1/2 pipeline.
922
+ *
923
+ * Conservative: only generates handlers for cases where the field
924
+ * modifications are statically determinable. Complex cases are skipped.
925
+ */
926
+ function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
927
+ if (topLevelBits.size === 0)
928
+ return null;
929
+ // Find the update function in the component config
930
+ let updateFn = null;
931
+ for (const prop of configArg.properties) {
932
+ if (ts.isPropertyAssignment(prop) &&
933
+ ts.isIdentifier(prop.name) &&
934
+ prop.name.text === 'update') {
935
+ if (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer)) {
936
+ updateFn = prop.initializer;
937
+ }
938
+ break;
939
+ }
940
+ }
941
+ if (!updateFn)
942
+ return null;
943
+ // Find the switch statement in the update body
944
+ const body = ts.isBlock(updateFn.body) ? updateFn.body : null;
945
+ if (!body)
946
+ return null;
947
+ let switchStmt = null;
948
+ for (const stmt of body.statements) {
949
+ if (ts.isSwitchStatement(stmt)) {
950
+ switchStmt = stmt;
951
+ break;
952
+ }
953
+ }
954
+ if (!switchStmt)
955
+ return null;
956
+ // Check the switch discriminant is msg.type pattern
957
+ const stateParam = updateFn.parameters[0]?.name;
958
+ const msgParam = updateFn.parameters[1]?.name;
959
+ if (!stateParam || !msgParam || !ts.isIdentifier(stateParam) || !ts.isIdentifier(msgParam))
960
+ return null;
961
+ const stateName = stateParam.text;
962
+ const _msgName = msgParam.text;
963
+ // Analyze each case clause
964
+ const handlers = [];
965
+ for (const clause of switchStmt.caseBlock.clauses) {
966
+ if (!ts.isCaseClause(clause))
967
+ continue;
968
+ // Extract the case label — must be a string literal like 'select'
969
+ if (!ts.isStringLiteral(clause.expression))
970
+ continue;
971
+ const msgType = clause.expression.text;
972
+ // Find the return statement in the case body
973
+ let returnExpr = null;
974
+ for (const stmt of clause.statements) {
975
+ if (ts.isReturnStatement(stmt) &&
976
+ stmt.expression &&
977
+ ts.isArrayLiteralExpression(stmt.expression)) {
978
+ returnExpr = stmt.expression;
979
+ break;
980
+ }
981
+ // Handle block-scoped cases: case 'x': { ... return [...] }
982
+ if (ts.isBlock(stmt)) {
983
+ for (const inner of stmt.statements) {
984
+ if (ts.isReturnStatement(inner) &&
985
+ inner.expression &&
986
+ ts.isArrayLiteralExpression(inner.expression)) {
987
+ returnExpr = inner.expression;
988
+ break;
989
+ }
990
+ }
991
+ }
992
+ }
993
+ if (!returnExpr || returnExpr.elements.length < 2)
994
+ continue;
995
+ // Analyze the state expression (first element of return [newState, effects])
996
+ const stateExpr = returnExpr.elements[0];
997
+ // Determine which top-level fields change
998
+ const modifiedFields = analyzeModifiedFields(stateExpr, stateName, topLevelBits);
999
+ if (!modifiedFields)
1000
+ continue; // too complex to analyze
1001
+ // Compute the dirty mask for this case
1002
+ let caseDirty = 0;
1003
+ for (const field of modifiedFields) {
1004
+ caseDirty |= topLevelBits.get(field) ?? 0xffffffff | 0;
1005
+ }
1006
+ // Detect array operation pattern for structural block optimization
1007
+ const arrayOp = detectArrayOp(clause, stateName, modifiedFields, structuralMask, caseDirty);
1008
+ const handler = buildCaseHandler(f, caseDirty, arrayOp);
1009
+ handlers.push(f.createPropertyAssignment(f.createStringLiteral(msgType), handler));
1010
+ }
1011
+ if (handlers.length === 0)
1012
+ return null;
1013
+ return f.createPropertyAssignment('__handlers', f.createObjectLiteralExpression(handlers, true));
1014
+ }
1015
+ /**
1016
+ * Detect the array operation pattern in a case body.
1017
+ * - 'none': no array field modified (e.g., only `selected` changes)
1018
+ * - 'clear': array set to empty literal `[]`
1019
+ * - 'mutate': array created via `.slice()` then mutated in place (same keys)
1020
+ * - 'general': unknown pattern, use generic reconcile
1021
+ */
1022
+ function detectArrayOp(clause, stateName, modifiedFields, structuralMask, caseDirty) {
1023
+ // No fields modified or dirty bits don't intersect any structural block →
1024
+ // skip structural blocks entirely (e.g., only `selected` changes)
1025
+ if (modifiedFields.length === 0)
1026
+ return 'none';
1027
+ if (structuralMask !== undefined && caseDirty !== undefined && (structuralMask & caseDirty) === 0)
1028
+ return 'none';
1029
+ // Look at the return expression's array field values
1030
+ for (const stmt of clause.statements) {
1031
+ const returnExpr = findReturnArray(stmt);
1032
+ if (!returnExpr)
1033
+ continue;
1034
+ const stateExpr = returnExpr.elements[0];
1035
+ if (!stateExpr || !ts.isObjectLiteralExpression(stateExpr))
1036
+ continue;
1037
+ for (const prop of stateExpr.properties) {
1038
+ const name = ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)
1039
+ ? prop.name.text
1040
+ : ts.isShorthandPropertyAssignment(prop)
1041
+ ? prop.name.text
1042
+ : null;
1043
+ if (!name)
1044
+ continue;
1045
+ // Check for empty array literal: `field: []`
1046
+ if (ts.isPropertyAssignment(prop) &&
1047
+ ts.isArrayLiteralExpression(prop.initializer) &&
1048
+ prop.initializer.elements.length === 0) {
1049
+ return 'clear';
1050
+ }
1051
+ // Check for shorthand `field` where field was assigned via `.slice()` earlier
1052
+ // This catches: `const rows = state.rows.slice(); rows[i] = ...; return { ...state, rows }`
1053
+ if (ts.isShorthandPropertyAssignment(prop)) {
1054
+ const varName = prop.name.text;
1055
+ if (hasSliceAssignment(clause, stateName, varName)) {
1056
+ // Check for strided for-loop: for (let i = 0; i < arr.length; i += STRIDE)
1057
+ const stride = detectStrideLoop(clause, varName);
1058
+ if (stride > 1)
1059
+ return { type: 'strided', stride };
1060
+ return 'mutate';
1061
+ }
1062
+ }
1063
+ // Check for property assignment with filter: `field: state.field.filter(...)`
1064
+ if (ts.isPropertyAssignment(prop) && ts.isCallExpression(prop.initializer)) {
1065
+ const call = prop.initializer;
1066
+ if (ts.isPropertyAccessExpression(call.expression) &&
1067
+ call.expression.name.text === 'filter') {
1068
+ return 'remove';
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ return 'general';
1074
+ }
1075
+ function findReturnArray(stmt) {
1076
+ if (ts.isReturnStatement(stmt) && stmt.expression && ts.isArrayLiteralExpression(stmt.expression))
1077
+ return stmt.expression;
1078
+ if (ts.isBlock(stmt)) {
1079
+ for (const inner of stmt.statements) {
1080
+ const result = findReturnArray(inner);
1081
+ if (result)
1082
+ return result;
1083
+ }
1084
+ }
1085
+ return null;
1086
+ }
1087
+ /**
1088
+ * Detect a strided for-loop: `for (let i = 0; i < arr.length; i += STRIDE)`
1089
+ * where `arr` is the named variable. Returns the stride or 0 if not found.
1090
+ */
1091
+ function detectStrideLoop(clause, _arrName) {
1092
+ function walk(node) {
1093
+ if (ts.isForStatement(node) && node.incrementor) {
1094
+ // Check incrementor: i += STRIDE
1095
+ const inc = node.incrementor;
1096
+ if (ts.isBinaryExpression(inc) &&
1097
+ inc.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken &&
1098
+ ts.isNumericLiteral(inc.right)) {
1099
+ const stride = parseInt(inc.right.text, 10);
1100
+ if (stride > 1)
1101
+ return stride;
1102
+ }
1103
+ }
1104
+ return ts.forEachChild(node, walk) ?? 0;
1105
+ }
1106
+ for (const stmt of clause.statements) {
1107
+ const result = walk(stmt);
1108
+ if (result > 0)
1109
+ return result;
1110
+ }
1111
+ return 0;
1112
+ }
1113
+ function hasSliceAssignment(clause, stateName, varName) {
1114
+ function walk(node) {
1115
+ // Look for: const varName = stateName.field.slice()
1116
+ if (ts.isVariableDeclaration(node) &&
1117
+ ts.isIdentifier(node.name) &&
1118
+ node.name.text === varName &&
1119
+ node.initializer) {
1120
+ const init = node.initializer;
1121
+ if (ts.isCallExpression(init) &&
1122
+ ts.isPropertyAccessExpression(init.expression) &&
1123
+ init.expression.name.text === 'slice') {
1124
+ return true;
1125
+ }
1126
+ }
1127
+ return ts.forEachChild(node, walk) ?? false;
1128
+ }
1129
+ for (const stmt of clause.statements) {
1130
+ if (walk(stmt))
1131
+ return true;
1132
+ }
1133
+ return false;
1134
+ }
1135
+ /**
1136
+ * Analyze which top-level state fields are modified in a return expression.
1137
+ * Returns the set of field names, or null if too complex to determine.
1138
+ */
1139
+ function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
1140
+ // Pattern: { ...state, field1: ..., field2: ... } or { field1: ..., field2: ... }
1141
+ if (ts.isObjectLiteralExpression(stateExpr)) {
1142
+ const modified = [];
1143
+ for (const prop of stateExpr.properties) {
1144
+ if (ts.isSpreadAssignment(prop)) {
1145
+ // { ...state } — the spread doesn't modify fields
1146
+ continue;
1147
+ }
1148
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1149
+ const fieldName = prop.name.text;
1150
+ if (topLevelBits.has(fieldName)) {
1151
+ modified.push(fieldName);
1152
+ }
1153
+ }
1154
+ // Handle shorthand: { ...state, rows } where rows is a local variable
1155
+ if (ts.isShorthandPropertyAssignment(prop)) {
1156
+ const fieldName = prop.name.text;
1157
+ if (topLevelBits.has(fieldName)) {
1158
+ modified.push(fieldName);
1159
+ }
1160
+ }
1161
+ }
1162
+ return modified.length > 0 ? modified : null;
1163
+ }
1164
+ // Pattern: state (no change — early return)
1165
+ if (ts.isIdentifier(stateExpr) && stateExpr.text === stateName) {
1166
+ return []; // no fields modified
1167
+ }
1168
+ return null; // too complex
1169
+ }
1170
+ /**
1171
+ * Build a handler function for a specific message type case.
1172
+ *
1173
+ * Generated: (inst, msg) => {
1174
+ * const [s, e] = inst.def.update(inst.state, msg)
1175
+ * inst.state = s
1176
+ * const bl = inst.structuralBlocks, b = inst.allBindings, p = b.length
1177
+ * // Phase 1: gated by caseDirty
1178
+ * for (let i = 0; i < bl.length; i++) {
1179
+ * if (bl[i].mask & caseDirty) bl[i].reconcile(s, caseDirty)
1180
+ * }
1181
+ * // Phase 2
1182
+ * __runPhase2(s, caseDirty, b, p)
1183
+ * return [s, e]
1184
+ * }
1185
+ */
1186
+ /**
1187
+ * Build a handler that delegates to __handleMsg(inst, msg, dirty, method).
1188
+ * method: 0=reconcile, 1=reconcileItems, 2=reconcileClear, 3=reconcileRemove, -1=skip blocks
1189
+ */
1190
+ function buildCaseHandler(f, caseDirty, arrayOp) {
1191
+ const method = typeof arrayOp === 'object' && arrayOp.type === 'strided'
1192
+ ? 10 + arrayOp.stride // reconcileChanged with stride
1193
+ : arrayOp === 'none'
1194
+ ? -1
1195
+ : arrayOp === 'mutate'
1196
+ ? 1
1197
+ : arrayOp === 'clear'
1198
+ ? 2
1199
+ : arrayOp === 'remove'
1200
+ ? 3
1201
+ : 0; // general
1202
+ // (inst, msg) => __handleMsg(inst, msg, dirty, method)
1203
+ return f.createArrowFunction(undefined, undefined, [
1204
+ f.createParameterDeclaration(undefined, undefined, 'inst'),
1205
+ f.createParameterDeclaration(undefined, undefined, 'msg'),
1206
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createCallExpression(f.createIdentifier('__handleMsg'), undefined, [
1207
+ f.createIdentifier('inst'),
1208
+ f.createIdentifier('msg'),
1209
+ createMaskLiteral(f, caseDirty),
1210
+ method >= 0
1211
+ ? f.createNumericLiteral(method)
1212
+ : f.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f.createNumericLiteral(1)),
1213
+ ]));
1214
+ }
1215
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1216
+ function _deadCode_legacyCaseHandler(f, caseDirty, arrayOp) {
1217
+ const stmts = [];
1218
+ // const [s, e] = inst.def.update(inst.state, msg)
1219
+ stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
1220
+ f.createVariableDeclaration(f.createArrayBindingPattern([
1221
+ f.createBindingElement(undefined, undefined, 's'),
1222
+ f.createBindingElement(undefined, undefined, 'e'),
1223
+ ]), undefined, undefined, f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier('inst'), 'def'), 'update'), undefined, [
1224
+ f.createPropertyAccessExpression(f.createIdentifier('inst'), 'state'),
1225
+ f.createIdentifier('msg'),
1226
+ ])),
1227
+ ], ts.NodeFlags.Const)));
1228
+ // inst.state = s
1229
+ stmts.push(f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('inst'), 'state'), ts.SyntaxKind.EqualsToken, f.createIdentifier('s'))));
1230
+ // Phase 1 + Phase 2, specialized by array operation pattern
1231
+ if (caseDirty !== 0) {
1232
+ // Determine the reconcile method based on array operation
1233
+ const reconcileMethod = arrayOp === 'clear'
1234
+ ? 'reconcileClear'
1235
+ : arrayOp === 'mutate'
1236
+ ? 'reconcileItems'
1237
+ : arrayOp === 'remove'
1238
+ ? 'reconcileRemove'
1239
+ : 'reconcile';
1240
+ if (arrayOp === 'none') {
1241
+ // No structural changes — skip Phase 1, only run Phase 2
1242
+ // (e.g., select: only selector binding needs updating)
1243
+ }
1244
+ else {
1245
+ // Phase 1: call specialized reconciler on matching blocks
1246
+ // const bl = inst.structuralBlocks
1247
+ stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
1248
+ f.createVariableDeclaration('bl', undefined, undefined, f.createPropertyAccessExpression(f.createIdentifier('inst'), 'structuralBlocks')),
1249
+ ], ts.NodeFlags.Const)));
1250
+ // for (let i = 0; i < bl.length; i++) { if (bl[i].mask & dirty) bl[i].METHOD(s, dirty) }
1251
+ const blockEl = f.createElementAccessExpression(f.createIdentifier('bl'), f.createIdentifier('i'));
1252
+ const reconcileArgs = reconcileMethod === 'reconcileClear'
1253
+ ? []
1254
+ : [
1255
+ f.createIdentifier('s'),
1256
+ ...(reconcileMethod === 'reconcile' ? [createMaskLiteral(f, caseDirty)] : []),
1257
+ ];
1258
+ stmts.push(f.createForStatement(f.createVariableDeclarationList([f.createVariableDeclaration('i', undefined, undefined, f.createNumericLiteral(0))], ts.NodeFlags.Let), f.createBinaryExpression(f.createIdentifier('i'), ts.SyntaxKind.LessThanToken, f.createPropertyAccessExpression(f.createIdentifier('bl'), 'length')), f.createPostfixUnaryExpression(f.createIdentifier('i'), ts.SyntaxKind.PlusPlusToken), f.createBlock([
1259
+ f.createIfStatement(f.createBinaryExpression(f.createPropertyAccessExpression(blockEl, 'mask'), ts.SyntaxKind.AmpersandToken, createMaskLiteral(f, caseDirty)), f.createExpressionStatement(f.createCallExpression(
1260
+ // Use specialized method if available, fall back to reconcile
1261
+ // bl[i].reconcileItems?.(s) ?? bl[i].reconcile(s, dirty)
1262
+ // Simplified: just call the method — it exists on each() blocks
1263
+ f.createPropertyAccessExpression(blockEl, reconcileMethod), undefined, reconcileArgs))),
1264
+ ], true)));
1265
+ }
1266
+ // Phase 2: compact + update bindings
1267
+ // const b = inst.allBindings, p = b.length
1268
+ stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
1269
+ f.createVariableDeclaration('b', undefined, undefined, f.createPropertyAccessExpression(f.createIdentifier('inst'), 'allBindings')),
1270
+ f.createVariableDeclaration('p', undefined, undefined, f.createPropertyAccessExpression(f.createIdentifier('b'), 'length')),
1271
+ ], ts.NodeFlags.Const)));
1272
+ // __runPhase2(s, caseDirty, b, p)
1273
+ stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__runPhase2'), undefined, [
1274
+ f.createIdentifier('s'),
1275
+ createMaskLiteral(f, caseDirty),
1276
+ f.createIdentifier('b'),
1277
+ f.createIdentifier('p'),
1278
+ ])));
1279
+ }
1280
+ // return [s, e]
1281
+ stmts.push(f.createReturnStatement(f.createArrayLiteralExpression([f.createIdentifier('s'), f.createIdentifier('e')])));
1282
+ return f.createArrowFunction(undefined, undefined, [
1283
+ f.createParameterDeclaration(undefined, undefined, 'inst'),
1284
+ f.createParameterDeclaration(undefined, undefined, 'msg'),
1285
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(stmts, true));
1286
+ }
1287
+ // ── Row Factory ─────────────────────────────────────────────────
1288
+ /**
1289
+ * Transform an eligible each() render to use a row factory — shared update
1290
+ * function with zero per-row closures for bindings.
1291
+ *
1292
+ * Runs AFTER the element transform pass has converted tr/td/text() calls
1293
+ * into elTemplate() calls, so we can analyze the template structure.
1294
+ */
1295
+ function tryEmitRowFactory(eachCall, f, _originalSource) {
1296
+ const arg = eachCall.arguments[0];
1297
+ if (!arg || !ts.isObjectLiteralExpression(arg))
1298
+ return null;
1299
+ // Find render property
1300
+ let renderProp = null;
1301
+ for (const prop of arg.properties) {
1302
+ if (ts.isPropertyAssignment(prop) &&
1303
+ ts.isIdentifier(prop.name) &&
1304
+ prop.name.text === 'render') {
1305
+ renderProp = prop;
1306
+ break;
1307
+ }
1308
+ }
1309
+ if (!renderProp)
1310
+ return null;
1311
+ const renderFn = renderProp.initializer;
1312
+ if (!ts.isArrowFunction(renderFn) && !ts.isFunctionExpression(renderFn))
1313
+ return null;
1314
+ const body = ts.isBlock(renderFn.body) ? renderFn.body : null;
1315
+ if (!body)
1316
+ return null;
1317
+ // Find the elTemplate call in the transformed render body
1318
+ let templateCall = null;
1319
+ let templateVarName = null;
1320
+ for (const stmt of body.statements) {
1321
+ if (ts.isVariableStatement(stmt)) {
1322
+ for (const decl of stmt.declarationList.declarations) {
1323
+ if (ts.isIdentifier(decl.name) &&
1324
+ decl.initializer &&
1325
+ ts.isCallExpression(decl.initializer)) {
1326
+ if (ts.isIdentifier(decl.initializer.expression) &&
1327
+ decl.initializer.expression.text === 'elTemplate') {
1328
+ if (templateCall)
1329
+ return null; // multiple templates — bail
1330
+ templateCall = decl.initializer;
1331
+ templateVarName = decl.name.text;
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ // Check for nested structural primitives — bail
1337
+ if (containsStructuralCall(stmt))
1338
+ return null;
1339
+ // Bail on selector.bind() — row factory + selector causes V8 deopt
1340
+ // even without per-row disposers (selector fn declarations per render)
1341
+ if (_containsSelectorBind(stmt))
1342
+ return null;
1343
+ }
1344
+ if (!templateCall || templateCall.arguments.length < 2)
1345
+ return null;
1346
+ // Extract HTML string
1347
+ const htmlArg = templateCall.arguments[0];
1348
+ if (!htmlArg || !ts.isStringLiteral(htmlArg))
1349
+ return null;
1350
+ const html = htmlArg.text;
1351
+ // Extract patch function
1352
+ const patchFn = templateCall.arguments[1];
1353
+ if (!patchFn || (!ts.isArrowFunction(patchFn) && !ts.isFunctionExpression(patchFn)))
1354
+ return null;
1355
+ const patchBody = ts.isBlock(patchFn.body) ? patchFn.body : null;
1356
+ if (!patchBody)
1357
+ return null;
1358
+ const rootParam = patchFn.parameters[0];
1359
+ const bindParam = patchFn.parameters[1];
1360
+ if (!rootParam || !bindParam)
1361
+ return null;
1362
+ const rootName = ts.isIdentifier(rootParam.name) ? rootParam.name.text : null;
1363
+ const bindName = ts.isIdentifier(bindParam.name) ? bindParam.name.text : null;
1364
+ if (!rootName || !bindName)
1365
+ return null;
1366
+ const bindings = [];
1367
+ const nodeVarInitializers = new Map();
1368
+ for (const stmt of patchBody.statements) {
1369
+ if (ts.isVariableStatement(stmt)) {
1370
+ for (const decl of stmt.declarationList.declarations) {
1371
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
1372
+ nodeVarInitializers.set(decl.name.text, decl.initializer);
1373
+ }
1374
+ }
1375
+ }
1376
+ if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
1377
+ const call = stmt.expression;
1378
+ if (ts.isIdentifier(call.expression) &&
1379
+ call.expression.text === bindName &&
1380
+ call.arguments.length >= 5) {
1381
+ const nodeArg = call.arguments[0];
1382
+ const maskArg = call.arguments[1];
1383
+ const kindArg = call.arguments[2];
1384
+ const keyArg = call.arguments[3];
1385
+ const accessorArg = call.arguments[4];
1386
+ // Must be per-item (mask -1)
1387
+ if (ts.isPrefixUnaryExpression(maskArg) && maskArg.operator === ts.SyntaxKind.MinusToken) {
1388
+ // -1 → per-item ✓
1389
+ }
1390
+ else if (ts.isBinaryExpression(maskArg)) {
1391
+ // -1 | 0 or 4294967295 | 0 → per-item ✓
1392
+ }
1393
+ else {
1394
+ return null; // state-level binding — bail
1395
+ }
1396
+ const kind = ts.isStringLiteral(kindArg) ? kindArg.text : '';
1397
+ const key = ts.isStringLiteral(keyArg) ? keyArg.text : undefined;
1398
+ // Resolve node path — recursively expand variable references to get
1399
+ // the full path from root, then create fresh factory nodes
1400
+ function resolveNodePath(expr) {
1401
+ if (ts.isIdentifier(expr)) {
1402
+ if (expr.text === rootName)
1403
+ return f.createIdentifier(rootName);
1404
+ const init = nodeVarInitializers.get(expr.text);
1405
+ if (init)
1406
+ return resolveNodePath(init);
1407
+ return f.createIdentifier(expr.text);
1408
+ }
1409
+ if (ts.isPropertyAccessExpression(expr)) {
1410
+ return f.createPropertyAccessExpression(resolveNodePath(expr.expression), expr.name.text);
1411
+ }
1412
+ if (ts.isElementAccessExpression(expr)) {
1413
+ return f.createElementAccessExpression(resolveNodePath(expr.expression), expr.argumentExpression);
1414
+ }
1415
+ return expr;
1416
+ }
1417
+ const nodeInit = resolveNodePath(nodeArg);
1418
+ // Clone accessor to strip source position — prevents mixed-position errors
1419
+ const clonedAccessor = ts.isIdentifier(accessorArg)
1420
+ ? f.createIdentifier(accessorArg.text)
1421
+ : accessorArg;
1422
+ bindings.push({ nodeInitializer: nodeInit, kind, key, accessor: clonedAccessor });
1423
+ }
1424
+ }
1425
+ }
1426
+ if (bindings.length === 0)
1427
+ return null;
1428
+ // Build map of __a{N} → __s{N} for rewriting accessor references.
1429
+ // After dedup, `__a{N} = acc(__s{N})`. In the row factory, __a{N} declarations
1430
+ // are eliminated, so all references must be rewritten to __s{N}.
1431
+ const accToSelector = new Map();
1432
+ for (const stmt of body.statements) {
1433
+ if (!ts.isVariableStatement(stmt))
1434
+ continue;
1435
+ for (const decl of stmt.declarationList.declarations) {
1436
+ if (!ts.isIdentifier(decl.name) || !decl.name.text.startsWith('__a'))
1437
+ continue;
1438
+ if (!decl.initializer || !ts.isCallExpression(decl.initializer))
1439
+ continue;
1440
+ const callArg0 = decl.initializer.arguments[0];
1441
+ if (callArg0 && ts.isIdentifier(callArg0) && callArg0.text.startsWith('__s')) {
1442
+ accToSelector.set(decl.name.text, callArg0.text);
1443
+ }
1444
+ }
1445
+ }
1446
+ // Rewrite binding accessors: __a{N} → __s{N}
1447
+ for (const b of bindings) {
1448
+ if (ts.isIdentifier(b.accessor) && accToSelector.has(b.accessor.text)) {
1449
+ b.accessor = f.createIdentifier(accToSelector.get(b.accessor.text));
1450
+ }
1451
+ }
1452
+ // Collect __s{N} selector definitions — needed by __rowUpd and render init.
1453
+ // These are currently scoped to the render body; we'll hoist them into the
1454
+ // __rowUpd IIFE so they're accessible.
1455
+ const selectorDefs = new Map();
1456
+ for (const stmt of body.statements) {
1457
+ if (!ts.isVariableStatement(stmt))
1458
+ continue;
1459
+ for (const decl of stmt.declarationList.declarations) {
1460
+ if (ts.isIdentifier(decl.name) && decl.name.text.startsWith('__s') && decl.initializer) {
1461
+ selectorDefs.set(decl.name.text, decl.initializer);
1462
+ }
1463
+ }
1464
+ }
1465
+ // === Generate the row factory ===
1466
+ // 1. __tpl: IIFE that creates + caches the template element
1467
+ const tplInit = f.createCallExpression(f.createParenthesizedExpression(f.createArrowFunction(undefined, undefined, [], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock([
1468
+ f.createVariableStatement(undefined, f.createVariableDeclarationList([
1469
+ f.createVariableDeclaration('t', undefined, undefined, f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('document'), 'createElement'), undefined, [f.createStringLiteral('template')])),
1470
+ ], ts.NodeFlags.Const)),
1471
+ f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('t'), 'innerHTML'), ts.SyntaxKind.EqualsToken, f.createStringLiteral(html))),
1472
+ f.createReturnStatement(f.createIdentifier('t')),
1473
+ ], true))), undefined, []);
1474
+ // 2. __rowUpd: (e) => { const t = e.current; for each binding: check + write }
1475
+ const updStmts = [];
1476
+ updStmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
1477
+ f.createVariableDeclaration('t', undefined, undefined, f.createPropertyAccessExpression(f.createIdentifier('e'), 'current')),
1478
+ ], ts.NodeFlags.Const)));
1479
+ for (let i = 0; i < bindings.length; i++) {
1480
+ const b = bindings[i];
1481
+ const vId = f.createIdentifier(`v${i}`);
1482
+ const cachedProp = f.createElementAccessExpression(f.createIdentifier('e'), f.createStringLiteral(`_v${i}`));
1483
+ const nodeProp = f.createElementAccessExpression(f.createIdentifier('e'), f.createStringLiteral(`_n${i}`));
1484
+ // const v{i} = accessor(t)
1485
+ updStmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
1486
+ f.createVariableDeclaration(vId, undefined, undefined, f.createCallExpression(b.accessor, undefined, [f.createIdentifier('t')])),
1487
+ ], ts.NodeFlags.Const)));
1488
+ // DOM write expression
1489
+ const domWrite = b.kind === 'text'
1490
+ ? f.createBinaryExpression(f.createPropertyAccessExpression(nodeProp, 'nodeValue'), ts.SyntaxKind.EqualsToken, vId)
1491
+ : b.kind === 'class'
1492
+ ? f.createBinaryExpression(f.createPropertyAccessExpression(nodeProp, 'className'), ts.SyntaxKind.EqualsToken, vId)
1493
+ : f.createBinaryExpression(f.createPropertyAccessExpression(nodeProp, 'nodeValue'), ts.SyntaxKind.EqualsToken, vId);
1494
+ // if (v{i} !== e['_v{i}']) { e['_v{i}'] = v{i}; DOM_WRITE }
1495
+ updStmts.push(f.createIfStatement(f.createBinaryExpression(vId, ts.SyntaxKind.ExclamationEqualsEqualsToken, cachedProp), f.createBlock([
1496
+ f.createExpressionStatement(f.createBinaryExpression(cachedProp, ts.SyntaxKind.EqualsToken, vId)),
1497
+ f.createExpressionStatement(domWrite),
1498
+ ], true)));
1499
+ }
1500
+ // Wrap __rowUpd in IIFE that declares selectors (they're scoped to the
1501
+ // render body but __rowUpd lives on the options object outside render).
1502
+ const rawUpdFn = f.createArrowFunction(undefined, undefined, [f.createParameterDeclaration(undefined, undefined, 'e')], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(updStmts, true));
1503
+ // Build: (() => { const __s0 = ...; const __s1 = ...; return (e) => { ... } })()
1504
+ const selectorDecls = [];
1505
+ for (const [name, init] of selectorDefs) {
1506
+ selectorDecls.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(name, undefined, undefined, init)], ts.NodeFlags.Const)));
1507
+ }
1508
+ selectorDecls.push(f.createReturnStatement(rawUpdFn));
1509
+ const rowUpdFn = f.createCallExpression(f.createParenthesizedExpression(f.createArrowFunction(undefined, undefined, [], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(selectorDecls, true))), undefined, []);
1510
+ // 3. New render callback: ({ entry: e, __tpl, __rowUpd }) => { ... }
1511
+ const renderStmts = [];
1512
+ // Declare selectors at the top of render body (they're used for initial values)
1513
+ for (const [name, init] of selectorDefs) {
1514
+ renderStmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(name, undefined, undefined, init)], ts.NodeFlags.Const)));
1515
+ }
1516
+ // const r = __tpl.content.firstElementChild.cloneNode(true)
1517
+ renderStmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
1518
+ f.createVariableDeclaration('r', undefined, undefined, f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier('__tpl'), 'content'), 'firstElementChild'), 'cloneNode'), undefined, [f.createTrue()])),
1519
+ ], ts.NodeFlags.Const)));
1520
+ // For each binding: store node ref, compute initial, apply
1521
+ for (let i = 0; i < bindings.length; i++) {
1522
+ const b = bindings[i];
1523
+ const nProp = f.createElementAccessExpression(f.createIdentifier('e'), f.createStringLiteral(`_n${i}`));
1524
+ const vProp = f.createElementAccessExpression(f.createIdentifier('e'), f.createStringLiteral(`_v${i}`));
1525
+ // Rewrite node path: replace root param name with 'r'
1526
+ const rewrittenPath = rewriteRoot(b.nodeInitializer, rootName, 'r', f);
1527
+ // e['_n{i}'] = rewrittenPath
1528
+ renderStmts.push(f.createExpressionStatement(f.createBinaryExpression(nProp, ts.SyntaxKind.EqualsToken, rewrittenPath)));
1529
+ // e['_v{i}'] = accessor(e.current)
1530
+ renderStmts.push(f.createExpressionStatement(f.createBinaryExpression(vProp, ts.SyntaxKind.EqualsToken, f.createCallExpression(b.accessor, undefined, [
1531
+ f.createPropertyAccessExpression(f.createIdentifier('e'), 'current'),
1532
+ ]))));
1533
+ // DOM write: e['_n{i}'].nodeValue = e['_v{i}']
1534
+ const initWrite = b.kind === 'text'
1535
+ ? f.createBinaryExpression(f.createPropertyAccessExpression(nProp, 'nodeValue'), ts.SyntaxKind.EqualsToken, vProp)
1536
+ : b.kind === 'class'
1537
+ ? f.createBinaryExpression(f.createPropertyAccessExpression(nProp, 'className'), ts.SyntaxKind.EqualsToken, vProp)
1538
+ : f.createBinaryExpression(f.createPropertyAccessExpression(nProp, 'nodeValue'), ts.SyntaxKind.EqualsToken, vProp);
1539
+ renderStmts.push(f.createExpressionStatement(initWrite));
1540
+ }
1541
+ // e.__rowUpdate = __rowUpd
1542
+ renderStmts.push(f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('e'), '__rowUpdate'), ts.SyntaxKind.EqualsToken, f.createIdentifier('__rowUpd'))));
1543
+ // Rewrite a statement: replace __a{N}() → __s{N}(e.current),
1544
+ // replace template var → r, strip positions via deep clone.
1545
+ function rewriteStmt(stmt) {
1546
+ function visit(node) {
1547
+ // Rewrite __a{N}() → __s{N}(e.current)
1548
+ if (ts.isCallExpression(node) &&
1549
+ ts.isIdentifier(node.expression) &&
1550
+ accToSelector.has(node.expression.text) &&
1551
+ node.arguments.length === 0) {
1552
+ const selectorName = accToSelector.get(node.expression.text);
1553
+ return f.createCallExpression(f.createIdentifier(selectorName), undefined, [
1554
+ f.createPropertyAccessExpression(f.createIdentifier('e'), 'current'),
1555
+ ]);
1556
+ }
1557
+ // Rewrite template variable → r
1558
+ if (ts.isIdentifier(node) && templateVarName && node.text === templateVarName) {
1559
+ return f.createIdentifier('r');
1560
+ }
1561
+ // Clone identifiers to strip positions
1562
+ if (ts.isIdentifier(node)) {
1563
+ return f.createIdentifier(node.text);
1564
+ }
1565
+ return ts.visitEachChild(node, visit, undefined);
1566
+ }
1567
+ return ts.visitEachChild(stmt, visit, undefined);
1568
+ }
1569
+ // Preserve non-template, non-compiler-generated, non-return statements.
1570
+ for (const stmt of body.statements) {
1571
+ if (ts.isReturnStatement(stmt))
1572
+ continue;
1573
+ if (ts.isVariableStatement(stmt)) {
1574
+ // Skip template declaration
1575
+ const isTemplate = stmt.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === templateVarName);
1576
+ if (isTemplate)
1577
+ continue;
1578
+ // Skip __a{N} and __s{N} declarations (compiler-generated acc/selector)
1579
+ const isCompilerOnly = stmt.declarationList.declarations.every((d) => ts.isIdentifier(d.name) &&
1580
+ (d.name.text.startsWith('__a') || d.name.text.startsWith('__s')));
1581
+ if (isCompilerOnly)
1582
+ continue;
1583
+ }
1584
+ // Rewrite and include
1585
+ renderStmts.push(rewriteStmt(stmt));
1586
+ }
1587
+ // return [r]
1588
+ renderStmts.push(f.createReturnStatement(f.createArrayLiteralExpression([f.createIdentifier('r')])));
1589
+ const newRenderFn = f.createArrowFunction(undefined, undefined, [
1590
+ f.createParameterDeclaration(undefined, undefined, f.createObjectBindingPattern([
1591
+ f.createBindingElement(undefined, 'entry', 'e'),
1592
+ f.createBindingElement(undefined, undefined, '__tpl'),
1593
+ f.createBindingElement(undefined, undefined, '__rowUpd'),
1594
+ ])),
1595
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(renderStmts, true));
1596
+ // 4. Build new each options. To avoid mixed-position AST issues, we keep
1597
+ // original properties unchanged and only ADD __tpl, __rowUpd, and replace render.
1598
+ // The trick: return the original node structure but with the render property
1599
+ // swapped. Use ts.factory.updateObjectLiteralExpression which preserves positions.
1600
+ const updatedProps = arg.properties.map((p) => p === renderProp ? f.createPropertyAssignment('render', newRenderFn) : p);
1601
+ updatedProps.push(f.createPropertyAssignment('__tpl', tplInit));
1602
+ updatedProps.push(f.createPropertyAssignment('__rowUpd', rowUpdFn));
1603
+ const newOpts = f.updateObjectLiteralExpression(arg, updatedProps);
1604
+ return f.updateCallExpression(eachCall, eachCall.expression, eachCall.typeArguments, [
1605
+ newOpts,
1606
+ ...eachCall.arguments.slice(1),
1607
+ ]);
1608
+ }
1609
+ function _containsSelectorBind(node) {
1610
+ if (ts.isCallExpression(node) &&
1611
+ ts.isPropertyAccessExpression(node.expression) &&
1612
+ node.expression.name.text === 'bind') {
1613
+ return true;
1614
+ }
1615
+ return ts.forEachChild(node, _containsSelectorBind) ?? false;
1616
+ }
1617
+ function containsStructuralCall(node) {
1618
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1619
+ if (['each', 'branch', 'show', 'child', 'foreign'].includes(node.expression.text))
1620
+ return true;
1621
+ }
1622
+ return ts.forEachChild(node, containsStructuralCall) ?? false;
1623
+ }
1624
+ /** Rewrite property access chains replacing oldRoot identifier with newRoot */
1625
+ function rewriteRoot(expr, oldRoot, newRoot, f) {
1626
+ if (ts.isIdentifier(expr) && expr.text === oldRoot)
1627
+ return f.createIdentifier(newRoot);
1628
+ if (ts.isPropertyAccessExpression(expr)) {
1629
+ return f.createPropertyAccessExpression(rewriteRoot(expr.expression, oldRoot, newRoot, f), expr.name.text);
1630
+ }
1631
+ if (ts.isElementAccessExpression(expr)) {
1632
+ return f.createElementAccessExpression(rewriteRoot(expr.expression, oldRoot, newRoot, f), expr.argumentExpression);
1633
+ }
1634
+ return expr;
1635
+ }
1636
+ /** Parse a statement string into a fresh AST node (no source positions) */
1637
+ function _parseStmt(code) {
1638
+ const sf = ts.createSourceFile('__gen.ts', code, ts.ScriptTarget.Latest, false);
1639
+ return sf.statements[0] ?? null;
1640
+ }
1641
+ /**
1642
+ * Compute the OR of all structural block masks found in the view function.
1643
+ * Returns FULL_MASK if any structural block uses FULL_MASK or if no blocks found.
1644
+ */
1645
+ function computeStructuralMask(configArg, fieldBits) {
1646
+ const viewProp = configArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'view');
1647
+ if (!viewProp || !ts.isPropertyAssignment(viewProp))
1648
+ return 0xffffffff | 0;
1649
+ let mask = 0;
1650
+ let foundStructural = false;
1651
+ function walk(node) {
1652
+ if (ts.isCallExpression(node)) {
1653
+ const name = ts.isIdentifier(node.expression) ? node.expression.text : '';
1654
+ if (['each', 'branch', 'show'].includes(name) && node.arguments[0]) {
1655
+ foundStructural = true;
1656
+ const opts = node.arguments[0];
1657
+ if (ts.isObjectLiteralExpression(opts)) {
1658
+ // Check for __mask property (already injected by tryInjectStructuralMask)
1659
+ for (const prop of opts.properties) {
1660
+ if (ts.isPropertyAssignment(prop) &&
1661
+ ts.isIdentifier(prop.name) &&
1662
+ prop.name.text === '__mask') {
1663
+ if (ts.isNumericLiteral(prop.initializer)) {
1664
+ mask |= parseInt(prop.initializer.text, 10);
1665
+ return;
1666
+ }
1667
+ if (ts.isPrefixUnaryExpression(prop.initializer)) {
1668
+ // Handle negative literals like -1
1669
+ mask = 0xffffffff | 0;
1670
+ return;
1671
+ }
1672
+ }
1673
+ }
1674
+ // No __mask found — use driving accessor mask
1675
+ const driverProp = name === 'each' ? 'items' : name === 'branch' ? 'on' : 'when';
1676
+ for (const prop of opts.properties) {
1677
+ if (ts.isPropertyAssignment(prop) &&
1678
+ ts.isIdentifier(prop.name) &&
1679
+ prop.name.text === driverProp) {
1680
+ if (ts.isArrowFunction(prop.initializer) ||
1681
+ ts.isFunctionExpression(prop.initializer)) {
1682
+ const { mask: m } = computeAccessorMask(prop.initializer, fieldBits);
1683
+ mask |= m || 0xffffffff | 0;
1684
+ }
1685
+ break;
1686
+ }
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+ ts.forEachChild(node, walk);
1692
+ }
1693
+ walk(viewProp.initializer);
1694
+ return foundStructural ? mask || 0xffffffff | 0 : 0;
1695
+ }
1696
+ /**
1697
+ * Compute the OR of all component-level binding masks from text() calls
1698
+ * and element bindings in the view. Returns 0 if no component-level bindings.
1699
+ */
1700
+ function computePhase2Mask(_configArg, _fieldBits) {
1701
+ // For now, return FULL_MASK — a future pass can analyze all binding sites
1702
+ // in the view to compute the precise aggregate. The key optimization is
1703
+ // already in Phase 1 gating: when structuralMask doesn't intersect dirty,
1704
+ // the entire reconciliation is skipped.
1705
+ return 0xffffffff | 0;
1706
+ }
1707
+ /**
1708
+ * Build the __update function body:
1709
+ * {
1710
+ * // Phase 1 — structural reconciliation (gated by structuralMask)
1711
+ * if (d & structuralMask) {
1712
+ * for (let i = 0, len = bl.length; i < len; i++) {
1713
+ * const block = bl[i]
1714
+ * if ((block.mask & d) === 0) continue
1715
+ * block.reconcile(s, d)
1716
+ * }
1717
+ * // Compact dead bindings
1718
+ * if (b.length > p || (p > 0 && b[0].dead)) {
1719
+ * let w = 0
1720
+ * for (let r = 0; r < b.length; r++) { if (!b[r].dead) b[w++] = b[r] }
1721
+ * b.length = w
1722
+ * p = Math.min(w, p)
1723
+ * }
1724
+ * }
1725
+ * // Phase 2 — binding updates
1726
+ * if (d !== 0) {
1727
+ * for (let i = 0; i < p; i++) {
1728
+ * const bn = b[i]
1729
+ * if (bn.dead || (bn.mask & d) === 0) continue
1730
+ * const v = bn.accessor(s)
1731
+ * const l = bn.lastValue
1732
+ * if (v === l || (v !== v && l !== l)) continue
1733
+ * bn.lastValue = v
1734
+ * __runPhase2(s, d, b, p)
1735
+ * }
1736
+ * }
1737
+ * }
1738
+ */
1739
+ function buildUpdateBody(f, structuralMask, _phase2Mask) {
1740
+ const stmts = [];
1741
+ // Phase 1: structural block reconciliation, gated by aggregate mask
1742
+ if (structuralMask !== 0) {
1743
+ const phase1Stmts = [];
1744
+ // for (let i = 0, len = bl.length; i < len; i++) {
1745
+ // const block = bl[i]; if ((block.mask & d) === 0) continue; block.reconcile(s, d)
1746
+ // }
1747
+ const blockLoop = f.createForStatement(f.createVariableDeclarationList([
1748
+ f.createVariableDeclaration('i', undefined, undefined, f.createNumericLiteral(0)),
1749
+ f.createVariableDeclaration('len', undefined, undefined, f.createPropertyAccessExpression(f.createIdentifier('bl'), 'length')),
1750
+ ], ts.NodeFlags.Let), f.createBinaryExpression(f.createIdentifier('i'), ts.SyntaxKind.LessThanToken, f.createIdentifier('len')), f.createPostfixUnaryExpression(f.createIdentifier('i'), ts.SyntaxKind.PlusPlusToken), f.createBlock([
1751
+ f.createVariableStatement(undefined, f.createVariableDeclarationList([
1752
+ f.createVariableDeclaration('bk', undefined, undefined, f.createElementAccessExpression(f.createIdentifier('bl'), f.createIdentifier('i'))),
1753
+ ], ts.NodeFlags.Const)),
1754
+ f.createIfStatement(f.createBinaryExpression(f.createParenthesizedExpression(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'mask'), ts.SyntaxKind.AmpersandToken, f.createIdentifier('d'))), ts.SyntaxKind.EqualsEqualsEqualsToken, f.createNumericLiteral(0)), f.createContinueStatement()),
1755
+ f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'reconcile'), undefined, [f.createIdentifier('s'), f.createIdentifier('d')])),
1756
+ ], true));
1757
+ phase1Stmts.push(blockLoop);
1758
+ // Compaction: if (b.length > p || (p > 0 && b[0].dead)) { ... }
1759
+ const compactBody = f.createBlock([
1760
+ // let w = 0
1761
+ f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration('w', undefined, undefined, f.createNumericLiteral(0))], ts.NodeFlags.Let)),
1762
+ // for (let r = 0; r < b.length; r++) { if (!b[r].dead) b[w++] = b[r] }
1763
+ f.createForStatement(f.createVariableDeclarationList([f.createVariableDeclaration('r', undefined, undefined, f.createNumericLiteral(0))], ts.NodeFlags.Let), f.createBinaryExpression(f.createIdentifier('r'), ts.SyntaxKind.LessThanToken, f.createPropertyAccessExpression(f.createIdentifier('b'), 'length')), f.createPostfixUnaryExpression(f.createIdentifier('r'), ts.SyntaxKind.PlusPlusToken), f.createBlock([
1764
+ f.createIfStatement(f.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, f.createPropertyAccessExpression(f.createElementAccessExpression(f.createIdentifier('b'), f.createIdentifier('r')), 'dead')), f.createExpressionStatement(f.createBinaryExpression(f.createElementAccessExpression(f.createIdentifier('b'), f.createPostfixUnaryExpression(f.createIdentifier('w'), ts.SyntaxKind.PlusPlusToken)), ts.SyntaxKind.EqualsToken, f.createElementAccessExpression(f.createIdentifier('b'), f.createIdentifier('r'))))),
1765
+ ], true)),
1766
+ // b.length = w
1767
+ f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('b'), 'length'), ts.SyntaxKind.EqualsToken, f.createIdentifier('w'))),
1768
+ // p = Math.min(w, p)
1769
+ f.createExpressionStatement(f.createBinaryExpression(f.createIdentifier('p'), ts.SyntaxKind.EqualsToken, f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('Math'), 'min'), undefined, [f.createIdentifier('w'), f.createIdentifier('p')]))),
1770
+ ], true);
1771
+ const compactCondition = f.createBinaryExpression(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('b'), 'length'), ts.SyntaxKind.GreaterThanToken, f.createIdentifier('p')), ts.SyntaxKind.BarBarToken, f.createParenthesizedExpression(f.createBinaryExpression(f.createBinaryExpression(f.createIdentifier('p'), ts.SyntaxKind.GreaterThanToken, f.createNumericLiteral(0)), ts.SyntaxKind.AmpersandAmpersandToken, f.createPropertyAccessExpression(f.createElementAccessExpression(f.createIdentifier('b'), f.createNumericLiteral(0)), 'dead'))));
1772
+ phase1Stmts.push(f.createIfStatement(compactCondition, compactBody));
1773
+ // Wrap Phase 1 in mask gate
1774
+ if (structuralMask !== (0xffffffff | 0)) {
1775
+ stmts.push(f.createIfStatement(f.createBinaryExpression(f.createParenthesizedExpression(f.createBinaryExpression(f.createIdentifier('d'), ts.SyntaxKind.AmpersandToken, createMaskLiteral(f, structuralMask))), ts.SyntaxKind.ExclamationEqualsEqualsToken, f.createNumericLiteral(0)), f.createBlock(phase1Stmts, true)));
1776
+ }
1777
+ else {
1778
+ stmts.push(...phase1Stmts);
1779
+ }
1780
+ }
1781
+ // Phase 2: delegate to shared runtime — __runPhase2(s, d, b, p)
1782
+ stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__runPhase2'), undefined, [
1783
+ f.createIdentifier('s'),
1784
+ f.createIdentifier('d'),
1785
+ f.createIdentifier('b'),
1786
+ f.createIdentifier('p'),
1787
+ ])));
1788
+ return f.createBlock(stmts, true);
1789
+ }
812
1790
  function buildAccess(f, root, parts) {
813
1791
  let expr = f.createIdentifier(root);
814
1792
  for (const part of parts) {
@@ -823,8 +1801,8 @@ function buildAccess(f, root, parts) {
823
1801
  return expr;
824
1802
  }
825
1803
  // ── Pass 3: Import cleanup ───────────────────────────────────────
826
- function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, f) {
827
- if (compiled.size === 0 && !usesElTemplate && !usesElSplit && !usesMemo)
1804
+ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, f) {
1805
+ if (compiled.size === 0 && !usesElTemplate && !usesElSplit && !usesMemo && !usesApplyBinding)
828
1806
  return sf;
829
1807
  const clause = lluiImport.importClause;
830
1808
  if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
@@ -842,6 +1820,14 @@ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElT
842
1820
  if (!hasMemo && usesMemo) {
843
1821
  remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('memo')));
844
1822
  }
1823
+ if (usesApplyBinding) {
1824
+ if (!clause.namedBindings.elements.some((s) => s.name.text === '__runPhase2')) {
1825
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__runPhase2')));
1826
+ }
1827
+ if (!clause.namedBindings.elements.some((s) => s.name.text === '__handleMsg')) {
1828
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__handleMsg')));
1829
+ }
1830
+ }
845
1831
  const newBindings = f.createNamedImports(remaining);
846
1832
  const newClause = f.createImportClause(false, undefined, newBindings);
847
1833
  const newImportDecl = f.createImportDeclaration(undefined, newClause, lluiImport.moduleSpecifier);