@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.
- package/README.md +28 -15
- package/android/build.gradle +17 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
- package/bin/generate-map.cjs +556 -126
- package/ios/Podfile +63 -0
- package/ios/Podfile.lock +2290 -0
- package/ios/Podfile.properties.json +4 -0
- package/ios/mobileaireactnative/AppDelegate.swift +69 -0
- package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
- package/ios/mobileaireactnative/Info.plist +55 -0
- package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
- package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
- package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
- package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
- package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
- package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
- package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
- package/lib/module/components/AIAgent.js +407 -148
- package/lib/module/components/AgentChatBar.js +253 -62
- package/lib/module/components/FloatingOverlayWrapper.js +68 -32
- package/lib/module/config/endpoints.js +22 -1
- package/lib/module/core/AgentRuntime.js +192 -24
- package/lib/module/core/FiberTreeWalker.js +410 -34
- package/lib/module/core/OutcomeVerifier.js +149 -0
- package/lib/module/core/systemPrompt.js +126 -44
- package/lib/module/providers/GeminiProvider.js +9 -3
- package/lib/module/services/MobileAIKnowledgeRetriever.js +1 -1
- package/lib/module/services/telemetry/MobileAI.js +1 -1
- package/lib/module/services/telemetry/TelemetryService.js +21 -2
- package/lib/module/services/telemetry/TouchAutoCapture.js +45 -35
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/lib/module/support/supportPrompt.js +22 -7
- package/lib/module/support/supportStyle.js +55 -0
- package/lib/module/support/types.js +2 -0
- package/lib/module/tools/tapTool.js +77 -6
- package/lib/module/tools/typeTool.js +20 -0
- package/lib/module/utils/humanizeScreenName.js +49 -0
- package/lib/typescript/src/components/AIAgent.d.ts +6 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
- package/lib/typescript/src/config/endpoints.d.ts +4 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
- package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
- package/lib/typescript/src/core/types.d.ts +37 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +1 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
- package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +1 -0
- package/lib/typescript/src/support/supportStyle.d.ts +9 -0
- package/lib/typescript/src/support/types.d.ts +3 -0
- package/lib/typescript/src/tools/tapTool.d.ts +3 -2
- package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
- package/lib/typescript/test-tree.d.ts +2 -0
- package/package.json +5 -2
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
- package/ios/MobileAIPilotIntents.swift +0 -51
package/bin/generate-map.cjs
CHANGED
|
@@ -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
|
|
129
|
-
|
|
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 =
|
|
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
|
|
566
|
-
return
|
|
581
|
+
if (!t.isJSXElement(jsxElement)) return [];
|
|
582
|
+
return extractTextCandidatesRecursive(jsxElement, pressablePath.scope);
|
|
567
583
|
}
|
|
568
584
|
|
|
569
|
-
function
|
|
570
|
-
if (depth > 4) return
|
|
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
|
|
574
|
-
if (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
|
-
|
|
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
|
-
|
|
585
|
-
if (text) return text;
|
|
600
|
+
labels.push(...extractTextCandidatesRecursive(child, scope, depth + 1));
|
|
586
601
|
}
|
|
587
602
|
}
|
|
588
|
-
return
|
|
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
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
990
|
-
if (
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1216
|
+
|
|
1217
|
+
if (info.hasLocalDefaultExport) return null;
|
|
1010
1218
|
}
|
|
1011
1219
|
|
|
1012
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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 (!
|
|
1286
|
+
if (!importName) return null;
|
|
1060
1287
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
if (!ref) return null;
|
|
1288
|
+
const importPath = imports.get(importName);
|
|
1289
|
+
if (!importPath) return null;
|
|
1064
1290
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1314
|
+
|
|
1315
|
+
if (tsConfig.baseUrl) {
|
|
1316
|
+
const resolved = resolveFilePath(tsConfig.baseUrl, importPath);
|
|
1317
|
+
if (resolved) return resolved;
|
|
1073
1318
|
}
|
|
1074
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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 ${
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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`);
|