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.
- package/README.md +9 -59
- package/dist/plugin/esm/index.mjs +190 -60
- package/dist/plugin/esm/index.mjs.map +1 -1
- package/dist/plugin/index.js +189 -59
- package/dist/plugin/index.js.map +1 -1
- package/package.json +3 -2
- package/src/plugin/index.ts +4 -0
- package/src/plugin/optimizers/text/index.ts +73 -61
- package/src/plugin/optimizers/view/index.ts +114 -0
- package/src/plugin/types/index.ts +10 -1
- package/src/plugin/utils/common.ts +71 -0
|
@@ -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 (
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
189
|
+
return textBlacklistedProperties.has(attribute.name.name);
|
|
176
190
|
}
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
+
};
|