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.

Is Plasmo still maintained

Check the signals yourself before you decide. These values come from npm and GitHub on 14 September 2026. Plasmo still installs and still builds. Downloads stay high because existing projects keep it pinned. There has been no release in sixteen months. If your extension works today, you do not need to move. If you start a new extension, or you need Node.js 22, current Manifest V3 changes, or a bundler that still receives updates, this guide takes about ten minutes.

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. Code that calls 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

  • Icons in assets/: Plasmo auto-generates icon sizes from assets/icon.png. Extension.js uses whatever "icons" declares in the manifest, so 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, because it only depends on React and chrome.storage.

See also