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!
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)
})
}
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();
}
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"]`);
})
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)