Search code examples
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;
          }
          chrome.scripting.executeScript({
            files: cs.js,
            target: {tabId: tab.id, allFrames: cs.all_frames},
            injectImmediately: cs.run_at === 'document_start',
            // world: cs.world, // uncomment if you use it in manifest.json in Chrome 111+
          });
        }
      }
    });
    

    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

    • If you still have old tabs open you may get an error: Cannot access contents of the page. Extension manifest must request permission to access the respective host. You will need to refresh those tabs so that they now have the new extension code that will auto-reload them.
    • You may now get a new error or still get Error: Extension context invalidated. If you are still receiving this you will need to remove any old event handlers on the page for the content script see: Chrome Extension: How to remove orphaned script after Chrome extension update

    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.