what-compiler 0.6.0 → 0.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-compiler",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "JSX compiler for What Framework - transforms JSX to optimized DOM operations",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -642,6 +642,28 @@ export default function whatBabelPlugin({ types: t }) {
642
642
 
643
643
  const attrName = getAttrName(attr);
644
644
 
645
+ // Ref handling — assign element to ref object/callback
646
+ if (attrName === 'ref') {
647
+ const refExpr = getAttributeValue(attr.value);
648
+ // Generate: typeof ref === 'function' ? ref(el) : ref.current = el
649
+ statements.push(
650
+ t.expressionStatement(
651
+ t.conditionalExpression(
652
+ t.binaryExpression('===',
653
+ t.unaryExpression('typeof', refExpr),
654
+ t.stringLiteral('function')
655
+ ),
656
+ t.callExpression(t.cloneNode(refExpr), [t.identifier(elId)]),
657
+ t.assignmentExpression('=',
658
+ t.memberExpression(t.cloneNode(refExpr), t.identifier('current')),
659
+ t.identifier(elId)
660
+ )
661
+ )
662
+ )
663
+ );
664
+ continue;
665
+ }
666
+
645
667
  // Event handlers
646
668
  if (attrName.startsWith('on') && !attrName.includes('|')) {
647
669
  const event = attrName.slice(2).toLowerCase();
@@ -805,6 +827,12 @@ export default function whatBabelPlugin({ types: t }) {
805
827
  }
806
828
 
807
829
  function applyDynamicChildren(statements, elId, children, parentNode, state) {
830
+ // Two-pass approach: first collect all children needing DOM references,
831
+ // then pre-capture markers before any _$insert() calls shift indices.
832
+ // This fixes issue #1: childNodes index shifting with multiple dynamic children.
833
+
834
+ // --- Pass 1: Scan children and collect entries ---
835
+ const entries = [];
808
836
  let childIndex = 0;
809
837
 
810
838
  for (const child of children) {
@@ -816,9 +844,73 @@ export default function whatBabelPlugin({ types: t }) {
816
844
 
817
845
  if (t.isJSXExpressionContainer(child)) {
818
846
  if (t.isJSXEmptyExpression(child.expression)) continue;
847
+ entries.push({ type: 'expression', child, childIndex });
848
+ childIndex++;
849
+ continue;
850
+ }
851
+
852
+ if (t.isJSXElement(child)) {
853
+ const childTag = child.openingElement.name.name;
854
+ if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
855
+ entries.push({ type: 'component', child, childIndex });
856
+ childIndex++;
857
+ } else {
858
+ const hasAnythingDynamic = child.openingElement.attributes.some(isDynamicAttr) ||
859
+ child.openingElement.attributes.some(a => !t.isJSXSpreadAttribute(a) && getAttrName(a)?.startsWith('on')) ||
860
+ !child.children.every(isStaticChild);
861
+
862
+ entries.push({ type: 'static', child, childIndex, hasAnythingDynamic });
863
+ childIndex++;
864
+ }
865
+ continue;
866
+ }
867
+
868
+ if (t.isJSXFragment(child)) {
869
+ entries.push({ type: 'fragment', child });
870
+ }
871
+ }
872
+
873
+ // --- Pre-capture marker references if needed ---
874
+ // When there are multiple entries needing DOM refs and at least one _$insert(),
875
+ // capture all markers upfront to avoid index shifting after DOM mutations.
876
+ const entriesNeedingRef = entries.filter(e =>
877
+ e.type === 'expression' || e.type === 'component' ||
878
+ (e.type === 'static' && e.hasAnythingDynamic)
879
+ );
880
+ const hasDynamicInsert = entries.some(e => e.type === 'expression' || e.type === 'component');
881
+ const needsPreCapture = entriesNeedingRef.length >= 2 && hasDynamicInsert;
882
+
883
+ const markerVars = new Map(); // childIndex → variable name
884
+ if (needsPreCapture) {
885
+ for (const entry of entriesNeedingRef) {
886
+ const varName = `_m$${entry.childIndex}`;
887
+ // Use a unique name to avoid collisions with element vars
888
+ const markerVar = state.nextVarId();
889
+ markerVars.set(entry.childIndex, markerVar);
890
+ statements.push(
891
+ t.variableDeclaration('const', [
892
+ t.variableDeclarator(
893
+ t.identifier(markerVar),
894
+ buildChildAccess(elId, entry.childIndex)
895
+ )
896
+ ])
897
+ );
898
+ }
899
+ }
819
900
 
820
- const expr = child.expression;
821
- const marker = buildChildAccess(elId, childIndex);
901
+ // Helper: get a marker reference (pre-captured var or inline access)
902
+ function getMarker(idx) {
903
+ if (markerVars.has(idx)) {
904
+ return t.identifier(markerVars.get(idx));
905
+ }
906
+ return buildChildAccess(elId, idx);
907
+ }
908
+
909
+ // --- Pass 2: Generate code using stable references ---
910
+ for (const entry of entries) {
911
+ if (entry.type === 'expression') {
912
+ const expr = entry.child.expression;
913
+ const marker = getMarker(entry.childIndex);
822
914
  state.needsInsert = true;
823
915
 
824
916
  if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
@@ -827,7 +919,6 @@ export default function whatBabelPlugin({ types: t }) {
827
919
  t.arrowFunctionExpression([], expr),
828
920
  marker
829
921
  ]);
830
- // In dev mode, add a leading comment when the reactive wrapping is uncertain
831
922
  if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
832
923
  t.addComment(insertCall, 'leading',
833
924
  ' @what-dev: reactive wrapping may be unnecessary — expression contains a non-signal function call with reactive args ',
@@ -846,53 +937,48 @@ export default function whatBabelPlugin({ types: t }) {
846
937
  )
847
938
  );
848
939
  }
849
- childIndex++;
850
940
  continue;
851
941
  }
852
942
 
853
- if (t.isJSXElement(child)) {
854
- const childTag = child.openingElement.name.name;
855
- if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
856
- const transformed = transformElementFineGrained({ node: child }, state);
857
- const marker = buildChildAccess(elId, childIndex);
858
- state.needsInsert = true;
943
+ if (entry.type === 'component') {
944
+ const transformed = transformElementFineGrained({ node: entry.child }, state);
945
+ const marker = getMarker(entry.childIndex);
946
+ state.needsInsert = true;
947
+ statements.push(
948
+ t.expressionStatement(
949
+ t.callExpression(t.identifier('_$insert'), [
950
+ t.identifier(elId),
951
+ transformed,
952
+ marker
953
+ ])
954
+ )
955
+ );
956
+ continue;
957
+ }
958
+
959
+ if (entry.type === 'static' && entry.hasAnythingDynamic) {
960
+ // Static child with dynamic content — get element reference
961
+ let childElRef;
962
+ if (markerVars.has(entry.childIndex)) {
963
+ childElRef = markerVars.get(entry.childIndex);
964
+ } else {
965
+ childElRef = state.nextVarId();
859
966
  statements.push(
860
- t.expressionStatement(
861
- t.callExpression(t.identifier('_$insert'), [
862
- t.identifier(elId),
863
- transformed,
864
- marker
865
- ])
866
- )
967
+ t.variableDeclaration('const', [
968
+ t.variableDeclarator(
969
+ t.identifier(childElRef),
970
+ buildChildAccess(elId, entry.childIndex)
971
+ )
972
+ ])
867
973
  );
868
- childIndex++;
869
- } else {
870
- // Static child element — already in template
871
- // But check if it has dynamic children/attrs that need effects
872
- const hasAnythingDynamic = child.openingElement.attributes.some(isDynamicAttr) ||
873
- child.openingElement.attributes.some(a => !t.isJSXSpreadAttribute(a) && getAttrName(a)?.startsWith('on')) ||
874
- !child.children.every(isStaticChild);
875
-
876
- if (hasAnythingDynamic) {
877
- const childElId = state.nextVarId();
878
- statements.push(
879
- t.variableDeclaration('const', [
880
- t.variableDeclarator(
881
- t.identifier(childElId),
882
- buildChildAccess(elId, childIndex)
883
- )
884
- ])
885
- );
886
- applyDynamicAttrs(statements, childElId, child.openingElement.attributes, state);
887
- applyDynamicChildren(statements, childElId, child.children, child, state);
888
- }
889
- childIndex++;
890
974
  }
975
+ applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state);
976
+ applyDynamicChildren(statements, childElRef, entry.child.children, entry.child, state);
891
977
  continue;
892
978
  }
893
979
 
894
- if (t.isJSXFragment(child)) {
895
- for (const fChild of child.children) {
980
+ if (entry.type === 'fragment') {
981
+ for (const fChild of entry.child.children) {
896
982
  if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
897
983
  state.needsInsert = true;
898
984
  const expr = fChild.expression;