react-native-boost 1.1.0 → 1.2.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.
@@ -289,21 +289,80 @@ export function extractSelectableAndUpdateStyle(styleExpr: t.Expression): boolea
289
289
  }
290
290
 
291
291
  /**
292
- * Checks if a node represents a string value.
292
+ * Determines whether an expression is statically provable to evaluate to a `string` or `number`
293
+ * primitive — and therefore can never be a React element.
293
294
  */
294
- export const isStringNode = (path: NodePath<t.JSXOpeningElement>, child: t.Node): boolean => {
295
- if (t.isJSXText(child) || t.isStringLiteral(child)) return true;
295
+ const isPrimitiveExpression = (
296
+ path: NodePath<t.JSXOpeningElement>,
297
+ expression: t.Expression,
298
+ // Identifier names already resolved along this chain, to break circular `const` references
299
+ // (`const a = b; const b = a;`) that would otherwise recurse forever.
300
+ resolved: Set<string> = new Set()
301
+ ): boolean => {
302
+ // Unambiguous primitives — these can never be a React element.
303
+ if (t.isStringLiteral(expression) || t.isNumericLiteral(expression)) return true;
296
304
 
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
+ // A template literal ALWAYS coerces its interpolations to string, regardless of their types.
306
+ if (t.isTemplateLiteral(expression)) return true;
307
+
308
+ // `a + b` (and numeric `-`, `*`, ...) is primitive only when BOTH operands are themselves
309
+ // provably primitive. `in`/`instanceof` have a non-Expression `left` (a `PrivateName`), so the
310
+ // `isExpression` guard rejects them.
311
+ if (t.isBinaryExpression(expression)) {
312
+ const { left, right } = expression;
313
+ return (
314
+ t.isExpression(left) &&
315
+ isPrimitiveExpression(path, left, resolved) &&
316
+ isPrimitiveExpression(path, right, resolved)
317
+ );
318
+ }
319
+
320
+ // `c ? a : b` — only the reachable results (`a`, `b`) are rendered; the test is irrelevant.
321
+ if (t.isConditionalExpression(expression)) {
322
+ return (
323
+ isPrimitiveExpression(path, expression.consequent, resolved) &&
324
+ isPrimitiveExpression(path, expression.alternate, resolved)
325
+ );
326
+ }
327
+
328
+ // `a && b` / `a || b` / `a ?? b` — short-circuiting makes the result one of the operands, so both
329
+ // must be primitive (unlike the conditional, the left operand is itself a reachable result).
330
+ if (t.isLogicalExpression(expression)) {
331
+ return (
332
+ isPrimitiveExpression(path, expression.left, resolved) && isPrimitiveExpression(path, expression.right, resolved)
333
+ );
334
+ }
335
+
336
+ // Identifier resolving to a non-reassigned `const` whose initializer is itself provably primitive.
337
+ // `binding.constant` excludes reassigned bindings (`let x = 'a'; x = <Foo/>`).
338
+ if (t.isIdentifier(expression)) {
339
+ if (resolved.has(expression.name)) return false; // circular reference — give up
340
+ const binding = path.scope.getBinding(expression.name);
341
+ if (binding && binding.constant && binding.path.node && t.isVariableDeclarator(binding.path.node)) {
342
+ const init = binding.path.node.init;
343
+ return (
344
+ !!init && t.isExpression(init) && isPrimitiveExpression(path, init, new Set(resolved).add(expression.name))
345
+ );
305
346
  }
306
- if (t.isStringLiteral(expression)) return true;
347
+ return false;
348
+ }
349
+
350
+ return false;
351
+ };
352
+
353
+ /**
354
+ * Checks whether a Text child node is statically provable to render as a string/number primitive.
355
+ * Used to gate the Text optimizer: only such children are safe to keep when rewriting `<Text>` to
356
+ * its native host. See {@link isPrimitiveExpression} for the underlying expression rules.
357
+ */
358
+ export const isPrimitiveChild = (path: NodePath<t.JSXOpeningElement>, child: t.Node): boolean => {
359
+ if (t.isJSXText(child)) return true; // raw text between tags
360
+ if (t.isStringLiteral(child)) return true; // explicit `children="..."` attribute value
361
+
362
+ if (t.isJSXExpressionContainer(child)) {
363
+ const { expression } = child;
364
+ if (t.isJSXEmptyExpression(expression)) return false; // `{/* comment */}` is not a primitive
365
+ return isPrimitiveExpression(path, expression);
307
366
  }
308
367
  return false;
309
368
  };
@@ -1,5 +1,5 @@
1
1
  import { NodePath, types as t } from '@babel/core';
2
- import { ensureArray } from '../helpers';
2
+ import { ensureArray, BailoutCheck } from '../helpers';
3
3
  import { HubFile } from '../../types';
4
4
  import { minimatch } from 'minimatch';
5
5
  import nodePath from 'node:path';
@@ -177,8 +177,7 @@ export const isReactNativeImport = (path: NodePath<t.JSXOpeningElement>, expecte
177
177
  return false;
178
178
  };
179
179
 
180
- type AncestorClassification = 'safe' | 'text' | 'unknown';
181
- export type ViewAncestorClassification = AncestorClassification;
180
+ export type AncestorClassification = 'safe' | 'text' | 'unknown';
182
181
  type ScopeBinding = NonNullable<ReturnType<NodePath<t.Node>['scope']['getBinding']>>;
183
182
 
184
183
  type AncestorAnalysisContext = {
@@ -187,11 +186,7 @@ type AncestorAnalysisContext = {
187
186
  renderExpressionInProgress: WeakSet<t.Node>;
188
187
  };
189
188
 
190
- export const getViewAncestorClassification = (path: NodePath<t.JSXOpeningElement>): ViewAncestorClassification => {
191
- return classifyViewAncestors(path);
192
- };
193
-
194
- function classifyViewAncestors(path: NodePath<t.JSXOpeningElement>): AncestorClassification {
189
+ export const getAncestorClassification = (path: NodePath<t.JSXOpeningElement>): AncestorClassification => {
195
190
  const context: AncestorAnalysisContext = {
196
191
  componentCache: new WeakMap<t.Node, AncestorClassification>(),
197
192
  componentInProgress: new WeakSet<t.Node>(),
@@ -213,7 +208,35 @@ function classifyViewAncestors(path: NodePath<t.JSXOpeningElement>): AncestorCla
213
208
  }
214
209
 
215
210
  return classification;
216
- }
211
+ };
212
+
213
+ /**
214
+ * The ancestor-safety bailout checks shared by the Text and View optimizers. An element nested under a
215
+ * `Text` renders as the inline `NativeVirtualText` host (`RCTVirtualText`) instead of the block
216
+ * `NativeText`/`NativeView` host, so optimizing it would emit the wrong host; an `'unknown'` ancestor
217
+ * chain cannot be proven free of such a `Text`, so it bails too unless the caller opts into the risk.
218
+ *
219
+ * The ancestor walk is lazy (it runs only if these checks are reached) and memoized across the two
220
+ * checks. Each optimizer passes its own resolved `dangerouslyOptimize*WithUnknownAncestors` flag.
221
+ */
222
+ export const ancestorBailoutChecks = (
223
+ path: NodePath<t.JSXOpeningElement>,
224
+ dangerousOptimizationEnabled: boolean
225
+ ): BailoutCheck[] => {
226
+ let classification: AncestorClassification | undefined;
227
+ const classify = () => (classification ??= getAncestorClassification(path));
228
+
229
+ return [
230
+ {
231
+ reason: 'has Text ancestor',
232
+ shouldBail: () => classify() === 'text',
233
+ },
234
+ {
235
+ reason: 'has unresolved ancestor and dangerous optimization is disabled',
236
+ shouldBail: () => classify() === 'unknown' && !dangerousOptimizationEnabled,
237
+ },
238
+ ];
239
+ };
217
240
 
218
241
  function classifyJSXElementAsAncestor(
219
242
  path: NodePath<t.JSXElement>,
@@ -1,4 +1,4 @@
1
- import { TextProps, TextStyle, StyleSheet } from 'react-native';
1
+ import { TextProps, TextStyle, StyleSheet, Platform } from 'react-native';
2
2
  import { GenericStyleProp } from './types';
3
3
  import { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from './utils/constants';
4
4
 
@@ -49,14 +49,28 @@ export function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<Te
49
49
  }
50
50
 
51
51
  /**
52
- * Normalizes accessibility and ARIA props for runtime native components.
52
+ * The default value `Text` resolves for `accessible` when the prop is omitted: `true` on iOS (text is
53
+ * an accessibility element unless opted out), `false` on Android, and `undefined` elsewhere.
54
+ *
55
+ * @remarks
56
+ * Runtime fallback for the common optimized `<Text>` path (no accessibility props) when the target
57
+ * platform is unknown at build time. When it is known (Metro reports it on the Babel caller), the
58
+ * plugin inlines the literal instead and this is not emitted. Evaluated per render — like `Text`'s own
59
+ * `Platform.select` — rather than hoisted to a constant, so it always reflects the current platform.
60
+ */
61
+ export const getDefaultTextAccessible = (): boolean | undefined => Platform.select({ ios: true, android: false });
62
+
63
+ /**
64
+ * Normalizes accessibility and ARIA props for runtime native components, mirroring the reconciliation
65
+ * `Text` performs before handing off to its native host.
53
66
  *
54
67
  * @param props - Accessibility and ARIA props.
55
68
  * @returns Props with normalized accessibility fields.
56
69
  * @remarks
57
70
  * - Merges `aria-label` with `accessibilityLabel`
58
71
  * - Merges ARIA state fields into `accessibilityState`
59
- * - Defaults `accessible` to `true` when omitted
72
+ * - Reconciles `disabled` with `accessibilityState.disabled` (the explicit `disabled` prop wins)
73
+ * - Resolves the platform-specific `accessible` default (see {@link getDefaultTextAccessible})
60
74
  */
61
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
76
  export function processAccessibilityProps(props: Record<string, any>): Record<string, any> {
@@ -70,6 +84,7 @@ export function processAccessibilityProps(props: Record<string, any>): Record<st
70
84
  ['aria-expanded']: ariaExpanded,
71
85
  ['aria-selected']: ariaSelected,
72
86
  accessible,
87
+ disabled,
73
88
  ...restProperties
74
89
  } = props;
75
90
 
@@ -97,14 +112,33 @@ export function processAccessibilityProps(props: Record<string, any>): Record<st
97
112
  };
98
113
  }
99
114
 
100
- // For the accessible prop, if not provided, default to `true`
101
- const normalizedAccessible = accessible == null ? true : accessible;
115
+ // Reconcile `disabled` with `accessibilityState.disabled`. When the two are out of sync (and not
116
+ // both falsy) the explicit `disabled` prop wins and is mirrored back into the state object, so the
117
+ // native host receives a consistent value on both fields.
118
+ const stateDisabled = normalizedState?.disabled;
119
+ const normalizedDisabled = disabled ?? stateDisabled;
120
+ if (
121
+ normalizedDisabled !== stateDisabled &&
122
+ ((normalizedDisabled != null && normalizedDisabled !== false) || (stateDisabled != null && stateDisabled !== false))
123
+ ) {
124
+ normalizedState = { ...normalizedState, disabled: normalizedDisabled };
125
+ }
126
+
127
+ // Resolve `accessible` exactly as `Text` does: opt-out on iOS, off by default on Android. The
128
+ // Android pressable case (`onPress`/`onLongPress`) never applies — press handlers bail out of
129
+ // optimization — so an omitted prop falls back to the platform default.
130
+ const normalizedAccessible = Platform.select({
131
+ ios: accessible !== false,
132
+ android: accessible ?? false,
133
+ default: accessible,
134
+ });
102
135
 
103
136
  return {
104
137
  ...restProperties,
105
138
  accessibilityLabel: normalizedLabel,
106
139
  accessibilityState: normalizedState,
107
140
  accessible: normalizedAccessible,
141
+ disabled: normalizedDisabled,
108
142
  };
109
143
  }
110
144
 
@@ -5,6 +5,11 @@ import { GenericStyleProp } from './types';
5
5
 
6
6
  export const processTextStyle = (style: GenericStyleProp<TextStyle>) => ({ style }) as Partial<TextProps>;
7
7
 
8
+ // On Web there is no platform-specific `accessible` default to apply; react-native-web's `Text`
9
+ // derives accessibility from the rendered DOM. Returning `undefined` makes the injected
10
+ // `accessible={getDefaultTextAccessible()}` a no-op.
11
+ export const getDefaultTextAccessible = (): boolean | undefined => undefined;
12
+
8
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
14
  export function processAccessibilityProps(props: Record<string, any>): Record<string, any> {
10
15
  return props;