Landing here because
vite build fails with [crx:manifest-post] Content script fileName is undefined on Vite 8? See the dedicated fix
page for workarounds
before migrating.What changes, what stays
Stays the same: your React/Vue/Svelte components, your Tailwind config, your tests, yourchrome.* API calls.
Changes:
vite.config.ts+@crxjs/vite-pluginbecomesextension.config.js(or no config at all).manifest.config.ts(TypeScript module) becomes plainmanifest.jsonwith browser-prefixed keys.vite/vite buildscripts becomeextension dev/extension build.- Extension.js dev replaces the Vite dev server, with browser launch, profile management, and per-target reload built in.
Step 1: install Extension.js
Step 2: convert the manifest
CRXJS uses a TypeScript module:manifest.config.ts
manifest.json and reference real files:
manifest.json
- File extensions stay
.ts/.tsxin the manifest. Extension.js compiles them at build time. - Drop the
src/prefix if you flatten directories. Extension.js follows whatever paths your manifest declares. - For browser-specific values, use prefixed keys (
firefox:browser_specific_settings) rather than per-build manifests.
Step 3: update package.json scripts
Replace:Step 4: remove vite.config.ts
If yourvite.config.ts only existed to wire CRXJS, delete it. If it has other plugins, move equivalents to extension.config.js or to Rspack configuration for advanced bundler customization.
Step 5: handle HMR differences
CRXJS HMR pushes updates over a Vite WebSocket. Extension.js uses a different model documented in Reload and HMR:- Popup, options, devtools pages: HMR.
- Content scripts: targeted reload.
- Background service worker: full restart.
import.meta.hot for content-script logic, replace those branches with regular module code. Extension.js handles reload orchestration outside your source.
Step 6: cross-browser output
CRXJS targets Chromium. To start emitting Firefox output too:dist/chrome and dist/firefox with browser-correct manifests and .zip archives ready for the Chrome Web Store and addons.mozilla.org. See Cross-browser compatibility.
Step 7: verify
chrome.* runs on Firefox as written, because Firefox supports the chrome.* namespace natively. The --polyfill flag is for the other direction: it supplies browser.* on Chromium targets. extension dev applies it by default, and extension build leaves it off unless you pass the flag.
Common gotchas
- Service worker
importstatements: Extension.js bundles your background entry, soimportin your source needs no manifest change. Extension.js does not add"type": "module"for you. Declare that key yourself if you want it, and the build keeps it. web_accessible_resourcestyping: Manifest V3 uses[{resources, matches}]blocks. Both frameworks emit the right shape, so copy your existing entries verbatim.- Hashed asset paths: production builds under
dist/<browser>use clean names.extension devappends a build hash to content scripts, for examplecontent_scripts/content-0.813e908a.js, to defeat the extension resource cache. See Reload and HMR. If your code hardcoded Vite-style hashed filenames, replace them with manifest-relative paths.

