manifest.json
as the source of truth, Rspack builds, and output that mirrors what the browser loads. See
Extension.js vs WXT for the full comparison. This
guide migrates a typical WXT project to Extension.js without rewriting your UI code.
What changes, what stays
Stays the same: your React/Vue/Svelte components, your styles, your tests, yourbrowser.*/chrome.* API calls (use --polyfill to keep the browser namespace
cross-browser), and @wxt-dev/storage if you use it standalone (it wraps the extension
storage APIs, which work the same here).
Changes:
- File-convention entrypoints (
entrypoints/popup/,entrypoints/content.ts) become explicit entries in a realmanifest.json. - The
manifestoption inwxt.config.ts(and<meta name="manifest.*">tags in HTML entrypoints) move intomanifest.json. defineBackground()/defineContentScript()wrappers unwrap into plain modules.- WXT auto-imports become explicit imports.
wxt/wxt build/wxt zipbecomeextension dev/extension build --zip.import.meta.env.WXT_*env vars becomeEXTENSION_PUBLIC_*.
Step 1: install Extension.js
Step 2: write the manifest
WXT generates the manifest fromwxt.config.ts plus the entrypoints/ layout.
Extension.js treats manifest.json as the source of truth. Translate each convention:
| WXT convention | manifest.json entry |
|---|---|
entrypoints/popup/index.html | "action": {"default_popup": "popup/index.html"} |
entrypoints/options/index.html | "options_ui": {"page": "options/index.html"} |
entrypoints/newtab/index.html | "chrome_url_overrides": {"newtab": "newtab/index.html"} |
entrypoints/background.ts | "background": {"service_worker": "background.ts"} |
entrypoints/content.ts (or *.content.ts) | "content_scripts": [{...}] entry |
manifest: {...} in wxt.config.ts | merge into manifest.json directly |
<meta name="manifest.*"> in HTML pages | the equivalent manifest.json key |
popup/, options/, content/) and point the manifest at them. File extensions stay
.ts/.tsx in the manifest and in script tags — Extension.js compiles them at build time.
Step 3: unwrap defineBackground and defineContentScript
WXT wraps runtime code so it can parse manifest options out of your source:
entrypoints/content.ts
matches live in the manifest and the file is a plain module — the
body of main() becomes top-level code:
manifest.json
content/script.ts
defineBackground(() => {...}) — its body becomes the top level of
your background file. Two WXT-specific helpers need replacing:
ctx(ContentScriptContext): WXT’sctxcancels work when the extension updates and the content script is orphaned. Replacectx-bound listeners with regularaddEventListenercalls; if you relied on invalidation handling, guard long-lived callbacks with achrome.runtime.idcheck.createShadowRootUi/createIntegratedUi: mount your component with regular DOM code instead — create a container element, append it, and render into it. See Content scripts for the full pattern, including shadow DOM isolation for styles.
Step 4: make auto-imports explicit
WXT auto-importsbrowser, defineContentScript, storage, and friends. Extension.js
does not inject globals, so add explicit imports:
browser.*calls: keep them and run with--polyfill, or switch tochrome.*.storagefrom WXT:import { storage } from "@wxt-dev/storage"keeps working as a standalone package.- Framework auto-imports (from
@wxt-dev/module-reactand similar): import from the framework package directly.
Step 5: env vars and scripts
- Rename
WXT_*(andVITE_*) variables toEXTENSION_PUBLIC_*in your.envfiles, and replaceimport.meta.env.WXT_FOOwithprocess.env.EXTENSION_PUBLIC_FOO. See Environment variables. - Update
package.jsonscripts:
wxt build -b firefox and wxt zip, Extension.js targets browsers and
packages in one command:
dist/chrome and dist/firefox (instead of .output/chrome-mv3) with
browser-correct manifests and .zip archives ready for the Chrome Web Store and
addons.mozilla.org.
Step 6: verify
--browser=firefox. Use --polyfill if your code calls browser.* and you
want identical source running on both.
Common gotchas
- Manifest V2: WXT supports MV2 as a build target; Extension.js targets Manifest V3. If you still ship an MV2 build, finish that migration first — see Manifest V3 concepts.
public/directory: files in WXT’spublic/are copied as-is. Extension.js does the same withpublic/— paths referenced from the manifest or HTML keep working.assets/and the~/@aliases: map aliases intsconfig.jsonpaths, or switch to relative imports. See Path resolution.- WXT modules (
@wxt-dev/module-react,-vue,-svelte): not needed — framework support is built in. Compare with a fresh template for the reference setup. app.config.tsruntime config: replace with your own module (a plain exported object gives you the same thing without the framework layer).

