Building Modern Cross Browser Web Extensions: Project Setup (Part 2)

  January 20, 2025

 #webextensions#typescript#react#wxt

In the previous post, we discussed the basics of Web Extensions and the tools we will be using to build our extension. Out of the three mentioned frameworks, we will be using WXT. In this post, we will set up the project and install the necessary dependencies and get an overview of the project structure.

Note

The project uses pnpm as the package manager. If you are using other package managers, adjust the commands accordingly.

Installing and setting up WXT

The documentation for WXT is pretty straightforward, and you should refer to it for more detailed information at any step. The commands below are directly taken from the documentation:

pnpm dlx wxt@latest init
bunx wxt@latest init
npx wxt@latest init
# Use NPM initially, but select Yarn when prompted
npx wxt@latest init

Enter your project name when prompted. You will be asked to select a template, and a package manager. We will be choosing React and PNPM (or you can choose any other package manager) respectively. Once the command finishes, you will be asked to cd into the project folder and install the packages.

Warning

WXT is in active development, and the commands/structure might change in the future. The version used in the guide is 0.19.23 which you can pin in your package.json file to follow along.

You are encouraged to go through the documentation once at this point to get a better understanding of the project structure and the commands available.

We will add a src directory for better project organization. Create a src directory in your project’s root and move assets, entrypoints and public folders inside it. In the wxt.config.ts in root of your project, add the following:

wxt.config.ts
export default defineConfig({
  srcDir: "src",
});

Run pnpm dev (or pnpm dev:firefox for firefox) to ensure that the setup is successful. Your browser should open, and you will see the extension installed as below:

During development, it is recommended to test the extension on both Chrome and Firefox continuously to ensure that it works as expected on both the browsers, as well as to catch any potential browser-specific bugs early.

Installing TailwindCSS and Shadcn

Although WXT uses Vite under the hood, the Shadcn theme instructions for Vite do not work directly with WXT. We will use the Manual installation steps with a few modifications.

  1. The first few commands are for installing TailwindCSS and some other dependencies. Run the following commands:
pnpm add -D tailwindcss postcss autoprefixer
pnpm dlx tailwindcss init -p
pnpm add tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
  1. In the tsconfig.json file, add the following paths and baseUrl in the compilerOptions:
tsconfig.json
{
  "extends": "./.wxt/tsconfig.json",
  "compilerOptions": {
    "allowImportingTsExtensions": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  }
}

Warning

WXT explicitly advises against adding paths directly to tsconfig.json, and suggests using the alias option in wxt.config.ts in their documentation. Doing so could cause issues in the future if WXT changes its configurations. However, Shadcn fails to resolve the paths correctly if we do not add them to tsconfig.json directly. There is an open issue in Shadcn about the same. The manual addition is just a temporary workaround and you are advised to monitor the issue’s status and update your configuration accordingly in the future.

  1. Copy the contents of the tailwind.config.js file from the Shadcn documentation. Change the value of content key as follows:
content: ["./src/**/*.{html,js,ts,jsx,tsx}", "./index.html"],
  1. Create a file index.css in the src/ folder. Copy the style from the Configure styles section of the Shadcn documentation and paste them in the index.css file.

  2. Create lib/ folder in src/. Inside lib/, create a file utils.ts and add the cn helper code.

  3. Create a components.json file in the project’s root and add the components.json code.

  4. Try adding a button with the following command:

pnpm dlx shadcn@latest add button

If everything is set up correctly, the button should be added in src/components/ui/button.tsx.

  1. Replace the contents of src/entrypoints/popup/App.tsx with the following:
src/entrypoints/popup/App.tsx
import { useState } from "react";
import { Button } from "@/components/ui/button";

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

  return (
    <div className="m-10">
      <Button onClick={() => setCount(count + 1)}>Count: {count}</Button>
    </div>
  );
}

export default App;

(The main goal is to remove the App.css import, but we also removed rest of the code and added a simple button to test the setup.)

  1. In the src/entrypoints/popup/main.tsx file, remove the style.css import and add @/index.css import.

  2. Run pnpm dev to start the server. You should see the Shadcn button in the popup window. We can now use TailwindCSS and Shadcn components (adding via CLI will also work).

  3. Also, we can delete the src/entrypoints/popup/App.css file and the src/entrypoints/popup/style.css file as they are not needed.

Understanding The Project Structure

Our directory structure should look something like this:

.
├── components.json
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── README.md
├── src
│   ├── assets
│   │   └── react.svg
│   ├── components
│   │   ├── button.tsx
│   │   └── ui
│   │       └── button.tsx
│   ├── entrypoints
│   │   ├── background.ts
│   │   ├── content.ts
│   │   └── popup
│   │       ├── App.tsx
│   │       ├── index.html
│   │       ├── main.tsx
│   ├── index.css
│   ├── lib
│   │   └── utils.ts
│   └── public
│       ├── icon
│       │   ├── 128.png
│       │   :
│       └── wxt.svg
├── tailwind.config.js
├── tsconfig.json
└── wxt.config.ts

All our configuration files are in the project’s root, and all the source code in src/ directory. It is mostly similar to the one shown in the WXT documentation, with some differences due to adding Shadcn as follows:

  • components.json - Shadcn configuration file
  • components/ui/ - Shadcn components directory
  • index.css - Our global CSS file
  • tailwind.config.js - TailwindCSS configuration file
  • postcss.config.js - PostCSS configuration file

The main files we are interested are the ones in the src/entrypoints/ directory. These files are the entrypoints for our extension. We have three entrypoints initially:

  1. The background.ts file is the entrypoint for the background script.
  2. The content.ts file is the entrypoint for the content script.
  3. The popup/ directory contains the entrypoint for the popup.

More information about these can be found here. As of writing this guide, we can have only one background script in WXT, but multiple content scripts.

On inspecting the contents of the background.ts file, we can see the following code:

background.ts
export default defineBackground(() => {
  console.log("Hello background!", { id: browser.runtime.id });
});

and in the content.ts file:

content.ts
export default defineContentScript({
  matches: ["*://*.google.com/*"],
  main() {
    console.log("Hello content.");
  },
});

Notice that the default exports are wrapped in defineBackground and defineContentScript functions. These functions are provided by WXT and are used to define the entrypoints for the background and content scripts. The matches property in the defineContentScript function is used to specify the domains where the we want the content script to run (User is notified that we need access to these URLs). In the above example, the content script will run only on *.google.com domains.

The content script and background scripts use different browser consoles for logging. To see the above log statements, run pnpm dev and wait for the browser to open. As the content script has match only for *.google.com domain, the content script will only load when the domain matches. Navigate to www.google.com and open the browser console by pressing F12. You should see the Hello content message in the console.

To see the log from background script:

  1. In chrome / chrome based browsers, navigate to chrome://extensions/ and click on the service worker link for your extension.
  2. In firefox, navigate to about:debugging#/runtime/this-firefox and click on the Inspect link for your extension and go to Console.

This should open a console where you should see the Hello background message along with the browser.runtime.id

Both these consoles are useful for debugging and logging messages. For our UI components, we can find logs in the browser console, while for background scripts they will be in the service worker / extension’s developer console.

Configurations

The wxt.config.ts file is the main configuration file for WXT. It is used to configure the extension’s name, version, and other settings. The wxt.config.ts file in our project looks like this:

wxt.config.ts
import { defineConfig } from "wxt";

// See https://wxt.dev/api/config.html
export default defineConfig({
  srcDir: "src",
  extensionApi: "chrome",
  modules: ["@wxt-dev/module-react"],
});

In the first post, we discussed about the manifest.json file, which contains the metadata for the extension. We have to list our content scripts with the URLs we want to run them on, as well as our background scripts in it for our extension to run. However, manually writing manifest.json is complex, and along with that, the property names are different in MV2 and MV3. WXT abstracts away the manifest file and generates it based on our project structure. (Recall the defineBackground and defineContentScript?). Let’s add the name and description of our project in the manifest:

wxt.config.ts
export default defineConfig({
  srcDir: "src",
  extensionApi: "chrome",
  modules: ["@wxt-dev/module-react"],
  manifest: {
    name: "Command Palette",
    description: "A command palette to quickly perform actions",
  },
});

Run the pnpm dev command. The name and description of the extension should reflect in the browser. In the .output/chrome-mv3/ or .output/firefox-mv2 folder in your project’s root, you should see the manifest.json generated by WXT.

Note

WXT uses Manifest V3 for chrome and Manifest V2 for Firefox by default. In the wxt.config.ts, we only have to add the manifest key names as per MV3, and WXT will automatically convert the keys based on MV2 or MV3.

Auto Imports And Extension APIs

Notice that there is no import for defineBackground in background script or defineContentScript in the content script. By default WXT, auto imports its own APIs, as well as few source directories. The one we will be using most is the browser global variable. It allows us to access the various browser APIs such as browser.tabs, browser.bookmarks and a lot more.

Chrome use the chrome global variable instead of browser. WXT abstracts this away and provides a consistent API across browsers. We only have to use the browser global everywhere - however we have to check if the APIs are available, as WXT assumes all APIs exist for all browsers.

For reference, a list of all available Firefox APIs can be found on MDN Web Docs (along with compatibility details with different browsers) and for Chrome can be found on Chrome Developer Docs.

In the next post, we will discuss about content scripts and UI for our extension.

Share your feedback: [email protected]

Last updated on