---
url: /
title: Next-gen Web Extension Framework
---
# WXTNext-gen Web Extension Framework
An open source tool that makes web extension development faster than ever before.
[Get Started](/guide/installation.html)
[Learn More](/guide/introduction.html)

[πSupported BrowsersWXT will build extensions for Chrome, Firefox, Edge, Safari, and any Chromium based browser.Read docs ](/guide/essentials/target-different-browsers.html)
[β
MV2 and MV3Build Manifest V2 or V3 extensions for any browser using the same codebase.Read docs ](/guide/essentials/config/manifest.html)
β‘
## Fast Dev Mode
Lightning fast HMR for UI development and fast reloads for content/background scripts enables faster iterations.
[πFile Based EntrypointsManifest is generated based on files in the project with inline configuration.See project structure ](/guide/essentials/project-structure.html)
π
## TypeScript
Create large projects with confidence using TS by default.
[π¦ΎAuto-importsNuxt-like auto-imports to speed up development.Read docs ](/guide/essentials/config/auto-imports.html)
π€
## Automated Publishing
Automatically zip, upload, submit, and publish extensions.
[π¨Frontend Framework AgnosticWorks with any front-end framework with a Vite plugin.Add a framework ](/guide/essentials/frontend-frameworks.html)
[π¦Module SystemReuse build-time and runtime-code across multiple extensions.Read docs ](/guide/essentials/wxt-modules.html)
[ποΈBootstrap a New ProjectGet started quickly with several awesome project templates.See templates ](/guide/installation.html#bootstrap-project)
π
## Bundle Analysis
Tools for analyzing the final extension bundle and minimizing your extension's size.
[β¬οΈBundle Remote CodeDownloads and bundles remote code imported from URLs.Read docs ](/guide/essentials/remote-code.html)
## Sponsors [β](#sponsors)
WXT is a [MIT-licensed](https://github.com/wxt-dev/wxt/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring WXT's development](https://github.com/sponsors/wxt-dev).
[](https://github.com/sponsors/wxt-dev)
## Put Developer Experience First [β](#put-developer-experience-first)
WXT simplifies the web extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
And who doesn't appreciate a beautiful CLI?
## Who's Using WXT? [β](#who-s-using-wxt)
Battle tested and ready for production. Explore web extensions made with WXT.
---
url: /guide/introduction.html
title: Welcome to WXT!
---
# Welcome to WXT! [β](#welcome-to-wxt)
WXT is a modern, open-source framework for building web extensions. Inspired by Nuxt, its goals are to:
* Provide an awesome [DX](https://about.gitlab.com/topics/devops/what-is-developer-experience/)
* Provide first-class support for all major browsers
Check out the [comparison](/guide/resources/compare.html) to see how WXT compares to other tools for building web extensions.
## Prerequisites [β](#prerequisites)
These docs assume you have a basic knowledge of how web extensions are structured and how you access the extension APIs.
:::warning New to extension development?
If you have never written an extension before, follow Chrome's [Hello World tutorial](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world) to first **_create an extension without WXT_**, then come back here.
:::
You should also be aware of [Chrome's extension docs](https://developer.chrome.com/docs/extensions) and [Mozilla's extension docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions). WXT does not change how you use the extension APIs, and you'll need to refer to these docs often when using specific APIs.
---
Alright, got a basic understanding of how web extensions are structured? Do you know how to access the extension APIs? Then continue to the [Installation page](/guide/installation.html) to create your first WXT extension.
---
url: /guide/installation.html
title: Installation
---
# Installation [β](#installation)
Bootstrap a new project, start from scratch, or [migrate an existing project](/guide/resources/migrate.html).
* [Bootstrap Project](#bootstrap-project)
* [Demo](#demo)
* [From Scratch](#from-scratch)
* [Next Steps](#next-steps)
## Bootstrap Project [β](#bootstrap-project)
Run the [init command](/api/cli/wxt-init.html), and follow the instructions.
:::code-group
```sh [PNPM]
pnpm dlx wxt@latest init
```
```sh [Bun]
bunx wxt@latest init
```
```sh [NPM]
npx wxt@latest init
```
```sh [Yarn]
# Use NPM initially, but select Yarn when prompted
npx wxt@latest init
```
:::
:::info Starter Templates:
[Vanilla](https://github.com/wxt-dev/wxt/tree/main/templates/vanilla)
[Vue](https://github.com/wxt-dev/wxt/tree/main/templates/vue)
[React](https://github.com/wxt-dev/wxt/tree/main/templates/react)
[Svelte](https://github.com/wxt-dev/wxt/tree/main/templates/svelte)
[Solid](https://github.com/wxt-dev/wxt/tree/main/templates/solid)
All templates use TypeScript by default. To use JavaScript, change the file extensions.
:::
### Demo [β](#demo)

Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
## From Scratch [β](#from-scratch)
1. Create a new project
:::code-group
```sh [PNPM]
cd my-project
pnpm init
```
```sh [Bun]
cd my-project
bun init
```
```sh [NPM]
cd my-project
npm init
```
```sh [Yarn]
cd my-project
yarn init
```
:::
2. Install WXT:
:::code-group
```sh [PNPM]
pnpm i -D wxt
```
```sh [Bun]
bun i -D wxt
```
```sh [NPM]
npm i -D wxt
```
```sh [Yarn]
yarn add --dev wxt
```
:::
3. Add an entrypoint, `my-project/entrypoints/background.ts`:
:::code-group
```ts [ts]
export default defineBackground(() => {
console.log('Hello world!');
});
```
:::
4. Add scripts to your `package.json`:
package.json
```json
{
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"postinstall": "wxt prepare"
}
}
```
5. Run your extension in dev mode
:::code-group
```sh [PNPM]
pnpm dev
```
```sh [Bun]
bun run dev
```
```sh [NPM]
npm run dev
```
```sh [Yarn]
yarn dev
```
:::
WXT will automatically open a browser window with your extension installed.
## Next Steps [β](#next-steps)
* Keep reading on about WXT's [Project Structure](/guide/essentials/project-structure.html) and other essential concepts to learn
* Configure [automatic browser startup](/guide/essentials/config/browser-startup.html) during dev mode
* Explore [WXT's example library](/examples.html) to see how to use specific APIs or perform common tasks
* Checkout the [community page](/guide/resources/community.html) for a list of resources made by the community!
---
url: /guide/essentials/project-structure.html
title: Project Structure
---
# Project Structure [β](#project-structure)
WXT follows a strict project structure. By default, it's a flat folder structure that looks like this:
```html
π {rootDir}/
π .output/
π .wxt/
π assets/
π components/
π composables/
π entrypoints/
π hooks/
π modules/
π public/
π utils/
π .env
π .env.publish
π app.config.ts
π package.json
π tsconfig.json
π web-ext.config.ts
π wxt.config.ts
```
Here's a brief summary of each of these files and directories:
* `.output/`: All build artifacts will go here
* `.wxt/`: Generated by WXT, it contains TS config
* `assets/`: Contains all CSS, images, and other assets that should be processed by WXT
* `components/`: Auto-imported by default, contains UI components
* `composables/`: Auto-imported by default, contains composable functions for Vue
* `entrypoints/`: Contains all the entrypoints that get bundled into your extension
* `hooks/`: Auto-imported by default, contains hooks for React and Solid
* `public/`: Contains any files you want to copy into the output folder as-is, without being processed by WXT
* `utils/`: Auto-imported by default, contains generic utilities used throughout your project
* `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables.html)
* `.env.publish`: Contains Environment Variables for [publishing](/guide/essentials/publishing.html)
* `app.config.ts`: Contains [Runtime Config](/guide/essentials/config/runtime.html)
* `package.json`: The standard file used by your package manager
* `tsconfig.json`: Config telling TypeScript how to behave
* `web-ext.config.ts`: Configure [Browser Startup](/guide/essentials/config/browser-startup.html)
* `wxt.config.ts`: The main config file for WXT projects
## Adding a `src/` Directory [β](#adding-a-src-directory)
Many developers like having a `src/` directory to separate source code from configuration files. You can enable it inside the `wxt.config.ts` file:
wxt.config.ts
```ts
export default defineConfig({
srcDir: 'src',
});
```
After enabling it, your project structure should look like this:
```html
π {rootDir}/
π .output/
π .wxt/
π src/
π assets/
π components/
π composables/
π entrypoints/
π hooks/
π modules/
π public/
π utils/
π app.config.ts
π .env
π .env.publish
π package.json
π tsconfig.json
π web-ext.config.ts
π wxt.config.ts
```
## Customizing Other Directories [β](#customizing-other-directories)
You can configure the following directories:
wxt.config.ts
```ts
export default defineConfig({
// Relative to project root
srcDir: "src", // default: "."
outDir: "dist", // default: ".output"
// Relative to srcDir
entrypointsDir: "entries", // default: "entrypoints"
modulesDir: "wxt-modules", // default: "modules"
publicDir: "static", // default: "public"
})
```
You can use absolute or relative paths.
---
url: /guide/essentials/entrypoints.html
title: Entrypoints
---
# Entrypoints [β](#entrypoints)
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (Pug, TS, JSX, SCSS, etc).
Here's an example set of entrypoints:
```html
π entrypoints/
π popup/
π index.html
π main.ts
π style.css
π background.ts
π content.ts
```
* [Listed vs Unlisted](#listed-vs-unlisted)
* [Adding Entrypoints](#adding-entrypoints)
* [Defining Manifest Options](#defining-manifest-options)
* [Entrypoint Types](#entrypoint-types)
* [Background](#background)
* [Bookmarks](#bookmarks)
* [Content Scripts](#content-scripts)
* [Devtools](#devtools)
* [History](#history)
* [Newtab](#newtab)
* [Options](#options)
* [Popup](#popup)
* [Sandbox](#sandbox)
* [Side Panel](#side-panel)
* [Unlisted CSS](#unlisted-css)
* [Unlisted Pages](#unlisted-pages)
* [Unlisted Scripts](#unlisted-scripts)
## Listed vs Unlisted [β](#listed-vs-unlisted)
For web extensions, there are two types of entrypoints:
* **Listed**: Referenced in the `manifest.json`
* **Unlisted**: Not referenced in the `manifest.json`
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
* Popup
* Options
* Background
* Content Scripts
* Etc.
Some examples of "unlisted" entrypoints:
* A welcome page shown when the extension is installed
* JS files injected by content scripts into the page's main world
:::tip
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
:::
## Adding Entrypoints [β](#adding-entrypoints)
An entrypoint can be defined as a single file or directory with an `index` file inside it.
:::code-group
```html [Single File]
π entrypoints/
π background.ts
```
```html [Directory]
π entrypoints/
π background/
π index.ts
```
:::
The entrypoint's name dictates the type of entrypoint, listed vs unlisted. In this example, "background" is the name of the ["Background" entrypoint](#background).
Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns.
## Defining Manifest Options [β](#defining-manifest-options)
Most listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
For example, here's how to define `matches` for content scripts:
entrypoints/content.ts
```ts
export default defineContentScript({
matches: ['*://*.wxt.dev/*'],
main() {
// ...
},
});
```
For HTML entrypoints, options are configured as `` tags. For example, to use a `page_action` for your MV2 popup:
```html
```
> Refer to the [Entrypoint Types](#entrypoint-types) sections for a list of options configurable inside each entrypoint, and how to define them.
When building your extension, WXT will look at the options defined in your entrypoints, and generate the manifest accordingly.
## Entrypoint Types [β](#entrypoint-types)
### Background [β](#background)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
| Filename | Output Path |
| ------------------------------------ | -------------- |
| entrypoints/background.\[jt\]s | /background.js |
| entrypoints/background/index.\[jt\]s | /background.js |
:::code-group
```ts [Minimal]
export default defineBackground(() => {
// Executed when background is loaded
});
```
```ts [With Manifest Options]
export default defineBackground({
// Set manifest options
persistent: undefined | true | false,
type: undefined | 'module',
// Set include/exclude if the background should be removed from some builds
include: undefined | string[],
exclude: undefined | string[],
main() {
// Executed when background is loaded, CANNOT BE ASYNC
},
});
```
:::
### Bookmarks [β](#bookmarks)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome%5Furl%5Foverrides)
| Filename | Output Path |
| -------------------------------- | --------------- |
| entrypoints/bookmarks.html | /bookmarks.html |
| entrypoints/bookmarks/index.html | /bookmarks.html |
```html
Title
```
### Content Scripts [β](#content-scripts)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content%5Fscripts/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content%5Fscripts)
See [Content Script UI](/guide/essentials/content-scripts.html) for more info on creating UIs and including CSS in content scripts.
| Filename | Output Path |
| ------------------------------------------ | --------------------------- |
| entrypoints/content.\[jt\]sx? | /content-scripts/content.js |
| entrypoints/content/index.\[jt\]sx? | /content-scripts/content.js |
| entrypoints/.content.\[jt\]sx? | /content-scripts/.js |
| entrypoints/.content/index.\[jt\]sx? | /content-scripts/.js |
```ts
export default defineContentScript({
// Set manifest options
matches: string[],
excludeMatches: undefined | [],
includeGlobs: undefined | [],
excludeGlobs: undefined | [],
allFrames: undefined | true | false,
runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
matchAboutBlank: undefined | true | false,
matchOriginAsFallback: undefined | true | false,
world: undefined | 'ISOLATED' | 'MAIN',
// Set include/exclude if the background should be removed from some builds
include: undefined | string[],
exclude: undefined | string[],
// Configure how CSS is injected onto the page
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
// Configure how/when content script will be registered
registration: undefined | "manifest" | "runtime",
main(ctx: ContentScriptContext) {
// Executed when content script is loaded, can be async
},
});
```
### Devtools [β](#devtools)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools%5Fpage)
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
| Filename | Output Path |
| ------------------------------- | -------------- |
| entrypoints/devtools.html | /devtools.html |
| entrypoints/devtools/index.html | /devtools.html |
```html
```
### History [β](#history)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome%5Furl%5Foverrides)
| Filename | Output Path |
| ------------------------------ | ------------- |
| entrypoints/history.html | /history.html |
| entrypoints/history/index.html | /history.html |
```html
Title
```
### Newtab [β](#newtab)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome%5Furl%5Foverrides)
| Filename | Output Path |
| ----------------------------- | ------------ |
| entrypoints/newtab.html | /newtab.html |
| entrypoints/newtab/index.html | /newtab.html |
```html
Title
```
### Options [β](#options)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options%5Fui)
| Filename | Output Path |
| ------------------------------ | ------------- |
| entrypoints/options.html | /options.html |
| entrypoints/options/index.html | /options.html |
```html
Options Title
```
### Popup [β](#popup)
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/action/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action)
| Filename | Output Path |
| ---------------------------- | ----------- |
| entrypoints/popup.html | /popup.html |
| entrypoints/popup/index.html | /popup.html |
```html
Default Popup Title
```
### Sandbox [β](#sandbox)
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/sandbox/)
:::warning Chromium Only
Firefox does not support sandboxed pages.
:::
| Filename | Output Path |
| ------------------------------------- | ------------- |
| entrypoints/sandbox.html | /sandbox.html |
| entrypoints/sandbox/index.html | /sandbox.html |
| entrypoints/.sandbox.html | /.html |
| entrypoints/.sandbox/index.html | /.html |
```html
Title
```
### Side Panel [β](#side-panel)
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user%5Finterface/Sidebars)
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
| Filename | Output Path |
| --------------------------------------- | --------------- |
| entrypoints/sidepanel.html | /sidepanel.html |
| entrypoints/sidepanel/index.html | /sidepanel.html |
| entrypoints/.sidepanel.html | /.html\` |
| entrypoints/.sidepanel/index.html | /.html\` |
```html
Default Side Panel Title
```
### Unlisted CSS [β](#unlisted-css)
Follow Vite's guide to setup your preprocessor of choice:
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts.html#css) docs.
| Filename | Output Path |
| ------------------------------------------------------------------ | ---------------------------- |
| entrypoints/.(css\|scss|sass|less|styl|stylus) | /.css |
| entrypoints//index.(css\|scss|sass|less|styl|stylus) | /.css |
| entrypoints/content.(css\|scss|sass|less|styl|stylus) | /content-scripts/content.css |
| entrypoints/content/index.(css\|scss|sass|less|styl|stylus) | /content-scripts/content.css |
| entrypoints/.content.(css\|scss|sass|less|styl|stylus) | /content-scripts/.css |
| entrypoints/.content/index.(css\|scss|sass|less|styl|stylus) | /content-scripts/.css |
```css
body {
/* ... */
}
```
### Unlisted Pages [β](#unlisted-pages)
| Filename | Output Path |
| ----------------------------- | ------------ |
| entrypoints/.html | /.html |
| entrypoints//index.html | /.html |
```html
Title
```
Pages are accessible at `/.html`:
```ts
const url = browser.runtime.getURL('/.html');
console.log(url); // "chrome-extension:///.html"
```
### Unlisted Scripts [β](#unlisted-scripts)
| Filename | Output Path |
| ---------------------------------- | ----------- |
| entrypoints/.\[jt\]sx? | /.js |
| entrypoints//index.\[jt\]sx? | /.js |
:::code-group
```ts [Minimal]
export default defineUnlistedScript(() => {
// Executed when script is loaded
});
```
```ts [With Options]
export default defineUnlistedScript({
// Set include/exclude if the script should be removed from some builds
include: undefined | string[],
exclude: undefined | string[],
main() {
// Executed when script is loaded
},
});
```
:::
Scripts are accessible from `/.js`:
```ts
const url = browser.runtime.getURL('/.js');
console.log(url); // "chrome-extension:///.js"
```
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related assets to [web\_accessible\_resources](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
---
url: /guide/essentials/config/manifest.html
title: Manifest
---
# Manifest [β](#manifest)
In WXT, there is no `manifest.json` file in your source code. Instead, WXT generates the manifest from multiple sources:
* Global options [defined in your wxt.config.ts file](#global-options)
* Entrypoint-specific options [defined in your entrypoints](/guide/essentials/entrypoints.html#defining-manifest-options)
* [WXT Modules](/guide/essentials/wxt-modules.html) added to your project can modify your manifest
* [Hooks](/guide/essentials/config/hooks.html) defined in your project can modify your manifest
Your extension's `manifest.json` will be output to `.output/{target}/manifest.json` when running `wxt build`.
## Global Options [β](#global-options)
To add a property to your manifest, use the `manifest` config inside your `wxt.config.ts`:
```ts
export default defineConfig({
manifest: {
// Put manual changes here
},
});
```
You can also define the manifest as a function, and use JS to generate it based on the target browser, mode, and more.
```ts
export default defineConfig({
manifest: ({ browser, manifestVersion, mode, command }) => {
return {
// ...
};
},
});
```
### MV2 and MV3 Compatibility [β](#mv2-and-mv3-compatibility)
When adding properties to the manifest, always define the property in it's MV3 format when possible. When targeting MV2, WXT will automatically convert these properties to their MV2 format.
For example, for this config:
```ts
export default defineConfig({
manifest: {
action: {
default_title: 'Some Title',
},
web_accessible_resources: [
{
matches: ['*://*.google.com/*'],
resources: ['icon/*.png'],
},
],
},
});
```
WXT will generate the following manifests:
:::code-group
```json [MV2]
{
"manifest_version": 2,
// ...
"browser_action": {
"default_title": "Some Title"
},
"web_accessible_resources": ["icon/*.png"]
}
```
```json [MV3]
{
"manifest_version": 3,
// ...
"action": {
"default_title": "Some Title"
},
"web_accessible_resources": [
{
"matches": ["*://*.google.com/*"],
"resources": ["icon/*.png"]
}
]
}
```
:::
You can also specify properties specific to a single manifest version, and they will be stripped out when targeting the other manifest version.
## Name [β](#name)
> [Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/name/)
If not provided via the `manifest` config, the manifest's `name` property defaults to your `package.json`'s `name` property.
## Version and Version Name [β](#version-and-version-name)
> [Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/version/)
Your extension's `version` and `version_name` is based on the `version` from your `package.json`.
* `version_name` is the exact string listed
* `version` is the string cleaned up, with any invalid suffixes removed
Example:
```json
// package.json
{
"version": "1.3.0-alpha2"
}
```
```json
// .output//manifest.json
{
"version": "1.3.0",
"version_name": "1.3.0-alpha2"
}
```
If a version is not present in your `package.json`, it defaults to `"0.0.0"`.
## Icons [β](#icons)
WXT automatically discovers your extension's icon by looking at files in the `public/` directory:
```
public/
ββ icon-16.png
ββ icon-24.png
ββ icon-48.png
ββ icon-96.png
ββ icon-128.png
```
Specifically, an icon must match one of these regex to be discovered:
```ts
const iconRegex = [
/^icon-([0-9]+)\.png$/, // icon-16.png
/^icon-([0-9]+)x[0-9]+\.png$/, // icon-16x16.png
/^icon@([0-9]+)w\.png$/, // icon@16w.png
/^icon@([0-9]+)h\.png$/, // icon@16h.png
/^icon@([0-9]+)\.png$/, // icon@16.png
/^icons?[/\\]([0-9]+)\.png$/, // icon/16.png | icons/16.png
/^icons?[/\\]([0-9]+)x[0-9]+\.png$/, // icon/16x16.png | icons/16x16.png
];
```
If you don't like these filename or you're migrating to WXT and don't want to rename the files, you can manually specify an `icon` in your manifest:
```ts
export default defineConfig({
manifest: {
icons: {
16: '/extension-icon-16.png',
24: '/extension-icon-24.png',
48: '/extension-icon-48.png',
96: '/extension-icon-96.png',
128: '/extension-icon-128.png',
},
},
});
```
Alternatively, you can use [@wxt-dev/auto-icons](https://www.npmjs.com/package/@wxt-dev/auto-icons) to let WXT generate your icon at the required sizes.
## Permissions [β](#permissions)
> [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/)
Most of the time, you need to manually add permissions to your manifest. Only in a few specific situations are permissions added automatically:
* During development: the `tabs` and `scripting` permissions will be added to enable hot reloading.
* When a `sidepanel` entrypoint is present: The `sidepanel` permission is added.
```ts
export default defineConfig({
manifest: {
permissions: ['storage', 'tabs'],
},
});
```
## Host Permissions [β](#host-permissions)
> [Chrome docs](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions)
```ts
export default defineConfig({
manifest: {
host_permissions: ['https://www.google.com/*'],
},
});
```
:::warning
If you use host permissions and target both MV2 and MV3, make sure to only include the required host permissions for each version:
```ts
export default defineConfig({
manifest: ({ manifestVersion }) => ({
host_permissions: manifestVersion === 2 ? [...] : [...],
}),
});
```
:::
## Default Locale [β](#default-locale)
```ts
export default defineConfig({
manifest: {
name: '__MSG_extName__',
description: '__MSG_extDescription__',
default_locale: 'en',
},
});
```
> See [I18n docs](/guide/essentials/i18n.html) for a full guide on internationalizing your extension.
## Actions [β](#actions)
In MV2, you have two options: [browser\_action](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser%5Faction) and [page\_action](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page%5Faction). In MV3, they were merged into a single [action](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action) API.
By default, whenever an `action` is generated, WXT falls back to `browser_action` when targeting MV2.
### Action With Popup [β](#action-with-popup)
To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints.html#popup). If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head:
```html
```
### Action Without Popup [β](#action-without-popup)
If you want to use the `activeTab` permission or the `browser.action.onClicked` event, but don't want to show a popup:
1. Delete the [Popup entrypoint](/guide/essentials/entrypoints.html#popup) if it exists
2. Add the `action` key to your manifest:
```ts
export default defineConfig({
manifest: {
action: {},
},
});
```
Same as an action with a popup, WXT will fallback on using `browser_action` for MV2\. To use a `page_action` instead, add that key as well:
```ts
export default defineConfig({
manifest: {
action: {},
page_action: {},
},
});
```
---
url: /guide/essentials/config/browser-startup.html
title: Browser Startup
---
# Browser Startup [β](#browser-startup)
> See the [API Reference](/api/reference/wxt/interfaces/ExtensionRunnerConfig.html) for a full list of config.
During development, WXT uses [web-ext by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
## Config Files [β](#config-files)
You can configure browser startup in 3 places:
1. `/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
```ts
import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
// ...
});
```
2. `/wxt.config.ts`: Via the [runner config](/api/reference/wxt/interfaces/InlineConfig.html#runner), included in version control
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
## Recipes [β](#recipes)
### Set Browser Binaries [β](#set-browser-binaries)
To set or customize the browser opened during development:
```ts
export default defineRunnerConfig({
binaries: {
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
},
});
```
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
### Persist Data [β](#persist-data)
By default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script.
Right now, Chromium based browsers are the only browsers that support overriding this behavior and persisting data when running the `dev` script multiple times.
To persist data, set the `--user-data-dir` flag:
:::code-group
```ts [Mac/Linux]
export default defineRunnerConfig({
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
});
```
```ts [Windows]
import { resolve } from 'node:path';
export default defineRunnerConfig({
// On Windows, the path must be absolute
chromiumProfile: resolve('.wxt/chrome-data'),
keepProfileChanges: true,
});
```
:::
Now, next time you run the `dev` script, a persistent profile will be created in `.wxt/chrome-data/{profile-name}`. With a persistent profile, you can install devtools extensions to help with development, allow the browser to remember logins, etc, without worrying about the profile being reset the next time you run the `dev` script.
:::tip
You can use any directory you'd like for `--user-data-dir`, the examples above create a persistent profile for each WXT project. To create a profile for all WXT projects, you can put the `chrome-data` directory inside your user's home directory.
:::
### Disable Opening Browser [β](#disable-opening-browser)
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
```ts
export default defineRunnerConfig({
disabled: true,
});
```
---
url: /guide/essentials/config/auto-imports.html
title: Auto-imports
---
# Auto-imports [β](#auto-imports)
WXT uses [unimport](https://www.npmjs.com/package/unimport), the same tool as Nuxt, to setup auto-imports.
```ts
export default defineConfig({
// See https://www.npmjs.com/package/unimport#configurations
imports: {
// ...
},
});
```
By default, WXT sets up auto-imports for all of it's own APIs:
* [browser](/api/reference/wxt/browser/variables/browser.html) from `wxt/browser`
* [defineContentScript](/api/reference/wxt/sandbox/functions/defineContentScript.html) from `wxt/sandbox`
* [defineBackground](/api/reference/wxt/sandbox/functions/defineBackground.html) from `wxt/sandbox`
* [defineUnlistedScript](/api/reference/wxt/sandbox/functions/defineUnlistedScript.html) from `wxt/sandbox`
* [createIntegratedUi](/api/reference/wxt/client/functions/createIntegratedUi.html) from `wxt/client`
* [createShadowRootUi](/api/reference/wxt/client/functions/createShadowRootUi.html) from `wxt/client`
* [createIframeUi](/api/reference/wxt/client/functions/createIframeUi.html) from `wxt/client`
* [fakeBrowser](/api/reference/wxt/testing/variables/fakeBrowser.html) from `wxt/testing`
* And more!
WXT also adds some project directories as auto-import sources automatically:
* `/components/*`
* `/composables/*`
* `/hooks/*`
* `/utils/*`
All named and default exports from files in these directories are available everywhere else in your project without having to import them.
## TypeScript [β](#typescript)
For TypeScript and your editor to recognize auto-imported variables, you need to run the [wxt prepare command](/api/cli/wxt-prepare.html).
Add this command to your `postinstall` script so your editor has everything it needs to report type errors after installing dependencies:
```jsonc
// package.json
{
"scripts": {
"postinstall": "wxt prepare",
},
}
```
## ESLint [β](#eslint)
ESLint doesn't know about the auto-imported variables unless they are explicitly defined in the ESLint's `globals`. By default, WXT will generate the config if it detects ESLint is installed in your project. If the config isn't generated automatically, you can manually tell WXT to generate it.
:::code-group
```ts [ESLint 9]
export default defineConfig({
imports: {
eslintrc: {
enabled: 9,
},
},
});
```
```ts [ESLint 8]
export default defineConfig({
imports: {
eslintrc: {
enabled: 8,
},
},
});
```
:::
Then in your ESLint config, import and use the generated file:
:::code-group
```js [ESLint 9]
// eslint.config.mjs
import autoImports from './.wxt/eslint-auto-imports.mjs';
export default [
autoImports,
{
// The rest of your config...
},
];
```
```js [ESLint 8]
// .eslintrc.mjs
export default {
extends: ['./.wxt/eslintrc-auto-import.json'],
// The rest of your config...
};
```
:::
## Disabling Auto-imports [β](#disabling-auto-imports)
Not all developers like auto-imports. To disable them, set `imports` to `false`.
```ts
export default defineConfig({
imports: false,
});
```
---
url: /guide/essentials/config/environment-variables.html
title: Environment Variables
---
# Environment Variables [β](#environment-variables)
## Dotenv Files [β](#dotenv-files)
WXT supports [dotenv files the same way as Vite](https://vite.dev/guide/env-and-mode.html#env-files). Create any of the following files:
```
.env
.env.local
.env.[mode]
.env.[mode].local
.env.[browser]
.env.[browser].local
.env.[mode].[browser]
.env.[mode].[browser].local
```
And any environment variables listed inside them will be available at runtime:
```sh
# .env
WXT_API_KEY=...
```
```ts
await fetch(`/some-api?apiKey=${import.meta.env.WXT_API_KEY}`);
```
Remember to prefix any environment variables with `WXT_` or `VITE_`, otherwise they won't be available at runtime, as per [Vite's convention](https://vite.dev/guide/env-and-mode.html#env-files).
## Built-in Environment Variables [β](#built-in-environment-variables)
WXT provides some custom environment variables based on the current command:
| Usage | Type | Description |
| --------------------------------- | ------- | --------------------------------------------------- |
| import.meta.env.MANIFEST\_VERSION | 2 β 3 | The target manifest version |
| import.meta.env.BROWSER | string | The target browser |
| import.meta.env.CHROME | boolean | Equivalent to import.meta.env.BROWSER === "chrome" |
| import.meta.env.FIREFOX | boolean | Equivalent to import.meta.env.BROWSER === "firefox" |
| import.meta.env.SAFARI | boolean | Equivalent to import.meta.env.BROWSER === "safari" |
| import.meta.env.EDGE | boolean | Equivalent to import.meta.env.BROWSER === "edge" |
| import.meta.env.OPERA | boolean | Equivalent to import.meta.env.BROWSER === "opera" |
You can also access all of [Vite's environment variables](https://vite.dev/guide/env-and-mode.html#env-variables):
| Usage | Type | Description |
| -------------------- | ------- | -------------------------------------------------------------------------------- |
| import.meta.env.MODE | string | The [mode](/guide/essentials/config/build-mode.html) the extension is running in |
| import.meta.env.PROD | boolean | When NODE\_ENV='production' |
| import.meta.env.DEV | boolean | Opposite of import.meta.env.PROD |
:::details Other Vite Environment Variables
Vite provides two other environment variables, but they aren't useful in WXT projects:
* `import.meta.env.BASE_URL`: Use `browser.runtime.getURL` instead.
* `import.meta.env.SSR`: Always `false`.
:::
## Manifest [β](#manifest)
To use environment variables in the manifest, you need to use the function syntax:
```ts
export default defineConfig({
extensionApi: 'chrome',
modules: ['@wxt-dev/module-vue'],
manifest: {
oauth2: {
client_id: import.meta.env.WXT_APP_CLIENT_ID
}
}
manifest: () => ({
oauth2: {
client_id: import.meta.env.WXT_APP_CLIENT_ID
}
}),
});
```
WXT can't load your `.env` files until after the config file has been loaded. So by using the function syntax for `manifest`, it defers creating the object until after the `.env` files are loaded into the process.
---
url: /guide/essentials/config/runtime.html
title: Runtime Config
---
# Runtime Config [β](#runtime-config)
> This API is still a WIP, with more features coming soon!
Define runtime configuration in a single place, `/app.config.ts`:
```ts
import { defineAppConfig } from 'wxt/sandbox';
// Define types for your config
declare module 'wxt/sandbox' {
export interface WxtAppConfig {
theme?: 'light' | 'dark';
}
}
export default defineAppConfig({
theme: 'dark',
});
```
:::warning
This file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](#environment-variables)
:::
To access runtime config, WXT provides the `useAppConfig` function:
```ts
import { useAppConfig } from 'wxt/sandbox';
console.log(useAppConfig()); // { theme: "dark" }
```
## Environment Variables in App Config [β](#environment-variables-in-app-config)
You can use environment variables in the `app.config.ts` file.
```ts
declare module 'wxt/sandbox' {
export interface WxtAppConfig {
apiKey?: string;
skipWelcome: boolean;
}
}
export default defineAppConfig({
apiKey: import.meta.env.WXT_API_KEY,
skipWelcome: import.meta.env.WXT_SKIP_WELCOME === 'true',
});
```
This has several advantages:
* Define all expected environment variables in a single file
* Convert strings to other types, like booleans or arrays
* Provide default values if an environment variable is not provided
---
url: /guide/essentials/config/vite.html
title: Vite
---
# Vite [β](#vite)
WXT uses [Vite](https://vitejs.dev/) under the hood to bundle your extension.
This page explains how to customize your project's Vite config. Refer to [Vite's documentation](https://vite.dev/config/) to learn more about configuring the bundler.
:::tip
In most cases, you shouldn't change Vite's build settings. WXT provides sensible defaults that output a valid extension accepted by all stores when publishing.
:::
## Change Vite Config [β](#change-vite-config)
You can change Vite's config via the `wxt.config.ts` file:
wxt.config.ts
```ts
import { defineConfig } from 'wxt';
export default defineConfig({
vite: () => ({
// Override config here, same as `defineConfig({ ... })`
// inside vite.config.ts files
}),
});
```
## Add Vite Plugins [β](#add-vite-plugins)
To add a plugin, install the NPM package and add it to the Vite config:
wxt.config.ts
```ts
import { defineConfig } from 'wxt';
import VueRouter from 'unplugin-vue-router/vite';
export default defineConfig({
vite: () => ({
plugins: [
VueRouter({
/* ... */
}),
],
}),
});
```
:::warning
Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. For example, `vite-plugin-remove-console` normally only runs when you build for production (`vite build`). However, WXT uses a combination of dev server and builds during development, so you need to manually tell it when to run:
wxt.config.ts
```ts
import { defineConfig } from 'wxt';
import removeConsole from 'vite-plugin-remove-console';
export default defineConfig({
vite: (configEnv) => ({
plugins:
configEnv.mode === 'production'
? [removeConsole({ includes: ['log'] })]
: [],
}),
});
```
Search [GitHub issues](https://github.com/wxt-dev/wxt/issues?q=is%3Aissue+label%3A%22vite+plugin%22) if you run into issues with a specific plugin.
If an issue doesn't exist for your plugin, [open a new one](https://github.com/wxt-dev/wxt/issues/new/choose).
:::
---
url: /guide/essentials/config/build-mode.html
title: Build Modes
---
# Build Modes [β](#build-modes)
Because WXT is powered by Vite, it supports [modes](https://vite.dev/guide/env-and-mode.html#modes) in the same way.
When running any dev or build commands, pass the `--mode` flag:
```sh
wxt --mode production
wxt build --mode development
wxt zip --mode testing
```
By default, `--mode` is `development` for the dev command and `production` for all other commands (build, zip, etc).
## Get Mode at Runtime [β](#get-mode-at-runtime)
You can access the current mode in your extension using `import.meta.env.MODE`:
```ts
switch (import.meta.env.MODE) {
case 'development': // ...
case 'production': // ...
// Custom modes specified with --mode
case 'testing': // ...
case 'staging': // ...
// ...
}
```
---
url: /guide/essentials/config/typescript.html
title: TypeScript Configuration
---
# TypeScript Configuration [β](#typescript-configuration)
When you run [wxt prepare](/api/cli/wxt-prepare.html), WXT generates a base TSConfig file for your project at `/.wxt/tsconfig.json`.
At a minimum, you need to create a TSConfig in your root directory that looks like this:
```jsonc
// /tsconfig.json
{
"extends": ".wxt/tsconfig.json",
}
```
Or if you're in a monorepo, you may not want to extend the config. If you don't extend it, you need to add `.wxt/wxt.d.ts` to the TypeScript project:
```ts
///
```
## Compiler Options [β](#compiler-options)
To specify custom compiler options, add them in `/tsconfig.json`:
```jsonc
// /tsconfig.json
{
"extends": ".wxt/tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
},
}
```
## TSConfig Paths [β](#tsconfig-paths)
WXT provides a default set of path aliases.
| Alias | To | Example |
| ----- | ------------ | ---------------------------------------------- |
| \~\~ | /\* | import "\~\~/scripts" |
| @@ | /\* | import "@@/scripts" |
| \~ | /\* | import { toLowerCase } from "\~/utils/strings" |
| @ | /\* | import { toLowerCase } from "@/utils/strings" |
To add your own, DO NOT add them to your `tsconfig.json`! Instead, use the [alias option](/api/reference/wxt/interfaces/InlineConfig.html#alias) in `wxt.config.ts`.
This will add your custom aliases to `/.wxt/tsconfig.json` next time you run `wxt prepare`. It also adds your alias to the bundler so it can resolve imports.
```ts
import { resolve } from 'node:path';
export default defineConfig({
alias: {
// Directory:
testing: resolve('utils/testing'),
// File:
strings: resolve('utils/strings.ts'),
},
});
```
```ts
import { fakeTab } from 'testing/fake-objects';
import { toLowerCase } from 'strings';
```
---
url: /guide/essentials/config/hooks.html
title: Hooks
---
# Hooks [β](#hooks)
WXT includes a system that lets you hook into the build process and make changes.
## Adding Hooks [β](#adding-hooks)
The easiest way to add a hook is via the `wxt.config.ts`. Here's an example hook that modifies the `manifest.json` file before it is written to the output directory:
wxt.config.ts
```ts
export default defineConfig({
hooks: {
'build:manifestGenerated': (wxt, manifest) => {
if (wxt.config.mode === 'development') {
manifest.title += ' (DEV)';
}
},
},
});
```
Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.
Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules.html) instead.
## Execution Order [β](#execution-order)
Because hooks can be defined in multiple places, including [WXT Modules](/guide/essentials/wxt-modules.html), the order which they're executed can matter. Hooks are executed in the following order:
1. NPM modules in the order listed in the [modules config](/api/reference/wxt/interfaces/InlineConfig.html#modules)
2. User modules in [/modules folder](/guide/essentials/project-structure.html), loaded alphabetically
3. Hooks listed in your `wxt.config.ts`
To see the order for your project, run `wxt prepare --debug` flag and search for the "Hook execution order":
```
β Hook execution order:
β 1. wxt:built-in:unimport
β 2. src/modules/auto-icons.ts
β 3. src/modules/example.ts
β 4. src/modules/i18n.ts
β 5. wxt.config.ts > hooks
```
Changing execution order is simple:
* Prefix your user modules with a number (lower numbers are loaded first):
```html
π modules/
π 0.my-module.ts
π 1.another-module.ts
```
* If you need to run an NPM module after user modules, just make it a user module and prefix the filename with a number!
```ts
// modules/2.i18n.ts
export { default } from '@wxt-dev/i18n/module';
```
---
url: /guide/essentials/config/entrypoint-loaders.html
title: Entrypoint Loaders
---
# Entrypoint Loaders [β](#entrypoint-loaders)
To generate the manifest and other files at build-time, WXT must import each entrypoint to get their options, like content script `matches`. For HTML files, this is easy. For JS/TS entrypoints, the process is more complicated.
When loading your JS/TS entrypoints, they are imported into a NodeJS environment, not the `browser` environment that they normally run in. This can lead to issues commonly seen when running browser-only code in a NodeJS environment, like missing global variables.
WXT does several pre-processing steps to try and prevent errors during this process:
1. Use `linkedom` to make a small set of browser globals (`window`, `document`, etc) available.
2. Use `@webext-core/fake-browser` to create a fake version of the `chrome` and `browser` globals expected by extensions.
3. Pre-process the JS/TS code, stripping out the `main` function then tree-shaking unused code from the file
However, this process is not perfect. It doesn't setup all the globals found in the browser and the APIs may behave differently. As such, **_you should avoid using browser or extension APIs outside the `main` function of your entrypoints!_**
:::tip
If you're running into errors while importing entrypoints, run `wxt prepare --debug` to see more details about this process. When debugging, WXT will print out the pre-processed code to help you identify issues.
:::
Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.
There are two options for loading your entrypoints:
1. `vite-node` \- default as of `v0.19.0`
2. `jiti` (**DEPRECATED, will be removed in `v0.20.0`**) - Default before `v0.19.0`
## vite-node [β](#vite-node)
Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files. It re-uses the same vite config used when building your extension, making it the most stable entrypoint loader.
## jiti [β](#jiti)
To enable `jiti`:
```ts
export default defineConfig({
entrypointLoader: 'jiti',
});
```
This is the original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, it does one additional pre-processing step: It removes **_ALL_** imports from your code.
That means you cannot use imported variables outside the `main` function in JS entrypoints, like for content script `matches` or other options:
entrypoints/content.ts
```ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
export default defineContentScript({
matches: GOOGLE_MATCHES,
main() {
// ...
},
});
```
```
$ wxt build
wxt build
WXT 0.14.1
βΉ Building chrome-mv3 for production with Vite 5.0.5
β Command failed after 360 ms
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function.
```
Usually, this error occurs when you try to extract options into a shared file or when running code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
```ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
export default defineContentScript({
matches: GOOGLE_MATCHES,
matches: ['*//*.google.com/*'],
main() {
// ...
},
});
```
---
url: /guide/essentials/extension-apis.html
title: Extension APIs
---
# Extension APIs [β](#extension-apis)
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/api) β’ [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser%5Fsupport%5Ffor%5FJavaScript%5FAPIs)
Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).
WXT simplifies this - always use `browser`:
```ts
browser.action.onClicked.addListener(() => {
// ...
});
```
Other than that, refer to Chrome and Mozilla's documentation for how to use specific APIs. Everything a normal extension can do, WXT can do as well, just via `browser` instead of `chrome`.
## Webextension Polyfill [β](#webextension-polyfill)
> Since `v0.1.0`
By default, WXT uses the [webextension-polyfill by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers.
To access types, you should import the relevant namespace from `wxt/browser`:
```ts
import { Runtime } from 'wxt/browser';
function handleMessage(message: any, sender: Runtime.Sender) {
// ...
}
```
### Disabling the polyfill [β](#disabling-the-polyfill)
> Since `v0.19.0`
After the release of MV3 and Chrome's official deprecation of MV2 in June 2024, the polyfill isn't really doing anything useful anymore.
You can disable it with a single line:
wxt.config.ts
```ts
export default defineConfig({
extensionApi: 'chrome',
});
```
This will change `wxt/browser` to simply export the `browser` or `chrome` globals based on browser at runtime:
```mjs
export const browser = globalThis.browser?.runtime?.id
? globalThis.browser
: globalThis.chrome;
```
Accessing types is a little different with the polyfill disabled. They do not need to be imported; they're available on the `browser` object itself:
```ts
function handleMessage(message: any, sender: browser.runtime.Sender) {
// ...
}
```
## Feature Detection [β](#feature-detection)
Depending on the manifest version and browser, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
:::warning
Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not.
:::
To check if an API is available, use feature detection:
```ts
if (browser.runtime.onSuspend != null) {
browser.runtime.onSuspend.addListener(() => {
// ...
});
}
```
Here, [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional%5Fchaining) is your best friend:
```ts
browser.runtime.onSuspend?.addListener(() => {
// ...
});
```
Alternatively, if you're trying to use similar APIs under different names (to support MV2 and MV3), you can do something like this:
```ts
(browser.action ?? browser.browser_action).onClicked.addListener(() => {
//
});
```
---
url: /guide/essentials/assets.html
title: Assets
---
# Assets [β](#assets)
## `/assets` Directory [β](#assets-directory)
Any assets imported or referenced inside the `/assets/` directory will be processed by WXT's bundler.
Here's how you access them:
:::code-group
```ts [JS]
import imageUrl from '~/assets/image.png';
const img = document.createElement('img');
img.src = imageUrl;
```
```html [HTML]
```
```css [CSS]
.bg-image {
background-image: url(~/assets/image.png);
}
```
```vue [Vue]
```
```jsx [JSX]
import image from '~/assets/image.png';
;
```
:::
## `/public` Directory [β](#public-directory)
Files inside `/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
Here's how you access them:
:::code-group
```ts [JS]
import imageUrl from '/image.png';
const img = document.createElement('img');
img.src = imageUrl;
```
```html [HTML]
```
```css [CSS]
.bg-image {
background-image: url(/image.png);
}
```
```vue [Vue]
```
```jsx [JSX]
```
:::
## Inside Content Scripts [β](#inside-content-scripts)
Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname.
But, inside content scripts, the hostname is whatever the tab is set to. So if you try to fetch the asset, manually or as an `
`'s `src`, it will be loaded from the tab's website, not your extension.
To fix this, you need to convert the image to a full URL using `browser.runtime.getURL`:
entrypoints/content.ts
```ts
import iconUrl from '/icon/128.png';
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log(iconUrl); // "/icon/128.png"
console.log(browser.runtime.getURL(iconUrl)); // "chrome-extension:///icon/128.png"
},
});
```
## WASM [β](#wasm)
How a `.wasm` file is loaded varies greatly between packages, but most follow a basic setup: Use a JS API to load and execute the `.wasm` file.
For an extension, that means two things:
1. The `.wasm` file needs to be present in output folder so it can be loaded.
2. You must import the JS API to load and initialize the `.wasm` file, usually provided by the NPM package.
For an example, let's say you have a content script needs to parse TS code into AST. We'll use [@oxc-parser/wasm](https://www.npmjs.com/package/@oxc-parser/wasm) to do it!
First, we need to copy the `.wasm` file to the output directory. We'll do it with a [WXT module](/guide/essentials/wxt-modules.html):
```ts
// modules/oxc-parser-wasm.ts
import { resolve } from 'node:path';
export default defineWxtModule((wxt) => {
wxt.hook('build:publicAssets', (_, assets) => {
assets.push({
absoluteSrc: resolve(
'node_modules/@oxc-parser/wasm/web/oxc_parser_wasm_bg.wasm',
),
relativeDest: 'oxc_parser_wasm_bg.wasm',
});
});
});
```
Run `wxt build`, and you should see the WASM file copied into your `.output/chrome-mv3` folder!
Next, since this is in a content script and we'll be fetching the WASM file over the network to load it, we need to add the file to the `web_accessible_resources`:
wxt.config.ts
```ts
export default defineConfig({
manifest: {
web_accessible_resources: [
{
// We'll use this matches in the content script as well
matches: ['*://*.github.com/*'],
// Use the same path as `relativeDest` from the WXT module
resources: ['/oxc_parser_wasm_bg.wasm'],
},
],
},
});
```
And finally, we need to load and initialize the `.wasm` file inside the content script to use it:
entrypoints/content.ts
```ts
import initWasm, { parseSync } from '@oxc-parser/wasm';
export default defineContentScript({
matches: '*://*.github.com/*',
async main(ctx) {
if (!location.pathname.endsWith('.ts')) return;
// Get text from GitHub
const code = document.getElementById(
'read-only-cursor-text-area',
)?.textContent;
if (!code) return;
const sourceFilename = document.getElementById('file-name-id')?.textContent;
if (!sourceFilename) return;
// Load the WASM file:
await initWasm({
module_or_path: browser.runtime.getURL('/oxc_parser_wasm_bg.wasm'),
});
// Once loaded, we can use `parseSync`!
const ast = parseSync(code, { sourceFilename });
console.log(ast);
},
});
```
This code is taken directly from `@oxc-parser/wasm` docs with one exception: We manually pass in a file path. In a standard NodeJS or web project, the default path works just fine so you don't have to pass anything in. However, extensions are different. You should always explicitly pass in the full URL to the WASM file in your output directory, which is what `browser.runtime.getURL` returns.
Run your extension, and you should see OXC parse the TS file!
---
url: /guide/essentials/target-different-browsers.html
title: Targeting Different Browsers
---
# Targeting Different Browsers [β](#targeting-different-browsers)
When building an extension with WXT, you can create multiple builds of your extension targeting different browsers and manifest versions.
## Target a Browser [β](#target-a-browser)
Use the `-b` CLI flag to create a separate build of your extension for a specific browser. By default, `chrome` is targeted.
```sh
wxt # same as: wxt -b chrome
wxt -b firefox
wxt -b custom
```
During development, if you target Firefox, Firefox will open. All other strings open Chrome by default. To customize which browsers open, see [Set Browser Binaries](/guide/essentials/config/browser-startup.html#set-browser-binaries).
Additionally, WXT defines several constants you can use at runtime to detect which browser is in use:
```ts
if (import.meta.env.BROWSER === 'firefox') {
console.log('Do something only in Firefox builds');
}
if (import.meta.env.FIREFOX) {
// Shorthand, equivalent to the if-statement above
}
```
Read about [Built-in Environment Variables](/guide/essentials/config/environment-variables.html#built-in-environment-variables) for more details.
## Target a Manifest Version [β](#target-a-manifest-version)
To target specific manifest versions, use the `--mv2` or `--mv3` CLI flags.
:::tip Default Manifest Version
By default, WXT will target MV2 for Safari and Firefox and MV3 for all other browsers.
:::
Similar to the browser, you can get the target manifest version at runtime using the [built-in environment variable](/guide/essentials/config/environment-variables.html#built-in-environment-variables):
```ts
if (import.meta.env.MANIFEST_VERSION === 2) {
console.log('Do something only in MV2 builds');
}
```
## Filtering Entrypoints [β](#filtering-entrypoints)
Every entrypoint can be included or excluded when targeting specific browsers via the `include` and `exclude` options.
Here are some examples:
* Content script only built when targeting `firefox`:
```ts
export default defineContentScript({
include: ['firefox'],
main(ctx) {
// ...
},
});
```
* HTML file only built for all targets other than `chrome`:
```html
```
Alternatively, you can use the [filterEntrypoints config](/api/reference/wxt/interfaces/InlineConfig.html#filterentrypoints) to list all the entrypoints you want to build.
---
url: /guide/essentials/content-scripts.html
title: Content Scripts
---
# Content Scripts [β](#content-scripts)
> To create a content script, see [Entrypoint Types](/guide/essentials/entrypoints.html#content-scripts).
## Context [β](#context)
The first argument to a content script's `main` function is its "context".
```ts
// entrypoints/example.content.ts
export default defineContentScript({
main(ctx) {},
});
```
This object is responsible for tracking whether or not the content script's context is "invalidated". Most browsers, by default, do not stop content scripts if the extension is uninstalled, updated, or disabled. When this happens, content scripts start reporting this error:
```
Error: Extension context invalidated.
```
The `ctx` object provides several helpers to stop asynchronous code from running once the context is invalidated:
```ts
ctx.addEventListener(...);
ctx.setTimeout(...);
ctx.setInterval(...);
ctx.requestAnimationFrame(...);
// and more
```
You can also check if the context is invalidated manually:
```ts
if (ctx.isValid) {
// do something
}
// OR
if (ctx.isInvalid) {
// do something
}
```
## CSS [β](#css)
In regular web extensions, CSS for content scripts is usually a separate CSS file, that is added to a CSS array in the manifest:
```json
{
"content_scripts": [
{
"css": ["content/style.css"],
"js": ["content/index.js"],
"matches": ["*://*/*"]
}
]
}
```
In WXT, to add CSS to a content script, simply import the CSS file into your JS entrypoint, and WXT will automatically add the bundled CSS output to the `css` array.
```ts
// entrypoints/example.content/index.ts
import './style.css';
export default defineContentScript({
// ...
});
```
To create a standalone content script that only includes a CSS file:
1. Create the CSS file: `entrypoints/example.content.css`
2. Use the `build:manifestGenerated` hook to add the content script to the manifest:
wxt.config.ts
```ts
export default defineConfig({
hooks: {
'build:manifestGenerated': (wxt, manifest) => {
manifest.content_scripts ??= [];
manifest.content_scripts.push({
// Build extension once to see where your CSS get's written to
css: ['content-scripts/example.css'],
matches: ['*://*/*'],
});
},
},
});
```
## UI [β](#ui)
WXT provides 3 built-in utilities for adding UIs to a page from a content script:
* [Integrated](#integrated) \- `createIntegratedUi`
* [Shadow Root](#shadow-root) \-`createShadowRootUi`
* [IFrame](#iframe) \- `createIframeUi`
Each has their own set of advantages and disadvantages.
| Method | Isolated Styles | Isolated Events | HMR | Use page's context |
| ----------- | --------------- | ------------------ | --- | ------------------ |
| Integrated | β | β | β | β
|
| Shadow Root | β
| β
(off by default) | β | β
|
| IFrame | β
| β
| β
| β |
### Integrated [β](#integrated)
Integrated content script UIs are injected alongside the content of a page. This means that they are affected by CSS on that page.
:::code-group
```ts [Vanilla]
// entrypoints/example-ui.content.ts
export default defineContentScript({
matches: [''],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Append children to the container
const app = document.createElement('p');
app.textContent = '...';
container.append(app);
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```ts [Vue]
// entrypoints/example-ui.content/index.ts
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: [''],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Create the app and mount it to the UI container
const app = createApp(App);
app.mount(container);
return app;
},
onRemove: (app) => {
// Unmount the app when the UI is removed
app.unmount();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```tsx [React]
// entrypoints/example-ui.content/index.tsx
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: [''],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Create a root on the UI container and render a component
const root = ReactDOM.createRoot(container);
root.render();
return root;
},
onRemove: (root) => {
// Unmount the root when the UI is removed
root.unmount();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```ts [Svelte]
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
matches: [''],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Create the Svelte app inside the UI container
mount(App, {
target: container,
});
},
onRemove: (app) => {
// Destroy the app when the UI is removed
unmount(app);
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```tsx [Solid]
// entrypoints/example-ui.content/index.ts
import { render } from 'solid-js/web';
export default defineContentScript({
matches: [''],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Render your app to the UI container
const unmount = render(() => ...
, container);
return unmount;
},
onRemove: (unmount) => {
// Unmount the app when the UI is removed
unmount();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
:::
See the [API Reference](/api/reference/wxt/client/functions/createIntegratedUi.html) for the complete list of options.
### Shadow Root [β](#shadow-root)
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
WXT's [createShadowRootUi](/api/reference/wxt/client/functions/createShadowRootUi.html) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
To use `createShadowRootUi`, follow these steps:
1. Import your CSS file at the top of your content script
2. Set [cssInjectionMode: "ui"](/api/reference/wxt/interfaces/BaseContentScriptEntrypointOptions.html#cssinjectionmode) inside `defineContentScript`
3. Define your UI with `createShadowRootUi()`
4. Mount the UI so it is visible to users
:::code-group
```ts [Vanilla]
// 1. Import the style
import './style.css';
export default defineContentScript({
matches: [''],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount(container) {
// Define how your UI will be mounted inside the container
const app = document.createElement('p');
app.textContent = 'Hello world!';
container.append(app);
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```ts [Vue]
// 1. Import the style
import './style.css';
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: [''],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Define how your UI will be mounted inside the container
const app = createApp(App);
app.mount(container);
return app;
},
onRemove: (app) => {
// Unmount the app when the UI is removed
app?.unmount();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```tsx [React]
// 1. Import the style
import './style.css';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: [''],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Container is a body, and React warns when creating a root on the body, so create a wrapper div
const app = document.createElement('div');
container.append(app);
// Create a root on the UI container and render a component
const root = ReactDOM.createRoot(app);
root.render();
return root;
},
onRemove: (root) => {
// Unmount the root when the UI is removed
root?.unmount();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```ts [Svelte]
// 1. Import the style
import './style.css';
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
matches: [''],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Create the Svelte app inside the UI container
mount(App, {
target: container,
});
},
onRemove: () => {
// Destroy the app when the UI is removed
unmount(app);
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```tsx [Solid]
// 1. Import the style
import './style.css';
import { render } from 'solid-js/web';
export default defineContentScript({
matches: [''],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount: (container) => {
// Render your app to the UI container
const unmount = render(() => ...
, container);
},
onRemove: (unmount) => {
// Unmount the app when the UI is removed
unmount?.();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
:::
See the [API Reference](/api/reference/wxt/client/functions/createShadowRootUi.html) for the complete list of options.
Full examples:
* [react-content-script-ui](https://github.com/wxt-dev/examples/tree/main/examples/react-content-script-ui)
* [tailwindcss](https://github.com/wxt-dev/examples/tree/main/examples/tailwindcss)
### IFrame [β](#iframe)
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
WXT provides a helper function, [createIframeUi](/api/reference/wxt/client/functions/createIframeUi.html), which simplifies setting up the IFrame.
1. Create an HTML page that will be loaded into your IFrame:
```html
Your Content Script IFrame
```
2. Add the page to the manifest's `web_accessible_resources`:
wxt.config.ts
```ts
export default defineConfig({
manifest: {
web_accessible_resources: [
{
resources: ['example-iframe.html'],
matches: [...],
},
],
},
});
```
3. Create and mount the IFrame:
```ts
export default defineContentScript({
matches: [''],
main(ctx) {
// Define the UI
const ui = createIframeUi(ctx, {
page: '/example-iframe.html',
position: 'inline',
anchor: 'body',
onMount: (wrapper, iframe) => {
// Add styles to the iframe like width
iframe.width = '123';
},
});
// Show UI to user
ui.mount();
},
});
```
See the [API Reference](/api/reference/wxt/client/functions/createIframeUi.html) for the complete list of options.
## Isolated World vs Main World [β](#isolated-world-vs-main-world)
By default, all content scripts run in an isolated context where only the DOM is shared with the webpage it is running on - an "isolated world". In MV3, Chromium introduced the ability to run content scripts in the "main" world - where everything, not just the DOM, is available to the content script, just like if the script were loaded by the webpage.
You can enable this for a content script by setting the `world` option:
```ts
export default defineContentScript({
world: 'MAIN',
});
```
However, this approach has several notable drawbacks:
* Doesn't support MV2
* `world: "MAIN"` is only supported by Chromium browsers
* Main world content scripts don't have access to the extension API
Instead, WXT recommends injecting a script into the main world manually using it's `injectScript` function. This will address the drawbacks mentioned before.
* `injectScript` supports both MV2 and MV3
* `injectScript` supports all browsers
* Having a "parent" content script means you can send messages back and forth, making it possible to access the extension API
To use `injectScript`, we need two entrypoints, one content script and one unlisted script:
```html
π entrypoints/
π example.content.ts
π example-main-world.ts
```
```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
console.log('Hello from the main world');
});
```
```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
console.log('Injecting script...');
await injectScript('/example-main-world.js', {
keepInDom: true,
});
console.log('Done!');
},
});
```
```json
export default defineConfig({
manifest: {
// ...
web_accessible_resources: [
{
resources: ["example-main-world.js"],
matches: ["*://*/*"],
}
]
}
});
```
`injectScript` works by creating a `script` element on the page pointing to your script. This loads the script into the page's context so it runs in the main world.
`injectScript` returns a promise, that when resolved, means the script has been evaluated by the browser and you can start communicating with it.
:::warning Warning: run\_at Caveat
For MV3, `injectScript` is synchronous and the injected script will be evaluated at the same time as your the content script's `run_at`.
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `
```
## Background β₯0.16.0 [β](#background)
By default, your background will be bundled into a single file as IIFE. You can change this by setting `type: "module"` in your background entrypoint:
```ts
export default defineBackground({
type: 'module',
main() {
// ...
},
});
```
This will change the output format to ESM, enable code-spliting between your background script and HTML pages, and set `"type": "module"` in your manifest.
:::warning
Only MV3 supports ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
:::
## Content Scripts [β](#content-scripts)
WXT does not yet include built-in support for bundling content scripts as ESM. The plan is to add support for chunking to reduce bundle size, but not support HMR for now. There are several technical issues that make implementing a generic solution for HMR impossible. See [Content Script ESM Support #357](https://github.com/wxt-dev/wxt/issues/357) for details.
If you can't wait, and need ESM support right now, you can implement ESM support manually. See the [ESM Content Script UI](https://github.com/wxt-dev/examples/tree/main/examples/esm-content-script-ui) example to learn how.
---
url: /guide/essentials/remote-code.html
title: Remote Code
---
# Remote Code [β](#remote-code)
WXT will automatically download and bundle imports with the `url:` prefix so the extension does not depend on remote code, [a requirement from Google for MV3](https://developer.chrome.com/docs/extensions/migrating/improve-security/#remove-remote-code).
## Google Analytics [β](#google-analytics)
For example, you can import Google Analytics:
```ts
// utils/google-analytics.ts
import 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';
window.dataLayer = window.dataLayer || [];
// NOTE: This line is different from Google's documentation
window.gtag = function () {
dataLayer.push(arguments);
};
gtag('js', new Date());
gtag('config', 'G-XXXXXX');
```
Then you can import this in your HTML files to enable Google Analytics:
```ts
// popup/main.ts
import '~/utils/google-analytics';
gtag('event', 'event_name', {
key: 'value',
});
```
---
url: /guide/essentials/unit-testing.html
title: Unit Testing
---
# Unit Testing [β](#unit-testing)
* [Vitest](#vitest)
* [Example Tests](#example-tests)
* [Other Testing Frameworks](#other-testing-frameworks)
## Vitest [β](#vitest)
WXT provides first class support for Vitest for unit testing:
```ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { WxtVitest } from 'wxt/testing';
export default defineConfig({
plugins: [WxtVitest()],
});
```
This plugin does several things:
* Polyfills the extension API, `browser`, with an in-memory implementation using [@webext-core/fake-browser](https://webext-core.aklinker1.io/fake-browser/installation)
* Adds all vite config or plugins in `wxt.config.ts`
* Configures auto-imports (if enabled)
* Applies internal WXT vite plugins for things like [bundling remote code](/guide/essentials/remote-code.html)
* Sets up global variables provided by WXT (`import.meta.env.BROWSER`, `import.meta.env.MANIFEST_VERSION`, `import.meta.env.IS_CHROME`, etc)
* Configures aliases (`@/*`, `@@/*`, etc) so imports can be resolved
Here are real projects with unit testing setup. Look at the code and tests to see how they're written.
* [aklinker1/github-better-line-counts](https://github.com/aklinker1/github-better-line-counts)
* [wxt-dev/examples's Vitest Example](https://github.com/wxt-dev/examples/tree/main/examples/vitest-unit-testing)
### Example Tests [β](#example-tests)
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/storage`) in tests - [@webext-core/fake-browser](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
```ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';
const accountStorage = storage.defineItem('local:account');
async function isLoggedIn(): Promise {
const value = await accountStorage.getValue();
return value != null;
}
describe('isLoggedIn', () => {
beforeEach(() => {
// See https://webext-core.aklinker1.io/fake-browser/reseting-state
fakeBrowser.reset();
});
it('should return true when the account exists in storage', async () => {
const account: Account = {
username: '...',
preferences: {
// ...
},
};
await accountStorage.setValue(account);
expect(await isLoggedIn()).toBe(true);
});
it('should return false when the account does not exist in storage', async () => {
await accountStorage.deleteValue();
expect(await isLoggedIn()).toBe(false);
});
});
```
## Other Testing Frameworks [β](#other-testing-frameworks)
To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use.
It is possible to do, but will require a bit more setup. Refer to Vitest's setup for an example of how to setup a test environment:
---
url: /guide/essentials/e2e-testing.html
title: E2E Testing
---
# E2E Testing [β](#e2e-testing)
## Playwright [β](#playwright)
[Playwright](https://playwright.dev) is the only good option for writing Chrome Extension end-to-end tests.
To add E2E tests to your project, follow Playwright's [Chrome Extension docs](https://playwright.dev/docs/chrome-extensions). When you have to pass the path to your extension, pass the output directory, `/path/to/project/.output/chrome-mv3`.
For a complete example, see the [WXT's Playwright Example](https://github.com/wxt-dev/examples/tree/main/examples/playwright-e2e-testing).
---
url: /guide/essentials/publishing.html
title: Publishing
---
# Publishing [β](#publishing)
WXT can ZIP your extension and submit it to various stores for review or for self-hosting.
## First Time Publishing [β](#first-time-publishing)
If you're publishing an extension to a store for the first time, you must manually navigate the process. WXT doesn't help you create listings, each store has unique steps and requirements that you need to familiarize yourself with.
For specific details about each store, see the stores sections below.
* [Chrome Web Store](#chrome-web-store)
* [Firefox Addon Store](#firefox-addon-store)
* [Edge Addons](#edge-addons)
## Automation [β](#automation)
WXT provides two commands to help automate submitting a new version for review and publishing:
* `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command
* `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
To get started, run `wxt submit init` and follow the prompts. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
> In CI, make sure you add all the environment variables to the submit step.
To submit a new version for publishing, build all the ZIPs you plan on releasing:
```sh
wxt zip
wxt zip -b firefox
```
Then run the `wxt submit` command, passing in all the ZIP files you want to release. In this case, we'll do a release for all 3 major stores: Chrome Web Store, Edge Addons, and Firefox Addons Store.
If it's your first time running the command or you recently made changes to the release process, you'll want to test your secrets by passing the `--dry-run` flag.
```sh
wxt submit --dry-run \
--chrome-zip .output/{your-extension}-{version}-chrome.zip \
--firefox-zip .output/{your-extension}-{version}-firefox.zip --firefox-sources-zip .output/{your-extension}-{version}-sources.zip \
--edge-zip .output/{your-extension}-{version}-chrome.zip
```
If the dry run passes, remove the flag and do the actual release:
```sh
wxt submit \
--chrome-zip .output/{your-extension}-{version}-chrome.zip \
--firefox-zip .output/{your-extension}-{version}-firefox.zip --firefox-sources-zip .output/{your-extension}-{version}-sources.zip \
--edge-zip .output/{your-extension}-{version}-chrome.zip
```
:::warning
See the [Firefox Addon Store](#firefox-addon-store) section for more details about the `--firefox-sources-zip` option.
:::
## GitHub Action [β](#github-action)
Here's an example of a GitHub Action that submits new versions of an extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
```yml
name: Release
on:
workflow_dispatch:
jobs:
submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Zip extensions
run: |
pnpm zip
pnpm zip:firefox
- name: Submit to stores
run: |
pnpm wxt submit \
--chrome-zip .output/*-chrome.zip \
--firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip
env:
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
```
The action above lays the foundation for a basic workflow, including `zip` and `submit` steps. To further enhance your GitHub Action and delve into more complex scenarios, consider exploring the following examples from real projects. They introduce advanced features such as version management, changelog generation, and GitHub releases, tailored for different needs:
* [aklinker1/github-better-line-counts](https://github.com/aklinker1/github-better-line-counts/blob/main/.github/workflows/submit.yml) \- Conventional commits, automated version bump and changelog generation, triggered manually, optional dry run for testing
* [GuiEpi/plex-skipper](https://github.com/GuiEpi/plex-skipper/blob/main/.github/workflows/deploy.yml) \- Triggered automatically when `package.json` version is changed, creates and uploads artifacts to GitHub release.
> These examples are designed to provide clear insights and are a good starting point for customizing your own workflows. Feel free to explore and adapt them to your project needs.
## Stores [β](#stores)
### Chrome Web Store [β](#chrome-web-store)
> β
Supported β’ [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) β’ [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
To create a ZIP for Chrome:
```sh
wxt zip
```
### Firefox Addon Store [β](#firefox-addon-store)
> β
Supported β’ [Developer Dashboard](https://addons.mozilla.org/developers/) β’ [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
When running `wxt zip -b firefox`, WXT will zip both your extension and sources. Certain files (such as config files, hidden files, tests, and excluded entrypoints) are automatically excluded from your sources. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
To customize which files are zipped, add the `zip` option to your config file.
wxt.config.ts
```ts
import { defineConfig } from 'wxt';
export default defineConfig({
zip: {
// ...
},
});
```
If it's your first time submitting to the Firefox Addon Store, or if you've updated your project layout, always test your sources ZIP! The commands below should allow you to rebuild your extension from inside the extracted ZIP.
:::code-group
```sh [pnpm]
pnpm i
pnpm zip:firefox
```
```sh [npm]
npm i
npm run zip:firefox
```
```sh [yarn]
yarn
yarn zip:firefox
```
```sh [bun]
bun i
bun zip:firefox
```
:::
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.
:::warning
If you use a `.env` files, they can affect the chunk hashes in the output directory. Either delete the .env file before running `wxt zip -b firefox`, or include it in your sources zip with the [zip.includeSources](/api/reference/wxt/interfaces/InlineConfig.html#includesources) option. Be careful to not include any secrets in your `.env` files.
See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
:::
#### Private Packages [β](#private-packages)
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
wxt.config.ts
```ts
export default defineConfig({
zip: {
downloadPackages: [
'@mycompany/some-package',
//...
],
},
});
```
Depending on your package manager, the `package.json` in the sources zip will be modified to use the downloaded dependencies via the `overrides` or `resolutions` field.
:::warning
WXT uses the command `npm pack ` to download the package. That means regardless of your package manager, you need to properly setup a `.npmrc` file. NPM and PNPM both respect `.npmrc` files, but Yarn and Bun have their own ways of authorizing private registries, so you'll need to add a `.npmrc` file.
:::
### Safari [β](#safari)
> π§ Not supported yet
WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide:
* [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari%5Fweb%5Fextensions/converting%5Fa%5Fweb%5Fextension%5Ffor%5Fsafari) \- "Convert your existing extension to a Safari web extension using Xcodeβs command-line tool."
When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
```sh
pnpm wxt build -b safari
xcrun safari-web-extension-converter .output/safari-mv2
```
### Edge Addons [β](#edge-addons)
> β
Supported β’ [Developer Dashboard](https://aka.ms/PartnerCenterLogin) β’ [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
No need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP.
However, if you have features specifically for Edge, create a separate ZIP with:
```sh
wxt zip -b edge
```
---
url: /guide/essentials/testing-updates.html
title: Testing Updates
---
# Testing Updates [β](#testing-updates)
## Testing Permission Changes [β](#testing-permission-changes)
When `permissions`/`host_permissions` change during an update, depending on what exactly changed, the browser will disable your extension until the user accepts the new permissions.
You can test if your permission changes will result in a disabled extension:
* Chromium: Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)
* Firefox: See their [Test Permission Requests](https://extensionworkshop.com/documentation/develop/test-permission-requests/) page
* Safari: Everyone breaks something in production eventually... π«‘ Good luck soldier
## Update Event [β](#update-event)
You can setup a callback that runs after your extension updates like so:
```ts
browser.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'update') {
// Do something
}
});
```
If the logic is simple, write a unit test to cover this logic. If you feel the need to manually test this callback, you can either:
1. In dev mode, remove the `if` statement and reload the extension from `chrome://extensions`
2. Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)
---
url: /guide/resources/compare.html
title: Compare
---
# Compare [β](#compare)
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another framework) and [CRXJS](https://crxjs.dev/vite-plugin) (a bundler plugin).
## Overview [β](#overview)
* β
- Full support
* π‘ - Partial support
* β - No support
| Features | WXT | Plasmo | CRXJS |
| ----------------------------------------------- | ------------------ | ------------------ | ------------------ |
| Maintained | β
| π‘ [\[1\]](#fn1) | π‘ [\[2\]](#fn2) |
| Supports all browsers | β
| β
| π‘ [\[3\]](#fn3) |
| MV2 Support | β
| β
| π‘ [\[4\]](#fn4) |
| MV3 Support | β
| β
| π‘ [\[4\]](#fn4) |
| Create Extension ZIPs | β
| β
| β |
| Create Firefox Sources ZIP | β
| β | β |
| First-class TypeScript support | β
| β
| β
|
| Entrypoint discovery | β
[\[5\]](#fn5) | β
[\[5\]](#fn5) | β |
| Inline entrypoint config | β
| β
| β [\[6\]](#fn6) |
| Auto-imports | β
| β | β |
| Reusable module system | β
| β | β |
| Supports all frontend frameworks | β
| π‘ [\[7\]](#fn7) | β
|
| Framework specific entrypoints (like Popup.tsx) | π‘ [\[8\]](#fn8) | β
[\[9\]](#fn9) | β |
| Automated publishing | β
| β
| β |
| Remote Code Bundling (Google Analytics) | β
| β
| β |
| Unlisted HTML Pages | β
| β
| β
|
| Unlisted Scripts | β
| β | β |
| ESM Content Scripts | β [\[10\]](#fn10) | β | β
|
| **Dev Mode** | | | |
| .env Files | β
| β
| β
|
| Opens browser with extension installed | β
| β | β |
| HMR for UIs | β
| π‘ [\[11\]](#fn11) | β
|
| Reload HTML Files on Change | β
| π‘ [\[12\]](#fn12) | β
|
| Reload Content Scripts on Change | β
| π‘ [\[12\]](#fn12) | β
|
| Reload Background on Change | π‘ [\[12\]](#fn12) | π‘ [\[12\]](#fn12) | π‘ [\[12\]](#fn12) |
| Respects Content Script run\_at | β
| β
| β [\[13\]](#fn13) |
| **Built-in Wrappers** | | | |
| Storage | β
| β
| β [\[14\]](#fn14) |
| Messaging | β [\[14\]](#fn14) | β
| β [\[14\]](#fn14) |
| Content Script UI | β
| β
| β [\[14\]](#fn14) |
| I18n | β
| β | β |
---
1. Appears to be in maintenance mode with little to no maintainers nor feature development happening and _(see [wxt-dev/wxt#1404 (comment)](https://github.com/wxt-dev/wxt/pull/1404#issuecomment-2643089518))_ [β©οΈ](#fnref1)
2. See [crxjs/chrome-extension-tools#974](https://github.com/crxjs/chrome-extension-tools/discussions/974) [β©οΈ](#fnref2)
3. As of `v2.0.0-beta.23`, but v2 stable hasn't been released yet. [β©οΈ](#fnref3)
4. Either MV2 or MV3, not both. [β©οΈ](#fnref4) [β©οΈ](#fnref4)
5. File based. [β©οΈ](#fnref5) [β©οΈ](#fnref5)
6. Entrypoint options all configured in `manifest.json`. [β©οΈ](#fnref6)
7. Only React, Vue, and Svelte. [β©οΈ](#fnref7)
8. `.html`, `.ts`, `.tsx`. [β©οΈ](#fnref8)
9. `.html`, `.ts`, `.tsx`, `.vue`, `.svelte`. [β©οΈ](#fnref9)
10. WIP, moving very slowly. Follow [wxt-dev/wxt#357](https://github.com/wxt-dev/wxt/issues/357) for updates. [β©οΈ](#fnref10)
11. React only. [β©οΈ](#fnref11)
12. Reloads entire extension. [β©οΈ](#fnref12) [β©οΈ](#fnref12) [β©οΈ](#fnref12) [β©οΈ](#fnref12) [β©οΈ](#fnref12)
13. ESM-style loaders run asynchronously. [β©οΈ](#fnref13)
14. There is no built-in wrapper around this API. However, you can still access the standard APIs via `chrome`/`browser` globals or use any 3rd party NPM package. [β©οΈ](#fnref14) [β©οΈ](#fnref14) [β©οΈ](#fnref14) [β©οΈ](#fnref14)
---
url: /guide/resources/faq.html
title: FAQ
---
# FAQ [β](#faq)
Commonly asked questions about how to use WXT or why it behaves the way it does.
* [Why aren't content scripts added to the manifest?](#why-aren-t-content-scripts-added-to-the-manifest)
* [How do I disable opening the browser automatically during development?](#how-do-i-disable-opening-the-browser-automatically-during-development)
* [How do I stay logged into a website during development?](#how-do-i-stay-logged-into-a-website-during-development)
* [My component library doesn't work in content scripts!](#my-component-library-doesn-t-work-in-content-scripts)
* [Is there an LLM trained on WXT's docs that I chat with?](#is-there-an-llm-trained-on-wxt-s-docs-that-i-chat-with)
## Why aren't content scripts added to the manifest? [β](#why-aren-t-content-scripts-added-to-the-manifest)
During development, WXT registers content scripts dynamically so they can be reloaded individually when a file is saved without reloading your entire extension.
To list the content scripts registered during development, open the service worker's console and run:
```js
await chrome.scripting.getRegisteredContentScripts();
```
## How do I disable opening the browser automatically during development? [β](#how-do-i-disable-opening-the-browser-automatically-during-development)
See
## How do I stay logged into a website during development? [β](#how-do-i-stay-logged-into-a-website-during-development)
See
## My component library doesn't work in content scripts! [β](#my-component-library-doesn-t-work-in-content-scripts)
This is usually caused by one of two things (or both) when using `createShadowRootUi`:
1. Styles are added outside the `ShadowRoot`
:::details
Some component libraries manually add CSS to the page by adding a `