react-native-boost 1.1.0 → 1.2.0

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/README.md CHANGED
@@ -20,10 +20,13 @@ The documentation is available at [react-native-boost.oss.kuatsu.de](https://rea
20
20
  The app in the `apps/example` directory serves as a benchmark for the performance of the plugin.
21
21
 
22
22
  <div align="center">
23
- <img src="./apps/docs/content/docs/information/img/benchmark-ios.png" width="500" />
23
+ <picture>
24
+ <source media="(prefers-color-scheme: dark)" srcset="./apps/docs/content/docs/information/img/fps-ios.svg" />
25
+ <img alt="React Native Boost — iOS frame rate vs render load" src="./apps/docs/content/docs/information/img/fps-ios-light.svg" width="640" />
26
+ </picture>
24
27
  </div>
25
28
 
26
- More benchmarks are available in the [docs](https://react-native-boost.oss.kuatsu.de/docs/information/benchmarks).
29
+ See the [benchmarks page](https://react-native-boost.oss.kuatsu.de/docs/information/benchmarks) for Android results and the full methodology.
27
30
 
28
31
  ## Compatibility
29
32
 
@@ -125,10 +125,7 @@ const isReactNativeImport = (path, expectedImportedName) => {
125
125
  }
126
126
  return false;
127
127
  };
128
- const getViewAncestorClassification = (path) => {
129
- return classifyViewAncestors(path);
130
- };
131
- function classifyViewAncestors(path) {
128
+ const getAncestorClassification = (path) => {
132
129
  const context = {
133
130
  componentCache: /* @__PURE__ */ new WeakMap(),
134
131
  componentInProgress: /* @__PURE__ */ new WeakSet(),
@@ -145,7 +142,21 @@ function classifyViewAncestors(path) {
145
142
  ancestorPath = ancestorPath.parentPath;
146
143
  }
147
144
  return classification;
148
- }
145
+ };
146
+ const ancestorBailoutChecks = (path, dangerousOptimizationEnabled) => {
147
+ let classification;
148
+ const classify = () => classification != null ? classification : classification = getAncestorClassification(path);
149
+ return [
150
+ {
151
+ reason: "has Text ancestor",
152
+ shouldBail: () => classify() === "text"
153
+ },
154
+ {
155
+ reason: "has unresolved ancestor and dangerous optimization is disabled",
156
+ shouldBail: () => classify() === "unknown" && !dangerousOptimizationEnabled
157
+ }
158
+ ];
159
+ };
149
160
  function classifyJSXElementAsAncestor(path, context) {
150
161
  const openingElementName = path.node.openingElement.name;
151
162
  if (types.isJSXIdentifier(openingElementName)) {
@@ -673,18 +684,37 @@ function extractSelectableAndUpdateStyle(styleExpr) {
673
684
  }
674
685
  return void 0;
675
686
  }
676
- const isStringNode = (path, child) => {
677
- if (types.isJSXText(child) || types.isStringLiteral(child)) return true;
678
- if (types.isJSXExpressionContainer(child)) {
679
- const expression = child.expression;
680
- if (types.isIdentifier(expression)) {
681
- const binding = path.scope.getBinding(expression.name);
682
- if (binding && binding.path.node && types.isVariableDeclarator(binding.path.node)) {
683
- return !!binding.path.node.init && types.isStringLiteral(binding.path.node.init);
684
- }
685
- return false;
687
+ const isPrimitiveExpression = (path, expression, resolved = /* @__PURE__ */ new Set()) => {
688
+ if (types.isStringLiteral(expression) || types.isNumericLiteral(expression)) return true;
689
+ if (types.isTemplateLiteral(expression)) return true;
690
+ if (types.isBinaryExpression(expression)) {
691
+ const { left, right } = expression;
692
+ return types.isExpression(left) && isPrimitiveExpression(path, left, resolved) && isPrimitiveExpression(path, right, resolved);
693
+ }
694
+ if (types.isConditionalExpression(expression)) {
695
+ return isPrimitiveExpression(path, expression.consequent, resolved) && isPrimitiveExpression(path, expression.alternate, resolved);
696
+ }
697
+ if (types.isLogicalExpression(expression)) {
698
+ return isPrimitiveExpression(path, expression.left, resolved) && isPrimitiveExpression(path, expression.right, resolved);
699
+ }
700
+ if (types.isIdentifier(expression)) {
701
+ if (resolved.has(expression.name)) return false;
702
+ const binding = path.scope.getBinding(expression.name);
703
+ if (binding && binding.constant && binding.path.node && types.isVariableDeclarator(binding.path.node)) {
704
+ const init = binding.path.node.init;
705
+ return !!init && types.isExpression(init) && isPrimitiveExpression(path, init, new Set(resolved).add(expression.name));
686
706
  }
687
- if (types.isStringLiteral(expression)) return true;
707
+ return false;
708
+ }
709
+ return false;
710
+ };
711
+ const isPrimitiveChild = (path, child) => {
712
+ if (types.isJSXText(child)) return true;
713
+ if (types.isStringLiteral(child)) return true;
714
+ if (types.isJSXExpressionContainer(child)) {
715
+ const { expression } = child;
716
+ if (types.isJSXEmptyExpression(expression)) return false;
717
+ return isPrimitiveExpression(path, expression);
688
718
  }
689
719
  return false;
690
720
  };
@@ -723,6 +753,10 @@ const replaceWithNativeComponent = (path, parent, file, nativeComponentName) =>
723
753
  };
724
754
 
725
755
  const textBlacklistedProperties = /* @__PURE__ */ new Set([
756
+ // The `Text` wrapper translates `aria-hidden` into `accessibilityElementsHidden` /
757
+ // `importantForAccessibility`, which `processAccessibilityProps` does not yet handle. Passing it
758
+ // through would drop it, so bail. TODO: handle this in the runtime helper instead.
759
+ "aria-hidden",
726
760
  "id",
727
761
  "nativeID",
728
762
  "onLongPress",
@@ -740,7 +774,9 @@ const textBlacklistedProperties = /* @__PURE__ */ new Set([
740
774
  "selectionColor"
741
775
  // TODO: we can use react-native's internal `processColor` to process this at runtime
742
776
  ]);
743
- const textOptimizer = (path, logger) => {
777
+ const NORMALIZED_PROPERTIES = /* @__PURE__ */ new Set([...ACCESSIBILITY_PROPERTIES, "disabled"]);
778
+ const isNormalizedProperty = (attribute) => types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && NORMALIZED_PROPERTIES.has(attribute.name.name);
779
+ const textOptimizer = (path, logger, options, platform) => {
744
780
  if (!isValidJSXComponent(path, "Text")) return;
745
781
  if (!isReactNativeImport(path, "Text")) return;
746
782
  const parent = path.parent;
@@ -754,10 +790,13 @@ const textOptimizer = (path, logger) => {
754
790
  reason: "is a direct child of expo-router Link with asChild",
755
791
  shouldBail: () => hasExpoRouterLinkParentWithAsChild(path)
756
792
  },
793
+ // The local children check runs before the ancestor checks because it is cheap and prunes the
794
+ // common nested-element `Text` before the unbounded ancestor walk those checks trigger.
757
795
  {
758
- reason: "contains non-string children",
796
+ reason: "contains non-primitive children",
759
797
  shouldBail: () => hasInvalidChildren(path, parent)
760
- }
798
+ },
799
+ ...ancestorBailoutChecks(path, (options == null ? void 0 : options.dangerouslyOptimizeTextWithUnknownAncestors) === true)
761
800
  ];
762
801
  if (forced) {
763
802
  const overriddenReason = getFirstBailoutReason(overridableChecks);
@@ -789,18 +828,18 @@ const textOptimizer = (path, logger) => {
789
828
  fixNegativeNumberOfLines({ path, logger });
790
829
  addDefaultProperty(path, "allowFontScaling", types.booleanLiteral(true));
791
830
  addDefaultProperty(path, "ellipsizeMode", types.stringLiteral("tail"));
792
- processProps(path, file);
831
+ processProps(path, file, platform);
793
832
  replaceWithNativeComponent(path, parent, file, "NativeText");
794
833
  };
795
834
  function hasInvalidChildren(path, parent) {
796
835
  for (const attribute of path.node.attributes) {
797
836
  if (types.isJSXSpreadAttribute(attribute)) continue;
798
- if (types.isJSXIdentifier(attribute.name) && attribute.value && // For a "children" attribute, optimization is allowed only if it is a string
799
- attribute.name.name === "children" && !isStringNode(path, attribute.value)) {
837
+ if (types.isJSXIdentifier(attribute.name) && attribute.value && // For a "children" attribute, optimization is allowed only if it is a provable primitive
838
+ attribute.name.name === "children" && !isPrimitiveChild(path, attribute.value)) {
800
839
  return true;
801
840
  }
802
841
  }
803
- return !parent.children.every((child) => isStringNode(path, child));
842
+ return !parent.children.every((child) => isPrimitiveChild(path, child));
804
843
  }
805
844
  function fixNegativeNumberOfLines({ path, logger }) {
806
845
  for (const attribute of path.node.attributes) {
@@ -822,16 +861,15 @@ function fixNegativeNumberOfLines({ path, logger }) {
822
861
  }
823
862
  }
824
863
  }
825
- function processProps(path, file) {
864
+ function processProps(path, file, platform) {
826
865
  const currentAttributes = [...path.node.attributes];
827
866
  const { styleExpr, styleAttribute } = extractStyleAttribute(currentAttributes);
828
- const hasA11y = hasAccessibilityProperty(path, currentAttributes);
867
+ const shouldNormalize = hasAccessibilityProperty(path, currentAttributes) || currentAttributes.some(
868
+ (attribute) => types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "disabled" })
869
+ );
829
870
  const spreadAttributes = [];
830
- if (hasA11y) {
831
- const accessibilityAttributes = currentAttributes.filter((attribute) => {
832
- if (!types.isJSXAttribute(attribute)) return false;
833
- return types.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name);
834
- });
871
+ if (shouldNormalize) {
872
+ const normalizedAttributes = currentAttributes.filter((attribute) => isNormalizedProperty(attribute));
835
873
  const normalizeIdentifier = addFileImportHint({
836
874
  file,
837
875
  nameHint: "processAccessibilityProps",
@@ -839,7 +877,7 @@ function processProps(path, file) {
839
877
  importName: "processAccessibilityProps",
840
878
  moduleName: RUNTIME_MODULE_NAME
841
879
  });
842
- const accessibilityObject = buildPropertiesFromAttributes(accessibilityAttributes);
880
+ const accessibilityObject = buildPropertiesFromAttributes(normalizedAttributes);
843
881
  const accessibilityExpr = types.callExpression(types.identifier(normalizeIdentifier.name), [accessibilityObject]);
844
882
  spreadAttributes.push(types.jsxSpreadAttribute(accessibilityExpr));
845
883
  }
@@ -865,15 +903,31 @@ function processProps(path, file) {
865
903
  const remainingAttributes = [];
866
904
  for (const attribute of currentAttributes) {
867
905
  if (styleAttribute && attribute === styleAttribute) continue;
868
- if (hasA11y && types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name)) {
869
- continue;
870
- }
906
+ if (shouldNormalize && isNormalizedProperty(attribute)) continue;
871
907
  remainingAttributes.push(attribute);
872
908
  }
873
- path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes].filter(
909
+ const accessibleAttribute = shouldNormalize ? void 0 : buildAccessibleDefault(path, file, platform);
910
+ path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes, accessibleAttribute].filter(
874
911
  (attribute) => attribute !== void 0
875
912
  );
876
913
  }
914
+ function buildAccessibleDefault(path, file, platform) {
915
+ if (platform === "web") return void 0;
916
+ let value;
917
+ if (platform === "ios" || platform === "android") {
918
+ value = types.booleanLiteral(platform === "ios");
919
+ } else {
920
+ const accessibleIdentifier = addFileImportHint({
921
+ file,
922
+ nameHint: "getDefaultTextAccessible",
923
+ path,
924
+ importName: "getDefaultTextAccessible",
925
+ moduleName: RUNTIME_MODULE_NAME
926
+ });
927
+ value = types.callExpression(types.identifier(accessibleIdentifier.name), []);
928
+ }
929
+ return types.jsxAttribute(types.jsxIdentifier("accessible"), types.jsxExpressionContainer(value));
930
+ }
877
931
 
878
932
  const LOG_PREFIX = "[react-native-boost]";
879
933
  const ANSI_RESET = "\x1B[0m";
@@ -972,7 +1026,9 @@ function formatPathLocation(payloadPath) {
972
1026
  }
973
1027
 
974
1028
  const viewBlacklistedProperties = /* @__PURE__ */ new Set([
975
- // TODO: process a11y props at runtime
1029
+ // The `View` wrapper translates these into native props (e.g. `aria-*` → `accessibility*`,
1030
+ // `tabIndex` → `focusable`). The native host does not understand them, so passing them through
1031
+ // would silently drop them. TODO: process these at runtime instead of bailing.
976
1032
  "accessible",
977
1033
  "accessibilityLabel",
978
1034
  "accessibilityState",
@@ -980,37 +1036,29 @@ const viewBlacklistedProperties = /* @__PURE__ */ new Set([
980
1036
  "aria-checked",
981
1037
  "aria-disabled",
982
1038
  "aria-expanded",
1039
+ "aria-hidden",
983
1040
  "aria-label",
1041
+ "aria-labelledby",
1042
+ "aria-live",
984
1043
  "aria-selected",
1044
+ "aria-valuemax",
1045
+ "aria-valuemin",
1046
+ "aria-valuenow",
1047
+ "aria-valuetext",
985
1048
  "id",
986
1049
  "nativeID",
987
- "style"
988
- // TODO: process style at runtime
1050
+ "tabIndex"
989
1051
  ]);
990
1052
  const viewOptimizer = (path, logger, options) => {
991
1053
  if (!isValidJSXComponent(path, "View")) return;
992
1054
  if (!isReactNativeImport(path, "View")) return;
993
- let ancestorClassification;
994
- const getAncestorClassification = () => {
995
- if (!ancestorClassification) {
996
- ancestorClassification = getViewAncestorClassification(path);
997
- }
998
- return ancestorClassification;
999
- };
1000
1055
  const forced = isForcedLine(path);
1001
1056
  const overridableChecks = [
1002
1057
  {
1003
1058
  reason: "contains blacklisted props",
1004
1059
  shouldBail: () => hasBlacklistedProperty(path, viewBlacklistedProperties)
1005
1060
  },
1006
- {
1007
- reason: "has Text ancestor",
1008
- shouldBail: () => getAncestorClassification() === "text"
1009
- },
1010
- {
1011
- reason: "has unresolved ancestor and dangerous optimization is disabled",
1012
- shouldBail: () => getAncestorClassification() === "unknown" && (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) !== true
1013
- }
1061
+ ...ancestorBailoutChecks(path, (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) === true)
1014
1062
  ];
1015
1063
  if (forced) {
1016
1064
  const overriddenReason = getFirstBailoutReason(overridableChecks);
@@ -1045,6 +1093,7 @@ const viewOptimizer = (path, logger, options) => {
1045
1093
 
1046
1094
  var index = declare((api) => {
1047
1095
  api.assertVersion(7);
1096
+ const platform = api.caller((caller) => caller == null ? void 0 : caller.platform);
1048
1097
  return {
1049
1098
  name: "react-native-boost",
1050
1099
  visitor: {
@@ -1054,7 +1103,7 @@ var index = declare((api) => {
1054
1103
  const options = (_a = pluginState.opts) != null ? _a : {};
1055
1104
  const logger = getOrCreateLogger(pluginState, options);
1056
1105
  if (isIgnoredFile(path, (_b = options.ignores) != null ? _b : [])) return;
1057
- if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger);
1106
+ if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger, options, platform);
1058
1107
  if (((_d = options.optimizations) == null ? void 0 : _d.view) !== false) viewOptimizer(path, logger, options);
1059
1108
  }
1060
1109
  }