| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| @JS() |
| library; |
| |
| import 'package:dwds/data/debug_info.dart'; |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 |
| // ignore: deprecated_member_use |
| import 'package:js/js.dart'; |
| |
| import 'chrome_api.dart'; |
| import 'cider_connection.dart'; |
| import 'cross_extension_communication.dart'; |
| import 'data_types.dart'; |
| import 'debug_session.dart'; |
| import 'logger.dart'; |
| import 'messaging.dart'; |
| import 'storage.dart'; |
| import 'utils.dart'; |
| |
| void main() { |
| _registerListeners(); |
| } |
| |
| void _registerListeners() { |
| chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages)); |
| // The only extension allowed to send messages to this extension is the |
| // AngularDart DevTools extension. Its permission is set in the manifest.json |
| // externally_connectable field. |
| chrome.runtime.onMessageExternal.addListener( |
| allowInterop(handleMessagesFromAngularDartDevTools), |
| ); |
| // The only external service that sends messages to the Dart Debug Extension |
| // is Cider. |
| chrome.runtime.onConnectExternal.addListener( |
| allowInterop(handleCiderConnectRequest), |
| ); |
| // Update the extension icon on tab navigation: |
| chrome.tabs.onActivated.addListener( |
| allowInterop((ActiveInfo info) async { |
| await _updateIcon(info.tabId); |
| }), |
| ); |
| chrome.windows.onFocusChanged.addListener( |
| allowInterop((_) async { |
| final currentTab = await activeTab; |
| if (currentTab?.id case final tabId?) { |
| await _updateIcon(tabId); |
| } |
| }), |
| ); |
| chrome.webNavigation.onCommitted.addListener( |
| allowInterop(_detectNavigationAwayFromDartApp), |
| ); |
| |
| chrome.commands.onCommand.addListener( |
| allowInterop(_maybeSendCopyAppIdRequest), |
| ); |
| } |
| |
| Future<void> _handleRuntimeMessages( |
| dynamic jsRequest, |
| MessageSender sender, |
| Function sendResponse, |
| ) async { |
| if (jsRequest is! String) return; |
| |
| interceptMessage<String>( |
| message: jsRequest, |
| expectedType: MessageType.isAuthenticated, |
| expectedSender: Script.detector, |
| expectedRecipient: Script.background, |
| sender: sender, |
| messageHandler: (String isAuthenticated) async { |
| final dartTab = sender.tab; |
| if (dartTab == null) { |
| debugWarn('Received auth info but tab is missing.'); |
| return; |
| } |
| // Save the authentication info in storage: |
| await setStorageObject<String>( |
| type: StorageObject.isAuthenticated, |
| value: isAuthenticated, |
| tabId: dartTab.id, |
| ); |
| }, |
| ); |
| |
| interceptMessage<DebugInfo>( |
| message: jsRequest, |
| expectedType: MessageType.debugInfo, |
| expectedSender: Script.detector, |
| expectedRecipient: Script.background, |
| sender: sender, |
| messageHandler: (DebugInfo debugInfo) async { |
| final dartTab = sender.tab; |
| if (dartTab == null) { |
| debugWarn('Received debug info but tab is missing.'); |
| return; |
| } |
| // If this is a new Dart app, we need to clear old debug session data: |
| if (!await _matchesAppInStorage(debugInfo.appId, tabId: dartTab.id)) { |
| await clearStaleDebugSession(dartTab.id); |
| } |
| // Save the debug info for the Dart app in storage: |
| await setStorageObject<DebugInfo>( |
| type: StorageObject.debugInfo, |
| value: _addTabInfo(debugInfo, tab: dartTab), |
| tabId: dartTab.id, |
| ); |
| // Update the icon to show that a Dart app has been detected: |
| final currentTab = await activeTab; |
| if (currentTab?.id == dartTab.id) { |
| await _updateIcon(dartTab.id); |
| } |
| }, |
| ); |
| |
| interceptMessage<DebugStateChange>( |
| message: jsRequest, |
| expectedType: MessageType.debugStateChange, |
| expectedSender: Script.debuggerPanel, |
| expectedRecipient: Script.background, |
| sender: sender, |
| messageHandler: (DebugStateChange debugStateChange) { |
| final newState = debugStateChange.newState; |
| final tabId = debugStateChange.tabId; |
| if (newState == DebugStateChange.startDebugging) { |
| attachDebugger(tabId, trigger: Trigger.extensionPanel); |
| } |
| }, |
| ); |
| |
| interceptMessage<DebugStateChange>( |
| message: jsRequest, |
| expectedType: MessageType.debugStateChange, |
| expectedSender: Script.popup, |
| expectedRecipient: Script.background, |
| sender: sender, |
| messageHandler: (DebugStateChange debugStateChange) { |
| final newState = debugStateChange.newState; |
| final tabId = debugStateChange.tabId; |
| if (newState == DebugStateChange.startDebugging) { |
| attachDebugger(tabId, trigger: Trigger.extensionIcon); |
| } |
| }, |
| ); |
| |
| interceptMessage<String>( |
| message: jsRequest, |
| expectedType: MessageType.multipleAppsDetected, |
| expectedSender: Script.detector, |
| expectedRecipient: Script.background, |
| sender: sender, |
| messageHandler: (String multipleAppsDetected) async { |
| final dartTab = sender.tab; |
| if (dartTab == null) { |
| debugWarn('Received multiple apps detected but tab is missing.'); |
| return; |
| } |
| // Save the multiple apps info in storage: |
| await setStorageObject<String>( |
| type: StorageObject.multipleAppsDetected, |
| value: multipleAppsDetected, |
| tabId: dartTab.id, |
| ); |
| _setWarningIcon(dartTab.id); |
| }, |
| ); |
| |
| interceptMessage<String>( |
| message: jsRequest, |
| expectedType: MessageType.appId, |
| expectedSender: Script.copier, |
| expectedRecipient: Script.background, |
| sender: sender, |
| messageHandler: (String appId) { |
| displayNotification('Copied app ID: $appId'); |
| }, |
| ); |
| |
| sendResponse(defaultResponse); |
| } |
| |
| Future<void> _detectNavigationAwayFromDartApp( |
| NavigationInfo navigationInfo, |
| ) async { |
| // Ignore any navigation events within the page itself (e.g., opening a link, |
| // reloading the page, reloading an IFRAME, etc): |
| if (_isInternalNavigation(navigationInfo)) return; |
| final tabId = navigationInfo.tabId; |
| final debugInfo = await _fetchDebugInfo(navigationInfo.tabId); |
| if (debugInfo == null) return; |
| if (debugInfo.tabUrl != navigationInfo.url) { |
| _setDefaultIcon(navigationInfo.tabId); |
| await clearStaleDebugSession(tabId); |
| await removeStorageObject(type: StorageObject.debugInfo, tabId: tabId); |
| await detachDebugger( |
| tabId, |
| type: TabType.dartApp, |
| reason: DetachReason.navigatedAwayFromApp, |
| ); |
| } |
| } |
| |
| bool _isInternalNavigation(NavigationInfo navigationInfo) { |
| return [ |
| 'auto_subframe', |
| 'form_submit', |
| 'link', |
| 'manual_subframe', |
| 'reload', |
| ].contains(navigationInfo.transitionType); |
| } |
| |
| DebugInfo _addTabInfo(DebugInfo debugInfo, {required Tab tab}) { |
| return DebugInfo( |
| (b) => |
| b |
| ..appEntrypointPath = debugInfo.appEntrypointPath |
| ..appId = debugInfo.appId |
| ..appInstanceId = debugInfo.appInstanceId |
| ..appOrigin = debugInfo.appOrigin |
| ..appUrl = debugInfo.appUrl |
| ..authUrl = debugInfo.authUrl |
| ..extensionUrl = debugInfo.extensionUrl |
| ..isInternalBuild = debugInfo.isInternalBuild |
| ..isFlutterApp = debugInfo.isFlutterApp |
| ..workspaceName = debugInfo.workspaceName |
| ..tabUrl = tab.url |
| ..tabId = tab.id, |
| ); |
| } |
| |
| Future<bool> _maybeSendCopyAppIdRequest(String command, [Tab? tab]) async { |
| if (command != 'copyAppId') return false; |
| final tabId = (tab ?? await activeTab)?.id; |
| if (tabId == null) return false; |
| final debugInfo = await _fetchDebugInfo(tabId); |
| final workspaceName = debugInfo?.workspaceName; |
| if (workspaceName == null) return false; |
| final appId = '$workspaceName-$tabId'; |
| return sendTabsMessage( |
| tabId: tabId, |
| type: MessageType.appId, |
| body: appId, |
| sender: Script.background, |
| recipient: Script.copier, |
| ); |
| } |
| |
| Future<void> _updateIcon(int activeTabId) async { |
| final debugInfo = await _fetchDebugInfo(activeTabId); |
| if (debugInfo == null) { |
| _setDefaultIcon(activeTabId); |
| return; |
| } |
| final multipleApps = await fetchStorageObject<String>( |
| type: StorageObject.multipleAppsDetected, |
| tabId: activeTabId, |
| ); |
| multipleApps == null |
| ? _setDebuggableIcon(activeTabId) |
| : _setWarningIcon(activeTabId); |
| } |
| |
| void _setDebuggableIcon(int tabId) { |
| setExtensionIcon(IconInfo(path: 'static_assets/dart.png')); |
| setExtensionPopup( |
| PopupDetails(popup: 'static_assets/popup.html', tabId: tabId), |
| ); |
| } |
| |
| void _setWarningIcon(int tabId) { |
| setExtensionPopup( |
| PopupDetails(popup: 'static_assets/popup.html', tabId: tabId), |
| ); |
| } |
| |
| void _setDefaultIcon(int tabId) { |
| final iconPath = |
| isDevMode ? 'static_assets/dart_dev.png' : 'static_assets/dart_grey.png'; |
| setExtensionIcon(IconInfo(path: iconPath)); |
| setExtensionPopup(PopupDetails(popup: '', tabId: tabId)); |
| } |
| |
| Future<DebugInfo?> _fetchDebugInfo(int tabId) { |
| return fetchStorageObject<DebugInfo>( |
| type: StorageObject.debugInfo, |
| tabId: tabId, |
| ); |
| } |
| |
| Future<bool> _matchesAppInStorage(String? appId, {required int tabId}) async { |
| final debugInfo = await _fetchDebugInfo(tabId); |
| return appId != null && appId == debugInfo?.appId; |
| } |