DEV Community

Steve Baker
Steve Baker

Posted on • Edited on

HamsterScript - a micro-library for TamperMonkey

The following is just some thoughts and findings I've had on creating a micro-library for personal use for TamperMonkey and Userscripts.

The library is not published yet as it is still a proof of concept and a little messy currently, meaning the API isn't very stable

TamperMonkey Intro

Something I've recently started goofing around with is TamperMonkey, a browser extension that allows you to "install and create userscripts, which are JavaScript programs that can be used to modify web pages".

I enjoy automating and hacking things together and TamperMonkey scratches that itch for me. Browser extensions are very powerful but sometimes you just need to restyle a webpage or inject a little bit of JS. A few scripts I've created recently that I found myself using daily are:

  • Display area stats for properties on Rightmove (i've been looking at houses a lot lately)
  • Add a hyperlink on every GitHub PR to a "short lived environment" & a link to JIRA tickets based on ticket ID found in the branch name (assuming one exists)
  • Autopopulating web forms for testing systems (browser extensions like iMacros do exist but can be very limited)
  • Enrich websites I'm currently developing with detailed debugging information (e.g a toolbar to display Session info, the current git commit_ref, links to Kibana for the logged in user, cloud tools, links to CMS and CRM entries, envconfigs at a glance, all sorts reall). Can be very useful for less technical people too such as testers and product owners so they can diagnose issues on dev environments.

Why Create A Library Though?

I find myself constantly copy/pasting boilerplate code between userscripts or re-writing from memory and introducing bugs. Want to insert an element, then you'll need a lot of lines of code for this:

var textEl = document.createElement('p')
textEl.innerText = "foo";
textEl.className = "";
textEl.onclick = populate;
logo.appendChild(textEl);
// or
someOtherElement.parentNode.appendChild();
// or this monstrosity
textEl.insertBefore(someOtherElement, textEl.firstChild);
// or there's other ways to do this!
Enter fullscreen mode Exit fullscreen mode

The same applies for making fetch requests too. The browsers built in fetch function blocks most requests due to CORS. So you'll need to use GM_xmlhttpRequest instead. But that doesn't use promises because xmlhttpRequest is ancient. So you'll probably want to wrap that in a Promise. Then you'll need to parse the response. Then some other stuff. I've needed to copy/paste something like this and change bits for multiple scripts now:

// The following userscript @grant is required to make cross origin requests:
// @grant GM_xmlhttpRequest

async function parseWebsite(postcode) {
    const url = `https://www.ilivehere.co.uk/check-my-postcode/${postcode}`;

    const response = await Request(url); // Request is defined below
    const html = response.responseText;
    var parser = new DOMParser();

    // Parse the text
    var doc = parser.parseFromString(html, "text/html");
    const number = doc.querySelector('.ilivehere-rating-number').textContent;
    return number;
}

// Wrap xmlRequest in a Promise
function Request(url, opt={}) {
    Object.assign(opt, {
        url,
        timeout: 2000,
        responseType: 'json'
    })

    return new Promise((resolve, reject) => {
        opt.onerror = opt.ontimeout = reject
        opt.onload = resolve

        GM_xmlhttpRequest(opt)
    })
}
Enter fullscreen mode Exit fullscreen mode

Another example - reading a cookie. Unfortunately there's no document.readCookie("foo") so I find myself copy/pasting this and other 'utility' functions across Userscripts:

const getCookie = (name)=> {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

Enter fullscreen mode Exit fullscreen mode

HamsterScript Intro

Assuming you've made it this far, thanks for sticking around! I decided to create HamsterScript for two reasons:

  • Make it slightly easier to create future Userscripts by using a library with a set of easy to use, common helper functions
  • Create a well documented "cheatsheet" for personal Userscript knowledge

HamsterScript Usage

Installing a library is really simple in a Userscript. Heck, you could even include frameworks like Vue or jQuery if you wanted or libraries like D3 or your own raw JS files hosted on a CDN such as github!

// @name         [HamsterScript Example]
...
// @grant        GM_xmlhttpRequest
// @require      <placeholder>/hamsterscript-latest.min.js
...
(async function() {
    'use strict';

    const doc = await hamster.fetchDom("<some-url>");
    const stats = doc.querySelector('.body-band-highlight').childNodes[1].textContent;
        const newBtn = hamster.insert({tag: "p", text}, `[itemprop="streetAddress"]`);
})

Enter fullscreen mode Exit fullscreen mode

Only 3 lines of code for one of my most common use cases for Tampermoney. The same in vanilla JS is 20+ lines. Understandably you may lose some readability (what does hamster.fetchDom actually do?). Well as I previously mentioned, one goal of this library is to create a "cheatsheet" along with abstracting common functions into a reusable library! You can see what hamster.fetchDom does in the HamsterScript docs OR view the annotated code on GitHub, along with a full explanation and alternative methods

Findings

I doubt anybody will ever come across or use this framework or choose to use it but it's been a fun little exercise for a few reasons:

  • Learnt JSDoc and generating documentation
  • Found out about multiple TamperMonkey APIs and other scripts like waitForKeyElements
  • Comparing the "before" and "after" of using HamsterScript (in one case, 95 lines was simplified to 55 lines)
  • Creating pipelines from scratch to minify and deploy my library

Future Steps

As this is only a prototype, there's definitely a lot of improvements I could add in the future:

  • Think about versioning my library
  • Hosting multiple versions on a CDN along with multiple versions of documentation
  • Prevent breaking changes
  • Improve minification pipelines
  • Add HamsterScript (and general TamperMonkey) examples
  • Create a testing pipeline

Top comments (0)