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 +13 -7
- package/dist/plugin/esm/index.mjs +219 -43
- package/dist/plugin/esm/index.mjs.map +1 -1
- package/dist/plugin/index.d.ts +54 -0
- package/dist/plugin/index.js +219 -43
- package/dist/plugin/index.js.map +1 -1
- package/dist/runtime/esm/index.mjs +15 -3
- package/dist/runtime/esm/index.mjs.map +1 -1
- package/dist/runtime/esm/index.web.mjs.map +1 -1
- package/dist/runtime/index.d.ts +49 -3
- package/dist/runtime/index.js +16 -4
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/index.web.d.ts +11 -0
- package/dist/runtime/index.web.js.map +1 -1
- package/package.json +3 -2
- package/src/plugin/index.ts +26 -4
- package/src/plugin/optimizers/text/index.ts +52 -21
- package/src/plugin/optimizers/view/index.ts +57 -10
- package/src/plugin/types/index.ts +61 -21
- package/src/plugin/utils/common/validation.ts +23 -28
- package/src/plugin/utils/generate-test-plugin.ts +7 -1
- package/src/plugin/utils/helpers.ts +15 -0
- package/src/plugin/utils/logger.ts +123 -2
- package/src/runtime/components/native-text.tsx +21 -5
- package/src/runtime/components/native-view.tsx +21 -5
- package/src/runtime/index.ts +20 -0
- package/src/runtime/types/index.ts +5 -0
- package/src/runtime/utils/constants.ts +6 -2
package/dist/plugin/index.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
117
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
782
|
-
|
|
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
|
|
843
|
-
|
|
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,
|
|
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
|
-
|
|
869
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
|
890
|
-
const
|
|
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
|