react-native-boost 0.6.1 → 0.7.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.
@@ -16,7 +16,7 @@ export default declare((api) => {
16
16
  const logger = options.verbose ? log : () => {};
17
17
  if (isIgnoredFile(path, options.ignores ?? [])) return;
18
18
  if (options.optimizations?.text !== false) textOptimizer(path, logger);
19
- if (options.optimizations?.view !== false) viewOptimizer(path, logger);
19
+ if (options.optimizations?.view !== false) viewOptimizer(path, logger, options);
20
20
  },
21
21
  },
22
22
  };
@@ -2,6 +2,7 @@ import { NodePath, types as t } from '@babel/core';
2
2
  import { HubFile, Optimizer } from '../../types';
3
3
  import PluginError from '../../utils/plugin-error';
4
4
  import {
5
+ addDefaultProperty,
5
6
  addFileImportHint,
6
7
  buildPropertiesFromAttributes,
7
8
  hasAccessibilityProperty,
@@ -11,12 +12,13 @@ import {
11
12
  isReactNativeImport,
12
13
  replaceWithNativeComponent,
13
14
  isStringNode,
15
+ hasExpoRouterLinkParentWithAsChild,
14
16
  } from '../../utils/common';
15
17
  import { RUNTIME_MODULE_NAME } from '../../utils/constants';
18
+ import { ACCESSIBILITY_PROPERTIES } from '../../utils/constants';
19
+ import { extractStyleAttribute, extractSelectableAndUpdateStyle } from '../../utils/common';
16
20
 
17
21
  export const textBlacklistedProperties = new Set([
18
- 'allowFontScaling',
19
- 'ellipsizeMode',
20
22
  'id',
21
23
  'nativeID',
22
24
  'onLongPress',
@@ -31,8 +33,7 @@ export const textBlacklistedProperties = new Set([
31
33
  'onStartShouldSetResponder',
32
34
  'pressRetentionOffset',
33
35
  'suppressHighlighting',
34
- 'selectable',
35
- 'selectionColor',
36
+ 'selectionColor', // TODO: we can use react-native's internal `processColor` to process this at runtime
36
37
  ]);
37
38
 
38
39
  export const textOptimizer: Optimizer = (path, log = () => {}) => {
@@ -40,6 +41,7 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
40
41
  if (!isValidJSXComponent(path, 'Text')) return;
41
42
  if (!isReactNativeImport(path, 'Text')) return;
42
43
  if (hasBlacklistedProperty(path, textBlacklistedProperties)) return;
44
+ if (hasExpoRouterLinkParentWithAsChild(path)) return;
43
45
 
44
46
  // Verify that the Text only has string children
45
47
  const parent = path.parent as t.JSXElement;
@@ -57,9 +59,10 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
57
59
  log(`Optimizing Text component in ${filename}:${lineNumber}`);
58
60
 
59
61
  // Process props
60
- const originalAttributes = [...path.node.attributes];
61
62
  fixNegativeNumberOfLines({ path, log });
62
- processProps(path, file, originalAttributes);
63
+ addDefaultProperty(path, 'allowFontScaling', t.booleanLiteral(true));
64
+ addDefaultProperty(path, 'ellipsizeMode', t.stringLiteral('tail'));
65
+ processProps(path, file);
63
66
 
64
67
  // Replace the Text component with NativeText
65
68
  replaceWithNativeComponent(path, parent, file, 'NativeText');
@@ -129,49 +132,29 @@ function fixNegativeNumberOfLines({
129
132
  }
130
133
  }
131
134
 
132
- /**
133
- * Extracts the style attribute from JSX attributes.
134
- */
135
- function extractStyleAttribute(attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute>): {
136
- styleAttribute?: t.JSXAttribute;
137
- styleExpr?: t.Expression;
138
- } {
139
- for (const attribute of attributes) {
140
- if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {
141
- if (
142
- attribute.value &&
143
- t.isJSXExpressionContainer(attribute.value) &&
144
- !t.isJSXEmptyExpression(attribute.value.expression)
145
- ) {
146
- return {
147
- styleAttribute: attribute,
148
- styleExpr: attribute.value.expression,
149
- };
150
- }
151
- return { styleAttribute: attribute };
152
- }
153
- }
154
- return {};
155
- }
156
-
157
135
  /**
158
136
  * Processes style and accessibility attributes, replacing them with optimized versions.
159
137
  */
160
- function processProps(
161
- path: NodePath<t.JSXOpeningElement>,
162
- file: HubFile,
163
- originalAttributes: Array<t.JSXAttribute | t.JSXSpreadAttribute>
164
- ) {
165
- const { styleExpr } = extractStyleAttribute(originalAttributes);
166
- const hasA11y = hasAccessibilityProperty(path, originalAttributes);
167
-
168
- if (styleExpr && hasA11y) {
169
- // When both style and accessibility properties exist, we split them into two separate spread attributes
170
- const accessibilityAttributes = originalAttributes.filter(
171
- (attribute) => !(t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' }))
172
- );
173
-
174
- // Set up the accessibility import
138
+ function processProps(path: NodePath<t.JSXOpeningElement>, file: HubFile) {
139
+ // Grab the up-to-date list of attributes
140
+ const currentAttributes = [...path.node.attributes];
141
+
142
+ const { styleExpr, styleAttribute } = extractStyleAttribute(currentAttributes);
143
+ const hasA11y = hasAccessibilityProperty(path, currentAttributes);
144
+
145
+ // ============================================
146
+ // 1. Prepare spread attributes (style / a11y)
147
+ // ============================================
148
+
149
+ const spreadAttributes: t.JSXSpreadAttribute[] = [];
150
+
151
+ // --- Accessibility ---
152
+ if (hasA11y) {
153
+ const accessibilityAttributes = currentAttributes.filter((attribute) => {
154
+ if (!t.isJSXAttribute(attribute)) return false;
155
+ return t.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name as string);
156
+ });
157
+
175
158
  const normalizeIdentifier = addFileImportHint({
176
159
  file,
177
160
  nameHint: 'processAccessibilityProps',
@@ -179,10 +162,25 @@ function processProps(
179
162
  importName: 'processAccessibilityProps',
180
163
  moduleName: RUNTIME_MODULE_NAME,
181
164
  });
165
+
182
166
  const accessibilityObject = buildPropertiesFromAttributes(accessibilityAttributes);
183
167
  const accessibilityExpr = t.callExpression(t.identifier(normalizeIdentifier.name), [accessibilityObject]);
168
+ spreadAttributes.push(t.jsxSpreadAttribute(accessibilityExpr));
169
+ }
170
+
171
+ // --- Style ---
172
+ let selectableAttribute: t.JSXAttribute | undefined;
173
+ if (styleExpr) {
174
+ // Attempt a compile-time extraction of `userSelect`
175
+ const selectableValue = extractSelectableAndUpdateStyle(styleExpr);
176
+
177
+ if (selectableValue != null) {
178
+ selectableAttribute = t.jsxAttribute(
179
+ t.jsxIdentifier('selectable'),
180
+ t.jsxExpressionContainer(t.booleanLiteral(selectableValue))
181
+ );
182
+ }
184
183
 
185
- // Set up the style import
186
184
  const flattenIdentifier = addFileImportHint({
187
185
  file,
188
186
  nameHint: 'processTextStyle',
@@ -191,31 +189,32 @@ function processProps(
191
189
  moduleName: RUNTIME_MODULE_NAME,
192
190
  });
193
191
  const flattenedStyleExpr = t.callExpression(t.identifier(flattenIdentifier.name), [styleExpr]);
192
+ spreadAttributes.push(t.jsxSpreadAttribute(flattenedStyleExpr));
193
+ }
194
194
 
195
- // Use two separate JSX spread attributes
196
- path.node.attributes = [t.jsxSpreadAttribute(accessibilityExpr), t.jsxSpreadAttribute(flattenedStyleExpr)];
197
- } else if (styleExpr) {
198
- // Only style attribute is present
199
- const flattenIdentifier = addFileImportHint({
200
- file,
201
- nameHint: 'processTextStyle',
202
- path,
203
- importName: 'processTextStyle',
204
- moduleName: RUNTIME_MODULE_NAME,
205
- });
206
- const flattened = t.callExpression(t.identifier(flattenIdentifier.name), [styleExpr]);
207
- path.node.attributes = [t.jsxSpreadAttribute(flattened)];
208
- } else if (hasA11y) {
209
- // Only accessibility properties are present
210
- const normalizeIdentifier = addFileImportHint({
211
- file,
212
- nameHint: 'processAccessibilityProps',
213
- path,
214
- importName: 'processAccessibilityProps',
215
- moduleName: RUNTIME_MODULE_NAME,
216
- });
217
- const propsObject = buildPropertiesFromAttributes(originalAttributes);
218
- const normalized = t.callExpression(t.identifier(normalizeIdentifier.name), [propsObject]);
219
- path.node.attributes = [t.jsxSpreadAttribute(normalized)];
195
+ // ============================================
196
+ // 2. Collect the remaining (non-processed) attributes
197
+ // ============================================
198
+ const remainingAttributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [];
199
+
200
+ for (const attribute of currentAttributes) {
201
+ // Skip the style attribute (we have replaced it with a spread)
202
+ if (styleAttribute && attribute === styleAttribute) continue;
203
+
204
+ // Skip accessibility attributes if we processed them
205
+ if (
206
+ hasA11y &&
207
+ t.isJSXAttribute(attribute) &&
208
+ t.isJSXIdentifier(attribute.name) &&
209
+ ACCESSIBILITY_PROPERTIES.has(attribute.name.name as string)
210
+ ) {
211
+ continue;
212
+ }
213
+
214
+ remainingAttributes.push(attribute);
220
215
  }
216
+
217
+ path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes].filter(
218
+ (attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined
219
+ );
221
220
  }
@@ -7,51 +7,31 @@ import {
7
7
  isValidJSXComponent,
8
8
  isReactNativeImport,
9
9
  replaceWithNativeComponent,
10
- hasComponentAncestor,
10
+ hasUnsafeViewAncestor,
11
11
  } from '../../utils/common';
12
12
 
13
13
  export const viewBlacklistedProperties = new Set([
14
+ // TODO: process a11y props at runtime
14
15
  'accessible',
15
16
  'accessibilityLabel',
16
17
  'accessibilityState',
17
- 'allowFontScaling',
18
18
  'aria-busy',
19
19
  'aria-checked',
20
20
  'aria-disabled',
21
21
  'aria-expanded',
22
22
  'aria-label',
23
23
  'aria-selected',
24
- 'ellipsizeMode',
25
- 'disabled',
26
24
  'id',
27
25
  'nativeID',
28
- 'numberOfLines',
29
- 'onLongPress',
30
- 'onPress',
31
- 'onPressIn',
32
- 'onPressOut',
33
- 'onResponderGrant',
34
- 'onResponderMove',
35
- 'onResponderRelease',
36
- 'onResponderTerminate',
37
- 'onResponderTerminationRequest',
38
- 'onStartShouldSetResponder',
39
- 'pressRetentionOffset',
40
- 'selectable',
41
- 'selectionColor',
42
- 'suppressHighlighting',
43
- 'style',
26
+ 'style', // TODO: process style at runtime
44
27
  ]);
45
28
 
46
- // Components to skip when checking for indirect Text ancestors
47
- const skipComponents = ['View', 'Fragment', 'ScrollView', 'FlatList'];
48
-
49
- export const viewOptimizer: Optimizer = (path, log = () => {}) => {
29
+ export const viewOptimizer: Optimizer = (path, log = () => {}, options) => {
50
30
  if (isIgnoredLine(path)) return;
51
31
  if (!isValidJSXComponent(path, 'View')) return;
52
32
  if (!isReactNativeImport(path, 'View')) return;
53
33
  if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
54
- if (hasComponentAncestor(path, 'Text', skipComponents)) return;
34
+ if (hasUnsafeViewAncestor(path, options?.dangerouslyOptimizeViewWithUnknownAncestors === true)) return;
55
35
 
56
36
  // Extract the file from the Babel hub
57
37
  const hub = path.hub as unknown;
@@ -25,9 +25,20 @@ export interface PluginOptions {
25
25
  */
26
26
  view?: boolean;
27
27
  };
28
+ /**
29
+ * Opt-in flag that allows View optimization when ancestor components cannot be statically resolved.
30
+ *
31
+ * This may introduce behavioral changes when unresolved ancestors render react-native Text wrappers.
32
+ * @default false
33
+ */
34
+ dangerouslyOptimizeViewWithUnknownAncestors?: boolean;
28
35
  }
29
36
 
30
- export type Optimizer = (path: NodePath<t.JSXOpeningElement>, log?: (message: string) => void) => void;
37
+ export type Optimizer = (
38
+ path: NodePath<t.JSXOpeningElement>,
39
+ log?: (message: string) => void,
40
+ options?: PluginOptions
41
+ ) => void;
31
42
 
32
43
  export type HubFile = t.File & {
33
44
  opts: {
@@ -1,5 +1,6 @@
1
1
  import { NodePath, types as t } from '@babel/core';
2
2
  import { ACCESSIBILITY_PROPERTIES } from '../constants';
3
+ import { USER_SELECT_STYLE_TO_SELECTABLE_PROP } from '../constants';
3
4
 
4
5
  /**
5
6
  * Checks if the JSX element has a blacklisted property.
@@ -46,6 +47,67 @@ export const hasBlacklistedProperty = (path: NodePath<t.JSXOpeningElement>, blac
46
47
  });
47
48
  };
48
49
 
50
+ /**
51
+ * Adds a default property to a JSX element if it's not already defined. It avoids adding a default
52
+ * if it cannot statically determine whether the property is already set.
53
+ *
54
+ * @param path - The path to the JSXOpeningElement.
55
+ * @param key - The property key.
56
+ * @param value - The default value expression.
57
+ */
58
+ export const addDefaultProperty = (path: NodePath<t.JSXOpeningElement>, key: string, value: t.Expression) => {
59
+ let propertyIsFound = false;
60
+ let hasUnresolvableSpread = false;
61
+
62
+ for (const attribute of path.node.attributes) {
63
+ if (t.isJSXAttribute(attribute) && attribute.name.name === key) {
64
+ propertyIsFound = true;
65
+ break;
66
+ }
67
+
68
+ if (t.isJSXSpreadAttribute(attribute)) {
69
+ if (t.isObjectExpression(attribute.argument)) {
70
+ const propertyInSpread = attribute.argument.properties.some(
71
+ (p) =>
72
+ (t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === key) ||
73
+ (t.isObjectProperty(p) && t.isStringLiteral(p.key) && p.key.value === key)
74
+ );
75
+ if (propertyInSpread) {
76
+ propertyIsFound = true;
77
+ break;
78
+ }
79
+ } else if (t.isIdentifier(attribute.argument)) {
80
+ const binding = path.scope.getBinding(attribute.argument.name);
81
+ if (
82
+ binding?.path.node &&
83
+ t.isVariableDeclarator(binding.path.node) &&
84
+ t.isObjectExpression(binding.path.node.init)
85
+ ) {
86
+ const propertyInSpread = binding.path.node.init.properties.some(
87
+ (p) =>
88
+ (t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === key) ||
89
+ (t.isObjectProperty(p) && t.isStringLiteral(p.key) && p.key.value === key)
90
+ );
91
+ if (propertyInSpread) {
92
+ propertyIsFound = true;
93
+ break;
94
+ }
95
+ } else {
96
+ hasUnresolvableSpread = true;
97
+ break;
98
+ }
99
+ } else {
100
+ hasUnresolvableSpread = true;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+
106
+ if (!propertyIsFound && !hasUnresolvableSpread) {
107
+ path.node.attributes.push(t.jsxAttribute(t.jsxIdentifier(key), t.jsxExpressionContainer(value)));
108
+ }
109
+ };
110
+
49
111
  /**
50
112
  * Helper that builds an Object.assign expression out of the existing JSX attributes.
51
113
  * It handles both plain JSXAttributes and spread attributes.
@@ -142,3 +204,106 @@ export const hasAccessibilityProperty = (
142
204
  }
143
205
  return false;
144
206
  };
207
+
208
+ /**
209
+ * Extracts the `style` attribute from a JSX attributes list.
210
+ *
211
+ * @returns An object containing the attribute node itself (if found) and the expression inside
212
+ */
213
+ export function extractStyleAttribute(attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute>): {
214
+ styleAttribute?: t.JSXAttribute;
215
+ styleExpr?: t.Expression;
216
+ } {
217
+ for (const attribute of attributes) {
218
+ if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {
219
+ if (
220
+ attribute.value &&
221
+ t.isJSXExpressionContainer(attribute.value) &&
222
+ !t.isJSXEmptyExpression(attribute.value.expression)
223
+ ) {
224
+ return {
225
+ styleAttribute: attribute,
226
+ styleExpr: attribute.value.expression,
227
+ };
228
+ }
229
+ return { styleAttribute: attribute };
230
+ }
231
+ }
232
+ return {};
233
+ }
234
+
235
+ /**
236
+ * Attempts to statically extract the `userSelect` style property from a style expression.
237
+ *
238
+ * If the `userSelect` value can be resolved at compile-time, the property is removed from the
239
+ * object literal (or array element) and its mapped boolean value for the native `selectable`
240
+ * prop is returned. When the value is unknown or the expression is not statically analysable,
241
+ * `undefined` is returned and no modification is made.
242
+ */
243
+ export function extractSelectableAndUpdateStyle(styleExpr: t.Expression): boolean | undefined {
244
+ // Helper to process a single ObjectExpression
245
+ const handleObjectExpression = (objectExpr: t.ObjectExpression): boolean | undefined => {
246
+ let selectableValue: boolean | undefined;
247
+
248
+ objectExpr.properties = objectExpr.properties.filter((property) => {
249
+ if (
250
+ !t.isObjectProperty(property) ||
251
+ (!t.isIdentifier(property.key, { name: 'userSelect' }) &&
252
+ !(t.isStringLiteral(property.key) && property.key.value === 'userSelect'))
253
+ ) {
254
+ return true; // keep property
255
+ }
256
+
257
+ if (t.isStringLiteral(property.value)) {
258
+ const mapped = USER_SELECT_STYLE_TO_SELECTABLE_PROP[property.value.value];
259
+ if (mapped !== undefined) {
260
+ selectableValue = mapped;
261
+ }
262
+ }
263
+
264
+ // Remove the `userSelect` property
265
+ return false;
266
+ });
267
+
268
+ return selectableValue;
269
+ };
270
+
271
+ if (t.isObjectExpression(styleExpr)) {
272
+ return handleObjectExpression(styleExpr);
273
+ }
274
+
275
+ if (t.isArrayExpression(styleExpr)) {
276
+ let selectableValue: boolean | undefined;
277
+ for (const element of styleExpr.elements) {
278
+ if (element && t.isObjectExpression(element)) {
279
+ const value = handleObjectExpression(element);
280
+ if (value !== undefined) {
281
+ selectableValue = value; // prefer last defined value
282
+ }
283
+ }
284
+ }
285
+ return selectableValue;
286
+ }
287
+
288
+ return undefined; // not statically analysable
289
+ }
290
+
291
+ /**
292
+ * Checks if a node represents a string value.
293
+ */
294
+ export const isStringNode = (path: NodePath<t.JSXOpeningElement>, child: t.Node): boolean => {
295
+ if (t.isJSXText(child) || t.isStringLiteral(child)) return true;
296
+
297
+ if (t.isJSXExpressionContainer(child)) {
298
+ const expression = child.expression;
299
+ if (t.isIdentifier(expression)) {
300
+ const binding = path.scope.getBinding(expression.name);
301
+ if (binding && binding.path.node && t.isVariableDeclarator(binding.path.node)) {
302
+ return !!binding.path.node.init && t.isStringLiteral(binding.path.node.init);
303
+ }
304
+ return false;
305
+ }
306
+ if (t.isStringLiteral(expression)) return true;
307
+ }
308
+ return false;
309
+ };
@@ -1,5 +1,3 @@
1
- export * from './base';
2
1
  export * from './validation';
3
2
  export * from './attributes';
4
- export * from './node-types';
5
- export * from './ancestors';
3
+ export * from './base';