48

For security reasons, Tampermonkey scripts are not saved in accessible files, but in a plugin data. The only way to edit them is to use Tampermonkey's integrated editor.

However, I'd rather use IDE, with all its features. I also want to use webpack to pack the script from multiple files.

To do that, I need a way to programmatically change the script in Tampermonkey to a new version. So far, what I did was manually copy & paste the new script into Tampermonkey's editor and that's really exhausting.

How can I do this automatically?

double-beep
  • 5,031
  • 17
  • 33
  • 41
Tomáš Zato
  • 50,171
  • 52
  • 268
  • 778
  • If the file is on a local path `file://...`, can you fetch it and inject into the page through the tampermonkey script? I know that's not exactly what you are looking for, but sounds like something that would work. – Nisarg Shah Mar 27 '18 at 10:07
  • 1
    Yeah, all you say is true. It would work but it's not what I want so I posted here to get a better solution. – Tomáš Zato Mar 27 '18 at 10:16
  • Have you tried running local https server, installing from there and updating and/or setting the `@updateURL` meta? I didn't manage to make it work right now, but there are some clues it might have worked at least in some point in the past: https://stackoverflow.com/questions/38023717/ – myf Mar 27 '18 at 10:52
  • @myf That wouldn't update automatically when I reload the page though. I will have to run local server anyway, to run webpack etc, but I need to force tampermonkey to update when the server wants it to – Tomáš Zato Mar 27 '18 at 10:53

8 Answers8

67

I found my own way to it, so it's not official or anything, feel free to make your own adjustments. You'll be able to code in your editor and see the changes reflected in the browser without a nuisance.

Set up

We'll make our TM script to be without code but @require a local file. This way we can edit it locally and changes will take effect on browser reload, without copy-pasting.

  1. Go to Chrome -> Extensions (or paste 'chrome://extensions' to your URL bar) and find the TamperMonkey 'card'. Click details. On the page that opens, allow it access to file URLs:

allow access to file URLs switch

  1. Save your script file wherever you want in your filesystem. Save the entire thing, including the ==UserScript== header. I'm using macOS, so my path is: /Users/me/Scripts/SameWindowHref.user.js

  2. Now, go to the TM's dashboard in your browser, open the script in question in its TM editor and delete everything except the entire ==UserScript== header

  3. Add to the header a @require property pointing to the script's absolute path.

At this point, TM's editor should look something like this:

TM's editor with only the userscript metadata

Possible gotcha: Using the file:// URI scheme at the beginning of your @require path is now required. On Windows systems would be:

// @require      file://C:\path\to\userscript.user.js

For macOS and *nix, you need three slashes in a row:

// @require      file:///path/to/userscript.user.js

Execution Contexts

If you have multiple JavaScript files, each specified with a @require key, it is important to understand how and when each script is executed. This is important when using external libraries (like jQuery), or when breaking up the monolith of a script you have there in a few files.

The @require paths can reference *.user.js or simply *.js files, and any UserScript-style comment headers in these files have no effect.

From the main script's ==UserScript== header, all @require files are text-concatenated in the order specified, with a single newline separating each file. This amalgamation is then executed as one large script. Note that this means any function or variable declared in the outermost scope of any file behaves as if it was declared in the outermost scope of every file, and certain syntactic errors in one file may influence how subsequent files are interpreted. Additionally, to enable Strict mode on all of your files, 'use strict'; must be the first statement of the first @require'd file.

After all the @require files are run, the primary UserScript (the one accessed by TamperMonkey's editor) is run in a separate context. If Strict mode is desired, it must also be enabled here.

Given such opportunity for confusion, it is good practice for each file to wrap all code within an IIFE (and a function-level 'use strict';) in order to limit the scope to individual files.

Workflow

Now every time that script matches (@match), TamperMonkey will directly load and run the code straight from the file on disk, whichever path is in @require.

I use VSCode, so that's where I work on the script, but any text editor will do. It should look like this:

VSCode IDE with the userscript's code

Notice how TM's editor and your IDE/Editor have the same header. You can now close the TM's editor. If everything is correct, you won't need it open anymore.

Now, every change in the code is saved automatically by this particular editor. If yours doesn't autosave, remember to save before going to the browser to test it.

Lastly, you'll have to reload the website to see the changes, just like before.

Bonus tips

Consider using it git. It's not beginner-friendly but it will make you look cool, helps to have a sane development experience (can roll back changes and easily try parallel ideas), and if you pair it with GitHub you can release new updates to your future users for free.

Working with GitHub (or other SCMs)

You have to add an @updateURL tag followed by the URL with the raw file from GitHub or whatever provider you chose. GitHub's example:

updateURL on a userscript on GitHub

Note that a @version tag is required to make update checks work. The vast majority of devs won't need the @downloadURL tag, so unless your script has a massive follower base, use @updateURL.

TM will check for updates as often as it's configured; from the settings tab:

enter image description here

Externals sets how often the scripts called from your script's @require are checked to update (e.g., jQuery).

You can also "force" an update check:

Check for userscript updates option

Using external libraries (like jQuery)

It must be present in at least the header shown in TM's editor for Chrome to load it. However, I recommend keeping both headers the same (the shown in TM and the local file) to avoid confusion and maybe weird bugs. Then, you just @require it like this:

// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js

RTFM

Take a look at TM's documentation page; it doesn't bite! It's actually very concise, and with a quick read you'll have a big picture of what you can do without much effort!

Neithan Max
  • 11,004
  • 5
  • 40
  • 58
  • 2
    In case anyone runs into a `Tampermonkey: couldn't load @require from forbidden URL` error on a Mac, try adding `file:`. That worked for me. – Harm Jul 20 '19 at 12:07
  • I am very excited about this, and the Browsersync feature is a must I believe. But how do you set that up together with the script you have demonstrated above? – Martin Klasson Sep 20 '19 at 08:37
  • I don't have any code with me right now, but it's surprisingly simple. What you want is browser-sync monitoring your local userscript.js file. If that file changes, you want your browser to refresh. That's called file sync. Install the browser-sync command line and check out the options. In the end, it's a simple one-liner. – Neithan Max Sep 21 '19 at 02:31
  • Can not use this for firefox? Since firefox does not have the option "Allow access file URLs" – mingchau Jan 10 '20 at 08:38
  • @mingchau I don't know Firefox that good but I think it will work regardless. Have you tried it directly and see if it works? Please let me know so I can give more information in the answer. – Neithan Max Jan 15 '20 at 04:10
  • 3
    It does not work on Firefox. Console output: `Tampermonkey: couldn't load @require from URL file://F:\Dropbox\codes\xxx.user.js`, I also found the issue on Github: https://github.com/Tampermonkey/tampermonkey/issues/347 – mingchau Jan 17 '20 at 14:02
  • Hello, I saw the answer of UserScript local development, which mentioned that browser-sync can be used to refresh automatically, but browser-sync does not seem to be used for UserScript development. UserScript works on web pages designed by others, and browser-sync seems to only refresh local HTML documents automatically. I looked through all the documents I could find, but couldn't find a solution. How to solve this problem? Have you ever used browser-sync to help develop UserScript? https://stackoverflow.com/questions/63436668 @Carles Alcolea – Andy Aug 16 '20 at 12:09
  • @if_ok_button I'm sorry, I tried to figure it out but this was a while ago. Maybe I added to the userscript the browser-sync callback or used a different tool altogether. I removed it from my answer to avoid confusion. – Neithan Max Aug 22 '20 at 22:45
  • 1
    thanks,I found that livereload can solve this problem @Carles Alcolea – Andy Aug 30 '20 at 00:55
  • a rather nice solution (based on this great answer) for the Firefox issue was for me with simple nginx HTTP server setup: https://stackoverflow.com/a/69713481/1915920 – Andreas Covidiot Oct 25 '21 at 19:11
5

in server mode, you can add your code entry as script src

// ==UserScript==
// @name               dev:example
// @namespace          https://github.com/lisonge
// @version            1.0.1
// @author             lisonge
// @description        default description zh
// @icon               https://vitejs.dev/logo.svg
// @match              https://i.songe.li/
// @grant              GM.info
// @grant              GM.deleteValue
// @grant              GM.getValue
// @grant              GM.listValues
// @grant              GM.setValue
// @grant              GM.getResourceUrl
// @grant              GM.notification
// @grant              GM.openInTab
// @grant              GM.registerMenuCommand
// @grant              GM.setClipboard
// @grant              GM.xmlHttpRequest
// @grant              unsafeWindow
// @grant              window.close
// @grant              window.focus
// @grant              GM_info
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_deleteValue
// @grant              GM_listValues
// @grant              GM_addValueChangeListener
// @grant              GM_removeValueChangeListener
// @grant              GM_getResourceText
// @grant              GM_getResourceURL
// @grant              GM_addElement
// @grant              GM_addStyle
// @grant              GM_openInTab
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @grant              GM_notification
// @grant              GM_setClipboard
// @grant              GM_xmlhttpRequest
// @grant              GM_download
// @grant              GM.addStyle
// @grant              GM.addElement
// @grant              window.onurlchange
// @grant              GM_log
// @grant              GM_getTab
// @grant              GM_saveTab
// @grant              GM_getTabs
// @grant              GM_cookie
// ==/UserScript==

;(({
  entryList = [],
  mountGmApi = false
}) => {
  window.GM;
  const monkeyWindow = window;
  if (mountGmApi) {
    const _unsafeWindow = Reflect.get(monkeyWindow, "unsafeWindow");
    if (_unsafeWindow) {
      Reflect.set(_unsafeWindow, "unsafeWindow", _unsafeWindow);
      console.log(`[vite-plugin-monkey] mount unsafeWindow to unsafeWindow`);
      const mountedApiList = [];
      Object.entries(monkeyWindow).filter(([k]) => k.startsWith("GM")).forEach(([k, fn]) => {
        Reflect.set(_unsafeWindow, k, fn);
        mountedApiList.push(k);
      });
      console.log(
        `[vite-plugin-monkey] mount ${mountedApiList.length} GM_api to unsafeWindow`
      );
    }
  }
  (() => {
    Object.defineProperty(document, "__monkeyWindow", {
      value: monkeyWindow,
      writable: false,
      enumerable: false
    });
    console.log(`[vite-plugin-monkey] mount monkeyWindow to document`);
  })();
  const createScript = (src) => {
    const el = document.createElement("script");
    el.src = src;
    el.type = "module";
    el.dataset.source = "vite-plugin-monkey";
    el.dataset.entry = "";
    return el;
  };
  const { head } = document;
  entryList.reverse().forEach((s) => {
    head.insertBefore(createScript(s), head.firstChild);
  });
  console.log(
    `[vite-plugin-monkey] mount ${entryList.length} entry module to document.head`
  );
})({
  "entryList": [
    "http://127.0.0.1:5173/@vite/client",
    "http://127.0.0.1:5173/src/main.ts"
  ],
  "mountGmApi": false
});

now you can use My library vite-plugin-monkey, its consistent with the normal web app development experience, just like vue/react

vue-ts

Hot Module Replacement

hmr

lisonge
  • 429
  • 4
  • 9
  • 2
    I just wanted to say that this is amazing! I just started with it, but it's 100% everything I was looking for and without question the best solution for this I've found. Thank you! – jreed121 Apr 27 '23 at 17:11
2

You can use the ViolentMonkey extension instead of TamperMonkey. ViolentMonkey has a feature to regularly check for updates to a local file, making the development process much more streamlined.

During the installation of the userscript, simply check the "Track local file before this window is closed" checkbox and leave the installation dialog tab open. The userscript will be automatically updated when changes are made to the file on disk, without any user intervention required. Simply refresh the page on which the script is running, and the new version of the userscript will be loaded.

Here is a screenshot of the "Track local file" checkbox in the ViolentMonkey installation dialog:

Screenshot of the "Track local file" checkbox in the ViolentMonkey installation dialog

1

to the @require-approach in Carles' answer I'd like to add a rather nice solution for Firefox devs (or any other) where local file access is not possible:

use nginx as a simple local HTTP server as follows:

  • download nginx and unzip somewhere, e.g. C:\nginx

  • adjust <nginx-dir>/conf/nginx.conf to serve your projects (e.g. C:\my-project\src\tampermonkey.myapp.js) scripts directly and immediately (no caching) on access:

    server {
      ...
      location / {
        #root   html;
        root    C:/my-project/;
        ...
    
        # kill cache
        expires -1;
    
  • start nginx, e.g. via cmd /c "C:\nginx\nginx.exe"

    • (stop it later e.g. via cmd /c "C:\nginx\nginx.exe -s stop")
  • change the @require accordingly (removing everything below //==/UserScript==) (and save it):

    // ==UserScript==
    //@name         My App Addon
    //@match        http://foo
    //@require      http://localhost/src/tampermonkey.myapp.js
    //==/UserScript==
    
  • done

Zoe
  • 27,060
  • 21
  • 118
  • 148
Andreas Covidiot
  • 4,286
  • 5
  • 51
  • 96
  • 1
    Don't add fluff to your posts: https://meta.stackoverflow.com/q/260776/6296561 – Zoe Nov 01 '21 at 09:11
0

I would indeed use @require, but if you have design changes also use GM_addStyle

// @require     file:///
// @grant       GM_addStyle
Ibn Rushd
  • 731
  • 6
  • 7
0

Trim21 provides, probably the best large-scale UserScript development solution so far, using webpack to cooperate LiveReloadPlugin realizes modular development and automated testing.

Here is the project.

It can use ES5/ES6 and TypeScript to develop modular scripts on IDE. It's really easy to use!

Integrated LiveReloadPlugin, you can directly refresh any @matchURL.

It is better than the previous scheme, which greatly improves the development efficiency of UserScript!

Andy
  • 1,077
  • 1
  • 8
  • 20
0

Rather than setting up Nginx, or having to configure Chrome to allow local files you can just use ngrok to proxy the local files.

This example assumes your files are in the /dist directory and you have installed nodejs.

npx serve -p 5000 ./dist & npx ngrok http 5000 --log stdout

or using package.json which you can run using npm start:

  "scripts": {
    "start": "npm-run-all -l -p serve watch ngrok",
    "build": "webpack",
    "watch": "webpack --watch",
    "serve": "npx serve -p 5000 ./dist",
    "ngrok": "ngrok http 5000 --log stdout"
  },

This will serve the files on port 5000 and give you the external URL, look for this line in the console output:

addr=http://localhost:5000 url=https://101b-110-150-114-111.ngrok.io

Then create a userscript like this:

// ==UserScript==
// @name         Script Name
// @namespace    http://example.com/
// @version      0.1
// @description  Description
// @author       Your Name
// @match        https://example.com/*
// @grant        none
// ==/UserScript==

(function() {
  var ngrokUrl = 'https://101b-110-150-114-111.ngrok.io'; // <-- Update this
  var remoteScript = document.createElement('script');
  remoteScript.src = ngrokUrl  + '/bundle.js?ts='+(+new Date());
  document.body.appendChild(remoteScript);
})();

This will inject a script stored in ./dist/bundle.js and reload it everytime the page refreshes.

Note If you don't sign up for ngrok account your connection will time out after two hours and you will need to restart it and update the ngrokUrl URL.

P. Galbraith
  • 2,477
  • 21
  • 24
-2

Hack around: I needed to reference a static image file, so I converted the image to a base64 string and then added it directly to the script. I believe, a similar approach could also work for other files of small size.

penduDev
  • 4,743
  • 35
  • 37
  • Yes, this is not an uncommon approach, but it doesn't address the issue in the question at all. – Tomáš Zato Jun 23 '21 at 00:03
  • Hey Tomas! I could have never known that I posted it on a question that's not even related. I somehow landed on this page while searching for a solution to my problem, so posted my hackaround here. Will let this answer lie around until someone downvote it. – penduDev Jun 26 '21 at 06:57