31

I need to programmatically inject multiple script files (followed by a code snippet) into the current page from my Google Chrome extension. The chrome.tabs.executeScript method allows for a single InjectDetails object (representing a script file or code snippet), as well as a callback function to be executed after the script. Current answers propose nesting executeScript calls:

chrome.browserAction.onClicked.addListener(function(tab) {
    chrome.tabs.executeScript(null, { file: "jquery.js" }, function() {
        chrome.tabs.executeScript(null, { file: "master.js" }, function() {
            chrome.tabs.executeScript(null, { file: "helper.js" }, function() {
                chrome.tabs.executeScript(null, { code: "transformPage();" })
            })
        })
    })
});

However, the callback nesting gets unwieldy. Is there a way of abstracting this?

Community
  • 1
  • 1
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • One potential solution, that may not fit your situation: concatenate all of your files using a tool like grunt or gulp into one single file, concat.js, then simply execute the concatenated file. – SethWhite Oct 13 '15 at 19:02

7 Answers7

42

This is my proposed solution:

function executeScripts(tabId, injectDetailsArray)
{
    function createCallback(tabId, injectDetails, innerCallback) {
        return function () {
            chrome.tabs.executeScript(tabId, injectDetails, innerCallback);
        };
    }

    var callback = null;

    for (var i = injectDetailsArray.length - 1; i >= 0; --i)
        callback = createCallback(tabId, injectDetailsArray[i], callback);

    if (callback !== null)
        callback();   // execute outermost function
}

Subsequently, the sequence of InjectDetails scripts can be specified as an array:

chrome.browserAction.onClicked.addListener(function (tab) {
    executeScripts(null, [ 
        { file: "jquery.js" }, 
        { file: "master.js" },
        { file: "helper.js" },
        { code: "transformPage();" }
    ])
});
Xan
  • 74,770
  • 16
  • 179
  • 206
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • @Lior: What do you mean? The standard `addListener` function doesn't take arrays, but my helper `executeScripts` function can take an array and convert it into a nested callback. – Douglas May 28 '14 at 20:19
  • Oh ok, you wrote "the sequence of InjectDetails scripts can be specified as an array", you meant injectDetailsArray? I got confused with the chrome.tabs.executeScript's argument InjectDetails. – Lior May 29 '14 at 07:16
  • Yes. I meant that you can specify an array of [`InjectDetails`](https://developer.chrome.com/extensions/tabs#type-InjectDetails) objects to my helper `executeScripts` function. – Douglas May 29 '14 at 18:48
  • Works perfectly. @Piyey, please mark Douglas' answer as the best, as it seems to have solved your problem :) – ljs.dev Jan 15 '15 at 06:14
  • i was having the same issue, one of the scripts were relying on the previous script being available. it worked most of the time, but this seems like a better solution. Voted up! – baskint Jan 26 '16 at 21:28
10

From Chrome v32, it supports Promise. We should use it for making code clean.

Here is an example:

new ScriptExecution(tab.id)
    .executeScripts("js/jquery.js", "js/script.js")
    .then(s => s.executeCodes('console.log("executes code...")'))
    .then(s => s.injectCss("css/style.css"))
    .then(s => console.log('done'));

ScriptExecution source:

(function() {
    function ScriptExecution(tabId) {
        this.tabId = tabId;
    }

    ScriptExecution.prototype.executeScripts = function(fileArray) {
        fileArray = Array.prototype.slice.call(arguments); // ES6: Array.from(arguments)
        return Promise.all(fileArray.map(file => exeScript(this.tabId, file))).then(() => this); // 'this' will be use at next chain
    };

    ScriptExecution.prototype.executeCodes = function(fileArray) {
        fileArray = Array.prototype.slice.call(arguments);
        return Promise.all(fileArray.map(code => exeCodes(this.tabId, code))).then(() => this);
    };

    ScriptExecution.prototype.injectCss = function(fileArray) {
        fileArray = Array.prototype.slice.call(arguments);
        return Promise.all(fileArray.map(file => exeCss(this.tabId, file))).then(() => this);
    };

    function promiseTo(fn, tabId, info) {
        return new Promise(resolve => {
            fn.call(chrome.tabs, tabId, info, x => resolve());
        });
    }


    function exeScript(tabId, path) {
        let info = { file : path, runAt: 'document_end' };
        return promiseTo(chrome.tabs.executeScript, tabId, info);
    }

    function exeCodes(tabId, code) {
        let info = { code : code, runAt: 'document_end' };
        return promiseTo(chrome.tabs.executeScript, tabId, info);
    }

    function exeCss(tabId, path) {
        let info = { file : path, runAt: 'document_end' };
        return promiseTo(chrome.tabs.insertCSS, tabId, info);
    }

    window.ScriptExecution = ScriptExecution;
})()

If you would like to use ES5, you can use online compiler to compile above codes to ES5.

Fork me on GitHub: chrome-script-execution

ninhjs.dev
  • 7,203
  • 1
  • 49
  • 35
  • 1
    Yes! Promise is the best! No more manual synchronizing and nested callbacks! –  Jun 03 '16 at 18:27
  • If you want to use Promises, nowadays you can use Firefox’ `browser.*` APIs with [their Chrome polyfill](https://github.com/mozilla/webextension-polyfill) – fregante Aug 08 '19 at 18:28
4

Fun fact, the scripts are injected in order and you don't need to wait for each one to be injected.

chrome.browserAction.onClicked.addListener(tab => {
    chrome.tabs.executeScript(tab.id, { file: "jquery.js" });
    chrome.tabs.executeScript(tab.id, { file: "master.js" });
    chrome.tabs.executeScript(tab.id, { file: "helper.js" });
    chrome.tabs.executeScript(tab.id, { code: "transformPage();" }, () => {
        // All scripts loaded
    });
});

This is considerably faster than manually waiting for each one. You can verify that they are loaded in order by loading a huge library first (like d3.js) and then loading a small file after. The order will still be preserved.

Note: errors aren't caught, but this should never happen if all files exist.

I wrote a little module to simplify this even further, including proper error handling, Promise support and scripting API in Manifest v3:

executeScript({
    tabId: tab.id,
    files: ["jquery.js", "master.js", "helper.js"]
}).then(() => {
    // All scripts loaded
});
fregante
  • 29,050
  • 14
  • 119
  • 159
3

Since Manifest v3, you can use promise chains and async/await:

Promises

MV3 provides first-class support for promises: many popular APIs support promises now, and we will eventually support promises on all appropriate methods.

You can use promise chains, as well as async/await. [...]

The following should work.

chrome.browserAction.onClicked.addListener(async (tab) => {
    await chrome.scripting.executeScript({ files: ["jquery.js"] });
    await chrome.scripting.executeScript({ files: ["master.js"] });
    await chrome.scripting.executeScript({ files: ["helper.js"] });
    // await chrome.tabs.executeScript({ code: "transformPage();" });
});

Note that, despite the argument name, files must specify exactly one file. Note that you can't execute arbitrary code anymore, so best move that transformPage(); into a file and execute it.

Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249
2

Given your answer, I expected synchronously injecting the scripts to cause problems (namely, I thought that the scripts might be loaded in the wrong order), but it works well for me.

var scripts = [
  'first.js',
  'middle.js',
  'last.js'
];
scripts.forEach(function(script) {
  chrome.tabs.executeScript(null, { file: script }, function(resp) {
    if (script!=='last.js') return;
    // Your callback code here
  });
});

This assumes you only want one callback at the end and don't need the results of each executed script.

willlma
  • 7,353
  • 2
  • 30
  • 45
  • 1
    Thanks for the observation! However, even though your code might work correctly under the current version of Chrome, I would not rely on this behaviour. Given that [`executeScript`](https://developer.chrome.com/extensions/tabs#method-executeScript) is defined as asynchronous (taking a callback function), one should not assume that the operation is complete before the callback is called. This concern would not apply if Chrome guaranteed that the injected scripts will always execute in order, but I didn't find that assurance documented. – Douglas Jun 20 '15 at 13:29
  • It works to me! I have put it in this way for ease of use of others too: ```function loadScripts(scripts) { scripts.forEach(function(script) { chrome.tabs.executeScript(null, { file: script }, function(resp) {}); }); }``` then I use it like: `loadScripts(["generics.js", "content-o.js"]);` – danivicario Sep 05 '19 at 21:06
0

This is mostly an updated answer (on the other answer) :P

const executeScripts = (tabId, scripts, finalCallback) => {
  try {
    if (scripts.length && scripts.length > 0) {
      const execute = (index = 0) => {
        chrome.tabs.executeScript(tabId, scripts[index], () => {
          const newIndex = index + 1;
          if (scripts[newIndex]) {
            execute(newIndex);
          } else {
            finalCallback();
          }
        });
      }
      execute();
    } else {
      throw new Error('scripts(array) undefined or empty');
    }
  } catch (err) {
    console.log(err);
  }
}
executeScripts(
  null, 
  [
    { file: "jquery.js" }, 
    { file: "master.js" },
    { file: "helper.js" },
    { code: "transformPage();" }
  ],
  () => {
    // Do whatever you want to do, after the last script is executed.
  }
)

Or return a promise.

const executeScripts = (tabId, scripts) => {
  return new Promise((resolve, reject) => {
    try {
      if (scripts.length && scripts.length > 0) {
        const execute = (index = 0) => {
          chrome.tabs.executeScript(tabId, scripts[index], () => {
            const newIndex = index + 1;
            if (scripts[newIndex]) {
              execute(newIndex);
            } else {
              resolve();
            }
          });
        }
        execute();
      } else {
        throw new Error('scripts(array) undefined or empty');
      }
    } catch (err) {
      reject(err);
    }
  });
};
executeScripts(
  null, 
  [
    { file: "jquery.js" }, 
    { file: "master.js" },
    { file: "helper.js" },
    { code: "transformPage();" }
  ]
).then(() => {
  // Do whatever you want to do, after the last script is executed.
})

Aytacworld
  • 145
  • 2
  • 12
0

with v3:

chrome.action.onClicked.addListener((tab) => {
  console.log("entering");
  chrome.scripting
    .executeScript({
      target: { tabId: tab.id },
      files: [
        "scripts/jquery.min.js",
        "scripts/bootstrap.min.js",
        "scripts/script.js",
      ],
    })
    .then(() => {
      // All scripts loaded
      console.log("done");
    });
});
sm3sher
  • 2,764
  • 1
  • 11
  • 23
ampecs
  • 1
  • 1