react-native-boost 0.0.5 → 0.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 CHANGED
@@ -9,17 +9,33 @@ A powerful Babel plugin that automatically optimizes React Native apps through s
9
9
 
10
10
  - ⚡ Automatic performance optimization through source code analysis
11
11
  - 🔒 Safe optimizations that don't break your app
12
- - 🎯 Zero runtime overhead - all optimizations happen during build time
12
+ - 🎯 Virtually zero runtime overhead
13
13
  - 📱 Cross-platform compatible
14
14
  - 🧪 Works seamlessly with Expo
15
15
  - 🎨 Configurable optimization strategies
16
16
 
17
+ ## Benchmark
18
+
19
+ The example app in the `apps/example` directory is a benchmark for the performance of the plugin.
20
+
21
+ <div align="center">
22
+ <img src="./packages/react-native-boost/img/benchmark.png" width="500" />
23
+ <p>
24
+ <b>1,000 Text components</b>: Render time of 1,000 Text components with and without React Native Boost.<br/>
25
+ Measured in milliseconds on an iPhone 16 Pro in development mode, lower is better.
26
+ </p>
27
+ </div>
28
+
29
+ The data is averaged over 10 runs on an iPhone 16 Pro Simulator running the app in development mode.
30
+
17
31
  ## Installation
18
32
 
33
+ Install the package using your favorite package manager. Please **do not** install the package as a dev dependency. While the Babel plugin itself would work as a dev dependency, some optimizations import minimal helpers into your code, which requires the package to be installed as a regular dependency.
34
+
19
35
  ```sh
20
- npm install --save-dev react-native-boost
36
+ npm install react-native-boost
21
37
  # or
22
- yarn add --dev react-native-boost
38
+ yarn add react-native-boost
23
39
  ```
24
40
 
25
41
  Then, add the plugin to your Babel configuration (`babel.config.js`):
@@ -30,6 +46,8 @@ module.exports = {
30
46
  };
31
47
  ```
32
48
 
49
+ That's it! No imports in your code, pod installing, or anything else is required.
50
+
33
51
  Optionally, you can configure the plugin to log optimized files to the console and disable specific optimizations:
34
52
 
35
53
  ```js
@@ -48,6 +66,13 @@ module.exports = {
48
66
  };
49
67
  ```
50
68
 
69
+ You can also skip optimization for a specific comment using a decorator comment above the component's opening tag:
70
+
71
+ ```jsx
72
+ // @boost-ignore
73
+ <Text>This will not be optimized.</Text>
74
+ ```
75
+
51
76
  ## How It Works
52
77
 
53
78
  Several standard components in React Native are actually wrappers around their native counterparts. These wrappers often only handle edge cases and aren't needed in most cases. However, they add additional runtime overhead and depth to the component tree, which can lead to performance bottlenecks.
@@ -0,0 +1,41 @@
1
+ import { flattenStyle } from 'react-native/Libraries/StyleSheet/flattenStyle';
2
+
3
+ const propsCache = /* @__PURE__ */ new WeakMap();
4
+ function flattenTextStyle(style) {
5
+ if (!style) return {};
6
+ let props = propsCache.get(style);
7
+ if (props) return props;
8
+ props = {};
9
+ propsCache.set(style, props);
10
+ style = flattenStyle(style);
11
+ if (!style) return {};
12
+ if (typeof (style == null ? void 0 : style.fontWeight) === "number") {
13
+ style.fontWeight = style.fontWeight.toString();
14
+ }
15
+ if ((style == null ? void 0 : style.userSelect) != null) {
16
+ props.selectable = userSelectToSelectableMap[style.userSelect];
17
+ delete style.userSelect;
18
+ }
19
+ if ((style == null ? void 0 : style.verticalAlign) != null) {
20
+ style.textAlignVertical = verticalAlignToTextAlignVerticalMap[style.verticalAlign];
21
+ delete style.verticalAlign;
22
+ }
23
+ props.style = style;
24
+ return props;
25
+ }
26
+ const userSelectToSelectableMap = {
27
+ auto: true,
28
+ text: true,
29
+ none: false,
30
+ contain: true,
31
+ all: true
32
+ };
33
+ const verticalAlignToTextAlignVerticalMap = {
34
+ auto: "auto",
35
+ top: "top",
36
+ bottom: "bottom",
37
+ middle: "center"
38
+ };
39
+
40
+ export { flattenTextStyle, userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap };
41
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../../src/runtime/index.ts"],"sourcesContent":["import { TextStyle } from 'react-native';\nimport { flattenStyle } from 'react-native/Libraries/StyleSheet/flattenStyle';\nimport { GenericStyleProp } from './types';\n\nconst propsCache = new WeakMap();\n\nexport function flattenTextStyle(style: GenericStyleProp<TextStyle>) {\n if (!style) return {};\n\n // Cache the computed props\n let props = propsCache.get(style);\n if (props) return props;\n\n props = {};\n propsCache.set(style, props);\n\n style = flattenStyle(style);\n\n if (!style) return {};\n\n if (typeof style?.fontWeight === 'number') {\n style.fontWeight = style.fontWeight.toString() as TextStyle['fontWeight'];\n }\n\n if (style?.userSelect != null) {\n props.selectable = userSelectToSelectableMap[style.userSelect];\n delete style.userSelect;\n }\n\n if (style?.verticalAlign != null) {\n style.textAlignVertical = verticalAlignToTextAlignVerticalMap[\n style.verticalAlign\n ] as TextStyle['textAlignVertical'];\n delete style.verticalAlign;\n }\n\n props.style = style;\n return props;\n}\n\n// Maps the `userSelect` prop to the native `selectable` prop\nexport const userSelectToSelectableMap = {\n auto: true,\n text: true,\n none: false,\n contain: true,\n all: true,\n};\n\n// Maps the `verticalAlign` prop to the native `textAlignVertical` prop\nexport const verticalAlignToTextAlignVerticalMap = {\n auto: 'auto',\n top: 'top',\n bottom: 'bottom',\n middle: 'center',\n};\n\nexport * from './types';\n"],"names":[],"mappings":";;AAIA,MAAM,UAAA,uBAAiB,OAAQ,EAAA;AAExB,SAAS,iBAAiB,KAAoC,EAAA;AACnE,EAAI,IAAA,CAAC,KAAO,EAAA,OAAO,EAAC;AAGpB,EAAI,IAAA,KAAA,GAAQ,UAAW,CAAA,GAAA,CAAI,KAAK,CAAA;AAChC,EAAA,IAAI,OAAc,OAAA,KAAA;AAElB,EAAA,KAAA,GAAQ,EAAC;AACT,EAAW,UAAA,CAAA,GAAA,CAAI,OAAO,KAAK,CAAA;AAE3B,EAAA,KAAA,GAAQ,aAAa,KAAK,CAAA;AAE1B,EAAI,IAAA,CAAC,KAAO,EAAA,OAAO,EAAC;AAEpB,EAAI,IAAA,QAAO,KAAO,IAAA,IAAA,GAAA,MAAA,GAAA,KAAA,CAAA,UAAA,CAAA,KAAe,QAAU,EAAA;AACzC,IAAM,KAAA,CAAA,UAAA,GAAa,KAAM,CAAA,UAAA,CAAW,QAAS,EAAA;AAAA;AAG/C,EAAI,IAAA,CAAA,KAAA,IAAA,IAAA,GAAA,MAAA,GAAA,KAAA,CAAO,eAAc,IAAM,EAAA;AAC7B,IAAM,KAAA,CAAA,UAAA,GAAa,yBAA0B,CAAA,KAAA,CAAM,UAAU,CAAA;AAC7D,IAAA,OAAO,KAAM,CAAA,UAAA;AAAA;AAGf,EAAI,IAAA,CAAA,KAAA,IAAA,IAAA,GAAA,MAAA,GAAA,KAAA,CAAO,kBAAiB,IAAM,EAAA;AAChC,IAAM,KAAA,CAAA,iBAAA,GAAoB,mCACxB,CAAA,KAAA,CAAM,aACR,CAAA;AACA,IAAA,OAAO,KAAM,CAAA,aAAA;AAAA;AAGf,EAAA,KAAA,CAAM,KAAQ,GAAA,KAAA;AACd,EAAO,OAAA,KAAA;AACT;AAGO,MAAM,yBAA4B,GAAA;AAAA,EACvC,IAAM,EAAA,IAAA;AAAA,EACN,IAAM,EAAA,IAAA;AAAA,EACN,IAAM,EAAA,KAAA;AAAA,EACN,OAAS,EAAA,IAAA;AAAA,EACT,GAAK,EAAA;AACP;AAGO,MAAM,mCAAsC,GAAA;AAAA,EACjD,IAAM,EAAA,MAAA;AAAA,EACN,GAAK,EAAA,KAAA;AAAA,EACL,MAAQ,EAAA,QAAA;AAAA,EACR,MAAQ,EAAA;AACV;;;;"}
@@ -0,0 +1,20 @@
1
+ import { TextStyle } from 'react-native';
2
+
3
+ type GenericStyleProp<T> = null | void | T | false | '' | ReadonlyArray<GenericStyleProp<T>>;
4
+
5
+ declare function flattenTextStyle(style: GenericStyleProp<TextStyle>): any;
6
+ declare const userSelectToSelectableMap: {
7
+ auto: boolean;
8
+ text: boolean;
9
+ none: boolean;
10
+ contain: boolean;
11
+ all: boolean;
12
+ };
13
+ declare const verticalAlignToTextAlignVerticalMap: {
14
+ auto: string;
15
+ top: string;
16
+ bottom: string;
17
+ middle: string;
18
+ };
19
+
20
+ export { type GenericStyleProp, flattenTextStyle, userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap };
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ var flattenStyle = require('react-native/Libraries/StyleSheet/flattenStyle');
4
+
5
+ const propsCache = /* @__PURE__ */ new WeakMap();
6
+ function flattenTextStyle(style) {
7
+ if (!style) return {};
8
+ let props = propsCache.get(style);
9
+ if (props) return props;
10
+ props = {};
11
+ propsCache.set(style, props);
12
+ style = flattenStyle.flattenStyle(style);
13
+ if (!style) return {};
14
+ if (typeof (style == null ? void 0 : style.fontWeight) === "number") {
15
+ style.fontWeight = style.fontWeight.toString();
16
+ }
17
+ if ((style == null ? void 0 : style.userSelect) != null) {
18
+ props.selectable = userSelectToSelectableMap[style.userSelect];
19
+ delete style.userSelect;
20
+ }
21
+ if ((style == null ? void 0 : style.verticalAlign) != null) {
22
+ style.textAlignVertical = verticalAlignToTextAlignVerticalMap[style.verticalAlign];
23
+ delete style.verticalAlign;
24
+ }
25
+ props.style = style;
26
+ return props;
27
+ }
28
+ const userSelectToSelectableMap = {
29
+ auto: true,
30
+ text: true,
31
+ none: false,
32
+ contain: true,
33
+ all: true
34
+ };
35
+ const verticalAlignToTextAlignVerticalMap = {
36
+ auto: "auto",
37
+ top: "top",
38
+ bottom: "bottom",
39
+ middle: "center"
40
+ };
41
+
42
+ exports.flattenTextStyle = flattenTextStyle;
43
+ exports.userSelectToSelectableMap = userSelectToSelectableMap;
44
+ exports.verticalAlignToTextAlignVerticalMap = verticalAlignToTextAlignVerticalMap;
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/runtime/index.ts"],"sourcesContent":["import { TextStyle } from 'react-native';\nimport { flattenStyle } from 'react-native/Libraries/StyleSheet/flattenStyle';\nimport { GenericStyleProp } from './types';\n\nconst propsCache = new WeakMap();\n\nexport function flattenTextStyle(style: GenericStyleProp<TextStyle>) {\n if (!style) return {};\n\n // Cache the computed props\n let props = propsCache.get(style);\n if (props) return props;\n\n props = {};\n propsCache.set(style, props);\n\n style = flattenStyle(style);\n\n if (!style) return {};\n\n if (typeof style?.fontWeight === 'number') {\n style.fontWeight = style.fontWeight.toString() as TextStyle['fontWeight'];\n }\n\n if (style?.userSelect != null) {\n props.selectable = userSelectToSelectableMap[style.userSelect];\n delete style.userSelect;\n }\n\n if (style?.verticalAlign != null) {\n style.textAlignVertical = verticalAlignToTextAlignVerticalMap[\n style.verticalAlign\n ] as TextStyle['textAlignVertical'];\n delete style.verticalAlign;\n }\n\n props.style = style;\n return props;\n}\n\n// Maps the `userSelect` prop to the native `selectable` prop\nexport const userSelectToSelectableMap = {\n auto: true,\n text: true,\n none: false,\n contain: true,\n all: true,\n};\n\n// Maps the `verticalAlign` prop to the native `textAlignVertical` prop\nexport const verticalAlignToTextAlignVerticalMap = {\n auto: 'auto',\n top: 'top',\n bottom: 'bottom',\n middle: 'center',\n};\n\nexport * from './types';\n"],"names":["flattenStyle"],"mappings":";;;;AAIA,MAAM,UAAA,uBAAiB,OAAQ,EAAA;AAExB,SAAS,iBAAiB,KAAoC,EAAA;AACnE,EAAI,IAAA,CAAC,KAAO,EAAA,OAAO,EAAC;AAGpB,EAAI,IAAA,KAAA,GAAQ,UAAW,CAAA,GAAA,CAAI,KAAK,CAAA;AAChC,EAAA,IAAI,OAAc,OAAA,KAAA;AAElB,EAAA,KAAA,GAAQ,EAAC;AACT,EAAW,UAAA,CAAA,GAAA,CAAI,OAAO,KAAK,CAAA;AAE3B,EAAA,KAAA,GAAQA,0BAAa,KAAK,CAAA;AAE1B,EAAI,IAAA,CAAC,KAAO,EAAA,OAAO,EAAC;AAEpB,EAAI,IAAA,QAAO,KAAO,IAAA,IAAA,GAAA,MAAA,GAAA,KAAA,CAAA,UAAA,CAAA,KAAe,QAAU,EAAA;AACzC,IAAM,KAAA,CAAA,UAAA,GAAa,KAAM,CAAA,UAAA,CAAW,QAAS,EAAA;AAAA;AAG/C,EAAI,IAAA,CAAA,KAAA,IAAA,IAAA,GAAA,MAAA,GAAA,KAAA,CAAO,eAAc,IAAM,EAAA;AAC7B,IAAM,KAAA,CAAA,UAAA,GAAa,yBAA0B,CAAA,KAAA,CAAM,UAAU,CAAA;AAC7D,IAAA,OAAO,KAAM,CAAA,UAAA;AAAA;AAGf,EAAI,IAAA,CAAA,KAAA,IAAA,IAAA,GAAA,MAAA,GAAA,KAAA,CAAO,kBAAiB,IAAM,EAAA;AAChC,IAAM,KAAA,CAAA,iBAAA,GAAoB,mCACxB,CAAA,KAAA,CAAM,aACR,CAAA;AACA,IAAA,OAAO,KAAM,CAAA,aAAA;AAAA;AAGf,EAAA,KAAA,CAAM,KAAQ,GAAA,KAAA;AACd,EAAO,OAAA,KAAA;AACT;AAGO,MAAM,yBAA4B,GAAA;AAAA,EACvC,IAAM,EAAA,IAAA;AAAA,EACN,IAAM,EAAA,IAAA;AAAA,EACN,IAAM,EAAA,KAAA;AAAA,EACN,OAAS,EAAA,IAAA;AAAA,EACT,GAAK,EAAA;AACP;AAGO,MAAM,mCAAsC,GAAA;AAAA,EACjD,IAAM,EAAA,MAAA;AAAA,EACN,GAAK,EAAA,KAAA;AAAA,EACL,MAAQ,EAAA,QAAA;AAAA,EACR,MAAQ,EAAA;AACV;;;;;;"}
@@ -0,0 +1,218 @@
1
+ import { declare } from '@babel/helper-plugin-utils';
2
+ import { types } from '@babel/core';
3
+ import { addNamed } from '@babel/helper-module-imports';
4
+
5
+ class PluginError extends Error {
6
+ constructor(message) {
7
+ super(`[react-native-boost] Babel plugin exception: ${message}`);
8
+ this.name = "PluginError";
9
+ }
10
+ }
11
+
12
+ const ensureArray = (value) => {
13
+ if (Array.isArray(value)) return value;
14
+ return [value];
15
+ };
16
+
17
+ const shouldIgnoreOptimization = (path) => {
18
+ var _a, _b, _c;
19
+ if ((_a = path.node.leadingComments) == null ? void 0 : _a.some((comment) => comment.value.includes("@boost-ignore"))) {
20
+ return true;
21
+ }
22
+ const jsxElementPath = path.parentPath;
23
+ if ((_b = jsxElementPath.node.leadingComments) == null ? void 0 : _b.some((comment) => comment.value.includes("@boost-ignore"))) {
24
+ return true;
25
+ }
26
+ const propertyPath = jsxElementPath.parentPath;
27
+ if (propertyPath && propertyPath.isObjectProperty() && ((_c = propertyPath.node.leadingComments) == null ? void 0 : _c.some((comment) => comment.value.includes("@boost-ignore")))) {
28
+ return true;
29
+ }
30
+ if (!jsxElementPath.parentPath) return false;
31
+ const containerPath = jsxElementPath.parentPath;
32
+ const siblings = ensureArray(containerPath.get("children"));
33
+ const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
34
+ if (index === -1) return false;
35
+ for (let index_ = index - 1; index_ >= 0; index_--) {
36
+ const sibling = siblings[index_];
37
+ if (sibling.isJSXText() && sibling.node.value.trim() === "") {
38
+ continue;
39
+ }
40
+ if (sibling.isJSXExpressionContainer()) {
41
+ const expression = sibling.get("expression");
42
+ if (expression && expression.node) {
43
+ const comments = [
44
+ ...expression.node.leadingComments || [],
45
+ ...expression.node.trailingComments || [],
46
+ ...expression.node.innerComments || []
47
+ ].map((comment) => comment.value.trim());
48
+ if (comments.some((comment) => comment.includes("@boost-ignore"))) {
49
+ return true;
50
+ }
51
+ }
52
+ }
53
+ if (sibling.node.leadingComments && sibling.node.leadingComments.some((comment) => comment.value.includes("@boost-ignore"))) {
54
+ return true;
55
+ }
56
+ break;
57
+ }
58
+ return false;
59
+ };
60
+
61
+ const textOptimizer = (path, log = () => {
62
+ }) => {
63
+ var _a, _b, _c;
64
+ if (!types.isJSXIdentifier(path.node.name)) return;
65
+ const parent = path.parent;
66
+ if (!types.isJSXElement(parent)) return;
67
+ const elementName = path.node.name.name;
68
+ if (elementName !== "Text") return;
69
+ if (shouldIgnoreOptimization(path)) {
70
+ return;
71
+ }
72
+ const binding = path.scope.getBinding(elementName);
73
+ if (!binding) return;
74
+ if (binding.kind === "module") {
75
+ const parentNode = binding.path.parent;
76
+ if (!types.isImportDeclaration(parentNode) || parentNode.source.value !== "react-native") {
77
+ return;
78
+ }
79
+ }
80
+ if (hasBlacklistedProperties(path)) return;
81
+ if (!hasOnlyStringChildren(path, parent)) return;
82
+ const hub = path.hub;
83
+ const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
84
+ if (!file) {
85
+ throw new PluginError("No file found in Babel hub");
86
+ }
87
+ const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
88
+ const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
89
+ log(`Optimizing Text component in ${filename}:${lineNumber}`);
90
+ optimizeStyleTag({ path, file });
91
+ if (!file.__hasImports) {
92
+ file.__hasImports = {};
93
+ }
94
+ if (!file.__hasImports.NativeText) {
95
+ file.__hasImports.NativeText = addNamed(path, "NativeText", "react-native/Libraries/Text/TextNativeComponent");
96
+ }
97
+ const nativeTextIdentifier = file.__hasImports.NativeText;
98
+ path.node.name.name = nativeTextIdentifier.name;
99
+ if (!path.node.selfClosing && parent.closingElement && types.isJSXIdentifier(parent.closingElement.name) && parent.closingElement.name.name === "Text") {
100
+ parent.closingElement.name.name = nativeTextIdentifier.name;
101
+ }
102
+ };
103
+ function hasOnlyStringChildren(path, node) {
104
+ return node.children.every((child) => isStringNode(path, child));
105
+ }
106
+ function isStringNode(path, child) {
107
+ if (types.isJSXText(child)) return true;
108
+ if (types.isJSXExpressionContainer(child)) {
109
+ const expression = child.expression;
110
+ if (types.isIdentifier(expression)) {
111
+ const binding = path.scope.getBinding(expression.name);
112
+ return binding ? types.isStringLiteral(binding.path.node) : false;
113
+ }
114
+ }
115
+ return false;
116
+ }
117
+ const blacklistedProperties = /* @__PURE__ */ new Set([
118
+ "accessible",
119
+ "accessibilityLabel",
120
+ "accessibilityState",
121
+ "allowFontScaling",
122
+ "aria-busy",
123
+ "aria-checked",
124
+ "aria-disabled",
125
+ "aria-expanded",
126
+ "aria-label",
127
+ "aria-selected",
128
+ "ellipsizeMode",
129
+ "id",
130
+ "nativeID",
131
+ "onLongPress",
132
+ "onPress",
133
+ "onPressIn",
134
+ "onPressOut",
135
+ "onResponderGrant",
136
+ "onResponderMove",
137
+ "onResponderRelease",
138
+ "onResponderTerminate",
139
+ "onResponderTerminationRequest",
140
+ "onStartShouldSetResponder",
141
+ "pressRetentionOffset",
142
+ "suppressHighlighting"
143
+ ]);
144
+ function optimizeStyleTag({ path, file }) {
145
+ var _a;
146
+ let shouldImportFlattenTextStyle = false;
147
+ const nameHint = "_flattenTextStyle";
148
+ for (const [index, attribute] of path.node.attributes.entries()) {
149
+ if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "style" })) {
150
+ shouldImportFlattenTextStyle = true;
151
+ if (types.isJSXExpressionContainer(attribute.value) && !types.isJSXEmptyExpression(attribute.value.expression)) {
152
+ path.node.attributes[index] = types.jsxSpreadAttribute(
153
+ types.callExpression(types.identifier(nameHint), [attribute.value.expression])
154
+ );
155
+ }
156
+ }
157
+ }
158
+ if (shouldImportFlattenTextStyle && !((_a = file.__hasImports) == null ? void 0 : _a.flattenTextStyle)) {
159
+ if (!file.__hasImports) file.__hasImports = {};
160
+ file.__hasImports.flattenTextStyle = addNamed(path, "flattenTextStyle", "react-native-boost", { nameHint });
161
+ }
162
+ }
163
+ function hasBlacklistedProperties(path) {
164
+ return path.node.attributes.some((attribute) => {
165
+ if (types.isJSXSpreadAttribute(attribute)) {
166
+ if (types.isIdentifier(attribute.argument)) {
167
+ const binding = path.scope.getBinding(attribute.argument.name);
168
+ let objectExpression;
169
+ if (binding) {
170
+ if (types.isVariableDeclarator(binding.path.node)) {
171
+ objectExpression = binding.path.node.init;
172
+ } else if (types.isObjectExpression(binding.path.node)) {
173
+ objectExpression = binding.path.node;
174
+ }
175
+ }
176
+ if (objectExpression && types.isObjectExpression(objectExpression)) {
177
+ return objectExpression.properties.some((property) => {
178
+ if (types.isObjectProperty(property) && types.isIdentifier(property.key)) {
179
+ return blacklistedProperties.has(property.key.name);
180
+ }
181
+ return false;
182
+ });
183
+ }
184
+ }
185
+ return true;
186
+ }
187
+ if (types.isJSXIdentifier(attribute.name) && attribute.value) {
188
+ if (attribute.name.name === "children") {
189
+ return isStringNode(path, attribute.value);
190
+ }
191
+ return blacklistedProperties.has(attribute.name.name);
192
+ }
193
+ return false;
194
+ });
195
+ }
196
+
197
+ const log = (message) => {
198
+ console.log(`[react-native-boost] ${message}`);
199
+ };
200
+
201
+ var index = declare((api) => {
202
+ api.assertVersion(7);
203
+ return {
204
+ name: "react-native-boost",
205
+ visitor: {
206
+ JSXOpeningElement(path, state) {
207
+ var _a, _b;
208
+ const options = (_a = state.opts) != null ? _a : {};
209
+ const logger = options.verbose ? log : () => {
210
+ };
211
+ if (((_b = options.optimizations) == null ? void 0 : _b.text) !== false) textOptimizer(path, logger);
212
+ }
213
+ }
214
+ };
215
+ });
216
+
217
+ export { index as default };
218
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../../../src/plugin/utils/plugin-error.ts","../../../src/plugin/utils/helpers.ts","../../../src/plugin/utils/common.ts","../../../src/plugin/optimizers/text/index.ts","../../../src/plugin/utils/logger.ts","../../../src/plugin/index.ts"],"sourcesContent":["export default class PluginError extends Error {\n constructor(message: string) {\n super(`[react-native-boost] Babel plugin exception: ${message}`);\n this.name = 'PluginError';\n }\n}\n","export const ensureArray = <T>(value: T | T[]): T[] => {\n if (Array.isArray(value)) return value;\n return [value];\n};\n","import { NodePath, types as t } from '@babel/core';\nimport { ensureArray } from './helpers';\n\n/**\n * Checks if the JSX element should be ignored based on a preceding comment.\n *\n * The function looks up the JSXOpeningElement's own leading comments as well as\n * the parent element's comments before falling back to inspect siblings.\n */\nexport const shouldIgnoreOptimization = (path: NodePath<t.JSXOpeningElement>): boolean => {\n // Check for @boost-ignore in the leading comments on the JSX opening element.\n if (path.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {\n return true;\n }\n\n // Check for @boost-ignore in the leading comments on the parent JSX element.\n const jsxElementPath = path.parentPath;\n if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {\n return true;\n }\n\n // NEW: Check for @boost-ignore in the leading comments on the ObjectProperty (if it exists)\n // This handles cases where the JSX element is used as a value inside an object literal.\n const propertyPath = jsxElementPath.parentPath;\n if (\n propertyPath &&\n propertyPath.isObjectProperty() &&\n propertyPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))\n ) {\n return true;\n }\n\n if (!jsxElementPath.parentPath) return false;\n\n // Get the container that holds this element (for example, a JSX fragment or JSX element)\n const containerPath = jsxElementPath.parentPath;\n const siblings = ensureArray(containerPath.get('children'));\n const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);\n if (index === -1) return false;\n\n // Look backward from the current element for a non-empty node.\n for (let index_ = index - 1; index_ >= 0; index_--) {\n const sibling = siblings[index_];\n // Skip over any whitespace (only in JSXText nodes)\n if (sibling.isJSXText() && sibling.node.value.trim() === '') {\n continue;\n }\n // If the sibling is a JSX expression container, check its empty expression's comments.\n if (sibling.isJSXExpressionContainer()) {\n const expression = sibling.get('expression');\n if (expression && expression.node) {\n const comments = [\n ...(expression.node.leadingComments || []),\n ...(expression.node.trailingComments || []),\n ...(expression.node.innerComments || []),\n ].map((comment) => comment.value.trim());\n if (comments.some((comment) => comment.includes('@boost-ignore'))) {\n return true;\n }\n }\n }\n // Also check if the node itself carries a leadingComments property.\n if (\n sibling.node.leadingComments &&\n sibling.node.leadingComments.some((comment) => comment.value.includes('@boost-ignore'))\n ) {\n return true;\n }\n break; // if the immediate non-whitespace node is not our ignore marker, stop\n }\n return false;\n};\n","import { NodePath, types as t } from '@babel/core';\nimport { addNamed } from '@babel/helper-module-imports';\nimport { HubFile, Optimizer } from '../../types';\nimport PluginError from '../../utils/plugin-error';\nimport { shouldIgnoreOptimization } from '../../utils/common';\n\nexport const textOptimizer: Optimizer = (path, log = () => {}) => {\n // Ensure we're processing a JSX Text element\n if (!t.isJSXIdentifier(path.node.name)) return;\n\n const parent = path.parent;\n if (!t.isJSXElement(parent)) return;\n\n const elementName = path.node.name.name;\n if (elementName !== 'Text') return;\n\n // If the component is preceded by an ignore comment, do not optimize.\n if (shouldIgnoreOptimization(path)) {\n return;\n }\n\n // Ensure Text element comes from react-native\n const binding = path.scope.getBinding(elementName);\n if (!binding) return;\n if (binding.kind === 'module') {\n const parentNode = binding.path.parent;\n if (!t.isImportDeclaration(parentNode) || parentNode.source.value !== 'react-native') {\n return;\n }\n }\n\n // Bail if the element has any blacklisted properties or non-string children props\n if (hasBlacklistedProperties(path)) return;\n if (!hasOnlyStringChildren(path, parent)) return;\n\n // Extract the file from the Babel hub and add flags for logging & import caching\n const hub = path.hub as unknown;\n const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;\n\n if (!file) {\n throw new PluginError('No file found in Babel hub');\n }\n\n const filename = file.opts?.filename || 'unknown file';\n const lineNumber = path.node.loc?.start.line ?? 'unknown line';\n log(`Optimizing Text component in ${filename}:${lineNumber}`);\n\n // Optimize props\n optimizeStyleTag({ path, file });\n\n // Add TextNativeComponent import (cached on file) so we only add it once per file\n if (!file.__hasImports) {\n file.__hasImports = {};\n }\n if (!file.__hasImports.NativeText) {\n file.__hasImports.NativeText = addNamed(path, 'NativeText', 'react-native/Libraries/Text/TextNativeComponent');\n }\n const nativeTextIdentifier = file.__hasImports.NativeText;\n path.node.name.name = nativeTextIdentifier.name;\n\n // If the element is not self-closing, update the closing element as well\n if (\n !path.node.selfClosing &&\n parent.closingElement &&\n t.isJSXIdentifier(parent.closingElement.name) &&\n parent.closingElement.name.name === 'Text'\n ) {\n parent.closingElement.name.name = nativeTextIdentifier.name;\n }\n};\n\nfunction hasOnlyStringChildren(path: NodePath<t.JSXOpeningElement>, node: t.JSXElement): boolean {\n return node.children.every((child) => isStringNode(path, child));\n}\n\nfunction isStringNode(path: NodePath<t.JSXOpeningElement>, child: t.Node): boolean {\n if (t.isJSXText(child)) return true;\n\n // Check for JSX expressions\n if (t.isJSXExpressionContainer(child)) {\n const expression = child.expression;\n\n // If the expression is an identifier, look it up in the current scope\n if (t.isIdentifier(expression)) {\n const binding = path.scope.getBinding(expression.name);\n return binding ? t.isStringLiteral(binding.path.node) : false;\n }\n }\n return false;\n}\n\nconst blacklistedProperties = new Set([\n 'accessible',\n 'accessibilityLabel',\n 'accessibilityState',\n 'allowFontScaling',\n 'aria-busy',\n 'aria-checked',\n 'aria-disabled',\n 'aria-expanded',\n 'aria-label',\n 'aria-selected',\n 'ellipsizeMode',\n 'id',\n 'nativeID',\n 'onLongPress',\n 'onPress',\n 'onPressIn',\n 'onPressOut',\n 'onResponderGrant',\n 'onResponderMove',\n 'onResponderRelease',\n 'onResponderTerminate',\n 'onResponderTerminationRequest',\n 'onStartShouldSetResponder',\n 'pressRetentionOffset',\n 'suppressHighlighting',\n]);\n\nfunction optimizeStyleTag({ path, file }: { path: NodePath<t.JSXOpeningElement>; file: HubFile }) {\n let shouldImportFlattenTextStyle = false;\n const nameHint = '_flattenTextStyle';\n\n for (const [index, attribute] of path.node.attributes.entries()) {\n if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {\n shouldImportFlattenTextStyle = true;\n\n if (t.isJSXExpressionContainer(attribute.value) && !t.isJSXEmptyExpression(attribute.value.expression)) {\n path.node.attributes[index] = t.jsxSpreadAttribute(\n t.callExpression(t.identifier(nameHint), [attribute.value.expression])\n );\n }\n }\n }\n\n if (shouldImportFlattenTextStyle && !file.__hasImports?.flattenTextStyle) {\n if (!file.__hasImports) file.__hasImports = {};\n file.__hasImports.flattenTextStyle = addNamed(path, 'flattenTextStyle', 'react-native-boost', { nameHint });\n }\n}\n\nfunction hasBlacklistedProperties(path: NodePath<t.JSXOpeningElement>): boolean {\n return path.node.attributes.some((attribute) => {\n // Check if we can resolve the spread attribute\n if (t.isJSXSpreadAttribute(attribute)) {\n if (t.isIdentifier(attribute.argument)) {\n const binding = path.scope.getBinding(attribute.argument.name);\n let objectExpression: t.ObjectExpression | undefined;\n if (binding) {\n // If the binding node is a VariableDeclarator, use its initializer\n if (t.isVariableDeclarator(binding.path.node)) {\n objectExpression = binding.path.node.init as t.ObjectExpression;\n } else if (t.isObjectExpression(binding.path.node)) {\n objectExpression = binding.path.node;\n }\n }\n if (objectExpression && t.isObjectExpression(objectExpression)) {\n return objectExpression.properties.some((property) => {\n if (t.isObjectProperty(property) && t.isIdentifier(property.key)) {\n return blacklistedProperties.has(property.key.name);\n }\n return false;\n });\n }\n }\n // Bail if we can't resolve the spread attribute\n return true;\n }\n\n if (t.isJSXIdentifier(attribute.name) && attribute.value) {\n // For a \"children\" attribute, optimization is allowed only if it is a string\n if (attribute.name.name === 'children') {\n return isStringNode(path, attribute.value);\n }\n return blacklistedProperties.has(attribute.name.name);\n }\n\n // For other attribute types (e.g. namespaced), assume no blacklisting\n return false;\n });\n}\n","export const log = (message: string) => {\n console.log(`[react-native-boost] ${message}`);\n};\n","import { declare } from '@babel/helper-plugin-utils';\nimport { textOptimizer } from './optimizers/text';\nimport { PluginOptions } from './types';\nimport { log } from './utils/logger';\n\nexport default declare((api) => {\n api.assertVersion(7);\n\n return {\n name: 'react-native-boost',\n visitor: {\n JSXOpeningElement(path, state) {\n const options = (state.opts ?? {}) as PluginOptions;\n const logger = options.verbose ? log : () => {};\n if (options.optimizations?.text !== false) textOptimizer(path, logger);\n },\n },\n };\n});\n"],"names":["t"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import * as _babel_traverse from '@babel/traverse';
2
+ import * as _babel_types from '@babel/types';
3
+ import * as _babel_core from '@babel/core';
4
+
5
+ declare const _default: (api: object, options: Record<string, any> | null | undefined, dirname: string) => {
6
+ name: string;
7
+ visitor: {
8
+ JSXOpeningElement(this: _babel_core.PluginPass, path: _babel_traverse.NodePath<_babel_types.JSXOpeningElement>, state: _babel_core.PluginPass): void;
9
+ };
10
+ };
11
+
12
+ export { _default as default };
@@ -0,0 +1,220 @@
1
+ 'use strict';
2
+
3
+ var helperPluginUtils = require('@babel/helper-plugin-utils');
4
+ var core = require('@babel/core');
5
+ var helperModuleImports = require('@babel/helper-module-imports');
6
+
7
+ class PluginError extends Error {
8
+ constructor(message) {
9
+ super(`[react-native-boost] Babel plugin exception: ${message}`);
10
+ this.name = "PluginError";
11
+ }
12
+ }
13
+
14
+ const ensureArray = (value) => {
15
+ if (Array.isArray(value)) return value;
16
+ return [value];
17
+ };
18
+
19
+ const shouldIgnoreOptimization = (path) => {
20
+ var _a, _b, _c;
21
+ if ((_a = path.node.leadingComments) == null ? void 0 : _a.some((comment) => comment.value.includes("@boost-ignore"))) {
22
+ return true;
23
+ }
24
+ const jsxElementPath = path.parentPath;
25
+ if ((_b = jsxElementPath.node.leadingComments) == null ? void 0 : _b.some((comment) => comment.value.includes("@boost-ignore"))) {
26
+ return true;
27
+ }
28
+ const propertyPath = jsxElementPath.parentPath;
29
+ if (propertyPath && propertyPath.isObjectProperty() && ((_c = propertyPath.node.leadingComments) == null ? void 0 : _c.some((comment) => comment.value.includes("@boost-ignore")))) {
30
+ return true;
31
+ }
32
+ if (!jsxElementPath.parentPath) return false;
33
+ const containerPath = jsxElementPath.parentPath;
34
+ const siblings = ensureArray(containerPath.get("children"));
35
+ const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
36
+ if (index === -1) return false;
37
+ for (let index_ = index - 1; index_ >= 0; index_--) {
38
+ const sibling = siblings[index_];
39
+ if (sibling.isJSXText() && sibling.node.value.trim() === "") {
40
+ continue;
41
+ }
42
+ if (sibling.isJSXExpressionContainer()) {
43
+ const expression = sibling.get("expression");
44
+ if (expression && expression.node) {
45
+ const comments = [
46
+ ...expression.node.leadingComments || [],
47
+ ...expression.node.trailingComments || [],
48
+ ...expression.node.innerComments || []
49
+ ].map((comment) => comment.value.trim());
50
+ if (comments.some((comment) => comment.includes("@boost-ignore"))) {
51
+ return true;
52
+ }
53
+ }
54
+ }
55
+ if (sibling.node.leadingComments && sibling.node.leadingComments.some((comment) => comment.value.includes("@boost-ignore"))) {
56
+ return true;
57
+ }
58
+ break;
59
+ }
60
+ return false;
61
+ };
62
+
63
+ const textOptimizer = (path, log = () => {
64
+ }) => {
65
+ var _a, _b, _c;
66
+ if (!core.types.isJSXIdentifier(path.node.name)) return;
67
+ const parent = path.parent;
68
+ if (!core.types.isJSXElement(parent)) return;
69
+ const elementName = path.node.name.name;
70
+ if (elementName !== "Text") return;
71
+ if (shouldIgnoreOptimization(path)) {
72
+ return;
73
+ }
74
+ const binding = path.scope.getBinding(elementName);
75
+ if (!binding) return;
76
+ if (binding.kind === "module") {
77
+ const parentNode = binding.path.parent;
78
+ if (!core.types.isImportDeclaration(parentNode) || parentNode.source.value !== "react-native") {
79
+ return;
80
+ }
81
+ }
82
+ if (hasBlacklistedProperties(path)) return;
83
+ if (!hasOnlyStringChildren(path, parent)) return;
84
+ const hub = path.hub;
85
+ const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
86
+ if (!file) {
87
+ throw new PluginError("No file found in Babel hub");
88
+ }
89
+ const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
90
+ const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
91
+ log(`Optimizing Text component in ${filename}:${lineNumber}`);
92
+ optimizeStyleTag({ path, file });
93
+ if (!file.__hasImports) {
94
+ file.__hasImports = {};
95
+ }
96
+ if (!file.__hasImports.NativeText) {
97
+ file.__hasImports.NativeText = helperModuleImports.addNamed(path, "NativeText", "react-native/Libraries/Text/TextNativeComponent");
98
+ }
99
+ const nativeTextIdentifier = file.__hasImports.NativeText;
100
+ path.node.name.name = nativeTextIdentifier.name;
101
+ if (!path.node.selfClosing && parent.closingElement && core.types.isJSXIdentifier(parent.closingElement.name) && parent.closingElement.name.name === "Text") {
102
+ parent.closingElement.name.name = nativeTextIdentifier.name;
103
+ }
104
+ };
105
+ function hasOnlyStringChildren(path, node) {
106
+ return node.children.every((child) => isStringNode(path, child));
107
+ }
108
+ function isStringNode(path, child) {
109
+ if (core.types.isJSXText(child)) return true;
110
+ if (core.types.isJSXExpressionContainer(child)) {
111
+ const expression = child.expression;
112
+ if (core.types.isIdentifier(expression)) {
113
+ const binding = path.scope.getBinding(expression.name);
114
+ return binding ? core.types.isStringLiteral(binding.path.node) : false;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+ const blacklistedProperties = /* @__PURE__ */ new Set([
120
+ "accessible",
121
+ "accessibilityLabel",
122
+ "accessibilityState",
123
+ "allowFontScaling",
124
+ "aria-busy",
125
+ "aria-checked",
126
+ "aria-disabled",
127
+ "aria-expanded",
128
+ "aria-label",
129
+ "aria-selected",
130
+ "ellipsizeMode",
131
+ "id",
132
+ "nativeID",
133
+ "onLongPress",
134
+ "onPress",
135
+ "onPressIn",
136
+ "onPressOut",
137
+ "onResponderGrant",
138
+ "onResponderMove",
139
+ "onResponderRelease",
140
+ "onResponderTerminate",
141
+ "onResponderTerminationRequest",
142
+ "onStartShouldSetResponder",
143
+ "pressRetentionOffset",
144
+ "suppressHighlighting"
145
+ ]);
146
+ function optimizeStyleTag({ path, file }) {
147
+ var _a;
148
+ let shouldImportFlattenTextStyle = false;
149
+ const nameHint = "_flattenTextStyle";
150
+ for (const [index, attribute] of path.node.attributes.entries()) {
151
+ if (core.types.isJSXAttribute(attribute) && core.types.isJSXIdentifier(attribute.name, { name: "style" })) {
152
+ shouldImportFlattenTextStyle = true;
153
+ if (core.types.isJSXExpressionContainer(attribute.value) && !core.types.isJSXEmptyExpression(attribute.value.expression)) {
154
+ path.node.attributes[index] = core.types.jsxSpreadAttribute(
155
+ core.types.callExpression(core.types.identifier(nameHint), [attribute.value.expression])
156
+ );
157
+ }
158
+ }
159
+ }
160
+ if (shouldImportFlattenTextStyle && !((_a = file.__hasImports) == null ? void 0 : _a.flattenTextStyle)) {
161
+ if (!file.__hasImports) file.__hasImports = {};
162
+ file.__hasImports.flattenTextStyle = helperModuleImports.addNamed(path, "flattenTextStyle", "react-native-boost", { nameHint });
163
+ }
164
+ }
165
+ function hasBlacklistedProperties(path) {
166
+ return path.node.attributes.some((attribute) => {
167
+ if (core.types.isJSXSpreadAttribute(attribute)) {
168
+ if (core.types.isIdentifier(attribute.argument)) {
169
+ const binding = path.scope.getBinding(attribute.argument.name);
170
+ let objectExpression;
171
+ if (binding) {
172
+ if (core.types.isVariableDeclarator(binding.path.node)) {
173
+ objectExpression = binding.path.node.init;
174
+ } else if (core.types.isObjectExpression(binding.path.node)) {
175
+ objectExpression = binding.path.node;
176
+ }
177
+ }
178
+ if (objectExpression && core.types.isObjectExpression(objectExpression)) {
179
+ return objectExpression.properties.some((property) => {
180
+ if (core.types.isObjectProperty(property) && core.types.isIdentifier(property.key)) {
181
+ return blacklistedProperties.has(property.key.name);
182
+ }
183
+ return false;
184
+ });
185
+ }
186
+ }
187
+ return true;
188
+ }
189
+ if (core.types.isJSXIdentifier(attribute.name) && attribute.value) {
190
+ if (attribute.name.name === "children") {
191
+ return isStringNode(path, attribute.value);
192
+ }
193
+ return blacklistedProperties.has(attribute.name.name);
194
+ }
195
+ return false;
196
+ });
197
+ }
198
+
199
+ const log = (message) => {
200
+ console.log(`[react-native-boost] ${message}`);
201
+ };
202
+
203
+ var index = helperPluginUtils.declare((api) => {
204
+ api.assertVersion(7);
205
+ return {
206
+ name: "react-native-boost",
207
+ visitor: {
208
+ JSXOpeningElement(path, state) {
209
+ var _a, _b;
210
+ const options = (_a = state.opts) != null ? _a : {};
211
+ const logger = options.verbose ? log : () => {
212
+ };
213
+ if (((_b = options.optimizations) == null ? void 0 : _b.text) !== false) textOptimizer(path, logger);
214
+ }
215
+ }
216
+ };
217
+ });
218
+
219
+ module.exports = index;
220
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/plugin/utils/plugin-error.ts","../../src/plugin/utils/helpers.ts","../../src/plugin/utils/common.ts","../../src/plugin/optimizers/text/index.ts","../../src/plugin/utils/logger.ts","../../src/plugin/index.ts"],"sourcesContent":["export default class PluginError extends Error {\n constructor(message: string) {\n super(`[react-native-boost] Babel plugin exception: ${message}`);\n this.name = 'PluginError';\n }\n}\n","export const ensureArray = <T>(value: T | T[]): T[] => {\n if (Array.isArray(value)) return value;\n return [value];\n};\n","import { NodePath, types as t } from '@babel/core';\nimport { ensureArray } from './helpers';\n\n/**\n * Checks if the JSX element should be ignored based on a preceding comment.\n *\n * The function looks up the JSXOpeningElement's own leading comments as well as\n * the parent element's comments before falling back to inspect siblings.\n */\nexport const shouldIgnoreOptimization = (path: NodePath<t.JSXOpeningElement>): boolean => {\n // Check for @boost-ignore in the leading comments on the JSX opening element.\n if (path.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {\n return true;\n }\n\n // Check for @boost-ignore in the leading comments on the parent JSX element.\n const jsxElementPath = path.parentPath;\n if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {\n return true;\n }\n\n // NEW: Check for @boost-ignore in the leading comments on the ObjectProperty (if it exists)\n // This handles cases where the JSX element is used as a value inside an object literal.\n const propertyPath = jsxElementPath.parentPath;\n if (\n propertyPath &&\n propertyPath.isObjectProperty() &&\n propertyPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))\n ) {\n return true;\n }\n\n if (!jsxElementPath.parentPath) return false;\n\n // Get the container that holds this element (for example, a JSX fragment or JSX element)\n const containerPath = jsxElementPath.parentPath;\n const siblings = ensureArray(containerPath.get('children'));\n const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);\n if (index === -1) return false;\n\n // Look backward from the current element for a non-empty node.\n for (let index_ = index - 1; index_ >= 0; index_--) {\n const sibling = siblings[index_];\n // Skip over any whitespace (only in JSXText nodes)\n if (sibling.isJSXText() && sibling.node.value.trim() === '') {\n continue;\n }\n // If the sibling is a JSX expression container, check its empty expression's comments.\n if (sibling.isJSXExpressionContainer()) {\n const expression = sibling.get('expression');\n if (expression && expression.node) {\n const comments = [\n ...(expression.node.leadingComments || []),\n ...(expression.node.trailingComments || []),\n ...(expression.node.innerComments || []),\n ].map((comment) => comment.value.trim());\n if (comments.some((comment) => comment.includes('@boost-ignore'))) {\n return true;\n }\n }\n }\n // Also check if the node itself carries a leadingComments property.\n if (\n sibling.node.leadingComments &&\n sibling.node.leadingComments.some((comment) => comment.value.includes('@boost-ignore'))\n ) {\n return true;\n }\n break; // if the immediate non-whitespace node is not our ignore marker, stop\n }\n return false;\n};\n","import { NodePath, types as t } from '@babel/core';\nimport { addNamed } from '@babel/helper-module-imports';\nimport { HubFile, Optimizer } from '../../types';\nimport PluginError from '../../utils/plugin-error';\nimport { shouldIgnoreOptimization } from '../../utils/common';\n\nexport const textOptimizer: Optimizer = (path, log = () => {}) => {\n // Ensure we're processing a JSX Text element\n if (!t.isJSXIdentifier(path.node.name)) return;\n\n const parent = path.parent;\n if (!t.isJSXElement(parent)) return;\n\n const elementName = path.node.name.name;\n if (elementName !== 'Text') return;\n\n // If the component is preceded by an ignore comment, do not optimize.\n if (shouldIgnoreOptimization(path)) {\n return;\n }\n\n // Ensure Text element comes from react-native\n const binding = path.scope.getBinding(elementName);\n if (!binding) return;\n if (binding.kind === 'module') {\n const parentNode = binding.path.parent;\n if (!t.isImportDeclaration(parentNode) || parentNode.source.value !== 'react-native') {\n return;\n }\n }\n\n // Bail if the element has any blacklisted properties or non-string children props\n if (hasBlacklistedProperties(path)) return;\n if (!hasOnlyStringChildren(path, parent)) return;\n\n // Extract the file from the Babel hub and add flags for logging & import caching\n const hub = path.hub as unknown;\n const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;\n\n if (!file) {\n throw new PluginError('No file found in Babel hub');\n }\n\n const filename = file.opts?.filename || 'unknown file';\n const lineNumber = path.node.loc?.start.line ?? 'unknown line';\n log(`Optimizing Text component in ${filename}:${lineNumber}`);\n\n // Optimize props\n optimizeStyleTag({ path, file });\n\n // Add TextNativeComponent import (cached on file) so we only add it once per file\n if (!file.__hasImports) {\n file.__hasImports = {};\n }\n if (!file.__hasImports.NativeText) {\n file.__hasImports.NativeText = addNamed(path, 'NativeText', 'react-native/Libraries/Text/TextNativeComponent');\n }\n const nativeTextIdentifier = file.__hasImports.NativeText;\n path.node.name.name = nativeTextIdentifier.name;\n\n // If the element is not self-closing, update the closing element as well\n if (\n !path.node.selfClosing &&\n parent.closingElement &&\n t.isJSXIdentifier(parent.closingElement.name) &&\n parent.closingElement.name.name === 'Text'\n ) {\n parent.closingElement.name.name = nativeTextIdentifier.name;\n }\n};\n\nfunction hasOnlyStringChildren(path: NodePath<t.JSXOpeningElement>, node: t.JSXElement): boolean {\n return node.children.every((child) => isStringNode(path, child));\n}\n\nfunction isStringNode(path: NodePath<t.JSXOpeningElement>, child: t.Node): boolean {\n if (t.isJSXText(child)) return true;\n\n // Check for JSX expressions\n if (t.isJSXExpressionContainer(child)) {\n const expression = child.expression;\n\n // If the expression is an identifier, look it up in the current scope\n if (t.isIdentifier(expression)) {\n const binding = path.scope.getBinding(expression.name);\n return binding ? t.isStringLiteral(binding.path.node) : false;\n }\n }\n return false;\n}\n\nconst blacklistedProperties = new Set([\n 'accessible',\n 'accessibilityLabel',\n 'accessibilityState',\n 'allowFontScaling',\n 'aria-busy',\n 'aria-checked',\n 'aria-disabled',\n 'aria-expanded',\n 'aria-label',\n 'aria-selected',\n 'ellipsizeMode',\n 'id',\n 'nativeID',\n 'onLongPress',\n 'onPress',\n 'onPressIn',\n 'onPressOut',\n 'onResponderGrant',\n 'onResponderMove',\n 'onResponderRelease',\n 'onResponderTerminate',\n 'onResponderTerminationRequest',\n 'onStartShouldSetResponder',\n 'pressRetentionOffset',\n 'suppressHighlighting',\n]);\n\nfunction optimizeStyleTag({ path, file }: { path: NodePath<t.JSXOpeningElement>; file: HubFile }) {\n let shouldImportFlattenTextStyle = false;\n const nameHint = '_flattenTextStyle';\n\n for (const [index, attribute] of path.node.attributes.entries()) {\n if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {\n shouldImportFlattenTextStyle = true;\n\n if (t.isJSXExpressionContainer(attribute.value) && !t.isJSXEmptyExpression(attribute.value.expression)) {\n path.node.attributes[index] = t.jsxSpreadAttribute(\n t.callExpression(t.identifier(nameHint), [attribute.value.expression])\n );\n }\n }\n }\n\n if (shouldImportFlattenTextStyle && !file.__hasImports?.flattenTextStyle) {\n if (!file.__hasImports) file.__hasImports = {};\n file.__hasImports.flattenTextStyle = addNamed(path, 'flattenTextStyle', 'react-native-boost', { nameHint });\n }\n}\n\nfunction hasBlacklistedProperties(path: NodePath<t.JSXOpeningElement>): boolean {\n return path.node.attributes.some((attribute) => {\n // Check if we can resolve the spread attribute\n if (t.isJSXSpreadAttribute(attribute)) {\n if (t.isIdentifier(attribute.argument)) {\n const binding = path.scope.getBinding(attribute.argument.name);\n let objectExpression: t.ObjectExpression | undefined;\n if (binding) {\n // If the binding node is a VariableDeclarator, use its initializer\n if (t.isVariableDeclarator(binding.path.node)) {\n objectExpression = binding.path.node.init as t.ObjectExpression;\n } else if (t.isObjectExpression(binding.path.node)) {\n objectExpression = binding.path.node;\n }\n }\n if (objectExpression && t.isObjectExpression(objectExpression)) {\n return objectExpression.properties.some((property) => {\n if (t.isObjectProperty(property) && t.isIdentifier(property.key)) {\n return blacklistedProperties.has(property.key.name);\n }\n return false;\n });\n }\n }\n // Bail if we can't resolve the spread attribute\n return true;\n }\n\n if (t.isJSXIdentifier(attribute.name) && attribute.value) {\n // For a \"children\" attribute, optimization is allowed only if it is a string\n if (attribute.name.name === 'children') {\n return isStringNode(path, attribute.value);\n }\n return blacklistedProperties.has(attribute.name.name);\n }\n\n // For other attribute types (e.g. namespaced), assume no blacklisting\n return false;\n });\n}\n","export const log = (message: string) => {\n console.log(`[react-native-boost] ${message}`);\n};\n","import { declare } from '@babel/helper-plugin-utils';\nimport { textOptimizer } from './optimizers/text';\nimport { PluginOptions } from './types';\nimport { log } from './utils/logger';\n\nexport default declare((api) => {\n api.assertVersion(7);\n\n return {\n name: 'react-native-boost',\n visitor: {\n JSXOpeningElement(path, state) {\n const options = (state.opts ?? {}) as PluginOptions;\n const logger = options.verbose ? log : () => {};\n if (options.optimizations?.text !== false) textOptimizer(path, logger);\n },\n },\n };\n});\n"],"names":["t","addNamed","declare"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,27 @@
1
1
  {
2
2
  "name": "react-native-boost",
3
3
  "description": "🚀 Boost your React Native app's performance with a single line of code",
4
- "version": "0.0.5",
5
- "main": "dist/plugin.js",
4
+ "version": "0.2.0",
5
+ "main": "dist/index.js",
6
+ "module": "dist/esm/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": {
11
+ "import": {
12
+ "default": "./dist/esm/index.mjs",
13
+ "types": "./dist/index.d.ts"
14
+ },
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./plugin": {
18
+ "import": {
19
+ "default": "./dist/plugin/esm/index.mjs",
20
+ "types": "./dist/plugin/index.d.ts"
21
+ },
22
+ "default": "./dist/plugin/index.js"
23
+ }
24
+ },
6
25
  "keywords": [
7
26
  "react-native",
8
27
  "ios",
@@ -13,8 +32,8 @@
13
32
  "optimize"
14
33
  ],
15
34
  "scripts": {
16
- "clean": "rm -rf plugin",
17
- "build": "yarn clean && node scripts/esbuild.js",
35
+ "clean": "rm -rf dist",
36
+ "build": "yarn clean && rollup -c",
18
37
  "test": "vitest",
19
38
  "typecheck": "tsc --noEmit",
20
39
  "lint": "eslint src/**/*.ts",
@@ -25,7 +44,7 @@
25
44
  },
26
45
  "files": [
27
46
  "src",
28
- "plugin",
47
+ "dist",
29
48
  "!**/__tests__",
30
49
  "!**/__fixtures__",
31
50
  "!**/__mocks__",
@@ -54,27 +73,28 @@
54
73
  "@babel/plugin-syntax-jsx": "^7.25.0",
55
74
  "@babel/preset-typescript": "^7.25.0",
56
75
  "@release-it/conventional-changelog": "^10.0.0",
76
+ "@rollup/plugin-alias": "^5.1.1",
77
+ "@rollup/plugin-node-resolve": "^16.0.0",
78
+ "@rollup/plugin-replace": "^6.0.2",
79
+ "@rollup/plugin-typescript": "^12.1.2",
57
80
  "@types/babel__helper-module-imports": "^7.0.0",
58
81
  "@types/babel__helper-plugin-utils": "^7.0.0",
59
82
  "@types/node": "^20",
60
83
  "babel-plugin-tester": "^11.0.4",
61
- "esbuild": "^0.25.0",
62
84
  "esbuild-node-externals": "^1.18.0",
63
85
  "globals": "^16.0.0",
86
+ "react-native": "0.76.7",
64
87
  "release-it": "^18.1.2",
88
+ "rollup": "^4.34.8",
89
+ "rollup-plugin-dts": "^6.1.1",
90
+ "rollup-plugin-esbuild": "^6.2.0",
65
91
  "typescript": "^5.7.3",
66
92
  "vitest": "^3.0.6"
67
93
  },
68
94
  "peerDependencies": {
69
- "expo": "*",
70
95
  "react": "*",
71
96
  "react-native": "*"
72
97
  },
73
- "peerDependenciesMeta": {
74
- "expo": {
75
- "optional": true
76
- }
77
- },
78
98
  "release-it": {
79
99
  "git": {
80
100
  "commitMessage": "chore: release ${version}",
@@ -2,6 +2,7 @@ import { NodePath, types as t } from '@babel/core';
2
2
  import { addNamed } from '@babel/helper-module-imports';
3
3
  import { HubFile, Optimizer } from '../../types';
4
4
  import PluginError from '../../utils/plugin-error';
5
+ import { shouldIgnoreOptimization } from '../../utils/common';
5
6
 
6
7
  export const textOptimizer: Optimizer = (path, log = () => {}) => {
7
8
  // Ensure we're processing a JSX Text element
@@ -13,6 +14,11 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
13
14
  const elementName = path.node.name.name;
14
15
  if (elementName !== 'Text') return;
15
16
 
17
+ // If the component is preceded by an ignore comment, do not optimize.
18
+ if (shouldIgnoreOptimization(path)) {
19
+ return;
20
+ }
21
+
16
22
  // Ensure Text element comes from react-native
17
23
  const binding = path.scope.getBinding(elementName);
18
24
  if (!binding) return;
@@ -26,7 +32,6 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
26
32
  // Bail if the element has any blacklisted properties or non-string children props
27
33
  if (hasBlacklistedProperties(path)) return;
28
34
  if (!hasOnlyStringChildren(path, parent)) return;
29
- // TODO: Don't bail if the element has a style prop
30
35
 
31
36
  // Extract the file from the Babel hub and add flags for logging & import caching
32
37
  const hub = path.hub as unknown;
@@ -36,18 +41,21 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
36
41
  throw new PluginError('No file found in Babel hub');
37
42
  }
38
43
 
39
- // Log the file's optimized status only once
40
- if (!file.__optimized) {
41
- const filename = file.opts?.filename || 'unknown file';
42
- log(`Optimizing file: ${filename}`);
43
- file.__optimized = true;
44
- }
44
+ const filename = file.opts?.filename || 'unknown file';
45
+ const lineNumber = path.node.loc?.start.line ?? 'unknown line';
46
+ log(`Optimizing Text component in ${filename}:${lineNumber}`);
47
+
48
+ // Optimize props
49
+ optimizeStyleTag({ path, file });
45
50
 
46
51
  // Add TextNativeComponent import (cached on file) so we only add it once per file
47
- if (!file.__nativeTextImport) {
48
- file.__nativeTextImport = addNamed(path, 'NativeText', 'react-native/Libraries/Text/TextNativeComponent');
52
+ if (!file.__hasImports) {
53
+ file.__hasImports = {};
49
54
  }
50
- const nativeTextIdentifier = file.__nativeTextImport;
55
+ if (!file.__hasImports.NativeText) {
56
+ file.__hasImports.NativeText = addNamed(path, 'NativeText', 'react-native/Libraries/Text/TextNativeComponent');
57
+ }
58
+ const nativeTextIdentifier = file.__hasImports.NativeText;
51
59
  path.node.name.name = nativeTextIdentifier.name;
52
60
 
53
61
  // If the element is not self-closing, update the closing element as well
@@ -107,9 +115,30 @@ const blacklistedProperties = new Set([
107
115
  'onStartShouldSetResponder',
108
116
  'pressRetentionOffset',
109
117
  'suppressHighlighting',
110
- 'style',
111
118
  ]);
112
119
 
120
+ function optimizeStyleTag({ path, file }: { path: NodePath<t.JSXOpeningElement>; file: HubFile }) {
121
+ let shouldImportFlattenTextStyle = false;
122
+ const nameHint = '_flattenTextStyle';
123
+
124
+ for (const [index, attribute] of path.node.attributes.entries()) {
125
+ if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {
126
+ shouldImportFlattenTextStyle = true;
127
+
128
+ if (t.isJSXExpressionContainer(attribute.value) && !t.isJSXEmptyExpression(attribute.value.expression)) {
129
+ path.node.attributes[index] = t.jsxSpreadAttribute(
130
+ t.callExpression(t.identifier(nameHint), [attribute.value.expression])
131
+ );
132
+ }
133
+ }
134
+ }
135
+
136
+ if (shouldImportFlattenTextStyle && !file.__hasImports?.flattenTextStyle) {
137
+ if (!file.__hasImports) file.__hasImports = {};
138
+ file.__hasImports.flattenTextStyle = addNamed(path, 'flattenTextStyle', 'react-native-boost', { nameHint });
139
+ }
140
+ }
141
+
113
142
  function hasBlacklistedProperties(path: NodePath<t.JSXOpeningElement>): boolean {
114
143
  return path.node.attributes.some((attribute) => {
115
144
  // Check if we can resolve the spread attribute
@@ -24,6 +24,6 @@ export type HubFile = t.File & {
24
24
  opts: {
25
25
  filename: string;
26
26
  };
27
- __nativeTextImport?: t.Identifier;
27
+ __hasImports?: Record<string, t.Identifier>;
28
28
  __optimized?: boolean;
29
29
  };
@@ -0,0 +1,72 @@
1
+ import { NodePath, types as t } from '@babel/core';
2
+ import { ensureArray } from './helpers';
3
+
4
+ /**
5
+ * Checks if the JSX element should be ignored based on a preceding comment.
6
+ *
7
+ * The function looks up the JSXOpeningElement's own leading comments as well as
8
+ * the parent element's comments before falling back to inspect siblings.
9
+ */
10
+ export const shouldIgnoreOptimization = (path: NodePath<t.JSXOpeningElement>): boolean => {
11
+ // Check for @boost-ignore in the leading comments on the JSX opening element.
12
+ if (path.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {
13
+ return true;
14
+ }
15
+
16
+ // Check for @boost-ignore in the leading comments on the parent JSX element.
17
+ const jsxElementPath = path.parentPath;
18
+ if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {
19
+ return true;
20
+ }
21
+
22
+ // NEW: Check for @boost-ignore in the leading comments on the ObjectProperty (if it exists)
23
+ // This handles cases where the JSX element is used as a value inside an object literal.
24
+ const propertyPath = jsxElementPath.parentPath;
25
+ if (
26
+ propertyPath &&
27
+ propertyPath.isObjectProperty() &&
28
+ propertyPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))
29
+ ) {
30
+ return true;
31
+ }
32
+
33
+ if (!jsxElementPath.parentPath) return false;
34
+
35
+ // Get the container that holds this element (for example, a JSX fragment or JSX element)
36
+ const containerPath = jsxElementPath.parentPath;
37
+ const siblings = ensureArray(containerPath.get('children'));
38
+ const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
39
+ if (index === -1) return false;
40
+
41
+ // Look backward from the current element for a non-empty node.
42
+ for (let index_ = index - 1; index_ >= 0; index_--) {
43
+ const sibling = siblings[index_];
44
+ // Skip over any whitespace (only in JSXText nodes)
45
+ if (sibling.isJSXText() && sibling.node.value.trim() === '') {
46
+ continue;
47
+ }
48
+ // If the sibling is a JSX expression container, check its empty expression's comments.
49
+ if (sibling.isJSXExpressionContainer()) {
50
+ const expression = sibling.get('expression');
51
+ if (expression && expression.node) {
52
+ const comments = [
53
+ ...(expression.node.leadingComments || []),
54
+ ...(expression.node.trailingComments || []),
55
+ ...(expression.node.innerComments || []),
56
+ ].map((comment) => comment.value.trim());
57
+ if (comments.some((comment) => comment.includes('@boost-ignore'))) {
58
+ return true;
59
+ }
60
+ }
61
+ }
62
+ // Also check if the node itself carries a leadingComments property.
63
+ if (
64
+ sibling.node.leadingComments &&
65
+ sibling.node.leadingComments.some((comment) => comment.value.includes('@boost-ignore'))
66
+ ) {
67
+ return true;
68
+ }
69
+ break; // if the immediate non-whitespace node is not our ignore marker, stop
70
+ }
71
+ return false;
72
+ };
@@ -0,0 +1,4 @@
1
+ export const ensureArray = <T>(value: T | T[]): T[] => {
2
+ if (Array.isArray(value)) return value;
3
+ return [value];
4
+ };
@@ -0,0 +1,58 @@
1
+ import { TextStyle } from 'react-native';
2
+ import { flattenStyle } from 'react-native/Libraries/StyleSheet/flattenStyle';
3
+ import { GenericStyleProp } from './types';
4
+
5
+ const propsCache = new WeakMap();
6
+
7
+ export function flattenTextStyle(style: GenericStyleProp<TextStyle>) {
8
+ if (!style) return {};
9
+
10
+ // Cache the computed props
11
+ let props = propsCache.get(style);
12
+ if (props) return props;
13
+
14
+ props = {};
15
+ propsCache.set(style, props);
16
+
17
+ style = flattenStyle(style);
18
+
19
+ if (!style) return {};
20
+
21
+ if (typeof style?.fontWeight === 'number') {
22
+ style.fontWeight = style.fontWeight.toString() as TextStyle['fontWeight'];
23
+ }
24
+
25
+ if (style?.userSelect != null) {
26
+ props.selectable = userSelectToSelectableMap[style.userSelect];
27
+ delete style.userSelect;
28
+ }
29
+
30
+ if (style?.verticalAlign != null) {
31
+ style.textAlignVertical = verticalAlignToTextAlignVerticalMap[
32
+ style.verticalAlign
33
+ ] as TextStyle['textAlignVertical'];
34
+ delete style.verticalAlign;
35
+ }
36
+
37
+ props.style = style;
38
+ return props;
39
+ }
40
+
41
+ // Maps the `userSelect` prop to the native `selectable` prop
42
+ export const userSelectToSelectableMap = {
43
+ auto: true,
44
+ text: true,
45
+ none: false,
46
+ contain: true,
47
+ all: true,
48
+ };
49
+
50
+ // Maps the `verticalAlign` prop to the native `textAlignVertical` prop
51
+ export const verticalAlignToTextAlignVerticalMap = {
52
+ auto: 'auto',
53
+ top: 'top',
54
+ bottom: 'bottom',
55
+ middle: 'center',
56
+ };
57
+
58
+ export * from './types';
@@ -0,0 +1 @@
1
+ export type GenericStyleProp<T> = null | void | T | false | '' | ReadonlyArray<GenericStyleProp<T>>;
@@ -0,0 +1,5 @@
1
+ declare module 'react-native/Libraries/StyleSheet/flattenStyle' {
2
+ type GenericStyleProp<T> = null | void | T | false | '' | ReadonlyArray<GenericStyleProp<T>>;
3
+
4
+ export function flattenStyle<T>(style: T): T extends GenericStyleProp<infer U> ? U : never;
5
+ }
package/plugin/index.js DELETED
@@ -1 +0,0 @@
1
- "use strict";var d=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var P=(e,n)=>{for(var i in n)d(e,i,{get:n[i],enumerable:!0})},h=(e,n,i,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of O(n))!E.call(e,r)&&r!==i&&d(e,r,{get:()=>n[r],enumerable:!(t=v(n,r))||t.enumerable});return e};var I=e=>h(d({},"__esModule",{value:!0}),e);var X={};P(X,{default:()=>T});module.exports=I(X);var S=require("@babel/helper-plugin-utils");var o=require("@babel/core"),u=require("@babel/helper-module-imports");var l=class extends Error{constructor(n){super(`[react-native-boost] Babel plugin exception: ${n}`),this.name="PluginError"}};var g=(e,n=()=>{})=>{var p;if(!o.types.isJSXIdentifier(e.node.name))return;let i=e.parent;if(!o.types.isJSXElement(i))return;let t=e.node.name.name;if(t!=="Text")return;let r=e.scope.getBinding(t);if(!r)return;if(r.kind==="module"){let m=r.path.parent;if(!o.types.isImportDeclaration(m)||m.source.value!=="react-native")return}if(N(e)||!J(e,i))return;let s=e.hub,a=typeof s=="object"&&s!==null&&"file"in s?s.file:void 0;if(!a)throw new l("No file found in Babel hub");if(!a.__optimized){let m=((p=a.opts)==null?void 0:p.filename)||"unknown file";n(`Optimizing file: ${m}`),a.__optimized=!0}a.__nativeTextImport||(a.__nativeTextImport=(0,u.addNamed)(e,"NativeText","react-native/Libraries/Text/TextNativeComponent"));let f=a.__nativeTextImport;e.node.name.name=f.name,!e.node.selfClosing&&i.closingElement&&o.types.isJSXIdentifier(i.closingElement.name)&&i.closingElement.name.name==="Text"&&(i.closingElement.name.name=f.name)};function J(e,n){return n.children.every(i=>b(e,i))}function b(e,n){if(o.types.isJSXText(n))return!0;if(o.types.isJSXExpressionContainer(n)){let i=n.expression;if(o.types.isIdentifier(i)){let t=e.scope.getBinding(i.name);return t?o.types.isStringLiteral(t.path.node):!1}}return!1}var c=new Set(["accessible","accessibilityLabel","accessibilityState","allowFontScaling","aria-busy","aria-checked","aria-disabled","aria-expanded","aria-label","aria-selected","ellipsizeMode","id","nativeID","onLongPress","onPress","onPressIn","onPressOut","onResponderGrant","onResponderMove","onResponderRelease","onResponderTerminate","onResponderTerminationRequest","onStartShouldSetResponder","pressRetentionOffset","suppressHighlighting","style"]);function N(e){return e.node.attributes.some(n=>{if(o.types.isJSXSpreadAttribute(n)){if(o.types.isIdentifier(n.argument)){let i=e.scope.getBinding(n.argument.name),t;if(i&&(o.types.isVariableDeclarator(i.path.node)?t=i.path.node.init:o.types.isObjectExpression(i.path.node)&&(t=i.path.node)),t&&o.types.isObjectExpression(t))return t.properties.some(r=>o.types.isObjectProperty(r)&&o.types.isIdentifier(r.key)?c.has(r.key.name):!1)}return!0}return o.types.isJSXIdentifier(n.name)&&n.value?n.name.name==="children"?b(e,n.value):c.has(n.name.name):!1})}var x=e=>{console.log(`[react-native-boost] ${e}`)};var T=(0,S.declare)(e=>(e.assertVersion(7),{name:"react-native-boost",visitor:{JSXOpeningElement(n,i){var s;let t=i.opts??{},r=t.verbose?x:()=>{};((s=t.optimizations)==null?void 0:s.text)!==!1&&g(n,r)}}}));
File without changes
File without changes
File without changes