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.
- package/dist/plugin/esm/index.mjs +528 -168
- package/dist/plugin/esm/index.mjs.map +1 -1
- package/dist/plugin/index.js +528 -168
- package/dist/plugin/index.js.map +1 -1
- package/dist/runtime/esm/index.mjs.map +1 -1
- package/dist/runtime/esm/index.web.mjs.map +1 -1
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/index.web.d.ts +2 -1
- package/dist/runtime/index.web.js.map +1 -1
- package/package.json +12 -13
- package/src/plugin/index.ts +1 -1
- package/src/plugin/optimizers/text/index.ts +71 -72
- package/src/plugin/optimizers/view/index.ts +5 -25
- package/src/plugin/types/index.ts +12 -1
- package/src/plugin/utils/common/attributes.ts +165 -0
- package/src/plugin/utils/common/index.ts +1 -3
- package/src/plugin/utils/common/validation.ts +515 -0
- package/src/plugin/utils/constants.ts +9 -0
- package/src/plugin/utils/format-test-result.ts +29 -0
- package/src/plugin/utils/generate-test-plugin.ts +3 -3
- package/src/plugin/utils/common/ancestors.ts +0 -120
- package/src/plugin/utils/common/node-types.ts +0 -22
|
@@ -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
|
-
}
|