react-native-boost 0.7.0 → 1.1.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.
@@ -39,54 +39,52 @@ export const isIgnoredFile = (path: NodePath<t.JSXOpeningElement>, ignores: stri
39
39
  return false;
40
40
  };
41
41
 
42
+ export const isForcedLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
43
+ return hasDecoratorComment(path, '@boost-force');
44
+ };
45
+
46
+ export const isIgnoredLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
47
+ return hasDecoratorComment(path, '@boost-ignore');
48
+ };
49
+
42
50
  /**
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.
51
+ * Checks if the JSX element has a preceding comment containing the given decorator string.
47
52
  *
48
- * @param path - The path to the JSXOpeningElement.
49
- * @returns true if the JSX element should be ignored.
53
+ * Scans the JSXOpeningElement's own leading comments, the parent element's comments,
54
+ * ObjectProperty containers, and backward siblings.
50
55
  */
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'))) {
56
+ function hasDecoratorComment(path: NodePath<t.JSXOpeningElement>, decorator: string): boolean {
57
+ if (path.node.leadingComments?.some((comment) => comment.value.includes(decorator))) {
54
58
  return true;
55
59
  }
56
60
 
57
- // Check for @boost-ignore in the leading comments on the parent JSX element.
58
61
  const jsxElementPath = path.parentPath;
59
- if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {
62
+ if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))) {
60
63
  return true;
61
64
  }
62
65
 
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.
66
+ // Check leading comments on the ObjectProperty (if the JSX element is a value inside an object literal).
65
67
  const propertyPath = jsxElementPath.parentPath;
66
68
  if (
67
69
  propertyPath &&
68
70
  propertyPath.isObjectProperty() &&
69
- propertyPath.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))
71
+ propertyPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))
70
72
  ) {
71
73
  return true;
72
74
  }
73
75
 
74
76
  if (!jsxElementPath.parentPath) return false;
75
77
 
76
- // Get the container that holds this element (for example, a JSX fragment or JSX element)
77
78
  const containerPath = jsxElementPath.parentPath;
78
79
  const siblings = ensureArray(containerPath.get('children'));
79
80
  const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
80
81
  if (index === -1) return false;
81
82
 
82
- // Look backward from the current element for a non-empty node.
83
83
  for (let index_ = index - 1; index_ >= 0; index_--) {
84
84
  const sibling = siblings[index_];
85
- // Skip over any whitespace (only in JSXText nodes)
86
85
  if (sibling.isJSXText() && sibling.node.value.trim() === '') {
87
86
  continue;
88
87
  }
89
- // If the sibling is a JSX expression container, check its empty expression's comments.
90
88
  if (sibling.isJSXExpressionContainer()) {
91
89
  const expression = sibling.get('expression');
92
90
  if (expression && expression.node) {
@@ -95,22 +93,21 @@ export const isIgnoredLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
95
93
  ...(expression.node.trailingComments || []),
96
94
  ...(expression.node.innerComments || []),
97
95
  ].map((comment) => comment.value.trim());
98
- if (comments.some((comment) => comment.includes('@boost-ignore'))) {
96
+ if (comments.some((comment) => comment.includes(decorator))) {
99
97
  return true;
100
98
  }
101
99
  }
102
100
  }
103
- // Also check if the node itself carries a leadingComments property.
104
101
  if (
105
102
  sibling.node.leadingComments &&
106
- sibling.node.leadingComments.some((comment) => comment.value.includes('@boost-ignore'))
103
+ sibling.node.leadingComments.some((comment) => comment.value.includes(decorator))
107
104
  ) {
108
105
  return true;
109
106
  }
110
- break; // if the immediate non-whitespace node is not our ignore marker, stop
107
+ break;
111
108
  }
112
109
  return false;
113
- };
110
+ }
114
111
 
115
112
  /**
116
113
  * Checks if the path represents a valid JSX component with the specified name.
@@ -181,6 +178,7 @@ export const isReactNativeImport = (path: NodePath<t.JSXOpeningElement>, expecte
181
178
  };
182
179
 
183
180
  type AncestorClassification = 'safe' | 'text' | 'unknown';
181
+ export type ViewAncestorClassification = AncestorClassification;
184
182
  type ScopeBinding = NonNullable<ReturnType<NodePath<t.Node>['scope']['getBinding']>>;
185
183
 
186
184
  type AncestorAnalysisContext = {
@@ -189,11 +187,8 @@ type AncestorAnalysisContext = {
189
187
  renderExpressionInProgress: WeakSet<t.Node>;
190
188
  };
191
189
 
192
- export const hasUnsafeViewAncestor = (path: NodePath<t.JSXOpeningElement>, allowUnknownAncestors = false): boolean => {
193
- const classification = classifyViewAncestors(path);
194
- if (classification === 'text') return true;
195
- if (classification === 'unknown' && !allowUnknownAncestors) return true;
196
- return false;
190
+ export const getViewAncestorClassification = (path: NodePath<t.JSXOpeningElement>): ViewAncestorClassification => {
191
+ return classifyViewAncestors(path);
197
192
  };
198
193
 
199
194
  function classifyViewAncestors(path: NodePath<t.JSXOpeningElement>): AncestorClassification {
@@ -1,7 +1,13 @@
1
1
  import { declare } from '@babel/helper-plugin-utils';
2
2
  import { Optimizer, PluginOptions } from '../types';
3
+ import { createLogger } from './logger';
3
4
 
4
5
  export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions = {}) => {
6
+ const logger = createLogger({
7
+ verbose: false,
8
+ silent: true,
9
+ });
10
+
5
11
  return declare((api) => {
6
12
  api.assertVersion(7);
7
13
 
@@ -9,7 +15,7 @@ export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions
9
15
  name: 'react-native-boost',
10
16
  visitor: {
11
17
  JSXOpeningElement(path) {
12
- optimizer(path, undefined, options);
18
+ optimizer(path, logger, options);
13
19
  },
14
20
  },
15
21
  };
@@ -2,3 +2,18 @@ export const ensureArray = <T>(value: T | T[]): T[] => {
2
2
  if (Array.isArray(value)) return value;
3
3
  return [value];
4
4
  };
5
+
6
+ export type BailoutCheck = {
7
+ reason: string;
8
+ shouldBail: () => boolean;
9
+ };
10
+
11
+ export const getFirstBailoutReason = (checks: readonly BailoutCheck[]): string | null => {
12
+ for (const check of checks) {
13
+ if (check.shouldBail()) {
14
+ return check.reason;
15
+ }
16
+ }
17
+
18
+ return null;
19
+ };
@@ -1,3 +1,124 @@
1
- export const log = (message: string) => {
2
- console.log(`[react-native-boost] ${message}`);
1
+ import {
2
+ HubFile,
3
+ OptimizationLogPayload,
4
+ PluginLogger,
5
+ SkippedOptimizationLogPayload,
6
+ WarningLogPayload,
7
+ } from '../types';
8
+
9
+ const LOG_PREFIX = '[react-native-boost]';
10
+
11
+ const ANSI_RESET = '\u001B[0m';
12
+ const ANSI_GREEN = '\u001B[32m';
13
+ const ANSI_YELLOW = '\u001B[33m';
14
+ const ANSI_MAGENTA = '\u001B[35m';
15
+ const ANSI_RED = '\u001B[31m';
16
+
17
+ export const noopLogger: PluginLogger = {
18
+ optimized() {},
19
+ skipped() {},
20
+ forced() {},
21
+ warning() {},
3
22
  };
23
+
24
+ export const createLogger = ({ verbose, silent }: { verbose: boolean; silent: boolean }): PluginLogger => {
25
+ if (silent) return noopLogger;
26
+
27
+ return {
28
+ optimized(payload) {
29
+ writeLog('optimized', `Optimized ${payload.component} in ${formatPathLocation(payload.path)}`);
30
+ },
31
+ skipped(payload) {
32
+ if (!verbose) return;
33
+ writeLog('skipped', `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`);
34
+ },
35
+ forced(payload) {
36
+ writeLog(
37
+ 'forced',
38
+ `Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})`
39
+ );
40
+ },
41
+ warning(payload) {
42
+ const context = formatWarningContext(payload);
43
+ const message = context.length > 0 ? `${context}: ${payload.message}` : payload.message;
44
+ writeLog('warning', message);
45
+ },
46
+ };
47
+ };
48
+
49
+ function formatWarningContext(payload: WarningLogPayload): string {
50
+ const location = formatPathLocation(payload.path);
51
+
52
+ if (payload.component && location.length > 0) {
53
+ return `${payload.component} in ${location}`;
54
+ }
55
+
56
+ if (payload.component) {
57
+ return payload.component;
58
+ }
59
+
60
+ return location;
61
+ }
62
+
63
+ type LogLevel = 'optimized' | 'skipped' | 'forced' | 'warning';
64
+
65
+ function writeLog(level: LogLevel, message: string): void {
66
+ const levelTag = formatLevel(level);
67
+ console.log(`${LOG_PREFIX} ${levelTag} ${message}`);
68
+ }
69
+
70
+ function formatLevel(level: LogLevel): string {
71
+ if (level === 'optimized') {
72
+ return colorize('[optimized]', ANSI_GREEN);
73
+ }
74
+
75
+ if (level === 'skipped') {
76
+ return colorize('[skipped]', ANSI_YELLOW);
77
+ }
78
+
79
+ if (level === 'forced') {
80
+ return colorize('[forced]', ANSI_RED);
81
+ }
82
+
83
+ return colorize('[warning]', ANSI_MAGENTA);
84
+ }
85
+
86
+ function colorize(value: string, colorCode: string): string {
87
+ if (!shouldUseColor()) return value;
88
+ return `${colorCode}${value}${ANSI_RESET}`;
89
+ }
90
+
91
+ function shouldUseColor(): boolean {
92
+ if (process.env.NO_COLOR != null) return false;
93
+
94
+ if (process.env.FORCE_COLOR === '0') return false;
95
+ if (process.env.FORCE_COLOR != null) return true;
96
+
97
+ if (process.env.CLICOLOR === '0') return false;
98
+ if (process.env.CLICOLOR_FORCE != null && process.env.CLICOLOR_FORCE !== '0') return true;
99
+
100
+ if (process.stdout?.isTTY === true || process.stderr?.isTTY === true) {
101
+ return true;
102
+ }
103
+
104
+ const colorTerm = process.env.COLORTERM;
105
+ if (colorTerm != null && colorTerm !== '') {
106
+ return true;
107
+ }
108
+
109
+ const term = process.env.TERM;
110
+ return term != null && term !== '' && term.toLowerCase() !== 'dumb';
111
+ }
112
+
113
+ function formatPathLocation(
114
+ payloadPath: OptimizationLogPayload['path'] | SkippedOptimizationLogPayload['path'] | undefined
115
+ ): string {
116
+ if (!payloadPath) return 'unknown file:unknown line';
117
+
118
+ const hub = payloadPath.hub as unknown;
119
+ const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;
120
+ const filename = file?.opts?.filename ?? 'unknown file';
121
+ const lineNumber = payloadPath.node.loc?.start.line ?? 'unknown line';
122
+
123
+ return `${filename}:${lineNumber}`;
124
+ }
@@ -1,7 +1,23 @@
1
1
  /* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
2
- import { Platform } from 'react-native';
3
2
 
4
- export const NativeText =
5
- Platform.OS === 'web'
6
- ? require('react-native').Text
7
- : require('react-native/Libraries/Text/TextNativeComponent').NativeText;
3
+ import type { ComponentType } from 'react';
4
+ import type { TextProps } from 'react-native';
5
+
6
+ const reactNative = require('react-native');
7
+ const isWeb = reactNative.Platform.OS === 'web';
8
+
9
+ let nativeText = reactNative.unstable_NativeText;
10
+
11
+ if (isWeb || nativeText == null) {
12
+ // Fallback to regular Text component if unstable_NativeText is not available or we're on Web
13
+ nativeText = reactNative.Text;
14
+ }
15
+
16
+ /**
17
+ * Native Text component with graceful fallback.
18
+ *
19
+ * @remarks
20
+ * Uses `unstable_NativeText` on supported native runtimes and falls back to `Text`
21
+ * on web or when the unstable export is unavailable.
22
+ */
23
+ export const NativeText: ComponentType<TextProps> = nativeText;
@@ -1,7 +1,23 @@
1
1
  /* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
2
- import { Platform } from 'react-native';
3
2
 
4
- export const NativeView =
5
- Platform.OS === 'web'
6
- ? require('react-native').View
7
- : require('react-native/Libraries/Components/View/ViewNativeComponent').default;
3
+ import type { ComponentType } from 'react';
4
+ import type { ViewProps } from 'react-native';
5
+
6
+ const reactNative = require('react-native');
7
+ const isWeb = reactNative.Platform.OS === 'web';
8
+
9
+ let nativeView = reactNative.unstable_NativeView;
10
+
11
+ if (isWeb || nativeView == null) {
12
+ // Fallback to regular View component if unstable_NativeView is not available or we're on Web
13
+ nativeView = reactNative.View;
14
+ }
15
+
16
+ /**
17
+ * Native View component with graceful fallback.
18
+ *
19
+ * @remarks
20
+ * Uses `unstable_NativeView` on supported native runtimes and falls back to `View`
21
+ * on web or when the unstable export is unavailable.
22
+ */
23
+ export const NativeView: ComponentType<ViewProps> = nativeView;
@@ -4,6 +4,16 @@ import { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from '
4
4
 
5
5
  const propsCache = new WeakMap();
6
6
 
7
+ /**
8
+ * Normalizes `Text` style values for `NativeText`.
9
+ *
10
+ * @param style - Style prop passed to a text-like component.
11
+ * @returns Native-friendly text props. Returns an empty object when `style` is falsy or cannot be normalized.
12
+ * @remarks
13
+ * - Flattens style arrays via `StyleSheet.flatten`
14
+ * - Converts numeric `fontWeight` values to string values
15
+ * - Maps `userSelect` and `verticalAlign` to native-compatible props
16
+ */
7
17
  export function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<TextProps> {
8
18
  if (!style) return {};
9
19
 
@@ -38,6 +48,16 @@ export function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<Te
38
48
  return props;
39
49
  }
40
50
 
51
+ /**
52
+ * Normalizes accessibility and ARIA props for runtime native components.
53
+ *
54
+ * @param props - Accessibility and ARIA props.
55
+ * @returns Props with normalized accessibility fields.
56
+ * @remarks
57
+ * - Merges `aria-label` with `accessibilityLabel`
58
+ * - Merges ARIA state fields into `accessibilityState`
59
+ * - Defaults `accessible` to `true` when omitted
60
+ */
41
61
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
62
  export function processAccessibilityProps(props: Record<string, any>): Record<string, any> {
43
63
  const {
@@ -1 +1,6 @@
1
+ /**
2
+ * Recursive style prop shape accepted by runtime style helpers.
3
+ *
4
+ * @template T - Style object type.
5
+ */
1
6
  export type GenericStyleProp<T> = null | void | T | false | '' | ReadonlyArray<GenericStyleProp<T>>;
@@ -1,4 +1,6 @@
1
- // Maps the `userSelect` prop to the native `selectable` prop
1
+ /**
2
+ * Maps CSS-like `userSelect` values to React Native's `selectable` prop.
3
+ */
2
4
  export const userSelectToSelectableMap = {
3
5
  auto: true,
4
6
  text: true,
@@ -7,7 +9,9 @@ export const userSelectToSelectableMap = {
7
9
  all: true,
8
10
  };
9
11
 
10
- // Maps the `verticalAlign` prop to the native `textAlignVertical` prop
12
+ /**
13
+ * Maps CSS-like `verticalAlign` values to React Native's `textAlignVertical`.
14
+ */
11
15
  export const verticalAlignToTextAlignVerticalMap = {
12
16
  auto: 'auto',
13
17
  top: 'top',