@llui/vite-plugin 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/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +991 -5
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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);
|