react-native-boost 0.6.2 → 1.0.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.
Files changed (34) hide show
  1. package/README.md +0 -3
  2. package/dist/plugin/esm/index.mjs +709 -199
  3. package/dist/plugin/esm/index.mjs.map +1 -1
  4. package/dist/plugin/index.d.ts +54 -0
  5. package/dist/plugin/index.js +709 -199
  6. package/dist/plugin/index.js.map +1 -1
  7. package/dist/runtime/esm/index.mjs +15 -3
  8. package/dist/runtime/esm/index.mjs.map +1 -1
  9. package/dist/runtime/esm/index.web.mjs.map +1 -1
  10. package/dist/runtime/index.d.ts +51 -4
  11. package/dist/runtime/index.js +16 -4
  12. package/dist/runtime/index.js.map +1 -1
  13. package/dist/runtime/index.web.d.ts +13 -1
  14. package/dist/runtime/index.web.js.map +1 -1
  15. package/package.json +13 -14
  16. package/src/plugin/index.ts +27 -5
  17. package/src/plugin/optimizers/text/index.ts +116 -92
  18. package/src/plugin/optimizers/view/index.ts +53 -31
  19. package/src/plugin/types/index.ts +67 -17
  20. package/src/plugin/utils/common/attributes.ts +165 -0
  21. package/src/plugin/utils/common/index.ts +1 -3
  22. package/src/plugin/utils/common/validation.ts +513 -0
  23. package/src/plugin/utils/constants.ts +9 -0
  24. package/src/plugin/utils/format-test-result.ts +29 -0
  25. package/src/plugin/utils/generate-test-plugin.ts +9 -3
  26. package/src/plugin/utils/helpers.ts +15 -0
  27. package/src/plugin/utils/logger.ts +109 -2
  28. package/src/runtime/components/native-text.tsx +21 -5
  29. package/src/runtime/components/native-view.tsx +21 -5
  30. package/src/runtime/index.ts +20 -0
  31. package/src/runtime/types/index.ts +5 -0
  32. package/src/runtime/utils/constants.ts +6 -2
  33. package/src/plugin/utils/common/ancestors.ts +0 -120
  34. package/src/plugin/utils/common/node-types.ts +0 -22
@@ -179,3 +179,516 @@ export const isReactNativeImport = (path: NodePath<t.JSXOpeningElement>, expecte
179
179
  }
180
180
  return false;
181
181
  };
182
+
183
+ type AncestorClassification = 'safe' | 'text' | 'unknown';
184
+ export type ViewAncestorClassification = AncestorClassification;
185
+ type ScopeBinding = NonNullable<ReturnType<NodePath<t.Node>['scope']['getBinding']>>;
186
+
187
+ type AncestorAnalysisContext = {
188
+ componentCache: WeakMap<t.Node, AncestorClassification>;
189
+ componentInProgress: WeakSet<t.Node>;
190
+ renderExpressionInProgress: WeakSet<t.Node>;
191
+ };
192
+
193
+ export const getViewAncestorClassification = (path: NodePath<t.JSXOpeningElement>): ViewAncestorClassification => {
194
+ return classifyViewAncestors(path);
195
+ };
196
+
197
+ function classifyViewAncestors(path: NodePath<t.JSXOpeningElement>): AncestorClassification {
198
+ const context: AncestorAnalysisContext = {
199
+ componentCache: new WeakMap<t.Node, AncestorClassification>(),
200
+ componentInProgress: new WeakSet<t.Node>(),
201
+ renderExpressionInProgress: new WeakSet<t.Node>(),
202
+ };
203
+
204
+ let classification: AncestorClassification = 'safe';
205
+ let ancestorPath: NodePath<t.Node> | null = path.parentPath.parentPath;
206
+
207
+ while (ancestorPath) {
208
+ if (ancestorPath.isJSXElement()) {
209
+ const ancestorClassification = classifyJSXElementAsAncestor(ancestorPath, context);
210
+ classification = mergeAncestorClassification(classification, ancestorClassification);
211
+
212
+ if (classification === 'text') return classification;
213
+ }
214
+
215
+ ancestorPath = ancestorPath.parentPath;
216
+ }
217
+
218
+ return classification;
219
+ }
220
+
221
+ function classifyJSXElementAsAncestor(
222
+ path: NodePath<t.JSXElement>,
223
+ context: AncestorAnalysisContext
224
+ ): AncestorClassification {
225
+ const openingElementName = path.node.openingElement.name;
226
+
227
+ if (t.isJSXIdentifier(openingElementName)) {
228
+ return classifyJSXIdentifierAsAncestor(path, openingElementName.name, context);
229
+ }
230
+
231
+ if (t.isJSXMemberExpression(openingElementName)) {
232
+ return classifyJSXMemberExpressionAsAncestor(path, openingElementName);
233
+ }
234
+
235
+ return 'unknown';
236
+ }
237
+
238
+ function classifyJSXIdentifierAsAncestor(
239
+ path: NodePath<t.JSXElement>,
240
+ identifierName: string,
241
+ context: AncestorAnalysisContext
242
+ ): AncestorClassification {
243
+ if (identifierName === 'Fragment') return 'safe';
244
+
245
+ const binding = path.scope.getBinding(identifierName);
246
+ if (!binding) return 'unknown';
247
+
248
+ return classifyBindingAsAncestor(binding, context);
249
+ }
250
+
251
+ function classifyJSXMemberExpressionAsAncestor(
252
+ path: NodePath<t.JSXElement>,
253
+ expression: t.JSXMemberExpression
254
+ ): AncestorClassification {
255
+ if (!t.isJSXIdentifier(expression.object) || !t.isJSXIdentifier(expression.property)) {
256
+ return 'unknown';
257
+ }
258
+
259
+ const binding = path.scope.getBinding(expression.object.name);
260
+ if (!binding || binding.kind !== 'module' || !t.isImportNamespaceSpecifier(binding.path.node)) {
261
+ return 'unknown';
262
+ }
263
+
264
+ const importDeclaration = binding.path.parent;
265
+ if (!t.isImportDeclaration(importDeclaration)) return 'unknown';
266
+
267
+ if (importDeclaration.source.value === 'react-native') {
268
+ return expression.property.name === 'Text' ? 'text' : 'safe';
269
+ }
270
+
271
+ if (importDeclaration.source.value === 'react' && expression.property.name === 'Fragment') {
272
+ return 'safe';
273
+ }
274
+
275
+ return 'unknown';
276
+ }
277
+
278
+ function classifyBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorClassification {
279
+ if (binding.kind === 'module') {
280
+ return classifyModuleBindingAsAncestor(binding);
281
+ }
282
+
283
+ return classifyLocalBindingAsAncestor(binding, context);
284
+ }
285
+
286
+ function classifyModuleBindingAsAncestor(binding: ScopeBinding): AncestorClassification {
287
+ const importDeclaration = binding.path.parent;
288
+ if (!t.isImportDeclaration(importDeclaration)) return 'unknown';
289
+
290
+ const source = importDeclaration.source.value;
291
+ if (source === 'react-native') {
292
+ if (t.isImportSpecifier(binding.path.node)) {
293
+ const importedName = getImportSpecifierImportedName(binding.path.node);
294
+ if (!importedName) return 'unknown';
295
+ return importedName === 'Text' ? 'text' : 'safe';
296
+ }
297
+
298
+ if (t.isImportNamespaceSpecifier(binding.path.node)) {
299
+ return 'safe';
300
+ }
301
+
302
+ return 'unknown';
303
+ }
304
+
305
+ if (source === 'react' && t.isImportSpecifier(binding.path.node)) {
306
+ const importedName = getImportSpecifierImportedName(binding.path.node);
307
+ if (importedName === 'Fragment') return 'safe';
308
+ }
309
+
310
+ return 'unknown';
311
+ }
312
+
313
+ function classifyLocalBindingAsAncestor(
314
+ binding: ScopeBinding,
315
+ context: AncestorAnalysisContext
316
+ ): AncestorClassification {
317
+ const cacheKey = binding.path.node;
318
+ const cached = context.componentCache.get(cacheKey);
319
+ if (cached) return cached;
320
+
321
+ if (context.componentInProgress.has(cacheKey)) {
322
+ return 'unknown';
323
+ }
324
+
325
+ context.componentInProgress.add(cacheKey);
326
+
327
+ let classification: AncestorClassification;
328
+ if (binding.path.isFunctionDeclaration()) {
329
+ classification = analyzeFunctionComponent(binding.path, context);
330
+ } else if (binding.path.isVariableDeclarator()) {
331
+ classification = analyzeVariableDeclaratorComponent(binding.path, context);
332
+ } else {
333
+ classification = 'unknown';
334
+ }
335
+
336
+ context.componentInProgress.delete(cacheKey);
337
+ context.componentCache.set(cacheKey, classification);
338
+
339
+ return classification;
340
+ }
341
+
342
+ function analyzeVariableDeclaratorComponent(
343
+ path: NodePath<t.VariableDeclarator>,
344
+ context: AncestorAnalysisContext
345
+ ): AncestorClassification {
346
+ const initPath = path.get('init');
347
+ if (!initPath.node) return 'unknown';
348
+
349
+ if (initPath.isArrowFunctionExpression() || initPath.isFunctionExpression()) {
350
+ return analyzeFunctionComponent(initPath, context);
351
+ }
352
+
353
+ if (initPath.isCallExpression()) {
354
+ return analyzeCallWrappedComponent(initPath, context);
355
+ }
356
+
357
+ if (initPath.isIdentifier()) {
358
+ const aliasBinding = path.scope.getBinding(initPath.node.name);
359
+ if (!aliasBinding) return 'unknown';
360
+
361
+ return classifyBindingAsAncestor(aliasBinding, context);
362
+ }
363
+
364
+ return 'unknown';
365
+ }
366
+
367
+ function analyzeCallWrappedComponent(
368
+ path: NodePath<t.CallExpression>,
369
+ context: AncestorAnalysisContext
370
+ ): AncestorClassification {
371
+ if (!isReactMemoOrForwardRefCall(path)) return 'unknown';
372
+
373
+ const [firstArgumentPath] = path.get('arguments');
374
+ if (!firstArgumentPath?.node) return 'unknown';
375
+
376
+ if (firstArgumentPath.isArrowFunctionExpression() || firstArgumentPath.isFunctionExpression()) {
377
+ return analyzeFunctionComponent(firstArgumentPath, context);
378
+ }
379
+
380
+ if (firstArgumentPath.isIdentifier()) {
381
+ const wrappedComponentBinding = path.scope.getBinding(firstArgumentPath.node.name);
382
+ if (!wrappedComponentBinding) return 'unknown';
383
+
384
+ return classifyBindingAsAncestor(wrappedComponentBinding, context);
385
+ }
386
+
387
+ if (firstArgumentPath.isCallExpression()) {
388
+ return analyzeCallWrappedComponent(firstArgumentPath, context);
389
+ }
390
+
391
+ return 'unknown';
392
+ }
393
+
394
+ function isReactMemoOrForwardRefCall(path: NodePath<t.CallExpression>): boolean {
395
+ const calleePath = path.get('callee');
396
+
397
+ if (calleePath.isIdentifier()) {
398
+ if (!isMemoOrForwardRefName(calleePath.node.name)) return false;
399
+
400
+ const binding = path.scope.getBinding(calleePath.node.name);
401
+ return isReactImportBinding(binding);
402
+ }
403
+
404
+ if (calleePath.isMemberExpression()) {
405
+ const objectPath = calleePath.get('object');
406
+ const propertyPath = calleePath.get('property');
407
+
408
+ if (!objectPath.isIdentifier() || !propertyPath.isIdentifier()) return false;
409
+ if (!isMemoOrForwardRefName(propertyPath.node.name)) return false;
410
+
411
+ const objectBinding = path.scope.getBinding(objectPath.node.name);
412
+ return isReactImportBinding(objectBinding);
413
+ }
414
+
415
+ return false;
416
+ }
417
+
418
+ function isMemoOrForwardRefName(name: string): boolean {
419
+ return name === 'memo' || name === 'forwardRef';
420
+ }
421
+
422
+ function isReactImportBinding(binding: ScopeBinding | undefined): binding is ScopeBinding {
423
+ if (!binding || binding.kind !== 'module') return false;
424
+
425
+ const importDeclaration = binding.path.parent;
426
+ return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'react';
427
+ }
428
+
429
+ function analyzeFunctionComponent(
430
+ path: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
431
+ context: AncestorAnalysisContext
432
+ ): AncestorClassification {
433
+ const bodyPath = path.get('body');
434
+
435
+ if (!bodyPath.isBlockStatement()) {
436
+ return analyzeRenderExpression(bodyPath as NodePath<t.Node>, context);
437
+ }
438
+
439
+ let classification: AncestorClassification = 'safe';
440
+
441
+ for (const statementPath of bodyPath.get('body')) {
442
+ if (!statementPath.isReturnStatement()) continue;
443
+
444
+ const argumentPath = statementPath.get('argument');
445
+ if (!argumentPath.node) continue;
446
+
447
+ const returnClassification = analyzeRenderExpression(argumentPath as NodePath<t.Node>, context);
448
+ classification = mergeAncestorClassification(classification, returnClassification);
449
+
450
+ if (classification === 'text') return classification;
451
+ }
452
+
453
+ return classification;
454
+ }
455
+
456
+ function analyzeRenderExpression(path: NodePath<t.Node>, context: AncestorAnalysisContext): AncestorClassification {
457
+ if (path.isJSXFragment()) {
458
+ return analyzeJSXChildren(path.get('children'), context);
459
+ }
460
+
461
+ let classification: AncestorClassification = 'safe';
462
+ let hasJSX = false;
463
+
464
+ path.traverse({
465
+ JSXOpeningElement(jsxPath) {
466
+ hasJSX = true;
467
+
468
+ const jsxElementPath = jsxPath.parentPath;
469
+ if (!jsxElementPath.isJSXElement()) {
470
+ classification = mergeAncestorClassification(classification, 'unknown');
471
+ return;
472
+ }
473
+
474
+ const jsxClassification = classifyJSXElementAsAncestor(jsxElementPath, context);
475
+ classification = mergeAncestorClassification(classification, jsxClassification);
476
+
477
+ if (classification === 'text') {
478
+ jsxPath.stop();
479
+ }
480
+ },
481
+ });
482
+
483
+ if (hasJSX) return classification;
484
+
485
+ if (path.isIdentifier()) {
486
+ return analyzeIdentifierRenderExpression(path, context);
487
+ }
488
+
489
+ if (path.isMemberExpression() && isPropsChildrenMemberExpression(path.node)) {
490
+ return 'safe';
491
+ }
492
+
493
+ if (
494
+ path.isNullLiteral() ||
495
+ path.isBooleanLiteral() ||
496
+ path.isNumericLiteral() ||
497
+ path.isStringLiteral() ||
498
+ path.isBigIntLiteral()
499
+ ) {
500
+ return 'safe';
501
+ }
502
+
503
+ return 'unknown';
504
+ }
505
+
506
+ function analyzeJSXChildren(
507
+ children: Array<NodePath<t.JSXText | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXElement | t.JSXFragment>>,
508
+ context: AncestorAnalysisContext
509
+ ): AncestorClassification {
510
+ let classification: AncestorClassification = 'safe';
511
+
512
+ for (const childPath of children) {
513
+ if (childPath.isJSXElement()) {
514
+ const childClassification = classifyJSXElementAsAncestor(childPath, context);
515
+ classification = mergeAncestorClassification(classification, childClassification);
516
+ } else if (childPath.isJSXFragment()) {
517
+ const fragmentClassification = analyzeJSXChildren(childPath.get('children'), context);
518
+ classification = mergeAncestorClassification(classification, fragmentClassification);
519
+ } else if (childPath.isJSXExpressionContainer()) {
520
+ const expressionPath = childPath.get('expression');
521
+ if (!expressionPath.node || expressionPath.isJSXEmptyExpression()) continue;
522
+
523
+ const expressionClassification = analyzeRenderExpression(expressionPath as NodePath<t.Node>, context);
524
+ classification = mergeAncestorClassification(classification, expressionClassification);
525
+ } else if (childPath.isJSXSpreadChild()) {
526
+ classification = mergeAncestorClassification(classification, 'unknown');
527
+ }
528
+
529
+ if (classification === 'text') {
530
+ return classification;
531
+ }
532
+ }
533
+
534
+ return classification;
535
+ }
536
+
537
+ function analyzeIdentifierRenderExpression(
538
+ path: NodePath<t.Identifier>,
539
+ context: AncestorAnalysisContext
540
+ ): AncestorClassification {
541
+ if (path.node.name === 'children') return 'safe';
542
+
543
+ const binding = path.scope.getBinding(path.node.name);
544
+ if (!binding) return 'unknown';
545
+
546
+ if (binding.kind === 'param') {
547
+ return binding.identifier.name === 'children' ? 'safe' : 'unknown';
548
+ }
549
+
550
+ if (!binding.path.isVariableDeclarator()) return 'unknown';
551
+
552
+ const cacheKey = binding.path.node;
553
+ if (context.renderExpressionInProgress.has(cacheKey)) {
554
+ return 'unknown';
555
+ }
556
+
557
+ const initPath = binding.path.get('init');
558
+ if (!initPath.node) return 'unknown';
559
+
560
+ context.renderExpressionInProgress.add(cacheKey);
561
+ const classification = analyzeRenderExpression(initPath as NodePath<t.Node>, context);
562
+ context.renderExpressionInProgress.delete(cacheKey);
563
+
564
+ return classification;
565
+ }
566
+
567
+ function isPropsChildrenMemberExpression(expression: t.MemberExpression): boolean {
568
+ if (!t.isIdentifier(expression.object, { name: 'props' })) return false;
569
+ if (!t.isIdentifier(expression.property, { name: 'children' })) return false;
570
+ return !expression.computed;
571
+ }
572
+
573
+ function mergeAncestorClassification(
574
+ current: AncestorClassification,
575
+ next: AncestorClassification
576
+ ): AncestorClassification {
577
+ if (current === 'text' || next === 'text') return 'text';
578
+ if (current === 'unknown' || next === 'unknown') return 'unknown';
579
+ return 'safe';
580
+ }
581
+
582
+ function getImportSpecifierImportedName(specifier: t.ImportSpecifier): string | undefined {
583
+ if (t.isIdentifier(specifier.imported)) {
584
+ return specifier.imported.name;
585
+ }
586
+
587
+ if (t.isStringLiteral(specifier.imported)) {
588
+ return specifier.imported.value;
589
+ }
590
+
591
+ return undefined;
592
+ }
593
+
594
+ /**
595
+ * Checks whether the closest JSX element ancestor is expo-router Link with a truthy asChild prop.
596
+ *
597
+ * We only bail on Text optimization when Link is effectively slotting that Text as the clickable child.
598
+ */
599
+ export const hasExpoRouterLinkParentWithAsChild = (path: NodePath<t.JSXOpeningElement>): boolean => {
600
+ const textElementPath = path.parentPath;
601
+ if (!textElementPath.isJSXElement()) return false;
602
+
603
+ let ancestorPath: NodePath<t.Node> | null = textElementPath.parentPath;
604
+
605
+ while (ancestorPath) {
606
+ if (ancestorPath.isJSXElement()) {
607
+ if (!isExpoRouterLinkElement(ancestorPath)) return false;
608
+
609
+ return hasTruthyAsChildAttribute(ancestorPath.node.openingElement.attributes);
610
+ }
611
+
612
+ ancestorPath = ancestorPath.parentPath;
613
+ }
614
+
615
+ return false;
616
+ };
617
+
618
+ function isExpoRouterLinkElement(path: NodePath<t.JSXElement>): boolean {
619
+ const openingElementName = path.node.openingElement.name;
620
+
621
+ if (t.isJSXIdentifier(openingElementName)) {
622
+ const binding = path.scope.getBinding(openingElementName.name);
623
+ if (!binding || binding.kind !== 'module') return false;
624
+ if (!t.isImportSpecifier(binding.path.node)) return false;
625
+
626
+ const importDeclaration = binding.path.parent;
627
+ if (!t.isImportDeclaration(importDeclaration) || importDeclaration.source.value !== 'expo-router') return false;
628
+
629
+ const imported = binding.path.node.imported;
630
+ return t.isIdentifier(imported, { name: 'Link' }) || (t.isStringLiteral(imported) && imported.value === 'Link');
631
+ }
632
+
633
+ if (t.isJSXMemberExpression(openingElementName)) {
634
+ if (!t.isJSXIdentifier(openingElementName.object)) return false;
635
+ if (!t.isJSXIdentifier(openingElementName.property, { name: 'Link' })) return false;
636
+
637
+ const namespaceBinding = path.scope.getBinding(openingElementName.object.name);
638
+ if (!namespaceBinding || namespaceBinding.kind !== 'module') return false;
639
+ if (!t.isImportNamespaceSpecifier(namespaceBinding.path.node)) return false;
640
+
641
+ const importDeclaration = namespaceBinding.path.parent;
642
+ return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'expo-router';
643
+ }
644
+
645
+ return false;
646
+ }
647
+
648
+ function hasTruthyAsChildAttribute(attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): boolean {
649
+ let asChildAttribute: t.JSXAttribute | undefined;
650
+
651
+ for (const attribute of attributes) {
652
+ if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'asChild' })) {
653
+ asChildAttribute = attribute;
654
+ }
655
+ }
656
+
657
+ if (!asChildAttribute) return false;
658
+
659
+ return isJSXAttributeValueTruthy(asChildAttribute.value);
660
+ }
661
+
662
+ function isJSXAttributeValueTruthy(value: t.JSXAttribute['value']): boolean {
663
+ if (!value) return true;
664
+ if (t.isStringLiteral(value)) return value.value.length > 0;
665
+ if (t.isJSXElement(value) || t.isJSXFragment(value)) return true;
666
+
667
+ if (t.isJSXExpressionContainer(value)) {
668
+ const staticTruthiness = getStaticExpressionTruthiness(value.expression);
669
+ return staticTruthiness ?? true;
670
+ }
671
+
672
+ return true;
673
+ }
674
+
675
+ function getStaticExpressionTruthiness(expression: t.Expression | t.JSXEmptyExpression): boolean | undefined {
676
+ if (t.isJSXEmptyExpression(expression)) return false;
677
+ if (t.isBooleanLiteral(expression)) return expression.value;
678
+ if (t.isNullLiteral(expression)) return false;
679
+ if (t.isStringLiteral(expression)) return expression.value.length > 0;
680
+ if (t.isNumericLiteral(expression)) return expression.value !== 0 && !Number.isNaN(expression.value);
681
+ if (t.isBigIntLiteral(expression)) return expression.value !== '0';
682
+ if (t.isIdentifier(expression, { name: 'undefined' })) return false;
683
+
684
+ if (t.isTemplateLiteral(expression) && expression.expressions.length === 0) {
685
+ return (expression.quasis[0]?.value.cooked ?? '').length > 0;
686
+ }
687
+
688
+ if (t.isUnaryExpression(expression, { operator: '!' })) {
689
+ const staticTruthiness = getStaticExpressionTruthiness(expression.argument);
690
+ return staticTruthiness === undefined ? undefined : !staticTruthiness;
691
+ }
692
+
693
+ return undefined;
694
+ }
@@ -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,13 @@
1
1
  import { declare } from '@babel/helper-plugin-utils';
2
- import { Optimizer } from '../types';
2
+ import { Optimizer, PluginOptions } from '../types';
3
+ import { createLogger } from './logger';
4
+
5
+ export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions = {}) => {
6
+ const logger = createLogger({
7
+ verbose: false,
8
+ silent: true,
9
+ });
3
10
 
4
- export const generateTestPlugin = (optimizer: Optimizer) => {
5
11
  return declare((api) => {
6
12
  api.assertVersion(7);
7
13
 
@@ -9,7 +15,7 @@ export const generateTestPlugin = (optimizer: Optimizer) => {
9
15
  name: 'react-native-boost',
10
16
  visitor: {
11
17
  JSXOpeningElement(path) {
12
- optimizer(path);
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
+ };