exupero's blog
RSSApps

Live reloading Tampermonkey scripts

In a previous post I shared an HTTP file server that can hold a request for a file until the file changes. Besides live reloading this blog as I write posts, I've also used it for writing Tampermonkey scripts.

Tampermonkey is a browser extension that injects custom JavaScript into webpages. Rather than use Tampermonkey's built-in code editor, we can load our script from the file system, though we'd need to grant permission to read local files and I prefer to serve files from a particular directory through a local server. Once serving the script file, we can load it by adding a header comment to the Tampermonkey script:

// @require http://localhost:8000/script.js

While the technique that follows works with plain JavaScript using the built-in eval function, I chose to use Scittle to run Clojure scripts; here's a Tampermonkey header comment that loads a CDN version of Scittle:

// @require https://cdn.jsdelivr.net/npm/scittle@0.5.14/dist/scittle.js

Now, in script.js, we can write:

scittle.core.eval_string('(js/alert "Hello, world")')

and when we load a page with the script injected, it pops up the expected dialog.

Besides the trouble of embedding a string of Clojure code within JavaScript, I've found that Tampermonkey requires some fiddling to clear its cache of external scripts. To avoid that, we can serve our Clojure code from the file system, putting it in the same directory as script.js and updating script.js to load the Clojure file and evaluate it:

fetch('http://localhost:8081/tools.cljs')
  .then(response => response.text())
  .then(script => {
    scittle.core.eval_string(script));
    console.log('Loaded')
  };

Now the script loads anew every time a webpage is loaded.

To add live reloading and evaluate script changes without having to refresh the page, serve the files from a server that holds requests until the file updates (such as the one in the previous post). Then we can add a simple reload loop by fetching the file accordingly (in the case of my server, by adding ?on-update to the URL), and when it returns fetching it again:

const liveReload = () => {
  fetch('http://localhost:8081/tools.cljs?on-update')
    .then(response => response.text())
    .then(() => {
      scittle.core.eval_string(script);
      console.log('Reloaded');
    })
    .finally(liveReload);

liveReload();

Now when changes to the Clojure script file are saved, the Tampermonkey script gets the new code, evaluates it, and applies the changes without requiring the page to be refreshed.

To add UI, we can bring in React and Reagent (CDN paths are given in the Scittle docs) by including them as Tampermonkey header comments.

My full scaffolding script, with some error handling to quit reloading when the server shuts down, looks like this:

const basePath = 'http://localhost:8081/tools.cljs';

fetch(basePath)
  .then(response => response.text())
  .then(script => {
    scittle.core.eval_string(script);
    console.log('Loaded');
  })

let failures = 0;

const liveReload = () => {
    fetch(`${basePath}?on-update`)
        .then(response => response.text())
        .then(script => {
            failures = 0;
            scittle.core.eval_string(script);
            console.log('Reloaded');
        })
        .catch(error => {
            console.error(error);
            failures++;
        })
        .finally(() => {
            if (failures < 20) liveReload();
        })
};

liveReload();

With this setup I wrote about 500 lines of Clojure to add a toolbar of shortcut actions to a complex website, and the process was quite smooth, especially being able to leverage Clojure's defonce to preserve UI state between reloads. If I'd had to reload the entire page every time I wanted to test a new version of the script, iterating on the code would have taken much longer.