what-compiler 0.6.0 → 0.6.2

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