11

I'm currently trying to figure out how I can include a javascript, I have on my webserver, in my scriptish/greasemonkey script and make it reload the script everytime the userscript gets called.

I'm editing the script on my webserver and I really don't want to reinstall the userscript each time I make changes on the included script.

Is there any way around this problem? I've been searching for an answer but had no luck so far.

So to be clear, my userscript looks like this:

// ==UserScript==
// @id             HET
// @name           SettingsHandler
// @version        1.0
// @namespace      HET
// @require        http://urltoscript/scripts/he/lib.js
// @run-at         document-end
// ==/UserScript==

and my external script looks like this:

alert('got it');

All still very easy for testing purposes. This setup works, but only the first time and when I change my lib.js script, then the userscript still reads the old one. Is there a way to prevent the userscript from caching the external script? Or is there any other metatag that can help me?

Thanks in advance, Dave

DaMaGeX
  • 693
  • 7
  • 18
  • http://stackoverflow.com/questions/9210542/node-js-require-cache-possible-to-invalidate – Andreas May 07 '15 at 19:09
  • 3
    How does this apply to Scriptish or Greasemonkey? – DaMaGeX May 07 '15 at 19:11
  • 1
    [`@require` is for caching at install time.](http://wiki.greasespot.net/Metadata_Block#.40require) - "Each @require is downloaded once, when the script is installed, and stored on the user's hard drive alongside the script." If you don't want caching, just insert a ` – rampion May 10 '15 at 02:46
  • 1
    @rampion huh? – EpiX Aug 31 '19 at 01:19
  • @RyanLee Rob M’s answer below does exactly what I said. – rampion Aug 31 '19 at 02:53
  • Most of the solutions in this answer and even on github discussions have no effect, or, for example, remove breakpoints on refresh. That's additionally to http CORS problem, and additionally to lack of edit and continue on Fox. Unfortunately, most practical solution is to use file:/// and Chrome. – halt9k Nov 07 '22 at 04:35

5 Answers5

6

Here is the only workable answer https://github.com/Tampermonkey/tampermonkey/issues/475

Recommended is option 4. However they are loaded asynchronously so loading orders might differ

There are several ways to ease your pain. :)

  1. You can increase the version number before saving the script and all external resources will be reloaded.
  2. After setting "Config mode" to "Advanced" you can configure the external update interval. Note: "Always" still means after the resource was used. So you might need to execute/load the page twice.
  3. If you use Tampermonkey Beta (Chrome or Firefox) you can edit the external resource in place (because there now is a edit button besides the delete button).
  4. Copy the resources and store them locally. After you've enabled "Local file access" at Chrome's extension management page or Tampermonkey's settings page (if you're using Firefox) you can @require them via a local file:// URI.
EpiX
  • 1,281
  • 2
  • 16
  • 22
5

Not sure how to accomplish this with GM/userscript directives, but you could easily add the script yourself and append a timestamp to the url to prevent the browser from caching it:

var remoteScript = document.createElement('script');
remoteScript.src = 'http://domain.com/path/to/script.js?ts='+(+new Date());
remoteScript.onload = init;
document.body.appendChild(remoteScript);

function init() {
  ... do stuff
}
Rob M.
  • 35,491
  • 6
  • 51
  • 50
  • 4
    That wont work with CSP blocking you from accessing `domain.com`. – coulix Feb 25 '17 at 05:51
  • @TomášZato: The question said the `@require` directive would point to JS code on "[their] web server" so I don't think local files are an issue. – Alex Quinn Feb 01 '18 at 16:33
2

The answer from Rob M. doesn't work for me, because Tampermonkey script location and target site where it got injected could be different. For example, in my case I also have a locally running webserver to develop in an IDE using Tampermonkey for Firefox without requiring any file system access for Tampermonkey from the Browser.

This script should got injected to a third party site like example.com, where it applys modifications. So the browser would block this script, since it's from a different domain than example.com.

During development, I'd like to get my script without any caching to apply changes immediately. Worked around this by fetching the scripts content using `GM.xmlHttpRequest. Additionally, a GET parameter with the current timestamp acts as cache buster:

let url = `http://localhost/myscript.js?ts=${(+new Date())}`

GM.xmlHttpRequest({
    method: "GET",
    url: url,
    onload: function(response) {
        let remoteScript = document.createElement('script')
        remoteScript.id = 'tm-dev-script'
        remoteScript.innerHTML = response.responseText
        document.body.appendChild(remoteScript)
    }
})

Notice that since GM.xmlHttpRequest can bypass the same origin policy, access musst be explicitly granded in the head of your script:

// @grant        GM.xmlHttpRequest
Rob Kwasowski
  • 2,690
  • 3
  • 13
  • 32
Lion
  • 16,606
  • 23
  • 86
  • 148
  • This is the only solution here that worked for me (not only because I have it on different domain e.g. localhost or similar), but also because my Firefox 82 forces http:// to https:// . Thank you very much! – Tomáš Diviš Dec 01 '20 at 12:22
  • I literally just figured this out, I wish I would have found this answer a couple of days ago. The date is a nice addition though. Thank you. – TYPKRFT Jul 11 '22 at 20:02
0

What works for me is to retrieve my files in a text format via GM_xmlhttpRequest, wrap that inside of an eval() and then execute functions as though @require was used.

It's worth noting that using eval is extremely dangerous and should almost never be used. That being said, this is the exception for me as there is low risk.

In my case, I have a couple of different scripts that need to be run before my main application script (such as utility functions). Therefore, similar to how you would place <script> tags in an order depending on when you want them to run, I order them to run first to last in an Array.

Also, I wrap all my TM functions inside of an initialize() function, and that's what I call at the end which kicks off my script.

(async function() {
    try {
        const scriptsToExecute = [
            { resource: 'waitForElement', url: 'https://www.example.org/waitForElement.js', },
            { resource: 'utils', url: 'https://www.example.org/utils.js', },
            { resource: 'main', url: 'https://www.example.org/mainApp.js'},
        ];

        const getScripts = await retrieveScripts(scriptsToExecute).catch(e => {debugger;console.error('Error caught @ retrieveScripts',e);});
        if (getScripts?.status !== "success" || !Array.isArray(getScripts?.scripts || getScripts?.find(f => f.status !== "success"))) throw {getScripts};

        try {
            const scripts = getScripts?.scripts;
            const mainAppScript = scripts?.find(f => f?.resource === "main");

            const scriptsToExecute = scripts?.filter(f => f?.resource !== "main");
            for (let i in scriptsToExecute){
                if (scriptsToExecute[i]?.status !== "success" || !scripts[i]?.retrieved) throw {"erroredScript": scripts[i]}
                const thisScript = scripts[i]?.retrieved;
                // eslint-disable-next-line
                eval(thisScript?.script);
            }
            // eslint-disable-next-line
            eval(mainAppScript);
            try {
                // once you've eval'd the script, you can call functions inside that script from within your UserScript environment
                // all my main app scripts are wrapped inside of a function called `initialize()`.
                // though once you've eval'd the script, you can call whatever you want.
                initialize();  
            } catch (err){debugger; console.error('Error caught @ attempting to initialize', err);}
        } catch(err){debugger; console.error('Error caught @ top level', err);}

    } catch (err) {debugger}

    async function retrieveScripts(scriptsToRetrieve){
        try {
            const scriptsContent = await Promise.all(scriptsToRetrieve.map(m => retrieveScript(m))).catch(e => {debugger;});
    
            if (!Array.isArray(scriptsContent) || scriptsContent?.length !== scriptsToRetrieve?.length && scriptsContent?.find(f => f.status !== "success")) {debugger;return {status: "error", msg: "unable to retrieve the script(s) requested.", scriptsContent,};}
            else return {status: "success", "scripts": scriptsContent};
        }
        catch (err){debugger;return {status: "error", msg: "(caught) unable to retrieve the script(s) requested.", scriptsToRetrieve, "error": err, "errorStringified": String(err)};}
    
        function retrieveScript(scriptToRetrieve){
            if (!scriptToRetrieve?.url) return {status: "error", msg: "no url found", scriptToRetrieve};
            try {
                    return new Promise((resolve,reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: scriptToRetrieve.url,
                            onerror: function (response) {debugger;return reject({status: "error", response, scriptToRetrieve });},
                            onload: function (response) {
                                if (response?.status !== 200) {debugger;return reject({status: "error", response, scriptToRetrieve });}
                                else {
                                    try {
                                        if (response?.response) {
                                            scriptToRetrieve.script = response.response;
                                            return resolve({status: "success", "retrieved": scriptToRetrieve})
                                        }
                                        else throw {status: "error", "response": response.response, scriptToRetrieve }
                                    } catch (err) {return reject(err);}
                                }
                            }
                        });
                    });
            } catch (err){debugger}
        }
    }

})();

Once you've eval'd your scripts, you can now run them from within the context of your Userscript. What's more, if you initialize any variables from within the function where you use eval, they are available to you as well (such as machine-specific code). Or if you have eval'd scripts before this, all those functions will available as well -- essentially importing them.

Joey
  • 613
  • 1
  • 6
  • 17
-1

You can add the following to your .htaccess file:

<FilesMatch "filename.js">
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>

And remove it when you are done developing.

Sidd
  • 1,389
  • 7
  • 17
  • do you even know what tampermonkey is? – Faizan Anwer Ali Rupani Aug 20 '17 at 16:41
  • Sure, although I didn't think that was particularly relevant to the question. – Sidd Aug 20 '17 at 17:27
  • This answer is correct. The question said their @require directive would point to JS code on "[their] web server". If that server runs Apache, then this .htaccess code should solve the problem just fine. – Alex Quinn Feb 01 '18 at 16:29
  • 1
    not specific to tampermonkey. does not solve the issue when using require with tampermonkey. for example, my require is calling a raw file from GitHib where I manage my script. Tampermonkey is not updating it, so a solution is needed to stop tampermonkey caching scripts and to call the fresh one each time. – Laurence Cope May 08 '18 at 08:33