@mobileai/react-native 0.9.17 → 0.9.19
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/LICENSE +28 -20
- package/MobileAIFloatingOverlay.podspec +25 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +151 -0
- package/android/src/main/java/com/mobileai/overlay/MobileAIOverlayPackage.kt +23 -0
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +45 -0
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +29 -0
- package/ios/MobileAIFloatingOverlayComponentView.mm +73 -0
- package/lib/module/components/AIAgent.js +902 -136
- package/lib/module/components/AIConsentDialog.js +439 -0
- package/lib/module/components/AgentChatBar.js +828 -134
- package/lib/module/components/AgentOverlay.js +2 -1
- package/lib/module/components/DiscoveryTooltip.js +21 -9
- package/lib/module/components/FloatingOverlayWrapper.js +108 -0
- package/lib/module/components/Icons.js +123 -0
- package/lib/module/config/endpoints.js +12 -2
- package/lib/module/core/AgentRuntime.js +373 -27
- package/lib/module/core/FiberAdapter.js +56 -0
- package/lib/module/core/FiberTreeWalker.js +186 -80
- package/lib/module/core/IdleDetector.js +19 -0
- package/lib/module/core/NativeAlertInterceptor.js +191 -0
- package/lib/module/core/systemPrompt.js +203 -45
- package/lib/module/index.js +3 -0
- package/lib/module/providers/GeminiProvider.js +72 -56
- package/lib/module/providers/ProviderFactory.js +6 -2
- package/lib/module/services/AudioInputService.js +3 -12
- package/lib/module/services/AudioOutputService.js +1 -13
- package/lib/module/services/ConversationService.js +166 -0
- package/lib/module/services/MobileAIKnowledgeRetriever.js +41 -0
- package/lib/module/services/VoiceService.js +29 -8
- package/lib/module/services/telemetry/MobileAI.js +44 -0
- package/lib/module/services/telemetry/TelemetryService.js +13 -1
- package/lib/module/services/telemetry/TouchAutoCapture.js +44 -18
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +19 -0
- package/lib/module/support/CSATSurvey.js +95 -12
- package/lib/module/support/EscalationSocket.js +70 -1
- package/lib/module/support/ReportedIssueEventSource.js +148 -0
- package/lib/module/support/escalateTool.js +4 -2
- package/lib/module/support/index.js +1 -0
- package/lib/module/support/reportIssueTool.js +127 -0
- package/lib/module/support/supportPrompt.js +77 -9
- package/lib/module/tools/guideTool.js +2 -1
- package/lib/module/tools/longPressTool.js +4 -3
- package/lib/module/tools/pickerTool.js +6 -4
- package/lib/module/tools/tapTool.js +12 -3
- package/lib/module/tools/typeTool.js +19 -10
- package/lib/module/utils/logger.js +175 -6
- package/lib/typescript/react-native.config.d.ts +11 -0
- package/lib/typescript/src/components/AIAgent.d.ts +28 -2
- package/lib/typescript/src/components/AIConsentDialog.d.ts +153 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -2
- package/lib/typescript/src/components/DiscoveryTooltip.d.ts +3 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +51 -0
- package/lib/typescript/src/components/Icons.d.ts +8 -0
- package/lib/typescript/src/config/endpoints.d.ts +5 -3
- package/lib/typescript/src/core/AgentRuntime.d.ts +4 -0
- package/lib/typescript/src/core/FiberAdapter.d.ts +25 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +2 -0
- package/lib/typescript/src/core/IdleDetector.d.ts +11 -0
- package/lib/typescript/src/core/NativeAlertInterceptor.d.ts +55 -0
- package/lib/typescript/src/core/types.d.ts +106 -1
- package/lib/typescript/src/index.d.ts +9 -4
- package/lib/typescript/src/providers/GeminiProvider.d.ts +6 -5
- package/lib/typescript/src/services/ConversationService.d.ts +55 -0
- package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +9 -0
- package/lib/typescript/src/services/telemetry/MobileAI.d.ts +7 -0
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +1 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +9 -6
- package/lib/typescript/src/services/telemetry/types.d.ts +3 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +17 -0
- package/lib/typescript/src/support/EscalationSocket.d.ts +17 -0
- package/lib/typescript/src/support/ReportedIssueEventSource.d.ts +24 -0
- package/lib/typescript/src/support/escalateTool.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +2 -1
- package/lib/typescript/src/support/reportIssueTool.d.ts +20 -0
- package/lib/typescript/src/support/types.d.ts +56 -1
- package/lib/typescript/src/utils/logger.d.ts +15 -0
- package/package.json +20 -9
- package/react-native.config.js +12 -0
- package/lib/module/__cli_tmp__.js.map +0 -1
- package/lib/module/components/AIAgent.js.map +0 -1
- package/lib/module/components/AIZone.js.map +0 -1
- package/lib/module/components/AgentChatBar.js.map +0 -1
- package/lib/module/components/AgentErrorBoundary.js.map +0 -1
- package/lib/module/components/AgentOverlay.js.map +0 -1
- package/lib/module/components/DiscoveryTooltip.js.map +0 -1
- package/lib/module/components/HighlightOverlay.js.map +0 -1
- package/lib/module/components/Icons.js.map +0 -1
- package/lib/module/components/ProactiveHint.js.map +0 -1
- package/lib/module/components/cards/InfoCard.js.map +0 -1
- package/lib/module/components/cards/ReviewSummary.js.map +0 -1
- package/lib/module/config/endpoints.js.map +0 -1
- package/lib/module/core/ActionRegistry.js.map +0 -1
- package/lib/module/core/AgentRuntime.js.map +0 -1
- package/lib/module/core/FiberTreeWalker.js.map +0 -1
- package/lib/module/core/IdleDetector.js.map +0 -1
- package/lib/module/core/MCPBridge.js.map +0 -1
- package/lib/module/core/ScreenDehydrator.js.map +0 -1
- package/lib/module/core/ZoneRegistry.js.map +0 -1
- package/lib/module/core/systemPrompt.js.map +0 -1
- package/lib/module/core/types.js.map +0 -1
- package/lib/module/hooks/useAction.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/plugin/withAppIntents.js.map +0 -1
- package/lib/module/providers/GeminiProvider.js.map +0 -1
- package/lib/module/providers/OpenAIProvider.js.map +0 -1
- package/lib/module/providers/ProviderFactory.js.map +0 -1
- package/lib/module/services/AudioInputService.js.map +0 -1
- package/lib/module/services/AudioOutputService.js.map +0 -1
- package/lib/module/services/KnowledgeBaseService.js.map +0 -1
- package/lib/module/services/VoiceService.js.map +0 -1
- package/lib/module/services/flags/FlagService.js.map +0 -1
- package/lib/module/services/telemetry/MobileAI.js.map +0 -1
- package/lib/module/services/telemetry/PiiScrubber.js.map +0 -1
- package/lib/module/services/telemetry/TelemetryService.js.map +0 -1
- package/lib/module/services/telemetry/TouchAutoCapture.js.map +0 -1
- package/lib/module/services/telemetry/device.js.map +0 -1
- package/lib/module/services/telemetry/deviceMetadata.js.map +0 -1
- package/lib/module/services/telemetry/index.js.map +0 -1
- package/lib/module/services/telemetry/types.js.map +0 -1
- package/lib/module/support/CSATSurvey.js.map +0 -1
- package/lib/module/support/EscalationEventSource.js.map +0 -1
- package/lib/module/support/EscalationSocket.js.map +0 -1
- package/lib/module/support/SupportChatModal.js.map +0 -1
- package/lib/module/support/SupportGreeting.js.map +0 -1
- package/lib/module/support/TicketStore.js.map +0 -1
- package/lib/module/support/escalateTool.js.map +0 -1
- package/lib/module/support/index.js.map +0 -1
- package/lib/module/support/supportPrompt.js.map +0 -1
- package/lib/module/support/types.js.map +0 -1
- package/lib/module/tools/datePickerTool.js.map +0 -1
- package/lib/module/tools/guideTool.js.map +0 -1
- package/lib/module/tools/index.js.map +0 -1
- package/lib/module/tools/keyboardTool.js.map +0 -1
- package/lib/module/tools/longPressTool.js.map +0 -1
- package/lib/module/tools/pickerTool.js.map +0 -1
- package/lib/module/tools/restoreTool.js.map +0 -1
- package/lib/module/tools/scrollTool.js.map +0 -1
- package/lib/module/tools/simplifyTool.js.map +0 -1
- package/lib/module/tools/sliderTool.js.map +0 -1
- package/lib/module/tools/tapTool.js.map +0 -1
- package/lib/module/tools/typeTool.js.map +0 -1
- package/lib/module/tools/types.js.map +0 -1
- package/lib/module/types/jsx.d.js.map +0 -1
- package/lib/module/utils/audioUtils.js.map +0 -1
- package/lib/module/utils/logger.js.map +0 -1
- package/lib/typescript/babel.config.d.ts.map +0 -1
- package/lib/typescript/bin/generate-map.d.cts.map +0 -1
- package/lib/typescript/eslint.config.d.mts.map +0 -1
- package/lib/typescript/generate-map.d.ts.map +0 -1
- package/lib/typescript/src/__cli_tmp__.d.ts.map +0 -1
- package/lib/typescript/src/components/AIAgent.d.ts.map +0 -1
- package/lib/typescript/src/components/AIZone.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentErrorBoundary.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentOverlay.d.ts.map +0 -1
- package/lib/typescript/src/components/DiscoveryTooltip.d.ts.map +0 -1
- package/lib/typescript/src/components/HighlightOverlay.d.ts.map +0 -1
- package/lib/typescript/src/components/Icons.d.ts.map +0 -1
- package/lib/typescript/src/components/ProactiveHint.d.ts.map +0 -1
- package/lib/typescript/src/components/cards/InfoCard.d.ts.map +0 -1
- package/lib/typescript/src/components/cards/ReviewSummary.d.ts.map +0 -1
- package/lib/typescript/src/config/endpoints.d.ts.map +0 -1
- package/lib/typescript/src/core/ActionRegistry.d.ts.map +0 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +0 -1
- package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +0 -1
- package/lib/typescript/src/core/IdleDetector.d.ts.map +0 -1
- package/lib/typescript/src/core/MCPBridge.d.ts.map +0 -1
- package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +0 -1
- package/lib/typescript/src/core/ZoneRegistry.d.ts.map +0 -1
- package/lib/typescript/src/core/systemPrompt.d.ts.map +0 -1
- package/lib/typescript/src/core/types.d.ts.map +0 -1
- package/lib/typescript/src/hooks/useAction.d.ts.map +0 -1
- package/lib/typescript/src/index.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withAppIntents.d.ts.map +0 -1
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +0 -1
- package/lib/typescript/src/providers/OpenAIProvider.d.ts.map +0 -1
- package/lib/typescript/src/providers/ProviderFactory.d.ts.map +0 -1
- package/lib/typescript/src/services/AudioInputService.d.ts.map +0 -1
- package/lib/typescript/src/services/AudioOutputService.d.ts.map +0 -1
- package/lib/typescript/src/services/KnowledgeBaseService.d.ts.map +0 -1
- package/lib/typescript/src/services/VoiceService.d.ts.map +0 -1
- package/lib/typescript/src/services/flags/FlagService.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/MobileAI.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/PiiScrubber.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/device.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/index.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/types.d.ts.map +0 -1
- package/lib/typescript/src/support/CSATSurvey.d.ts.map +0 -1
- package/lib/typescript/src/support/EscalationEventSource.d.ts.map +0 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts.map +0 -1
- package/lib/typescript/src/support/SupportChatModal.d.ts.map +0 -1
- package/lib/typescript/src/support/SupportGreeting.d.ts.map +0 -1
- package/lib/typescript/src/support/TicketStore.d.ts.map +0 -1
- package/lib/typescript/src/support/escalateTool.d.ts.map +0 -1
- package/lib/typescript/src/support/index.d.ts.map +0 -1
- package/lib/typescript/src/support/supportPrompt.d.ts.map +0 -1
- package/lib/typescript/src/support/types.d.ts.map +0 -1
- package/lib/typescript/src/tools/datePickerTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/guideTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/index.d.ts.map +0 -1
- package/lib/typescript/src/tools/keyboardTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/longPressTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/pickerTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/restoreTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/scrollTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/simplifyTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/sliderTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/tapTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/typeTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/types.d.ts.map +0 -1
- package/lib/typescript/src/utils/audioUtils.d.ts.map +0 -1
- package/lib/typescript/src/utils/logger.d.ts.map +0 -1
- package/src/__cli_tmp__.tsx +0 -9
- package/src/cli/analyzers/chain-analyzer.ts +0 -183
- package/src/cli/extractors/ai-extractor.ts +0 -6
- package/src/cli/extractors/ast-extractor.ts +0 -551
- package/src/cli/generate-intents.ts +0 -140
- package/src/cli/generate-map.ts +0 -121
- package/src/cli/generate-swift.ts +0 -116
- package/src/cli/scanners/expo-scanner.ts +0 -203
- package/src/cli/scanners/rn-scanner.ts +0 -445
- package/src/components/AIAgent.tsx +0 -1716
- package/src/components/AIZone.tsx +0 -147
- package/src/components/AgentChatBar.tsx +0 -1143
- package/src/components/AgentErrorBoundary.tsx +0 -78
- package/src/components/AgentOverlay.tsx +0 -73
- package/src/components/DiscoveryTooltip.tsx +0 -148
- package/src/components/HighlightOverlay.tsx +0 -136
- package/src/components/Icons.tsx +0 -253
- package/src/components/ProactiveHint.tsx +0 -145
- package/src/components/cards/InfoCard.tsx +0 -58
- package/src/components/cards/ReviewSummary.tsx +0 -76
- package/src/config/endpoints.ts +0 -22
- package/src/core/ActionRegistry.ts +0 -105
- package/src/core/AgentRuntime.ts +0 -1471
- package/src/core/FiberTreeWalker.ts +0 -930
- package/src/core/IdleDetector.ts +0 -72
- package/src/core/MCPBridge.ts +0 -163
- package/src/core/ScreenDehydrator.ts +0 -53
- package/src/core/ZoneRegistry.ts +0 -44
- package/src/core/systemPrompt.ts +0 -431
- package/src/core/types.ts +0 -521
- package/src/hooks/useAction.ts +0 -182
- package/src/index.ts +0 -83
- package/src/plugin/withAppIntents.ts +0 -98
- package/src/providers/GeminiProvider.ts +0 -357
- package/src/providers/OpenAIProvider.ts +0 -379
- package/src/providers/ProviderFactory.ts +0 -36
- package/src/services/AudioInputService.ts +0 -226
- package/src/services/AudioOutputService.ts +0 -236
- package/src/services/KnowledgeBaseService.ts +0 -156
- package/src/services/VoiceService.ts +0 -451
- package/src/services/flags/FlagService.ts +0 -137
- package/src/services/telemetry/MobileAI.ts +0 -66
- package/src/services/telemetry/PiiScrubber.ts +0 -17
- package/src/services/telemetry/TelemetryService.ts +0 -323
- package/src/services/telemetry/TouchAutoCapture.ts +0 -165
- package/src/services/telemetry/device.ts +0 -93
- package/src/services/telemetry/deviceMetadata.ts +0 -13
- package/src/services/telemetry/index.ts +0 -13
- package/src/services/telemetry/types.ts +0 -75
- package/src/support/CSATSurvey.tsx +0 -304
- package/src/support/EscalationEventSource.ts +0 -190
- package/src/support/EscalationSocket.ts +0 -152
- package/src/support/SupportChatModal.tsx +0 -563
- package/src/support/SupportGreeting.tsx +0 -161
- package/src/support/TicketStore.ts +0 -100
- package/src/support/escalateTool.ts +0 -174
- package/src/support/index.ts +0 -29
- package/src/support/supportPrompt.ts +0 -55
- package/src/support/types.ts +0 -155
- package/src/tools/datePickerTool.ts +0 -60
- package/src/tools/guideTool.ts +0 -76
- package/src/tools/index.ts +0 -20
- package/src/tools/keyboardTool.ts +0 -30
- package/src/tools/longPressTool.ts +0 -61
- package/src/tools/pickerTool.ts +0 -115
- package/src/tools/restoreTool.ts +0 -33
- package/src/tools/scrollTool.ts +0 -156
- package/src/tools/simplifyTool.ts +0 -33
- package/src/tools/sliderTool.ts +0 -65
- package/src/tools/tapTool.ts +0 -93
- package/src/tools/typeTool.ts +0 -113
- package/src/tools/types.ts +0 -58
- package/src/types/jsx.d.ts +0 -20
- package/src/utils/audioUtils.ts +0 -54
- package/src/utils/logger.ts +0 -38
|
@@ -1,551 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AST-based content extractor for React Native screen files.
|
|
3
|
-
* Parses JSX to extract interactive elements, text labels, and navigation links.
|
|
4
|
-
*
|
|
5
|
-
* Supports comprehensive component detection via category-based classification:
|
|
6
|
-
* - Inputs: TextInput, *Input
|
|
7
|
-
* - Toggles: Switch, *Toggle
|
|
8
|
-
* - Buttons: Button, Pressable, TouchableOpacity, *Button, *Btn
|
|
9
|
-
* - Images: Image, FastImage, *Avatar, *Image
|
|
10
|
-
* - Lists: FlatList, SectionList, ScrollView, VirtualizedList
|
|
11
|
-
* - Modals: Modal, *Modal, *Sheet, *Dialog, *BottomSheet
|
|
12
|
-
* - Icons: Ionicons, MaterialIcon, FontAwesome, *_Dark, *_Light (skipped — noise)
|
|
13
|
-
* - Custom: Any PascalCase component not matched above
|
|
14
|
-
*
|
|
15
|
-
* Also captures navigation links from:
|
|
16
|
-
* - Expo Router: <Link href="..." />, router.navigate/push/replace
|
|
17
|
-
* - React Navigation: navigation.navigate/push/replace, <Link screen="..." />
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { parse } from '@babel/parser';
|
|
21
|
-
import * as _traverse from '@babel/traverse';
|
|
22
|
-
import * as t from '@babel/types';
|
|
23
|
-
|
|
24
|
-
const traverse = (_traverse as any).default || _traverse;
|
|
25
|
-
|
|
26
|
-
export interface ExtractedContent {
|
|
27
|
-
elements: string[];
|
|
28
|
-
navigationLinks: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─── Component classification ────────────────────────────────
|
|
32
|
-
|
|
33
|
-
/** RN layout primitives we always skip (they carry no semantic meaning) */
|
|
34
|
-
const LAYOUT_PRIMITIVES = new Set([
|
|
35
|
-
'View', 'SafeAreaView', 'Fragment', 'KeyboardAvoidingView',
|
|
36
|
-
'StatusBar', 'LinearGradient', 'Animated',
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
/** Exact-match icon component names */
|
|
40
|
-
const ICON_EXACT = new Set([
|
|
41
|
-
'Ionicons', 'MaterialIcon', 'MaterialCommunityIcons',
|
|
42
|
-
'FontAwesome', 'FontAwesome5', 'Feather', 'Entypo',
|
|
43
|
-
'AntDesign', 'EvilIcons', 'Foundation', 'Octicons',
|
|
44
|
-
'SimpleLineIcons', 'Zocial', 'MaterialIcons',
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
/** Exact-match pressable component names */
|
|
48
|
-
const PRESSABLE_EXACT = new Set([
|
|
49
|
-
'Pressable', 'TouchableOpacity', 'TouchableHighlight',
|
|
50
|
-
'TouchableWithoutFeedback', 'TouchableNativeFeedback',
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
/** Exact-match list component names */
|
|
54
|
-
const LIST_EXACT = new Set([
|
|
55
|
-
'FlatList', 'SectionList', 'VirtualizedList',
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
/** Exact-match image component names */
|
|
59
|
-
const IMAGE_EXACT = new Set([
|
|
60
|
-
'Image', 'FastImage', 'ImageBackground',
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
type ComponentCategory =
|
|
64
|
-
| 'input' | 'toggle' | 'button' | 'image'
|
|
65
|
-
| 'list' | 'modal' | 'icon' | 'navigation'
|
|
66
|
-
| 'custom' | 'skip';
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Classify a JSX element name into a semantic category.
|
|
70
|
-
* Order matters: more specific checks first, custom catch-all last.
|
|
71
|
-
*/
|
|
72
|
-
function classifyComponent(name: string): ComponentCategory {
|
|
73
|
-
// Skip layout primitives
|
|
74
|
-
if (LAYOUT_PRIMITIVES.has(name)) return 'skip';
|
|
75
|
-
// Skip RN text wrappers (we extract text content, not the wrapper)
|
|
76
|
-
if (name === 'Text' || name === 'ScrollView') return 'skip';
|
|
77
|
-
|
|
78
|
-
// Exact matches
|
|
79
|
-
if (name === 'TextInput') return 'input';
|
|
80
|
-
if (name === 'Switch') return 'toggle';
|
|
81
|
-
if (name === 'Button') return 'button';
|
|
82
|
-
if (name === 'Modal') return 'modal';
|
|
83
|
-
if (name === 'Link' || name === 'Redirect') return 'navigation';
|
|
84
|
-
if (PRESSABLE_EXACT.has(name)) return 'button';
|
|
85
|
-
if (LIST_EXACT.has(name)) return 'list';
|
|
86
|
-
if (IMAGE_EXACT.has(name)) return 'image';
|
|
87
|
-
if (ICON_EXACT.has(name)) return 'icon';
|
|
88
|
-
|
|
89
|
-
// Pattern matches (suffix/contains)
|
|
90
|
-
if (name.endsWith('Input') || name.endsWith('Field')) return 'input';
|
|
91
|
-
if (name.endsWith('Toggle')) return 'toggle';
|
|
92
|
-
if (name.endsWith('Button') || name.endsWith('Btn')) return 'button';
|
|
93
|
-
if (name.endsWith('Image') || name.endsWith('Avatar') || name.endsWith('Photo')) return 'image';
|
|
94
|
-
if (name.endsWith('List')) return 'list';
|
|
95
|
-
if (name.endsWith('Modal') || name.endsWith('Sheet') || name.endsWith('Dialog') || name.includes('BottomSheet')) return 'modal';
|
|
96
|
-
|
|
97
|
-
// Icon patterns: ends with Icon, or SVG asset convention (*_Dark, *_Light)
|
|
98
|
-
if (name.endsWith('Icon') || name.endsWith('_Dark') || name.endsWith('_Light')) return 'icon';
|
|
99
|
-
|
|
100
|
-
// If it's PascalCase (starts with uppercase) and not a known primitive, it's a custom component
|
|
101
|
-
if (name[0] === name[0]?.toUpperCase() && name[0] !== name[0]?.toLowerCase()) {
|
|
102
|
-
return 'custom';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return 'skip';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ─── Core extraction ──────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Extract interactive elements and navigation links from a screen file's source code.
|
|
112
|
-
*/
|
|
113
|
-
export function extractContentFromAST(sourceCode: string, filePath: string): ExtractedContent {
|
|
114
|
-
const elements: string[] = [];
|
|
115
|
-
const navigationLinks: string[] = [];
|
|
116
|
-
|
|
117
|
-
let ast: ReturnType<typeof parse>;
|
|
118
|
-
try {
|
|
119
|
-
ast = parse(sourceCode, {
|
|
120
|
-
sourceType: 'module',
|
|
121
|
-
plugins: ['jsx', 'typescript', 'decorators-legacy'],
|
|
122
|
-
});
|
|
123
|
-
} catch {
|
|
124
|
-
console.warn(`[generate-map] Failed to parse ${filePath}, skipping AST extraction`);
|
|
125
|
-
return { elements, navigationLinks };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
traverse(ast, {
|
|
129
|
-
JSXOpeningElement(path: any) {
|
|
130
|
-
const nameNode = path.node.name;
|
|
131
|
-
const elementName = getJSXElementName(nameNode);
|
|
132
|
-
if (!elementName) return;
|
|
133
|
-
|
|
134
|
-
const category = classifyComponent(elementName);
|
|
135
|
-
|
|
136
|
-
switch (category) {
|
|
137
|
-
case 'input': {
|
|
138
|
-
const placeholder = getStringAttribute(path.node, 'placeholder');
|
|
139
|
-
elements.push(placeholder ? `${placeholder} (text-input)` : 'text input (text-input)');
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
case 'toggle': {
|
|
144
|
-
const label = findSiblingTextLabel(path);
|
|
145
|
-
elements.push(label ? `${label} (switch)` : 'toggle (switch)');
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
case 'button': {
|
|
150
|
-
// RN built-in Button uses `title` prop
|
|
151
|
-
if (elementName === 'Button') {
|
|
152
|
-
const title = getStringAttribute(path.node, 'title');
|
|
153
|
-
elements.push(title ? `${title} (button)` : 'button (button)');
|
|
154
|
-
|
|
155
|
-
// React Navigation: <Button screen="Details" />
|
|
156
|
-
const screenTarget = getStringAttribute(path.node, 'screen');
|
|
157
|
-
if (screenTarget) navigationLinks.push(screenTarget);
|
|
158
|
-
} else {
|
|
159
|
-
// Pressable/TouchableOpacity — find text label in children
|
|
160
|
-
const buttonLabel = findChildTextContentRecursive(path);
|
|
161
|
-
if (buttonLabel) {
|
|
162
|
-
elements.push(`${buttonLabel} (button)`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
case 'image': {
|
|
169
|
-
const alt = getStringAttribute(path.node, 'alt')
|
|
170
|
-
|| getStringAttribute(path.node, 'accessibilityLabel');
|
|
171
|
-
elements.push(alt ? `${alt} (image)` : `${elementName} (image)`);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
case 'list': {
|
|
176
|
-
elements.push(`${elementName} (list)`);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
case 'modal': {
|
|
181
|
-
const title = getStringAttribute(path.node, 'title');
|
|
182
|
-
elements.push(title ? `${title} (modal)` : `${elementName} (modal)`);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
case 'navigation': {
|
|
187
|
-
// Expo Router: <Link href="..." /> or <Redirect href="..." />
|
|
188
|
-
const target = extractRouteFromAttribute(path.node, 'href');
|
|
189
|
-
if (target) navigationLinks.push(target);
|
|
190
|
-
// React Navigation: <Link screen="Details" />
|
|
191
|
-
const screenTarget = getStringAttribute(path.node, 'screen');
|
|
192
|
-
if (screenTarget) navigationLinks.push(screenTarget);
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
case 'custom': {
|
|
197
|
-
// Extract a meaningful label from common props
|
|
198
|
-
const label = getStringAttribute(path.node, 'title')
|
|
199
|
-
|| getStringAttribute(path.node, 'label')
|
|
200
|
-
|| getStringAttribute(path.node, 'placeholder')
|
|
201
|
-
|| getStringAttribute(path.node, 'text');
|
|
202
|
-
elements.push(label ? `${label} (${elementName})` : `${elementName} (component)`);
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
case 'icon':
|
|
207
|
-
case 'skip':
|
|
208
|
-
// Intentionally ignored
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
|
|
213
|
-
// ─── Navigation link extraction from imperative calls ─────
|
|
214
|
-
CallExpression(path: any) {
|
|
215
|
-
const target = extractRouteFromCall(path.node);
|
|
216
|
-
if (Array.isArray(target)) navigationLinks.push(...target);
|
|
217
|
-
else if (target) navigationLinks.push(target);
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
elements: deduplicateAndPrioritize(elements),
|
|
223
|
-
navigationLinks: [...new Set(navigationLinks)],
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Build a description string from extracted content.
|
|
229
|
-
*/
|
|
230
|
-
export function buildDescription(extracted: ExtractedContent): string {
|
|
231
|
-
const parts: string[] = [];
|
|
232
|
-
if (extracted.elements.length > 0) {
|
|
233
|
-
parts.push(extracted.elements.join(', '));
|
|
234
|
-
}
|
|
235
|
-
return parts.join('. ') || 'Screen content';
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ─── Deduplication & prioritization ──────────────────────────
|
|
239
|
-
|
|
240
|
-
/** Priority order for element categories (lower = higher priority) */
|
|
241
|
-
const CATEGORY_PRIORITY: Record<string, number> = {
|
|
242
|
-
'text-input': 0,
|
|
243
|
-
'switch': 1,
|
|
244
|
-
'button': 2,
|
|
245
|
-
'modal': 3,
|
|
246
|
-
'list': 4,
|
|
247
|
-
'image': 5,
|
|
248
|
-
'component': 6,
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const MAX_ELEMENTS = 8;
|
|
252
|
-
|
|
253
|
-
function deduplicateAndPrioritize(elements: string[]): string[] {
|
|
254
|
-
// Deduplicate
|
|
255
|
-
const unique = [...new Set(elements)];
|
|
256
|
-
|
|
257
|
-
// Sort by category priority (interactive first)
|
|
258
|
-
unique.sort((a, b) => {
|
|
259
|
-
const catA = extractCategoryTag(a);
|
|
260
|
-
const catB = extractCategoryTag(b);
|
|
261
|
-
return (CATEGORY_PRIORITY[catA] ?? 99) - (CATEGORY_PRIORITY[catB] ?? 99);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Cap to avoid overly long descriptions
|
|
265
|
-
return unique.slice(0, MAX_ELEMENTS);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function extractCategoryTag(element: string): string {
|
|
269
|
-
const match = element.match(/\(([^)]+)\)$/);
|
|
270
|
-
return match ? match[1]! : 'unknown';
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ─── Route extraction helpers ─────────────────────────────────
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Extract a route target from a JSX attribute that can be:
|
|
277
|
-
* - String literal: href="/path"
|
|
278
|
-
* - Template literal: href={`/path/${id}`}
|
|
279
|
-
* - Object with pathname: href={{ pathname: '/path/[id]', params: {...} }}
|
|
280
|
-
*/
|
|
281
|
-
function extractRouteFromAttribute(node: t.JSXOpeningElement, attrName: string): string | null {
|
|
282
|
-
for (const attr of node.attributes) {
|
|
283
|
-
if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name) || attr.name.name !== attrName) {
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// href="/about" — plain string
|
|
288
|
-
if (t.isStringLiteral(attr.value)) {
|
|
289
|
-
return attr.value.value;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// href={expression}
|
|
293
|
-
if (t.isJSXExpressionContainer(attr.value)) {
|
|
294
|
-
return extractRouteFromExpression(attr.value.expression);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Extract a route target from an imperative call expression:
|
|
302
|
-
* router.push('/path') | router.navigate({...})
|
|
303
|
-
* navigation.navigate('Screen') | navigation.push('Screen', params)
|
|
304
|
-
*
|
|
305
|
-
* Methods matched: navigate, push, replace
|
|
306
|
-
*/
|
|
307
|
-
function extractRouteFromCall(node: t.CallExpression): string | string[] | null {
|
|
308
|
-
const callee = node.callee;
|
|
309
|
-
|
|
310
|
-
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
311
|
-
const method = callee.property.name;
|
|
312
|
-
|
|
313
|
-
// navigation.navigate/push/replace('Screen')
|
|
314
|
-
if (['navigate', 'push', 'replace'].includes(method)) {
|
|
315
|
-
const firstArg = node.arguments[0];
|
|
316
|
-
if (!firstArg) return null;
|
|
317
|
-
return extractRouteFromExpression(firstArg);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// navigation.reset({ routes: [{ name: 'Screen' }] })
|
|
321
|
-
if (method === 'reset') {
|
|
322
|
-
const firstArg = node.arguments[0];
|
|
323
|
-
if (t.isObjectExpression(firstArg)) {
|
|
324
|
-
for (const prop of firstArg.properties) {
|
|
325
|
-
if (
|
|
326
|
-
t.isObjectProperty(prop) &&
|
|
327
|
-
t.isIdentifier(prop.key) &&
|
|
328
|
-
prop.key.name === 'routes' &&
|
|
329
|
-
t.isArrayExpression(prop.value)
|
|
330
|
-
) {
|
|
331
|
-
const routes: string[] = [];
|
|
332
|
-
for (const el of prop.value.elements) {
|
|
333
|
-
if (t.isObjectExpression(el)) {
|
|
334
|
-
for (const rp of el.properties) {
|
|
335
|
-
if (t.isObjectProperty(rp) && t.isIdentifier(rp.key) && rp.key.name === 'name') {
|
|
336
|
-
const route = extractRouteFromExpression(rp.value);
|
|
337
|
-
if (route) routes.push(route);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return routes.length > 0 ? routes : null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Extract a route string from any expression type:
|
|
354
|
-
* - StringLiteral: '/path'
|
|
355
|
-
* - TemplateLiteral: `/path/${id}` → '/path/[param]'
|
|
356
|
-
* - ObjectExpression with pathname: { pathname: '/path/[id]' }
|
|
357
|
-
*/
|
|
358
|
-
function extractRouteFromExpression(expr: any): string | null {
|
|
359
|
-
if (!expr) return null;
|
|
360
|
-
|
|
361
|
-
// String literal: '/about' or 'Details'
|
|
362
|
-
if (t.isStringLiteral(expr)) {
|
|
363
|
-
return expr.value;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Template literal: `/item-reviews/${id}` → '/item-reviews/[param]'
|
|
367
|
-
if (t.isTemplateLiteral(expr) && expr.quasis.length > 0) {
|
|
368
|
-
return expr.quasis.map((q: any) => q.value.raw).join('[param]');
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// MemberExpression: StackNav.Register → 'Register'
|
|
372
|
-
if (t.isMemberExpression(expr) && t.isIdentifier(expr.property)) {
|
|
373
|
-
return expr.property.name;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Identifier: navigate(screenName) → '{screenName}'
|
|
377
|
-
if (t.isIdentifier(expr)) {
|
|
378
|
-
return `{${expr.name}}`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Object with pathname: { pathname: '/user/[id]', params: {...} }
|
|
382
|
-
if (t.isObjectExpression(expr)) {
|
|
383
|
-
for (const prop of expr.properties) {
|
|
384
|
-
if (
|
|
385
|
-
t.isObjectProperty(prop) &&
|
|
386
|
-
t.isIdentifier(prop.key) &&
|
|
387
|
-
prop.key.name === 'pathname'
|
|
388
|
-
) {
|
|
389
|
-
if (t.isStringLiteral(prop.value)) {
|
|
390
|
-
return prop.value.value;
|
|
391
|
-
}
|
|
392
|
-
if (t.isTemplateLiteral(prop.value) && prop.value.quasis.length > 0) {
|
|
393
|
-
return prop.value.quasis.map((q: any) => q.value.raw).join('[param]');
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// ─── JSX helpers ──────────────────────────────────────────────
|
|
403
|
-
|
|
404
|
-
function getJSXElementName(nameNode: t.JSXOpeningElement['name']): string {
|
|
405
|
-
if (t.isJSXIdentifier(nameNode)) return nameNode.name;
|
|
406
|
-
if (t.isJSXMemberExpression(nameNode) && t.isJSXIdentifier(nameNode.property)) {
|
|
407
|
-
return nameNode.property.name;
|
|
408
|
-
}
|
|
409
|
-
return '';
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function getStringAttribute(node: t.JSXOpeningElement, attrName: string): string | null {
|
|
413
|
-
for (const attr of node.attributes) {
|
|
414
|
-
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === attrName) {
|
|
415
|
-
if (t.isStringLiteral(attr.value)) return attr.value.value;
|
|
416
|
-
if (t.isJSXExpressionContainer(attr.value)) {
|
|
417
|
-
return extractSemanticHint(attr.value.expression);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Recursively unwrap a dynamic JS expression to extract a semantic label.
|
|
426
|
-
*
|
|
427
|
-
* Handles:
|
|
428
|
-
* StringLiteral → "Full Name"
|
|
429
|
-
* MemberExpression → strings.auth.fullName → "fullName"
|
|
430
|
-
* ConditionalExpression → cond ? a : b → try a, fallback b
|
|
431
|
-
* LogicalExpression (||) → a || b → try a, fallback b
|
|
432
|
-
* TemplateLiteral → `Hello ${name}` → "Hello ..."
|
|
433
|
-
* CallExpression → t('loginBtn') → "loginBtn"
|
|
434
|
-
*/
|
|
435
|
-
function extractSemanticHint(node: any, depth: number = 0): string | null {
|
|
436
|
-
if (depth > 5 || !node) return null;
|
|
437
|
-
|
|
438
|
-
// Direct string literal
|
|
439
|
-
if (t.isStringLiteral(node)) return node.value;
|
|
440
|
-
|
|
441
|
-
// Numeric literal — sometimes used as placeholder (e.g. placeholder={0})
|
|
442
|
-
if (t.isNumericLiteral(node)) return String(node.value);
|
|
443
|
-
|
|
444
|
-
// MemberExpression: strings.fullName, i18n.auth.loginButton → deepest property
|
|
445
|
-
if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
|
|
446
|
-
return camelToWords(node.property.name);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Ternary: condition ? consequent : alternate — try consequent first
|
|
450
|
-
if (t.isConditionalExpression(node)) {
|
|
451
|
-
return extractSemanticHint(node.consequent, depth + 1)
|
|
452
|
-
|| extractSemanticHint(node.alternate, depth + 1);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Logical OR: a || b — try left first
|
|
456
|
-
if (t.isLogicalExpression(node) && node.operator === '||') {
|
|
457
|
-
return extractSemanticHint(node.left, depth + 1)
|
|
458
|
-
|| extractSemanticHint(node.right, depth + 1);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Logical AND: a && b — the right side is the "real" value
|
|
462
|
-
if (t.isLogicalExpression(node) && node.operator === '&&') {
|
|
463
|
-
return extractSemanticHint(node.right, depth + 1);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Template literal: `Hello ${name}` → "Hello ..."
|
|
467
|
-
if (t.isTemplateLiteral(node) && node.quasis.length > 0) {
|
|
468
|
-
const staticParts = node.quasis.map((q: any) => q.value.raw).filter(Boolean);
|
|
469
|
-
if (staticParts.length > 0) return staticParts.join('...').trim() || null;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Call expression: t('key'), i18n.t('loginBtn'), translate('email')
|
|
473
|
-
if (t.isCallExpression(node) && node.arguments.length > 0) {
|
|
474
|
-
const firstArg = node.arguments[0];
|
|
475
|
-
if (t.isStringLiteral(firstArg)) return camelToWords(firstArg.value);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Convert camelCase/PascalCase to readable words: "fullName" → "full Name", "loginButton" → "login Button"
|
|
483
|
-
* Keeps it simple — just inserts spaces before uppercase letters.
|
|
484
|
-
*/
|
|
485
|
-
function camelToWords(str: string): string {
|
|
486
|
-
if (!str) return str;
|
|
487
|
-
// Don't transform if it's already sentence-like or a single word
|
|
488
|
-
if (str.includes(' ') || str.includes('_') || str.includes('-')) return str;
|
|
489
|
-
return str.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Find the nearest sibling <Text> element to determine a label for a Switch/toggle.
|
|
494
|
-
*/
|
|
495
|
-
function findSiblingTextLabel(switchPath: any): string | null {
|
|
496
|
-
const parent = switchPath.parentPath?.parentPath; // Go up to the View containing the Switch
|
|
497
|
-
if (!parent?.node || !t.isJSXElement(parent.node)) return null;
|
|
498
|
-
|
|
499
|
-
for (const child of parent.node.children) {
|
|
500
|
-
if (t.isJSXElement(child)) {
|
|
501
|
-
const text = extractTextRecursive(child);
|
|
502
|
-
if (text) return text;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Recursively find text content inside a Pressable/TouchableOpacity.
|
|
510
|
-
* Searches through nested Views and custom text-like components.
|
|
511
|
-
*/
|
|
512
|
-
function findChildTextContentRecursive(pressablePath: any): string | null {
|
|
513
|
-
const jsxElement = pressablePath.parent;
|
|
514
|
-
if (!t.isJSXElement(jsxElement)) return null;
|
|
515
|
-
return extractTextRecursive(jsxElement);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Recursively extract human-readable text from a JSX tree.
|
|
520
|
-
* Stops at the first meaningful text found to avoid noise.
|
|
521
|
-
*/
|
|
522
|
-
function extractTextRecursive(element: t.JSXElement, depth: number = 0): string | null {
|
|
523
|
-
if (depth > 4) return null; // Safety: don't recurse too deep
|
|
524
|
-
|
|
525
|
-
for (const child of element.children) {
|
|
526
|
-
// Direct text node
|
|
527
|
-
if (t.isJSXText(child)) {
|
|
528
|
-
const text = child.value.trim();
|
|
529
|
-
if (text) return text;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// String or dynamic expression: {"Sign In"}, {strings.editProfile}, {t('key')}
|
|
533
|
-
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
534
|
-
const hint = extractSemanticHint(child.expression);
|
|
535
|
-
if (hint) return hint;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Recurse into child JSX elements (e.g. <View><Text>Sign In</Text></View>)
|
|
539
|
-
if (t.isJSXElement(child)) {
|
|
540
|
-
const childName = getJSXElementName(child.openingElement.name);
|
|
541
|
-
// Skip icons inside buttons
|
|
542
|
-
if (ICON_EXACT.has(childName) || childName.endsWith('Icon') ||
|
|
543
|
-
childName.endsWith('_Dark') || childName.endsWith('_Light')) {
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
const text = extractTextRecursive(child, depth + 1);
|
|
547
|
-
if (text) return text;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { parse } from '@babel/parser';
|
|
6
|
-
import traverse from '@babel/traverse';
|
|
7
|
-
// @ts-ignore - no types installed
|
|
8
|
-
import glob from 'glob';
|
|
9
|
-
|
|
10
|
-
// Define the schema format for our extracted intents
|
|
11
|
-
export interface ExtractedIntent {
|
|
12
|
-
name: string;
|
|
13
|
-
description: string;
|
|
14
|
-
parameters: Record<string, any>;
|
|
15
|
-
sourceFile: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Validates and statically extracts `useAction` and `registerAction` definitions
|
|
20
|
-
* from a target directory by parsing the AST of all TS/JS files.
|
|
21
|
-
*/
|
|
22
|
-
export function extractIntentsFromAST(sourceDir: string): ExtractedIntent[] {
|
|
23
|
-
const files = glob.sync(`${sourceDir}/**/*.{ts,tsx,js,jsx}`, {
|
|
24
|
-
ignore: ['**/node_modules/**', '**/*.d.ts', '**/__tests__/**']
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const intents: ExtractedIntent[] = [];
|
|
28
|
-
|
|
29
|
-
for (const file of files) {
|
|
30
|
-
const code = fs.readFileSync(file, 'utf-8');
|
|
31
|
-
|
|
32
|
-
// Quick heuristic: ignore files that don't even talk about useAction
|
|
33
|
-
if (!code.includes('useAction') && !code.includes('registerAction')) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const ast = parse(code, {
|
|
39
|
-
sourceType: 'module',
|
|
40
|
-
plugins: ['jsx', 'typescript'],
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
traverse(ast, {
|
|
44
|
-
CallExpression(pathNode: any) {
|
|
45
|
-
const callee = pathNode.node.callee;
|
|
46
|
-
if (
|
|
47
|
-
callee.type === 'Identifier' &&
|
|
48
|
-
(callee.name === 'useAction' || callee.name === 'registerAction')
|
|
49
|
-
) {
|
|
50
|
-
const args = pathNode.node.arguments;
|
|
51
|
-
if (args.length >= 2) {
|
|
52
|
-
const nameArg = args[0];
|
|
53
|
-
const descArg = args[1];
|
|
54
|
-
const schemaArg = args[2];
|
|
55
|
-
|
|
56
|
-
// We only process if name and desc are static string literals
|
|
57
|
-
if (nameArg.type === 'StringLiteral' && descArg.type === 'StringLiteral') {
|
|
58
|
-
const name = nameArg.value;
|
|
59
|
-
const description = descArg.value;
|
|
60
|
-
let parameters: Record<string, any> = {};
|
|
61
|
-
|
|
62
|
-
// Parse schema object if provided
|
|
63
|
-
if (schemaArg && schemaArg.type === 'ObjectExpression') {
|
|
64
|
-
parameters = parseObjectExpression(schemaArg);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
intents.push({
|
|
68
|
-
name,
|
|
69
|
-
description,
|
|
70
|
-
parameters,
|
|
71
|
-
sourceFile: path.relative(process.cwd(), file)
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
} catch (error: any) {
|
|
79
|
-
console.warn(`[WARN] Skipping file ${file} due to parse error: ${error.message}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return intents;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Naively converts an AST ObjectExpression back to a JS Runtime object.
|
|
88
|
-
* Assumes the schema is statically defined (no variables/computed keys).
|
|
89
|
-
*/
|
|
90
|
-
function parseObjectExpression(node: any): any {
|
|
91
|
-
const result: any = {};
|
|
92
|
-
for (const prop of node.properties) {
|
|
93
|
-
if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
|
|
94
|
-
const key = prop.key.name;
|
|
95
|
-
|
|
96
|
-
if (prop.value.type === 'StringLiteral') {
|
|
97
|
-
result[key] = prop.value.value;
|
|
98
|
-
} else if (prop.value.type === 'NumericLiteral') {
|
|
99
|
-
result[key] = prop.value.value;
|
|
100
|
-
} else if (prop.value.type === 'BooleanLiteral') {
|
|
101
|
-
result[key] = prop.value.value;
|
|
102
|
-
} else if (prop.value.type === 'ObjectExpression') {
|
|
103
|
-
result[key] = parseObjectExpression(prop.value);
|
|
104
|
-
} else if (prop.value.type === 'ArrayExpression') {
|
|
105
|
-
result[key] = prop.value.elements.map((el: any) => {
|
|
106
|
-
if (el.type === 'StringLiteral') return el.value;
|
|
107
|
-
if (el.type === 'NumericLiteral') return el.value;
|
|
108
|
-
return null;
|
|
109
|
-
}).filter(Boolean);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function main() {
|
|
117
|
-
const sourceDir = process.argv[2] || 'src';
|
|
118
|
-
const outPath = process.argv[3] || 'intent-manifest.json';
|
|
119
|
-
|
|
120
|
-
const absoluteSource = path.resolve(process.cwd(), sourceDir);
|
|
121
|
-
const absoluteOut = path.resolve(process.cwd(), outPath);
|
|
122
|
-
|
|
123
|
-
console.log(`Scanning ${absoluteSource} for AI Actions...`);
|
|
124
|
-
|
|
125
|
-
const intents = extractIntentsFromAST(absoluteSource);
|
|
126
|
-
|
|
127
|
-
console.log(`Found ${intents.length} actions.`);
|
|
128
|
-
intents.forEach(i => console.log(` - ${i.name} (from ${i.sourceFile})`));
|
|
129
|
-
|
|
130
|
-
fs.writeFileSync(absoluteOut, JSON.stringify(intents, null, 2));
|
|
131
|
-
console.log(`\n✅ Wrote intent manifest to ${outPath}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Run if called directly
|
|
135
|
-
if (require.main === module) {
|
|
136
|
-
main().catch(err => {
|
|
137
|
-
console.error(err);
|
|
138
|
-
process.exit(1);
|
|
139
|
-
});
|
|
140
|
-
}
|