react-native-boost 0.6.1 → 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.
@@ -1,8 +1,8 @@
1
1
  import { declare } from '@babel/helper-plugin-utils';
2
2
  import { types } from '@babel/core';
3
- import { addDefault, addNamed } from '@babel/helper-module-imports';
4
3
  import { minimatch } from 'minimatch';
5
4
  import nodePath from 'node:path';
5
+ import { addDefault, addNamed } from '@babel/helper-module-imports';
6
6
 
7
7
  class PluginError extends Error {
8
8
  constructor(message) {
@@ -11,52 +11,6 @@ class PluginError extends Error {
11
11
  }
12
12
  }
13
13
 
14
- const RUNTIME_MODULE_NAME = "react-native-boost/runtime";
15
- const ACCESSIBILITY_PROPERTIES = /* @__PURE__ */ new Set([
16
- "accessibilityLabel",
17
- "aria-label",
18
- "accessibilityState",
19
- "aria-busy",
20
- "aria-checked",
21
- "aria-disabled",
22
- "aria-expanded",
23
- "aria-selected",
24
- "accessible"
25
- ]);
26
-
27
- function addFileImportHint({
28
- file,
29
- nameHint,
30
- path,
31
- importName,
32
- moduleName,
33
- importType = "named"
34
- }) {
35
- var _a;
36
- if (!((_a = file.__hasImports) == null ? void 0 : _a[nameHint])) {
37
- file.__hasImports = file.__hasImports || {};
38
- file.__hasImports[nameHint] = importType === "default" ? addDefault(path, moduleName, { nameHint }) : addNamed(path, importName, moduleName, { nameHint });
39
- }
40
- return file.__hasImports[nameHint];
41
- }
42
- const replaceWithNativeComponent = (path, parent, file, nativeComponentName) => {
43
- const nativeIdentifier = addFileImportHint({
44
- file,
45
- nameHint: nativeComponentName,
46
- path,
47
- importName: nativeComponentName,
48
- moduleName: RUNTIME_MODULE_NAME,
49
- importType: "named"
50
- });
51
- const currentName = path.node.name.name;
52
- const jsxName = path.node.name;
53
- jsxName.name = nativeIdentifier.name;
54
- if (!path.node.selfClosing && parent.closingElement && types.isJSXIdentifier(parent.closingElement.name) && parent.closingElement.name.name === currentName) {
55
- parent.closingElement.name.name = nativeIdentifier.name;
56
- }
57
- return nativeIdentifier;
58
- };
59
-
60
14
  const ensureArray = (value) => {
61
15
  if (Array.isArray(value)) return value;
62
16
  return [value];
@@ -157,6 +111,372 @@ const isReactNativeImport = (path, expectedImportedName) => {
157
111
  }
158
112
  return false;
159
113
  };
114
+ const hasUnsafeViewAncestor = (path, allowUnknownAncestors = false) => {
115
+ const classification = classifyViewAncestors(path);
116
+ if (classification === "text") return true;
117
+ if (classification === "unknown" && !allowUnknownAncestors) return true;
118
+ return false;
119
+ };
120
+ function classifyViewAncestors(path) {
121
+ const context = {
122
+ componentCache: /* @__PURE__ */ new WeakMap(),
123
+ componentInProgress: /* @__PURE__ */ new WeakSet(),
124
+ renderExpressionInProgress: /* @__PURE__ */ new WeakSet()
125
+ };
126
+ let classification = "safe";
127
+ let ancestorPath = path.parentPath.parentPath;
128
+ while (ancestorPath) {
129
+ if (ancestorPath.isJSXElement()) {
130
+ const ancestorClassification = classifyJSXElementAsAncestor(ancestorPath, context);
131
+ classification = mergeAncestorClassification(classification, ancestorClassification);
132
+ if (classification === "text") return classification;
133
+ }
134
+ ancestorPath = ancestorPath.parentPath;
135
+ }
136
+ return classification;
137
+ }
138
+ function classifyJSXElementAsAncestor(path, context) {
139
+ const openingElementName = path.node.openingElement.name;
140
+ if (types.isJSXIdentifier(openingElementName)) {
141
+ return classifyJSXIdentifierAsAncestor(path, openingElementName.name, context);
142
+ }
143
+ if (types.isJSXMemberExpression(openingElementName)) {
144
+ return classifyJSXMemberExpressionAsAncestor(path, openingElementName);
145
+ }
146
+ return "unknown";
147
+ }
148
+ function classifyJSXIdentifierAsAncestor(path, identifierName, context) {
149
+ if (identifierName === "Fragment") return "safe";
150
+ const binding = path.scope.getBinding(identifierName);
151
+ if (!binding) return "unknown";
152
+ return classifyBindingAsAncestor(binding, context);
153
+ }
154
+ function classifyJSXMemberExpressionAsAncestor(path, expression) {
155
+ if (!types.isJSXIdentifier(expression.object) || !types.isJSXIdentifier(expression.property)) {
156
+ return "unknown";
157
+ }
158
+ const binding = path.scope.getBinding(expression.object.name);
159
+ if (!binding || binding.kind !== "module" || !types.isImportNamespaceSpecifier(binding.path.node)) {
160
+ return "unknown";
161
+ }
162
+ const importDeclaration = binding.path.parent;
163
+ if (!types.isImportDeclaration(importDeclaration)) return "unknown";
164
+ if (importDeclaration.source.value === "react-native") {
165
+ return expression.property.name === "Text" ? "text" : "safe";
166
+ }
167
+ if (importDeclaration.source.value === "react" && expression.property.name === "Fragment") {
168
+ return "safe";
169
+ }
170
+ return "unknown";
171
+ }
172
+ function classifyBindingAsAncestor(binding, context) {
173
+ if (binding.kind === "module") {
174
+ return classifyModuleBindingAsAncestor(binding);
175
+ }
176
+ return classifyLocalBindingAsAncestor(binding, context);
177
+ }
178
+ function classifyModuleBindingAsAncestor(binding) {
179
+ const importDeclaration = binding.path.parent;
180
+ if (!types.isImportDeclaration(importDeclaration)) return "unknown";
181
+ const source = importDeclaration.source.value;
182
+ if (source === "react-native") {
183
+ if (types.isImportSpecifier(binding.path.node)) {
184
+ const importedName = getImportSpecifierImportedName(binding.path.node);
185
+ if (!importedName) return "unknown";
186
+ return importedName === "Text" ? "text" : "safe";
187
+ }
188
+ if (types.isImportNamespaceSpecifier(binding.path.node)) {
189
+ return "safe";
190
+ }
191
+ return "unknown";
192
+ }
193
+ if (source === "react" && types.isImportSpecifier(binding.path.node)) {
194
+ const importedName = getImportSpecifierImportedName(binding.path.node);
195
+ if (importedName === "Fragment") return "safe";
196
+ }
197
+ return "unknown";
198
+ }
199
+ function classifyLocalBindingAsAncestor(binding, context) {
200
+ const cacheKey = binding.path.node;
201
+ const cached = context.componentCache.get(cacheKey);
202
+ if (cached) return cached;
203
+ if (context.componentInProgress.has(cacheKey)) {
204
+ return "unknown";
205
+ }
206
+ context.componentInProgress.add(cacheKey);
207
+ let classification;
208
+ if (binding.path.isFunctionDeclaration()) {
209
+ classification = analyzeFunctionComponent(binding.path, context);
210
+ } else if (binding.path.isVariableDeclarator()) {
211
+ classification = analyzeVariableDeclaratorComponent(binding.path, context);
212
+ } else {
213
+ classification = "unknown";
214
+ }
215
+ context.componentInProgress.delete(cacheKey);
216
+ context.componentCache.set(cacheKey, classification);
217
+ return classification;
218
+ }
219
+ function analyzeVariableDeclaratorComponent(path, context) {
220
+ const initPath = path.get("init");
221
+ if (!initPath.node) return "unknown";
222
+ if (initPath.isArrowFunctionExpression() || initPath.isFunctionExpression()) {
223
+ return analyzeFunctionComponent(initPath, context);
224
+ }
225
+ if (initPath.isCallExpression()) {
226
+ return analyzeCallWrappedComponent(initPath, context);
227
+ }
228
+ if (initPath.isIdentifier()) {
229
+ const aliasBinding = path.scope.getBinding(initPath.node.name);
230
+ if (!aliasBinding) return "unknown";
231
+ return classifyBindingAsAncestor(aliasBinding, context);
232
+ }
233
+ return "unknown";
234
+ }
235
+ function analyzeCallWrappedComponent(path, context) {
236
+ if (!isReactMemoOrForwardRefCall(path)) return "unknown";
237
+ const [firstArgumentPath] = path.get("arguments");
238
+ if (!(firstArgumentPath == null ? void 0 : firstArgumentPath.node)) return "unknown";
239
+ if (firstArgumentPath.isArrowFunctionExpression() || firstArgumentPath.isFunctionExpression()) {
240
+ return analyzeFunctionComponent(firstArgumentPath, context);
241
+ }
242
+ if (firstArgumentPath.isIdentifier()) {
243
+ const wrappedComponentBinding = path.scope.getBinding(firstArgumentPath.node.name);
244
+ if (!wrappedComponentBinding) return "unknown";
245
+ return classifyBindingAsAncestor(wrappedComponentBinding, context);
246
+ }
247
+ if (firstArgumentPath.isCallExpression()) {
248
+ return analyzeCallWrappedComponent(firstArgumentPath, context);
249
+ }
250
+ return "unknown";
251
+ }
252
+ function isReactMemoOrForwardRefCall(path) {
253
+ const calleePath = path.get("callee");
254
+ if (calleePath.isIdentifier()) {
255
+ if (!isMemoOrForwardRefName(calleePath.node.name)) return false;
256
+ const binding = path.scope.getBinding(calleePath.node.name);
257
+ return isReactImportBinding(binding);
258
+ }
259
+ if (calleePath.isMemberExpression()) {
260
+ const objectPath = calleePath.get("object");
261
+ const propertyPath = calleePath.get("property");
262
+ if (!objectPath.isIdentifier() || !propertyPath.isIdentifier()) return false;
263
+ if (!isMemoOrForwardRefName(propertyPath.node.name)) return false;
264
+ const objectBinding = path.scope.getBinding(objectPath.node.name);
265
+ return isReactImportBinding(objectBinding);
266
+ }
267
+ return false;
268
+ }
269
+ function isMemoOrForwardRefName(name) {
270
+ return name === "memo" || name === "forwardRef";
271
+ }
272
+ function isReactImportBinding(binding) {
273
+ if (!binding || binding.kind !== "module") return false;
274
+ const importDeclaration = binding.path.parent;
275
+ return types.isImportDeclaration(importDeclaration) && importDeclaration.source.value === "react";
276
+ }
277
+ function analyzeFunctionComponent(path, context) {
278
+ const bodyPath = path.get("body");
279
+ if (!bodyPath.isBlockStatement()) {
280
+ return analyzeRenderExpression(bodyPath, context);
281
+ }
282
+ let classification = "safe";
283
+ for (const statementPath of bodyPath.get("body")) {
284
+ if (!statementPath.isReturnStatement()) continue;
285
+ const argumentPath = statementPath.get("argument");
286
+ if (!argumentPath.node) continue;
287
+ const returnClassification = analyzeRenderExpression(argumentPath, context);
288
+ classification = mergeAncestorClassification(classification, returnClassification);
289
+ if (classification === "text") return classification;
290
+ }
291
+ return classification;
292
+ }
293
+ function analyzeRenderExpression(path, context) {
294
+ if (path.isJSXFragment()) {
295
+ return analyzeJSXChildren(path.get("children"), context);
296
+ }
297
+ let classification = "safe";
298
+ let hasJSX = false;
299
+ path.traverse({
300
+ JSXOpeningElement(jsxPath) {
301
+ hasJSX = true;
302
+ const jsxElementPath = jsxPath.parentPath;
303
+ if (!jsxElementPath.isJSXElement()) {
304
+ classification = mergeAncestorClassification(classification, "unknown");
305
+ return;
306
+ }
307
+ const jsxClassification = classifyJSXElementAsAncestor(jsxElementPath, context);
308
+ classification = mergeAncestorClassification(classification, jsxClassification);
309
+ if (classification === "text") {
310
+ jsxPath.stop();
311
+ }
312
+ }
313
+ });
314
+ if (hasJSX) return classification;
315
+ if (path.isIdentifier()) {
316
+ return analyzeIdentifierRenderExpression(path, context);
317
+ }
318
+ if (path.isMemberExpression() && isPropsChildrenMemberExpression(path.node)) {
319
+ return "safe";
320
+ }
321
+ if (path.isNullLiteral() || path.isBooleanLiteral() || path.isNumericLiteral() || path.isStringLiteral() || path.isBigIntLiteral()) {
322
+ return "safe";
323
+ }
324
+ return "unknown";
325
+ }
326
+ function analyzeJSXChildren(children, context) {
327
+ let classification = "safe";
328
+ for (const childPath of children) {
329
+ if (childPath.isJSXElement()) {
330
+ const childClassification = classifyJSXElementAsAncestor(childPath, context);
331
+ classification = mergeAncestorClassification(classification, childClassification);
332
+ } else if (childPath.isJSXFragment()) {
333
+ const fragmentClassification = analyzeJSXChildren(childPath.get("children"), context);
334
+ classification = mergeAncestorClassification(classification, fragmentClassification);
335
+ } else if (childPath.isJSXExpressionContainer()) {
336
+ const expressionPath = childPath.get("expression");
337
+ if (!expressionPath.node || expressionPath.isJSXEmptyExpression()) continue;
338
+ const expressionClassification = analyzeRenderExpression(expressionPath, context);
339
+ classification = mergeAncestorClassification(classification, expressionClassification);
340
+ } else if (childPath.isJSXSpreadChild()) {
341
+ classification = mergeAncestorClassification(classification, "unknown");
342
+ }
343
+ if (classification === "text") {
344
+ return classification;
345
+ }
346
+ }
347
+ return classification;
348
+ }
349
+ function analyzeIdentifierRenderExpression(path, context) {
350
+ if (path.node.name === "children") return "safe";
351
+ const binding = path.scope.getBinding(path.node.name);
352
+ if (!binding) return "unknown";
353
+ if (binding.kind === "param") {
354
+ return binding.identifier.name === "children" ? "safe" : "unknown";
355
+ }
356
+ if (!binding.path.isVariableDeclarator()) return "unknown";
357
+ const cacheKey = binding.path.node;
358
+ if (context.renderExpressionInProgress.has(cacheKey)) {
359
+ return "unknown";
360
+ }
361
+ const initPath = binding.path.get("init");
362
+ if (!initPath.node) return "unknown";
363
+ context.renderExpressionInProgress.add(cacheKey);
364
+ const classification = analyzeRenderExpression(initPath, context);
365
+ context.renderExpressionInProgress.delete(cacheKey);
366
+ return classification;
367
+ }
368
+ function isPropsChildrenMemberExpression(expression) {
369
+ if (!types.isIdentifier(expression.object, { name: "props" })) return false;
370
+ if (!types.isIdentifier(expression.property, { name: "children" })) return false;
371
+ return !expression.computed;
372
+ }
373
+ function mergeAncestorClassification(current, next) {
374
+ if (current === "text" || next === "text") return "text";
375
+ if (current === "unknown" || next === "unknown") return "unknown";
376
+ return "safe";
377
+ }
378
+ function getImportSpecifierImportedName(specifier) {
379
+ if (types.isIdentifier(specifier.imported)) {
380
+ return specifier.imported.name;
381
+ }
382
+ if (types.isStringLiteral(specifier.imported)) {
383
+ return specifier.imported.value;
384
+ }
385
+ return void 0;
386
+ }
387
+ const hasExpoRouterLinkParentWithAsChild = (path) => {
388
+ const textElementPath = path.parentPath;
389
+ if (!textElementPath.isJSXElement()) return false;
390
+ let ancestorPath = textElementPath.parentPath;
391
+ while (ancestorPath) {
392
+ if (ancestorPath.isJSXElement()) {
393
+ if (!isExpoRouterLinkElement(ancestorPath)) return false;
394
+ return hasTruthyAsChildAttribute(ancestorPath.node.openingElement.attributes);
395
+ }
396
+ ancestorPath = ancestorPath.parentPath;
397
+ }
398
+ return false;
399
+ };
400
+ function isExpoRouterLinkElement(path) {
401
+ const openingElementName = path.node.openingElement.name;
402
+ if (types.isJSXIdentifier(openingElementName)) {
403
+ const binding = path.scope.getBinding(openingElementName.name);
404
+ if (!binding || binding.kind !== "module") return false;
405
+ if (!types.isImportSpecifier(binding.path.node)) return false;
406
+ const importDeclaration = binding.path.parent;
407
+ if (!types.isImportDeclaration(importDeclaration) || importDeclaration.source.value !== "expo-router") return false;
408
+ const imported = binding.path.node.imported;
409
+ return types.isIdentifier(imported, { name: "Link" }) || types.isStringLiteral(imported) && imported.value === "Link";
410
+ }
411
+ if (types.isJSXMemberExpression(openingElementName)) {
412
+ if (!types.isJSXIdentifier(openingElementName.object)) return false;
413
+ if (!types.isJSXIdentifier(openingElementName.property, { name: "Link" })) return false;
414
+ const namespaceBinding = path.scope.getBinding(openingElementName.object.name);
415
+ if (!namespaceBinding || namespaceBinding.kind !== "module") return false;
416
+ if (!types.isImportNamespaceSpecifier(namespaceBinding.path.node)) return false;
417
+ const importDeclaration = namespaceBinding.path.parent;
418
+ return types.isImportDeclaration(importDeclaration) && importDeclaration.source.value === "expo-router";
419
+ }
420
+ return false;
421
+ }
422
+ function hasTruthyAsChildAttribute(attributes) {
423
+ let asChildAttribute;
424
+ for (const attribute of attributes) {
425
+ if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "asChild" })) {
426
+ asChildAttribute = attribute;
427
+ }
428
+ }
429
+ if (!asChildAttribute) return false;
430
+ return isJSXAttributeValueTruthy(asChildAttribute.value);
431
+ }
432
+ function isJSXAttributeValueTruthy(value) {
433
+ if (!value) return true;
434
+ if (types.isStringLiteral(value)) return value.value.length > 0;
435
+ if (types.isJSXElement(value) || types.isJSXFragment(value)) return true;
436
+ if (types.isJSXExpressionContainer(value)) {
437
+ const staticTruthiness = getStaticExpressionTruthiness(value.expression);
438
+ return staticTruthiness != null ? staticTruthiness : true;
439
+ }
440
+ return true;
441
+ }
442
+ function getStaticExpressionTruthiness(expression) {
443
+ var _a, _b;
444
+ if (types.isJSXEmptyExpression(expression)) return false;
445
+ if (types.isBooleanLiteral(expression)) return expression.value;
446
+ if (types.isNullLiteral(expression)) return false;
447
+ if (types.isStringLiteral(expression)) return expression.value.length > 0;
448
+ if (types.isNumericLiteral(expression)) return expression.value !== 0 && !Number.isNaN(expression.value);
449
+ if (types.isBigIntLiteral(expression)) return expression.value !== "0";
450
+ if (types.isIdentifier(expression, { name: "undefined" })) return false;
451
+ if (types.isTemplateLiteral(expression) && expression.expressions.length === 0) {
452
+ return ((_b = (_a = expression.quasis[0]) == null ? void 0 : _a.value.cooked) != null ? _b : "").length > 0;
453
+ }
454
+ if (types.isUnaryExpression(expression, { operator: "!" })) {
455
+ const staticTruthiness = getStaticExpressionTruthiness(expression.argument);
456
+ return staticTruthiness === void 0 ? void 0 : !staticTruthiness;
457
+ }
458
+ return void 0;
459
+ }
460
+
461
+ const RUNTIME_MODULE_NAME = "react-native-boost/runtime";
462
+ const ACCESSIBILITY_PROPERTIES = /* @__PURE__ */ new Set([
463
+ "accessibilityLabel",
464
+ "aria-label",
465
+ "accessibilityState",
466
+ "aria-busy",
467
+ "aria-checked",
468
+ "aria-disabled",
469
+ "aria-expanded",
470
+ "aria-selected",
471
+ "accessible"
472
+ ]);
473
+ const USER_SELECT_STYLE_TO_SELECTABLE_PROP = {
474
+ auto: true,
475
+ text: true,
476
+ none: false,
477
+ contain: true,
478
+ all: true
479
+ };
160
480
 
161
481
  const hasBlacklistedProperty = (path, blacklist) => {
162
482
  return path.node.attributes.some((attribute) => {
@@ -188,6 +508,47 @@ const hasBlacklistedProperty = (path, blacklist) => {
188
508
  return false;
189
509
  });
190
510
  };
511
+ const addDefaultProperty = (path, key, value) => {
512
+ let propertyIsFound = false;
513
+ let hasUnresolvableSpread = false;
514
+ for (const attribute of path.node.attributes) {
515
+ if (types.isJSXAttribute(attribute) && attribute.name.name === key) {
516
+ propertyIsFound = true;
517
+ break;
518
+ }
519
+ if (types.isJSXSpreadAttribute(attribute)) {
520
+ if (types.isObjectExpression(attribute.argument)) {
521
+ const propertyInSpread = attribute.argument.properties.some(
522
+ (p) => types.isObjectProperty(p) && types.isIdentifier(p.key) && p.key.name === key || types.isObjectProperty(p) && types.isStringLiteral(p.key) && p.key.value === key
523
+ );
524
+ if (propertyInSpread) {
525
+ propertyIsFound = true;
526
+ break;
527
+ }
528
+ } else if (types.isIdentifier(attribute.argument)) {
529
+ const binding = path.scope.getBinding(attribute.argument.name);
530
+ if ((binding == null ? void 0 : binding.path.node) && types.isVariableDeclarator(binding.path.node) && types.isObjectExpression(binding.path.node.init)) {
531
+ const propertyInSpread = binding.path.node.init.properties.some(
532
+ (p) => types.isObjectProperty(p) && types.isIdentifier(p.key) && p.key.name === key || types.isObjectProperty(p) && types.isStringLiteral(p.key) && p.key.value === key
533
+ );
534
+ if (propertyInSpread) {
535
+ propertyIsFound = true;
536
+ break;
537
+ }
538
+ } else {
539
+ hasUnresolvableSpread = true;
540
+ break;
541
+ }
542
+ } else {
543
+ hasUnresolvableSpread = true;
544
+ break;
545
+ }
546
+ }
547
+ }
548
+ if (!propertyIsFound && !hasUnresolvableSpread) {
549
+ path.node.attributes.push(types.jsxAttribute(types.jsxIdentifier(key), types.jsxExpressionContainer(value)));
550
+ }
551
+ };
191
552
  const buildPropertiesFromAttributes = (attributes) => {
192
553
  const arguments_ = [];
193
554
  for (const attribute of attributes) {
@@ -253,7 +614,54 @@ const hasAccessibilityProperty = (path, attributes) => {
253
614
  }
254
615
  return false;
255
616
  };
256
-
617
+ function extractStyleAttribute(attributes) {
618
+ for (const attribute of attributes) {
619
+ if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "style" })) {
620
+ if (attribute.value && types.isJSXExpressionContainer(attribute.value) && !types.isJSXEmptyExpression(attribute.value.expression)) {
621
+ return {
622
+ styleAttribute: attribute,
623
+ styleExpr: attribute.value.expression
624
+ };
625
+ }
626
+ return { styleAttribute: attribute };
627
+ }
628
+ }
629
+ return {};
630
+ }
631
+ function extractSelectableAndUpdateStyle(styleExpr) {
632
+ const handleObjectExpression = (objectExpr) => {
633
+ let selectableValue;
634
+ objectExpr.properties = objectExpr.properties.filter((property) => {
635
+ if (!types.isObjectProperty(property) || !types.isIdentifier(property.key, { name: "userSelect" }) && !(types.isStringLiteral(property.key) && property.key.value === "userSelect")) {
636
+ return true;
637
+ }
638
+ if (types.isStringLiteral(property.value)) {
639
+ const mapped = USER_SELECT_STYLE_TO_SELECTABLE_PROP[property.value.value];
640
+ if (mapped !== void 0) {
641
+ selectableValue = mapped;
642
+ }
643
+ }
644
+ return false;
645
+ });
646
+ return selectableValue;
647
+ };
648
+ if (types.isObjectExpression(styleExpr)) {
649
+ return handleObjectExpression(styleExpr);
650
+ }
651
+ if (types.isArrayExpression(styleExpr)) {
652
+ let selectableValue;
653
+ for (const element of styleExpr.elements) {
654
+ if (element && types.isObjectExpression(element)) {
655
+ const value = handleObjectExpression(element);
656
+ if (value !== void 0) {
657
+ selectableValue = value;
658
+ }
659
+ }
660
+ }
661
+ return selectableValue;
662
+ }
663
+ return void 0;
664
+ }
257
665
  const isStringNode = (path, child) => {
258
666
  if (types.isJSXText(child) || types.isStringLiteral(child)) return true;
259
667
  if (types.isJSXExpressionContainer(child)) {
@@ -270,63 +678,40 @@ const isStringNode = (path, child) => {
270
678
  return false;
271
679
  };
272
680
 
273
- function hasComponentAncestor(path, componentName, skipComponents = ["Fragment"]) {
274
- const directAncestor = path.findParent((parentPath) => {
275
- return types.isJSXElement(parentPath.node) && types.isJSXIdentifier(parentPath.node.openingElement.name, { name: componentName });
276
- });
277
- if (directAncestor) return true;
278
- return !!path.findParent((parentPath) => {
279
- if (!types.isJSXElement(parentPath.node)) return false;
280
- const openingElement = parentPath.node.openingElement;
281
- if (!types.isJSXIdentifier(openingElement.name)) return false;
282
- const ancestorComponentName = openingElement.name.name;
283
- if (ancestorComponentName === componentName) {
284
- return false;
285
- }
286
- if (skipComponents.includes(ancestorComponentName)) {
287
- return false;
288
- }
289
- if (ancestorComponentName[0] === ancestorComponentName[0].toLowerCase()) {
290
- return false;
291
- }
292
- const binding = parentPath.scope.getBinding(ancestorComponentName);
293
- if (!binding) return false;
294
- if (types.isVariableDeclarator(binding.path.node)) {
295
- const init = binding.path.node.init;
296
- if (types.isArrowFunctionExpression(init) || types.isFunctionExpression(init)) {
297
- return types.isBlockStatement(init.body) ? hasComponentInReturnStatement(init.body, componentName) : hasComponentInExpression(init.body, componentName);
298
- }
299
- } else if (types.isFunctionDeclaration(binding.path.node)) {
300
- return hasComponentInReturnStatement(binding.path.node.body, componentName);
301
- }
302
- return false;
303
- });
304
- }
305
- function hasComponentInReturnStatement(blockStatement, componentName) {
306
- for (const statement of blockStatement.body) {
307
- if (types.isReturnStatement(statement) && statement.argument && hasComponentInExpression(statement.argument, componentName)) {
308
- return true;
309
- }
681
+ function addFileImportHint({
682
+ file,
683
+ nameHint,
684
+ path,
685
+ importName,
686
+ moduleName,
687
+ importType = "named"
688
+ }) {
689
+ var _a;
690
+ if (!((_a = file.__hasImports) == null ? void 0 : _a[nameHint])) {
691
+ file.__hasImports = file.__hasImports || {};
692
+ file.__hasImports[nameHint] = importType === "default" ? addDefault(path, moduleName, { nameHint }) : addNamed(path, importName, moduleName, { nameHint });
310
693
  }
311
- return false;
694
+ return file.__hasImports[nameHint];
312
695
  }
313
- function hasComponentInExpression(expression, componentName) {
314
- if (types.isJSXElement(expression)) {
315
- if (types.isJSXIdentifier(expression.openingElement.name, { name: componentName })) {
316
- return true;
317
- }
318
- for (const child of expression.children) {
319
- if (types.isJSXElement(child) && types.isJSXIdentifier(child.openingElement.name, { name: componentName })) {
320
- return true;
321
- }
322
- }
696
+ const replaceWithNativeComponent = (path, parent, file, nativeComponentName) => {
697
+ const nativeIdentifier = addFileImportHint({
698
+ file,
699
+ nameHint: nativeComponentName,
700
+ path,
701
+ importName: nativeComponentName,
702
+ moduleName: RUNTIME_MODULE_NAME,
703
+ importType: "named"
704
+ });
705
+ const currentName = path.node.name.name;
706
+ const jsxName = path.node.name;
707
+ jsxName.name = nativeIdentifier.name;
708
+ if (!path.node.selfClosing && parent.closingElement && types.isJSXIdentifier(parent.closingElement.name) && parent.closingElement.name.name === currentName) {
709
+ parent.closingElement.name.name = nativeIdentifier.name;
323
710
  }
324
- return false;
325
- }
711
+ return nativeIdentifier;
712
+ };
326
713
 
327
714
  const textBlacklistedProperties = /* @__PURE__ */ new Set([
328
- "allowFontScaling",
329
- "ellipsizeMode",
330
715
  "id",
331
716
  "nativeID",
332
717
  "onLongPress",
@@ -341,8 +726,8 @@ const textBlacklistedProperties = /* @__PURE__ */ new Set([
341
726
  "onStartShouldSetResponder",
342
727
  "pressRetentionOffset",
343
728
  "suppressHighlighting",
344
- "selectable",
345
729
  "selectionColor"
730
+ // TODO: we can use react-native's internal `processColor` to process this at runtime
346
731
  ]);
347
732
  const textOptimizer = (path, log = () => {
348
733
  }) => {
@@ -351,6 +736,7 @@ const textOptimizer = (path, log = () => {
351
736
  if (!isValidJSXComponent(path, "Text")) return;
352
737
  if (!isReactNativeImport(path, "Text")) return;
353
738
  if (hasBlacklistedProperty(path, textBlacklistedProperties)) return;
739
+ if (hasExpoRouterLinkParentWithAsChild(path)) return;
354
740
  const parent = path.parent;
355
741
  if (hasInvalidChildren(path, parent)) return;
356
742
  const hub = path.hub;
@@ -361,9 +747,10 @@ const textOptimizer = (path, log = () => {
361
747
  const filename = ((_a = file.opts) == null ? void 0 : _a.filename) || "unknown file";
362
748
  const lineNumber = (_c = (_b = path.node.loc) == null ? void 0 : _b.start.line) != null ? _c : "unknown line";
363
749
  log(`Optimizing Text component in ${filename}:${lineNumber}`);
364
- const originalAttributes = [...path.node.attributes];
365
750
  fixNegativeNumberOfLines({ path, log });
366
- processProps(path, file, originalAttributes);
751
+ addDefaultProperty(path, "allowFontScaling", types.booleanLiteral(true));
752
+ addDefaultProperty(path, "ellipsizeMode", types.stringLiteral("tail"));
753
+ processProps(path, file);
367
754
  replaceWithNativeComponent(path, parent, file, "NativeText");
368
755
  };
369
756
  function hasInvalidChildren(path, parent) {
@@ -397,27 +784,16 @@ function fixNegativeNumberOfLines({
397
784
  }
398
785
  }
399
786
  }
400
- function extractStyleAttribute(attributes) {
401
- for (const attribute of attributes) {
402
- if (types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "style" })) {
403
- if (attribute.value && types.isJSXExpressionContainer(attribute.value) && !types.isJSXEmptyExpression(attribute.value.expression)) {
404
- return {
405
- styleAttribute: attribute,
406
- styleExpr: attribute.value.expression
407
- };
408
- }
409
- return { styleAttribute: attribute };
410
- }
411
- }
412
- return {};
413
- }
414
- function processProps(path, file, originalAttributes) {
415
- const { styleExpr } = extractStyleAttribute(originalAttributes);
416
- const hasA11y = hasAccessibilityProperty(path, originalAttributes);
417
- if (styleExpr && hasA11y) {
418
- const accessibilityAttributes = originalAttributes.filter(
419
- (attribute) => !(types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name, { name: "style" }))
420
- );
787
+ function processProps(path, file) {
788
+ const currentAttributes = [...path.node.attributes];
789
+ const { styleExpr, styleAttribute } = extractStyleAttribute(currentAttributes);
790
+ const hasA11y = hasAccessibilityProperty(path, currentAttributes);
791
+ const spreadAttributes = [];
792
+ if (hasA11y) {
793
+ const accessibilityAttributes = currentAttributes.filter((attribute) => {
794
+ if (!types.isJSXAttribute(attribute)) return false;
795
+ return types.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name);
796
+ });
421
797
  const normalizeIdentifier = addFileImportHint({
422
798
  file,
423
799
  nameHint: "processAccessibilityProps",
@@ -427,6 +803,17 @@ function processProps(path, file, originalAttributes) {
427
803
  });
428
804
  const accessibilityObject = buildPropertiesFromAttributes(accessibilityAttributes);
429
805
  const accessibilityExpr = types.callExpression(types.identifier(normalizeIdentifier.name), [accessibilityObject]);
806
+ spreadAttributes.push(types.jsxSpreadAttribute(accessibilityExpr));
807
+ }
808
+ let selectableAttribute;
809
+ if (styleExpr) {
810
+ const selectableValue = extractSelectableAndUpdateStyle(styleExpr);
811
+ if (selectableValue != null) {
812
+ selectableAttribute = types.jsxAttribute(
813
+ types.jsxIdentifier("selectable"),
814
+ types.jsxExpressionContainer(types.booleanLiteral(selectableValue))
815
+ );
816
+ }
430
817
  const flattenIdentifier = addFileImportHint({
431
818
  file,
432
819
  nameHint: "processTextStyle",
@@ -435,29 +822,19 @@ function processProps(path, file, originalAttributes) {
435
822
  moduleName: RUNTIME_MODULE_NAME
436
823
  });
437
824
  const flattenedStyleExpr = types.callExpression(types.identifier(flattenIdentifier.name), [styleExpr]);
438
- path.node.attributes = [types.jsxSpreadAttribute(accessibilityExpr), types.jsxSpreadAttribute(flattenedStyleExpr)];
439
- } else if (styleExpr) {
440
- const flattenIdentifier = addFileImportHint({
441
- file,
442
- nameHint: "processTextStyle",
443
- path,
444
- importName: "processTextStyle",
445
- moduleName: RUNTIME_MODULE_NAME
446
- });
447
- const flattened = types.callExpression(types.identifier(flattenIdentifier.name), [styleExpr]);
448
- path.node.attributes = [types.jsxSpreadAttribute(flattened)];
449
- } else if (hasA11y) {
450
- const normalizeIdentifier = addFileImportHint({
451
- file,
452
- nameHint: "processAccessibilityProps",
453
- path,
454
- importName: "processAccessibilityProps",
455
- moduleName: RUNTIME_MODULE_NAME
456
- });
457
- const propsObject = buildPropertiesFromAttributes(originalAttributes);
458
- const normalized = types.callExpression(types.identifier(normalizeIdentifier.name), [propsObject]);
459
- path.node.attributes = [types.jsxSpreadAttribute(normalized)];
825
+ spreadAttributes.push(types.jsxSpreadAttribute(flattenedStyleExpr));
826
+ }
827
+ const remainingAttributes = [];
828
+ for (const attribute of currentAttributes) {
829
+ if (styleAttribute && attribute === styleAttribute) continue;
830
+ if (hasA11y && types.isJSXAttribute(attribute) && types.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name)) {
831
+ continue;
832
+ }
833
+ remainingAttributes.push(attribute);
460
834
  }
835
+ path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes].filter(
836
+ (attribute) => attribute !== void 0
837
+ );
461
838
  }
462
839
 
463
840
  const log = (message) => {
@@ -465,46 +842,29 @@ const log = (message) => {
465
842
  };
466
843
 
467
844
  const viewBlacklistedProperties = /* @__PURE__ */ new Set([
845
+ // TODO: process a11y props at runtime
468
846
  "accessible",
469
847
  "accessibilityLabel",
470
848
  "accessibilityState",
471
- "allowFontScaling",
472
849
  "aria-busy",
473
850
  "aria-checked",
474
851
  "aria-disabled",
475
852
  "aria-expanded",
476
853
  "aria-label",
477
854
  "aria-selected",
478
- "ellipsizeMode",
479
- "disabled",
480
855
  "id",
481
856
  "nativeID",
482
- "numberOfLines",
483
- "onLongPress",
484
- "onPress",
485
- "onPressIn",
486
- "onPressOut",
487
- "onResponderGrant",
488
- "onResponderMove",
489
- "onResponderRelease",
490
- "onResponderTerminate",
491
- "onResponderTerminationRequest",
492
- "onStartShouldSetResponder",
493
- "pressRetentionOffset",
494
- "selectable",
495
- "selectionColor",
496
- "suppressHighlighting",
497
857
  "style"
858
+ // TODO: process style at runtime
498
859
  ]);
499
- const skipComponents = ["View", "Fragment", "ScrollView", "FlatList"];
500
860
  const viewOptimizer = (path, log = () => {
501
- }) => {
861
+ }, options) => {
502
862
  var _a, _b, _c;
503
863
  if (isIgnoredLine(path)) return;
504
864
  if (!isValidJSXComponent(path, "View")) return;
505
865
  if (!isReactNativeImport(path, "View")) return;
506
866
  if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
507
- if (hasComponentAncestor(path, "Text", skipComponents)) return;
867
+ if (hasUnsafeViewAncestor(path, (options == null ? void 0 : options.dangerouslyOptimizeViewWithUnknownAncestors) === true)) return;
508
868
  const hub = path.hub;
509
869
  const file = typeof hub === "object" && hub !== null && "file" in hub ? hub.file : void 0;
510
870
  if (!file) {
@@ -529,7 +889,7 @@ var index = declare((api) => {
529
889
  };
530
890
  if (isIgnoredFile(path, (_b = options.ignores) != null ? _b : [])) return;
531
891
  if (((_c = options.optimizations) == null ? void 0 : _c.text) !== false) textOptimizer(path, logger);
532
- if (((_d = options.optimizations) == null ? void 0 : _d.view) !== false) viewOptimizer(path, logger);
892
+ if (((_d = options.optimizations) == null ? void 0 : _d.view) !== false) viewOptimizer(path, logger, options);
533
893
  }
534
894
  }
535
895
  };