react-native-boost 1.0.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.
@@ -42,17 +42,23 @@ const isIgnoredFile = (path, ignores) => {
42
42
  }
43
43
  return false;
44
44
  };
45
+ const isForcedLine = (path) => {
46
+ return hasDecoratorComment(path, "@boost-force");
47
+ };
45
48
  const isIgnoredLine = (path) => {
49
+ return hasDecoratorComment(path, "@boost-ignore");
50
+ };
51
+ function hasDecoratorComment(path, decorator) {
46
52
  var _a, _b, _c;
47
- if ((_a = path.node.leadingComments) == null ? void 0 : _a.some((comment) => comment.value.includes("@boost-ignore"))) {
53
+ if ((_a = path.node.leadingComments) == null ? void 0 : _a.some((comment) => comment.value.includes(decorator))) {
48
54
  return true;
49
55
  }
50
56
  const jsxElementPath = path.parentPath;
51
- if ((_b = jsxElementPath.node.leadingComments) == null ? void 0 : _b.some((comment) => comment.value.includes("@boost-ignore"))) {
57
+ if ((_b = jsxElementPath.node.leadingComments) == null ? void 0 : _b.some((comment) => comment.value.includes(decorator))) {
52
58
  return true;
53
59
  }
54
60
  const propertyPath = jsxElementPath.parentPath;
55
- if (propertyPath && propertyPath.isObjectProperty() && ((_c = propertyPath.node.leadingComments) == null ? void 0 : _c.some((comment) => comment.value.includes("@boost-ignore")))) {
61
+ if (propertyPath && propertyPath.isObjectProperty() && ((_c = propertyPath.node.leadingComments) == null ? void 0 : _c.some((comment) => comment.value.includes(decorator)))) {
56
62
  return true;
57
63
  }
58
64
  if (!jsxElementPath.parentPath) return false;
@@ -73,18 +79,18 @@ const isIgnoredLine = (path) => {
73
79
  ...expression.node.trailingComments || [],
74
80
  ...expression.node.innerComments || []
75
81
  ].map((comment) => comment.value.trim());
76
- if (comments.some((comment) => comment.includes("@boost-ignore"))) {
82
+ if (comments.some((comment) => comment.includes(decorator))) {
77
83
  return true;
78
84
  }
79
85
  }
80
86
  }
81
- if (sibling.node.leadingComments && sibling.node.leadingComments.some((comment) => comment.value.includes("@boost-ignore"))) {
87
+ if (sibling.node.leadingComments && sibling.node.leadingComments.some((comment) => comment.value.includes(decorator))) {
82
88
  return true;
83
89
  }
84
90
  break;
85
91
  }
86
92
  return false;
87
- };
93
+ }
88
94
  const isValidJSXComponent = (path, componentName) => {
89
95
  if (!core.types.isJSXIdentifier(path.node.name)) return false;
90
96
  const parent = path.parent;
@@ -121,10 +127,7 @@ const isReactNativeImport = (path, expectedImportedName) => {
121
127
  }
122
128
  return false;
123
129
  };
124
- const getViewAncestorClassification = (path) => {
125
- return classifyViewAncestors(path);
126
- };
127
- function classifyViewAncestors(path) {
130
+ const getAncestorClassification = (path) => {
128
131
  const context = {
129
132
  componentCache: /* @__PURE__ */ new WeakMap(),
130
133
  componentInProgress: /* @__PURE__ */ new WeakSet(),
@@ -141,7 +144,21 @@ function classifyViewAncestors(path) {
141
144
  ancestorPath = ancestorPath.parentPath;
142
145
  }
143
146
  return classification;
144
- }
147
+ };
148
+ const ancestorBailoutChecks = (path, dangerousOptimizationEnabled) => {
149
+ let classification;
150
+ const classify = () => classification != null ? classification : classification = getAncestorClassification(path);
151
+ return [
152
+ {
153
+ reason: "has Text ancestor",
154
+ shouldBail: () => classify() === "text"
155
+ },
156
+ {
157
+ reason: "has unresolved ancestor and dangerous optimization is disabled",
158
+ shouldBail: () => classify() === "unknown" && !dangerousOptimizationEnabled
159
+ }
160
+ ];
161
+ };
145
162
  function classifyJSXElementAsAncestor(path, context) {
146
163
  const openingElementName = path.node.openingElement.name;
147
164
  if (core.types.isJSXIdentifier(openingElementName)) {
@@ -669,18 +686,37 @@ function extractSelectableAndUpdateStyle(styleExpr) {
669
686
  }
670
687
  return void 0;
671
688
  }
672
- const isStringNode = (path, child) => {
673
- if (core.types.isJSXText(child) || core.types.isStringLiteral(child)) return true;
674
- if (core.types.isJSXExpressionContainer(child)) {
675
- const expression = child.expression;
676
- if (core.types.isIdentifier(expression)) {
677
- const binding = path.scope.getBinding(expression.name);
678
- if (binding && binding.path.node && core.types.isVariableDeclarator(binding.path.node)) {
679
- return !!binding.path.node.init && core.types.isStringLiteral(binding.path.node.init);
680
- }
681
- return false;
689
+ const isPrimitiveExpression = (path, expression, resolved = /* @__PURE__ */ new Set()) => {
690
+ if (core.types.isStringLiteral(expression) || core.types.isNumericLiteral(expression)) return true;
691
+ if (core.types.isTemplateLiteral(expression)) return true;
692
+ if (core.types.isBinaryExpression(expression)) {
693
+ const { left, right } = expression;
694
+ return core.types.isExpression(left) && isPrimitiveExpression(path, left, resolved) && isPrimitiveExpression(path, right, resolved);
695
+ }
696
+ if (core.types.isConditionalExpression(expression)) {
697
+ return isPrimitiveExpression(path, expression.consequent, resolved) && isPrimitiveExpression(path, expression.alternate, resolved);
698
+ }
699
+ if (core.types.isLogicalExpression(expression)) {
700
+ return isPrimitiveExpression(path, expression.left, resolved) && isPrimitiveExpression(path, expression.right, resolved);
701
+ }
702
+ if (core.types.isIdentifier(expression)) {
703
+ if (resolved.has(expression.name)) return false;
704
+ const binding = path.scope.getBinding(expression.name);
705
+ if (binding && binding.constant && binding.path.node && core.types.isVariableDeclarator(binding.path.node)) {
706
+ const init = binding.path.node.init;
707
+ return !!init && core.types.isExpression(init) && isPrimitiveExpression(path, init, new Set(resolved).add(expression.name));
682
708
  }
683
- if (core.types.isStringLiteral(expression)) return true;
709
+ return false;
710
+ }
711
+ return false;
712
+ };
713
+ const isPrimitiveChild = (path, child) => {
714
+ if (core.types.isJSXText(child)) return true;
715
+ if (core.types.isStringLiteral(child)) return true;
716
+ if (core.types.isJSXExpressionContainer(child)) {
717
+ const { expression } = child;
718
+ if (core.types.isJSXEmptyExpression(expression)) return false;
719
+ return isPrimitiveExpression(path, expression);
684
720
  }
685
721
  return false;
686
722
  };
@@ -719,6 +755,10 @@ const replaceWithNativeComponent = (path, parent, file, nativeComponentName) =>
719
755
  };
720
756
 
721
757
  const textBlacklistedProperties = /* @__PURE__ */ new Set([
758
+ // The `Text` wrapper translates `aria-hidden` into `accessibilityElementsHidden` /
759
+ // `importantForAccessibility`, which `processAccessibilityProps` does not yet handle. Passing it
760
+ // through would drop it, so bail. TODO: handle this in the runtime helper instead.
761
+ "aria-hidden",
722
762
  "id",
723
763
  "nativeID",
724
764
  "onLongPress",
@@ -736,18 +776,14 @@ const textBlacklistedProperties = /* @__PURE__ */ new Set([
736
776
  "selectionColor"
737
777
  // TODO: we can use react-native's internal `processColor` to process this at runtime
738
778
  ]);
739
- const textOptimizer = (path, logger) => {
779
+ const NORMALIZED_PROPERTIES = /* @__PURE__ */ new Set([...ACCESSIBILITY_PROPERTIES, "disabled"]);
780
+ const isNormalizedProperty = (attribute) => core.types.isJSXAttribute(attribute) && core.types.isJSXIdentifier(attribute.name) && NORMALIZED_PROPERTIES.has(attribute.name.name);
781
+ const textOptimizer = (path, logger, options, platform) => {
740
782
  if (!isValidJSXComponent(path, "Text")) return;
783
+ if (!isReactNativeImport(path, "Text")) return;
741
784
  const parent = path.parent;
742
- const skipReason = getFirstBailoutReason([
743
- {
744
- reason: "line is marked with @boost-ignore",
745
- shouldBail: () => isIgnoredLine(path)
746
- },
747
- {
748
- reason: "Text is not imported from react-native",
749
- shouldBail: () => !isReactNativeImport(path, "Text")
750
- },
785
+ const forced = isForcedLine(path);
786
+ const overridableChecks = [
751
787
  {
752
788
  reason: "contains blacklisted props",
753
789
  shouldBail: () => hasBlacklistedProperty(path, textBlacklistedProperties)
@@ -756,18 +792,31 @@ const textOptimizer = (path, logger) => {
756
792
  reason: "is a direct child of expo-router Link with asChild",
757
793
  shouldBail: () => hasExpoRouterLinkParentWithAsChild(path)
758
794
  },
795
+ // The local children check runs before the ancestor checks because it is cheap and prunes the
796
+ // common nested-element `Text` before the unbounded ancestor walk those checks trigger.
759
797
  {
760
- reason: "contains non-string children",
798
+ reason: "contains non-primitive children",
761
799
  shouldBail: () => hasInvalidChildren(path, parent)
800
+ },
801
+ ...ancestorBailoutChecks(path, (options == null ? void 0 : options.dangerouslyOptimizeTextWithUnknownAncestors) === true)
802
+ ];
803
+ if (forced) {
804
+ const overriddenReason = getFirstBailoutReason(overridableChecks);
805
+ if (overriddenReason) {
806
+ logger.forced({ component: "Text", path, reason: overriddenReason });
807
+ }
808
+ } else {
809
+ const skipReason = getFirstBailoutReason([
810
+ {
811
+ reason: "line is marked with @boost-ignore",
812
+ shouldBail: () => isIgnoredLine(path)
813
+ },
814
+ ...overridableChecks
815
+ ]);
816
+ if (skipReason) {
817
+ logger.skipped({ component: "Text", path, reason: skipReason });
818
+ return;
762
819
  }
763
- ]);
764
- if (skipReason) {
765
- logger.skipped({
766
- component: "Text",
767
- path,
768
- reason: skipReason
769
- });
770
- return;
771
820
  }
772
821
  const hub = path.hub;
773
822
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
@@ -781,18 +830,18 @@ const textOptimizer = (path, logger) => {
781
830
  fixNegativeNumberOfLines({ path, logger });
782
831
  addDefaultProperty(path, "allowFontScaling", core.types.booleanLiteral(true));
783
832
  addDefaultProperty(path, "ellipsizeMode", core.types.stringLiteral("tail"));
784
- processProps(path, file);
833
+ processProps(path, file, platform);
785
834
  replaceWithNativeComponent(path, parent, file, "NativeText");
786
835
  };
787
836
  function hasInvalidChildren(path, parent) {
788
837
  for (const attribute of path.node.attributes) {
789
838
  if (core.types.isJSXSpreadAttribute(attribute)) continue;
790
- if (core.types.isJSXIdentifier(attribute.name) && attribute.value && // For a "children" attribute, optimization is allowed only if it is a string
791
- attribute.name.name === "children" && !isStringNode(path, attribute.value)) {
839
+ if (core.types.isJSXIdentifier(attribute.name) && attribute.value && // For a "children" attribute, optimization is allowed only if it is a provable primitive
840
+ attribute.name.name === "children" && !isPrimitiveChild(path, attribute.value)) {
792
841
  return true;
793
842
  }
794
843
  }
795
- return !parent.children.every((child) => isStringNode(path, child));
844
+ return !parent.children.every((child) => isPrimitiveChild(path, child));
796
845
  }
797
846
  function fixNegativeNumberOfLines({ path, logger }) {
798
847
  for (const attribute of path.node.attributes) {
@@ -814,16 +863,15 @@ function fixNegativeNumberOfLines({ path, logger }) {
814
863
  }
815
864
  }
816
865
  }
817
- function processProps(path, file) {
866
+ function processProps(path, file, platform) {
818
867
  const currentAttributes = [...path.node.attributes];
819
868
  const { styleExpr, styleAttribute } = extractStyleAttribute(currentAttributes);
820
- const hasA11y = hasAccessibilityProperty(path, currentAttributes);
869
+ const shouldNormalize = hasAccessibilityProperty(path, currentAttributes) || currentAttributes.some(
870
+ (attribute) => core.types.isJSXAttribute(attribute) && core.types.isJSXIdentifier(attribute.name, { name: "disabled" })
871
+ );
821
872
  const spreadAttributes = [];
822
- if (hasA11y) {
823
- const accessibilityAttributes = currentAttributes.filter((attribute) => {
824
- if (!core.types.isJSXAttribute(attribute)) return false;
825
- return core.types.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name);
826
- });
873
+ if (shouldNormalize) {
874
+ const normalizedAttributes = currentAttributes.filter((attribute) => isNormalizedProperty(attribute));
827
875
  const normalizeIdentifier = addFileImportHint({
828
876
  file,
829
877
  nameHint: "processAccessibilityProps",
@@ -831,7 +879,7 @@ function processProps(path, file) {
831
879
  importName: "processAccessibilityProps",
832
880
  moduleName: RUNTIME_MODULE_NAME
833
881
  });
834
- const accessibilityObject = buildPropertiesFromAttributes(accessibilityAttributes);
882
+ const accessibilityObject = buildPropertiesFromAttributes(normalizedAttributes);
835
883
  const accessibilityExpr = core.types.callExpression(core.types.identifier(normalizeIdentifier.name), [accessibilityObject]);
836
884
  spreadAttributes.push(core.types.jsxSpreadAttribute(accessibilityExpr));
837
885
  }
@@ -857,26 +905,45 @@ function processProps(path, file) {
857
905
  const remainingAttributes = [];
858
906
  for (const attribute of currentAttributes) {
859
907
  if (styleAttribute && attribute === styleAttribute) continue;
860
- if (hasA11y && core.types.isJSXAttribute(attribute) && core.types.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name)) {
861
- continue;
862
- }
908
+ if (shouldNormalize && isNormalizedProperty(attribute)) continue;
863
909
  remainingAttributes.push(attribute);
864
910
  }
865
- path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes].filter(
911
+ const accessibleAttribute = shouldNormalize ? void 0 : buildAccessibleDefault(path, file, platform);
912
+ path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes, accessibleAttribute].filter(
866
913
  (attribute) => attribute !== void 0
867
914
  );
868
915
  }
916
+ function buildAccessibleDefault(path, file, platform) {
917
+ if (platform === "web") return void 0;
918
+ let value;
919
+ if (platform === "ios" || platform === "android") {
920
+ value = core.types.booleanLiteral(platform === "ios");
921
+ } else {
922
+ const accessibleIdentifier = addFileImportHint({
923
+ file,
924
+ nameHint: "getDefaultTextAccessible",
925
+ path,
926
+ importName: "getDefaultTextAccessible",
927
+ moduleName: RUNTIME_MODULE_NAME
928
+ });
929
+ value = core.types.callExpression(core.types.identifier(accessibleIdentifier.name), []);
930
+ }
931
+ return core.types.jsxAttribute(core.types.jsxIdentifier("accessible"), core.types.jsxExpressionContainer(value));
932
+ }
869
933
 
870
934
  const LOG_PREFIX = "[react-native-boost]";
871
935
  const ANSI_RESET = "\x1B[0m";
872
936
  const ANSI_GREEN = "\x1B[32m";
873
937
  const ANSI_YELLOW = "\x1B[33m";
874
938
  const ANSI_MAGENTA = "\x1B[35m";
939
+ const ANSI_RED = "\x1B[31m";
875
940
  const noopLogger = {
876
941
  optimized() {
877
942
  },
878
943
  skipped() {
879
944
  },
945
+ forced() {
946
+ },
880
947
  warning() {
881
948
  }
882
949
  };
@@ -890,6 +957,12 @@ const createLogger = ({ verbose, silent }) => {
890
957
  if (!verbose) return;
891
958
  writeLog("skipped", `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`);
892
959
  },
960
+ forced(payload) {
961
+ writeLog(
962
+ "forced",
963
+ `Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})`
964
+ );
965
+ },
893
966
  warning(payload) {
894
967
  const context = formatWarningContext(payload);
895
968
  const message = context.length > 0 ? `${context}: ${payload.message}` : payload.message;
@@ -918,6 +991,9 @@ function formatLevel(level) {
918
991
  if (level === "skipped") {
919
992
  return colorize("[skipped]", ANSI_YELLOW);
920
993
  }
994
+ if (level === "forced") {
995
+ return colorize("[forced]", ANSI_RED);
996
+ }
921
997
  return colorize("[warning]", ANSI_MAGENTA);
922
998
  }
923
999
  function colorize(value, colorCode) {
@@ -952,7 +1028,9 @@ function formatPathLocation(payloadPath) {
952
1028
  }
953
1029
 
954
1030
  const viewBlacklistedProperties = /* @__PURE__ */ new Set([
955
- // TODO: process a11y props at runtime
1031
+ // The `View` wrapper translates these into native props (e.g. `aria-*` → `accessibility*`,
1032
+ // `tabIndex` → `focusable`). The native host does not understand them, so passing them through
1033
+ // would silently drop them. TODO: process these at runtime instead of bailing.
956
1034
  "accessible",
957
1035
  "accessibilityLabel",
958
1036
  "accessibilityState",
@@ -960,51 +1038,47 @@ const viewBlacklistedProperties = /* @__PURE__ */ new Set([
960
1038
  "aria-checked",
961
1039
  "aria-disabled",
962
1040
  "aria-expanded",
1041
+ "aria-hidden",
963
1042
  "aria-label",
1043
+ "aria-labelledby",
1044
+ "aria-live",
964
1045
  "aria-selected",
1046
+ "aria-valuemax",
1047
+ "aria-valuemin",
1048
+ "aria-valuenow",
1049
+ "aria-valuetext",
965
1050
  "id",
966
1051
  "nativeID",
967
- "style"
968
- // TODO: process style at runtime
1052
+ "tabIndex"
969
1053
  ]);
970
1054
  const viewOptimizer = (path, logger, options) => {
971
1055
  if (!isValidJSXComponent(path, "View")) return;
972
- let ancestorClassification;
973
- const getAncestorClassification = () => {
974
- if (!ancestorClassification) {
975
- ancestorClassification = getViewAncestorClassification(path);
976
- }
977
- return ancestorClassification;
978
- };
979
- const skipReason = getFirstBailoutReason([
980
- {
981
- reason: "line is marked with @boost-ignore",
982
- shouldBail: () => isIgnoredLine(path)
983
- },
984
- {
985
- reason: "View is not imported from react-native",
986
- shouldBail: () => !isReactNativeImport(path, "View")
987
- },
1056
+ if (!isReactNativeImport(path, "View")) return;
1057
+ const forced = isForcedLine(path);
1058
+ const overridableChecks = [
988
1059
  {
989
1060
  reason: "contains blacklisted props",
990
1061
  shouldBail: () => hasBlacklistedProperty(path, viewBlacklistedProperties)
991
1062
  },
992
- {
993
- reason: "has Text ancestor",
994
- shouldBail: () => getAncestorClassification() === "text"
995
- },
996
- {
997
- reason: "has unresolved ancestor and dangerous optimization is disabled",
998
- shouldBail: () => getAncestorClassification() === "unknown" && (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) !== true
1063
+ ...ancestorBailoutChecks(path, (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) === true)
1064
+ ];
1065
+ if (forced) {
1066
+ const overriddenReason = getFirstBailoutReason(overridableChecks);
1067
+ if (overriddenReason) {
1068
+ logger.forced({ component: "View", path, reason: overriddenReason });
1069
+ }
1070
+ } else {
1071
+ const skipReason = getFirstBailoutReason([
1072
+ {
1073
+ reason: "line is marked with @boost-ignore",
1074
+ shouldBail: () => isIgnoredLine(path)
1075
+ },
1076
+ ...overridableChecks
1077
+ ]);
1078
+ if (skipReason) {
1079
+ logger.skipped({ component: "View", path, reason: skipReason });
1080
+ return;
999
1081
  }
1000
- ]);
1001
- if (skipReason) {
1002
- logger.skipped({
1003
- component: "View",
1004
- path,
1005
- reason: skipReason
1006
- });
1007
- return;
1008
1082
  }
1009
1083
  const hub = path.hub;
1010
1084
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
@@ -1021,6 +1095,7 @@ const viewOptimizer = (path, logger, options) => {
1021
1095
 
1022
1096
  var index = helperPluginUtils.declare((api) => {
1023
1097
  api.assertVersion(7);
1098
+ const platform = api.caller((caller) => caller == null ? void 0 : caller.platform);
1024
1099
  return {
1025
1100
  name: "react-native-boost",
1026
1101
  visitor: {
@@ -1030,7 +1105,7 @@ var index = helperPluginUtils.declare((api) => {
1030
1105
  const options = (_a = pluginState.opts) != null ? _a : {};
1031
1106
  const logger = getOrCreateLogger(pluginState, options);
1032
1107
  if (isIgnoredFile(path, (_b = options.ignores) != null ? _b : [])) return;
1033
- if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger);
1108
+ if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger, options, platform);
1034
1109
  if (((_d = options.optimizations) == null ? void 0 : _d.view) !== false) viewOptimizer(path, logger, options);
1035
1110
  }
1036
1111
  }