What changes, what stays
Stays the same: your React components, your Tailwind config, your tests, yourchrome.* API calls, and @plasmohq/storage if you use it (it wraps chrome.storage, which works the same here).
Changes:
- File-convention entrypoints (
popup.tsx,contents/*.ts) become explicit entries in a realmanifest.json. - The
manifestfield inpackage.jsonmoves intomanifest.json. plasmo dev/plasmo build/plasmo packagebecomeextension dev/extension build --zip.PLASMO_PUBLIC_*env vars becomeEXTENSION_PUBLIC_*.
Step 1: install Extension.js
Step 2: write the manifest
Plasmo generates the manifest frompackage.json plus file conventions. Extension.js treats manifest.json as the source of truth. Translate each convention:
| Plasmo convention | manifest.json entry |
|---|---|
popup.tsx or src/popup.tsx | "action": {"default_popup": "popup/index.html"} |
options.tsx | "options_ui": {"page": "options/index.html"} |
newtab.tsx | "chrome_url_overrides": {"newtab": "newtab/index.html"} |
background.ts | "background": {"service_worker": "background.ts"} |
contents/inline.ts with CS config | "content_scripts": [{...}] entry |
manifest field in package.json | merge into manifest.json directly |
popup/index.html
.ts/.tsx in the manifest and in script tags. Extension.js compiles them at build time.
Step 3: convert content scripts
Plasmo reads the exportedPlasmoCSConfig for matches:
contents/inline.tsx
manifest.json
getRootContainer and anchors), mount your component with regular DOM code instead. Create a container element, append it where you anchored, and render into it. See Content scripts for the full pattern, including shadow DOM isolation for styles.
Step 4: env vars and messaging
- Rename
PLASMO_PUBLIC_*toEXTENSION_PUBLIC_*in your.envfiles and code. See Environment variables. @plasmohq/storagekeeps working as-is.@plasmohq/messagingdepends on Plasmo’sbackground/messages/*build convention. Replace handlers with standardchrome.runtime.onMessagelisteners in your background script. See Messaging.
Step 5: update package.json scripts
--target=firefox-mv2, Extension.js targets browsers directly:
dist/chrome and dist/firefox 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 chrome.* and you want identical source running on Firefox.
Common gotchas
- Icons in
assets/: Plasmo auto-generates icon sizes fromassets/icon.png. Extension.js uses whatever"icons"declares in the manifest; export the sizes you need. See Icons. ~import alias: Plasmo aliases~to the project root. Map it intsconfig.jsonpaths, or switch to relative imports. See Path resolution.- Storage hooks:
useStoragefrom@plasmohq/storage/hookworks unchanged; it only depends on React andchrome.storage.

