@mobileai/react-native 0.9.26 → 0.9.27

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.
@@ -87,6 +87,7 @@ function deduplicateAndPrioritize(elements) {
87
87
  function extractContentFromAST(sourceCode, filePath) {
88
88
  const elements = [];
89
89
  const navigationLinks = [];
90
+ const visibleText = [];
90
91
 
91
92
  let ast;
92
93
  try {
@@ -125,8 +126,10 @@ function extractContentFromAST(sourceCode, filePath) {
125
126
  const screenTarget = getStringAttribute(astPath.node, 'screen');
126
127
  if (screenTarget) navigationLinks.push(screenTarget);
127
128
  } else {
128
- const buttonLabel = findChildTextContentRecursive(astPath);
129
- if (buttonLabel) elements.push(`${buttonLabel} (button)`);
129
+ const buttonLabels = findChildTextContentRecursive(astPath);
130
+ for (const buttonLabel of buttonLabels) {
131
+ elements.push(`${buttonLabel} (button)`);
132
+ }
130
133
  }
131
134
  break;
132
135
  }
@@ -171,11 +174,21 @@ function extractContentFromAST(sourceCode, filePath) {
171
174
  if (Array.isArray(target)) navigationLinks.push(...target);
172
175
  else if (target) navigationLinks.push(target);
173
176
  },
177
+
178
+ JSXElement(astPath) {
179
+ const elementName = getJSXElementName(astPath.node.openingElement.name);
180
+ if (elementName !== 'Text') return;
181
+ const labels = extractTextCandidatesRecursive(astPath.node, astPath.scope);
182
+ for (const label of labels) {
183
+ visibleText.push(label);
184
+ }
185
+ },
174
186
  });
175
187
 
176
188
  return {
177
189
  elements: deduplicateAndPrioritize(elements),
178
190
  navigationLinks: [...new Set(navigationLinks)],
191
+ visibleText: dedupeLabels(visibleText).slice(0, 6),
179
192
  };
180
193
  }
181
194
 
@@ -490,6 +503,9 @@ function buildDescription(extracted) {
490
503
  if (extracted.elements.length > 0) {
491
504
  return extracted.elements.join(', ');
492
505
  }
506
+ if (extracted.visibleText?.length) {
507
+ return extracted.visibleText.join(', ');
508
+ }
493
509
  return 'Screen content';
494
510
  }
495
511
 
@@ -553,7 +569,7 @@ function findSiblingTextLabel(switchPath) {
553
569
  if (!parent?.node || !t.isJSXElement(parent.node)) return null;
554
570
  for (const child of parent.node.children) {
555
571
  if (t.isJSXElement(child)) {
556
- const text = extractTextRecursive(child);
572
+ const text = extractTextCandidatesRecursive(child, parent.scope)[0];
557
573
  if (text) return text;
558
574
  }
559
575
  }
@@ -562,30 +578,138 @@ function findSiblingTextLabel(switchPath) {
562
578
 
563
579
  function findChildTextContentRecursive(pressablePath) {
564
580
  const jsxElement = pressablePath.parent;
565
- if (!t.isJSXElement(jsxElement)) return null;
566
- return extractTextRecursive(jsxElement);
581
+ if (!t.isJSXElement(jsxElement)) return [];
582
+ return extractTextCandidatesRecursive(jsxElement, pressablePath.scope);
567
583
  }
568
584
 
569
- function extractTextRecursive(element, depth = 0) {
570
- if (depth > 4) return null;
585
+ function extractTextCandidatesRecursive(element, scope, depth = 0) {
586
+ if (depth > 4) return [];
587
+ const labels = [];
571
588
  for (const child of element.children) {
572
589
  if (t.isJSXText(child)) {
573
- const text = child.value.trim();
574
- if (text) return text;
590
+ const text = normalizeLabel(child.value);
591
+ if (text) labels.push(text);
575
592
  }
576
593
  if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
577
- const hint = extractSemanticHint(child.expression);
578
- if (hint) return hint;
594
+ labels.push(...resolveExpressionCandidates(child.expression, scope));
579
595
  }
580
596
  if (t.isJSXElement(child)) {
581
597
  const childName = getJSXElementName(child.openingElement.name);
582
598
  if (ICON_EXACT.has(childName) || childName.endsWith('Icon') ||
583
599
  childName.endsWith('_Dark') || childName.endsWith('_Light')) continue;
584
- const text = extractTextRecursive(child, depth + 1);
585
- if (text) return text;
600
+ labels.push(...extractTextCandidatesRecursive(child, scope, depth + 1));
586
601
  }
587
602
  }
588
- return null;
603
+ return dedupeLabels(labels);
604
+ }
605
+
606
+ function resolveExpressionCandidates(node, scope, depth = 0) {
607
+ if (!node || depth > 6) return [];
608
+ if (t.isStringLiteral(node)) return [node.value];
609
+ if (t.isNumericLiteral(node)) return [String(node.value)];
610
+ if (t.isTemplateLiteral(node) && node.quasis.length > 0) {
611
+ const text = normalizeLabel(node.quasis.map(q => q.value.raw).join(' '));
612
+ return text ? [text] : [];
613
+ }
614
+ if (t.isIdentifier(node)) {
615
+ return resolveBindingCandidates(node.name, scope, depth + 1);
616
+ }
617
+ if (t.isConditionalExpression(node)) {
618
+ return dedupeLabels([
619
+ ...resolveExpressionCandidates(node.consequent, scope, depth + 1),
620
+ ...resolveExpressionCandidates(node.alternate, scope, depth + 1),
621
+ ]);
622
+ }
623
+ if (t.isLogicalExpression(node)) {
624
+ return dedupeLabels([
625
+ ...resolveExpressionCandidates(node.left, scope, depth + 1),
626
+ ...resolveExpressionCandidates(node.right, scope, depth + 1),
627
+ ]);
628
+ }
629
+ const hint = extractSemanticHint(node, depth + 1);
630
+ return hint ? [hint] : [];
631
+ }
632
+
633
+ function resolveBindingCandidates(name, scope, depth) {
634
+ if (!scope || depth > 6) return [];
635
+ const binding = scope.getBinding?.(name);
636
+ if (!binding?.path) return [];
637
+
638
+ if (binding.path.isVariableDeclarator()) {
639
+ return resolveExpressionCandidates(binding.path.node.init, binding.path.scope, depth + 1);
640
+ }
641
+
642
+ if (binding.path.isIdentifier() && binding.path.listKey === 'params') {
643
+ return dedupeLabels(
644
+ resolveFunctionParamValues(binding.path, depth + 1).flatMap(valueNode =>
645
+ resolveExpressionCandidates(valueNode, binding.path.scope, depth + 1)
646
+ )
647
+ );
648
+ }
649
+
650
+ return [];
651
+ }
652
+
653
+ function resolveFunctionParamValues(paramPath, depth) {
654
+ if (depth > 6) return [];
655
+ const functionPath = paramPath.findParent(p => p.isFunction());
656
+ const callPath = functionPath?.parentPath;
657
+ if (!callPath?.isCallExpression()) return [];
658
+
659
+ const callee = callPath.node.callee;
660
+ if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property)) return [];
661
+ if (!['map', 'flatMap'].includes(callee.property.name)) return [];
662
+
663
+ const sourceValues = resolveExpressionValueNodes(callee.object, callPath.scope, depth + 1);
664
+ const results = [];
665
+
666
+ for (const sourceNode of sourceValues) {
667
+ if (!t.isArrayExpression(sourceNode)) continue;
668
+ for (const element of sourceNode.elements) {
669
+ if (element && !t.isSpreadElement(element)) {
670
+ results.push(element);
671
+ }
672
+ }
673
+ }
674
+
675
+ return results;
676
+ }
677
+
678
+ function resolveExpressionValueNodes(node, scope, depth) {
679
+ if (!node || depth > 6) return [];
680
+ if (
681
+ t.isStringLiteral(node) ||
682
+ t.isNumericLiteral(node) ||
683
+ t.isArrayExpression(node) ||
684
+ t.isTemplateLiteral(node)
685
+ ) {
686
+ return [node];
687
+ }
688
+ if (t.isIdentifier(node)) {
689
+ const binding = scope?.getBinding?.(node.name);
690
+ if (binding?.path?.isVariableDeclarator()) {
691
+ return resolveExpressionValueNodes(binding.path.node.init, binding.path.scope, depth + 1);
692
+ }
693
+ }
694
+ return [];
695
+ }
696
+
697
+ function normalizeLabel(text) {
698
+ return text ? text.replace(/\s+/g, ' ').trim() : '';
699
+ }
700
+
701
+ function dedupeLabels(labels) {
702
+ const seen = new Set();
703
+ const result = [];
704
+ for (const label of labels) {
705
+ const normalized = normalizeLabel(label);
706
+ if (!normalized) continue;
707
+ const key = normalized.toLowerCase();
708
+ if (seen.has(key)) continue;
709
+ seen.add(key);
710
+ result.push(normalized);
711
+ }
712
+ return result;
589
713
  }
590
714
 
591
715
 
@@ -667,18 +791,20 @@ function scanExpoRouterApp(projectRoot) {
667
791
 
668
792
  const screens = [];
669
793
  const layoutTitles = new Map();
794
+ const extractedCache = new Map();
795
+ const resolvedImplementationCache = new Map();
670
796
  extractLayoutTitles(appDir, appDir, layoutTitles);
671
- scanDirectory(appDir, appDir, screens, layoutTitles);
797
+ scanDirectory(appDir, appDir, screens, layoutTitles, projectRoot, extractedCache, resolvedImplementationCache);
672
798
  return screens;
673
799
  }
674
800
 
675
- function scanDirectory(dir, appRoot, screens, layoutTitles) {
801
+ function scanDirectory(dir, appRoot, screens, layoutTitles, projectRoot, extractedCache, resolvedImplementationCache) {
676
802
  const entries = fs.readdirSync(dir, { withFileTypes: true });
677
803
  for (const entry of entries) {
678
804
  const fullPath = path.join(dir, entry.name);
679
805
  if (entry.isDirectory()) {
680
806
  if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
681
- scanDirectory(fullPath, appRoot, screens, layoutTitles);
807
+ scanDirectory(fullPath, appRoot, screens, layoutTitles, projectRoot, extractedCache, resolvedImplementationCache);
682
808
  continue;
683
809
  }
684
810
  if (!entry.name.match(/\.(tsx?|jsx?)$/)) continue;
@@ -686,18 +812,68 @@ function scanDirectory(dir, appRoot, screens, layoutTitles) {
686
812
  if (entry.name.startsWith('+') || entry.name.startsWith('_')) continue;
687
813
 
688
814
  const routeName = filePathToRouteName(fullPath, appRoot);
689
- const sourceCode = fs.readFileSync(fullPath, 'utf-8');
690
- const extracted = extractContentFromAST(sourceCode, fullPath);
691
815
  const title = layoutTitles.get(routeName);
816
+ const routeCandidate = buildScreenCandidate(routeName, title, fullPath, extractedCache);
817
+ const resolvedImplementation = resolvedImplementationCache.get(fullPath) || resolveProxyScreenFile(fullPath, projectRoot);
818
+ resolvedImplementationCache.set(fullPath, resolvedImplementation);
819
+ const implementationCandidate = resolvedImplementation !== fullPath
820
+ ? buildScreenCandidate(routeName, title, resolvedImplementation, extractedCache)
821
+ : routeCandidate;
822
+ const chosenCandidate = scoreScreenCandidate(implementationCandidate) > scoreScreenCandidate(routeCandidate)
823
+ ? implementationCandidate
824
+ : routeCandidate;
692
825
 
693
826
  screens.push({
694
- routeName, filePath: fullPath, title,
695
- description: buildDescription(extracted),
696
- navigationLinks: extracted.navigationLinks,
827
+ routeName: chosenCandidate.routeName,
828
+ filePath: chosenCandidate.filePath,
829
+ title: chosenCandidate.title,
830
+ description: chosenCandidate.description,
831
+ navigationLinks: chosenCandidate.navigationLinks,
697
832
  });
698
833
  }
699
834
  }
700
835
 
836
+ function buildScreenCandidate(routeName, title, filePath, extractedCache) {
837
+ let extracted = extractedCache.get(filePath);
838
+ if (!extracted) {
839
+ const sourceCode = fs.readFileSync(filePath, 'utf-8');
840
+ extracted = extractContentFromAST(sourceCode, filePath);
841
+ extractedCache.set(filePath, extracted);
842
+ }
843
+
844
+ return {
845
+ routeName,
846
+ filePath,
847
+ title,
848
+ description: buildDescription(extracted),
849
+ navigationLinks: extracted.navigationLinks,
850
+ };
851
+ }
852
+
853
+ function scoreScreenCandidate(screen) {
854
+ let score = 0;
855
+
856
+ if (fs.existsSync(screen.filePath)) score += 20;
857
+ if (screen.description && screen.description !== 'Screen content') score += 80;
858
+ if (screen.navigationLinks.length > 0) score += 12;
859
+ if (screen.title) score += 4;
860
+
861
+ const describedElements = screen.description
862
+ .split(',')
863
+ .map(part => part.trim())
864
+ .filter(part => part && part !== 'Screen content').length;
865
+ score += Math.min(describedElements, 8) * 6;
866
+
867
+ const componentOnlyElements = screen.description
868
+ .split(',')
869
+ .map(part => part.trim())
870
+ .filter(part => part.endsWith('(component)')).length;
871
+ if (componentOnlyElements > 0) score -= componentOnlyElements * 20;
872
+ if (describedElements > 0 && componentOnlyElements === describedElements) score -= 120;
873
+
874
+ return score;
875
+ }
876
+
701
877
  // ─── React Navigation Scanner ─────────────────────────────────
702
878
 
703
879
  const NAVIGATOR_FUNCTIONS = [
@@ -978,43 +1154,93 @@ function extractTitleFromOptions(optionsNode) {
978
1154
  return null;
979
1155
  }
980
1156
 
1157
+ const FILE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
1158
+ const DEFAULT_PROXY_HOPS = 10;
1159
+ const moduleProxyInfoCache = new Map();
1160
+ const tsConfigResolutionCache = new Map();
1161
+
981
1162
  function resolveComponentPath(componentName, imports, currentFile) {
982
1163
  const parts = componentName.split('.');
983
1164
  const baseComponent = parts[0];
984
1165
  const property = parts[1];
985
1166
 
1167
+ if (!baseComponent) return currentFile;
1168
+
986
1169
  const importPath = imports.get(baseComponent);
987
1170
  if (!importPath) return currentFile;
988
1171
 
989
- let resolvedBase = currentFile;
990
- if (importPath.startsWith('.')) {
991
- const dir = path.dirname(currentFile);
992
- const resolved = path.resolve(dir, importPath);
993
- let found = false;
994
- for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
995
- if (fs.existsSync(resolved + ext)) {
996
- resolvedBase = resolved + ext;
997
- found = true; break;
998
- }
999
- }
1000
- if (!found) {
1001
- for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
1002
- const idx = path.join(resolved, `index${ext}`);
1003
- if (fs.existsSync(idx)) {
1004
- resolvedBase = idx;
1005
- found = true; break;
1172
+ const resolvedBase = resolveImportSpecifier(importPath, currentFile, _projectRoot);
1173
+ if (!resolvedBase) return currentFile;
1174
+
1175
+ if (!property || resolvedBase === currentFile) return resolvedBase;
1176
+
1177
+ return traceExportProperty(resolvedBase, property, _projectRoot) || resolvedBase;
1178
+ }
1179
+
1180
+ function resolveProxyScreenFile(entryFile, projectRoot, maxHops = DEFAULT_PROXY_HOPS) {
1181
+ let currentFile = path.resolve(entryFile);
1182
+ let exportName = 'default';
1183
+ const visited = new Set();
1184
+
1185
+ for (let hop = 0; hop < maxHops; hop++) {
1186
+ const visitKey = `${currentFile}::${exportName}`;
1187
+ if (visited.has(visitKey)) return currentFile;
1188
+ visited.add(visitKey);
1189
+
1190
+ const next = resolveExportTarget(currentFile, exportName, projectRoot);
1191
+ if (!next) return currentFile;
1192
+ if (next.filePath === currentFile && next.exportName === exportName) return currentFile;
1193
+
1194
+ currentFile = next.filePath;
1195
+ exportName = next.exportName;
1196
+ }
1197
+
1198
+ return currentFile;
1199
+ }
1200
+
1201
+ function resolveExportTarget(filePath, exportName, projectRoot) {
1202
+ const info = getModuleProxyInfo(filePath);
1203
+ if (!info) return null;
1204
+
1205
+ if (exportName === 'default') {
1206
+ if (info.defaultExportLocal) {
1207
+ const imported = info.imports.get(info.defaultExportLocal);
1208
+ if (imported) {
1209
+ const resolved = resolveImportSpecifier(imported.source, filePath, projectRoot);
1210
+ if (resolved) {
1211
+ return { filePath: resolved, exportName: imported.importedName };
1006
1212
  }
1007
1213
  }
1214
+ return null;
1008
1215
  }
1009
- if (!found) resolvedBase = resolved;
1216
+
1217
+ if (info.hasLocalDefaultExport) return null;
1010
1218
  }
1011
1219
 
1012
- if (!property || resolvedBase === currentFile) return resolvedBase;
1220
+ const link = info.exportLinks.find(candidate => candidate.exportedName === exportName);
1221
+ if (!link) return null;
1013
1222
 
1014
- return traceExportProperty(resolvedBase, baseComponent, property) || resolvedBase;
1223
+ if (link.source) {
1224
+ const resolved = resolveImportSpecifier(link.source, filePath, projectRoot);
1225
+ if (!resolved) return null;
1226
+ return { filePath: resolved, exportName: link.importedName || 'default' };
1227
+ }
1228
+
1229
+ if (link.localName) {
1230
+ const imported = info.imports.get(link.localName);
1231
+ if (imported) {
1232
+ const resolved = resolveImportSpecifier(imported.source, filePath, projectRoot);
1233
+ if (!resolved) return null;
1234
+ return { filePath: resolved, exportName: imported.importedName };
1235
+ }
1236
+
1237
+ if (info.localBindings.has(link.localName)) return null;
1238
+ }
1239
+
1240
+ return null;
1015
1241
  }
1016
1242
 
1017
- function traceExportProperty(filePath, objectName, propertyName) {
1243
+ function traceExportProperty(filePath, propertyName, projectRoot) {
1018
1244
  if (!fs.existsSync(filePath)) return null;
1019
1245
  const sourceCode = fs.readFileSync(filePath, 'utf-8');
1020
1246
  let ast;
@@ -1022,112 +1248,277 @@ function traceExportProperty(filePath, objectName, propertyName) {
1022
1248
  ast = parse(sourceCode, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'] });
1023
1249
  } catch { return null; }
1024
1250
 
1025
- const innerImports = new Map(); // identifier → import path
1026
- const localVarInits = new Map(); // identifier → AST node (for const X = React.lazy(...))
1027
- let targetNode = null; // The AST node for the property value
1028
-
1029
- traverse(ast, {
1030
- ImportDeclaration(nodePath) {
1031
- const source = nodePath.node.source.value;
1032
- for (const specifier of nodePath.node.specifiers) {
1033
- if (t.isImportDefaultSpecifier(specifier) || t.isImportSpecifier(specifier)) {
1034
- innerImports.set(specifier.local.name, source);
1251
+ const imports = new Map();
1252
+ let importName = null;
1253
+
1254
+ for (const node of ast.program.body) {
1255
+ if (t.isImportDeclaration(node)) {
1256
+ const source = node.source.value;
1257
+ for (const specifier of node.specifiers) {
1258
+ if (t.isImportDefaultSpecifier(specifier)) {
1259
+ imports.set(specifier.local.name, source);
1260
+ } else if (t.isImportSpecifier(specifier)) {
1261
+ imports.set(specifier.local.name, t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value);
1035
1262
  }
1036
1263
  }
1037
- },
1038
- VariableDeclarator(nodePath) {
1039
- const id = nodePath.node.id;
1040
- const init = nodePath.node.init;
1041
- if (!t.isIdentifier(id)) return;
1042
-
1043
- // Collect the StackRoute = { ... } object and find the target property
1044
- if (id.name === objectName && t.isObjectExpression(init)) {
1045
- for (const prop of init.properties) {
1046
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === propertyName) {
1047
- targetNode = prop.value;
1264
+ continue;
1265
+ }
1266
+
1267
+ if (t.isVariableDeclaration(node)) {
1268
+ for (const declaration of node.declarations) {
1269
+ if (t.isIdentifier(declaration.id) && t.isObjectExpression(declaration.init)) {
1270
+ for (const property of declaration.init.properties) {
1271
+ if (
1272
+ t.isObjectProperty(property) &&
1273
+ !property.computed &&
1274
+ t.isIdentifier(property.key) &&
1275
+ property.key.name === propertyName &&
1276
+ t.isIdentifier(property.value)
1277
+ ) {
1278
+ importName = property.value.name;
1279
+ }
1048
1280
  }
1049
1281
  }
1050
1282
  }
1051
-
1052
- // Also collect all local variable assignments for later unwrapping
1053
- if (init) {
1054
- localVarInits.set(id.name, init);
1055
- }
1056
1283
  }
1057
- });
1284
+ }
1058
1285
 
1059
- if (!targetNode) return null;
1286
+ if (!importName) return null;
1060
1287
 
1061
- // Unwrap the target to find the component source
1062
- const ref = unwrapToComponentRef(targetNode, innerImports, localVarInits);
1063
- if (!ref) return null;
1288
+ const importPath = imports.get(importName);
1289
+ if (!importPath) return null;
1064
1290
 
1065
- const dir = path.dirname(filePath);
1066
- const resolved = path.resolve(dir, ref);
1067
- for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
1068
- if (fs.existsSync(resolved + ext)) return resolved + ext;
1291
+ return resolveImportSpecifier(importPath, filePath, projectRoot);
1292
+ }
1293
+
1294
+ function resolveImportSpecifier(importPath, currentFile, projectRoot) {
1295
+ const direct = resolveImportCandidate(importPath, currentFile, projectRoot);
1296
+ return direct ? path.resolve(direct) : null;
1297
+ }
1298
+
1299
+ function resolveImportCandidate(importPath, currentFile, projectRoot) {
1300
+ if (!importPath || importPath.includes('node_modules')) return null;
1301
+
1302
+ if (importPath.startsWith('.')) {
1303
+ return resolveFilePath(path.dirname(currentFile), importPath);
1304
+ }
1305
+
1306
+ const tsConfig = getTsConfigResolution(projectRoot);
1307
+ for (const alias of tsConfig.aliases) {
1308
+ const wildcardValue = matchAlias(importPath, alias.keyPrefix, alias.keySuffix);
1309
+ if (wildcardValue === null) continue;
1310
+ const target = `${alias.targetPrefix}${wildcardValue}${alias.targetSuffix}`;
1311
+ const resolved = resolveFilePath(projectRoot, target);
1312
+ if (resolved) return resolved;
1069
1313
  }
1070
- for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
1071
- const idx = path.join(resolved, `index${ext}`);
1072
- if (fs.existsSync(idx)) return idx;
1314
+
1315
+ if (tsConfig.baseUrl) {
1316
+ const resolved = resolveFilePath(tsConfig.baseUrl, importPath);
1317
+ if (resolved) return resolved;
1073
1318
  }
1074
- return resolved;
1319
+
1320
+ const srcResolved = resolveFilePath(path.join(projectRoot, 'src'), importPath);
1321
+ if (srcResolved) return srcResolved;
1322
+
1323
+ const rootResolved = resolveFilePath(projectRoot, importPath);
1324
+ if (rootResolved) return rootResolved;
1325
+
1326
+ return null;
1075
1327
  }
1076
1328
 
1077
- /**
1078
- * Recursively unwrap AST nodes to find the original component import path.
1079
- * Handles: Identifier (import lookup), React.lazy(() => import('path')),
1080
- * React.memo(X), connect(mapState)(X), observer(X), withX(X), etc.
1081
- * Returns the relative import path string or null.
1082
- */
1083
- function unwrapToComponentRef(node, imports, localVars, depth = 0) {
1084
- if (!node || depth > 5) return null; // safety: prevent infinite recursion
1329
+ function resolveFilePath(baseDir, rawPath) {
1330
+ const candidateBase = path.resolve(baseDir, rawPath);
1085
1331
 
1086
- // Direct identifier: look up in imports first, then local vars
1087
- if (t.isIdentifier(node)) {
1088
- const importPath = imports.get(node.name);
1089
- if (importPath && importPath.startsWith('.')) return importPath;
1332
+ if (fs.existsSync(candidateBase) && fs.statSync(candidateBase).isFile()) {
1333
+ return candidateBase;
1334
+ }
1335
+
1336
+ for (const extension of FILE_EXTENSIONS) {
1337
+ const withExtension = `${candidateBase}${extension}`;
1338
+ if (fs.existsSync(withExtension)) return withExtension;
1339
+ }
1340
+
1341
+ for (const extension of FILE_EXTENSIONS) {
1342
+ const indexPath = path.join(candidateBase, `index${extension}`);
1343
+ if (fs.existsSync(indexPath)) return indexPath;
1344
+ }
1090
1345
 
1091
- // Check if it's a local variable (const X = React.lazy(...))
1092
- const localInit = localVars.get(node.name);
1093
- if (localInit) return unwrapToComponentRef(localInit, imports, localVars, depth + 1);
1346
+ return null;
1347
+ }
1348
+
1349
+ function getTsConfigResolution(projectRoot) {
1350
+ const cached = tsConfigResolutionCache.get(projectRoot);
1351
+ if (cached) return cached;
1352
+
1353
+ const tsConfigPath = path.join(projectRoot, 'tsconfig.json');
1354
+ const resolution = { aliases: [], baseUrl: undefined };
1355
+
1356
+ try {
1357
+ if (fs.existsSync(tsConfigPath)) {
1358
+ const raw = JSON.parse(fs.readFileSync(tsConfigPath, 'utf-8'));
1359
+ const compilerOptions = (raw && raw.compilerOptions) || {};
1360
+ if (typeof compilerOptions.baseUrl === 'string') {
1361
+ resolution.baseUrl = path.resolve(projectRoot, compilerOptions.baseUrl);
1362
+ }
1363
+ resolution.aliases = normalizePathAliases(compilerOptions.paths || {});
1364
+ }
1365
+ } catch {
1366
+ // Ignore malformed tsconfig and fall back to conventional resolution.
1367
+ }
1368
+
1369
+ tsConfigResolutionCache.set(projectRoot, resolution);
1370
+ return resolution;
1371
+ }
1372
+
1373
+ function normalizePathAliases(pathsConfig) {
1374
+ const aliases = [];
1375
+ for (const [key, values] of Object.entries(pathsConfig)) {
1376
+ const target = values && values[0];
1377
+ if (!target) continue;
1378
+ const [keyPrefix, keySuffix] = splitAliasPattern(key);
1379
+ const [targetPrefix, targetSuffix] = splitAliasPattern(target);
1380
+ aliases.push({ keyPrefix, keySuffix, targetPrefix, targetSuffix });
1381
+ }
1382
+ return aliases;
1383
+ }
1384
+
1385
+ function splitAliasPattern(pattern) {
1386
+ const starIndex = pattern.indexOf('*');
1387
+ if (starIndex === -1) return [pattern, ''];
1388
+ return [pattern.slice(0, starIndex), pattern.slice(starIndex + 1)];
1389
+ }
1094
1390
 
1391
+ function matchAlias(value, prefix, suffix) {
1392
+ if (!value.startsWith(prefix)) return null;
1393
+ if (suffix && !value.endsWith(suffix)) return null;
1394
+ return value.slice(prefix.length, suffix ? value.length - suffix.length : undefined);
1395
+ }
1396
+
1397
+ function getModuleProxyInfo(filePath) {
1398
+ const cached = moduleProxyInfoCache.get(filePath);
1399
+ if (cached !== undefined) return cached;
1400
+
1401
+ if (!fs.existsSync(filePath)) {
1402
+ moduleProxyInfoCache.set(filePath, null);
1403
+ return null;
1404
+ }
1405
+
1406
+ let ast;
1407
+ try {
1408
+ ast = parse(fs.readFileSync(filePath, 'utf-8'), {
1409
+ sourceType: 'module',
1410
+ plugins: ['jsx', 'typescript', 'decorators-legacy'],
1411
+ });
1412
+ } catch {
1413
+ moduleProxyInfoCache.set(filePath, null);
1095
1414
  return null;
1096
1415
  }
1097
1416
 
1098
- // CallExpression: React.lazy(), React.memo(), connect()(), observer(), withX()
1099
- if (t.isCallExpression(node)) {
1100
- // React.lazy(() => import('./path')) — extract dynamic import path
1101
- const callee = node.callee;
1102
- if (t.isMemberExpression(callee) && t.isIdentifier(callee.property, { name: 'lazy' })) {
1103
- const arrowFn = node.arguments[0];
1104
- if (t.isArrowFunctionExpression(arrowFn) || t.isFunctionExpression(arrowFn)) {
1105
- const body = t.isBlockStatement(arrowFn.body)
1106
- ? arrowFn.body.body.find(s => t.isReturnStatement(s))?.argument
1107
- : arrowFn.body;
1108
- if (body && t.isCallExpression(body) && t.isImport(body.callee)) {
1109
- const importArg = body.arguments[0];
1110
- if (t.isStringLiteral(importArg)) return importArg.value;
1417
+ const info = {
1418
+ imports: new Map(),
1419
+ exportLinks: [],
1420
+ localBindings: new Set(),
1421
+ defaultExportLocal: undefined,
1422
+ hasLocalDefaultExport: false,
1423
+ };
1424
+
1425
+ for (const node of ast.program.body) {
1426
+ if (t.isImportDeclaration(node)) {
1427
+ const source = node.source.value;
1428
+ for (const specifier of node.specifiers) {
1429
+ if (t.isImportDefaultSpecifier(specifier)) {
1430
+ info.imports.set(specifier.local.name, { source, importedName: 'default' });
1431
+ } else if (t.isImportSpecifier(specifier)) {
1432
+ info.imports.set(specifier.local.name, {
1433
+ source,
1434
+ importedName: t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value,
1435
+ });
1111
1436
  }
1112
1437
  }
1438
+ continue;
1439
+ }
1440
+
1441
+ if (t.isExportDefaultDeclaration(node)) {
1442
+ if (t.isIdentifier(node.declaration)) {
1443
+ info.defaultExportLocal = node.declaration.name;
1444
+ } else {
1445
+ info.hasLocalDefaultExport = true;
1446
+ collectLocalBindingNames(node.declaration, info.localBindings);
1447
+ }
1448
+ continue;
1449
+ }
1450
+
1451
+ if (t.isExportNamedDeclaration(node)) {
1452
+ if (node.declaration) {
1453
+ collectLocalBindingNames(node.declaration, info.localBindings);
1454
+ }
1455
+
1456
+ for (const specifier of node.specifiers) {
1457
+ if (!t.isExportSpecifier(specifier)) continue;
1458
+
1459
+ info.exportLinks.push({
1460
+ exportedName: getModuleExportedName(specifier.exported),
1461
+ source: node.source ? node.source.value : undefined,
1462
+ importedName: node.source ? getModuleExportedName(specifier.local) : undefined,
1463
+ localName: node.source ? undefined : getModuleExportedName(specifier.local),
1464
+ });
1465
+ }
1113
1466
  }
1467
+ }
1468
+
1469
+ moduleProxyInfoCache.set(filePath, info);
1470
+ return info;
1471
+ }
1472
+
1473
+ function getModuleExportedName(node) {
1474
+ return t.isIdentifier(node) ? node.name : node.value;
1475
+ }
1114
1476
 
1115
- // For HOCs: React.memo(X), observer(X), withNavigation(X)
1116
- // The component is typically the first argument
1117
- if (node.arguments.length > 0) {
1118
- const result = unwrapToComponentRef(node.arguments[0], imports, localVars, depth + 1);
1119
- if (result) return result;
1477
+ function collectLocalBindingNames(node, bindings) {
1478
+ if (t.isFunctionDeclaration(node) || t.isClassDeclaration(node)) {
1479
+ if (node.id) bindings.add(node.id.name);
1480
+ return;
1481
+ }
1482
+
1483
+ if (t.isVariableDeclaration(node)) {
1484
+ for (const declaration of node.declarations) {
1485
+ collectBindingNamesFromPattern(declaration.id, bindings);
1120
1486
  }
1487
+ }
1488
+ }
1489
+
1490
+ function collectBindingNamesFromPattern(pattern, bindings) {
1491
+ if (t.isIdentifier(pattern)) {
1492
+ bindings.add(pattern.name);
1493
+ return;
1494
+ }
1121
1495
 
1122
- // Chained HOC: connect(mapState)(Component) — callee is itself a CallExpression
1123
- if (t.isCallExpression(callee) && node.arguments.length > 0) {
1124
- return unwrapToComponentRef(node.arguments[0], imports, localVars, depth + 1);
1496
+ if (t.isObjectPattern(pattern)) {
1497
+ for (const property of pattern.properties) {
1498
+ if (t.isRestElement(property)) {
1499
+ collectBindingNamesFromPattern(property.argument, bindings);
1500
+ } else if (t.isObjectProperty(property)) {
1501
+ collectBindingNamesFromPattern(property.value, bindings);
1502
+ }
1125
1503
  }
1504
+ return;
1505
+ }
1126
1506
 
1127
- return null;
1507
+ if (t.isArrayPattern(pattern)) {
1508
+ for (const element of pattern.elements) {
1509
+ if (element) collectBindingNamesFromPattern(element, bindings);
1510
+ }
1511
+ return;
1128
1512
  }
1129
1513
 
1130
- return null;
1514
+ if (t.isAssignmentPattern(pattern)) {
1515
+ collectBindingNamesFromPattern(pattern.left, bindings);
1516
+ return;
1517
+ }
1518
+
1519
+ if (t.isRestElement(pattern)) {
1520
+ collectBindingNamesFromPattern(pattern.argument, bindings);
1521
+ }
1131
1522
  }
1132
1523
 
1133
1524
  // ─── Main ──────────────────────────────────────────────────────