--- 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) ![WXT](/hero-logo.svg) [🌐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. Are you an LLM? View /llms.txt for optimized Markdown documentation, or /llms-full.txt for full documentation bundle ## 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). [![WXT Sponsors](https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors-wide.svg)](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 --- Are you an LLM? You can read better optimized documentation at /guide/introduction.md for this page in Markdown format # 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 --- Are you an LLM? You can read better optimized documentation at /guide/installation.md for this page in Markdown format # 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 [Bun] bunx wxt@latest init ``` ```sh [PNPM] pnpm dlx 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: [![TypeScript Logo](https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/typescript.svg)Vanilla](https://github.com/wxt-dev/wxt/tree/main/templates/vanilla) [![Vue Logo](https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/vue.svg)Vue](https://github.com/wxt-dev/wxt/tree/main/templates/vue) [![React Logo](https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/react.svg)React](https://github.com/wxt-dev/wxt/tree/main/templates/react) [![Svelte Logo](https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/svelte.svg)Svelte](https://github.com/wxt-dev/wxt/tree/main/templates/svelte) [![Solid Logo](https://www.solidjs.com/img/favicons/favicon-32x32.png)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) ![wxt init demo](/assets/init-demo.PlKCdzRF.gif) 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 [Bun] cd my-project bun init ``` ```sh [PNPM] cd my-project pnpm init ``` ```sh [NPM] cd my-project npm init ``` ```sh [Yarn] cd my-project yarn init ``` ::: 2. Install WXT: :::code-group ```sh [Bun] bun add -D wxt ``` ```sh [PNPM] pnpm 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 [Bun] bun run dev ``` ```sh [PNPM] pnpm 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/project-structure.md for this page in Markdown format # 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 source code for your project's composable functions for Vue * `entrypoints/`: Contains all the entrypoints that get bundled into your extension * `hooks/`: Auto-imported by default, contains source code for your project's hooks for React and Solid * `modules/`: Contains [local WXT Modules](/guide/essentials/wxt-modules.html) for your project * `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/ πŸ“ modules/ πŸ“ public/ πŸ“‚ src/ πŸ“ assets/ πŸ“ components/ πŸ“ composables/ πŸ“ entrypoints/ πŸ“ hooks/ πŸ“ 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: "." modulesDir: "wxt-modules", // default: "modules" outDir: "dist", // default: ".output" publicDir: "static", // default: "public" // Relative to srcDir entrypointsDir: "entries", // default: "entrypoints" }) ``` You can use absolute or relative paths. --- url: /guide/essentials/entrypoints.html title: Entrypoints --- Are you an LLM? You can read better optimized documentation at /guide/essentials/entrypoints.md for this page in Markdown format # 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 (TS, JSX, SCSS, etc). ## Folder Structure [​](#folder-structure) Inside the `entrypoints/` directory, an entrypoint is defined as a single file or directory (with an `index` file) inside it. :::code-group ```html [Single File] πŸ“‚ entrypoints/ πŸ“„ {name}.{ext} ``` ```html [Directory] πŸ“‚ entrypoints/ πŸ“‚ {name}/ πŸ“„ index.{ext} ``` ::: The entrypoint's `name` dictates the type of entrypoint. For example, to add a ["Background" entrypoint](#background), either of these files would work: :::code-group ```html [Single File] πŸ“‚ entrypoints/ πŸ“„ background.ts ``` ```html [Directory] πŸ“‚ entrypoints/ πŸ“‚ background/ πŸ“„ index.ts ``` ::: Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns. ### Including Other Files [​](#including-other-files) When using an entrypoint directory, `entrypoints/{name}/index.{ext}`, you can add related files next to the `index` file. ```html πŸ“‚ entrypoints/ πŸ“‚ popup/ πŸ“„ index.html ← This file is the entrypoint πŸ“„ main.ts πŸ“„ style.css πŸ“‚ background/ πŸ“„ index.ts ← This file is the entrypoint πŸ“„ alarms.ts πŸ“„ messaging.ts πŸ“‚ youtube.content/ πŸ“„ index.ts ← This file is the entrypoint πŸ“„ style.css ``` :::danger **DO NOT** put files related to an entrypoint directly inside the `entrypoints/` directory. WXT will treat them as entrypoints and try to build them, usually resulting in an error. Instead, use a directory for that entrypoint: ```html πŸ“‚ entrypoints/ πŸ“„ popup.html πŸ“„ popup.ts πŸ“„ popup.css πŸ“‚ popup/ πŸ“„ index.html πŸ“„ main.ts πŸ“„ style.css ``` ::: ### Deeply Nested Entrypoints [​](#deeply-nested-entrypoints) While the `entrypoints/` directory might resemble the `pages/` directory of other web frameworks, like Nuxt or Next.js, **it does not support deeply nesting entrypoints** in the same way. Entrypoints must be zero or one levels deep for WXT to discover and build them: ```html πŸ“‚ entrypoints/ πŸ“‚ youtube/ πŸ“‚ content/ πŸ“„ index.ts πŸ“„ ... πŸ“‚ injected/ πŸ“„ index.ts πŸ“„ ... πŸ“‚ youtube.content/ πŸ“„ index.ts πŸ“„ ... πŸ“‚ youtube-injected/ πŸ“„ index.ts πŸ“„ ... ``` ## Unlisted Entrypoints [​](#unlisted-entrypoints) In web extensions, there are two types of entrypoints: 1. **Listed**: Referenced in the `manifest.json` 2. **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 Script However, not all entrypoints in web extensions are listed in the manifest. Some are not listed in the manifest, but are still used by extensions. For example: * A welcome page shown in a new tab when the extension is installed * JS files injected by content scripts into the main world For more details on how to add unlisted entrypoints, see: * [Unlisted Pages](#unlisted-pages) * [Unlisted Scripts](#unlisted-scripts) * [Unlisted CSS](#unlisted-css) ## 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) | 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 }, }); ``` ::: For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker. When defining your background entrypoint, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function. ```ts browser.action.onClicked.addListener(() => { // ... }); export default defineBackground(() => { browser.action.onClicked.addListener(() => { // ... }); }); ``` Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders.html) documentation for more details. ### 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 ``` When you define a Bookmarks entrypoint, WXT will automatically update the manifest to override the browser's bookmarks page with your own HTML page. ### 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) | Filename | Output Path | | ------------------------------------------ | --------------------------- | | entrypoints/content.\[jt\]sx? | /content-scripts/content.js | | entrypoints/content/index.\[jt\]sx? | /content-scripts/content.js | | entrypoints/{name}.content.\[jt\]sx? | /content-scripts/{name}.js | | entrypoints/{name}.content/index.\[jt\]sx? | /content-scripts/{name}.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 }, }); ``` When defining content script entrypoints, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function. ```ts const container = document.createElement('div'); document.body.append(container); export default defineContentScript({ main: function () { const container = document.createElement('div'); document.body.append(container); }, }); ``` Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders.html) documentation for more details. See [Content Script UI](/guide/essentials/content-scripts.html) for more info on creating UIs and including CSS in content scripts. ### 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) | Filename | Output Path | | ------------------------------- | -------------- | | entrypoints/devtools.html | /devtools.html | | entrypoints/devtools/index.html | /devtools.html | ```html ``` Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes. ### 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 ``` When you define a History entrypoint, WXT will automatically update the manifest to override the browser's history page with your own HTML page. ### 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 ``` When you define a Newtab entrypoint, WXT will automatically update the manifest to override the browser's new tab page with your own HTML page. ### 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/{name}.sandbox.html | /{name}.html | | entrypoints/{name}.sandbox/index.html | /{name}.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) | Filename | Output Path | | --------------------------------------- | --------------- | | entrypoints/sidepanel.html | /sidepanel.html | | entrypoints/sidepanel/index.html | /sidepanel.html | | entrypoints/{name}.sidepanel.html | /{name}.html | | entrypoints/{name}.sidepanel/index.html | /{name}.html | ```html Default Side Panel Title ``` In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API. ### Unlisted CSS [​](#unlisted-css) | Filename | Output Path | | ------------------------------------------------------------------ | ---------------------------- | | entrypoints/{name}.(css\|scss|sass|less|styl|stylus) | /{name}.css | | entrypoints/{name}/index.(css\|scss|sass|less|styl|stylus) | /{name}.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/{name}.content.(css\|scss|sass|less|styl|stylus) | /content-scripts/{name}.css | | entrypoints/{name}.content/index.(css\|scss|sass|less|styl|stylus) | /content-scripts/{name}.css | ```css body { /* ... */ } ``` 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. ### Unlisted Pages [​](#unlisted-pages) | Filename | Output Path | | ----------------------------- | ------------ | | entrypoints/{name}.html | /{name}.html | | entrypoints/{name}/index.html | /{name}.html | ```html Title ``` At runtime, unlisted pages are accessible at `/{name}.html`: ```ts const url = browser.runtime.getURL('/{name}.html'); console.log(url); // "chrome-extension://{id}/{name}.html" window.open(url); // Open the page in a new tab ``` ### Unlisted Scripts [​](#unlisted-scripts) | Filename | Output Path | | ---------------------------------- | ----------- | | entrypoints/{name}.\[jt\]sx? | /{name}.js | | entrypoints/{name}/index.\[jt\]sx? | /{name}.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 }, }); ``` ::: At runtime, unlisted scripts are accessible from `/{name}.js`: ```ts const url = browser.runtime.getURL('/{name}.js'); console.log(url); // "chrome-extension://{id}/{name}.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). When defining an unlisted script, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function. ```ts document.querySelectorAll('a').forEach((anchor) => { // ... }); export default defineUnlistedScript(() => { document.querySelectorAll('a').forEach((anchor) => { // ... }); }); ``` Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders.html) documentation for more details. --- url: /guide/essentials/config/manifest.html title: Manifest --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/manifest.md for this page in Markdown format # 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: ```plaintext 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. ### Firefox `theme_icons` [​](#firefox-theme-icons) Firefox supports a [theme\_icons](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#theme%5Ficons) field on the toolbar action that swaps between a light and dark variant based on the current browser theme. When [targeting Firefox](/guide/essentials/target-different-browsers.html), WXT auto-discovers paired light/dark icons in the `public/` directory and attaches them to `action` (MV3) or `browser_action` (MV2). You only need to drop both variants next to your regular icons: ```plaintext public/ β”œβ”€ icon-16.png # regular (default_icon) β”œβ”€ icon-light-16.png # Firefox light theme β”œβ”€ icon-dark-16.png # Firefox dark theme β”œβ”€ icon-32.png β”œβ”€ icon-light-32.png └─ icon-dark-32.png ``` A size is only included in `theme_icons` if **both** a light and a dark file are present. The following filename patterns are discovered: ```ts const themeIconRegex = [ /^icon-(?light|dark)-(?[0-9]+)\.png$/, // icon-light-16.png /^icon-(?light|dark)-(?[0-9]+)x[0-9]+\.png$/, // icon-light-16x16.png /^icon-(?[0-9]+)-(?light|dark)\.png$/, // icon-16-light.png /^icon-(?[0-9]+)x[0-9]+-(?light|dark)\.png$/, // icon-16x16-light.png /^icons?[/\\](?light|dark)-(?[0-9]+)\.png$/, // icon/light-16.png | icons/light-16.png /^icons?[/\\](?light|dark)-(?[0-9]+)x[0-9]+\.png$/, // icon/light-16x16.png /^icons?[/\\](?[0-9]+)-(?light|dark)\.png$/, // icon/16-light.png /^icons?[/\\](?[0-9]+)x[0-9]+-(?light|dark)\.png$/, // icon/16x16-light.png ]; ``` Only `.png` files are discovered today, even though Firefox supports `.svg` \- follow [#1120](https://github.com/wxt-dev/wxt/issues/1120) for updates. If you set `manifest.action.theme_icons` (or `manifest.browser_action.theme_icons`) explicitly in `wxt.config.ts`, WXT will not overwrite it. ## 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'], }, }); ``` :::warning Different browsers support different permissions. You are responsible for passing only the permissions required for each browser: ```ts export default defineConfig({ manifest: ({ browser }) => ({ permissions: browser === 'chrome' ? ['storage', 'favicon', 'declarativeNetRequest'] : ['storage', 'webRequest'], }), }); ``` ::: ## 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/browser-startup.md for this page in Markdown format # Browser Startup [​](#browser-startup) During development, WXT will use any of the below packages to automatically open a browser with your extension installed. * [web-ext by Mozilla](https://www.npmjs.com/package/web-ext) Just install the dependency you want WXT to use to open the browser. ## `web-ext` Usage [​](#web-ext-usage) > See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig.html) for a full list of config. ### 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 web-ext.config.ts ```ts import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ // ... }); ``` 2. `/wxt.config.ts`: Via the [webExt config](/api/reference/wxt/interfaces/InlineConfig.html#webext), 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: web-ext.config.ts ```ts import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ 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" }, }); ``` wxt.config.ts ```ts export default defineConfig({ webExt: { 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 in any of the config files mentioned above: :::code-group ```ts [Mac/Linux] import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'], }); ``` ```ts [Windows] import { resolve } from 'node:path'; import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ // 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 don't want to uninstall `web-ext`, like to test in your normal profile, you can do so via `disabled: true`: web-ext.config.ts ```ts import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ disabled: true, }); ``` ### Enabling Chrome Features [​](#enabling-chrome-features) Some APIs are disabled in Chrome during development because of the default flags `web-ext` uses to launch Chrome, like the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api). If your extension depends on new features or services, you can enable them via `chromiumArgs`: ```ts import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ chromiumArgs: [ // For example, this flag enables the Prompt API '--disable-features=DisableLoadExtensionCommandLineSwitch', ], }); ``` There is no comprehensive list of what feature flags enable what APIs and services. Alternatively, if you can't find a flag that enables a feature you're looking for, [disable the opening the browser during development](#disable-opening-browser) and use your regular chrome profile for development. --- url: /guide/essentials/config/auto-imports.html title: Auto-imports --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/auto-imports.md for this page in Markdown format # 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 automatically sets up auto-imports for all of it's own APIs and some of your project directories: * `/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. To see the complete list of auto-imported APIs, run [wxt prepare](/api/cli/wxt-prepare.html) and look at your project's `.wxt/types/imports-module.d.ts` file. ## 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, }); ``` ## Explicit Imports (`#imports`) [​](#explicit-imports-imports) You can manually import all of WXT's APIs via the `#imports` module: ```ts import { createShadowRootUi, ContentScriptContext, MatchPattern, } from '#imports'; ``` To learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module.html). If you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place. --- url: /guide/essentials/config/environment-variables.html title: Environment Variables --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/environment-variables.md for this page in Markdown format # 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: ```plaintext .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 set the [targetBrowsers](/api/reference/wxt/interfaces/InlineConfig.html#targetbrowsers) option to make the `BROWSER` variable a more specific type, like `"chrome" | "firefox"`. 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({ 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. Note that Vite's runtime environment variables, like `import.meta.env.DEV`, will not be defined. Instead, access the `mode` like this: ```ts export default defineConfig({ manifest: ({ mode }) => { const isDev = mode === 'development'; console.log('Is development mode:', isDev); // ... }, }); ``` --- url: /guide/essentials/config/runtime.html title: Runtime Config --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/runtime.md for this page in Markdown format # 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 '#imports'; // Define types for your config declare module 'wxt/utils/define-app-config' { 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](/guide/essentials/config/environment-variables.html) ::: To access runtime config, WXT provides the `getAppConfig` function: ```ts import { getAppConfig } from '#imports'; console.log(getAppConfig()); // { 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/utils/define-app-config' { 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/vite.md for this page in Markdown format # 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/build-mode.md for this page in Markdown format # 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/typescript.md for this page in Markdown format # 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 /// ``` ## TSConfig Paths [​](#tsconfig-paths) WXT provides a default set of path aliases. | Alias | To | Example | | ----- | ------------ | ---------------------------------------------- | | \~\~ | /\* | import "\~\~/package.json" | | @@ | /\* | import "@@/package.json" | | \~ | /\* | 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'; ``` ## Custom Options [​](#custom-options) To specify custom compiler or top-level options, you have two options: 1. Override WXT's value completely by setting it in your root `tsconfig.json`: ```jsonc // /tsconfig.json { "extends": ".wxt/tsconfig.json", "compilerOptions": { "jsx": "preserve", }, } ``` 2. Merge custom values into values generated by WXT via the `prepare:tsconfig` hook: ```ts // wxt.config.ts export default defineConfig({ hooks: { 'prepare:tsconfig': (wxt, { tsconfig }) => { tsconfig.compilerOptions.lib.push('WebWorker'); }, }, }); ``` Overriding is great for simple boolean values, but not for complex options like `paths` or `libs`, where you can't just "add" a value to the object from your `tsconfig.json` file. So WXT provides the hook that let's you add or merge or delete or do whatever you want to the config file before it's written to the `.wxt/` directory. --- url: /guide/essentials/config/hooks.html title: Hooks --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/hooks.md for this page in Markdown format # 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.name += ' (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. You can find the [list of all available hooks](/api/reference/wxt/interfaces/WxtHooks.html) in the API reference. 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": ```plaintext βš™ 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/config/entrypoint-loaders.md for this page in Markdown format # 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!_** See [Entrypoint Limitations](/guide/essentials/extension-apis.html#entrypoint-limitations) for more details. :::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. --- url: /guide/essentials/extension-apis.html title: Extension APIs --- Are you an LLM? You can read better optimized documentation at /guide/essentials/extension-apis.md for this page in Markdown format # 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 merges these two into a unified API accessed through the `browser` variable. ```ts import { browser } from 'wxt/browser'; browser.action.onClicked.addListener(() => { // ... }); ``` :::tip With auto-imports enabled, you don't even need to import this variable from `wxt/browser`! ::: The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime: ```mjs export const browser = globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome; ``` This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc). ## Accessing Types [​](#accessing-types) All types can be accessed via WXT's `Browser` namespace: ```ts import { type Browser } from 'wxt/browser'; function handleMessage(message: any, sender: Browser.runtime.MessageSender) { // ... } ``` ## Using `webextension-polyfill` [​](#using-webextension-polyfill) If you want to use the `webextension-polyfill` when importing `browser`, you can do so by installing the `@wxt-dev/webextension-polyfill` package. See it's [Installation Guide](https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md) to get started. ## Feature Detection [​](#feature-detection) Depending on the manifest version, browser, and permissions, 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(() => { // }); ``` ## Add Firefox Types [​](#add-firefox-types) WXT's `browser` types are based on the `@types/chrome` package. That means some Firefox-specific APIs may not be typed, like `browser.sidebarAction`. If you want to add types for these APIs, you can augment the browser type by: 1. Installing `@wxt-dev/browser` as a direct dependency ```sh pnpm add @wxt-dev/browser ``` 2. Augmenting the `Browser` type: ```ts // /browser-types.d.ts import '@wxt-dev/browser'; import type { SidebarAction } from 'webextension-polyfill'; declare module '@wxt-dev/browser' { namespace Browser { export const sidebarAction: SidebarAction.Static; } } ``` You can add types from any source: `webextension-polyfill`, `@types/firefox-webext-browser`, or your own custom types. See TypeScript's documentation on [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) for more information. ## Entrypoint Limitations [​](#entrypoint-limitations) Because WXT imports your entrypoint files into a NodeJS, non-extension environment, the `chrome`/`browser` variables provided to extensions by the browser **will not be available**. To prevent some basic errors, WXT polyfills these globals with the same in-memory, fake implementation it uses for testing: [@webext-core/fake-browser](https://webext-core.aklinker1.io/fake-browser/installation/). However, not all the APIs have been implemented. So it is extremely important to NEVER use `browser.*` extension APIs outside the main function of any JS/TS entrypoints (background, content scripts, and unlisted scripts). If you do, you'll see an error like this: ```plaintext βœ– Command failed after 440 ms ERROR Browser.action.onClicked.addListener not implemented. ``` The fix is simple, just move your API usage into the entrypoint's main function: :::code-group ```ts [background.ts] browser.action.onClicked.addListener(() => { /* ... */ }); export default defineBackground(() => { browser.action.onClicked.addListener(() => { /* ... */ }); }); ``` ```ts [content.ts] browser.runtime.onMessage.addListener(() => { /* ... */ }); export default defineContentScript({ main() { browser.runtime.onMessage.addListener(() => { /* ... */ }); }, }); ``` ```ts [unlisted.ts] browser.runtime.onMessage.addListener(() => { /* ... */ }); export default defineUnlistedScript(() => { browser.runtime.onMessage.addListener(() => { /* ... */ }); }); ``` ::: Read [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders.html) for more technical details about this limitation. --- url: /guide/essentials/assets.html title: Assets --- Are you an LLM? You can read better optimized documentation at /guide/essentials/assets.md for this page in Markdown format # 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] ``` ::: :::warning Assets in the `public/` directory are **_not_** accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's [web\_accessible\_resources array](/api/reference/wxt/type-aliases/UserManifest.html#web-accessible-resources). ::: ## 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/target-different-browsers.md for this page in Markdown format # 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. ## Per-Browser Options [​](#per-browser-options) Some entrypoint options can be customized per build target by passing an object keyed by the [target browser](/guide/essentials/target-different-browsers.html#target-a-browser) instead of a single value. This is useful when different browsers need different match patterns, run timings, or other entrypoint behavior: ```ts export default defineContentScript({ matches: { chrome: ['*://chrome.example.com/*'], firefox: ['*://firefox.example.com/*'], }, runAt: { chrome: 'document_start', firefox: 'document_end', }, world: { firefox: 'MAIN', }, main(ctx) { // ... }, }); ``` --- url: /guide/essentials/content-scripts.html title: Content Scripts --- Are you an LLM? You can read better optimized documentation at /guide/essentials/content-scripts.md for this page in Markdown format # 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: ```plaintext 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 return 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/utils/content-script-ui/integrated/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/utils/content-script-ui/shadow-root/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 return mount(App, { target: container }); }, onRemove: (app) => { // 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/utils/content-script-ui/shadow-root/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) :::warning rem Units Are Not Fully Isolated WXT resets most inherited styles via `all: initial`. This doesn't reset the `` element's font size, which determines the relative size of `rem` units. If your CSS framework uses `rem` units, like Tailwind CSS, you may notice your UI's scale changing on different websites. See the [FAQ](/guide/resources/faq.html#my-content-script-ui-looks-different-on-certain-websites) for a fix. ::: ### 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/utils/content-script-ui/iframe/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/utils/content-script-ui/iframe/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-splitting 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/unit-testing.html title: Unit Testing --- Are you an LLM? You can read better optimized documentation at /guide/essentials/unit-testing.md for this page in Markdown format # Unit Testing [​](#unit-testing) * [Vitest](#vitest) * [Example Tests](#example-tests) * [Mocking WXT APIs](#mocking-wxt-apis) * [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/vitest-plugin'; 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) * 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/utils/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/fake-browser'; 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); }); }); ``` ### Mocking WXT APIs [​](#mocking-wxt-apis) First, you need to understand how the `#imports` module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path. For example, this is what your write in your source code: ```ts // What you write import { injectScript, createShadowRootUi } from '#imports'; ``` But Vitest sees this: ```ts import { injectScript } from 'wxt/utils/inject-script'; import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; ``` So in this case, if you wanted to mock `injectScript`, you need to pass in `"wxt/utils/inject-script"`, not `"#imports"`. ```ts vi.mock("wxt/utils/inject-script", () => ({ injectScript: ... })) ``` Refer to your project's `.wxt/types/imports-module.d.ts` file to lookup real import paths for `#imports`. If the file doesn't exist, run [wxt prepare](/guide/essentials/config/typescript.html). ## 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/e2e-testing.md for this page in Markdown format # 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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/publishing.md for this page in Markdown format # 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, or run `wxt submit --help` to view all available options. 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@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 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 install bun run 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: * [Packaging a web extension for Safari](https://developer.apple.com/documentation/safariservices/packaging-a-web-extension-for-safari) \- "Package your existing extension as a Safari web extension using Xcode’s command-line tool." When running the `safari-web-extension-packager` 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-packager .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 --- Are you an LLM? You can read better optimized documentation at /guide/essentials/testing-updates.md for this page in Markdown format # 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 --- Are you an LLM? You can read better optimized documentation at /guide/resources/compare.md for this page in Markdown format # 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 | βœ… | βœ… | ❌ | | 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 --- Are you an LLM? You can read better optimized documentation at /guide/resources/faq.md for this page in Markdown format # 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) * [My content script UI looks different on certain websites](#my-content-script-ui-looks-different-on-certain-websites) * [Does WXT provide docs for LLMs?](#does-wxt-provide-docs-for-llms) * [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) * [How do I run my WXT project with docker / devcontainers?](#how-do-i-run-my-wxt-project-with-docker-devcontainers) * [How do I use the new Prompt API in Chrome?](#how-do-i-use-the-new-prompt-api-in-chrome) ## 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 ` ...your app ``` ```html [After]
...your app
``` ::: Historically, CSS frameworks haven't had good support for shadow roots, so we needed a full `` structure for styles to be applied correctly. But that has change this past few years, frameworks have started to include the `host:` selector alongside `root:` (which is required for base styles to be applied to the shadow root's host element): ```css :root { :root, :host { /* base styles... */ } ``` If you use `createShadowRootUi`, see if your UI looks the same after this update without any changes. If the styles are broken, you can continue using the full `` structure by installing the v1 of `@webext-core/isolated-element` \- WXT will respect whatever version of the package is listed in your `package.json`. ```sh pnpm add @webext-core/isolated-element@^1 ``` In the next major version, support for `@webext-core/isolated-element` v1 will be dropped, so you have some time to migrate. ### Content & Unlisted Script `globalName` Now Defaults to `false` [​](#content-unlisted-script-globalname-now-defaults-to-false) Content scripts and unlisted scripts are built as an IIFE. Previously, WXT generated a global variable to hold the IIFE's return value for all scripts by default. Now, no global variable is generated by default, producing a smaller, anonymous IIFE that prevents variable collisions with the page. If you rely on the return value of your script, for example when injecting it with `browser.scripting.executeScript` and reading `InjectionResult.result`, set `globalName: true` to restore the old behavior: ```ts export default defineUnlistedScript({ globalName: true, main() { return 'some value'; }, }); ``` This applies per-entrypoint, to both `defineContentScript` and `defineUnlistedScript`. ### ESLint Auto-Import Config Now Detects Your Installed ESLint Version [​](#eslint-auto-import-config-now-detects-your-installed-eslint-version) Setting `imports.eslintrc.enabled: true` used to always assume you wanted the legacy ESLint 8 config format, regardless of which ESLint version you actually had installed. Now, `true` behaves the same as `'auto'`: WXT detects your installed ESLint version and generates the matching file automatically (ESLint 9+ β†’ `eslint-auto-imports.mjs`, ESLint ≀8 β†’ `eslintrc-auto-import.json`). * If you're on ESLint 9+ but never explicitly set `enabled: 9`, you'll now get the new flat-config file instead of the legacy one. * To force the old ESLint 8 format regardless of your installed version, set `enabled: 8` explicitly. See the [ESLint config docs](/guide/essentials/config/auto-imports.html#eslint) for the full set of options and how to wire up the generated file. ### Deprecated APIs Removed From v0.21 [​](#deprecated-apis-removed-from-v0-21) APIs deprecated in v0.20 have been removed: * `wxt.config.runner` / `ExtensionRunnerConfig` / `defineRunnerConfig` β†’ use `wxt.config.webExt` / `defineWebExtConfig` instead. * `dev.server.hostname` β†’ use `dev.server.host` instead. * The `wxt/testing` barrel export β†’ import from the specific submodule instead: ```ts import { fakeBrowser, WxtVitest } from 'wxt/testing'; import { fakeBrowser } from 'wxt/testing/fake-browser'; import { WxtVitest } from 'wxt/testing/vitest-plugin'; ``` * The `clean(root: string)` JS API overload β†’ Pass a root like this instead `clean({ root: './my-extension' })` ### CWS v2 API Support [​](#cws-v2-api-support) `publish-browser-extension` was upgraded to v5, which adds support for the new Chrome Web Store v2 API to `wxt submit`! Instead of using a refresh token for auth, it uses a service account, which is much easier to setup and never expires. It also supports a few other features: To setup your project to use v2, run `wxt submit init` and go through the setup process for the CWS, selecting v2 when prompted. v1 will stop working **15th October 2026**, so you have a few months to migrate. ### `@webext-core/fake-browser` v2 [​](#webext-core-fake-browser-v2) If you use `wxt/testing/fake-browser` (or `@webext-core/fake-browser` directly) in unit tests, note that its types now match `@wxt-dev/browser` instead of `webextension-polyfill`. This is the same change WXT's own `browser` went through in [v0.20](#webextension-polyfill-removed). Most notably, **mocking `browser.runtime.onMessage` listeners that return a promise no longer works**, use `sendResponse` instead: ```ts fakeBrowser.runtime.onMessage.addListener(async () => { return await someAsyncWork(); fakeBrowser.runtime.onMessage.addListener((_message, _sender, sendResponse) => { someAsyncWork().then(sendResponse); return true; }); ``` If you use use a messaging library, it will likely continue working as-is. ### New Deprecations in v0.20 [​](#new-deprecations-in-v0-20) Deprecated APIs will be removed in the next major release. * `useAppConfig` deprecated in favor of `getAppConfig` \- same function, just renamed. This avoids react linters mistaking it as a hook. ## v0.19.0 β†’ v0.20.0 [​](#v0-19-0-rarr-v0-20-0) v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0\. If all goes well, v1.0 will be released with no additional breaking changes. :::tip Read through all the changes once before updating your code. ::: ### `webextension-polyfill` Removed [​](#webextension-polyfill-removed) WXT's `browser` no longer uses the `webextension-polyfill`! :::details Why? See ::: To upgrade, you have two options: 1. **Stop using the polyfill** * If you're already using `extensionApi: "chrome"`, then you're not using the polyfill and there is nothing to change! * Otherwise there is only one change: `browser.runtime.onMessage` no longer supports using promises to return a response: ```ts browser.runtime.onMessage.addListener(async () => { const res = await someAsyncWork(); return res; browser.runtime.onMessage.addListener(async (_message, _sender, sendResponse) => { someAsyncWork().then((res) => { sendResponse(res); }); return true; }); ``` 2. **Continue using the polyfill** \- If you want to keep using the polyfill, you can! One less thing to worry about during this upgrade. * Install `webextension-polyfill` and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill): ```sh pnpm i webextension-polyfill @wxt-dev/webextension-polyfill ``` * Add the WXT module to your config: wxt.config.ts ```ts export default defineConfig({ modules: ['@wxt-dev/webextension-polyfill'], }); ``` The new `browser` object (and types) is backed by WXT's new package: [@wxt-dev/browser](https://www.npmjs.com/package/@wxt-dev/browser). This package continues WXT's mission of providing useful packages for the whole community. Just like [@wxt-dev/storage](https://www.npmjs.com/package/@wxt-dev/storage), [@wxt-dev/i18n](https://www.npmjs.com/package/@wxt-dev/i18n), [@wxt-dev/analytics](https://www.npmjs.com/package/@wxt-dev/analytics), it is designed to be easy to use in any web extension project, not just those using WXT, and provides a consistent API across all browsers and manifest versions. ### `extensionApi` Config Removed [​](#extensionapi-config-removed) The `extensionApi` config has been removed. Before, this config provided a way to opt into using the new `browser` object prior to v0.20.0. Remove it from your `wxt.config.ts` file if present: wxt.config.ts ```ts export default defineConfig({ extensionApi: 'chrome', }); ``` ### Extension API Type Changes [​](#extension-api-type-changes) With the new `browser` introduced in v0.20, how you access types has changed. WXT now provides types based on `@types/chrome` instead of `@types/webextension-polyfill`. These types are more up-to-date with MV3 APIs, contain less bugs, are better organized, and don't have any auto-generated names. To access types, use the new `Browser` namespace from `wxt/browser`: ```ts import type { Runtime } from 'wxt/browser'; import type { Browser } from 'wxt/browser'; function getMessageSenderUrl(sender: Runtime.MessageSender): string { function getMessageSenderUrl(sender: Browser.runtime.MessageSender): string { // ... } ``` > If you use auto-imports, `Browser` will be available without manually importing it. Not all type names will be the same as what `@types/webextension-polyfill` provides. You'll have to find the new type names by looking at the types of the `browser.*` API's you use. ### `public/` and `modules/` Directories Moved [​](#public-and-modules-directories-moved) The default location for the `public/` and `modules/` directories have changed to better align with standards set by other frameworks (Nuxt, Next, Astro, etc). Now, each path is relative to the project's **root directory**, not the src directory. * If you follow the default folder structure, you don't need to make any changes. * If you set a custom `srcDir`, you have two options: 1. Move the your `public/` and `modules/` directories to the project root: ```html πŸ“‚ {rootDir}/ πŸ“ modules/ πŸ“ public/ πŸ“‚ src/ πŸ“ components/ πŸ“ entrypoints/ πŸ“ modules/ πŸ“ public/ πŸ“ utils/ πŸ“„ app.config.ts πŸ“„ wxt.config.ts ``` 2. Keep the folders in the same place and update your project config: wxt.config.ts ```ts export default defineConfig({ srcDir: 'src', publicDir: 'src/public', modulesDir: 'src/modules', }); ``` ### Import Path Changes and `#imports` [​](#import-path-changes-and-imports) The APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to individual exports under the `wxt/utils/*` path. :::details Why? As WXT grows and more utilities are added, any helper with side-effects will not be tree-shaken out of your final bundle. This can cause problems because not every API used by these side-effects is available in every type of entrypoint. Some APIs can only be used in the background, sandboxed pages can't use any extension API, etc. This was leading to JS throwing errors in the top-level scope, preventing your code from running. Splitting each util into it's own module solves this problem, making sure you're only importing APIs and side-effects into entrypoints they can run in. ::: Refer to the updated [API Reference](/api/reference/) to see the list of new import paths. However, you don't need to memorize or learn the new import paths! v0.20 introduces a new virtual module, `#imports`, that abstracts all this away from developers. See the [blog post](/blog/2024-12-06-using-imports-module.html) for more details about how this module works. So to upgrade, just replace any imports from `wxt/storage`, `wxt/client`, and `wxt/sandbox` with an import to the new `#imports` module: ```ts import { storage } from 'wxt/storage'; import { defineContentScript } from 'wxt/sandbox'; import { ContentScriptContext, useAppConfig } from 'wxt/client'; import { storage } from '#imports'; import { defineContentScript } from '#imports'; import { ContentScriptContext, getAppConfig } from '#imports'; ``` You can combine the imports into a single import statement, but it's easier to just find/replace each statement. ```ts import { storage } from 'wxt/storage'; import { defineContentScript } from 'wxt/sandbox'; import { ContentScriptContext, useAppConfig } from 'wxt/client'; import { storage, defineContentScript, ContentScriptContext, getAppConfig, } from '#imports'; ``` :::tip Before types will work, you'll need to run `wxt prepare` after installing v0.20 to generate the new TypeScript declarations. ::: ### `createShadowRootUi` CSS Changes [​](#createshadowrootui-css-changes) WXT now resets styles inherited from the webpage (`visibility`, `color`, `font-size`, etc.) by setting `all: initial` inside the shadow root. :::warning This doesn't effect `rem` units. You should continue using `postcss-rem-to-px` or an equivalent library if the webpage sets the HTML element's `font-size`. ::: If you use `createShadowRootUi`: 1. Remove any manual CSS overrides that reset the style of specific websites. For example: entrypoints/reddit.content/style.css ```css body { /* Override Reddit's default "hidden" visibility on elements */ visibility: visible !important; } ``` 2. Double check that your UI looks the same as before. If you run into problems with the new behavior, you can disable it and continue using your current CSS: ```ts const ui = await createShadowRootUi({ inheritStyles: true, // ... }); ``` ### Default Output Directories Changed [​](#default-output-directories-changed) The default value for the [outDirTemplate](/api/reference/wxt/interfaces/InlineConfig.html#outdirtemplate) config has changed. Now, different build modes are output to different directories: * `--mode production` β†’ `.output/chrome-mv3`: Production builds are unchanged * `--mode development` β†’ `.output/chrome-mv3-dev`: Dev mode now has a `-dev` suffix so it doesn't overwrite production builds * `--mode custom` β†’ `.output/chrome-mv3-custom`: Other custom modes end with a `-[mode]` suffix To use the old behavior, writing all output to the same directory, set the `outDirTemplate` option: wxt.config.ts ```ts export default defineConfig({ outDirTemplate: '{{browser}}-mv{{manifestVersion}}', }); ``` :::warning If you've previously loaded the extension into your browser manually for development, you'll need to uninstall and re-install it from the new dev output directory. ::: ### Deprecated APIs Removed [​](#deprecated-apis-removed) * `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process. > This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps. * `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead: wxt.config.ts ```ts export default defineConfig({ transformManifest(manifest) { hooks: { 'build:manifestGenerated': (_, manifest) => { // ... }, }, }); ``` ### New Deprecations [​](#new-deprecations) #### `runner` APIs Renamed [​](#runner-apis-renamed) To improve consistency with the `web-ext.config.ts` filename, the "runner" API and config options have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version: 1. The `runner` option has been renamed to `webExt`: wxt.config.ts ```ts export default defineConfig({ runner: { webExt: { startUrls: ["https://wxt.dev"], }, }); ``` 2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`: web-ext.config.ts ```ts import { defineRunnerConfig } from 'wxt'; import { defineWebExtConfig } from 'wxt'; ``` 3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig` ```ts import type { ExtensionRunnerConfig } from 'wxt'; import type { WebExtConfig } from 'wxt'; ``` ## v0.18.5 β†’ v0.19.0 [​](#v0-18-5-rarr-v0-19-0) ### `vite-node` Entrypoint Loader [​](#vite-node-entrypoint-loader) The default entrypoint loader has changed to `vite-node`. If you use any NPM packages that depend on the `webextension-polyfill`, you need to add them to Vite's `ssr.noExternal` option: wxt.config.ts ```ts export default defineConfig({ vite: () => ({ ssr: { noExternal: ['@webext-core/messaging', '@webext-core/proxy-service'], }, }), }); ``` > [Read the full docs](/guide/essentials/config/entrypoint-loaders.html#vite-node) for more information. :::details This change enables: Importing variables and using them in the entrypoint options: entrypoints/content.ts ```ts import { GOOGLE_MATCHES } from '~/utils/constants' export default defineContentScript({ matches: [GOOGLE_MATCHES], main: () => ..., }) ``` Using Vite-specific APIs like `import.meta.glob` to define entrypoint options: entrypoints/content.ts ```ts const providers: Record = import.meta.glob('../providers/*', { eager: true, }); export default defineContentScript({ matches: Object.values(providers).flatMap( (provider) => provider.default.paths, ), async main() { console.log('Hello content.'); }, }); ``` Basically, you can now import and do things outside the `main` function of the entrypoint - you could not do that before. Still though, be careful. It is recommended to avoid running code outside the `main` function to keep your builds fast. ::: To continue using the old approach, add the following to your `wxt.config.ts` file: wxt.config.ts ```ts export default defineConfig({ entrypointLoader: 'jiti', }); ``` :::warning `entrypointLoader: "jiti"` is deprecated and will be removed in the next major version. ::: ### Drop CJS Support [​](#drop-cjs-support) WXT no longer ships with Common JS support. If you're using CJS, here's your migration steps: 1. Add ["type": "module"](https://nodejs.org/api/packages.html#type) to your `package.json`. 2. Change the file extension of any `.js` files that use CJS syntax to `.cjs`, or update them to use EMS syntax. Vite also provides steps for migrating to ESM. Check them out for more details: ## v0.18.0 β†’ v0.18.5 [​](#v0-18-0-rarr-v0-18-5) > When this version was released, it was not considered a breaking change... but it should have been. ### New `modules/` Directory [​](#new-modules-directory) WXT now recognizes the `modules/` directory as a folder containing [WXT modules](/guide/essentials/wxt-modules.html). If you already have `/modules` or `/Modules` directory, `wxt prepare` and other commands will fail. You have two options: 1. \[Recommended\] Keep your files where they are and tell WXT to look in a different folder: wxt.config.ts ```ts export default defineConfig({ modulesDir: 'wxt-modules', // defaults to "modules" }); ``` 2. Rename your `modules` directory to something else. ## v0.17.0 β†’ v0.18.0 [​](#v0-17-0-rarr-v0-18-0) ### Automatic MV3 `host_permissions` to MV2 `permissions` [​](#automatic-mv3-host-permissions-to-mv2-permissions) > Out of an abundance of caution, this change has been marked as a breaking change because permission generation is different. If you list `host_permissions` in your `wxt.config.ts`'s manifest and have released your extension, double check that your `permissions` and `host_permissions` have not changed for all browsers you target in your `.output/*/manifest.json` files. Permission changes can cause the extension to be disabled on update, and can cause a drop in users, so be sure to double check for differences compared to the previous manifest version. ## v0.16.0 β†’ v0.17.0 [​](#v0-16-0-rarr-v0-17-0) ### Storage - `defineItem` Requires `defaultValue` Option [​](#storage-defineitem-requires-defaultvalue-option) If you were using `defineItem` with versioning and no default value, you will need to add `defaultValue: null` to the options and update the first type parameter: ```ts const item = storage.defineItem("local:count", { const item = storage.defineItem("local:count", { defaultValue: null, version: ..., migrations: ..., }) ``` The `defaultValue` property is now required if passing in the second options argument. If you exclude the second options argument, it will default to being nullable, as before. ```ts const item: WxtStorageItem = storage.defineItem('local:count'); const value: number | null = await item.getValue(); ``` ### Storage - Fix Types In `watch` Callback [​](#storage-fix-types-in-watch-callback) > If you don't use TypeScript, this isn't a breaking change, this is just a type change. ```ts const item = storage.defineItem('local:count', { defaultValue: 0 }); item.watch((newValue: number | null, oldValue: number | null) => { item.watch((newValue: number, oldValue: number) => { // ... }); ``` ## v0.15.0 β†’ v0.16.0 [​](#v0-15-0-rarr-v0-16-0) ### Output Directory Structure Changed [​](#output-directory-structure-changed) JS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes. ```plaintext .output/ / chunks/ some-shared-chunk-.js popup-.js // [!code --] popup.html popup.html popup.js // [!code ++] ``` ## v0.14.0 β†’ v0.15.0 [​](#v0-14-0-rarr-v0-15-0) ### Renamed `zip.ignoredSources` to `zip.excludeSources` [​](#renamed-zip-ignoredsources-to-zip-excludesources) wxt.config.ts ```ts export default defineConfig({ zip: { ignoredSources: [ /*...*/ ], excludeSources: [ /*...*/ ], }, }); ``` ### Renamed Undocumented Constants [​](#renamed-undocumented-constants) Renamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: * `__BROWSER__` β†’ `import.meta.env.BROWSER` * `__COMMAND__` β†’ `import.meta.env.COMMAND` * `__MANIFEST_VERSION__` β†’ `import.meta.env.MANIFEST_VERSION` * `__IS_CHROME__` β†’ `import.meta.env.CHROME` * `__IS_FIREFOX__` β†’ `import.meta.env.FIREFOX` * `__IS_SAFARI__` β†’ `import.meta.env.SAFARI` * `__IS_EDGE__` β†’ `import.meta.env.EDGE` * `__IS_OPERA__` β†’ `import.meta.env.OPERA` ## v0.13.0 β†’ v0.14.0 [​](#v0-13-0-rarr-v0-14-0) ### Content Script UI API changes [​](#content-script-ui-api-changes) `createContentScriptUi` and `createContentScriptIframe`, and some of their options, have been renamed: * `createContentScriptUi({ ... })` β†’ `createShadowRootUi({ ... })` * `createContentScriptIframe({ ... })` β†’ `createIframeUi({ ... })` * `type: "inline" | "overlay" | "modal"` has been changed to `position: "inline" | "overlay" | "modal"` * `onRemove` is now called **_before_** the UI is removed from the DOM, previously it was called after the UI was removed * `mount` option has been renamed to `onMount`, to better match the related option, `onRemove`. ## v0.12.0 β†’ v0.13.0 [​](#v0-12-0-rarr-v0-13-0) ### New `wxt/storage` APIs [​](#new-wxt-storage-apis) `wxt/storage` no longer relies on [unstorage](https://www.npmjs.com/package/unstorage). Some `unstorage` APIs, like `prefixStorage`, have been removed, while others, like `snapshot`, are methods on the new `storage` object. Most of the standard usage remains the same. See and for more details ([#300](https://github.com/wxt-dev/wxt/pull/300)) ## v0.11.0 β†’ v0.12.0 [​](#v0-11-0-rarr-v0-12-0) ### API Exports Changed [​](#api-exports-changed) `defineContentScript` and `defineBackground` are now exported from `wxt/sandbox` instead of `wxt/client`. ([#284](https://github.com/wxt-dev/wxt/pull/284)) * If you use auto-imports, no changes are required. * If you have disabled auto-imports, you'll need to manually update your import statements: ```ts import { defineBackground, defineContentScript } from 'wxt/client'; import { defineBackground, defineContentScript } from 'wxt/sandbox'; ``` ## v0.10.0 β†’ v0.11.0 [​](#v0-10-0-rarr-v0-11-0) ### Vite 5 [​](#vite-5) You will need to update any other Vite plugins to a version that supports Vite 5. ## v0.9.0 β†’ v0.10.0 [​](#v0-9-0-rarr-v0-10-0) ### Extension Icon Discovery [​](#extension-icon-discovery) WXT no longer discovers icons other than `.png` files. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file. ## v0.8.0 β†’ v0.9.0 [​](#v0-8-0-rarr-v0-9-0) ### Removed `WebWorker` Types by Default [​](#removed-webworker-types-by-default) Removed ["WebWorker" types](https://www.typescriptlang.org/tsconfig/lib.html) from `.wxt/tsconfig.json`. These types are useful for MV3 projects using a service worker. To add them back to your project, add the following to your project's TSConfig: ```json { "extends": "./.wxt/tsconfig.json", "compilerOptions": { "lib": ["ESNext", "DOM", "WebWorker"] } } ``` ## v0.7.0 β†’ v0.8.0 [​](#v0-7-0-rarr-v0-8-0) ### `defineUnlistedScript` [​](#defineunlistedscript) Unlisted scripts must now `export default defineUnlistedScript(...)`. ### `BackgroundDefinition` Type [​](#backgrounddefinition-type) Rename `BackgroundScriptDefintition` to `BackgroundDefinition`. ## v0.6.0 β†’ v0.7.0 [​](#v0-6-0-rarr-v0-7-0) ### Content Script CSS Output Location Changed [​](#content-script-css-output-location-changed) Content script CSS used to be output to `assets/.css`, but is now `content-scripts/.css` to match the docs. ## v0.5.0 β†’ v0.6.0 [​](#v0-5-0-rarr-v0-6-0) ### Require a Function for `vite` Config [​](#require-a-function-for-vite-config) The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`. ## v0.4.0 β†’ v0.5.0 [​](#v0-4-0-rarr-v0-5-0) ### Revert Move Public Directory [​](#revert-move-public-directory) Change default `publicDir` to from `/public` to `/public`. ## v0.3.0 β†’ v0.4.0 [​](#v0-3-0-rarr-v0-4-0) ### Update Default Path Aliases [​](#update-default-path-aliases) Use relative path aliases inside `.wxt/tsconfig.json`. ## v0.2.0 β†’ v0.3.0 [​](#v0-2-0-rarr-v0-3-0) ### Move Public Directory [​](#move-public-directory) Change default `publicDir` to from `/public` to `/public`. ### Improve Type Safety [​](#improve-type-safety) Add type safety to `browser.runtime.getURL`. ## v0.1.0 β†’ v0.2.0 [​](#v0-1-0-rarr-v0-2-0) ### Rename `defineBackground` [​](#rename-definebackground) Rename `defineBackgroundScript` to `defineBackground`. --- url: /guide/resources/migrate.html title: Migrate to WXT --- Are you an LLM? You can read better optimized documentation at /guide/resources/migrate.md for this page in Markdown format # Migrate to WXT [​](#migrate-to-wxt) > If you have problems migrating to WXT, feel free to ask for help in GitHub by [starting a discussion](https://github.com/wxt-dev/wxt/discussions/new?category=q-a) or in [Discord](https://discord.gg/ZFsZqGery9)! ## Overview [​](#overview) Always start by generating a new vanilla project and merging it into your project one file at a time. ```sh cd path/to/your/project pnpm dlx wxt@latest init example-wxt --template vanilla ``` :::tip We recommend reviewing [project structure](/guide/essentials/project-structure.html) before you get started. You can customize directory names in `wxt.config.ts` to match your project's needs. ::: In general, you'll need to: Install `wxt` [Extend .wxt/tsconfig.json](/guide/essentials/config/typescript.html#typescript-configuration) in your project's `tsconfig.json` Update/create `package.json` scripts to use `wxt` (don't forget about `postinstall`) Move entrypoints into `entrypoints/` directory Move assets into either the `assets/` or `public/` directories Move `manifest.json` content into `wxt.config.ts` Convert custom import syntax to be compatible with Vite Add a default export to JS entrypoints (`defineBackground`, `defineContentScript`, or `defineUnlistedScript`) Use the `browser` global instead of `chrome` ⚠️ Compare final `manifest.json` files, making sure permissions and host permissions are unchanged :::warning If your extension is already live on the Chrome Web Store, use [Google's update testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool) to make sure no new permissions are being requested. ::: Every project is different, so there's no one-solution-fits-all to migrating your project. Just make sure `wxt dev` runs, `wxt build` results in a working extension, and the list of permissions in the `manifest.json` hasn't changed. If all that looks good, you've finished migrating your extension! ## Popular Tools/Frameworks [​](#popular-tools-frameworks) Here's specific steps for other popular frameworks/build tools. ### Plasmo [​](#plasmo) 1. Install `wxt` 2. Move entrypoints into `entrypoints/` directory * For JS entrypoints, merge the named exports used to configure your JS entrypoints into WXT's default export * For HTML entrypoints, you cannot use JSX/Vue/Svelte files directly, you need to create an HTML file and manually create and mount your app. Refer to the [React](https://github.com/wxt-dev/wxt/tree/main/templates/react/entrypoints/popup), [Vue](https://github.com/wxt-dev/wxt/tree/main/templates/vue/entrypoints/popup), and [Svelte](https://github.com/wxt-dev/wxt/tree/main/templates/svelte/src/entrypoints/popup) templates as an example. 3. Move public `assets/*` into the `public/` directory 4. If you use CSUI, migrate to WXT's `createContentScriptUi` 5. Convert Plasmo's custom import resolutions to Vite's 6. Importing remote code via a URL is not supported in WXT, see [this issue](https://github.com/wxt-dev/wxt/issues/2262) for alternatives 7. Replace your [Plasmo tags](https://docs.plasmo.com/framework/workflows/build#with-a-custom-tag) (`--tag`) with [WXT build modes](/guide/essentials/config/build-mode.html) (`--mode`) 8. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same. ### CRXJS [​](#crxjs) If you used CRXJS's vite plugin, it's a simple refactor! The main difference between CRXJS and WXT is how the tools decide which entrypoints to build. CRXJS looks at your `manifest` (and vite config for "unlisted" entries), while WXT looks at files in the `entrypoints` directory. To migrate: 1. Move all entrypoints into the `entrypoints` directory, refactoring to WXT's style (TS files have a default export). 2. Move [entrypoint specific options out of the manifest](/guide/essentials/entrypoints.html#defining-manifest-options) and into the entrypoint files themselves (like content script `matches` or `run_at`). 3. Move any other `manifest.json` options [into the wxt.config.ts file](/guide/essentials/config/manifest.html), like permissions. 4. For simplicity, you'll probably want to [disable auto-imports](/guide/essentials/config/auto-imports.html#disabling-auto-imports) at first (unless you were already using them via `unimport` or `unplugin-auto-imports`). If you like the feature, you can enable it later once you've finished the migration. 5. Update your `package.json` to include all of [WXT's suggested scripts (see step 4)](/guide/installation.html#from-scratch) 6. Specifically, make sure you add the `"postinstall": "wxt prepare"` script to your `package.json`. 7. Delete your `vite.config.ts` file. Move any plugins into the `wxt.config.ts` file. If you use a frontend framework, [install the relevant WXT module](/guide/essentials/frontend-frameworks.html). 8. Update your typescript project. [Extend WXT's generated config](/guide/essentials/config/typescript.html), and [add any path aliases to your wxt.config.ts file](/guide/essentials/config/typescript.html#tsconfig-paths). 9. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same. Here's an example migration: [GitHub Better Line Counts - CRXJS β†’ WXT](https://github.com/aklinker1/github-better-line-counts/commit/39d766d2ba86866efefc2e9004af554ee434e2a8) ### `vite-plugin-web-extension` [​](#vite-plugin-web-extension) Since you're already using Vite, it's a simple refactor. 1. Install `wxt` 2. Move and refactor your entrypoints to WXT's style (with a default export) 3. Update package.json scripts to use `wxt` 4. Add `"postinstall": "wxt prepare"` script 5. Move the `manifest.json` into `wxt.config.ts` 6. Move any custom settings from `vite.config.ts` into `wxt.config.ts`'s 7. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same. --- url: /guide/resources/how-wxt-works.html title: How WXT Works --- Are you an LLM? You can read better optimized documentation at /guide/resources/how-wxt-works.md for this page in Markdown format # How WXT Works [​](#how-wxt-works) :::warning 🚧 Under construction These docs will be coming soon! ::: --- url: /analytics.html title: WXT Analytics --- Are you an LLM? You can read better optimized documentation at /analytics.md for this page in Markdown format # WXT Analytics [​](#wxt-analytics) Report analytics events from your web extension extension. ## Supported Analytics Providers [​](#supported-analytics-providers) * [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol) * [Moderok](#moderok) * [PostHog](#posthog) * [Umami](#umami) ## Install With WXT [​](#install-with-wxt) 1. Install the NPM package: ```bash pnpm i @wxt-dev/analytics ``` 2. In your `wxt.config.ts`, add the WXT module: ```ts export default defineConfig({ modules: ['@wxt-dev/analytics/module'], }); ``` 3. In your `/app.config.ts`, add a provider: ```ts // /app.config.ts import { umami } from '@wxt-dev/analytics/providers/umami'; export default defineAppConfig({ analytics: { debug: true, providers: [ // ... ], }, }); ``` 4. Then use the `#analytics` module to report events: ```ts import { analytics } from '#analytics'; await analytics.track('some-event'); await analytics.page(); await analytics.identify('some-user-id'); analytics.autoTrack(document.body); ``` ## Install Without WXT [​](#install-without-wxt) 1. Install the NPM package: ```bash pnpm i @wxt-dev/analytics ``` 2. Create an `analytics` instance: ```ts // utils/analytics.ts import { createAnalytics } from '@wxt-dev/analytics'; export const analytics = createAnalytics({ providers: [ // ... ], }); ``` 3. Import your analytics module in the background to initialize the message listener: ```ts // background.ts import './utils/analytics'; ``` 4. Then use your `analytics` instance to report events: ```ts import { analytics } from './utils/analytics'; await analytics.track('some-event'); await analytics.page(); await analytics.identify('some-user-id'); analytics.autoTrack(document.body); ``` ## Providers [​](#providers) ### Google Analytics 4 (Measurement Protocol) [​](#google-analytics-4-measurement-protocol) The [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4) is an alternative to GTag for reporting events to Google Analytics for MV3 extensions. > [Why use the Measurement Protocol instead of GTag?](https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4#measurement-protocol) Follow [Google's documentation](https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4#setup-credentials) to obtain your credentials and put them in your `.env` file: ```dotenv WXT_GA_API_SECRET='...' ``` Then add the `googleAnalytics4` provider to your `/app.config.ts` file: ```ts import { googleAnalytics4 } from '@wxt-dev/analytics/providers/google-analytics-4'; export default defineAppConfig({ analytics: { providers: [ googleAnalytics4({ apiSecret: import.meta.env.WXT_GA_API_SECRET, measurementId: '...', }), ], }, }); ``` ### Moderok [​](#moderok) [Moderok](https://moderok.dev) is an analytics platform built specifically for browser extensions. It requires no `host_permissions`, works in Manifest V3 service workers, and collects only anonymous usage data. Sign up at [moderok.dev](https://moderok.dev) to get your app key, then save it to your `.env` file: ```dotenv WXT_MODEROK_APP_KEY='mk_...' ``` Then add the `moderok` provider to your `/app.config.ts` file: ```ts import { moderok } from '@wxt-dev/analytics/providers/moderok'; export default defineAppConfig({ analytics: { providers: [ moderok({ appKey: import.meta.env.WXT_MODEROK_APP_KEY, // Automatically track first open, install, update, and daily ping events (default: true) trackLifecycle: true, // Track when users uninstall the extension (default: false) trackUninstalls: false, // Optional: when trackUninstalls is on, redirect users to this page // after they uninstall (e.g. a feedback survey) uninstallUrl: 'https://example.com/uninstall', }), ], }, }); ``` For a full walkthrough β€” module setup, sending events, and all provider options β€” see the [Moderok WXT guide](https://docs.moderok.dev/guide/wxt). ### PostHog [​](#posthog) [PostHog](https://posthog.com/) is an open source product analytics platform. It supports event tracking, session recording, feature flags, surveys, and more. In your PostHog project settings, find your **Project API key** and save it to your `.env` file: ```dotenv WXT_POSTHOG_API_KEY='phc_...' ``` Then add the `posthog` provider to your `/app.config.ts` file: ```ts import { posthog } from '@wxt-dev/analytics/providers/posthog'; export default defineAppConfig({ analytics: { providers: [ posthog({ apiKey: import.meta.env.WXT_POSTHOG_API_KEY, // apiHost defaults to 'https://us.i.posthog.com'. // Change to 'https://eu.i.posthog.com' for EU Cloud, or your self-hosted URL. apiHost: 'https://eu.i.posthog.com', }), ], }, }); ``` ### Umami [​](#umami) [Umami](https://umami.is/) is a privacy-first, open source analytics platform. In Umami's dashboard, create a new website. The website's name and domain can be anything. Obviously, an extension doesn't have a domain, so make one up if you don't have one. After the website has been created, save the website ID and domain to your `.env` file: ```dotenv WXT_UMAMI_WEBSITE_ID='...' WXT_UMAMI_DOMAIN='...' ``` Then add the `umami` provider to your `/app.config.ts` file: ```ts import { umami } from '@wxt-dev/analytics/providers/umami'; export default defineAppConfig({ analytics: { providers: [ umami({ apiUrl: 'https:///api', websiteId: import.meta.env.WXT_UMAMI_WEBSITE_ID, domain: import.meta.env.WXT_UMAMI_DOMAIN, }), ], }, }); ``` ### Custom Provider [​](#custom-provider) If your analytics platform is not supported, you can provide an implementation of the `AnalyticsProvider` type in your `app.config.ts` instead: ```ts import { defineAnalyticsProvider } from '@wxt-dev/analytics/client'; interface CustomAnalyticsOptions { // ... } const customAnalytics = defineAnalyticsProvider( (analytics, analyticsConfig, providerOptions) => { // ... }, ); export default defineAppConfig({ analytics: { providers: [ customAnalytics({ // ... }), ], }, }); ``` Example `AnalyticsProvider` implementations can be found at [./modules/analytics/providers](https://github.com/wxt-dev/wxt/tree/main/packages/analytics/modules/analytics/providers). ## User Properties [​](#user-properties) User ID and properties are stored in `browser.storage.local`. To change this or customize where these values are stored, use the `userId` and `userProperties` config: ```ts // app.config.ts import { storage } from 'wxt/storage'; export default defineAppConfig({ analytics: { userId: storage.defineItem('local:custom-user-id-key'), userProperties: storage.defineItem('local:custom-user-properties-key'), }, }); ``` To set the values at runtime, use the `identify` function: ```ts await analytics.identify(userId, userProperties); ``` Alternatively, a common pattern is to use a random string as the user ID. This keeps the actual user information private, while still providing useful metrics in your analytics platform. This can be done very easily using WXT's storage API: ```ts // app.config.ts import { storage } from 'wxt/storage'; export default defineAppConfig({ analytics: { userId: storage.defineItem('local:custom-user-id-key', { init: () => crypto.randomUUID(), }), }, }); ``` If you aren't using `wxt` or `@wxt-dev/storage`, you can define custom implementations for the `userId` and `userProperties` config: ```ts const analytics = createAnalytics({ userId: { getValue: () => ..., setValue: (userId) => ..., } }) ``` ## Auto-track UI events [​](#auto-track-ui-events) Call `analytics.autoTrack(container)` to automatically track UI events so you don't have to manually add them. Currently it: * Tracks clicks to elements inside the `container` In your extension's HTML pages, you'll want to call it with `document`: ```ts analytics.autoTrack(document); ``` But in content scripts, you usually only care about interactions with your own UI: ```ts const ui = createIntegratedUi({ // ... onMount(container) { analytics.autoTrack(container); }, }); ui.mount(); ``` ## Enabling/Disabling [​](#enabling-disabling) By default, **analytics is disabled**. You can configure how the value is stored (and change the default value) via the `enabled` config: ```ts // app.config.ts import { storage } from 'wxt/storage'; export default defineAppConfig({ analytics: { enabled: storage.defineItem('local:analytics-enabled', { fallback: true, }), }, }); ``` At runtime, you can call `setEnabled` to change the value: ```ts analytics.setEnabled(true); ``` --- url: /auto-icons.html title: WXT Auto Icons --- Are you an LLM? You can read better optimized documentation at /auto-icons.md for this page in Markdown format # WXT Auto Icons [​](#wxt-auto-icons) [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/CHANGELOG.md) ## Features [​](#features) * Generate extension icons with the correct sizes * Make the icon greyscale or include a visible overlay during development * SVG is supported ## Usage [​](#usage) Install the package: ```sh npm i --save-dev @wxt-dev/auto-icons pnpm i -D @wxt-dev/auto-icons yarn add --dev @wxt-dev/auto-icons bun add -D @wxt-dev/auto-icons ``` Add the module to `wxt.config.ts`: ```ts export default defineConfig({ modules: ['@wxt-dev/auto-icons'], }); ``` And finally, save the base icon to `/assets/icon.png`. ## Configuration [​](#configuration) The module can be configured via the `autoIcons` config: ```ts export default defineConfig({ modules: ['@wxt-dev/auto-icons'], autoIcons: { // ... }, }); ``` Options have JSDocs available in your editor, or you can read them in the source code: [AutoIconsOptions](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/src/index.ts). --- url: /blog.html --- # Blog * [Introducing #importsLearn how WXT's new #imports module works and how to use it.Aaron Klinker β€’ December 6, 2024](/blog/2024-12-06-using-imports-module.html) --- url: /examples.html --- Are you an LLM? You can read better optimized documentation at /examples.md for this page in Markdown format # Examples Filter by APIs Filter by Permissions Filter by Packages No matching examples --- url: /i18n.html title: "@wxt-dev/i18n" --- Are you an LLM? You can read better optimized documentation at /i18n.md for this page in Markdown format # `@wxt-dev/i18n` [​](#wxt-dev-i18n) [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/i18n/CHANGELOG.md) `@wxt-dev/i18n` is a simple, type-safe wrapper around the `browser.i18n` APIs. It provides several benefits over the standard API: 1. Simple messages file format 2. Plural form support 3. Integrates with the [I18n Ally VS Code extension](#vs-code) It also provides several benefits over other non-web extension specific i18n packages: 1. Does not bundle localization files into every entrypoint 2. Don't need to fetch the localization files asynchronously 3. Can localize text in manifest and CSS files However, it does have one major downside: 1. Like the `browser.i18n` API, to change the language, users must change the browser's language :::unknown IMPORTANT You don't have to use `wxt` to use this package - it will work in any bundler setup. See [Installation without WXT](#without-wxt) for more details. ::: ## Installation [​](#installation) ### With WXT [​](#with-wxt) 1. Install `@wxt-dev/i18n` via your package manager: ```sh pnpm i @wxt-dev/i18n ``` 2. Add the WXT module to your `wxt.config.ts` file and setup a default locale: ```ts export default defineConfig({ modules: ['@wxt-dev/i18n/module'], manifest: { default_locale: 'en', }, }); ``` 3. Create a localization file at `/locales/.yml` or move your existing localization files there. ```yml # /locales/en.yml helloWorld: Hello world! ``` > `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `/public/_locale//messages.json`, you don't need to convert them to YAML or refactor them - just move them to `/locales/.json` and they'll just work out of the box! 4. To get a translation, use the auto-imported `i18n` object or import it manually: ```ts import { i18n } from '#i18n'; i18n.t('helloWorld'); // "Hello world!" ``` And you're done! Using WXT, you get type-safety out of the box. ### Without WXT [​](#without-wxt) 1. Install `@wxt-dev/i18n` via your package manager: ```sh pnpm i @wxt-dev/i18n ``` 2. Create a messages file at `_locales//messages.json` or move your existing translations there: ```json { "helloWorld": { "message": "Hello world!" } } ``` :::unknown NOTE For the best DX, you should to integrate `@wxt-dev/i18n` into your build process. This enables: 1. Plural forms 2. Simple messages file format 3. Type safety See [Build Integrations](#build-integrations) to set it up. ::: 3. Create the `i18n` object, export it, and use it wherever you want! ```ts import { createI18n } from '@wxt-dev/i18n'; export const i18n = createI18n(); i18n.t('helloWorld'); // "Hello world!"; ``` ## Configuration [​](#configuration) The module can be configured via the `i18n` config: ```ts export default defineConfig({ modules: ['@wxt-dev/i18n'], i18n: { // ... }, }); ``` Options have JSDocs available in your editor, or you can read them in the source code: [I18nOptions](https://github.com/wxt-dev/wxt/blob/main/packages/i18n/src/module.ts). ## Messages File Format [​](#messages-file-format) :::danger You can only use the file format discussed on this page if you have [integrated @wxt-dev/i18n into your build process](#build-integrations). If you have not integrated it into your build process, you must use JSON files in the `_locales` directory, like a normal web extension. ::: ### File Extensions [​](#file-extensions) You can define your messages in several different file types: * `.yml` * `.yaml` * `.json` * `.jsonc` * `.json5` * `.toml` ### Nested Keys [​](#nested-keys) You can have translations at the top level or nest them into groups: ```yml ok: OK cancel: Cancel welcome: title: Welcome to XYZ dialogs: confirmation: title: 'Are you sure?' ``` To access a nested key, use `.`: ```ts i18n.t('ok'); // "OK" i18n.t('cancel'); // "Cancel" i18n.t('welcome.title'); // "Welcome to XYZ" i18n.t('dialogs.confirmation.title'); // "Are you sure?" ``` ### Substitutions [​](#substitutions) Because `@wxt-dev/i18n` is based on `browser.i18n`, you define substitutions the same way, with `$1`\-`$9`: ```yml hello: Hello $1! order: Thanks for ordering your $1 ``` #### Escaping `$` [​](#escaping) To escape the dollar sign, put another `$` in front of it: ```yml dollars: $$$1 ``` ```ts i18n.t('dollars', ['1.00']); // "$1.00" ``` #### Named Substitutions [​](#named-substitutions) You can also use named substitutions with `{key}` placeholders: ```yml welcome: Hello {name}, welcome to {appName}! ``` ```ts i18n.t('welcome', { name: 'Ada', appName: 'WXT', }); // "Hello Ada, welcome to WXT!" ``` Missing keys are left unchanged in the returned message. ### Plural Forms [​](#plural-forms) :::warning Plural support languages like Arabic, that have different forms for "few" or "many", is not supported right now. Feel free to open a PR if you are interested in this! ::: To get a different translation based on a count: ```yml items: 1: 1 item n: $1 items ``` Then you pass in the count as the second argument to `i18n.t`: ```ts i18n.t('items', 0); // "0 items" i18n.t('items', 1); // "1 item" i18n.t('items', 2); // "2 items" ``` To add a custom translation for 0 items: ```yml items: 0: No items 1: 1 item n: $1 items ``` ```ts i18n.t('items', 0); // "No items" i18n.t('items', 1); // "1 item" i18n.t('items', 2); // "2 items" ``` If you need to pass a custom substitution for `$1` instead of the count, just add the substitution: ```yml items: 0: No items 1: $1 item n: $1 items ``` ```ts i18n.t('items', 0, ['Zero']); // "No items" i18n.t('items', 1, ['One']); // "One item" i18n.t('items', 2, ['Multiple']); // "Multiple items" ``` ### Verbose Keys [​](#verbose-keys) `@wxt-dev/i18n` is compatible with the message format used by [browser.i18n](https://developer.chrome.com/docs/extensions/reference/api/i18n). :::unknown IMPORTANT This means if you're migrating to `@wxt-dev/i18n` and you're already using the verbose format, you don't have to change anything! ::: A key is treated as "verbose" when it is: 1. At the top level (not nested) 2. Only contains the following properties: `message`, `description` and/or `placeholder` ```json { "appName": { "message": "GitHub - Better Line Counts", "description": "The app's name, should not be translated" }, "ok": "OK", "deleteConfirmation": { "title": "Delete XYZ?", "message": "You cannot undo this action once taken." } } ``` In this example, only `appName` is considered verbose. `deleteConfirmation` is not verbose because it contains the additional property `title`. ```ts i18n.t('appName'); // βœ… "GitHub - Better Line Counts" i18n.t('appName.message'); // ❌ i18n.t('ok'); // βœ… "OK" i18n.t('deleteConfirmation'); // ❌ i18n.t('deleteConfirmation.title'); // βœ… "Delete XYZ?" i18n.t('deleteConfirmation.message'); // βœ… "You cannot undo this action once taken." ``` If this is confusing, don't worry! With type-safety, you'll get a type error if you do it wrong. If type-safety is disabled, you'll get a runtime warning in the devtools console. :::warning Using the verbose format is not recommended. I have yet to see a translation service and software that supports this format out of the box. Stick with the simple format when you can. ::: ## Build Integrations [​](#build-integrations) To use the custom messages file format, you'll need to use `@wxt-dev/i18n/build` to transform the custom format to the one expected by browsers. ### WXT [​](#wxt) See [Installation with WXT](#with-wxt). But TLDR, all you need is: ```ts // wxt.config.ts export default defineConfig({ modules: ['@wxt-dev/i18n/module'], }); ``` Types are generated whenever you run `wxt prepare` or another build command. ### Custom [​](#custom) If you're not using WXT, you'll have to pre-process the localization files yourself. Here's a basic script to generate the `_locales/.../messages.json` and `wxt-i18n-structure.d.ts` files: ```ts // build-i18n.js import { parseMessagesFile, generateChromeMessagesFile, generateTypeFile, } from '@wxt-dev/i18n/build'; // Read your localization files const messages = { en: await parseMessagesFile('path/locales/en.yml'), de: await parseMessagesFile('path/locales/de.yml'), // ... }; // Generate JSON files for the extension await generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en); await generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de); // ... // Generate a types file based on your default_locale await generateTypeFile('wxt-i18n-structure.d.ts', messages.en); ``` Then run the script: ```sh node generate-i18n.js ``` If your build tool has a dev mode, you'll also want to rerun the script whenever you change a localization file. #### Type Safety [​](#type-safety) Once you've generated `wxt-i18n-structure.d.ts` (see the [Custom](#custom) section), you can use it to pass the generated type into `createI18n`: ```ts import type { WxtI18nStructure } from './wxt-i18n-structure'; export const i18n = createI18n(); ``` And just like that, you have type safety! ## Editor Support [​](#editor-support) For better DX, you can configure your editor with plugins and extensions. ### VS Code [​](#vs-code) The [I18n Ally Extension](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) adds several features to VS Code: * Go to translation definition * Inline previews of text * Hover to see other translations You'll need to configure it the extension so it knows where your localization files are and what function represents getting a translation: `.vscode/i18n-ally-custom-framework.yml`: ```yml # An array of strings which contain Language Ids defined by VS Code # You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers languageIds: - typescript - typescriptreact # Look for t("...") usageMatchRegex: - "[^\\w\\d]t\\(['\"`]({key})['\"`]" # Disable other built-in i18n ally frameworks monopoly: true ``` `.vscode/settings.json`: ```json { "i18n-ally.localesPaths": ["src/locales"], "i18n-ally.keystyle": "nested" } ``` ### Zed [​](#zed) As of time of writing, Aug 18, 2024, no extensions exist for Zed to add I18n support. ### Jetbrains IDEs [​](#jetbrains-ides) Install the [I18n Ally plugin](https://plugins.jetbrains.com/plugin/17212-i18n-ally). The docs are limited around their Jetbrains support, but you'll probably need to configure the plugin similar to [VS Code](#vs-code)... Not sure where the files go though. Please open a PR if you figure it out! --- url: /is-background.html title: "@wxt-dev/is-background" --- Are you an LLM? You can read better optimized documentation at /is-background.md for this page in Markdown format # `@wxt-dev/is-background` [​](#wxt-dev-is-background) Exports a getter to determine if the current JS context is the background or not. ## Installation [​](#installation) ```sh pnpm add @wxt-dev/is-background ``` ## Usage [​](#usage) ```ts import { isBackground } from '@wxt-dev/is-background'; isBackground(); // true | false ``` --- url: /runner.html title: "@wxt-dev/runner" --- Are you an LLM? You can read better optimized documentation at /runner.md for this page in Markdown format # `@wxt-dev/runner` [​](#wxt-dev-runner) Programmatically open a browser and install a web extension from a local directory. ## Usage [​](#usage) ### With WXT [​](#with-wxt) :::warning This package is intended to replace [web-ext](https://github.com/mozilla/web-ext) in the future, but it is not ready at the moment. Once it's ready for testing in WXT, more details will be added here. ::: ```ts // ~/wxt.runner.config.ts OR /wxt.runner.config.ts import { defineRunnerConfig } from 'wxt'; export default defineRunnerConfig({ // Options go here }); ``` ### JS API [​](#js-api) ```ts import { run } from '@wxt-dev/runner'; await run({ extensionDir: '/path/to/extension', // Other options... }); ``` ## Features [​](#features) * Supports all Chromium and Firefox based browsers * Zero dependencies * One-line config for persisting data between launches ## Requirements [​](#requirements) `@wxt-dev/runner` requires a JS runtime that implements the `WebSocket` standard: | JS Runtime | Version | | ---------- | -------- | | NodeJS | β‰₯ 22.4.0 | | Bun | β‰₯ 1.2.0 | You also need to have a specific version of the browser installed that supports the latest features so extensions can be loaded: | Browser | Version | | -------- | ------- | | Chromium | Unknown | | Firefox | β‰₯ 139 | ## TODO [​](#todo) * \[x\] Provide install functions to allow hooking into already running instances of Chrome/Firefox * \[ \] Try to setup E2E tests on Firefox with Puppeteer using this approach * \[ \] Try to setup E2E tests on Chrome with Puppeteer using this approach ## Options [​](#options) ### Target [​](#target) To open a specific browser, use the `target` option: ```ts import { run } from '@wxt-dev/runner'; await run({ extensionDir: 'path/to/extension', target: 'firefox', }); ``` Defaults to opening `chrome`. You may see type-hints for a list of popular browsers, but you can enter any string you want here. ### Data Persistence [​](#data-persistence) Browsers block you from using your normal browser profiles when using the [BiDi and CDP protocols](#implementation-details) for security reasons. To change how the new profile's data is saved between sessions, use the `dataPersistence` option: ```ts import { run } from '@wxt-dev/runner'; await run({ dataPersistence: 'user', }); ``` * `"none"` (default): Use a brand new browser profile every time the browser is opened (stored in the system's tmp directory) * `"project"`: Create a new profile that is re-used for your current directory (by default stored in `.wxt-runner` or `.wxt/runner` for WXT projects) * `"user"`: Create a new profile that is re-used for all projects using `@wxt-dev/runner` (by default stored in `$HOME/.wxt-runner`) These presets configure different flags for different operating systems when spawning the browser process. If you want to customize your data persistence beyond what these presets define, [you can override the browser flags yourself](#arguments) to configure persistence. ### Browser Binaries [​](#browser-binaries) `@wxt-dev/runner` will look for browser binaries/executables in [a hard-coded list of paths](https://github.com/wxt-dev/wxt/blob/main/packages/runner/src/browser-paths.ts). It does not and will not explore your filesystem/`$PATH` to find where the browser is installed. That means there are times you will need to specify the path to a browser's binary on your system: * Your browser's path is non-standard or missing from the hard-coded list. * You want to use a specific version/release of the browser. * You're using a less popular browser and `@wxt-dev/runner` doesn't have hard-coded paths for it. To do this, use the `browserBinaries` option and set the path to the browser's binary: ```ts import { run } from '@wxt-dev/runner'; await run({ extensionDir: 'path/to/extension', browserBinaries: { chrome: '/path/to/chrome', firefox: '/path/to/firefox', }, }); ``` ### Arguments [​](#arguments) To pass custom arguments to the browser on startup, use the `chromiumArgs` or `firefoxArgs` options: ```ts import { run } from '@wxt-dev/runner'; await run({ extensionDir: 'path/to/extension', chromiumArgs: ['--window-size=1920,1080'], firefoxArgs: ['--window-size', '1920,1080'], }); ``` ### Start URLs [​](#start-urls) To open specific URLs in tabs by default, you also use the `chromiumArgs` or `firefoxArgs` options. Any URLs passed as a CLI argument will be opened in the browser when it starts. ```ts import { run } from '@wxt-dev/runner'; await run({ extensionDir: 'path/to/extension', chromiumArgs: ['https://example.com'], firefoxArgs: ['https://example.com'], }); ``` ### Debugging [​](#debugging) To see debug logs, set the `DEBUG` env var to `"@wxt-dev/runner"`. This will print the resolved config, commands used to spawn the browser, any messages sent on the browser's communication protocol, and more for you to debug. Example debug output ```plaintext @wxt-dev/runner:options User options: { extensionDir: 'demo-extension', target: undefined } @wxt-dev/runner:options Resolved options: { browserBinary: '/usr/bin/chromium', chromiumArgs: [ '--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4', '--disable-component-extensions-with-background-pages', '--disable-background-networking', '--disable-component-update', '--disable-client-side-phishing-detection', '--disable-sync', '--metrics-recording-only', '--disable-default-apps', '--no-default-browser-check', '--no-first-run', '--disable-background-timer-throttling', '--disable-ipc-flooding-protection', '--password-store=basic', '--use-mock-keychain', '--force-fieldtrials=*BackgroundTracing/default/', '--disable-hang-monitor', '--disable-prompt-on-repost', '--disable-domain-reliability', '--propagate-iph-for-testing', '--remote-debugging-port=0', '--remote-debugging-pipe', '--user-data-dir=/tmp/wxt-runner-pWXLO1', '--enable-unsafe-extension-debugging' ], dataDir: '/tmp/wxt-runner-pWXLO1', dataPersistence: 'none', chromiumRemoteDebuggingPort: 0, extensionDir: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension', firefoxArgs: [ '--new-instance', '--no-remote', '--profile', '/tmp/wxt-runner-pWXLO1', '--remote-debugging-port=0', 'about:debugging#/runtime/this-firefox' ], firefoxRemoteDebuggingPort: 0, target: 'chrome' } @wxt-dev/runner:chrome:stderr DevTools listening on ws://127.0.0.1:38397/devtools/browser/93dc4de5-64cb-4e0b-a9d3-7549527015f0 @wxt-dev/runner:cdp Sending command: { id: 1, method: 'Extensions.loadUnpacked', params: { path: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension' } } @wxt-dev/runner:cdp Received response: { id: 1, result: { id: 'hckhakegfgenefhikdcfkaaonnclljmf' } } ``` ## Implementation Details [​](#implementation-details) All this package does is spawn a child process to open the browser with some default flags before using remote protocols to install the extension. ### Firefox [​](#firefox) We use the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi) to install the extension. This just involves connecting to a web socket and sending a few messages. ### Chrome [​](#chrome) We use the [CDP](https://chromedevtools.github.io/devtools-protocol/) with `--remote-debugging-pipe` and `--enable-unsafe-extension-debugging` to install the extension by sending a message via IO pipes 3 and 4. We don't use Webdriver Bidi because it's not built into Chrome yet. It requires us instantiating a separate child process for `chromedriver`. This is slower and more difficult than just using the CDP built into Chrome. --- url: /storage.html title: WXT Storage --- Are you an LLM? You can read better optimized documentation at /storage.md for this page in Markdown format # WXT Storage [​](#wxt-storage) [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) β€’ [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage.html) A simplified wrapper around the extension storage APIs. ## Installation [​](#installation) ### With WXT [​](#with-wxt) This module is built-in to WXT, so you don't need to install anything. ```ts import { storage } from '#imports'; ``` If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it! ### Without WXT [​](#without-wxt) Install the NPM package: ```sh npm i @wxt-dev/storage pnpm add @wxt-dev/storage yarn add @wxt-dev/storage bun add @wxt-dev/storage ``` ```ts import { storage } from '@wxt-dev/storage'; ``` ## Storage Permission [​](#storage-permission) To use the `@wxt-dev/storage` API, the `"storage"` permission must be added to the manifest: wxt.config.ts ```ts export default defineConfig({ manifest: { permissions: ['storage'], }, }); ``` ## Basic Usage [​](#basic-usage) All storage keys must be prefixed by their storage area. ```ts // ❌ This will throw an error await storage.getItem('installDate'); // βœ… This is good await storage.getItem('local:installDate'); ``` You can use `local:`, `session:`, `sync:`, or `managed:`. If you use TypeScript, you can add a type parameter to most methods to specify the expected type of the key's value: ```ts await storage.getItem('local:installDate'); await storage.watch( 'local:installDate', (newInstallDate, oldInstallDate) => { // ... }, ); await storage.getMeta<{ v: number }>('local:installDate'); ``` > This approach is fine for one-off storage fields or generic helpers, but [defining storage items](#defining-storage-items) is the recommended way to add type-safety. ## Watchers [​](#watchers) To listen for storage changes, use the `storage.watch` function. It lets you set up a listener for a single key: ```ts const unwatch = storage.watch('local:counter', (newCount, oldCount) => { console.log('Count changed:', { newCount, oldCount }); }); ``` To remove the listener, call the returned `unwatch` function: ```ts const unwatch = storage.watch(...); // Some time later... unwatch(); ``` ## Metadata [​](#metadata) `@wxt-dev/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc. [Other than versioning](#versioning), you are responsible for managing a field's metadata: ```ts await Promise.all([ storage.setItem('local:preference', true), storage.setMeta('local:preference', { lastModified: Date.now() }), ]); ``` When setting different properties of metadata from multiple calls, the properties are combined instead of overwritten: ```ts await storage.setMeta('local:preference', { lastModified: Date.now() }); await storage.setMeta('local:preference', { v: 2 }); await storage.getMeta('local:preference'); // { v: 2, lastModified: 1703690746007 } ``` You can remove all metadata associated with a key, or just specific properties: ```ts // Remove all properties await storage.removeMeta('local:preference'); // Remove only the "lastModified" property await storage.removeMeta('local:preference', 'lastModified'); // Remove multiple properties await storage.removeMeta('local:preference', ['lastModified', 'v']); ``` ## Defining Storage Items [​](#defining-storage-items) Writing the key and type parameter for the same key over and over again can be annoying. As an alternative, you can use `storage.defineItem` to create a "storage item". Storage items contain the same APIs as the `storage` variable, but you can configure its type, default value, and more in a single place: ```ts // utils/storage.ts const showChangelogOnUpdate = storage.defineItem( 'local:showChangelogOnUpdate', { fallback: true, }, ); ``` Now, instead of using the `storage` variable, you can use the storage item instead: ```ts await showChangelogOnUpdate.getValue(); await showChangelogOnUpdate.setValue(false); await showChangelogOnUpdate.removeValue(); const unwatch = showChangelogOnUpdate.watch((newValue) => { // ... }); ``` ### Versioning [​](#versioning) You can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1. For example, consider a storage item that stores a list of websites that are ignored by an extension. :::code-group ```ts [v1] type IgnoredWebsiteV1 = string; export const ignoredWebsites = storage.defineItem( 'local:ignoredWebsites', { fallback: [], version: 1, }, ); ``` ```ts [v2] import { nanoid } from 'nanoid'; type IgnoredWebsiteV1 = string; interface IgnoredWebsiteV2 { id: string; website: string; } export const ignoredWebsites = storage.defineItem( export const ignoredWebsites = storage.defineItem( 'local:ignoredWebsites', { fallback: [], version: 1, version: 2, migrations: { // Ran when migrating from v1 to v2 2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { return websites.map((website) => ({ id: nanoid(), website })); }, }, }, ); ``` ```ts [v3] import { nanoid } from 'nanoid'; type IgnoredWebsiteV1 = string; interface IgnoredWebsiteV2 { id: string; website: string; } interface IgnoredWebsiteV3 { id: string; website: string; enabled: boolean; } export const ignoredWebsites = storage.defineItem( export const ignoredWebsites = storage.defineItem( 'local:ignoredWebsites', { fallback: [], version: 2, version: 3, migrations: { // Ran when migrating from v1 to v2 2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { return websites.map((website) => ({ id: nanoid(), website })); }, // Ran when migrating from v2 to v3 3: (websites: IgnoredWebsiteV2[]): IgnoredWebsiteV3[] => { return websites.map((website) => ({ ...website, enabled: true })); }, }, }, ); ``` ::: :::info Internally, this uses a metadata property called `v` to track the value's current version. ::: In this case, we thought that the ignored website list might change in the future, and were able to set up a versioned storage item from the start. Realistically, you won't know an item needs versioning until you need to change its schema. Thankfully, it's simple to add versioning to an unversioned storage item. When a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work! Let's look at the same ignored websites example from before, but start with an unversioned item this time: :::code-group ```ts [Unversioned] export const ignoredWebsites = storage.defineItem( 'local:ignoredWebsites', { fallback: [], }, ); ``` ```ts [v2] import { nanoid } from 'nanoid'; // Retroactively add a type for the first version type IgnoredWebsiteV1 = string; interface IgnoredWebsiteV2 { id: string; website: string; } export const ignoredWebsites = storage.defineItem( export const ignoredWebsites = storage.defineItem( 'local:ignoredWebsites', { fallback: [], version: 2, migrations: { // Ran when migrating from v1 to v2 2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { return websites.map((website) => ({ id: nanoid(), website })); }, }, }, ); ``` ::: ### Running Migrations [​](#running-migrations) As soon as `storage.defineItem` is called, WXT checks if migrations need to be run, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc.) will automatically wait for the migration process to finish before actually reading or writing values. ### Default Values [​](#default-values) With `storage.defineItem`, there are multiple ways of defining default values: 1. **`fallback`** \- Return this value from `getValue` instead of `null` if the value is missing. This option is great for providing default values for settings: ```ts const theme = storage.defineItem('local:theme', { fallback: 'dark', }); const allowEditing = storage.defineItem('local:allow-editing', { fallback: true, }); ``` 2. **`init`** \- Initialize and save a value in storage if it is not already saved. This is great for values that need to be initialized or set once: ```ts const userId = storage.defineItem('local:user-id', { init: () => globalThis.crypto.randomUUID(), }); const installDate = storage.defineItem('local:install-date', { init: () => new Date().getTime(), }); ``` The value is initialized in storage immediately. ## Bulk Operations [​](#bulk-operations) When getting or setting multiple values in storage, you can perform bulk operations to improve performance by reducing the number of individual storage calls. The `storage` API provides several methods for performing bulk operations: * **`getItems`** \- Get multiple values at once. * **`getMetas`** \- Get metadata for multiple items at once. * **`setItems`** \- Set multiple values at once. * **`setMetas`** \- Set metadata for multiple items at once. * **`removeItems`** \- Remove multiple values (and optionally metadata) at once. All these APIs support both string keys and defined storage items: ```ts const userId = storage.defineItem('local:userId'); await storage.setItems([ { key: 'local:installDate', value: Date.now() }, { item: userId, value: generateUserId() }, ]); ``` Refer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage.html) for types and examples of how to use all the bulk APIs. --- url: /unocss.html title: WXT UnoCSS --- Are you an LLM? You can read better optimized documentation at /unocss.md for this page in Markdown format # WXT UnoCSS [​](#wxt-unocss) Use UnoCSS in your WXT extension! ## Usage [​](#usage) Install the package: ```sh npm i --save-dev @wxt-dev/unocss unocss pnpm i -D @wxt-dev/unocss unocss yarn add --dev @wxt-dev/unocss unocss bun add -D @wxt-dev/unocss unocss ``` Add the module to `wxt.config.ts`: ```ts export default defineConfig({ modules: ['@wxt-dev/unocss'], }); ``` Now in your entrypoint, import UnoCSS: ```ts import 'virtual:uno.css'; ``` :::unknown IMPORTANT While in dev mode, you may see a warning about `uno.css` not being found. This is because in development, we don't know which files should be injected with UnoCSS styles. The warning can be safely ignored as the styles will be properly applied during the build process. ::: ## Configuration [​](#configuration) The module can be configured via the `unocss` config: ```ts export default defineConfig({ modules: ['@wxt-dev/unocss'], unocss: { // Exclude unocss from running for the background excludeEntrypoints: ['background'], }, }); ``` Options have JSDocs available in your editor, or you can read them in the source code: [UnoCSSOptions](https://github.com/wxt-dev/wxt/blob/main/packages/unocss/src/index.ts).