google-chrome-extensionbrowser-extensioncontent-script

Chrome extension content script re-injection after upgrade or install


After the Chrome extension I'm working on is installed, or upgraded, the content scripts (specified in the manifest) are not re-injected so a page refresh is required to make the extension work. Is there a way to force the scripts to be injected again?

I believe I could inject them again programmatically by removing them from the manifest and then handling which pages to inject in the background page, but this is not a good solution.

I don't want to automatically refresh the user's tabs because that could lose some of their data. Safari automatically refreshes all pages when you install or upgrade an extension.


Solution

  • There's a way to allow a content script heavy extension to continue functioning after an upgrade, and to make it work immediately upon installation.

    Install/upgrade

    The install method is to simply iterate through all tabs in all windows, and inject some scripts programmatically into tabs with matching URLs.

    ManifestV3

    manifest.json:

    "background": {"service_worker": "background.js"},
    "permissions": ["scripting"],
    "host_permissions": ["<all_urls>"],
    

    These host_permissions should be the same as the content script's matches.

    background.js:

    chrome.runtime.onInstalled.addListener(async () => {
      for (const cs of chrome.runtime.getManifest().content_scripts) {
        for (const tab of await chrome.tabs.query({url: cs.matches})) {
          if (tab.url.match(/(chrome|chrome-extension):\/\//gi)) {
            continue;
          }
          const target = {tabId: tab.id, allFrames: cs.all_frames};
          if (cs.js[0]) chrome.scripting.executeScript({
            files: cs.js,
            injectImmediately: cs.run_at === 'document_start',
            world: cs.world, // requires Chrome 111+
            target,
          });
          if (cs.css[0]) chrome.scripting.insertCSS({
            files: cs.css,
            origin: cs.origin,
            target,
          });
        }
      }
    });
    

    This is a simplified example that doesn't handle frames. You can use getAllFrames API and match the URLs yourself, see the documentation for matching patterns.

    Caveats & Notes

    ManifestV2

    Obviously, you have to do it in a background page or event page script declared in manifest.json:

    "background": {
        "scripts": ["background.js"]
    },
    

    background.js:

    // Add a `manifest` property to the `chrome` object.
    chrome.manifest = chrome.runtime.getManifest();
    
    var injectIntoTab = function (tab) {
        // You could iterate through the content scripts here
        var scripts = chrome.manifest.content_scripts[0].js;
        var i = 0, s = scripts.length;
        for( ; i < s; i++ ) {
            chrome.tabs.executeScript(tab.id, {
                file: scripts[i]
            });
        }
    }
    
    // Get all windows
    chrome.windows.getAll({
        populate: true
    }, function (windows) {
        var i = 0, w = windows.length, currentWindow;
        for( ; i < w; i++ ) {
            currentWindow = windows[i];
            var j = 0, t = currentWindow.tabs.length, currentTab;
            for( ; j < t; j++ ) {
                currentTab = currentWindow.tabs[j];
                // Skip chrome:// and https:// pages
                if( ! currentTab.url.match(/(chrome|https):\/\//gi) ) {
                    injectIntoTab(currentTab);
                }
            }
        }
    });
    

    Historical trivia

    In ancient Chrome 26 and earlier content scripts could restore connection to the background script. It was fixed http://crbug.com/168263 in 2013. You can see an example of this trick in the earlier revisions of this answer.