react-native-boost 0.6.2 → 0.7.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.
@@ -179,3 +179,518 @@ export const isReactNativeImport = (path: NodePath<t.JSXOpeningElement>, expecte
179
179
  }
180
180
  return false;
181
181
  };
182
+
183
+ type AncestorClassification = 'safe' | 'text' | 'unknown';
184
+ type ScopeBinding = NonNullable<ReturnType<NodePath<t.Node>['scope']['getBinding']>>;
185
+
186
+ type AncestorAnalysisContext = {
187
+ componentCache: WeakMap<t.Node, AncestorClassification>;
188
+ componentInProgress: WeakSet<t.Node>;
189
+ renderExpressionInProgress: WeakSet<t.Node>;
190
+ };
191
+
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;
197
+ };
198
+
199
+ function classifyViewAncestors(path: NodePath<t.JSXOpeningElement>): AncestorClassification {
200
+ const context: AncestorAnalysisContext = {
201
+ componentCache: new WeakMap<t.Node, AncestorClassification>(),
202
+ componentInProgress: new WeakSet<t.Node>(),
203
+ renderExpressionInProgress: new WeakSet<t.Node>(),
204
+ };
205
+
206
+ let classification: AncestorClassification = 'safe';
207
+ let ancestorPath: NodePath<t.Node> | null = path.parentPath.parentPath;
208
+
209
+ while (ancestorPath) {
210
+ if (ancestorPath.isJSXElement()) {
211
+ const ancestorClassification = classifyJSXElementAsAncestor(ancestorPath, context);
212
+ classification = mergeAncestorClassification(classification, ancestorClassification);
213
+
214
+ if (classification === 'text') return classification;
215
+ }
216
+
217
+ ancestorPath = ancestorPath.parentPath;
218
+ }
219
+
220
+ return classification;
221
+ }
222
+
223
+ function classifyJSXElementAsAncestor(
224
+ path: NodePath<t.JSXElement>,
225
+ context: AncestorAnalysisContext
226
+ ): AncestorClassification {
227
+ const openingElementName = path.node.openingElement.name;
228
+
229
+ if (t.isJSXIdentifier(openingElementName)) {
230
+ return classifyJSXIdentifierAsAncestor(path, openingElementName.name, context);
231
+ }
232
+
233
+ if (t.isJSXMemberExpression(openingElementName)) {
234
+ return classifyJSXMemberExpressionAsAncestor(path, openingElementName);
235
+ }
236
+
237
+ return 'unknown';
238
+ }
239
+
240
+ function classifyJSXIdentifierAsAncestor(
241
+ path: NodePath<t.JSXElement>,
242
+ identifierName: string,
243
+ context: AncestorAnalysisContext
244
+ ): AncestorClassification {
245
+ if (identifierName === 'Fragment') return 'safe';
246
+
247
+ const binding = path.scope.getBinding(identifierName);
248
+ if (!binding) return 'unknown';
249
+
250
+ return classifyBindingAsAncestor(binding, context);
251
+ }
252
+
253
+ function classifyJSXMemberExpressionAsAncestor(
254
+ path: NodePath<t.JSXElement>,
255
+ expression: t.JSXMemberExpression
256
+ ): AncestorClassification {
257
+ if (!t.isJSXIdentifier(expression.object) || !t.isJSXIdentifier(expression.property)) {
258
+ return 'unknown';
259
+ }
260
+
261
+ const binding = path.scope.getBinding(expression.object.name);
262
+ if (!binding || binding.kind !== 'module' || !t.isImportNamespaceSpecifier(binding.path.node)) {
263
+ return 'unknown';
264
+ }
265
+
266
+ const importDeclaration = binding.path.parent;
267
+ if (!t.isImportDeclaration(importDeclaration)) return 'unknown';
268
+
269
+ if (importDeclaration.source.value === 'react-native') {
270
+ return expression.property.name === 'Text' ? 'text' : 'safe';
271
+ }
272
+
273
+ if (importDeclaration.source.value === 'react' && expression.property.name === 'Fragment') {
274
+ return 'safe';
275
+ }
276
+
277
+ return 'unknown';
278
+ }
279
+
280
+ function classifyBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorClassification {
281
+ if (binding.kind === 'module') {
282
+ return classifyModuleBindingAsAncestor(binding);
283
+ }
284
+
285
+ return classifyLocalBindingAsAncestor(binding, context);
286
+ }
287
+
288
+ function classifyModuleBindingAsAncestor(binding: ScopeBinding): AncestorClassification {
289
+ const importDeclaration = binding.path.parent;
290
+ if (!t.isImportDeclaration(importDeclaration)) return 'unknown';
291
+
292
+ const source = importDeclaration.source.value;
293
+ if (source === 'react-native') {
294
+ if (t.isImportSpecifier(binding.path.node)) {
295
+ const importedName = getImportSpecifierImportedName(binding.path.node);
296
+ if (!importedName) return 'unknown';
297
+ return importedName === 'Text' ? 'text' : 'safe';
298
+ }
299
+
300
+ if (t.isImportNamespaceSpecifier(binding.path.node)) {
301
+ return 'safe';
302
+ }
303
+
304
+ return 'unknown';
305
+ }
306
+
307
+ if (source === 'react' && t.isImportSpecifier(binding.path.node)) {
308
+ const importedName = getImportSpecifierImportedName(binding.path.node);
309
+ if (importedName === 'Fragment') return 'safe';
310
+ }
311
+
312
+ return 'unknown';
313
+ }
314
+
315
+ function classifyLocalBindingAsAncestor(
316
+ binding: ScopeBinding,
317
+ context: AncestorAnalysisContext
318
+ ): AncestorClassification {
319
+ const cacheKey = binding.path.node;
320
+ const cached = context.componentCache.get(cacheKey);
321
+ if (cached) return cached;
322
+
323
+ if (context.componentInProgress.has(cacheKey)) {
324
+ return 'unknown';
325
+ }
326
+
327
+ context.componentInProgress.add(cacheKey);
328
+
329
+ let classification: AncestorClassification;
330
+ if (binding.path.isFunctionDeclaration()) {
331
+ classification = analyzeFunctionComponent(binding.path, context);
332
+ } else if (binding.path.isVariableDeclarator()) {
333
+ classification = analyzeVariableDeclaratorComponent(binding.path, context);
334
+ } else {
335
+ classification = 'unknown';
336
+ }
337
+
338
+ context.componentInProgress.delete(cacheKey);
339
+ context.componentCache.set(cacheKey, classification);
340
+
341
+ return classification;
342
+ }
343
+
344
+ function analyzeVariableDeclaratorComponent(
345
+ path: NodePath<t.VariableDeclarator>,
346
+ context: AncestorAnalysisContext
347
+ ): AncestorClassification {
348
+ const initPath = path.get('init');
349
+ if (!initPath.node) return 'unknown';
350
+
351
+ if (initPath.isArrowFunctionExpression() || initPath.isFunctionExpression()) {
352
+ return analyzeFunctionComponent(initPath, context);
353
+ }
354
+
355
+ if (initPath.isCallExpression()) {
356
+ return analyzeCallWrappedComponent(initPath, context);
357
+ }
358
+
359
+ if (initPath.isIdentifier()) {
360
+ const aliasBinding = path.scope.getBinding(initPath.node.name);
361
+ if (!aliasBinding) return 'unknown';
362
+
363
+ return classifyBindingAsAncestor(aliasBinding, context);
364
+ }
365
+
366
+ return 'unknown';
367
+ }
368
+
369
+ function analyzeCallWrappedComponent(
370
+ path: NodePath<t.CallExpression>,
371
+ context: AncestorAnalysisContext
372
+ ): AncestorClassification {
373
+ if (!isReactMemoOrForwardRefCall(path)) return 'unknown';
374
+
375
+ const [firstArgumentPath] = path.get('arguments');
376
+ if (!firstArgumentPath?.node) return 'unknown';
377
+
378
+ if (firstArgumentPath.isArrowFunctionExpression() || firstArgumentPath.isFunctionExpression()) {
379
+ return analyzeFunctionComponent(firstArgumentPath, context);
380
+ }
381
+
382
+ if (firstArgumentPath.isIdentifier()) {
383
+ const wrappedComponentBinding = path.scope.getBinding(firstArgumentPath.node.name);
384
+ if (!wrappedComponentBinding) return 'unknown';
385
+
386
+ return classifyBindingAsAncestor(wrappedComponentBinding, context);
387
+ }
388
+
389
+ if (firstArgumentPath.isCallExpression()) {
390
+ return analyzeCallWrappedComponent(firstArgumentPath, context);
391
+ }
392
+
393
+ return 'unknown';
394
+ }
395
+
396
+ function isReactMemoOrForwardRefCall(path: NodePath<t.CallExpression>): boolean {
397
+ const calleePath = path.get('callee');
398
+
399
+ if (calleePath.isIdentifier()) {
400
+ if (!isMemoOrForwardRefName(calleePath.node.name)) return false;
401
+
402
+ const binding = path.scope.getBinding(calleePath.node.name);
403
+ return isReactImportBinding(binding);
404
+ }
405
+
406
+ if (calleePath.isMemberExpression()) {
407
+ const objectPath = calleePath.get('object');
408
+ const propertyPath = calleePath.get('property');
409
+
410
+ if (!objectPath.isIdentifier() || !propertyPath.isIdentifier()) return false;
411
+ if (!isMemoOrForwardRefName(propertyPath.node.name)) return false;
412
+
413
+ const objectBinding = path.scope.getBinding(objectPath.node.name);
414
+ return isReactImportBinding(objectBinding);
415
+ }
416
+
417
+ return false;
418
+ }
419
+
420
+ function isMemoOrForwardRefName(name: string): boolean {
421
+ return name === 'memo' || name === 'forwardRef';
422
+ }
423
+
424
+ function isReactImportBinding(binding: ScopeBinding | undefined): binding is ScopeBinding {
425
+ if (!binding || binding.kind !== 'module') return false;
426
+
427
+ const importDeclaration = binding.path.parent;
428
+ return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'react';
429
+ }
430
+
431
+ function analyzeFunctionComponent(
432
+ path: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
433
+ context: AncestorAnalysisContext
434
+ ): AncestorClassification {
435
+ const bodyPath = path.get('body');
436
+
437
+ if (!bodyPath.isBlockStatement()) {
438
+ return analyzeRenderExpression(bodyPath as NodePath<t.Node>, context);
439
+ }
440
+
441
+ let classification: AncestorClassification = 'safe';
442
+
443
+ for (const statementPath of bodyPath.get('body')) {
444
+ if (!statementPath.isReturnStatement()) continue;
445
+
446
+ const argumentPath = statementPath.get('argument');
447
+ if (!argumentPath.node) continue;
448
+
449
+ const returnClassification = analyzeRenderExpression(argumentPath as NodePath<t.Node>, context);
450
+ classification = mergeAncestorClassification(classification, returnClassification);
451
+
452
+ if (classification === 'text') return classification;
453
+ }
454
+
455
+ return classification;
456
+ }
457
+
458
+ function analyzeRenderExpression(path: NodePath<t.Node>, context: AncestorAnalysisContext): AncestorClassification {
459
+ if (path.isJSXFragment()) {
460
+ return analyzeJSXChildren(path.get('children'), context);
461
+ }
462
+
463
+ let classification: AncestorClassification = 'safe';
464
+ let hasJSX = false;
465
+
466
+ path.traverse({
467
+ JSXOpeningElement(jsxPath) {
468
+ hasJSX = true;
469
+
470
+ const jsxElementPath = jsxPath.parentPath;
471
+ if (!jsxElementPath.isJSXElement()) {
472
+ classification = mergeAncestorClassification(classification, 'unknown');
473
+ return;
474
+ }
475
+
476
+ const jsxClassification = classifyJSXElementAsAncestor(jsxElementPath, context);
477
+ classification = mergeAncestorClassification(classification, jsxClassification);
478
+
479
+ if (classification === 'text') {
480
+ jsxPath.stop();
481
+ }
482
+ },
483
+ });
484
+
485
+ if (hasJSX) return classification;
486
+
487
+ if (path.isIdentifier()) {
488
+ return analyzeIdentifierRenderExpression(path, context);
489
+ }
490
+
491
+ if (path.isMemberExpression() && isPropsChildrenMemberExpression(path.node)) {
492
+ return 'safe';
493
+ }
494
+
495
+ if (
496
+ path.isNullLiteral() ||
497
+ path.isBooleanLiteral() ||
498
+ path.isNumericLiteral() ||
499
+ path.isStringLiteral() ||
500
+ path.isBigIntLiteral()
501
+ ) {
502
+ return 'safe';
503
+ }
504
+
505
+ return 'unknown';
506
+ }
507
+
508
+ function analyzeJSXChildren(
509
+ children: Array<NodePath<t.JSXText | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXElement | t.JSXFragment>>,
510
+ context: AncestorAnalysisContext
511
+ ): AncestorClassification {
512
+ let classification: AncestorClassification = 'safe';
513
+
514
+ for (const childPath of children) {
515
+ if (childPath.isJSXElement()) {
516
+ const childClassification = classifyJSXElementAsAncestor(childPath, context);
517
+ classification = mergeAncestorClassification(classification, childClassification);
518
+ } else if (childPath.isJSXFragment()) {
519
+ const fragmentClassification = analyzeJSXChildren(childPath.get('children'), context);
520
+ classification = mergeAncestorClassification(classification, fragmentClassification);
521
+ } else if (childPath.isJSXExpressionContainer()) {
522
+ const expressionPath = childPath.get('expression');
523
+ if (!expressionPath.node || expressionPath.isJSXEmptyExpression()) continue;
524
+
525
+ const expressionClassification = analyzeRenderExpression(expressionPath as NodePath<t.Node>, context);
526
+ classification = mergeAncestorClassification(classification, expressionClassification);
527
+ } else if (childPath.isJSXSpreadChild()) {
528
+ classification = mergeAncestorClassification(classification, 'unknown');
529
+ }
530
+
531
+ if (classification === 'text') {
532
+ return classification;
533
+ }
534
+ }
535
+
536
+ return classification;
537
+ }
538
+
539
+ function analyzeIdentifierRenderExpression(
540
+ path: NodePath<t.Identifier>,
541
+ context: AncestorAnalysisContext
542
+ ): AncestorClassification {
543
+ if (path.node.name === 'children') return 'safe';
544
+
545
+ const binding = path.scope.getBinding(path.node.name);
546
+ if (!binding) return 'unknown';
547
+
548
+ if (binding.kind === 'param') {
549
+ return binding.identifier.name === 'children' ? 'safe' : 'unknown';
550
+ }
551
+
552
+ if (!binding.path.isVariableDeclarator()) return 'unknown';
553
+
554
+ const cacheKey = binding.path.node;
555
+ if (context.renderExpressionInProgress.has(cacheKey)) {
556
+ return 'unknown';
557
+ }
558
+
559
+ const initPath = binding.path.get('init');
560
+ if (!initPath.node) return 'unknown';
561
+
562
+ context.renderExpressionInProgress.add(cacheKey);
563
+ const classification = analyzeRenderExpression(initPath as NodePath<t.Node>, context);
564
+ context.renderExpressionInProgress.delete(cacheKey);
565
+
566
+ return classification;
567
+ }
568
+
569
+ function isPropsChildrenMemberExpression(expression: t.MemberExpression): boolean {
570
+ if (!t.isIdentifier(expression.object, { name: 'props' })) return false;
571
+ if (!t.isIdentifier(expression.property, { name: 'children' })) return false;
572
+ return !expression.computed;
573
+ }
574
+
575
+ function mergeAncestorClassification(
576
+ current: AncestorClassification,
577
+ next: AncestorClassification
578
+ ): AncestorClassification {
579
+ if (current === 'text' || next === 'text') return 'text';
580
+ if (current === 'unknown' || next === 'unknown') return 'unknown';
581
+ return 'safe';
582
+ }
583
+
584
+ function getImportSpecifierImportedName(specifier: t.ImportSpecifier): string | undefined {
585
+ if (t.isIdentifier(specifier.imported)) {
586
+ return specifier.imported.name;
587
+ }
588
+
589
+ if (t.isStringLiteral(specifier.imported)) {
590
+ return specifier.imported.value;
591
+ }
592
+
593
+ return undefined;
594
+ }
595
+
596
+ /**
597
+ * Checks whether the closest JSX element ancestor is expo-router Link with a truthy asChild prop.
598
+ *
599
+ * We only bail on Text optimization when Link is effectively slotting that Text as the clickable child.
600
+ */
601
+ export const hasExpoRouterLinkParentWithAsChild = (path: NodePath<t.JSXOpeningElement>): boolean => {
602
+ const textElementPath = path.parentPath;
603
+ if (!textElementPath.isJSXElement()) return false;
604
+
605
+ let ancestorPath: NodePath<t.Node> | null = textElementPath.parentPath;
606
+
607
+ while (ancestorPath) {
608
+ if (ancestorPath.isJSXElement()) {
609
+ if (!isExpoRouterLinkElement(ancestorPath)) return false;
610
+
611
+ return hasTruthyAsChildAttribute(ancestorPath.node.openingElement.attributes);
612
+ }
613
+
614
+ ancestorPath = ancestorPath.parentPath;
615
+ }
616
+
617
+ return false;
618
+ };
619
+
620
+ function isExpoRouterLinkElement(path: NodePath<t.JSXElement>): boolean {
621
+ const openingElementName = path.node.openingElement.name;
622
+
623
+ if (t.isJSXIdentifier(openingElementName)) {
624
+ const binding = path.scope.getBinding(openingElementName.name);
625
+ if (!binding || binding.kind !== 'module') return false;
626
+ if (!t.isImportSpecifier(binding.path.node)) return false;
627
+
628
+ const importDeclaration = binding.path.parent;
629
+ if (!t.isImportDeclaration(importDeclaration) || importDeclaration.source.value !== 'expo-router') return false;
630
+
631
+ const imported = binding.path.node.imported;
632
+ return t.isIdentifier(imported, { name: 'Link' }) || (t.isStringLiteral(imported) && imported.value === 'Link');
633
+ }
634
+
635
+ if (t.isJSXMemberExpression(openingElementName)) {
636
+ if (!t.isJSXIdentifier(openingElementName.object)) return false;
637
+ if (!t.isJSXIdentifier(openingElementName.property, { name: 'Link' })) return false;
638
+
639
+ const namespaceBinding = path.scope.getBinding(openingElementName.object.name);
640
+ if (!namespaceBinding || namespaceBinding.kind !== 'module') return false;
641
+ if (!t.isImportNamespaceSpecifier(namespaceBinding.path.node)) return false;
642
+
643
+ const importDeclaration = namespaceBinding.path.parent;
644
+ return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'expo-router';
645
+ }
646
+
647
+ return false;
648
+ }
649
+
650
+ function hasTruthyAsChildAttribute(attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): boolean {
651
+ let asChildAttribute: t.JSXAttribute | undefined;
652
+
653
+ for (const attribute of attributes) {
654
+ if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'asChild' })) {
655
+ asChildAttribute = attribute;
656
+ }
657
+ }
658
+
659
+ if (!asChildAttribute) return false;
660
+
661
+ return isJSXAttributeValueTruthy(asChildAttribute.value);
662
+ }
663
+
664
+ function isJSXAttributeValueTruthy(value: t.JSXAttribute['value']): boolean {
665
+ if (!value) return true;
666
+ if (t.isStringLiteral(value)) return value.value.length > 0;
667
+ if (t.isJSXElement(value) || t.isJSXFragment(value)) return true;
668
+
669
+ if (t.isJSXExpressionContainer(value)) {
670
+ const staticTruthiness = getStaticExpressionTruthiness(value.expression);
671
+ return staticTruthiness ?? true;
672
+ }
673
+
674
+ return true;
675
+ }
676
+
677
+ function getStaticExpressionTruthiness(expression: t.Expression | t.JSXEmptyExpression): boolean | undefined {
678
+ if (t.isJSXEmptyExpression(expression)) return false;
679
+ if (t.isBooleanLiteral(expression)) return expression.value;
680
+ if (t.isNullLiteral(expression)) return false;
681
+ if (t.isStringLiteral(expression)) return expression.value.length > 0;
682
+ if (t.isNumericLiteral(expression)) return expression.value !== 0 && !Number.isNaN(expression.value);
683
+ if (t.isBigIntLiteral(expression)) return expression.value !== '0';
684
+ if (t.isIdentifier(expression, { name: 'undefined' })) return false;
685
+
686
+ if (t.isTemplateLiteral(expression) && expression.expressions.length === 0) {
687
+ return (expression.quasis[0]?.value.cooked ?? '').length > 0;
688
+ }
689
+
690
+ if (t.isUnaryExpression(expression, { operator: '!' })) {
691
+ const staticTruthiness = getStaticExpressionTruthiness(expression.argument);
692
+ return staticTruthiness === undefined ? undefined : !staticTruthiness;
693
+ }
694
+
695
+ return undefined;
696
+ }
@@ -14,3 +14,12 @@ export const ACCESSIBILITY_PROPERTIES = new Set([
14
14
  'aria-selected',
15
15
  'accessible',
16
16
  ]);
17
+
18
+ // Maps the `userSelect` values to the corresponding boolean for the `selectable` prop
19
+ export const USER_SELECT_STYLE_TO_SELECTABLE_PROP: Record<string, boolean> = {
20
+ auto: true,
21
+ text: true,
22
+ none: false,
23
+ contain: true,
24
+ all: true,
25
+ };
@@ -0,0 +1,29 @@
1
+ import type { ResultFormatter } from 'babel-plugin-tester';
2
+ import { format, type FormatOptions } from 'oxfmt';
3
+ import oxfmtConfig from '../../../../../.oxfmtrc.json';
4
+
5
+ type RootOxfmtConfig = FormatOptions & {
6
+ $schema?: string;
7
+ ignorePatterns?: string[];
8
+ };
9
+
10
+ const oxfmtOptions = buildOxfmtOptions(oxfmtConfig as RootOxfmtConfig);
11
+
12
+ export const formatTestResult: ResultFormatter = async (code, options) => {
13
+ const filepath = options?.filepath ?? 'output.js';
14
+ const result = await format(filepath, code, oxfmtOptions);
15
+
16
+ if (result.errors.length > 0) {
17
+ throw new Error(result.errors[0].message);
18
+ }
19
+
20
+ return result.code;
21
+ };
22
+
23
+ function buildOxfmtOptions(config: RootOxfmtConfig): FormatOptions {
24
+ const { $schema, ignorePatterns, ...oxfmtOptions } = config;
25
+ void $schema;
26
+ void ignorePatterns;
27
+
28
+ return oxfmtOptions;
29
+ }
@@ -1,7 +1,7 @@
1
1
  import { declare } from '@babel/helper-plugin-utils';
2
- import { Optimizer } from '../types';
2
+ import { Optimizer, PluginOptions } from '../types';
3
3
 
4
- export const generateTestPlugin = (optimizer: Optimizer) => {
4
+ export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions = {}) => {
5
5
  return declare((api) => {
6
6
  api.assertVersion(7);
7
7
 
@@ -9,7 +9,7 @@ export const generateTestPlugin = (optimizer: Optimizer) => {
9
9
  name: 'react-native-boost',
10
10
  visitor: {
11
11
  JSXOpeningElement(path) {
12
- optimizer(path);
12
+ optimizer(path, undefined, options);
13
13
  },
14
14
  },
15
15
  };
@@ -1,120 +0,0 @@
1
- import { NodePath, types as t } from '@babel/core';
2
-
3
- /**
4
- * Checks if any ancestor element is of the specified component type or contains that component type.
5
- * This function handles both direct ancestors and custom components that may contain the specified component.
6
- *
7
- * @param path - The path to the JSXOpeningElement.
8
- * @param componentName - The name of the component to check for in ancestors.
9
- * @param skipComponents - Optional array of component names to skip when checking ancestors.
10
- * @returns true if any ancestor is or contains the specified component.
11
- */
12
- export function hasComponentAncestor(
13
- path: NodePath<t.JSXOpeningElement>,
14
- componentName: string,
15
- skipComponents: string[] = ['Fragment']
16
- ): boolean {
17
- // Check for direct ancestors of the specified component type
18
- const directAncestor = path.findParent((parentPath) => {
19
- return (
20
- t.isJSXElement(parentPath.node) && t.isJSXIdentifier(parentPath.node.openingElement.name, { name: componentName })
21
- );
22
- });
23
-
24
- if (directAncestor) return true;
25
-
26
- // Check for indirect ancestors (custom components that contain the specified component)
27
- return !!path.findParent((parentPath) => {
28
- // Only check JSX elements
29
- if (!t.isJSXElement(parentPath.node)) return false;
30
-
31
- // Get the component name
32
- const openingElement = parentPath.node.openingElement;
33
- if (!t.isJSXIdentifier(openingElement.name)) return false;
34
-
35
- const ancestorComponentName = openingElement.name.name;
36
-
37
- // Skip the component we're looking for
38
- if (ancestorComponentName === componentName) {
39
- return false;
40
- }
41
-
42
- // Skip components in the skipComponents list
43
- if (skipComponents.includes(ancestorComponentName)) {
44
- return false;
45
- }
46
-
47
- // Skip lowercase components (built-in HTML elements)
48
- if (ancestorComponentName[0] === ancestorComponentName[0].toLowerCase()) {
49
- return false;
50
- }
51
-
52
- // Try to find the component definition through variable binding
53
- const binding = parentPath.scope.getBinding(ancestorComponentName);
54
- if (!binding) return false;
55
-
56
- // Now check the component definition for the specified component
57
- if (t.isVariableDeclarator(binding.path.node)) {
58
- const init = binding.path.node.init;
59
-
60
- // Handle arrow functions or function expressions
61
- if (t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) {
62
- // Check the function body for the specified component
63
- return t.isBlockStatement(init.body)
64
- ? hasComponentInReturnStatement(init.body, componentName)
65
- : hasComponentInExpression(init.body, componentName);
66
- }
67
- } else if (t.isFunctionDeclaration(binding.path.node)) {
68
- // Handle function declarations
69
- return hasComponentInReturnStatement(binding.path.node.body, componentName);
70
- }
71
-
72
- return false;
73
- });
74
- }
75
-
76
- /**
77
- * Check if a block statement contains a return statement with the specified component
78
- *
79
- * @param blockStatement - The block statement to check
80
- * @param componentName - The name of the component to look for
81
- * @returns true if the block statement contains a return with the specified component
82
- */
83
- function hasComponentInReturnStatement(blockStatement: t.BlockStatement, componentName: string): boolean {
84
- for (const statement of blockStatement.body) {
85
- if (
86
- t.isReturnStatement(statement) &&
87
- statement.argument &&
88
- hasComponentInExpression(statement.argument, componentName)
89
- ) {
90
- return true;
91
- }
92
- }
93
- return false;
94
- }
95
-
96
- /**
97
- * Check if an expression contains the specified component
98
- *
99
- * @param expression - The expression to check
100
- * @param componentName - The name of the component to look for
101
- * @returns true if the expression contains the specified component
102
- */
103
- function hasComponentInExpression(expression: t.Expression, componentName: string): boolean {
104
- // If directly returning a JSX element
105
- if (t.isJSXElement(expression)) {
106
- // Check if it's the specified component
107
- if (t.isJSXIdentifier(expression.openingElement.name, { name: componentName })) {
108
- return true;
109
- }
110
-
111
- // Check if any children are the specified component
112
- for (const child of expression.children) {
113
- if (t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name, { name: componentName })) {
114
- return true;
115
- }
116
- }
117
- }
118
-
119
- return false;
120
- }