Creating a Browser Extension using SolidJS + WXT II
Tags: solidjs, browserextension, typescript, webdev
Published: 2024-03-22 17:10:28

It's no secret that browser extensions are a powerful tool for enhancing the browsing experience. They can be used to add new features, modify existing ones, or even create entirely new applications. In this article, we'll be talking about the process of creating a browser extension and things you should keep in mind using SolidJS and WXT.

## Lessons learnt

The first time I tried to create a browser extension I was under the impression that it required some kind of special programming language, compilation method or something. Turns out they're just web apps bundled to run in the browser runtime. This means that you can use most of the same technologies that you use to build web apps to build browser extensions. You can use HTML, CSS, and JavaScript to build the user interface, and you can use JavaScript to interact with the browser's APIs.

WXT takes it to the next level though. It's a tool that allows you to scaffold a browser extension project with a single command. It also provides a way to manage your manifest file, pages, scripts and service workers. Did I forget to mention that it also offers framework support? Yup you can use WXT with React, Vue, Svelte, and SolidJS. It really helped in managing and creating the manifest file, background scripts, content scripts, browser actions, page actions, and permissions. Which are all important parts of a browser extension.

In my [previous](https://essiet.dev/posts/creating-a-browser-extension-using-solidjs) article about creating a browser extension using SolidJS, I had initially thought that all you needed was Javascript, HTML, CSS, and a manifest file and you were good to go. Oh, how wrong I was. I had to learn about the limitations of browser extensions, the importance of messaging, and the different APIs that are available to browser extensions. I also had to learn about the different parts of the manifest file and how to manage them. For instance, the permissions, the background scripts, the content scripts, the browser actions, the page actions, and the storage. And did you know that the window size is limited to 800x600? I didn't. This is why I decided to write this article. To share what I've learnt and to help others who might be interested in creating a browser extension.

### Difference between browser extensions and web apps

Here's a table of the differences between browser extensions and web apps:

| Browser Extensions                                                                              | Web Apps                                                                                              |
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| can be used to add new features, modify existing ones, or even create entirely new applications | is a client-server software application in which the client (or user interface) runs in a web browser |
| can be used to modify the behavior of the browser itself                                        | are accessed through the internet and are designed to be used on a web browser                        |
| can be used to interact with the browser runtime's APIs and it's window APIs                    | can be used to interact with the server's APIs                                                        |
| can be used to interact with the browser's tabs, windows, and bookmarks                         | can only interact with their tab and the server                                                       |
| advised to use the browser's storage APIs e.g chrome.storage                                    | web storage APIs a pretty much all you need e.g localStorage and sessionStorage                       |

Concerning the last point, the browser's storage APIs are advised to be used in browser extensions because they can be used in service workers. Service workers are a type of web worker that runs in the background and can be used to intercept and handle network requests, cache resources, carry out messaging, and handle push notifications. They are also used to manage the browser's storage APIs.

### API limitations

There are some limitations to the APIs that are available to browser extensions. For instance, the `window` object is not available to background scripts. This is because background scripts are compiled ahead of time to a single file and can't use the `window` or `document` object. They can only use the `chrome` object and the `chrome.runtime` object. The `chrome` object is used to interact with the browser's APIs and the `chrome.runtime` object is used to interact with the browser's runtime. The `chrome.runtime` object is used to manage the extension's lifecycle, to manage the extension's manifest file, to manage the extension's storage, to manage the extension's messaging, and to manage the extension's permissions.

#### Storage

There are two types of storage that are available to browser extensions:

- Web Storage APIs (can't be used in service worker)
- Browser storage APIs e.g chrome.storage (can be used in service worker)

The web storage APIs are a set of APIs that are used to store web app user data in the browser. The most common example of a web storage API is `localStorage`, which offers a storage limit up to 5MB and is semi persistent. The browser storage APIs are a set of APIs that are used to store extension and service worker user data, with the `unlimitedStorage` permission they can even store data large than 5MB. The most common example is `chrome.storage.local`.

One major thing to keep in mind though is the difference in the way the two storage APIs are used. The web storage APIs are used to store data in the browser's window object and can only be used in the window object. The browser storage APIs are used to store data in the browser's storage object and can be used in the window object and the service worker object. Browser storage APIs are asynchronous as opposed to web storage APIs which are synchronous.

```javascript
// web storage APIs
localStorage.setItem("key", "value");
localStorage.getItem("key");

// browser storage APIs
chrome.storage.local.set({ key: "value" }, () => {
  console.log("Value is set to " + value);
});
chrome.storage.local.get(["key"], (result) => {
  console.log("Value currently is " + result.key);
});
```

WXT provides a wrapper for the different browser storage APIs. As you may have noticed `chrome.storage.local` is only usable on chromium browsers. The wrapper allows you to use single code to interact with different browsers. Making your extension compatible on Chrome, Safari, and Firefox.

```javascript
await storage.setItem("local:key", "value");
await storage.getItem("local:key");
```

#### Permissions

Permissions are something that you should get very acquainted with if you plan to build an extension that functions more than just some simple todo app. This part my be a bit tricky because you have to be very specific about the permissions you request. You can't just request all permissions and hope for the best, because, when submitting your extension to the web store, the permissions you request are reviewed by the Chrome Web Store team. They will only approve the permissions that are necessary for your extension to function. If you request permissions that are not necessary for your extension to function, your extension will be rejected and asked to resubmit with the unnecessary permissions removed.

Here's a comprehensive list of the permissions that are available to browser extensions using Manifest V3 as of the time of writing this article:

- [activeTab](https://developer.chrome.com/docs/extensions/develop/concepts/activeTab)
- [alarms](https://developer.chrome.com/docs/extensions/reference/api/alarms)
- [bookmarks](https://developer.chrome.com/docs/extensions/reference/api/bookmarks)
- [browsingData](https://developer.chrome.com/docs/extensions/reference/api/browsingData)
- [contentSettings](https://developer.chrome.com/docs/extensions/reference/api/contentSettings)
- [contextMenus](https://developer.chrome.com/docs/extensions/reference/api/contextMenus)
- [cookies](https://developer.chrome.com/docs/extensions/reference/api/cookies)
- [debugger](https://developer.chrome.com/docs/extensions/reference/api/debugger)
- [declarativeContent](https://developer.chrome.com/docs/extensions/reference/api/declarativeContent)
- [declarativeNetRequest](https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest)
- [declarativeNetRequestFeedback](https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest)
- [desktopCapture](https://developer.chrome.com/docs/extensions/reference/api/desktopCapture)
- [dns](https://developer.chrome.com/docs/extensions/reference/api/dns)
- [documentScan](https://developer.chrome.com/docs/extensions/reference/api/documentScan)
- [downloads](https://developer.chrome.com/docs/extensions/reference/api/downloads)
- [enterprise.deviceAttributes](https://developer.chrome.com/docs/extensions/reference/api/enterprise/deviceAttributes)
- [enterprise.platformKeys](https://developer.chrome.com/docs/extensions/reference/api/enterprise/platformKeys)
- [fileBrowserHandler](https://developer.chrome.com/docs/extensions/reference/api/fileBrowserHandler)
- [fileSystemProvider](https://developer.chrome.com/docs/extensions/reference/api/fileSystemProvider)
- [fontSettings](https://developer.chrome.com/docs/extensions/reference/api/fontSettings)
- [gcm](https://developer.chrome.com/docs/extensions/reference/api/gcm)
- [geolocation](https://developer.chrome.com/docs/extensions/how-to/web-platform/geolocation)
- [history](https://developer.chrome.com/docs/extensions/reference/api/history)
- [identity](https://developer.chrome.com/docs/extensions/reference/api/identity)
- [idle](https://developer.chrome.com/docs/extensions/reference/api/idle)
- [management](https://developer.chrome.com/docs/extensions/reference/api/management)
- [notifications](https://developer.chrome.com/docs/extensions/reference/api/notifications)
- [pageCapture](https://developer.chrome.com/docs/extensions/reference/api/pageCapture)
- [platformKeys](https://developer.chrome.com/docs/extensions/reference/api/platformKeys)
- [power](https://developer.chrome.com/docs/extensions/reference/api/power)
- [printerProvider](https://developer.chrome.com/docs/extensions/reference/api/printerProvider)
- [processes](https://developer.chrome.com/docs/extensions/reference/api/processes)
- [proxy](https://developer.chrome.com/docs/extensions/reference/api/proxy)
- [scripting](https://developer.chrome.com/docs/extensions/reference/api/scripting)
- [sessions](https://developer.chrome.com/docs/extensions/reference/api/sessions)
- [storage](https://developer.chrome.com/docs/extensions/reference/api/storage)
- [system.cpu](https://developer.chrome.com/docs/extensions/reference/api/system/cpu)
- [system.display](https://developer.chrome.com/docs/extensions/reference/api/system/display)
- [system.memory](https://developer.chrome.com/docs/extensions/reference/api/system/memory)
- [system.storage](https://developer.chrome.com/docs/extensions/reference/api/system/storage)
- [tabCapture](https://developer.chrome.com/docs/extensions/reference/api/tabCapture)
- [tabs](https://developer.chrome.com/docs/extensions/reference/api/tabs)
- [topSites](https://developer.chrome.com/docs/extensions/reference/api/topSites)
- [tts](https://developer.chrome.com/docs/extensions/reference/api/tts)
- [ttsEngine](https://developer.chrome.com/docs/extensions/reference/api/ttsEngine)
- [unlimitedStorage](https://developer.chrome.com/docs/extensions/reference/permissions-list)
- [vpnProvider](https://developer.chrome.com/docs/extensions/reference/api/vpnProvider)
- [webNavigation](https://developer.chrome.com/docs/extensions/reference/api/webNavigation)
- [webRequest](https://developer.chrome.com/docs/extensions/reference/api/webRequest)

#### Why Messaging is Important

Chrome extensions are sandboxed and can't access the DOM a page is running on. So in order to manipulate or identify things in the DOM there needs to be a messaging bridge created between the `contentScript`, `backgroundScript`, and `popup`. Anything from strings to booleans can be sent over this bridge, however, I recommend serializing objects as complex objects with methods and classes will not be able to be sent over the bridge.

Here's a quick example of how to create a messaging bridge between the `contentScript`, `backgroundScript`, and `popup`:

```javascript
// contentScript.js
chrome.runtime.sendMessage({
  text: "hello from the content script",
  to: "background",
  from: "content",
});

// backgroundScript.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log(message.text);
  if (message.to === "background" && message.from === "content") {
    sendResponse({ text: "hello content, this is from the background script" });
  }
  if (message.to === "background" && message.from === "popup") {
    console.log("hello popup, this is from the background script");
  }
});

// popup.js
chrome.runtime.sendMessage({
  text: "hello from the popup",
  to: "background",
  from: "popup",
});
```

We're using the `chrome.runtime` API to send and receive messages between the `contentScript`, `backgroundScript`, and `popup`. The `chrome.runtime.onMessage` method is used to listen for messages and the `chrome.runtime.sendMessage` method is used to send messages.

### Covering all basis in the manifest file

An extensions manifest file is a JSON file that contains metadata about the extension. It's used to configure the extension and to specify the extension's properties. Here's a quick example of a manifest file:

```json
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "description": "This is my first extension",
  "permissions": ["storage", "activeTab", "scripting"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "images/icon16.png",
      "48": "images/icon48.png",
      "128": "images/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ],
  "web_accessible_resources": ["images/*"],
  "options_ui": {
    "page": "options.html",
    "open_in_tab": true
  },
  "icons": {
    "16": "images/icon16.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  }
}
```

As of the time of writing this article manifest V3 is the latest version of the manifest file. Let's do a quick rundown of the properties in the snippet above:

- `manifest_version`: The version of the manifest file. As of the time of writing this article, the latest version is 3.
- `name`: The name of the extension.
- `version`: The version of the extension.
- `description`: A description of the extension.
- `permissions`: An array of permissions the extension requires. For example, the `storage` permission is required to use the `chrome.storage` API.
- `action`: The action property is used to define the browser action. The `default_popup` property is used to specify the HTML file that will be used as the popup. The `default_icon` property is used to specify the icon that will be used for the extension.
- `background`: The background property is used to define the background script. The `service_worker` property is used to specify the service worker that will be used as the background script.
- `content_scripts`: The content_scripts property is used to define the content scripts. The `matches` property is used to specify the URLs the content script will run on. The `js` property is used to specify the JavaScript files that will be used as the content script.
- `web_accessible_resources`: An array of resources that are accessible to the web. For example, the `images/*` resource is accessible to the web.
- `options_ui`: The options_ui property is used to define the options page. The `page` property is used to specify the HTML file that will be used as the options page. The `open_in_tab` property is used to specify whether the options page will open in a tab.
- `icons`: An object that contains the icons that will be used for the extension.

#### Content Security Policy

The `content_security_policy` property is used to define the content security policy for the extension. The content security policy is used to specify the rules for the resources the extension can load. For example, the `script-src 'self'; object-src 'self'` content security policy specifies that the extension can only load scripts and objects from the extension itself.

```json
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'",
    "content_scripts": "script-src 'self'; object-src 'self'",
    "web_accessible_resources": "script-src 'self'; object-src 'self'"
  }
}
```

Note: If you intend to use WASM in your extension, you'll need to add `wasm-eval` or `wasm-unsafe-eval` to the `script-src` directive in the `content_security_policy` property. Like so:

```json
{
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-eval'; object-src 'self'",
    "content_scripts": "script-src 'self' 'wasm-eval'; object-src 'self'",
    "web_accessible_resources": "script-src 'self' 'wasm-eval'; object-src 'self'"
  }
}
```

#### Content scripts

Content scripts are JavaScript files that run in the context of web pages. They can read and modify the DOM of web pages and interact with the web page's JavaScript. Content scripts are used to modify the appearance and behavior of web pages.

To add a content script to your extension, you'll first need to add it to the manifest file.

```json
{
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ]
}
```

Once it's added you can then use the `chrome.tabs.executeScript` method to inject the content script into the web page.

```javascript
// in-page script
chrome.tabs.executeScript({
  file: "content.js",
});

// content.js
console.log("I'm the content script");
```

However, if you plan on using WXT you won't need to do the above as WXT offers a method that can do this for you, eliminating the need for the `chrome.tabs.executeScript` method.

```javascript
export default defineContentScript({
  matches: ["<all_urls>"],
  main() {
    console.log("I'm the content script");
  },
});
```

#### Background scripts

These scrips can also be referred to as service workers. You might think that they are similar to the content script with the only differing feature being that they run in the background, but you couldn't be further from the truth. Background scripts are fundamentally different in a sense that they can not be used like normal JavaScript scripts that run on the web, because they don't have access to the DOM, web page or anything related to it(like localStorage, the document and window objects).

Now you might be wondering since they're so limited what can they even be used for? Well as their name implies they can be used to run background tasks such as notifications, tracking, analytics, event-handling and messaging. I hope you understand why they're also referred to as service workers now.

Setting up a background script is similar to setting up a content script in normal extension workflows, however, when using WXT it can be done even easier.

```javascript
export default defineBackground(() => {
  console.log("I'm the background script");
});
```

Something that you'll have to keep in mind is that the callback can not be made async as background scripts are synchronous. So if you're a fine of the `async/await` syntax you're out of luck, you'll have to use the good old `then/finally/catch` method chain.

#### Browser actions

Web extension browser actions are buttons that extensions add to the browser's toolbar, typically with an icon and optional popup functionality. These buttons allow users to interact with the extension directly from the browser interface. When a user clicks on a browser action button, it can trigger specific actions defined by the extension, such as opening a popup with HTML, CSS, and JavaScript content, or executing certain functions within the extension.

As of the time of writing this article I haven't been able to get these to work using WXT. It might be something with my implementation or WXT doesn't support it yet, however, [here's](https://chat.openai.com/share/0e0f5875-6bf3-48c5-a663-abf503820148) a ChatGPT thread explaining how you can go about trying it out yourself using a none WXT managed code base.

#### Page actions

Page actions in Chrome extensions are specific actions that are relevant to particular pages rather than the browser as a whole. They are represented by icons added inside the browser's URL bar and can be associated with specific web pages. Page actions can be used for features that make sense for only a few pages, and they are hidden by default but can be shown for a particular tab using the `pageAction.show()` method.

## Using WXT for Scaffolding

Now that we're past the basics, let's actually create a project. Run the following command to create a WXT project.

```bash
bunx wxt@latest init sample-project
```

The command will prompt you to choose a framework you would like to use. I recommend SolidJS, but go with whichever you're proficient in.

Once that's done your project's file structure will look like this:

```bash
.
├── README.md
├── assets
│   └── solid.svg
├── entrypoints
│   ├── background.ts
│   ├── content.ts
│   └── popup
│       ├── App.css
│       ├── App.tsx
│       ├── index.html
│       ├── main.tsx
│       └── style.css
├── package.json
├── public
│   ├── icon
│   │   ├── 128.png
│   │   ├── 16.png
│   │   ├── 32.png
│   │   ├── 48.png
│   │   └── 96.png
│   └── wxt.svg
├── tsconfig.json
└── wxt.config.ts

6 directories, 18 files
```

<sup>I used tree to print this. If you're using [pkgx](https://pkgx.sh), just run `pkgx tree` to print the dirctory tree</sup>

### Directory structure

- `assets`: This houses your images, svgs, and any other asset formats you'll be using.
- `entrypoints`: This houses your background scripts, content scripts, and popup.
- `wxt.config.ts`: This manages WXTs behavior and your manifest file settings.
- `package.json`, `public`, `tsconfig.json`: Same old, same old.

### Running the extension

```bash
bun dev
```

## Building the extension

### Using SolidJS for the popup

The popup directory is the UI of your extension. It's where you'll be using SolidJS to create the UI of your extension. The `index.html` file is the entry point of the popup, and the `main.tsx` file is the entry point of the SolidJS app. While the extension is running and open you can modify the `App.tsx` file and see it update in real time.

```tsx
import { createSignal } from "solid-js";
import solidLogo from "@/assets/solid.svg";
import wxtLogo from "/wxt.svg";
import "./App.css";

function App() {
  const [count, setCount] = createSignal(0);

  return (
    <>
      <div>
        <a href="https://wxt.dev" target="_blank">
          <img src={wxtLogo} class="logo" alt="WXT logo" />
        </a>
        <a href="https://solidjs.com" target="_blank">
          <img src={solidLogo} class="logo solid" alt="Solid logo" />
        </a>
      </div>
      <h1>WXT + Solid</h1>
      <div class="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count()}
        </button>
        <p>
          Edit <code>popup/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p class="read-the-docs">
        Click on the WXT and Solid logos to learn more
      </p>
    </>
  );
}

export default App;
```

Note: WXT uses the `@` symbol to refer to the root directory of your project. This is a feature of WXT and not SolidJS. You can get rid of it by changing the `baseUrl` in your `tsconfig.json` file and modifying the paths in your `wxt.config.ts` file. I recommend leaving it as is, because if you're not well versed in configs you might end up breaking the project.

#### Routing

You can add routing to your popup the exact same way you would any other SolidJS app. Here's an example of how you can do that:

```tsx
// App.tsx
import { Router, useRoutes } from "@solidjs/router";

export default function App() {
  const Routes = useRoutes([
    { path: "/", component: <Home /> },
    { path: "/about", component: <About /> },
  ]);

  return (
    <Router>
      <Routes />
    </Router>
  );
}
```

```tsx
// Home.tsx
export default function Home() {
  return <h1>Home</h1>;
}
```

```tsx
// About.tsx
export default function About() {
  return <h1>About</h1>;
}
```

### Managing your manifest properties with WXT

WXT uses a `wxt.config.ts` file to manage your manifest properties. Manifest properties can be assigned through the manifest object in the `defineConfig` function. Here's an example of how you can do that:

```ts
import { defineConfig } from "wxt";
import Solid from "vite-plugin-solid";

// See https://wxt.dev/api/config.html
export default defineConfig({
  vite: () => ({
    build: {
      target: "esnext",
    },
    plugins: [Solid()],
  }),
  manifest: {
    name: "WXT + Solid",
    description: "A WXT extension with SolidJS",
    version: "0.0.1",
    permissions: ["storage"],
    action: "popup",
    icon: {
      16: "/icon/16.png",
      48: "/icon/48.png",
      128: "/icon/128.png",
    },
  },
});
```

### Compile and build

```bash
bun zip
```

This builds and zips your extension into a `.zip` file. You can then upload this file to the Chrome Web Store or drag and drop it into the `chrome://extensions` page to install it.

## Conclusion

That's pretty much all the info you'll need on how to build and package your first extension using WXT + SolidJS. As you may have noticed this article leans heavily towards building for the Chrome Web Store. The same principles apply to building for Firefox, Edge, and Opera. The only difference is the manifest properties and the way you package your extension. I recommend giving the Chrome browser extension api doc a read (https://developer.chrome.com/docs/extensions/reference) to get a better understanding of how to use the Chrome extension api, and the differences between the different browsers.

If you would like to learn more about how to publish and distribute your extension, let me know and I might write an article on that. If you have any questions or need help with anything, feel free to reach out to me on [Twitter](https://twitter.com/devshogun). I'm always happy to help.