Writing a browser extension in typescript
Every once in a while I write some small web browser extension to do little things. You could, for example, write an extension for showing the ID's of elements in a web or replacing AI with "a bunch of dudes", and after firefox adopting WebExtensions as the way to write these plugins we can make it so the same code works with Firefox and Chrome!
While we're at it, let's see which extra steps we have to take to write these extensions on Typescript, after we have a running Javascript version.
So let's go ahead and write a simple extension which allows us show the ID's of elements on a web.
This could be used, for example, to generate links which end with #id-of-element
which when opened will
make the browser scroll to that element. We won't go in depth, let's make it fast.
The base
Firstly, for the extension to be understood by the browser it needs some files to be created, so let's make a directory for our extension. I'll go with show-element-ids
as the name, but you can use whatever you prefer.
mkdir show-element-ids
cd show-element-ids
Inside, create a manifest.json
file with a content like this (minus the #comments
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Note that you'll have to remove everything after the #
(included) from the manifest.
These are just comments and won't be understood by the browser.
Note, also, that this differs a little from what's on Mozilla wiki: Your first extension (a great resource!) because we'll write an extension that will run on demand instead of one that is automatically triggered when you enter a web. You can see that instead of "content_scripts"
we have "default_actions"
.
Now let's create a directory for the icons named icons
, in the same directory as the manifest.json
file.
We can get an icon from LogoDust for starters (they are open-source and pretty good), just resize it to 48x48 pixels and place it into icons/icon-48.png
.
One last step before we can load the firt version, let's create the view that will be created when the toolbar icon is pressed. Create a popup
directory and write some HTML inside the show-element-ids.html
file.
mkdir popup
echo 'This is a test' > popup/show-element-ids.html
Ok, now we're ready to load this extension on the browser.
Loading the extension
This step differs depending on the browser you use, so I'll just point you to the relevant documentation for each browser:
From here on I follow the steps considering we're using firefox to test the application, but the same code will be compatible with both browsers.
The result now show be this:
Adding some logic
Now that we have loaded a interface for the extension, we can add some logic. For starters we'll add a button that triggers an alert()
. To do this update the popup/show-element-ids.html
file to contain this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Now, to add functionality to the button, add (in the popup
directory) this show-element-ids.js
file:
function activate() {
alert("Running!");
}
document.getElementById("run_button").onclick = activate;
Note that we cannot add Javascript events on the HTML file (like handlers for onclick
events). Instead we have to add then from the Javascript file.
Right now, our extension should work as this:
Modifying the current tab
The goal of our extension was to show the ID's of elements in a web. So let's start by injecting code in the current tab, which then we can adapt for our goal.
Note that to manipulate the active tab we need the "activeTab"
permission, which we requested on the manifest.json
file. Also, special tabs like the ones containing browser pages (the ones starting with about:
) cannot be manipulated.
First, to launch the alert()
dialog on the tab, let's define some useful functions on show-element-ids.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
And update the code that we had previously there to:
1 2 3 4 5 6 7 8 9 10 11 |
|
Finally, just write the desired logic on the popup/injected.js
file, for example:
1 |
|
Do note something, though launching an alert()
inhibits the popup code to detect that the injected one has finished, we can see that the promise result (console.log("Execution successful");
) is never run. This wouldn't happen if we used console.log()
on the browser tab, instead of running alert()
.
Detecting and showing element IDs
One thing that remains before we can start moving our code to Typescript is to detect the elements with an ID
and show them somehow (the whole point of the extension). We can update popup/injected.js
to do this.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
This will just append to the HTML elements with and .id
property a text showing that ID with an #
before.
Moving to typescript
At last, the only thing that remains is to move our Javascript code to Typescript. To do this create a src
directory, which we'll use to store the "uncompiled" typescript code, and copy there the Javascript files.
mkdir src
mv popup/injected.js src/injected.ts
mv popup/show-element-ids.js src/show-element-ids.ts
Remember to install the Typescript compiler
npm install -g tsc
And write the typescript configuration for both files. Normally a single configuration can ingest multiple files but keep in mind that we want two outputs, as the injected
script has to be separate so it can be loaded into the tab. As such, these are the configurations, just add them on the same directory as the manifest.json
.
tsconfig_injected.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
tsconfig_popup.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
And let's throw in a Makefile
to simplify the compilation as using webpack for this seems overkill:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
With this we can build our extension with make
and it'll generate a nice ZIP file on dist/extension.xpi
.
But if we try to do this make
, we find the following errors:
> make
tsc --project tsconfig_injected.json
src/injected.ts:7:20 - error TS2339: Property 'insertRule' does not exist on type 'StyleSheet'.
7 stylesheet.insertRule(rule, stylesheet.cssRules.length);
~~~~~~~~~~
src/injected.ts:7:48 - error TS2339: Property 'cssRules' does not exist on type 'StyleSheet'.
7 stylesheet.insertRule(rule, stylesheet.cssRules.length);
~~~~~~~~
src/injected.ts:10:27 - error TS2495: Type 'NodeListOf<Element>' is not an array type or a string type.
10 for (const element of elements_with_ids) {
~~~~~~~~~~~~~~~~~
Found 3 errors.
make: *** [Makefile:12: popup/injected.js] Error 2
There are some typing ambiguities,
so we have to update our src/injected.ts
and show-element-ids.ts
file so we can compile them as Typescript:
src/injected.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
src/show-element-ids.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
And with this, the compilation now runs successfully:
> make
tsc --project tsconfig_injected.json
tsc --project tsconfig_popup.json
zip dist/extension.xpi popup/injected.js popup/show-element-ids.js manifest.json popup/show-element-ids.html icons/icon-48.png
adding: popup/injected.js (deflated 51%)
adding: popup/show-element-ids.js (deflated 53%)
adding: manifest.json (deflated 52%)
adding: popup/show-element-ids.html (deflated 38%)
adding: icons/icon-48.png (stored 0%)
And we have an extension which we can work on, built from Typescript code!
You can find the final code on GitHub or GitLab.