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.
package/README.md CHANGED
@@ -4,9 +4,6 @@
4
4
 
5
5
  A powerful Babel plugin that automatically optimizes React Native apps through static source code analysis. It replaces standard React Native components with their native counterparts where possible, leading to significant performance improvements.
6
6
 
7
- > [!WARNING]
8
- > The library and its Babel plugin are still experimental. You should expect things to break. Please report any issues you encounter in the issues tab with reproducible examples, and use the library's ignore mechanisms to disable optimizations for problematic files or lines of code.
9
-
10
7
  - ⚡ Automatic performance optimization through source code analysis
11
8
  - 🔒 Safe optimizations that don't break your app
12
9
  - 🎯 Virtually zero runtime overhead
@@ -23,10 +20,19 @@ The documentation is available at [react-native-boost.oss.kuatsu.de](https://rea
23
20
  The app in the `apps/example` directory serves as a benchmark for the performance of the plugin.
24
21
 
25
22
  <div align="center">
26
- <img src="./apps/docs/docs/introduction/img/benchmark-ios.png" width="500" />
23
+ <img src="./apps/docs/content/docs/information/img/benchmark-ios.png" width="500" />
27
24
  </div>
28
25
 
29
- More benchmarks are available in the [docs](https://react-native-boost.oss.kuatsu.de/docs/introduction/benchmarks).
26
+ More benchmarks are available in the [docs](https://react-native-boost.oss.kuatsu.de/docs/information/benchmarks).
27
+
28
+ ## Compatibility
29
+
30
+ | `react-native-boost` | React Native |
31
+ | -------------------- | ---------------- |
32
+ | `0.x` | All versions[^1] |
33
+ | `1.x` | `>=0.83` |
34
+
35
+ [^1]: Starting from React Native `0.80`, `react-native-boost@0` prints import deprecation warnings.
30
36
 
31
37
  ## Installation
32
38
 
@@ -62,11 +68,11 @@ yarn start --clear
62
68
 
63
69
  That's it! No imports in your code, rebuilding, or anything else is required.
64
70
 
65
- Optionally, you can configure the Babel plugin with a few options described in the [documentation](https://react-native-boost.oss.kuatsu.de/docs/babel-plugin/configure).
71
+ Optionally, you can configure the Babel plugin with a few options described in the [documentation](https://react-native-boost.oss.kuatsu.de/docs/configuration/configure).
66
72
 
67
73
  ## How it works
68
74
 
69
- A technical rundown of how the plugin works can be found in the [docs](https://react-native-boost.oss.kuatsu.de/docs/introduction/how-it-works).
75
+ A technical rundown of how the plugin works can be found in the [docs](https://react-native-boost.oss.kuatsu.de/docs/information/how-it-works).
70
76
 
71
77
  ## Contributing
72
78
 
@@ -15,6 +15,14 @@ const ensureArray = (value) => {
15
15
  if (Array.isArray(value)) return value;
16
16
  return [value];
17
17
  };
18
+ const getFirstBailoutReason = (checks) => {
19
+ for (const check of checks) {
20
+ if (check.shouldBail()) {
21
+ return check.reason;
22
+ }
23
+ }
24
+ return null;
25
+ };
18
26
 
19
27
  const isIgnoredFile = (path, ignores) => {
20
28
  const hub = path.hub;
@@ -32,17 +40,23 @@ const isIgnoredFile = (path, ignores) => {
32
40
  }
33
41
  return false;
34
42
  };
43
+ const isForcedLine = (path) => {
44
+ return hasDecoratorComment(path, "@boost-force");
45
+ };
35
46
  const isIgnoredLine = (path) => {
47
+ return hasDecoratorComment(path, "@boost-ignore");
48
+ };
49
+ function hasDecoratorComment(path, decorator) {
36
50
  var _a, _b, _c;
37
- if ((_a = path.node.leadingComments) == null ? void 0 : _a.some((comment) => comment.value.includes("@boost-ignore"))) {
51
+ if ((_a = path.node.leadingComments) == null ? void 0 : _a.some((comment) => comment.value.includes(decorator))) {
38
52
  return true;
39
53
  }
40
54
  const jsxElementPath = path.parentPath;
41
- if ((_b = jsxElementPath.node.leadingComments) == null ? void 0 : _b.some((comment) => comment.value.includes("@boost-ignore"))) {
55
+ if ((_b = jsxElementPath.node.leadingComments) == null ? void 0 : _b.some((comment) => comment.value.includes(decorator))) {
42
56
  return true;
43
57
  }
44
58
  const propertyPath = jsxElementPath.parentPath;
45
- if (propertyPath && propertyPath.isObjectProperty() && ((_c = propertyPath.node.leadingComments) == null ? void 0 : _c.some((comment) => comment.value.includes("@boost-ignore")))) {
59
+ if (propertyPath && propertyPath.isObjectProperty() && ((_c = propertyPath.node.leadingComments) == null ? void 0 : _c.some((comment) => comment.value.includes(decorator)))) {
46
60
  return true;
47
61
  }
48
62
  if (!jsxElementPath.parentPath) return false;
@@ -63,18 +77,18 @@ const isIgnoredLine = (path) => {
63
77
  ...expression.node.trailingComments || [],
64
78
  ...expression.node.innerComments || []
65
79
  ].map((comment) => comment.value.trim());
66
- if (comments.some((comment) => comment.includes("@boost-ignore"))) {
80
+ if (comments.some((comment) => comment.includes(decorator))) {
67
81
  return true;
68
82
  }
69
83
  }
70
84
  }
71
- if (sibling.node.leadingComments && sibling.node.leadingComments.some((comment) => comment.value.includes("@boost-ignore"))) {
85
+ if (sibling.node.leadingComments && sibling.node.leadingComments.some((comment) => comment.value.includes(decorator))) {
72
86
  return true;
73
87
  }
74
88
  break;
75
89
  }
76
90
  return false;
77
- };
91
+ }
78
92
  const isValidJSXComponent = (path, componentName) => {
79
93
  if (!types.isJSXIdentifier(path.node.name)) return false;
80
94
  const parent = path.parent;
@@ -111,11 +125,8 @@ const isReactNativeImport = (path, expectedImportedName) => {
111
125
  }
112
126
  return false;
113
127
  };
114
- const hasUnsafeViewAncestor = (path, allowUnknownAncestors = false) => {
115
- const classification = classifyViewAncestors(path);
116
- if (classification === "text") return true;
117
- if (classification === "unknown" && !allowUnknownAncestors) return true;
118
- return false;
128
+ const getViewAncestorClassification = (path) => {
129
+ return classifyViewAncestors(path);
119
130
  };
120
131
  function classifyViewAncestors(path) {
121
132
  const context = {
@@ -729,25 +740,53 @@ const textBlacklistedProperties = /* @__PURE__ */ new Set([
729
740
  "selectionColor"
730
741
  // TODO: we can use react-native's internal `processColor` to process this at runtime
731
742
  ]);
732
- const textOptimizer = (path, log = () => {
733
- }) => {
734
- var _a, _b, _c;
735
- if (isIgnoredLine(path)) return;
743
+ const textOptimizer = (path, logger) => {
736
744
  if (!isValidJSXComponent(path, "Text")) return;
737
745
  if (!isReactNativeImport(path, "Text")) return;
738
- if (hasBlacklistedProperty(path, textBlacklistedProperties)) return;
739
- if (hasExpoRouterLinkParentWithAsChild(path)) return;
740
746
  const parent = path.parent;
741
- if (hasInvalidChildren(path, parent)) return;
747
+ const forced = isForcedLine(path);
748
+ const overridableChecks = [
749
+ {
750
+ reason: "contains blacklisted props",
751
+ shouldBail: () => hasBlacklistedProperty(path, textBlacklistedProperties)
752
+ },
753
+ {
754
+ reason: "is a direct child of expo-router Link with asChild",
755
+ shouldBail: () => hasExpoRouterLinkParentWithAsChild(path)
756
+ },
757
+ {
758
+ reason: "contains non-string children",
759
+ shouldBail: () => hasInvalidChildren(path, parent)
760
+ }
761
+ ];
762
+ if (forced) {
763
+ const overriddenReason = getFirstBailoutReason(overridableChecks);
764
+ if (overriddenReason) {
765
+ logger.forced({ component: "Text", path, reason: overriddenReason });
766
+ }
767
+ } else {
768
+ const skipReason = getFirstBailoutReason([
769
+ {
770
+ reason: "line is marked with @boost-ignore",
771
+ shouldBail: () => isIgnoredLine(path)
772
+ },
773
+ ...overridableChecks
774
+ ]);
775
+ if (skipReason) {
776
+ logger.skipped({ component: "Text", path, reason: skipReason });
777
+ return;
778
+ }
779
+ }
742
780
  const hub = path.hub;
743
781
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
744
782
  if (!file) {
745
783
  throw new PluginError("No file found in Babel hub");
746
784
  }
747
- const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
748
- const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
749
- log(`Optimizing Text component in ${filename}:${lineNumber}`);
750
- fixNegativeNumberOfLines({ path, log });
785
+ logger.optimized({
786
+ component: "Text",
787
+ path
788
+ });
789
+ fixNegativeNumberOfLines({ path, logger });
751
790
  addDefaultProperty(path, "allowFontScaling", types.booleanLiteral(true));
752
791
  addDefaultProperty(path, "ellipsizeMode", types.stringLiteral("tail"));
753
792
  processProps(path, file);
@@ -763,10 +802,7 @@ function hasInvalidChildren(path, parent) {
763
802
  }
764
803
  return !parent.children.every((child) => isStringNode(path, child));
765
804
  }
766
- function fixNegativeNumberOfLines({
767
- path,
768
- log
769
- }) {
805
+ function fixNegativeNumberOfLines({ path, logger }) {
770
806
  for (const attribute of path.node.attributes) {
771
807
  if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "numberOfLines" }) && attribute.value && types.isJSXExpressionContainer(attribute.value)) {
772
808
  let originalValue;
@@ -776,9 +812,11 @@ function fixNegativeNumberOfLines({
776
812
  originalValue = -attribute.value.expression.argument.value;
777
813
  }
778
814
  if (originalValue !== void 0 && originalValue < 0) {
779
- log(
780
- `Warning: 'numberOfLines' in <Text> must be a non-negative number, received: ${originalValue}. The value will be set to 0.`
781
- );
815
+ logger.warning({
816
+ component: "Text",
817
+ path,
818
+ message: `'numberOfLines' must be a non-negative number, received: ${originalValue}. The value will be set to 0.`
819
+ });
782
820
  attribute.value.expression = types.numericLiteral(0);
783
821
  }
784
822
  }
@@ -837,9 +875,101 @@ function processProps(path, file) {
837
875
  );
838
876
  }
839
877
 
840
- const log = (message) => {
841
- console.log(`[react-native-boost] ${message}`);
878
+ const LOG_PREFIX = "[react-native-boost]";
879
+ const ANSI_RESET = "\x1B[0m";
880
+ const ANSI_GREEN = "\x1B[32m";
881
+ const ANSI_YELLOW = "\x1B[33m";
882
+ const ANSI_MAGENTA = "\x1B[35m";
883
+ const ANSI_RED = "\x1B[31m";
884
+ const noopLogger = {
885
+ optimized() {
886
+ },
887
+ skipped() {
888
+ },
889
+ forced() {
890
+ },
891
+ warning() {
892
+ }
842
893
  };
894
+ const createLogger = ({ verbose, silent }) => {
895
+ if (silent) return noopLogger;
896
+ return {
897
+ optimized(payload) {
898
+ writeLog("optimized", `Optimized ${payload.component} in ${formatPathLocation(payload.path)}`);
899
+ },
900
+ skipped(payload) {
901
+ if (!verbose) return;
902
+ writeLog("skipped", `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`);
903
+ },
904
+ forced(payload) {
905
+ writeLog(
906
+ "forced",
907
+ `Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})`
908
+ );
909
+ },
910
+ warning(payload) {
911
+ const context = formatWarningContext(payload);
912
+ const message = context.length > 0 ? `${context}: ${payload.message}` : payload.message;
913
+ writeLog("warning", message);
914
+ }
915
+ };
916
+ };
917
+ function formatWarningContext(payload) {
918
+ const location = formatPathLocation(payload.path);
919
+ if (payload.component && location.length > 0) {
920
+ return `${payload.component} in ${location}`;
921
+ }
922
+ if (payload.component) {
923
+ return payload.component;
924
+ }
925
+ return location;
926
+ }
927
+ function writeLog(level, message) {
928
+ const levelTag = formatLevel(level);
929
+ console.log(`${LOG_PREFIX} ${levelTag} ${message}`);
930
+ }
931
+ function formatLevel(level) {
932
+ if (level === "optimized") {
933
+ return colorize("[optimized]", ANSI_GREEN);
934
+ }
935
+ if (level === "skipped") {
936
+ return colorize("[skipped]", ANSI_YELLOW);
937
+ }
938
+ if (level === "forced") {
939
+ return colorize("[forced]", ANSI_RED);
940
+ }
941
+ return colorize("[warning]", ANSI_MAGENTA);
942
+ }
943
+ function colorize(value, colorCode) {
944
+ if (!shouldUseColor()) return value;
945
+ return `${colorCode}${value}${ANSI_RESET}`;
946
+ }
947
+ function shouldUseColor() {
948
+ var _a, _b;
949
+ if (process.env.NO_COLOR != null) return false;
950
+ if (process.env.FORCE_COLOR === "0") return false;
951
+ if (process.env.FORCE_COLOR != null) return true;
952
+ if (process.env.CLICOLOR === "0") return false;
953
+ if (process.env.CLICOLOR_FORCE != null && process.env.CLICOLOR_FORCE !== "0") return true;
954
+ if (((_a = process.stdout) == null ? void 0 : _a.isTTY) === true || ((_b = process.stderr) == null ? void 0 : _b.isTTY) === true) {
955
+ return true;
956
+ }
957
+ const colorTerm = process.env.COLORTERM;
958
+ if (colorTerm != null && colorTerm !== "") {
959
+ return true;
960
+ }
961
+ const term = process.env.TERM;
962
+ return term != null && term !== "" && term.toLowerCase() !== "dumb";
963
+ }
964
+ function formatPathLocation(payloadPath) {
965
+ var _a, _b, _c, _d;
966
+ if (!payloadPath) return "unknown file:unknown line";
967
+ const hub = payloadPath.hub;
968
+ const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
969
+ const filename = (_b = (_a = file == null ? void 0 : file.opts) == null ? void 0 : _a.filename) != null ? _b : "unknown file";
970
+ const lineNumber = (_d = (_c = payloadPath.node.loc) == null ? void 0 : _c.start.line) != null ? _d : "unknown line";
971
+ return `${filename}:${lineNumber}`;
972
+ }
843
973
 
844
974
  const viewBlacklistedProperties = /* @__PURE__ */ new Set([
845
975
  // TODO: process a11y props at runtime
@@ -857,22 +987,58 @@ const viewBlacklistedProperties = /* @__PURE__ */ new Set([
857
987
  "style"
858
988
  // TODO: process style at runtime
859
989
  ]);
860
- const viewOptimizer = (path, log = () => {
861
- }, options) => {
862
- var _a, _b, _c;
863
- if (isIgnoredLine(path)) return;
990
+ const viewOptimizer = (path, logger, options) => {
864
991
  if (!isValidJSXComponent(path, "View")) return;
865
992
  if (!isReactNativeImport(path, "View")) return;
866
- if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
867
- if (hasUnsafeViewAncestor(path, (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) === true)) return;
993
+ let ancestorClassification;
994
+ const getAncestorClassification = () => {
995
+ if (!ancestorClassification) {
996
+ ancestorClassification = getViewAncestorClassification(path);
997
+ }
998
+ return ancestorClassification;
999
+ };
1000
+ const forced = isForcedLine(path);
1001
+ const overridableChecks = [
1002
+ {
1003
+ reason: "contains blacklisted props",
1004
+ shouldBail: () => hasBlacklistedProperty(path, viewBlacklistedProperties)
1005
+ },
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
+ }
1014
+ ];
1015
+ if (forced) {
1016
+ const overriddenReason = getFirstBailoutReason(overridableChecks);
1017
+ if (overriddenReason) {
1018
+ logger.forced({ component: "View", path, reason: overriddenReason });
1019
+ }
1020
+ } else {
1021
+ const skipReason = getFirstBailoutReason([
1022
+ {
1023
+ reason: "line is marked with @boost-ignore",
1024
+ shouldBail: () => isIgnoredLine(path)
1025
+ },
1026
+ ...overridableChecks
1027
+ ]);
1028
+ if (skipReason) {
1029
+ logger.skipped({ component: "View", path, reason: skipReason });
1030
+ return;
1031
+ }
1032
+ }
868
1033
  const hub = path.hub;
869
1034
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
870
1035
  if (!file) {
871
1036
  throw new PluginError("No file found in Babel hub");
872
1037
  }
873
- const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
874
- const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
875
- log(`Optimizing View component in ${filename}:${lineNumber}`);
1038
+ logger.optimized({
1039
+ component: "View",
1040
+ path
1041
+ });
876
1042
  const parent = path.parent;
877
1043
  replaceWithNativeComponent(path, parent, file, "NativeView");
878
1044
  };
@@ -884,9 +1050,9 @@ var index = declare((api) => {
884
1050
  visitor: {
885
1051
  JSXOpeningElement(path, state) {
886
1052
  var _a, _b, _c, _d;
887
- const options = (_a = state.opts) != null ? _a : {};
888
- const logger = options.verbose ? log : () => {
889
- };
1053
+ const pluginState = state;
1054
+ const options = (_a = pluginState.opts) != null ? _a : {};
1055
+ const logger = getOrCreateLogger(pluginState, options);
890
1056
  if (isIgnoredFile(path, (_b = options.ignores) != null ? _b : [])) return;
891
1057
  if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger);
892
1058
  if (((_d = options.optimizations) == null ? void 0 : _d.view) !== false) viewOptimizer(path, logger, options);
@@ -894,6 +1060,16 @@ var index = declare((api) => {
894
1060
  }
895
1061
  };
896
1062
  });
1063
+ function getOrCreateLogger(state, options) {
1064
+ if (state.__reactNativeBoostLogger) {
1065
+ return state.__reactNativeBoostLogger;
1066
+ }
1067
+ state.__reactNativeBoostLogger = createLogger({
1068
+ verbose: options.verbose === true,
1069
+ silent: options.silent === true
1070
+ });
1071
+ return state.__reactNativeBoostLogger;
1072
+ }
897
1073
 
898
1074
  export { index as default };
899
1075
  //# sourceMappingURL=index.mjs.map