Blog post cover
Arek Nawo
06 Jul 2020
10 min read

Modern Web Extension Development with TypeScript

This series has been discontinued without a proper ending. For anyone affected and seeking guidance for the related topic, contact me through Twitter DM, my Facebook Page, or email, and we should work something out!

I don’t think any in-depth explanation of what browser extensions are is necessary. Many of you reading this have most likely a boat-load of them already installed on your desktop (and maybe even mobile) browsers. However, with this browser extension development series, we’re going to go far beyond just a simple client-side understanding of browser extensions. That’s right - we’re going to create our own browser extension - and do it with the latest and greatest tools available.

For all of you web developers who have mainly stick to making websites, web apps, or even Node.js and Electron apps, browser extension development might seem like a wild, uncharted territory. If so, I assure you - extensions are just as fun and easy to develop as any other JavaScript stuff you’ve done before.

In this series, we’re going to cover everything related to browser extension development - from setup to deployment - by creating our own productivity website blocking extension. To start, today we’ll cover all the general concepts you’ll need to know, as well as set up our modern boilerplate that will use TypeScript for the more maintainable codebase. Sounds good? Then let’s get started!

Why browser extension?

Before going any further, I think it’s important to understand why would you want to create your own browser extension in the first place.

With nowadays JS’ ubiquity, if you know the language and its ecosystem well, you can create anything from the server, desktop, or mobile app down to a simple website. How do browser extensions fit into all of this? They don’t give you the capabilities of native apps, nor simplicity and discoverability of websites. So, what are they good for?

A browser extension works well in a couple of scenarios.

You can use it as a standalone product. In this case, your intent is most likely to provide some browser-bound functionality that otherwise wouldn’t be possible. This includes popular use-cases like ad-blocking or browser theming.

Another option is website integration. Because extensions have access to a lot more features than a website, they can be used in tandem to provide more functionalities to the user. What’s more, they can even integrate with 3rd-party websites to inject custom features to them.

Lastly, a browser extension can be a part of your product ecosystem. It’s like website integration but taken to another level. Extensions work not only with websites but also with native apps. This gives you a possibility to create a unique, tightly-integrated native-app-extension-website experience.

If you find yourself in any of these three categories or already have your own, very personalized scenario, I think you’ll greatly benefit from developing your own browser extension.

General concepts

With the purpose of browser extensions hopefully explained, let’s cover some general concepts that you need to be aware of when developing an extension.

WebExtensions API

Although it wasn’t so good in the past, nowadays, all major browsers implement mostly the same WebExtensions API with only slight differences here and there. We’re talking Firefox which comes with the most spec-compliant implementation, all the Chromium-based browsers, with a little different but definitely the most feature-full implementation, and also Safari, that joined the club not so long ago after WWDC 2020 announcement.

For the rest of the series, we’ll be concentrating mainly on Firefox and Chromium-based browsers, as those have arguably the biggest browser extensions user bases.

Manifest

At the heart of every extension is a manifest.json file, containing all the specifics and metadata about your extension. Everything from your extension’s title and description through icons, resources, and all the main scripts to permissions required by your extension need to be placed there.

Permissions

Now, because extensions have access to a lot more capabilities of the user’s device, they need to be more strictly controlled. In comparison to websites that require permission only for very powerful APIs like Notification API, extensions require permissions for most of their APIs to be allowed to use.

There are a few different kinds of permissions - most notable of which are API and host-related ones. The API permissions, as the name implies, allow you to access the specific API by specifying its name in your manifest.json, while host permissions do the same for different URLs. That’s because, if left unchecked, a malicious extension could potentially steal user’s data from a website it wasn’t explicitly authorized to. Host permissions limit extensions to accessing only the websites the user allowed them to.

That’s pretty much all you need to know right now. Again, these are only some general concepts, and we’ll explore everything deeper when the time comes. Now, let’s get to creating our boilerplate.

Setup

To remind you, our extension will be a productivity-focused one, that will allow its user to block specific websites under certain conditions and for a given amount of time. Sure, it’s nothing revolutionary nor particularly effective (experienced procrastinators would most likely end up simply uninstalling the extension), but it should allow us to learn most of the common browser extensions mechanisms.

Let’s start by creating a folder and an NPM project inside it.

mkdir ProductivityBooster && cd ProductivityBooster && yarn init

I’ll be using Yarn, but you can use any package manager you like.

Manifest

We’ll leave our package.json for now, and instead move to create the mainfest.json. Keep in mind that it must be placed at the root of your extension’s folder.

{
  "manifest_version": 2,
  "name": "ProductivityBooster",
  "version": "0.1.0",
  "description": "An example browser extension for blocking distracting websites.",
  "permissions": [],
  "icons": {
    "64": "assets/icon/64.png",
    "128": "assets/icon/128.png",
    "256": "assets/icon/256.png"
  }
}

Above you can see my current manifest.json file. Aside from some metadata about the extension and manifest itself, we’ve got an empty permissions array which we’ll populate later on in the series and an icons field, which points to different sizes of our extension’s main icon. Because the icon can be shown in many places like the browser’s toolbar or extension page and at different screen resolutions, it’s recommended that you include at least 2 different sizes of it in your manifest - ideally 48x48 and up. In theory, you can use SVG icons instead of PNG, but this will still require you to specify (in this case the same) filename for each size in your icons field. And you’ll also need to keep your SVG without any unnecessary clutter and with proper viewBox attribute, making exporting multiple PNGs much easier.

Dependencies

Back to our package.json. Usually, when working with browser extensions the old way, you’d most likely need no NPM project setup whatsoever. But, because we’re planning on using TypeScript and modern tooling it is required.

So, let’s put it to a good use by installing our dependencies.

yarn add typescript parcel-bundler web-ext parcel-plugin-web-ext-tool eslint eslint-config-xtrict webextension-polyfill-ts

Well, that’s quite a lot of stuff being installed right there. Let me walk you through what each of them does.

We start by installing typescript and parcel-bundler. I’ve chosen Parcel as my bundler, mainly because of its simplicity. Its all-in-one zero-config design allows you to get to work essentially as soon as you install it. Its only downside is that the output bundle is pretty big when compared to its competitions, however, this doesn’t matter as much for browser extensions as it does for websites.

Next, we’re installing web-ext. It’s an open-source command-line tool created by Mozilla for developing Firefox extensions, although it works pretty well with Chromium-based browsers too. It provides useful features such as automatic extension reloading and built-in Firefox deployment commands.

The parcel-plugin-web-ext-tool is a 3rd-party Parcel plugin that integrates web-ext with Parcel, for it to run when the Parcel finishes bundling the code. It’s very helpful during development.

As a bonus, I also install eslint and my custom ESLint shareable config called Xtrict for strict code linting that will hopefully allow us to write more readable code.

And finally, the webextension-polyfill-ts package is a TypeScript wrapper around webextension-polyfill which on its own, is a polyfill by Mozilla to help out with cross-browser WebExtensions API incompatibilities. That’s how we get our extension to work (mostly) without any issues across many different browsers, together with this sweet, sweet TypeScript-powered autocompletion.

Configuration

With all dependencies installed, all we now need is a bit of additional configuration.

First, something that’s the least bound to our project - the ESLint config.

// .eslintrc.js
module.exports = {
  parserOptions: {
    project: "./tsconfig.json",
    ecmaVersion: 2019,
    sourceType: "module",
  },
  env: { browser: true, es6: true },
  root: true,
  extends: ["xtrict"],
};

Here, all I do is setting up the ESLint & TypeScript integration, extending from my shareable config.

I also create a .eslintignore file to let ESLint know which files it shouldn’t check:

web-ext-config.js

Visible here web-ext-config.js is a web-ext configuration file. Because it’s required to be in pure JS, we don’t want our ESLint setup to check it.

Inside of the file, we can configure options for each of the web-ext subcommands. The only one that we’re interested in right now is web-ext run that will be automatically executed when Parcel finishes bundling our code, thanks to parcel-plugin-web-ext-tool.

module.exports = {
  run: {
    target: ["chromium"],
  },
};

With the above configuration, we instruct the tool to load our extension for testing within a Chromium-based browser (either Chrome or Chromium). Without that, our extension would be loaded into Firefox by default. It’s a great option to have when testing cross-browser compatibility. However, keep in mind that you have to have the corresponding web browsers installed on your machine.

I’ve also created a .browserslistrc file, which will be used by Parcel to in turn be passed down to Babel to determine necessary code transforms.

last 2 versions

For simplicity, I decided to support only the 2 latest versions of each browser. Keep in mind that in reality, this depends more on the features you use rather than this setting alone. Most browsers that support WebExtensions API are “evergreen”, meaning they’re usually pretty up-to-date. If you’re still worried, remember that you can always limit your extension’s minimum supported browser version, to prohibit users from installing it on incompatible browsers. We’ll cover it later on in the series when we’ll be dealing with extension deployment.

Lastly, our tsconfig.json and package.json files.

{
  "extends": "./node_modules/eslint-config-xtrict/tsconfig.json",
  "exclude": ["node_modules"],
  "include": ["src/**/*"],
  "compilerOptions": {
    "lib": ["esnext", "dom"],
    "target": "ES2015",
    "outDir": "lib",
    "declaration": false
  }
}

Here, I extend my base tsconfig.json that comes bundled with my shareable ESLint config and configure a few additional options.

As for the package.json file, all that’s really left to configure here are the scripts.

{
  "name": "productivity-booster",
  "version": "0.1.0",
  "description": "An example browser extension for blocking distracting websites.",
  "repository": "https://github.com/areknawo/ProductivityBooster",
  "author": "Arek Nawo <[email protected]> (https://areknawo.com)",
  "license": "MIT",
  "private": true,
  "scripts": {
    "build": "parcel build [input.ts] --experimental-scope-hoisting --no-source-maps --public-url ./",
    "watch": "parcel watch [input.ts] --public-url ./"
  },
  "dependencies": {
    "eslint": "^7.4.0",
    "eslint-config-xtrict": "^2.0.1",
    "parcel-bundler": "^1.12.4",
    "parcel-plugin-web-ext-tool": "^0.1.2",
    "typescript": "^3.9.6",
    "web-ext": "^4.3.0",
    "webextension-polyfill-ts": "^0.19.0"
  }
}

We use Parcel’s watch and build commands to set them up accordingly. I also passed a few additional options like --experimental-scope-hoisting for smaller, tree-shaken builds, as well as --public-url to ensure our assets are correctly referenced within the output files, and --no-source-maps to disable source maps in the production builds.

For now, the [input.ts] serves mainly as a placeholder, as we’ll be putting some real code there very soon.

It’s only the beginning

With all that said, we now have what I’d call a pretty functional boilerplate for our upcoming project. We also have an understanding of motivation and basic concepts behind browsers extensions.

If you didn’t want to follow the setup guide and want to use the boilerplate, feel free to check out this GitHub repo.

So, that’s it for now. Again, be sure to follow me on Twitter, Facebook, or through my weekly newsletter to not miss the next installment of this series. Also, leave a comment if there’s anything related to this post that you’d like a deeper explanation on, or if you want me to cover something specific in the next part. And as always - happy coding!

If you need

Custom Web App

I can help you get your next project, from idea to reality.

© 2024 Arek Nawo Ideas