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 +5 -2
- package/dist/plugin/esm/index.mjs +103 -54
- package/dist/plugin/esm/index.mjs.map +1 -1
- package/dist/plugin/index.d.ts +10 -0
- package/dist/plugin/index.js +103 -54
- package/dist/plugin/index.js.map +1 -1
- package/dist/runtime/esm/index.mjs +16 -4
- package/dist/runtime/esm/index.mjs.map +1 -1
- package/dist/runtime/esm/index.web.mjs +2 -1
- package/dist/runtime/esm/index.web.mjs.map +1 -1
- package/dist/runtime/index.d.ts +16 -3
- package/dist/runtime/index.js +15 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/index.web.d.ts +2 -1
- package/dist/runtime/index.web.js +2 -0
- package/dist/runtime/index.web.js.map +1 -1
- package/package.json +5 -1
- package/src/plugin/index.ts +5 -1
- package/src/plugin/optimizers/text/index.ts +93 -31
- package/src/plugin/optimizers/view/index.ts +13 -22
- package/src/plugin/types/index.ts +17 -1
- package/src/plugin/utils/common/attributes.ts +71 -12
- package/src/plugin/utils/common/validation.ts +32 -9
- package/src/runtime/index.ts +39 -5
- package/src/runtime/index.web.ts +5 -0
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
|
-
<
|
|
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
|
-
|
|
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
|
|
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
|
|
677
|
-
if (types.
|
|
678
|
-
if (types.
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
799
|
-
attribute.name.name === "children" && !
|
|
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) =>
|
|
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
|
|
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 (
|
|
831
|
-
const
|
|
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(
|
|
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 (
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
906
|
+
if (shouldNormalize && isNormalizedProperty(attribute)) continue;
|
|
871
907
|
remainingAttributes.push(attribute);
|
|
872
908
|
}
|
|
873
|
-
|
|
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
|
-
//
|
|
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
|
-
"
|
|
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
|
}
|