react-native-boost 0.7.0 → 1.1.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.
@@ -17,6 +17,14 @@ const ensureArray = (value) => {
17
17
  if (Array.isArray(value)) return value;
18
18
  return [value];
19
19
  };
20
+ const getFirstBailoutReason = (checks) => {
21
+ for (const check of checks) {
22
+ if (check.shouldBail()) {
23
+ return check.reason;
24
+ }
25
+ }
26
+ return null;
27
+ };
20
28
 
21
29
  const isIgnoredFile = (path, ignores) => {
22
30
  const hub = path.hub;
@@ -34,17 +42,23 @@ const isIgnoredFile = (path, ignores) => {
34
42
  }
35
43
  return false;
36
44
  };
45
+ const isForcedLine = (path) => {
46
+ return hasDecoratorComment(path, "@boost-force");
47
+ };
37
48
  const isIgnoredLine = (path) => {
49
+ return hasDecoratorComment(path, "@boost-ignore");
50
+ };
51
+ function hasDecoratorComment(path, decorator) {
38
52
  var _a, _b, _c;
39
- 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))) {
40
54
  return true;
41
55
  }
42
56
  const jsxElementPath = path.parentPath;
43
- 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))) {
44
58
  return true;
45
59
  }
46
60
  const propertyPath = jsxElementPath.parentPath;
47
- 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)))) {
48
62
  return true;
49
63
  }
50
64
  if (!jsxElementPath.parentPath) return false;
@@ -65,18 +79,18 @@ const isIgnoredLine = (path) => {
65
79
  ...expression.node.trailingComments || [],
66
80
  ...expression.node.innerComments || []
67
81
  ].map((comment) => comment.value.trim());
68
- if (comments.some((comment) => comment.includes("@boost-ignore"))) {
82
+ if (comments.some((comment) => comment.includes(decorator))) {
69
83
  return true;
70
84
  }
71
85
  }
72
86
  }
73
- 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))) {
74
88
  return true;
75
89
  }
76
90
  break;
77
91
  }
78
92
  return false;
79
- };
93
+ }
80
94
  const isValidJSXComponent = (path, componentName) => {
81
95
  if (!core.types.isJSXIdentifier(path.node.name)) return false;
82
96
  const parent = path.parent;
@@ -113,11 +127,8 @@ const isReactNativeImport = (path, expectedImportedName) => {
113
127
  }
114
128
  return false;
115
129
  };
116
- const hasUnsafeViewAncestor = (path, allowUnknownAncestors = false) => {
117
- const classification = classifyViewAncestors(path);
118
- if (classification === "text") return true;
119
- if (classification === "unknown" && !allowUnknownAncestors) return true;
120
- return false;
130
+ const getViewAncestorClassification = (path) => {
131
+ return classifyViewAncestors(path);
121
132
  };
122
133
  function classifyViewAncestors(path) {
123
134
  const context = {
@@ -731,25 +742,53 @@ const textBlacklistedProperties = /* @__PURE__ */ new Set([
731
742
  "selectionColor"
732
743
  // TODO: we can use react-native's internal `processColor` to process this at runtime
733
744
  ]);
734
- const textOptimizer = (path, log = () => {
735
- }) => {
736
- var _a, _b, _c;
737
- if (isIgnoredLine(path)) return;
745
+ const textOptimizer = (path, logger) => {
738
746
  if (!isValidJSXComponent(path, "Text")) return;
739
747
  if (!isReactNativeImport(path, "Text")) return;
740
- if (hasBlacklistedProperty(path, textBlacklistedProperties)) return;
741
- if (hasExpoRouterLinkParentWithAsChild(path)) return;
742
748
  const parent = path.parent;
743
- if (hasInvalidChildren(path, parent)) return;
749
+ const forced = isForcedLine(path);
750
+ const overridableChecks = [
751
+ {
752
+ reason: "contains blacklisted props",
753
+ shouldBail: () => hasBlacklistedProperty(path, textBlacklistedProperties)
754
+ },
755
+ {
756
+ reason: "is a direct child of expo-router Link with asChild",
757
+ shouldBail: () => hasExpoRouterLinkParentWithAsChild(path)
758
+ },
759
+ {
760
+ reason: "contains non-string children",
761
+ shouldBail: () => hasInvalidChildren(path, parent)
762
+ }
763
+ ];
764
+ if (forced) {
765
+ const overriddenReason = getFirstBailoutReason(overridableChecks);
766
+ if (overriddenReason) {
767
+ logger.forced({ component: "Text", path, reason: overriddenReason });
768
+ }
769
+ } else {
770
+ const skipReason = getFirstBailoutReason([
771
+ {
772
+ reason: "line is marked with @boost-ignore",
773
+ shouldBail: () => isIgnoredLine(path)
774
+ },
775
+ ...overridableChecks
776
+ ]);
777
+ if (skipReason) {
778
+ logger.skipped({ component: "Text", path, reason: skipReason });
779
+ return;
780
+ }
781
+ }
744
782
  const hub = path.hub;
745
783
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
746
784
  if (!file) {
747
785
  throw new PluginError("No file found in Babel hub");
748
786
  }
749
- const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
750
- const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
751
- log(`Optimizing Text component in ${filename}:${lineNumber}`);
752
- fixNegativeNumberOfLines({ path, log });
787
+ logger.optimized({
788
+ component: "Text",
789
+ path
790
+ });
791
+ fixNegativeNumberOfLines({ path, logger });
753
792
  addDefaultProperty(path, "allowFontScaling", core.types.booleanLiteral(true));
754
793
  addDefaultProperty(path, "ellipsizeMode", core.types.stringLiteral("tail"));
755
794
  processProps(path, file);
@@ -765,10 +804,7 @@ function hasInvalidChildren(path, parent) {
765
804
  }
766
805
  return !parent.children.every((child) => isStringNode(path, child));
767
806
  }
768
- function fixNegativeNumberOfLines({
769
- path,
770
- log
771
- }) {
807
+ function fixNegativeNumberOfLines({ path, logger }) {
772
808
  for (const attribute of path.node.attributes) {
773
809
  if (core.types.isJSXAttribute(attribute) && core.types.isJSXIdentifier(attribute.name, { name: "numberOfLines" }) && attribute.value && core.types.isJSXExpressionContainer(attribute.value)) {
774
810
  let originalValue;
@@ -778,9 +814,11 @@ function fixNegativeNumberOfLines({
778
814
  originalValue = -attribute.value.expression.argument.value;
779
815
  }
780
816
  if (originalValue !== void 0 && originalValue < 0) {
781
- log(
782
- `Warning: 'numberOfLines' in <Text> must be a non-negative number, received: ${originalValue}. The value will be set to 0.`
783
- );
817
+ logger.warning({
818
+ component: "Text",
819
+ path,
820
+ message: `'numberOfLines' must be a non-negative number, received: ${originalValue}. The value will be set to 0.`
821
+ });
784
822
  attribute.value.expression = core.types.numericLiteral(0);
785
823
  }
786
824
  }
@@ -839,9 +877,101 @@ function processProps(path, file) {
839
877
  );
840
878
  }
841
879
 
842
- const log = (message) => {
843
- console.log(`[react-native-boost] ${message}`);
880
+ const LOG_PREFIX = "[react-native-boost]";
881
+ const ANSI_RESET = "\x1B[0m";
882
+ const ANSI_GREEN = "\x1B[32m";
883
+ const ANSI_YELLOW = "\x1B[33m";
884
+ const ANSI_MAGENTA = "\x1B[35m";
885
+ const ANSI_RED = "\x1B[31m";
886
+ const noopLogger = {
887
+ optimized() {
888
+ },
889
+ skipped() {
890
+ },
891
+ forced() {
892
+ },
893
+ warning() {
894
+ }
844
895
  };
896
+ const createLogger = ({ verbose, silent }) => {
897
+ if (silent) return noopLogger;
898
+ return {
899
+ optimized(payload) {
900
+ writeLog("optimized", `Optimized ${payload.component} in ${formatPathLocation(payload.path)}`);
901
+ },
902
+ skipped(payload) {
903
+ if (!verbose) return;
904
+ writeLog("skipped", `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`);
905
+ },
906
+ forced(payload) {
907
+ writeLog(
908
+ "forced",
909
+ `Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})`
910
+ );
911
+ },
912
+ warning(payload) {
913
+ const context = formatWarningContext(payload);
914
+ const message = context.length > 0 ? `${context}: ${payload.message}` : payload.message;
915
+ writeLog("warning", message);
916
+ }
917
+ };
918
+ };
919
+ function formatWarningContext(payload) {
920
+ const location = formatPathLocation(payload.path);
921
+ if (payload.component && location.length > 0) {
922
+ return `${payload.component} in ${location}`;
923
+ }
924
+ if (payload.component) {
925
+ return payload.component;
926
+ }
927
+ return location;
928
+ }
929
+ function writeLog(level, message) {
930
+ const levelTag = formatLevel(level);
931
+ console.log(`${LOG_PREFIX} ${levelTag} ${message}`);
932
+ }
933
+ function formatLevel(level) {
934
+ if (level === "optimized") {
935
+ return colorize("[optimized]", ANSI_GREEN);
936
+ }
937
+ if (level === "skipped") {
938
+ return colorize("[skipped]", ANSI_YELLOW);
939
+ }
940
+ if (level === "forced") {
941
+ return colorize("[forced]", ANSI_RED);
942
+ }
943
+ return colorize("[warning]", ANSI_MAGENTA);
944
+ }
945
+ function colorize(value, colorCode) {
946
+ if (!shouldUseColor()) return value;
947
+ return `${colorCode}${value}${ANSI_RESET}`;
948
+ }
949
+ function shouldUseColor() {
950
+ var _a, _b;
951
+ if (process.env.NO_COLOR != null) return false;
952
+ if (process.env.FORCE_COLOR === "0") return false;
953
+ if (process.env.FORCE_COLOR != null) return true;
954
+ if (process.env.CLICOLOR === "0") return false;
955
+ if (process.env.CLICOLOR_FORCE != null && process.env.CLICOLOR_FORCE !== "0") return true;
956
+ if (((_a = process.stdout) == null ? void 0 : _a.isTTY) === true || ((_b = process.stderr) == null ? void 0 : _b.isTTY) === true) {
957
+ return true;
958
+ }
959
+ const colorTerm = process.env.COLORTERM;
960
+ if (colorTerm != null && colorTerm !== "") {
961
+ return true;
962
+ }
963
+ const term = process.env.TERM;
964
+ return term != null && term !== "" && term.toLowerCase() !== "dumb";
965
+ }
966
+ function formatPathLocation(payloadPath) {
967
+ var _a, _b, _c, _d;
968
+ if (!payloadPath) return "unknown file:unknown line";
969
+ const hub = payloadPath.hub;
970
+ const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
971
+ const filename = (_b = (_a = file == null ? void 0 : file.opts) == null ? void 0 : _a.filename) != null ? _b : "unknown file";
972
+ const lineNumber = (_d = (_c = payloadPath.node.loc) == null ? void 0 : _c.start.line) != null ? _d : "unknown line";
973
+ return `${filename}:${lineNumber}`;
974
+ }
845
975
 
846
976
  const viewBlacklistedProperties = /* @__PURE__ */ new Set([
847
977
  // TODO: process a11y props at runtime
@@ -859,22 +989,58 @@ const viewBlacklistedProperties = /* @__PURE__ */ new Set([
859
989
  "style"
860
990
  // TODO: process style at runtime
861
991
  ]);
862
- const viewOptimizer = (path, log = () => {
863
- }, options) => {
864
- var _a, _b, _c;
865
- if (isIgnoredLine(path)) return;
992
+ const viewOptimizer = (path, logger, options) => {
866
993
  if (!isValidJSXComponent(path, "View")) return;
867
994
  if (!isReactNativeImport(path, "View")) return;
868
- if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
869
- if (hasUnsafeViewAncestor(path, (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) === true)) return;
995
+ let ancestorClassification;
996
+ const getAncestorClassification = () => {
997
+ if (!ancestorClassification) {
998
+ ancestorClassification = getViewAncestorClassification(path);
999
+ }
1000
+ return ancestorClassification;
1001
+ };
1002
+ const forced = isForcedLine(path);
1003
+ const overridableChecks = [
1004
+ {
1005
+ reason: "contains blacklisted props",
1006
+ shouldBail: () => hasBlacklistedProperty(path, viewBlacklistedProperties)
1007
+ },
1008
+ {
1009
+ reason: "has Text ancestor",
1010
+ shouldBail: () => getAncestorClassification() === "text"
1011
+ },
1012
+ {
1013
+ reason: "has unresolved ancestor and dangerous optimization is disabled",
1014
+ shouldBail: () => getAncestorClassification() === "unknown" && (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) !== true
1015
+ }
1016
+ ];
1017
+ if (forced) {
1018
+ const overriddenReason = getFirstBailoutReason(overridableChecks);
1019
+ if (overriddenReason) {
1020
+ logger.forced({ component: "View", path, reason: overriddenReason });
1021
+ }
1022
+ } else {
1023
+ const skipReason = getFirstBailoutReason([
1024
+ {
1025
+ reason: "line is marked with @boost-ignore",
1026
+ shouldBail: () => isIgnoredLine(path)
1027
+ },
1028
+ ...overridableChecks
1029
+ ]);
1030
+ if (skipReason) {
1031
+ logger.skipped({ component: "View", path, reason: skipReason });
1032
+ return;
1033
+ }
1034
+ }
870
1035
  const hub = path.hub;
871
1036
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
872
1037
  if (!file) {
873
1038
  throw new PluginError("No file found in Babel hub");
874
1039
  }
875
- const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
876
- const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
877
- log(`Optimizing View component in ${filename}:${lineNumber}`);
1040
+ logger.optimized({
1041
+ component: "View",
1042
+ path
1043
+ });
878
1044
  const parent = path.parent;
879
1045
  replaceWithNativeComponent(path, parent, file, "NativeView");
880
1046
  };
@@ -886,9 +1052,9 @@ var index = helperPluginUtils.declare((api) => {
886
1052
  visitor: {
887
1053
  JSXOpeningElement(path, state) {
888
1054
  var _a, _b, _c, _d;
889
- const options = (_a = state.opts) != null ? _a : {};
890
- const logger = options.verbose ? log : () => {
891
- };
1055
+ const pluginState = state;
1056
+ const options = (_a = pluginState.opts) != null ? _a : {};
1057
+ const logger = getOrCreateLogger(pluginState, options);
892
1058
  if (isIgnoredFile(path, (_b = options.ignores) != null ? _b : [])) return;
893
1059
  if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger);
894
1060
  if (((_d = options.optimizations) == null ? void 0 : _d.view) !== false) viewOptimizer(path, logger, options);
@@ -896,6 +1062,16 @@ var index = helperPluginUtils.declare((api) => {
896
1062
  }
897
1063
  };
898
1064
  });
1065
+ function getOrCreateLogger(state, options) {
1066
+ if (state.__reactNativeBoostLogger) {
1067
+ return state.__reactNativeBoostLogger;
1068
+ }
1069
+ state.__reactNativeBoostLogger = createLogger({
1070
+ verbose: options.verbose === true,
1071
+ silent: options.silent === true
1072
+ });
1073
+ return state.__reactNativeBoostLogger;
1074
+ }
899
1075
 
900
1076
  module.exports = index;
901
1077
  //# sourceMappingURL=index.js.map