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?

asked Feb 3, 2014 at 18:47
1
  • 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. Commented Oct 13, 2015 at 19:02

7 Answers 7

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
77.9k18 gold badges198 silver badges219 bronze badges
answered Feb 3, 2014 at 18:47
Sign up to request clarification or add additional context in comments.

5 Comments

@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.
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.
Yes. I meant that you can specify an array of InjectDetails objects to my helper executeScripts function.
Works perfectly. @Piyey, please mark Douglas' answer as the best, as it seems to have solved your problem :)
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!
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

answered Dec 24, 2015 at 17:35

2 Comments

Yes! Promise is the best! No more manual synchronizing and nested callbacks!
If you want to use Promises, nowadays you can use Firefox’ browser.* APIs with their Chrome polyfill
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
});
answered Aug 8, 2019 at 18:41

Comments

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.

answered Jun 14, 2021 at 15:27

1 Comment

Instead of code, it appears that you can specify {func:transformPage}.
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.

answered May 11, 2015 at 16:51

2 Comments

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 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.
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"]);
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.
})
answered Mar 13, 2019 at 11:22

Comments

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");
 });
});
rjumatov
3,5202 gold badges20 silver badges32 bronze badges
answered Oct 27, 2022 at 22:44

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.