Skip to main content
Plasmo is a Parcel-based browser extension framework with file-convention routing. Its release cadence has slowed and its toolchain has aged against current bundlers, which is why many teams are re-evaluating. This guide migrates a typical Plasmo project to Extension.js without rewriting your UI code.

What changes, what stays

Stays the same: your React components, your Tailwind config, your tests, your chrome.* 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 real manifest.json.
  • The manifest field in package.json moves into manifest.json.
  • plasmo dev / plasmo build / plasmo package become extension dev / extension build --zip.
  • PLASMO_PUBLIC_* env vars become EXTENSION_PUBLIC_*.

Step 1: install Extension.js

Step 2: write the manifest

Plasmo generates the manifest from package.json plus file conventions. Extension.js treats manifest.json as the source of truth. Translate each convention: HTML entrypoints are plain HTML files that load your React component. A popup looks like:
popup/index.html
File extensions stay .ts/.tsx in the manifest and in script tags. Extension.js compiles them at build time.

Step 3: convert content scripts

Plasmo reads the exported PlasmoCSConfig for matches:
contents/inline.tsx
In Extension.js, the matches live in the manifest and the file is a plain module:
manifest.json
If you used Plasmo CSUI (content script UI with 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_* to EXTENSION_PUBLIC_* in your .env files and code. See Environment variables.
  • @plasmohq/storage keeps working as-is.
  • @plasmohq/messaging depends on Plasmo’s background/messages/* build convention. Replace handlers with standard chrome.runtime.onMessage listeners in your background script. See Messaging.

Step 5: update package.json scripts

Where Plasmo used --target=firefox-mv2, Extension.js targets browsers directly:
You get 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

Check popup, options, content scripts, and background behavior, then run the same against Firefox with --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 from assets/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 in tsconfig.json paths, or switch to relative imports. See Path resolution.
  • Storage hooks: useStorage from @plasmohq/storage/hook works unchanged; it only depends on React and chrome.storage.

See also