
Introduction
I like "Awesome XYZ" lists on Github. They often provide a fantastic summary of the most important libraries/topics in a dev ecosystem. Often enough though, the linked repositories are orphaned and no activity happend in years. I therefore wrote a little browser extensionfor Firefox andChrome that indicates since how many days a repository was not changed. It's free and open source - give it a try!
In this article, I'd like to walk throughthe source code of my first web extension and share my lessons learned.
Mozilla MDN is great
The MozillaMDN documentation is great. I'm a mediocre Javascript developer with zero experience in browser extension development. However the documentation contains almost everything you need to get up and running quickly.
File Organization
The plugin consists of the following relevant files that I will discuss in this article:
- manifest.json
- addindicators.js
Extension Manifest
manifest.json
lives in the root folder of the extension and contains important meta-data:
First I describe my extension and give it a nice icon. For theversion
field it's recommended to usesemantic versioning.
For Firefox some additional settings are required:
- Set
id
to a guid. Even though it is stated as optional in the documentation, you should do it. Otherwise you will run into issues later on (e.g. you can't access local storage) - Set
strict_min_version
to something fairly recent. Otherwise you will get warnings when you try to access APIs that are not available in earlier versions.
Note that Chrome will complain that it does not know about this section. I just ignored this warning.
In thepermissions
section you have to specify the permissions that the plugin requires. Plugins run with high priviledges so only ask for permissions you really need:
- I request access to all pages in the github api subdomain. You can then just use
fetch
and CORS does not get in your way. - I also want to access local storage, so that I can save the settings of the plugin.
Withinoptions_ui
you define the html page that will be rendered as iframe in the settings page of the plugin. In my plugin it allows you to set a custom Github API access token.
content_scripts
allow you to inject custom Javascript into arbitrary pages:
matches
looks for URL patterns. My plugin should only be activated on github.com.js
lists the Javascript files that are injected into these pages.addindicators.js
is the interesting one. I've included abrowser polyfill so that the web extension works on both Chrome and Firefox.
Lesson learned: The order of the scripts matter. First I included the polyfill after my script and it took me a while to figure out why it's not working.
Implementation
addindicators.js
contains the implementation of the plugin. The general flow is very simple:
- Enable or disable debug logs
- Get the API key from the settings or use the default.
- Find all links to Github repositories
- Assemble a GraphQl query and query Github
- Add an indicator to each repository link
Let's look at some code:
Debug Flags
I check if the debug flag is set in the browser local storage and set a global variable accordingly. Alldlog
code you see later just checks on thedebugEnabled
flag and either prints the message to the console or not.
Lesson learned:browser
will only work in Firefox unless you use the polyfill.
Getting the API token
It took me a surprising amount of code to either get the token from local storage (set via the settings dialog), or use the default token. Improvement suggestions are very welcome!
A few things to note:
- I use
browser.storage.sync
instead ofbrowser.storage.local
. This ensures that the token is synced across user devices (if enabled). - In the
isTokenValid
function I also check for the string'Undefined'. I need to do this because I was lazy in my settings dialog implementation... - SECURITY REMARK: I created an unpriviledged Github token for my account that I use as default token. In general this is a horrible security sin so pleasedon't do this at home. I just couldn't quickly find a more convenient way without doing the lovely OAuth dance.
- My function naming is neither consistent nor ideomatic throughout the whole codebase. Sorry for that.
Find links to Github repositories
First I get all links on the page viadocument.getElementsByTagName
and then filter out all links that are not links to github repositories.
Lesson learned: At the first glancedocument.getElementsByTagName
seems to return an array. You can also iterate over it with a for loop like this:
for(const x of document.getElementsByTagName('a')){ // do something}
However it's not a real array thats why this would fail:
// this will fail!!document.getElementsByTagName('a').map(x => x.href())
You have to useArray.from
to change it into an array.
Inmaybe_repo_link
I filter out github urls that match theowner/repository pattern.
Many Github internal links match the same pattern, so I wroteis_repository
to catch at least a few more and avoid errors later on.
Lessons learned: At first I tried to filter the internal URLs like that and was annoyed that it didn't work:
// this does not work!norepo.forEach(x => { if(mayberepo.startsWith(x)){ return false; }});return true;
As very clearly described in theMDN Documentation,forEach
is useful if you're interested in triggering side effects. However you can't break the loop. My return statement was just ignored and the function always returnedtrue
.
Querying Github via GraphQL
GraphQL is a language for accessing your APIs. Compared to REST, you have more flexibility in defining what you want to query in a single call and which data should be included in the response.
I make use of theGithub GraphQL API because it only takes a single HTTP call to query the information for all repositories on a page:
First I assemble the query, then I query Github, and finally I extract the information from the response.
I use a single GraphQL query to fetch the last repository push date of all repositories at once.
Ingraphql_fragment_for
I assemble the query for a single repository. In order to have multiplerepository
clauses, I need to prefix them by an arbitrary unique name.
Note the...fields
section: this states that at this position the fragment with the name "fields" should be inserted. Fragments are a little bit like makros and avoid repetition.
The response of the function looks like this:
r0: repository(owner:"derhackler",name:"catexcel"){ ...fields},
SECURITY_ALERT: I use string concatenation to pass inowner
andname
. THIS IS DANGEROUS and in general a very bad idea. Think of SQL Injection but only for your API. GraphQL supports the concept ofvariables which should be used instead. I couldn't figure out though on how to use them in my scenario...
Ingraphql_for_repos
, I assemble the subqueries for the repositories. In addition I ask for how many API calls I have left (for debugging purposes).
The actual query is an HTTP POST to the Graphql endpoint of Github. Compared to REST, GraphQL APIs are exposed via a single URL.
Inextract_from_graph
I filter out all responses where no repository data could be found (and just ignore it) and convert the timestamp to days untilnow.
Augmenting the Repository Links
In the last step, I add a little indicator to each repository link. I'm not using the DOM elements that I queried orignially, but query for each link individually again. I think this is cleaner as the original element may have been gone already.
Final Verdict
- Writing browser extensions is fun and surprisingly painless
- Writing blogposts reviewing my own code takes me much longer than writing the code
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse