/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ /// import {ChromeMessageBus} from './chrome-message-bus'; import {getBackendUri, getContentScriptUri, getDetectAngularScriptUri} from './comm-utils'; import {SamePageMessageBus} from './same-page-message-bus'; let backgroundDisconnected = false; let backendInitialized = false; const port = chrome.runtime.connect({ name: `${document.title || location.href}`, }); // Since Manifest V3, the service worker (background) // gets terminated after 30s of inactivity. This can // break the initialization phase of DevTools or the // BE-FE communication channel, if already initialized. // To prevent that, we emit a heartbeat in a >30s interval. const HEARTBEAT_INTERVAL = 20000; // Keep below 30s const heartbeatInterval = setInterval(() => { port.postMessage('__NG_DEVTOOLS_BEAT'); }, HEARTBEAT_INTERVAL); const handleDisconnect = (): void => { localMessageBus.emit('shutdown'); localMessageBus.destroy(); chromeMessageBus.destroy(); clearInterval(heartbeatInterval); backgroundDisconnected = true; }; function attemptBackendHandshake() { if (!backendInitialized) { // tslint:disable-next-line:no-console console.log('Attempting handshake with backend', new Date()); const retry = () => { if (backendInitialized || backgroundDisconnected) { return; } handshakeWithBackend(); setTimeout(retry, 500); }; retry(); } } port.onDisconnect.addListener(handleDisconnect); const detectAngularMessageBus = new SamePageMessageBus( '[ContentScript=>DetectAngular]', getContentScriptUri(), getDetectAngularScriptUri(), ); detectAngularMessageBus.on('detectAngular', (detectionResult) => { if (detectionResult.isAngularDevTools !== true) { return; } if (detectionResult.isAngular !== true) { return; } // Defensive check against non html page. Realistically this should never happen. if (document.contentType !== 'text/html') { return; } // Inform the background page so it can toggle the popup and icon. void chrome.runtime.sendMessage(detectionResult); const script = document.createElement('script'); script.src = chrome.runtime.getURL('app/backend_bundle.js'); document.documentElement.appendChild(script); document.documentElement.removeChild(script); detectAngularMessageBus.emit('backendInstalled', [detectionResult]); attemptBackendHandshake(); }); const localMessageBus = new SamePageMessageBus( '[ConstentScript=>BackEnd]', getContentScriptUri(), getBackendUri(), ); const chromeMessageBus = new ChromeMessageBus(port); const handshakeWithBackend = (): void => { localMessageBus.emit('handshake'); }; // Relaying messages from FE to BE chromeMessageBus.onAny((topic, args) => { localMessageBus.emit(topic, args); }); // Relaying messages from BE to FE localMessageBus.onAny((topic, args) => { chromeMessageBus.emit(topic, args); }); localMessageBus.on('backendReady', () => { backendInitialized = true; }); const proxyEventFromWindowToDevToolsExtension = (event: MessageEvent) => { if (event.source === window && event.data && event.data.__NG_DEVTOOLS_EVENT__) { try { chrome.runtime.sendMessage(event.data); } catch (e) { const {message} = e as Error; if (message.includes('Extension context invalidated.')) { console.error( 'Angular DevTools: Disconnecting content script due to invalid extension context. Please reload the page.', ); window.removeEventListener('message', proxyEventFromWindowToDevToolsExtension); } throw e; } } }; window.addEventListener('message', proxyEventFromWindowToDevToolsExtension);