react-native-boost 0.2.0 → 0.4.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.
@@ -2,7 +2,37 @@ 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
+ import { hasBlacklistedProperty, shouldIgnoreOptimization } from '../../utils/common';
6
+
7
+ export const textBlacklistedProperties = new Set([
8
+ 'accessible',
9
+ 'accessibilityLabel',
10
+ 'accessibilityState',
11
+ 'allowFontScaling',
12
+ 'aria-busy',
13
+ 'aria-checked',
14
+ 'aria-disabled',
15
+ 'aria-expanded',
16
+ 'aria-label',
17
+ 'aria-selected',
18
+ 'ellipsizeMode',
19
+ 'id',
20
+ 'nativeID',
21
+ 'onLongPress',
22
+ 'onPress',
23
+ 'onPressIn',
24
+ 'onPressOut',
25
+ 'onResponderGrant',
26
+ 'onResponderMove',
27
+ 'onResponderRelease',
28
+ 'onResponderTerminate',
29
+ 'onResponderTerminationRequest',
30
+ 'onStartShouldSetResponder',
31
+ 'pressRetentionOffset',
32
+ 'suppressHighlighting',
33
+ 'selectable',
34
+ 'selectionColor',
35
+ ]);
6
36
 
7
37
  export const textOptimizer: Optimizer = (path, log = () => {}) => {
8
38
  // Ensure we're processing a JSX Text element
@@ -30,7 +60,8 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
30
60
  }
31
61
 
32
62
  // Bail if the element has any blacklisted properties or non-string children props
33
- if (hasBlacklistedProperties(path)) return;
63
+ if (hasBlacklistedProperty(path, textBlacklistedProperties)) return;
64
+ if (hasInvalidChildren(path)) return;
34
65
  if (!hasOnlyStringChildren(path, parent)) return;
35
66
 
36
67
  // Extract the file from the Babel hub and add flags for logging & import caching
@@ -46,6 +77,7 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
46
77
  log(`Optimizing Text component in ${filename}:${lineNumber}`);
47
78
 
48
79
  // Optimize props
80
+ fixNegativeNumberOfLines({ path, log });
49
81
  optimizeStyleTag({ path, file });
50
82
 
51
83
  // Add TextNativeComponent import (cached on file) so we only add it once per file
@@ -89,33 +121,39 @@ function isStringNode(path: NodePath<t.JSXOpeningElement>, child: t.Node): boole
89
121
  return false;
90
122
  }
91
123
 
92
- const blacklistedProperties = new Set([
93
- 'accessible',
94
- 'accessibilityLabel',
95
- 'accessibilityState',
96
- 'allowFontScaling',
97
- 'aria-busy',
98
- 'aria-checked',
99
- 'aria-disabled',
100
- 'aria-expanded',
101
- 'aria-label',
102
- 'aria-selected',
103
- 'ellipsizeMode',
104
- 'id',
105
- 'nativeID',
106
- 'onLongPress',
107
- 'onPress',
108
- 'onPressIn',
109
- 'onPressOut',
110
- 'onResponderGrant',
111
- 'onResponderMove',
112
- 'onResponderRelease',
113
- 'onResponderTerminate',
114
- 'onResponderTerminationRequest',
115
- 'onStartShouldSetResponder',
116
- 'pressRetentionOffset',
117
- 'suppressHighlighting',
118
- ]);
124
+ function fixNegativeNumberOfLines({
125
+ path,
126
+ log,
127
+ }: {
128
+ path: NodePath<t.JSXOpeningElement>;
129
+ log: (message: string) => void;
130
+ }) {
131
+ for (const attribute of path.node.attributes) {
132
+ if (
133
+ t.isJSXAttribute(attribute) &&
134
+ t.isJSXIdentifier(attribute.name, { name: 'numberOfLines' }) &&
135
+ attribute.value &&
136
+ t.isJSXExpressionContainer(attribute.value)
137
+ ) {
138
+ let originalValue: number | undefined;
139
+ if (t.isNumericLiteral(attribute.value.expression)) {
140
+ originalValue = attribute.value.expression.value;
141
+ } else if (
142
+ t.isUnaryExpression(attribute.value.expression) &&
143
+ attribute.value.expression.operator === '-' &&
144
+ t.isNumericLiteral(attribute.value.expression.argument)
145
+ ) {
146
+ originalValue = -attribute.value.expression.argument.value;
147
+ }
148
+ if (originalValue !== undefined && originalValue < 0) {
149
+ log(
150
+ `Warning: 'numberOfLines' in <Text> must be a non-negative number, received: ${originalValue}. The value will be set to 0.`
151
+ );
152
+ attribute.value.expression = t.numericLiteral(0);
153
+ }
154
+ }
155
+ }
156
+ }
119
157
 
120
158
  function optimizeStyleTag({ path, file }: { path: NodePath<t.JSXOpeningElement>; file: HubFile }) {
121
159
  let shouldImportFlattenTextStyle = false;
@@ -139,43 +177,17 @@ function optimizeStyleTag({ path, file }: { path: NodePath<t.JSXOpeningElement>;
139
177
  }
140
178
  }
141
179
 
142
- function hasBlacklistedProperties(path: NodePath<t.JSXOpeningElement>): boolean {
143
- return path.node.attributes.some((attribute) => {
144
- // Check if we can resolve the spread attribute
145
- if (t.isJSXSpreadAttribute(attribute)) {
146
- if (t.isIdentifier(attribute.argument)) {
147
- const binding = path.scope.getBinding(attribute.argument.name);
148
- let objectExpression: t.ObjectExpression | undefined;
149
- if (binding) {
150
- // If the binding node is a VariableDeclarator, use its initializer
151
- if (t.isVariableDeclarator(binding.path.node)) {
152
- objectExpression = binding.path.node.init as t.ObjectExpression;
153
- } else if (t.isObjectExpression(binding.path.node)) {
154
- objectExpression = binding.path.node;
155
- }
156
- }
157
- if (objectExpression && t.isObjectExpression(objectExpression)) {
158
- return objectExpression.properties.some((property) => {
159
- if (t.isObjectProperty(property) && t.isIdentifier(property.key)) {
160
- return blacklistedProperties.has(property.key.name);
161
- }
162
- return false;
163
- });
164
- }
165
- }
166
- // Bail if we can't resolve the spread attribute
167
- return true;
168
- }
180
+ function hasInvalidChildren(path: NodePath<t.JSXOpeningElement>): boolean {
181
+ for (const attribute of path.node.attributes) {
182
+ if (t.isJSXSpreadAttribute(attribute)) return false; // spread attributes are handled in hasBlacklistedProperty
169
183
 
170
184
  if (t.isJSXIdentifier(attribute.name) && attribute.value) {
171
185
  // For a "children" attribute, optimization is allowed only if it is a string
172
186
  if (attribute.name.name === 'children') {
173
187
  return isStringNode(path, attribute.value);
174
188
  }
175
- return blacklistedProperties.has(attribute.name.name);
189
+ return textBlacklistedProperties.has(attribute.name.name);
176
190
  }
177
-
178
- // For other attribute types (e.g. namespaced), assume no blacklisting
179
- return false;
180
- });
191
+ }
192
+ return false;
181
193
  }
@@ -0,0 +1,114 @@
1
+ import { NodePath, types as t } from '@babel/core';
2
+ import { addDefault } from '@babel/helper-module-imports';
3
+ import { HubFile, Optimizer } from '../../types';
4
+ import PluginError from '../../utils/plugin-error';
5
+ import { hasBlacklistedProperty, shouldIgnoreOptimization } from '../../utils/common';
6
+
7
+ export const viewBlacklistedProperties = new Set([
8
+ 'accessible',
9
+ 'accessibilityLabel',
10
+ 'accessibilityState',
11
+ 'allowFontScaling',
12
+ 'aria-busy',
13
+ 'aria-checked',
14
+ 'aria-disabled',
15
+ 'aria-expanded',
16
+ 'aria-label',
17
+ 'aria-selected',
18
+ 'ellipsizeMode',
19
+ 'disabled',
20
+ 'id',
21
+ 'nativeID',
22
+ 'numberOfLines',
23
+ 'onLongPress',
24
+ 'onPress',
25
+ 'onPressIn',
26
+ 'onPressOut',
27
+ 'onResponderGrant',
28
+ 'onResponderMove',
29
+ 'onResponderRelease',
30
+ 'onResponderTerminate',
31
+ 'onResponderTerminationRequest',
32
+ 'onStartShouldSetResponder',
33
+ 'pressRetentionOffset',
34
+ 'selectable',
35
+ 'selectionColor',
36
+ 'suppressHighlighting',
37
+ 'style',
38
+ ]);
39
+
40
+ export const viewOptimizer: Optimizer = (path, log = () => {}) => {
41
+ // Ensure we're processing a JSX element identifier.
42
+ if (!t.isJSXIdentifier(path.node.name)) return;
43
+
44
+ const parent = path.parent;
45
+ if (!t.isJSXElement(parent)) return;
46
+
47
+ const elementName = path.node.name.name;
48
+ if (elementName !== 'View') return;
49
+
50
+ // Respect comments that disable optimization.
51
+ if (shouldIgnoreOptimization(path)) return;
52
+
53
+ // Ensure the View element comes from react-native.
54
+ const binding = path.scope.getBinding(elementName);
55
+ if (!binding) return;
56
+ if (binding.kind === 'module') {
57
+ const parentNode = binding.path.parent;
58
+ if (!t.isImportDeclaration(parentNode) || parentNode.source.value !== 'react-native') {
59
+ return;
60
+ }
61
+ }
62
+
63
+ // Bail if any blacklisted props are present.
64
+ if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
65
+
66
+ // Bail if a <TextAncestor /> component exists as an ancestor.
67
+ if (hasTextAncestor(path)) return;
68
+
69
+ // Extract the file from the Babel hub and add flags for logging & import caching.
70
+ const hub = path.hub as unknown;
71
+ const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;
72
+
73
+ if (!file) {
74
+ throw new PluginError('No file found in Babel hub');
75
+ }
76
+
77
+ const filename = file.opts?.filename || 'unknown file';
78
+ const lineNumber = path.node.loc?.start.line ?? 'unknown line';
79
+ log(`Optimizing View component in ${filename}:${lineNumber}`);
80
+
81
+ // Add ViewNativeComponent import (cached on the file) to prevent duplicate imports.
82
+ if (!file.__hasImports) {
83
+ file.__hasImports = {};
84
+ }
85
+ if (!file.__hasImports.ViewNativeComponent) {
86
+ file.__hasImports.NativeView = addDefault(path, 'react-native/Libraries/Components/View/ViewNativeComponent', {
87
+ nameHint: 'NativeView',
88
+ });
89
+ }
90
+ const viewNativeIdentifier = file.__hasImports.NativeView;
91
+
92
+ // Replace the component with its native counterpart.
93
+ path.node.name.name = viewNativeIdentifier.name;
94
+
95
+ // If the element is not self-closing, update the closing element as well.
96
+ if (
97
+ !path.node.selfClosing &&
98
+ parent.closingElement &&
99
+ t.isJSXIdentifier(parent.closingElement.name) &&
100
+ parent.closingElement.name.name === 'View'
101
+ ) {
102
+ parent.closingElement.name.name = viewNativeIdentifier.name;
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Returns true if any ancestor element is a <Text />.
108
+ * TODO: This is dangerous as we can't resolve custom components and check if they have a <Text /> ancestor in the tree
109
+ */
110
+ function hasTextAncestor(path: NodePath<t.JSXOpeningElement>): boolean {
111
+ return !!path.findParent((parentPath) => {
112
+ return t.isJSXElement(parentPath.node) && t.isJSXIdentifier(parentPath.node.openingElement.name, { name: 'Text' });
113
+ });
114
+ }
@@ -1,6 +1,10 @@
1
1
  import { NodePath, types as t } from '@babel/core';
2
2
 
3
3
  export interface PluginOptions {
4
+ /**
5
+ * Paths to ignore from optimization. Relative to the Babel configuration file.
6
+ */
7
+ ignores?: string[];
4
8
  /**
5
9
  * Whether or not to log optimized files to the console.
6
10
  * @default false
@@ -11,10 +15,15 @@ export interface PluginOptions {
11
15
  */
12
16
  optimizations?: {
13
17
  /**
14
- * Whether or not to optimize the text component.
18
+ * Whether or not to optimize the Text component.
15
19
  * @default true
16
20
  */
17
21
  text?: boolean;
22
+ /**
23
+ * Whether or not to optimize the View component.
24
+ * @default true
25
+ */
26
+ view?: boolean;
18
27
  };
19
28
  }
20
29
 
@@ -1,5 +1,43 @@
1
1
  import { NodePath, types as t } from '@babel/core';
2
2
  import { ensureArray } from './helpers';
3
+ import { HubFile } from '../types';
4
+ import { minimatch } from 'minimatch';
5
+ import path from 'node:path';
6
+ import PluginError from './plugin-error';
7
+
8
+ /**
9
+ * Checks if the file is in the list of ignored files.
10
+ *
11
+ * @param p - The path to the JSXOpeningElement.
12
+ * @param ignores - List of glob paths (absolute or relative to import.meta.dirname).
13
+ * @returns true if the file matches any of the ignore patterns.
14
+ */
15
+ export const isIgnoredFile = (p: NodePath<t.JSXOpeningElement>, ignores: string[]): boolean => {
16
+ const hub = p.hub as unknown;
17
+ const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;
18
+
19
+ if (!file) {
20
+ throw new PluginError('No file found in Babel hub');
21
+ }
22
+
23
+ const fileName = file.opts.filename;
24
+
25
+ // Use the current working directory which typically corresponds to the user's project root.
26
+ const baseDirectory = 'cwd' in file.opts ? (file.opts.cwd as string) : process.cwd();
27
+
28
+ // Iterate through the ignore patterns.
29
+ for (const pattern of ignores) {
30
+ // If the pattern is not absolute, join it with the baseDir
31
+ const absolutePattern = path.isAbsolute(pattern) ? pattern : path.join(baseDirectory, pattern);
32
+
33
+ // Check if the file name matches the glob pattern.
34
+ if (minimatch(fileName, absolutePattern, { dot: true })) {
35
+ return true;
36
+ }
37
+ }
38
+
39
+ return false;
40
+ };
3
41
 
4
42
  /**
5
43
  * Checks if the JSX element should be ignored based on a preceding comment.
@@ -70,3 +108,36 @@ export const shouldIgnoreOptimization = (path: NodePath<t.JSXOpeningElement>): b
70
108
  }
71
109
  return false;
72
110
  };
111
+
112
+ export const hasBlacklistedProperty = (path: NodePath<t.JSXOpeningElement>, blacklist: Set<string>): boolean => {
113
+ return path.node.attributes.some((attribute) => {
114
+ // Check if we can resolve the spread attribute
115
+ if (t.isJSXSpreadAttribute(attribute)) {
116
+ if (t.isIdentifier(attribute.argument)) {
117
+ const binding = path.scope.getBinding(attribute.argument.name);
118
+ let objectExpression: t.ObjectExpression | undefined;
119
+ if (binding) {
120
+ // If the binding node is a VariableDeclarator, use its initializer
121
+ if (t.isVariableDeclarator(binding.path.node)) {
122
+ objectExpression = binding.path.node.init as t.ObjectExpression;
123
+ } else if (t.isObjectExpression(binding.path.node)) {
124
+ objectExpression = binding.path.node;
125
+ }
126
+ }
127
+ if (objectExpression && t.isObjectExpression(objectExpression)) {
128
+ return objectExpression.properties.some((property) => {
129
+ if (t.isObjectProperty(property) && t.isIdentifier(property.key)) {
130
+ return blacklist.has(property.key.name);
131
+ }
132
+ return false;
133
+ });
134
+ }
135
+ }
136
+ // Bail if we can't resolve the spread attribute
137
+ return true;
138
+ }
139
+
140
+ // For other attribute types (e.g. namespaced), assume no blacklisting
141
+ return false;
142
+ });
143
+ };