@farm.js/react 0.1.0-beta.1 → 0.1.0-beta.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/dist/vite.mjs CHANGED
@@ -258,6 +258,31 @@ function isTextExpression(expression, statesByValue, safeGlobals) {
258
258
  function jsxAttributeName(attribute) {
259
259
  return types.isJSXIdentifier(attribute.name) ? attribute.name.name : void 0;
260
260
  }
261
+ function jsxAttribute(element, name) {
262
+ return element.openingElement.attributes.find(
263
+ (attribute) => types.isJSXAttribute(attribute) && jsxAttributeName(attribute) === name
264
+ );
265
+ }
266
+ function staticJsxAttributeString(attribute) {
267
+ if (!attribute?.value) return attribute ? "" : void 0;
268
+ if (types.isStringLiteral(attribute.value)) return attribute.value.value;
269
+ if (types.isJSXExpressionContainer(attribute.value) && types.isStringLiteral(attribute.value.expression)) {
270
+ return attribute.value.expression.value;
271
+ }
272
+ return void 0;
273
+ }
274
+ function controlledSelectHasDynamicOptions(element) {
275
+ let dynamic = false;
276
+ types.traverseFast(element, (node) => {
277
+ if (dynamic || !types.isJSXElement(node)) return;
278
+ const tag = node.openingElement.name;
279
+ if (!types.isJSXIdentifier(tag) || tag.name !== "option" && tag.name !== "optgroup") return;
280
+ dynamic = node.openingElement.attributes.some(
281
+ (attribute) => types.isJSXSpreadAttribute(attribute) || types.isJSXAttribute(attribute) && types.isJSXExpressionContainer(attribute.value) && !types.isJSXEmptyExpression(attribute.value.expression)
282
+ );
283
+ });
284
+ return dynamic;
285
+ }
261
286
  function isEmptyConditionalBranch(expression) {
262
287
  return types.isNullLiteral(expression) || types.isBooleanLiteral(expression, { value: false });
263
288
  }
@@ -483,7 +508,87 @@ function validateKeyedRowEventHandler(handler) {
483
508
  });
484
509
  return reason;
485
510
  }
486
- function analyzeKeyedRowTree(row, renderCallback, safeGlobals) {
511
+ function stabilizeDeferredEventCurrentTarget(handler, compilerSetters) {
512
+ const cloned = types.cloneNode(handler, true);
513
+ const eventParameter = cloned.params[0];
514
+ if (!types.isIdentifier(eventParameter) || compilerSetters.size === 0) return cloned;
515
+ const file = expressionFile(cloned);
516
+ const expression = file.program.body[0].expression;
517
+ if (!types.isArrowFunctionExpression(expression) && !types.isFunctionExpression(expression)) {
518
+ return cloned;
519
+ }
520
+ const rootBindingIdentifier = expression.params[0];
521
+ const snapshots = [];
522
+ const snapshotsByProperty = /* @__PURE__ */ new Map();
523
+ const replaceCurrentTarget = (path) => {
524
+ const property = path.node.property;
525
+ const isCurrentTarget = path.node.computed ? types.isStringLiteral(property, { value: "currentTarget" }) : types.isIdentifier(property, { name: "currentTarget" });
526
+ if (!isCurrentTarget || !types.isIdentifier(path.node.object, { name: eventParameter.name }))
527
+ return;
528
+ const binding = path.scope.getBinding(eventParameter.name);
529
+ if (!binding || binding.identifier !== rootBindingIdentifier) return;
530
+ const setterCall = path.findParent(
531
+ (parent2) => parent2.isCallExpression() && types.isIdentifier(parent2.node.callee) && compilerSetters.has(parent2.node.callee.name)
532
+ );
533
+ if (!setterCall) return;
534
+ const deferredFunction = path.findParent(
535
+ (parent2) => parent2.isFunction() && parent2.node !== expression
536
+ );
537
+ if (!deferredFunction || !deferredFunction.findParent((parent2) => parent2 === setterCall))
538
+ return;
539
+ const parent = path.parentPath;
540
+ const propertyRead = (parent?.isMemberExpression() || parent?.isOptionalMemberExpression()) && parent.node.object === path.node ? parent : path;
541
+ const snapshotProperty = propertyRead === path ? "element" : propertyRead.node.computed ? types.isStringLiteral(propertyRead.node.property) ? propertyRead.node.property.value : `computed${snapshots.length}` : types.isIdentifier(propertyRead.node.property) ? propertyRead.node.property.name : `property${snapshots.length}`;
542
+ let snapshot = snapshotsByProperty.get(snapshotProperty);
543
+ if (!snapshot) {
544
+ const suffix = snapshotProperty.replace(/[^A-Za-z0-9_$]/g, " ").replace(
545
+ /(?:^|\s+)([A-Za-z0-9_$])/g,
546
+ (_match, character) => character.toUpperCase()
547
+ ).replace(/\s/g, "");
548
+ snapshot = uniqueLocalIdentifier(`_farmCurrentTarget${suffix}`, [
549
+ expression,
550
+ ...snapshots.map((entry) => entry.identifier)
551
+ ]);
552
+ snapshotsByProperty.set(snapshotProperty, snapshot);
553
+ snapshots.push({
554
+ identifier: snapshot,
555
+ value: types.cloneNode(propertyRead.node, true)
556
+ });
557
+ }
558
+ propertyRead.replaceWith(types.cloneNode(snapshot));
559
+ propertyRead.skip();
560
+ };
561
+ traverse(file, {
562
+ MemberExpression: replaceCurrentTarget,
563
+ OptionalMemberExpression: replaceCurrentTarget
564
+ });
565
+ if (snapshots.length === 0) return expression;
566
+ const declarations = snapshots.map(
567
+ ({ identifier, value }) => types.variableDeclaration("const", [types.variableDeclarator(types.cloneNode(identifier), value)])
568
+ );
569
+ if (types.isBlockStatement(expression.body)) {
570
+ expression.body.body.unshift(...declarations);
571
+ } else {
572
+ expression.body = types.blockStatement([...declarations, types.returnStatement(expression.body)]);
573
+ }
574
+ return expression;
575
+ }
576
+ function stabilizeDeferredEventCurrentTargets(root, compilerSetters) {
577
+ const file = expressionFile(types.cloneNode(root, true));
578
+ traverse(file, {
579
+ JSXAttribute(path) {
580
+ if (!types.isJSXIdentifier(path.node.name) || !/^on[A-Z]/.test(path.node.name.name)) return;
581
+ const value = path.node.value;
582
+ if (!types.isJSXExpressionContainer(value) || !types.isArrowFunctionExpression(value.expression) && !types.isFunctionExpression(value.expression)) {
583
+ return;
584
+ }
585
+ value.expression = stabilizeDeferredEventCurrentTarget(value.expression, compilerSetters);
586
+ }
587
+ });
588
+ const expression = file.program.body[0].expression;
589
+ return types.isJSXElement(expression) ? expression : types.cloneNode(root, true);
590
+ }
591
+ function analyzeKeyedRowTree(row, renderCallback, safeGlobals, compilerSetters) {
487
592
  if (renderCallback.async || renderCallback.generator || renderCallback.params.length < 1 || renderCallback.params.length > 2 || !types.isIdentifier(renderCallback.params[0]) || renderCallback.params[1] !== void 0 && !types.isIdentifier(renderCallback.params[1])) {
488
593
  return { reason: "compiled keyed rows require item and optional index identifiers" };
489
594
  }
@@ -499,8 +604,25 @@ function analyzeKeyedRowTree(row, renderCallback, safeGlobals) {
499
604
  const attributeNames = new Set(
500
605
  element.openingElement.attributes.filter((attribute) => types.isJSXAttribute(attribute)).map(jsxAttributeName).filter((name) => name !== void 0)
501
606
  );
502
- if ((tag.name === "input" || tag.name === "textarea" || tag.name === "select") && (attributeNames.has("value") || attributeNames.has("checked")) && [...attributeNames].some((name) => /^on[A-Z]/.test(name))) {
503
- return "controlled form elements in interactive compiled keyed rows require React ownership";
607
+ const controlled = attributeNames.has("value") || attributeNames.has("checked");
608
+ if (attributeNames.has("contentEditable")) {
609
+ return "content-editable keyed rows require React ownership";
610
+ }
611
+ if (tag.name === "input" && controlled) {
612
+ const type = jsxAttribute(element, "type");
613
+ const staticType = staticJsxAttributeString(type);
614
+ if (type && staticType === void 0) {
615
+ return "controlled keyed row inputs require a static type";
616
+ }
617
+ if (attributeNames.has("value") && staticType?.toLowerCase() === "file") {
618
+ return "file inputs require React ownership";
619
+ }
620
+ }
621
+ if (tag.name === "textarea" && controlled && meaningfulJsxChildren(element).length > 0) {
622
+ return "controlled keyed row textareas cannot also use children";
623
+ }
624
+ if (tag.name === "select" && controlled && controlledSelectHasDynamicOptions(element)) {
625
+ return "controlled keyed row selects require static options";
504
626
  }
505
627
  for (const attribute of element.openingElement.attributes) {
506
628
  if (types.isJSXSpreadAttribute(attribute)) {
@@ -531,7 +653,7 @@ function analyzeKeyedRowTree(row, renderCallback, safeGlobals) {
531
653
  id: events.length,
532
654
  path: [...path],
533
655
  name,
534
- value: types.cloneNode(handler, true)
656
+ value: stabilizeDeferredEventCurrentTarget(handler, compilerSetters)
535
657
  });
536
658
  continue;
537
659
  }
@@ -645,26 +767,14 @@ function analyzeKeyedRowTree(row, renderCallback, safeGlobals) {
645
767
  const reason = visit(row, []);
646
768
  return reason ? { reason } : { bindings, events, conditionals };
647
769
  }
648
- function analyzeKeyedRowsContainer(container, statesByValue, safeGlobals, listNames) {
649
- if (container.openingElement.attributes.some(
650
- (attribute) => types.isJSXSpreadAttribute(attribute) || types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && (attribute.name.name === "ref" || attribute.name.name === "dangerouslySetInnerHTML")
651
- )) {
652
- return void 0;
653
- }
654
- for (const attribute of container.openingElement.attributes) {
655
- if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && types.isJSXExpressionContainer(attribute.value) && !types.isJSXEmptyExpression(attribute.value.expression) && !/^on[A-Z]/.test(attribute.name.name) && collectStateDependencies(attribute.value.expression, statesByValue).length > 0) {
656
- return void 0;
657
- }
658
- }
659
- const children = meaningfulJsxChildren(container);
660
- if (children.length !== 1) return void 0;
770
+ function analyzeKeyedRowChild(child, statesByValue, safeGlobals, listNames) {
661
771
  let collection;
662
772
  let keyCallback;
663
773
  let renderCallback;
664
774
  let row;
665
775
  let dependencies;
666
776
  let syntax;
667
- const child = children[0];
777
+ let source;
668
778
  if (types.isJSXExpressionContainer(child) && !types.isJSXEmptyExpression(child.expression) && isMapCall(child.expression)) {
669
779
  const result = analyzeMapList(child.expression, statesByValue, safeGlobals);
670
780
  if (result.reason) return void 0;
@@ -684,6 +794,7 @@ function analyzeKeyedRowsContainer(container, statesByValue, safeGlobals, listNa
684
794
  row = returned;
685
795
  dependencies = result.dependencies || [];
686
796
  syntax = "map";
797
+ source = child.expression;
687
798
  } else if (types.isJSXElement(child) && isPublicListElement(child, listNames)) {
688
799
  const result = analyzePublicList(child, statesByValue, safeGlobals);
689
800
  if (result.reason) return void 0;
@@ -701,12 +812,19 @@ function analyzeKeyedRowsContainer(container, statesByValue, safeGlobals, listNa
701
812
  row = returned;
702
813
  dependencies = result.dependencies || [];
703
814
  syntax = "list";
815
+ source = child;
704
816
  } else {
705
817
  return void 0;
706
818
  }
707
- const rowAnalysis = analyzeKeyedRowTree(row, renderCallback, safeGlobals);
819
+ const rowAnalysis = analyzeKeyedRowTree(
820
+ row,
821
+ renderCallback,
822
+ safeGlobals,
823
+ new Set([...statesByValue.values()].map((state) => state.setterName))
824
+ );
708
825
  if (rowAnalysis.reason) return void 0;
709
826
  return {
827
+ source,
710
828
  collection,
711
829
  keyCallback,
712
830
  renderCallback,
@@ -718,6 +836,76 @@ function analyzeKeyedRowsContainer(container, statesByValue, safeGlobals, listNa
718
836
  syntax
719
837
  };
720
838
  }
839
+ function analyzeKeyedRowsContainer(container, statesByValue, safeGlobals, listNames) {
840
+ if (container.openingElement.attributes.some(
841
+ (attribute) => types.isJSXSpreadAttribute(attribute) || types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && (attribute.name.name === "ref" || attribute.name.name === "dangerouslySetInnerHTML")
842
+ )) {
843
+ return void 0;
844
+ }
845
+ for (const attribute of container.openingElement.attributes) {
846
+ if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && types.isJSXExpressionContainer(attribute.value) && !types.isJSXEmptyExpression(attribute.value.expression) && !/^on[A-Z]/.test(attribute.name.name) && collectStateDependencies(attribute.value.expression, statesByValue).length > 0) {
847
+ return void 0;
848
+ }
849
+ }
850
+ const children = meaningfulJsxChildren(container);
851
+ if (children.length !== 1) return void 0;
852
+ return analyzeKeyedRowChild(children[0], statesByValue, safeGlobals, listNames);
853
+ }
854
+ function isStaticKeyedRangeSibling(element, statesByValue, safeGlobals) {
855
+ for (const child of meaningfulJsxChildren(element)) {
856
+ if (types.isJSXFragment(child)) return false;
857
+ if (types.isJSXElement(child)) {
858
+ if (!isHostElement(child) || !isStaticKeyedRangeSibling(child, statesByValue, safeGlobals)) {
859
+ return false;
860
+ }
861
+ continue;
862
+ }
863
+ if (types.isJSXExpressionContainer(child) && !types.isJSXEmptyExpression(child.expression) && !isTextExpression(child.expression, statesByValue, safeGlobals)) {
864
+ return false;
865
+ }
866
+ }
867
+ return true;
868
+ }
869
+ function analyzeKeyedRangesContainer(container, statesByValue, safeGlobals, listNames, allowSingleRange = false) {
870
+ if (container.openingElement.attributes.some(
871
+ (attribute) => types.isJSXSpreadAttribute(attribute) || types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && (attribute.name.name === "ref" || attribute.name.name === "dangerouslySetInnerHTML")
872
+ )) {
873
+ return void 0;
874
+ }
875
+ const children = meaningfulJsxChildren(container);
876
+ if (children.length < (allowSingleRange ? 1 : 2)) return void 0;
877
+ const ranges = [];
878
+ const dependencies = /* @__PURE__ */ new Set();
879
+ let staticBefore = 0;
880
+ for (const child of children) {
881
+ const range = analyzeKeyedRowChild(child, statesByValue, safeGlobals, listNames);
882
+ if (range) {
883
+ if (range.events.length > 0 || range.conditionals.length > 0) return void 0;
884
+ ranges.push({
885
+ before: staticBefore,
886
+ source: range.source,
887
+ collection: range.collection,
888
+ keyCallback: range.keyCallback,
889
+ renderCallback: range.renderCallback,
890
+ row: range.row,
891
+ bindings: range.bindings,
892
+ syntax: range.syntax
893
+ });
894
+ for (const dependency of range.dependencies) dependencies.add(dependency);
895
+ staticBefore = 0;
896
+ continue;
897
+ }
898
+ if (!types.isJSXElement(child) || !isHostElement(child)) return void 0;
899
+ if (!isStaticKeyedRangeSibling(child, statesByValue, safeGlobals)) return void 0;
900
+ staticBefore += 1;
901
+ }
902
+ if (ranges.length === 0) return void 0;
903
+ return {
904
+ ranges,
905
+ trailing: staticBefore,
906
+ dependencies: [...dependencies].sort((left, right) => left - right)
907
+ };
908
+ }
721
909
  function analyzeHostConditionalTree(branch, safeGlobals) {
722
910
  const bindings = [];
723
911
  const visit = (element, path) => {
@@ -1207,6 +1395,74 @@ function keyedRowsBoundary(blockRuntime, plan) {
1207
1395
  true
1208
1396
  );
1209
1397
  }
1398
+ function keyedRangeObject(range) {
1399
+ const parameters = range.renderCallback.params.map(
1400
+ (parameter) => types.cloneNode(parameter, true)
1401
+ );
1402
+ return types.objectExpression([
1403
+ types.objectProperty(types.identifier("before"), types.numericLiteral(range.before)),
1404
+ types.objectProperty(
1405
+ types.identifier("items"),
1406
+ types.arrowFunctionExpression([], cloneExpression(range.collection))
1407
+ ),
1408
+ types.objectProperty(types.identifier("rowKey"), types.cloneNode(range.keyCallback, true)),
1409
+ types.objectProperty(
1410
+ types.identifier("create"),
1411
+ types.arrowFunctionExpression(
1412
+ parameters.map((parameter) => types.cloneNode(parameter, true)),
1413
+ hostElementDescriptor(range.row)
1414
+ )
1415
+ ),
1416
+ types.objectProperty(
1417
+ types.identifier("bindings"),
1418
+ types.arrayExpression(
1419
+ range.bindings.map((binding) => keyedRowBindingObject(binding, parameters))
1420
+ )
1421
+ )
1422
+ ]);
1423
+ }
1424
+ function keyedRangesBoundary(blockRuntime, plan) {
1425
+ const name = types.jsxMemberExpression(
1426
+ types.jsxIdentifier(blockRuntime.name),
1427
+ types.jsxIdentifier("KeyedRanges")
1428
+ );
1429
+ const source = types.cloneNode(plan.source, true);
1430
+ let rootRef;
1431
+ source.openingElement.attributes = source.openingElement.attributes.filter((attribute) => {
1432
+ if (!types.isJSXAttribute(attribute) || jsxAttributeName(attribute) !== "ref" || !types.isJSXExpressionContainer(attribute.value) || types.isJSXEmptyExpression(attribute.value.expression)) {
1433
+ return true;
1434
+ }
1435
+ rootRef = cloneExpression(attribute.value.expression);
1436
+ return false;
1437
+ });
1438
+ return types.jsxElement(
1439
+ types.jsxOpeningElement(
1440
+ name,
1441
+ [
1442
+ types.jsxAttribute(types.jsxIdentifier("id"), types.jsxExpressionContainer(types.numericLiteral(plan.id))),
1443
+ types.jsxAttribute(
1444
+ types.jsxIdentifier("render"),
1445
+ types.jsxExpressionContainer(types.arrowFunctionExpression([], source))
1446
+ ),
1447
+ ...rootRef ? [types.jsxAttribute(types.jsxIdentifier("rootRef"), types.jsxExpressionContainer(rootRef))] : [],
1448
+ types.jsxAttribute(
1449
+ types.jsxIdentifier("ranges"),
1450
+ types.jsxExpressionContainer(
1451
+ types.arrayExpression(plan.ranges.map((range) => keyedRangeObject(range)))
1452
+ )
1453
+ ),
1454
+ types.jsxAttribute(
1455
+ types.jsxIdentifier("trailing"),
1456
+ types.jsxExpressionContainer(types.numericLiteral(plan.trailing))
1457
+ )
1458
+ ],
1459
+ true
1460
+ ),
1461
+ null,
1462
+ [],
1463
+ true
1464
+ );
1465
+ }
1210
1466
  function hostConditionalBranchObject(branch, bindings) {
1211
1467
  return types.objectExpression([
1212
1468
  types.objectProperty(
@@ -1419,6 +1675,23 @@ function analyzeComposableBlocks(root, statesByValue, safeGlobals, listNames, al
1419
1675
  }
1420
1676
  if (types.isJSXElement(child)) {
1421
1677
  if (isHostElement(child)) {
1678
+ const keyedRanges = insideConditional ? void 0 : analyzeKeyedRangesContainer(child, statesByValue, safeGlobals, listNames);
1679
+ if (keyedRanges) {
1680
+ plans.push({
1681
+ kind: "keyed-ranges",
1682
+ id: nextId++,
1683
+ parent,
1684
+ dependencies: keyedRanges.dependencies,
1685
+ source: child,
1686
+ ranges: keyedRanges.ranges,
1687
+ trailing: keyedRanges.trailing
1688
+ });
1689
+ for (const range of keyedRanges.ranges) {
1690
+ if (types.isJSXElement(range.source)) ownedElements.add(range.source);
1691
+ else keyedExpressions.add(range.source);
1692
+ }
1693
+ continue;
1694
+ }
1422
1695
  const hostConditional = analyzeHostConditionalContainer(
1423
1696
  child,
1424
1697
  statesByValue,
@@ -1551,6 +1824,28 @@ function analyzeComposableBlocks(root, statesByValue, safeGlobals, listNames, al
1551
1824
  }
1552
1825
  return { dependencies };
1553
1826
  };
1827
+ const rootRanges = analyzeKeyedRangesContainer(root, statesByValue, safeGlobals, listNames, true);
1828
+ if (rootRanges) {
1829
+ plans.push({
1830
+ kind: "keyed-ranges",
1831
+ id: nextId++,
1832
+ dependencies: rootRanges.dependencies,
1833
+ source: root,
1834
+ ranges: rootRanges.ranges,
1835
+ trailing: rootRanges.trailing
1836
+ });
1837
+ for (const range of rootRanges.ranges) {
1838
+ if (types.isJSXElement(range.source)) ownedElements.add(range.source);
1839
+ else keyedExpressions.add(range.source);
1840
+ }
1841
+ return {
1842
+ plans,
1843
+ componentElements,
1844
+ conditionalExpressions,
1845
+ ownedElements,
1846
+ keyedExpressions
1847
+ };
1848
+ }
1554
1849
  const result = visitHost(root, void 0, false);
1555
1850
  if (result.reason) return { reason: result.reason };
1556
1851
  return {
@@ -1587,6 +1882,10 @@ function lowerComposableBlocks(root, plans, blockRuntime, listNames) {
1587
1882
  const planBySource = new Map(
1588
1883
  plans.map((plan) => [plan.source, plan])
1589
1884
  );
1885
+ const rootPlan = planBySource.get(root);
1886
+ if (rootPlan?.kind === "keyed-ranges") {
1887
+ return keyedRangesBoundary(blockRuntime, rootPlan);
1888
+ }
1590
1889
  const visit = (element) => {
1591
1890
  element.children = element.children.map((child) => {
1592
1891
  if (types.isJSXElement(child)) {
@@ -1597,6 +1896,9 @@ function lowerComposableBlocks(root, plans, blockRuntime, listNames) {
1597
1896
  if (plan2?.kind === "keyed-rows") {
1598
1897
  return keyedRowsBoundary(blockRuntime, plan2);
1599
1898
  }
1899
+ if (plan2?.kind === "keyed-ranges") {
1900
+ return keyedRangesBoundary(blockRuntime, plan2);
1901
+ }
1600
1902
  if (plan2?.kind === "component") {
1601
1903
  return componentIslandBoundary(blockRuntime, plan2, child);
1602
1904
  }
@@ -2030,11 +2332,15 @@ function compileCandidate(candidate, createComponentIdentifier, useStateNames, r
2030
2332
  const handlerRewrite = rewriteHandlerAccess(rootWithDerived, handlersByName);
2031
2333
  if (handlerRewrite.reason) return handlerRewrite.reason;
2032
2334
  if (!handlerRewrite.root) return "event handlers could not be prepared safely";
2033
- const expandedRoot = rewriteDestructuredPropAccess(
2335
+ const expandedRootWithDeferredEvents = rewriteDestructuredPropAccess(
2034
2336
  handlerRewrite.root,
2035
2337
  propsPlan.destructuredNames,
2036
2338
  propsPlan.definitionParameter
2037
2339
  );
2340
+ const expandedRoot = stabilizeDeferredEventCurrentTargets(
2341
+ expandedRootWithDeferredEvents,
2342
+ new Set(states.map((state) => state.setterName))
2343
+ );
2038
2344
  const setterReason = validateSetterUsage(expandedRoot, statesBySetter);
2039
2345
  if (setterReason) return setterReason;
2040
2346
  const referencedComponentNames = /* @__PURE__ */ new Set();