Building Modern Cross Browser Web Extensions: Project Setup (Part 2)
#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:
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.
- 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
- In the
tsconfig.json
file, add the following paths and baseUrl in thecompilerOptions
:
{
"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.
- Copy the contents of the
tailwind.config.js
file from the Shadcn documentation. Change the value ofcontent
key as follows:
content: ["./src/**/*.{html,js,ts,jsx,tsx}", "./index.html"],
Create a file
index.css
in thesrc/
folder. Copy the style from the Configure styles section of the Shadcn documentation and paste them in theindex.css
file.Create
lib/
folder insrc/
. Insidelib/
, create a file utils.ts and add the cn helper code.Create a
components.json
file in the project’s root and add the components.json code.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
.
- Replace the contents of
src/entrypoints/popup/App.tsx
with the following:
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.)
In the
src/entrypoints/popup/main.tsx
file, remove thestyle.css
import and add@/index.css
import.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).Also, we can delete the
src/entrypoints/popup/App.css
file and thesrc/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 filecomponents/ui/
- Shadcn components directoryindex.css
- Our global CSS filetailwind.config.js
- TailwindCSS configuration filepostcss.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:
- The
background.ts
file is the entrypoint for the background script. - The
content.ts
file is the entrypoint for the content script. - 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:
export default defineBackground(() => {
console.log("Hello background!", { id: browser.runtime.id });
});
and in the content.ts
file:
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:
- In chrome / chrome based browsers, navigate to
chrome://extensions/
and click on theservice worker
link for your extension. - In firefox, navigate to
about:debugging#/runtime/this-firefox
and click on theInspect
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:
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:
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]