@mobileai/react-native 0.9.26 → 0.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +28 -15
  2. package/android/build.gradle +17 -0
  3. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
  4. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
  5. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
  6. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
  7. package/bin/generate-map.cjs +556 -126
  8. package/ios/Podfile +63 -0
  9. package/ios/Podfile.lock +2290 -0
  10. package/ios/Podfile.properties.json +4 -0
  11. package/ios/mobileaireactnative/AppDelegate.swift +69 -0
  12. package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  13. package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
  14. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
  15. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
  16. package/ios/mobileaireactnative/Info.plist +55 -0
  17. package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
  18. package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
  19. package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
  20. package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
  21. package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
  22. package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
  23. package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
  24. package/lib/module/components/AIAgent.js +407 -148
  25. package/lib/module/components/AgentChatBar.js +253 -62
  26. package/lib/module/components/FloatingOverlayWrapper.js +68 -32
  27. package/lib/module/config/endpoints.js +22 -1
  28. package/lib/module/core/AgentRuntime.js +192 -24
  29. package/lib/module/core/FiberTreeWalker.js +410 -34
  30. package/lib/module/core/OutcomeVerifier.js +149 -0
  31. package/lib/module/core/systemPrompt.js +126 -44
  32. package/lib/module/providers/GeminiProvider.js +9 -3
  33. package/lib/module/services/MobileAIKnowledgeRetriever.js +1 -1
  34. package/lib/module/services/telemetry/MobileAI.js +1 -1
  35. package/lib/module/services/telemetry/TelemetryService.js +21 -2
  36. package/lib/module/services/telemetry/TouchAutoCapture.js +45 -35
  37. package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
  38. package/lib/module/support/supportPrompt.js +22 -7
  39. package/lib/module/support/supportStyle.js +55 -0
  40. package/lib/module/support/types.js +2 -0
  41. package/lib/module/tools/tapTool.js +77 -6
  42. package/lib/module/tools/typeTool.js +20 -0
  43. package/lib/module/utils/humanizeScreenName.js +49 -0
  44. package/lib/typescript/src/components/AIAgent.d.ts +6 -2
  45. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
  46. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
  47. package/lib/typescript/src/config/endpoints.d.ts +4 -0
  48. package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
  49. package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
  50. package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
  51. package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
  52. package/lib/typescript/src/core/types.d.ts +37 -1
  53. package/lib/typescript/src/index.d.ts +1 -0
  54. package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +1 -1
  55. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
  56. package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
  57. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
  58. package/lib/typescript/src/support/index.d.ts +1 -0
  59. package/lib/typescript/src/support/supportStyle.d.ts +9 -0
  60. package/lib/typescript/src/support/types.d.ts +3 -0
  61. package/lib/typescript/src/tools/tapTool.d.ts +3 -2
  62. package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
  63. package/lib/typescript/test-tree.d.ts +2 -0
  64. package/package.json +5 -2
  65. package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
  66. package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
  67. package/ios/MobileAIPilotIntents.swift +0 -51
@@ -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;
1222
+
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
+ }
1013
1228
 
1014
- return traceExportProperty(resolvedBase, baseComponent, property) || resolvedBase;
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
+ }
1345
+
1346
+ return null;
1347
+ }
1090
1348
 
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);
1349
+ function getTsConfigResolution(projectRoot) {
1350
+ const cached = tsConfigResolutionCache.get(projectRoot);
1351
+ if (cached) return cached;
1094
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
+ }
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;
1113
1439
  }
1114
1440
 
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;
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;
1120
1449
  }
1121
1450
 
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);
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
+ }
1125
1466
  }
1467
+ }
1126
1468
 
1127
- return null;
1469
+ moduleProxyInfoCache.set(filePath, info);
1470
+ return info;
1471
+ }
1472
+
1473
+ function getModuleExportedName(node) {
1474
+ return t.isIdentifier(node) ? node.name : node.value;
1475
+ }
1476
+
1477
+ function collectLocalBindingNames(node, bindings) {
1478
+ if (t.isFunctionDeclaration(node) || t.isClassDeclaration(node)) {
1479
+ if (node.id) bindings.add(node.id.name);
1480
+ return;
1128
1481
  }
1129
1482
 
1130
- return null;
1483
+ if (t.isVariableDeclaration(node)) {
1484
+ for (const declaration of node.declarations) {
1485
+ collectBindingNamesFromPattern(declaration.id, bindings);
1486
+ }
1487
+ }
1488
+ }
1489
+
1490
+ function collectBindingNamesFromPattern(pattern, bindings) {
1491
+ if (t.isIdentifier(pattern)) {
1492
+ bindings.add(pattern.name);
1493
+ return;
1494
+ }
1495
+
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
+ }
1503
+ }
1504
+ return;
1505
+ }
1506
+
1507
+ if (t.isArrayPattern(pattern)) {
1508
+ for (const element of pattern.elements) {
1509
+ if (element) collectBindingNamesFromPattern(element, bindings);
1510
+ }
1511
+ return;
1512
+ }
1513
+
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 ──────────────────────────────────────────────────────
@@ -1148,14 +1539,52 @@ function detectFramework(projectRoot) {
1148
1539
  }
1149
1540
 
1150
1541
  function parseArgs(argv) {
1151
- const args = { dir: '', watch: false };
1542
+ const args = {
1543
+ dir: '',
1544
+ watch: false,
1545
+ include: [],
1546
+ exclude: [],
1547
+ };
1152
1548
  for (const arg of argv) {
1153
1549
  if (arg === '--watch' || arg === '-w') args.watch = true;
1154
1550
  if (arg.startsWith('--dir=')) args.dir = arg.split('=')[1];
1551
+ if (arg.startsWith('--include=')) {
1552
+ args.include = arg.slice('--include='.length).split(',').map((entry) => entry.trim()).filter(Boolean);
1553
+ }
1554
+ if (arg.startsWith('--exclude=')) {
1555
+ args.exclude = arg.slice('--exclude='.length).split(',').map((entry) => entry.trim()).filter(Boolean);
1556
+ }
1155
1557
  }
1156
1558
  return args;
1157
1559
  }
1158
1560
 
1561
+ function routeMatchesPattern(routeName, pattern) {
1562
+ if (!pattern) return false;
1563
+ if (pattern === routeName) return true;
1564
+ if (!pattern.includes('*')) {
1565
+ return routeName === pattern;
1566
+ }
1567
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1568
+ const regex = new RegExp(`^${escaped.replace(/\\\*/g, '.*')}$`);
1569
+ return regex.test(routeName);
1570
+ }
1571
+
1572
+ function routeMatchesAnyPattern(routeName, patterns) {
1573
+ return patterns.some((pattern) => routeMatchesPattern(routeName, pattern));
1574
+ }
1575
+
1576
+ function filterScreensByRoutePatterns(screens, args) {
1577
+ return screens.filter((screen) => {
1578
+ if (args.include.length > 0 && !routeMatchesAnyPattern(screen.routeName, args.include)) {
1579
+ return false;
1580
+ }
1581
+ if (args.exclude.length > 0 && routeMatchesAnyPattern(screen.routeName, args.exclude)) {
1582
+ return false;
1583
+ }
1584
+ return true;
1585
+ });
1586
+ }
1587
+
1159
1588
  async function generate(args, projectRoot) {
1160
1589
  _projectRoot = projectRoot; // for module alias resolution in extractImportPaths
1161
1590
  _transitiveCache.clear(); // reset cache for fresh scan
@@ -1165,19 +1594,20 @@ async function generate(args, projectRoot) {
1165
1594
  const scannedScreens = framework === 'expo-router'
1166
1595
  ? scanExpoRouterApp(projectRoot)
1167
1596
  : scanReactNavigationApp(projectRoot);
1597
+ const filteredScreens = filterScreensByRoutePatterns(scannedScreens, args);
1168
1598
 
1169
- console.log(`📄 Found ${scannedScreens.length} screen(s)`);
1599
+ console.log(`📄 Found ${filteredScreens.length} screen(s)`);
1170
1600
 
1171
1601
  // Enrich navigation links with component-level navigation
1172
1602
  console.log('🔍 Scanning components for navigation calls...');
1173
1603
  const globalIndex = buildGlobalNavigateIndex(projectRoot);
1174
1604
  const navigatorFiles = framework === 'react-navigation' ? findNavigatorFiles(projectRoot) : [];
1175
- const added = enrichScreensWithComponentNavLinks(scannedScreens, globalIndex, navigatorFiles);
1605
+ const added = enrichScreensWithComponentNavLinks(filteredScreens, globalIndex, navigatorFiles);
1176
1606
  console.log(` Found ${globalIndex.size} component(s) with navigation, added ${added} link(s)`);
1177
1607
 
1178
1608
 
1179
1609
  // Build output — per-screen navigatesTo, filtered to known routes only
1180
- const allRouteSet = new Set(scannedScreens.map(s => s.routeName));
1610
+ const allRouteSet = new Set(filteredScreens.map(s => s.routeName));
1181
1611
 
1182
1612
  // Build basename lookup for fuzzy matching (e.g. "Chat" → "screens/Chat")
1183
1613
  const basenameMap = new Map(); // basename → full route (shortest path wins)
@@ -1201,7 +1631,7 @@ async function generate(args, projectRoot) {
1201
1631
  }
1202
1632
 
1203
1633
  const screenMap = { generatedAt: new Date().toISOString(), framework, screens: {} };
1204
- for (const screen of scannedScreens) {
1634
+ for (const screen of filteredScreens) {
1205
1635
  const validLinks = screen.navigationLinks.map(link => resolveNavLink(link)).filter(Boolean);
1206
1636
  // Deduplicate (two different raw links may resolve to the same route)
1207
1637
  const uniqueLinks = [...new Set(validLinks)];
@@ -1219,7 +1649,7 @@ async function generate(args, projectRoot) {
1219
1649
  const outputPath = path.join(projectRoot, 'ai-screen-map.json');
1220
1650
  fs.writeFileSync(outputPath, JSON.stringify(screenMap, null, 2));
1221
1651
 
1222
- const linkedCount = scannedScreens.filter(s => s.navigationLinks.map(l => resolveNavLink(l)).some(Boolean)).length;
1652
+ const linkedCount = filteredScreens.filter(s => s.navigationLinks.map(l => resolveNavLink(l)).some(Boolean)).length;
1223
1653
  console.log('━'.repeat(40));
1224
1654
  console.log(`✅ Generated ${outputPath}`);
1225
1655
  console.log(` ${Object.keys(screenMap.screens).length} screens, ${linkedCount} with navigation links`);