BREAKING CHANGE: Switch from using `jiti` to import entrypoints during the build process by default to `vite-node`.
To continue using `jiti`, add the following to your `wxt.config.ts` file:
```ts
export default defineConfig({
entrypointLoader: "jiti",
})
```
NOTE: "jiti" is deprecated and will be removed in the next major version, `v0.20.0`.
BREAKING CHANGE: Out of an abundance of caution, I've marked this as a breaking change because permission generation has changed. ***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. 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.
BREAKING CHANGE: The build output has changed slightly. This isn't a huge deal, and no steps are required for users unless you're doing something weird with the output files after a build.
Before:
```
.output/
<target>/
chunks/
popup-<hash>.js
popup.html
```
After:
```
.output/
<target>/
popup.html
popup.js
```
This applies for all HTML files, not just the popup.
BREAKING CHANGES: Several APIs/options have been renamed:
- `type: "inline" | "overlay" | "modal"` has been changed to `position: "inline" | "overlay" | "modal"`
- `createContentScriptUi({ ... })` → `createShadowRootUi({ ... })`
- `createContentScriptIframe({ ... })` → `createIframeUi({ ... })`
- `onRemove` is now called ***before*** the UI is removed from the DOM, previously it was called after the UI was removed
BREAKING CHANGE: `wxt/storage` no longer relies on `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.
BREAKING CHANGE: `defineContentScript` and `defineBackground` are now exported from `wxt/sandbox` instead of `wxt/client`. If you use auto-imports, no changes are required. If you have disabled auto-imports, you'll need to manually update your import statements.
BREAKING CHANGE: No longer discover icons with extensions other than `.png`. 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.
BREAKING CHANGE: The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`.
This reverts commit 547fee0e0e.
BREAKING CHANGE: 547fee previously added auto imports from subdirectories, but Nuxt does not do this, so to be consistent with Nuxt's DX, I'm also removing these auto-imports.
Entrypoint outputs and files in the public directory are typed properly.
Chunks and other generated files are not typed since they aren't often accessed at runtime.
BREAKING CHANGE: `browser` is now imported from `wxt/browser` instead of `webextension-polyfill`
Thanks for taking the time to fill out this bug report!
- type:textarea
id:bug-description
attributes:
label:Describe the bug
description:A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder:I am doing ... What I expect is ... What actually happening is ...
validations:
required:true
- type:textarea
id:reproduction
attributes:
label:Reproduction
description:|
Please provide a minimal reproduction. This can include:
- A PR with a failing test case
- A link to a github repo
- A ZIP you upload to this issue
A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required ([Why?](https://antfu.me/posts/why-reproductions-are-required)). If a report is vague (e.g. just a generic error message) and has no reproduction or a partial reproduction, it will be closed immediately and labeled with as "needs-reproduction". Once a reproduction is provided, it will be re-opened.
placeholder:Reproduction URL or attach a ZIP
validations:
required:true
- type:textarea
id:reproduction-steps
attributes:
label:Steps to reproduce
description:Please provide any reproduction steps that may need to be described. E.g. if it happens only when running the dev or build script make sure it's clear which one to use.
placeholder:Run `npm install` followed by `npm run dev`
- type:textarea
id:system-info
attributes:
label:System Info
description:Output of `npx envinfo --system --browsers --binaries --npmPackages wxt,vite`
render:shell
placeholder:System, Binaries, Browsers
validations:
required:true
- type:dropdown
id:package-manager
attributes:
label:Used Package Manager
description:Select the used package manager
options:
- npm
- yarn
- pnpm
- bun
validations:
required:true
- type:checkboxes
id:checkboxes
attributes:
label:Validations
description:Before submitting the issue, please make sure you do the following
options:
- label:Read the [Contributing Guidelines](https://github.com/wxt-dev/wxt/blob/main/CONTRIBUTING.md).
required:true
- label:Read the [docs](https://wxt.dev/guide/installation.html).
required:true
- label:Check that there isn't [already an issue](https://github.com/wxt-dev/wxt/issues) that reports the same bug to avoid creating a duplicate.
required:true
- label:Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/wxt-dev/wxt/discussions) or join our [Discord Chat Server](https://discord.gg/ZFsZqGery9).
required:true
- label:The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.
If you are changing the docs or fixing a bug, feel free to fork and open a PR.
If you want to add a new feature, please create an issue or discussion first so we can decide if the feature is inline with the vision for WXT.
## Conventional Commits
This project uses [Conventional Commit format](https://www.conventionalcommits.org/en/v1.0.0/) to automatically generate a changelog and better understand the changes in the project
Here are some examples of conventional commit messages:
-`feat: add new functionality`
-`fix: correct typos in code`
-`ci: add GitHub Actions for automated testing`
## Conventional PR Titles
The title of your pull request should follow the [conventional commit format](#conventional-commits). When a pull request is merged to the main branch, all changes are going to be squashed into a single commit. The message of this commit will be the title of the pull request. And for every release, the commit messages are used to generate the changelog.
## Setup
WXT uses `pnpm`, so make sure you have it installed.
```sh
corepack enable
```
Then, simply run the install command:
```sh
pnpm i
```
## Development
Here are some helpful commands:
```sh
# Build WXT package
cd packages/wxt
pnpm build
```
```sh
# Build WXT package, then build demo extension
cd packages/wxt-demo
pnpm build
```
```sh
# Build WXT package, then start the demo extension in dev mode
cd packages/wxt-demo
pnpm dev
```
```sh
# Run unit and E2E tests
pnpm test
```
```sh
# Start the docs website locally
pnpm docs:dev
```
## Profiling
```sh
# Build the latest version
pnpm --filter wxt build
# CD to the demo directory
cd packages/wxt-demo
# 1. Generate a flamechart with 0x
pnpm dlx 0x node_modules/wxt/bin/wxt.mjs build
# 2. Inspect the process with chrome @ chrome://inspect
Documentation is written with VitePress, and is located in the `docs/` directory.
The API reference is generated from JSDoc comments in the source code. If there's a typo or change you want to make in there, you'll need to update the source code instead of a file in the `docs/` directory.
## Testing
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones, if they exist.
> If they don't exist, feel free to create them, but that's a lot for a one-time contributor. A maintainer might add them to your PR though.
To run tests for a specific file, add the filename at the end of the test command:
```sh
pnpm test manifest-contents
```
All test (unit and E2E) for all packages are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR.
## Templates
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands
```sh
cd templates/vue
npm i
npm run dev
npm run build
```
Note that templates are hardcoded to a specific version of `wxt` from NPM, they do not use the local version. PR checks will test your PR's changes against the templates, but if you want to manually do it, update the package.json dependency:
```diff
"devDependencies": {
"typescript": "^5.3.2",
"vite-plugin-solid": "^2.7.0",
- "wxt": "^0.8.0"
+ "wxt": "../.."
}
```
Then run `npm i` again.
### Adding Templates
To add a template, copy the vanilla template and give it a new name.
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
## Releasing Updates
Releases are done with GitHub actions:
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
## Upgrading Dependencies
Use [`taze`](https://www.npmjs.com/package/taze) to upgrade dependencies throughout the entire monorepo.
```sh
pnpm dlx taze -r
```
Configuration is in [`taze.config.ts`](./taze.config.ts).
## Install Unreleased Versions
This repo uses https://pkg.pr.new to publish versions of all it's packages for almost every commit. You can install them via:
```sh
npm i https://pkg.pr.new/[package-name]@[ref]
```
Or use one of the shorthands:
```sh
# Install the latest build of `wxt` from a PR:
npm i https://pkg.pr.new/wxt@1283
# Install the latest build of `@wxt-dev/module-react` on the `main` branch
npm i https://pkg.pr.new/@wxt-dev/module-react@main
# Install `@wxt-dev/storage` from a specific commit:
npm i https://pkg.pr.new/@wxt-dev/module-react@426f907
```
## Blog Posts
Anyone is welcome to submit a blog post on https://wxt.dev/blog!
> [!NOTE]
> Before starting on a blog post, please message Aaron on Discord or start a discussion on GitHub to get permission to write about a topic, but most topics are welcome: Major version updates, tutorials, etc.
- **English only**: Blog posts should be written in English. Unfortunately, our maintainers doesn't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓
- **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that.
<p align="center"><i>Next gen framework for developing web extensions.<br/>Powered by <a href="https://vitejs.dev/" target="_blank">Vite</a>. Inspired by <a href="https://nuxt.com/" target="_blank">Nuxt</a>.</i></p>
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
## Features
- 🌐 Supports all browsers
- ✅ Supports both MV2 and MV3
-📂 Directory based entrypoints
-⚡ Dev mode with HMR & fast reload
- 📂 File based entrypoints
- 🚔 TypeScript
- 🦾 Auto-imports
- ⚡ Dev mode with HMR
- ⬇️ Download and bundle remote URL imports
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
### Todo
- 🤖 Automated publishing
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
- 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
-⚡ Dev mode with HMR and **_auto-reload_**
-⬇️ Download and bundle remote URL imports
## Get Started
## Sponsors
Checkout the [installation guide](https://wxtjs.dev/get-started) to get started with WXT.
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 provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
constogTitle=`${title}${titleSuffix}`;
constogUrl=origin;
constogImage=`${origin}/social-preview.png`;
constotherPackages={
analytics: analyticsVersion,
'auto-icons':autoIconsVersion,
i18n: i18nVersion,
storage: storageVersion,
unocss: unocssVersion,
};
// https://vitepress.dev/reference/site-config
exportdefaultdefineConfig({
extends:knowledge({
serverUrl:'https://knowledge.wxt.dev',
paths:{
'/':'docs',
'/api/':'api-reference',
},
pageSelectors:{
'examples.md':'#VPContent > .VPPage',
},
}),
titleTemplate:`:title${titleSuffix}`,
title:'WXT',
description,
vite:{
clearScreen: false,
plugins:[
groupIconVitePlugin({
customIcon:{
'wxt.config.ts':localIconLoader(
import.meta.url,
'../public/logo.svg',
),
},
}),
],
},
lastUpdated: true,
sitemap:{
hostname: origin,
},
asyncbuildEnd(site){
// Only construct the RSS document for production builds
The extension messaging APIs are difficult to learn. Let's go beyond the simple examples from Chrome and Firefox's documentation to build our own simple messaging system from scratch.
description: Learn how WXT's new <code>#imports</code> module works and how to use it.
authors:
- name: Aaron Klinker
github: aklinker1
date: 2024-12-06T14:39:00.000Z
---
WXT v0.20 introduced a new way of manually importing its APIs: **the `#imports` module**. This module was introduced to simplify import statements and provide more visibility into all the APIs WXT provides.
The `#imports` module is considered a "virtual module", because the file doesn't actually exist. At build-time, imports are split into individual statements for each API:
:::code-group
```ts [What you write]
import { defineContentScript, injectScript } from '#imports';
```
```ts [What the bundler sees]
import { defineContentScript } from 'wxt/utils/define-content-script';
import { injectScript } from 'wxt/utils/inject-script';
```
:::
Think of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size.
This enables better tree-shaking compared to v0.19 and below.
:::tip Need to lookup the full import path of an API?
Open up your project's `.wxt/types/imports-module.d.ts` file.
:::
## Mocking
When writing tests, you might need to mock APIs from the `#imports` module. While mocking these APIs is very easy, it may not be immediately clear how to accomplish it.
Let's look at an example using Vitest. When [configured with `wxt/testing`](/guide/essentials/unit-testing#vitest), Vitest sees the same transformed code as the bundler. That means to mock an API from `#imports`, you need to call `vi.mock` with the real import path, not `#imports`:
```ts
import { injectScript } from '#imports';
import { vi } from 'vitest';
vi.mock('wxt/utils/inject-script')
const injectScriptMock = vi.mocked(injectScript);
injectScriptMock.mockReturnValueOnce(...);
```
## Conclusion
You don't have to use `#imports` if you don't like - you can continue importing APIs from their submodules. However, using `#imports` is the recommended approach moving forwards.
- As more APIs are added, you won't have to memorize additional import paths.
- If breaking changes are made to import paths in future major versions, `#imports` won't break.
Happy Coding 😄
> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport).
Any assets imported or referenced inside the `<srcDir>/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]
<!-- In HTML tags, you must use the relative path --->
<img src="../assets/image.png" />
```
```css [CSS]
.bg-image {
background-image: url(~/assets/image.png);
}
```
```vue [Vue]
<script>
import image from '~/assets/image.png';
</script>
<template>
<img :src="image" />
</template>
```
```jsx [JSX]
import image from '~/assets/image.png';
<img src={image} />;
```
:::
## `/public` Directory
Files inside `<rootDir>/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]
<img src="/image.png" />
```
```css [CSS]
.bg-image {
background-image: url(/image.png);
}
```
```vue [Vue]
<template>
<img src="/image.png" />
</template>
```
```jsx [JSX]
<img src="/image.png" />
```
:::
:::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#web-accessible-resources).
:::
## 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 `<img>`'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`:
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):
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`:
```ts [wxt.config.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:
```ts [entrypoints/content.ts]
import initWasm, { parseSync } from '@oxc-parser/wasm';
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!
WXT uses [`unimport`](https://www.npmjs.com/package/unimport), the same tool as Nuxt, to setup auto-imports.
```ts
exportdefaultdefineConfig({
// 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:
-`<srcDir>/components/*`
-`<srcDir>/composables/*`
-`<srcDir>/hooks/*`
-`<srcDir>/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) and look at your project's `.wxt/types/imports-module.d.ts` file.
## TypeScript
For TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare).
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",// [!code ++]
},
}
```
## 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
Not all developers like auto-imports. To disable them, set `imports` to `false`.
```ts
export default defineConfig({
imports: false, // [!code ++]
});
```
## 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).
If you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place.
> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
## Config Files
You can configure browser startup in 3 places:
1.`<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
```ts
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
// ...
});
```
2. `<rootDir>/wxt.config.ts`: Via the [`runner` config](/api/reference/wxt/interfaces/InlineConfig#runner), included in version control
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
## Recipes
### Set Browser Binaries
To set or customize the browser opened during development:
```ts
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"
},
});
```
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
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.
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
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
To generate the manifest and other files at build-time, WXT must import each entrypoint to get their options, like content script `matches`. For HTML files, this is easy. For JS/TS entrypoints, the process is more complicated.
When loading your JS/TS entrypoints, they are imported into a NodeJS environment, not the `browser` environment that they normally run in. This can lead to issues commonly seen when running browser-only code in a NodeJS environment, like missing global variables.
WXT does several pre-processing steps to try and prevent errors during this process:
1. Use `linkedom` to make a small set of browser globals (`window`, `document`, etc) available.
2. Use `@webext-core/fake-browser` to create a fake version of the `chrome` and `browser` globals expected by extensions.
3. Pre-process the JS/TS code, stripping out the `main` function then tree-shaking unused code from the file
However, this process is not perfect. It doesn't setup all the globals found in the browser and the APIs may behave differently. As such, **_you should avoid using browser or extension APIs outside the `main` function of your entrypoints!_**
:::tip
If you're running into errors while importing entrypoints, run `wxt prepare --debug` to see more details about this process. When debugging, WXT will print out the pre-processed code to help you identify issues.
:::
Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.
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
WXT provides some custom environment variables based on the current command:
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.
WXT includes a system that lets you hook into the build process and make changes.
## 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:
```ts [wxt.config.ts]
export default defineConfig({
hooks: {
'build:manifestGenerated': (wxt, manifest) => {
if (wxt.config.mode === 'development') {
manifest.title += ' (DEV)';
}
},
},
});
```
Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.
Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules) instead.
## Execution Order
Because hooks can be defined in multiple places, including [WXT Modules](/guide/essentials/wxt-modules), 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#modules)
2. User modules in [`/modules` folder](/guide/essentials/project-structure), loaded alphabetically
3. Hooks listed in your `wxt.config.ts`
To see the order for your project, run `wxt prepare --debug` flag and search for the "Hook execution order":
```
⚙ Hook execution order:
⚙ 1. wxt:built-in:unimport
⚙ 2. src/modules/auto-icons.ts
⚙ 3. src/modules/example.ts
⚙ 4. src/modules/i18n.ts
⚙ 5. wxt.config.ts > hooks
```
Changing execution order is simple:
- Prefix your user modules with a number (lower numbers are loaded first):
<!-- prettier-ignore -->
```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!
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.
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.
> See [I18n docs](/guide/essentials/i18n) for a full guide on internationalizing your extension.
## Actions
In MV2, you have two options: [`browser_action`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action) and [`page_action`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action). 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
To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints#popup). If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head:
To add your own, DO NOT add them to your `tsconfig.json`! Instead, use the [`alias` option](/api/reference/wxt/interfaces/InlineConfig#alias) in `wxt.config.ts`.
This will add your custom aliases to `<rootDir>/.wxt/tsconfig.json` next time you run `wxt prepare`. It also adds your alias to the bundler so it can resolve imports.
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
You can change Vite's config via the `wxt.config.ts` file:
```ts [wxt.config.ts]
import { defineConfig } from 'wxt';
export default defineConfig({
vite: () => ({
// Override config here, same as `defineConfig({ ... })`
// inside vite.config.ts files
}),
});
```
## Add Vite Plugins
To add a plugin, install the NPM package and add it to the Vite config:
```ts [wxt.config.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:
```ts [wxt.config.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).
> To create a content script, see [Entrypoint Types](/guide/essentials/entrypoints#content-scripts).
## Context
The first argument to a content script's `main` function is its "context".
```ts
// entrypoints/example.content.ts
export default defineContentScript({
main(ctx) {},
});
```
This object is responsible for tracking whether or not the content script's context is "invalidated". Most browsers, by default, do not stop content scripts if the extension is uninstalled, updated, or disabled. When this happens, content scripts start reporting this error:
```
Error: Extension context invalidated.
```
The `ctx` object provides several helpers to stop asynchronous code from running once the context is invalidated:
```ts
ctx.addEventListener(...);
ctx.setTimeout(...);
ctx.setInterval(...);
ctx.requestAnimationFrame(...);
// and more
```
You can also check if the context is invalidated manually:
```ts
if (ctx.isValid) {
// do something
}
// OR
if (ctx.isInvalid) {
// do something
}
```
## CSS
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:
```ts [wxt.config.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
WXT provides 3 built-in utilities for adding UIs to a page from a content script:
See the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options.
### 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) 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#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: ['<all_urls>'],
// 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: ['<all_urls>'],
// 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: ['<all_urls>'],
// 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
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), which simplifies setting up the IFrame.
1. Create an HTML page that will be loaded into your IFrame:
1. Add the page to the manifest's `web_accessible_resources`:
```ts [wxt.config.ts]
export default defineConfig({
manifest: {
web_accessible_resources: [
{
resources: ['example-iframe.html'],
matches: [...],
},
],
},
});
```
1. Create and mount the IFrame:
```ts
export default defineContentScript({
matches: ['<all_urls>'],
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) for the complete list of options.
## 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:
<!-- prettier-ignore -->
```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 `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
:::
## Mounting UI to dynamic element
In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
```ts
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
// It observes the anchor
anchor: '#your-target-dynamic-element',
onMount: (container) => {
// Append children to the container
const app = document.createElement('p');
app.textContent = '...';
container.append(app);
},
});
// Call autoMount to observe anchor element for add/remove.
ui.autoMount();
},
});
```
:::tip
When the `ui.remove` is called, `autoMount` also stops.
:::
See the [API Reference](/api/reference/wxt/utils/content-script-ui/types/interfaces/ContentScriptUi#automount) for the complete list of options.
## Dealing with SPAs
It is difficult to write content scripts for SPAs (single page applications) and websites using HTML5 history mode for navigation because content scripts are only ran on full page reloads. SPAs and websites that take advantage of HTML5 history mode **_do not perform a full reload when changing paths_**, and thus your content script isn't going to be ran when you expect it to be.
Let's look at an example. Say you want to add a UI to YouTube when watching a video:
```ts
export default defineContentScript({
matches: ['*://*.youtube.com/watch*'],
main(ctx) {
console.log('YouTube content script loaded');
mountUi(ctx);
},
});
function mountUi(ctx: ContentScriptContext): void {
// ...
}
```
You're only going to see "YouTube content script loaded" when reloading the watch page or when navigating directly to it from another website.
To get around this, you'll need to manually listen for the path to change and run your content script when the URL matches what you expect it to match.
```ts
const watchPattern = new MatchPattern('*://*.youtube.com/watch*');
[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).
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (Pug, TS, JSX, SCSS, etc).
Here's an example set of entrypoints:
<!-- prettier-ignore -->
```html
📂 entrypoints/
📂 popup/
📄 index.html
📄 main.ts
📄 style.css
📄 background.ts
📄 content.ts
```
[[toc]]
## Listed vs Unlisted
For web extensions, there are two types of entrypoints:
- **Listed**: Referenced in the `manifest.json`
- **Unlisted**: Not referenced in the `manifest.json`
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
- Popup
- Options
- Background
- Content Scripts
- Etc.
Some examples of "unlisted" entrypoints:
- A welcome page shown when the extension is installed
- JS files injected by content scripts into the page's main world
:::tip
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
:::
## Adding Entrypoints
An entrypoint can be defined as a single file or directory with an `index` file inside it.
:::code-group
<!-- prettier-ignore -->
```html [Single File]
📂 entrypoints/
📄 background.ts
```
<!-- prettier-ignore -->
```html [Directory]
📂 entrypoints/
📂 background/
📄 index.ts
```
:::
The entrypoint's name dictates the type of entrypoint, listed vs unlisted. In this example, "background" is the name of the ["Background" entrypoint](#background).
Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns.
## Defining Manifest Options
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:
```ts [entrypoints/content.ts]
export default defineContentScript({
matches: ['*://*.wxt.dev/*'],
main() {
// ...
},
});
```
For HTML entrypoints, options are configured as `<meta>` tags. For example, to use a `page_action` for your MV2 popup:
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).
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', // [!code ++]
main() {
// ...
},
});
```
This will change the output format to ESM, enable code-spliting between your background script and HTML pages, and set `"type": "module"` in your manifest.
:::warning
Only MV3 supports ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
:::
## Content Scripts
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.
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:
<<< @/../packages/browser/src/index.mjs#snippet
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
All types can be accessed via WXT's `Browser` namespace:
```ts
import { Browser } from 'wxt/browser';
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
// ...
}
```
## 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
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_chaining) 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:
Install the module for your framework, then add it to your config:
:::code-group
```ts [React]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-react'],
});
```
```ts [Vue]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-vue'],
});
```
```ts [Svelte]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-svelte'],
});
```
```ts [Solid]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-solid'],
});
```
:::
## Adding Vite Plugins
If your framework doesn't have an official WXT module, no worries! WXT supports any framework with a Vite plugin.
Just add the Vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts and it will just work 👍
```ts
import { defineConfig } from 'wxt';
import react from '@vitejs/plugin-react';
export default defineConfig({
vite: () => ({
plugins: [react()],
}),
});
```
> The WXT modules just simplify the configuration and add auto-imports. They're not much different than the above.
## Multiple Apps
Since web extensions usually contain multiple UIs across multiple entrypoints (popup, options, changelog, side panel, content scripts, etc), you'll need to create individual app instances, one per entrypoint.
Usually, this means each entrypoint should be a directory with it's own files inside it. Here's the recommended folder structure:
<!-- prettier-ignore -->
```html
📂 {srcDir}/
📂 assets/ <---------- Put shared assets here
📄 tailwind.css
📂 components/
📄 Button.tsx
📂 entrypoints/
📂 options/ <--------- Use a folder with an index.html file in it
📁 pages/ <--------- A good place to put your router pages if you have them
📄 index.html
📄 App.tsx
📄 main.tsx <--------- Create and mount your app here
📄 style.css <--------- Entrypoint-specific styles
📄 router.ts
```
## Configuring Routers
All frameworks come with routers for building a multi-page app using the URL's path... But web extensions don't work like this. Since HTML files are static, `chrome-extension://{id}/popup.html`, there's no way to change the entire path for routing.
Instead, you need to configure the router to run in "hash" mode, where the routing information is a part of the URL's hash, not the path (ie: `popup.html#/` and `popup.html#/account/settings`).
Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
This page discusses how to setup internationalization using the vanilla `browser.i18n` APIs and mentions some alternatives if you want to use something else.
[[toc]]
## Usage
1. Add `default_locale` to your manifest:
```ts
export default defineConfig({
manifest: {
default_locale: 'en',
},
});
```
2. Create `messages.json` files in the `public/` directory:
<!-- prettier-ignore -->
```html
📂 {srcDir}/
📂 public/
📂 _locales/
📂 en/
📄 messages.json
📂 de/
📄 messages.json
📂 ko/
📄 messages.json
```
```jsonc
// public/_locales/en/messages.json
{
"helloWorld": {
"message": "Hello world!",
},
}
```
3. Get the translation:
```ts
browser.i18n.getMessage('helloWorld');
```
4. _Optional_: Add translations for extension name and description:
```json
{
"extName": {
"message": "..."
},
"extDescription": {
"message": "..."
},
"helloWorld": {
"message": "Hello world!"
}
}
```
```ts
export default defineConfig({
manifest: {
name: '__MSG_extName__',
description: '__MSG_extDescription__',
default_locale: 'en',
},
});
```
## Alternatives
The vanilla API has very few features, which is why you may want to consider using third-party NPM packages like `i18next`, `react-i18n`, `vue-i18n`, etc.
However, it is recommended you stick with the vanilla API (or a package based on top of the vanilla API, like [`@wxt-dev/i18n`](/i18n)), because:
- They can localize text in your manifest and CSS files
- Translations are loaded synchronously
- Translations are not bundled multiple times, keeping your extension small
- Zero configuration
However, there is one major downside to the vanilla API and any packages built on top of it:
- Language cannot be changed without changing your browser/system language
Here are some examples of how to setup a third party i18n library:
Read the docs linked above to learn more about using the vanilla messaging APIs.
## Alternatives
The vanilla APIs are difficult to use and are a pain point to many new extension developers. For this reason, WXT recommends installing an NPM package that wraps around the vanilla APIs.
Here are some popular messaging libraries that support all browsers and work with WXT:
- [`trpc-chrome`](https://www.npmjs.com/package/trpc-chrome) - [tRPC](https://trpc.io/) adapter for Web Extensions.
- [`webext-bridge`](https://www.npmjs.com/package/webext-bridge) - Messaging in WebExtensions made super easy. Out of the box.
- [`@webext-core/messaging`](https://www.npmjs.com/package/@webext-core/messaging) - Light weight, type-safe wrapper around the web extension messaging APIs
- [`@webext-core/proxy-service`](https://www.npmjs.com/package/@webext-core/proxy-service) - A type-safe wrapper around the web extension messaging APIs that lets you call a function from anywhere, but execute it in the background.
- [`Comctx`](https://github.com/molvqingtai/comctx) - Cross-context RPC solution with type safety and flexible adapters.
WXT can ZIP your extension and submit it to various stores for review or for self-hosting.
## 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
WXT provides two commands to help automate submitting a new version for review and publishing:
- `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command
- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
To get started, run `wxt submit init` and follow the prompts. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
> In CI, make sure you add all the environment variables to the submit step.
To submit a new version for publishing, build all the ZIPs you plan on releasing:
```sh
wxt zip
wxt zip -b firefox
```
Then run the `wxt submit` command, passing in all the ZIP files you want to release. In this case, we'll do a release for all 3 major stores: Chrome Web Store, Edge Addons, and Firefox Addons Store.
If it's your first time running the command or you recently made changes to the release process, you'll want to test your secrets by passing the `--dry-run` flag.
See the [Firefox Addon Store](#firefox-addon-store) section for more details about the `--firefox-sources-zip` option.
:::
## 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.
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.
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.
```ts [wxt.config.ts]
import { defineConfig } from 'wxt';
export default defineConfig({
zip: {
// ...
},
});
```
If it's your first time submitting to the Firefox Addon Store, or if you've updated your project layout, always test your sources ZIP! The commands below should allow you to rebuild your extension from inside the extracted ZIP.
:::code-group
```sh [pnpm]
pnpm i
pnpm zip:firefox
```
```sh [npm]
npm i
npm run zip:firefox
```
```sh [yarn]
yarn
yarn zip:firefox
```
```sh [bun]
bun i
bun zip:firefox
```
:::
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.
:::warning
If you use a `.env` files, they can affect the chunk hashes in the output directory. Either delete the .env file before running `wxt zip -b firefox`, or include it in your sources zip with the [`zip.includeSources`](/api/reference/wxt/interfaces/InlineConfig#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
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.
```ts [wxt.config.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 <package-name>` 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
> 🚧 Not supported yet
WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide:
- [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari) - "Convert your existing extension to a Safari web extension using Xcode’s command-line tool."
When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
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
```
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.