Search code examples
javascriptgoogle-chrome-extensionchrome-extension-manifest-v3

In a browser extension using manifest v3, how can I communicate between a content script and a devtools panel?


I have a new browser extension I'm developing, which means that to make it publicly available on the Chrome Web Store, I must use manifest v3. My extension is a DevTools extension, which means that to communicate with the content script, I have to use a background service worker to proxy the messages. Unfortunately, the docs on DevTools extensions haven't been updated for manifest v3, and the technique they suggest for messaging between the content script and the DevTools panel via the background script won't work if the background worker is terminated.

I've seen some answers here and Chromium project issue report comments suggest that the only available workaround is to reset the connection every five minutes. That seems hacky and unreliable. Is there a better mechanism for this, something more event based than an arbitrary timer?


Solution

  • We can make the connection hub out of the devtools_page itself. This hidden page runs inside devtools for the current tab, it doesn't unload while devtools is open, and it has full access to all of chrome API same as the background script.

    manifest.json:

      "devtools_page": "devtools.html",
      "content_scripts": [{
        "matches": ["<all_urls>"],
        "js": ["content.js"],
        "run_at": "document_start"
      }]
    

    devtools.html:

    <script src="devtools.js"></script>
    

    devtools.js:

    let portDev, portTab;
    const tabId = chrome.devtools.inspectedWindow.tabId;
    const onDevMessage = msg => portTab.postMessage(msg);
    const onTabMessage = msg => portDev.postMessage(msg);
    
    chrome.runtime.onConnect.addListener(port => {
      if (+port.name !== tabId) return;
      portDev = port;
      portDev.onMessage.addListener(onDevMessage);
      portTab = chrome.tabs.connect(tabId, {name: 'dev'});
      portTab.onMessage.addListener(onTabMessage);
    });
    
    // chrome.devtools.panels.create...
    

    panel.js:

    const port = chrome.runtime.connect({
      name: `${chrome.devtools.inspectedWindow.tabId}`,
    });
    port.onMessage.addListener(msg => {
      // This prints in devtools-on-devtools: https://stackoverflow.com/q/12291138
      // To print in tab's console see `chrome.devtools.inspectedWindow.eval`
      console.log(msg);
    });
    self.onclick = () => port.postMessage('foo');
    

    content.js:

    let portDev;
    const onMessage = msg => {
      console.log(msg);
      portDev.postMessage('bar');
    };
    const onDisconnect = () => {
      portDev = null;
    };
    chrome.runtime.onConnect.addListener(port => {
      if (port.name !== 'dev') return;
      portDev = port;
      portDev.onMessage.addListener(onMessage);
      portDev.onDisconnect.addListener(onDisconnect);
    });
    

    P.S. Regarding the 5-minute timer reset trick, if you still need the background script to be persistent, in this case it is reasonably reliable because the tab is guaranteed to be open while devtools for this tab is open.