react-native-boost 0.5.3 → 0.5.4

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.
@@ -1,7 +1,13 @@
1
1
  import { NodePath, types as t } from '@babel/core';
2
2
  import { HubFile, Optimizer } from '../../types';
3
3
  import PluginError from '../../utils/plugin-error';
4
- import { addFileImportHint, hasBlacklistedProperty, shouldIgnoreOptimization } from '../../utils/common';
4
+ import {
5
+ hasBlacklistedProperty,
6
+ isIgnoredLine,
7
+ isValidJSXComponent,
8
+ isReactNativeImport,
9
+ replaceWithNativeComponent,
10
+ } from '../../utils/common';
5
11
 
6
12
  export const viewBlacklistedProperties = new Set([
7
13
  'accessible',
@@ -37,32 +43,10 @@ export const viewBlacklistedProperties = new Set([
37
43
  ]);
38
44
 
39
45
  export const viewOptimizer: Optimizer = (path, log = () => {}) => {
40
- // Ensure we're processing a JSX element identifier.
41
- if (!t.isJSXIdentifier(path.node.name)) return;
42
-
43
- const parent = path.parent;
44
- if (!t.isJSXElement(parent)) return;
45
-
46
- const elementName = path.node.name.name;
47
- if (elementName !== 'View') return;
48
-
49
- // Respect comments that disable optimization.
50
- if (shouldIgnoreOptimization(path)) return;
51
-
52
- // Ensure the View element comes from react-native.
53
- const binding = path.scope.getBinding(elementName);
54
- if (!binding) return;
55
- if (binding.kind === 'module') {
56
- const parentNode = binding.path.parent;
57
- if (!t.isImportDeclaration(parentNode) || parentNode.source.value !== 'react-native') {
58
- return;
59
- }
60
- }
61
-
62
- // Bail if any blacklisted props are present.
46
+ if (isIgnoredLine(path)) return;
47
+ if (!isValidJSXComponent(path, 'View')) return;
48
+ if (!isReactNativeImport(path, 'View')) return;
63
49
  if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
64
-
65
- // Bail if a <TextAncestor /> component exists as an ancestor.
66
50
  if (hasTextAncestor(path)) return;
67
51
 
68
52
  // Extract the file from the Babel hub and add flags for logging & import caching.
@@ -77,28 +61,10 @@ export const viewOptimizer: Optimizer = (path, log = () => {}) => {
77
61
  const lineNumber = path.node.loc?.start.line ?? 'unknown line';
78
62
  log(`Optimizing View component in ${filename}:${lineNumber}`);
79
63
 
80
- // Add ViewNativeComponent import (cached on the file) to prevent duplicate imports.
81
- const viewNativeIdentifier = addFileImportHint({
82
- file,
83
- path,
84
- importName: 'NativeView',
85
- moduleName: 'react-native-boost',
86
- importType: 'named',
87
- nameHint: 'NativeView',
88
- });
64
+ const parent = path.parent as t.JSXElement;
89
65
 
90
- // Replace the component with its native counterpart.
91
- path.node.name.name = viewNativeIdentifier.name;
92
-
93
- // If the element is not self-closing, update the closing element as well.
94
- if (
95
- !path.node.selfClosing &&
96
- parent.closingElement &&
97
- t.isJSXIdentifier(parent.closingElement.name) &&
98
- parent.closingElement.name.name === 'View'
99
- ) {
100
- parent.closingElement.name.name = viewNativeIdentifier.name;
101
- }
66
+ // Replace the Text component with NativeText
67
+ replaceWithNativeComponent(path, parent, file, 'NativeView');
102
68
  };
103
69
 
104
70
  /**
@@ -0,0 +1,144 @@
1
+ import { NodePath, types as t } from '@babel/core';
2
+ import { ACCESSIBILITY_PROPERTIES } from '../constants';
3
+
4
+ /**
5
+ * Checks if the JSX element has a blacklisted property.
6
+ *
7
+ * @param path - The path to the JSXOpeningElement.
8
+ * @param blacklist - The set of blacklisted properties.
9
+ * @returns true if the JSX element has a blacklisted property.
10
+ */
11
+ export const hasBlacklistedProperty = (path: NodePath<t.JSXOpeningElement>, blacklist: Set<string>): boolean => {
12
+ return path.node.attributes.some((attribute) => {
13
+ // Check direct attributes (e.g., onPress={handler})
14
+ if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && blacklist.has(attribute.name.name)) {
15
+ return true;
16
+ }
17
+
18
+ // Check spread attributes (e.g., {...props})
19
+ if (t.isJSXSpreadAttribute(attribute)) {
20
+ if (t.isIdentifier(attribute.argument)) {
21
+ const binding = path.scope.getBinding(attribute.argument.name);
22
+ let objectExpression: t.ObjectExpression | undefined;
23
+ if (binding) {
24
+ // If the binding node is a VariableDeclarator, use its initializer
25
+ if (t.isVariableDeclarator(binding.path.node)) {
26
+ objectExpression = binding.path.node.init as t.ObjectExpression;
27
+ } else if (t.isObjectExpression(binding.path.node)) {
28
+ objectExpression = binding.path.node;
29
+ }
30
+ }
31
+ if (objectExpression && t.isObjectExpression(objectExpression)) {
32
+ return objectExpression.properties.some((property) => {
33
+ if (t.isObjectProperty(property) && t.isIdentifier(property.key)) {
34
+ return blacklist.has(property.key.name);
35
+ }
36
+ return false;
37
+ });
38
+ }
39
+ }
40
+ // Bail if we can't resolve the spread attribute
41
+ return true;
42
+ }
43
+
44
+ // For other attribute types, assume no blacklisting
45
+ return false;
46
+ });
47
+ };
48
+
49
+ /**
50
+ * Helper that builds an Object.assign expression out of the existing JSX attributes.
51
+ * It handles both plain JSXAttributes and spread attributes.
52
+ *
53
+ * @param attributes - The attributes to build the expression from.
54
+ * @returns The Object.assign expression.
55
+ */
56
+ export const buildPropertiesFromAttributes = (attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): t.Expression => {
57
+ const arguments_: t.Expression[] = [];
58
+ for (const attribute of attributes) {
59
+ if (t.isJSXSpreadAttribute(attribute)) {
60
+ arguments_.push(attribute.argument);
61
+ } else if (t.isJSXAttribute(attribute)) {
62
+ const key = attribute.name.name;
63
+ let value: t.Expression;
64
+ if (!attribute.value) {
65
+ value = t.booleanLiteral(true);
66
+ } else if (t.isStringLiteral(attribute.value)) {
67
+ value = attribute.value;
68
+ } else if (t.isJSXExpressionContainer(attribute.value)) {
69
+ value = t.isJSXEmptyExpression(attribute.value.expression)
70
+ ? t.booleanLiteral(true)
71
+ : attribute.value.expression;
72
+ } else {
73
+ value = t.nullLiteral();
74
+ }
75
+ // If the key is not a valid JavaScript identifier (e.g. "aria-label"), use a string literal.
76
+ const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
77
+ const keyNode =
78
+ typeof key === 'string' && validIdentifierRegex.test(key) ? t.identifier(key) : t.stringLiteral(key.toString());
79
+
80
+ arguments_.push(t.objectExpression([t.objectProperty(keyNode, value)]));
81
+ }
82
+ }
83
+ if (arguments_.length === 0) {
84
+ return t.objectExpression([]);
85
+ }
86
+ return t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('assign')), [
87
+ t.objectExpression([]),
88
+ ...arguments_,
89
+ ]);
90
+ };
91
+
92
+ /**
93
+ * Checks if the JSX element has an accessibility property.
94
+ *
95
+ * @param path - The NodePath for the JSXOpeningElement, used for scope lookup.
96
+ * @param attributes - The attributes to check.
97
+ * @returns true if the JSX element has an accessibility property.
98
+ */
99
+ export const hasAccessibilityProperty = (
100
+ path: NodePath<t.JSXOpeningElement>,
101
+ attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]
102
+ ): boolean => {
103
+ for (const attribute of attributes) {
104
+ if (t.isJSXAttribute(attribute)) {
105
+ const key = attribute.name.name;
106
+ if (typeof key === 'string' && ACCESSIBILITY_PROPERTIES.has(key)) {
107
+ return true;
108
+ }
109
+ } else if (t.isJSXSpreadAttribute(attribute)) {
110
+ if (t.isObjectExpression(attribute.argument)) {
111
+ for (const property of attribute.argument.properties) {
112
+ if (
113
+ t.isObjectProperty(property) &&
114
+ t.isIdentifier(property.key) &&
115
+ ACCESSIBILITY_PROPERTIES.has(property.key.name)
116
+ ) {
117
+ return true;
118
+ }
119
+ }
120
+ } else if (t.isIdentifier(attribute.argument)) {
121
+ const binding = path.scope.getBinding(attribute.argument.name);
122
+ if (binding && t.isVariableDeclarator(binding.path.node)) {
123
+ const declarator = binding.path.node as t.VariableDeclarator;
124
+ if (declarator.init && t.isObjectExpression(declarator.init)) {
125
+ for (const property of declarator.init.properties) {
126
+ if (
127
+ t.isObjectProperty(property) &&
128
+ t.isIdentifier(property.key) &&
129
+ ACCESSIBILITY_PROPERTIES.has(property.key.name)
130
+ ) {
131
+ return true;
132
+ }
133
+ }
134
+ continue;
135
+ }
136
+ }
137
+ return true;
138
+ } else {
139
+ return true;
140
+ }
141
+ }
142
+ }
143
+ return false;
144
+ };
@@ -0,0 +1,82 @@
1
+ import { NodePath, types as t } from '@babel/core';
2
+ import { addDefault, addNamed } from '@babel/helper-module-imports';
3
+ import { FileImportOptions, HubFile } from '../../types';
4
+ import { RUNTIME_MODULE_NAME } from '../constants';
5
+
6
+ /**
7
+ * Adds a hint to the file object to ensure that a specific import is added only once and cached on the file object.
8
+ *
9
+ * @param opts - Object containing the function arguments:
10
+ * - file: The Babel file object (e.g. HubFile)
11
+ * - nameHint: The name hint which also acts as the cache key to ensure the import is only added once (e.g. 'normalizeAccessibilityProps')
12
+ * - path: The current Babel NodePath
13
+ * - importName: The named import string (e.g. 'normalizeAccessibilityProps'), used when importType is 'named'
14
+ * - moduleName: The module to import from (e.g. 'react-native-boost')
15
+ * - importType: Either 'named' (default) or 'default' to determine the type of import to use.
16
+ *
17
+ * @returns The identifier returned by addNamed or addDefault.
18
+ */
19
+ export function addFileImportHint({
20
+ file,
21
+ nameHint,
22
+ path,
23
+ importName,
24
+ moduleName,
25
+ importType = 'named',
26
+ }: FileImportOptions): t.Identifier {
27
+ if (!file.__hasImports?.[nameHint]) {
28
+ file.__hasImports = file.__hasImports || {};
29
+ file.__hasImports[nameHint] =
30
+ importType === 'default'
31
+ ? addDefault(path, moduleName, { nameHint })
32
+ : addNamed(path, importName, moduleName, { nameHint });
33
+ }
34
+ return file.__hasImports[nameHint];
35
+ }
36
+
37
+ /**
38
+ * Replaces a component with its native counterpart.
39
+ * This function handles both the opening and closing tags.
40
+ *
41
+ * @param path - The path to the JSXOpeningElement.
42
+ * @param parent - The parent JSX element.
43
+ * @param file - The Babel file object.
44
+ * @param nativeComponentName - The name of the native component to import.
45
+ * @param moduleName - The module to import the native component from.
46
+ * @returns The identifier for the imported native component.
47
+ */
48
+ export const replaceWithNativeComponent = (
49
+ path: NodePath<t.JSXOpeningElement>,
50
+ parent: t.JSXElement,
51
+ file: HubFile,
52
+ nativeComponentName: string
53
+ ): t.Identifier => {
54
+ // Add native component import (cached on file) to prevent duplicate imports
55
+ const nativeIdentifier = addFileImportHint({
56
+ file,
57
+ nameHint: nativeComponentName,
58
+ path,
59
+ importName: nativeComponentName,
60
+ moduleName: RUNTIME_MODULE_NAME,
61
+ importType: 'named',
62
+ });
63
+
64
+ // Get the current name of the component, which may be aliased (i.e. Text -> RNText)
65
+ const currentName = (path.node.name as t.JSXIdentifier).name;
66
+
67
+ // Replace the component with its native counterpart
68
+ const jsxName = path.node.name as t.JSXIdentifier;
69
+ jsxName.name = nativeIdentifier.name;
70
+
71
+ // If the element is not self-closing, update the closing element as well
72
+ if (
73
+ !path.node.selfClosing &&
74
+ parent.closingElement &&
75
+ t.isJSXIdentifier(parent.closingElement.name) &&
76
+ parent.closingElement.name.name === currentName
77
+ ) {
78
+ parent.closingElement.name.name = nativeIdentifier.name;
79
+ }
80
+
81
+ return nativeIdentifier;
82
+ };
@@ -0,0 +1,4 @@
1
+ export * from './base';
2
+ export * from './validation';
3
+ export * from './attributes';
4
+ export * from './node-types';
@@ -0,0 +1,22 @@
1
+ import { NodePath, types as t } from '@babel/core';
2
+
3
+ /**
4
+ * Checks if a node represents a string value.
5
+ */
6
+ export const isStringNode = (path: NodePath<t.JSXOpeningElement>, child: t.Node): boolean => {
7
+ if (t.isJSXText(child) || t.isStringLiteral(child)) return true;
8
+
9
+ // Check for JSX expressions
10
+ if (t.isJSXExpressionContainer(child)) {
11
+ const expression = child.expression;
12
+ if (t.isIdentifier(expression)) {
13
+ const binding = path.scope.getBinding(expression.name);
14
+ if (binding && binding.path.node && t.isVariableDeclarator(binding.path.node)) {
15
+ return !!binding.path.node.init && t.isStringLiteral(binding.path.node.init);
16
+ }
17
+ return false;
18
+ }
19
+ if (t.isStringLiteral(expression)) return true;
20
+ }
21
+ return false;
22
+ };
@@ -0,0 +1,181 @@
1
+ import { NodePath, types as t } from '@babel/core';
2
+ import { ensureArray } from '../helpers';
3
+ import { HubFile } from '../../types';
4
+ import { minimatch } from 'minimatch';
5
+ import nodePath 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 path - 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 = (path: NodePath<t.JSXOpeningElement>, ignores: string[]): boolean => {
16
+ const hub = path.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 = nodePath.isAbsolute(pattern) ? pattern : nodePath.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
+ };
41
+
42
+ /**
43
+ * Checks if the JSX element should be ignored based on a preceding comment.
44
+ *
45
+ * The function looks up the JSXOpeningElement's own leading comments as well as
46
+ * the parent element's comments before falling back to inspect siblings.
47
+ *
48
+ * @param path - The path to the JSXOpeningElement.
49
+ * @returns true if the JSX element should be ignored.
50
+ */
51
+ export const isIgnoredLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
52
+ // Check for @boost-ignore in the leading comments on the JSX opening element.
53
+ if (path.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {
54
+ return true;
55
+ }
56
+
57
+ // Check for @boost-ignore in the leading comments on the parent JSX element.
58
+ const jsxElementPath = path.parentPath;
59
+ if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {
60
+ return true;
61
+ }
62
+
63
+ // NEW: Check for @boost-ignore in the leading comments on the ObjectProperty (if it exists)
64
+ // This handles cases where the JSX element is used as a value inside an object literal.
65
+ const propertyPath = jsxElementPath.parentPath;
66
+ if (
67
+ propertyPath &&
68
+ propertyPath.isObjectProperty() &&
69
+ propertyPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))
70
+ ) {
71
+ return true;
72
+ }
73
+
74
+ if (!jsxElementPath.parentPath) return false;
75
+
76
+ // Get the container that holds this element (for example, a JSX fragment or JSX element)
77
+ const containerPath = jsxElementPath.parentPath;
78
+ const siblings = ensureArray(containerPath.get('children'));
79
+ const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
80
+ if (index === -1) return false;
81
+
82
+ // Look backward from the current element for a non-empty node.
83
+ for (let index_ = index - 1; index_ >= 0; index_--) {
84
+ const sibling = siblings[index_];
85
+ // Skip over any whitespace (only in JSXText nodes)
86
+ if (sibling.isJSXText() && sibling.node.value.trim() === '') {
87
+ continue;
88
+ }
89
+ // If the sibling is a JSX expression container, check its empty expression's comments.
90
+ if (sibling.isJSXExpressionContainer()) {
91
+ const expression = sibling.get('expression');
92
+ if (expression && expression.node) {
93
+ const comments = [
94
+ ...(expression.node.leadingComments || []),
95
+ ...(expression.node.trailingComments || []),
96
+ ...(expression.node.innerComments || []),
97
+ ].map((comment) => comment.value.trim());
98
+ if (comments.some((comment) => comment.includes('@boost-ignore'))) {
99
+ return true;
100
+ }
101
+ }
102
+ }
103
+ // Also check if the node itself carries a leadingComments property.
104
+ if (
105
+ sibling.node.leadingComments &&
106
+ sibling.node.leadingComments.some((comment) => comment.value.includes('@boost-ignore'))
107
+ ) {
108
+ return true;
109
+ }
110
+ break; // if the immediate non-whitespace node is not our ignore marker, stop
111
+ }
112
+ return false;
113
+ };
114
+
115
+ /**
116
+ * Checks if the path represents a valid JSX component with the specified name.
117
+ *
118
+ * @param path - The NodePath to check.
119
+ * @param componentName - The name of the component to validate against.
120
+ * @returns true if the path is a valid JSX component with the specified name.
121
+ */
122
+ export const isValidJSXComponent = (path: NodePath<t.JSXOpeningElement>, componentName: string): boolean => {
123
+ // Check if the node name is a JSX identifier
124
+ if (!t.isJSXIdentifier(path.node.name)) return false;
125
+
126
+ // Check if the parent is a JSX element
127
+ const parent = path.parent;
128
+ if (!t.isJSXElement(parent)) return false;
129
+
130
+ // For aliasing, we check if the underlying imported name matches the expected name
131
+ const componentIdentifier = path.node.name.name;
132
+ const binding = path.scope.getBinding(componentIdentifier);
133
+ if (!binding) return false;
134
+ if (
135
+ binding.kind === 'module' &&
136
+ t.isImportDeclaration(binding.path.parent) &&
137
+ t.isImportSpecifier(binding.path.node)
138
+ ) {
139
+ const imported = binding.path.node.imported;
140
+ if (t.isIdentifier(imported)) {
141
+ return imported.name === componentName;
142
+ }
143
+ }
144
+
145
+ // Fallback to string match if binding is not available
146
+ return path.node.name.name === componentName;
147
+ };
148
+
149
+ /**
150
+ * Checks if the component is imported from 'react-native' and not from a custom module.
151
+ *
152
+ * @param path - The NodePath to check.
153
+ * @param expectedImportedName - The expected import name of the component (we'll also check for aliased imports).
154
+ * @returns true if the component is imported from 'react-native'
155
+ */
156
+ export const isReactNativeImport = (path: NodePath<t.JSXOpeningElement>, expectedImportedName: string): boolean => {
157
+ if (!t.isJSXIdentifier(path.node.name)) return false;
158
+ const localName = path.node.name.name;
159
+ const binding = path.scope.getBinding(localName);
160
+ if (!binding) return false;
161
+ if (binding.kind === 'module') {
162
+ const importDeclaration = binding.path.parent;
163
+ if (!t.isImportDeclaration(importDeclaration)) return false;
164
+ // Verify it's imported from 'react-native'
165
+ if (importDeclaration.source.value !== 'react-native') return false;
166
+
167
+ // For named imports, check the imported name (not the alias)
168
+ if (t.isImportSpecifier(binding.path.node)) {
169
+ const imported = binding.path.node.imported;
170
+ if (t.isIdentifier(imported)) {
171
+ return imported.name === expectedImportedName;
172
+ }
173
+ }
174
+
175
+ // For default imports, we just assume it's valid if imported from react-native.
176
+ if (t.isImportDefaultSpecifier(binding.path.node)) {
177
+ return true;
178
+ }
179
+ }
180
+ return false;
181
+ };
@@ -0,0 +1,16 @@
1
+ export const RUNTIME_MODULE_NAME = 'react-native-boost';
2
+
3
+ /**
4
+ * The set of accessibility properties that need to be normalized.
5
+ */
6
+ export const ACCESSIBILITY_PROPERTIES = new Set([
7
+ 'accessibilityLabel',
8
+ 'aria-label',
9
+ 'accessibilityState',
10
+ 'aria-busy',
11
+ 'aria-checked',
12
+ 'aria-disabled',
13
+ 'aria-expanded',
14
+ 'aria-selected',
15
+ 'accessible',
16
+ ]);