TL;DR: which should you choose?
Choose Extension.js if…
- You want the
manifest.jsonas the source of truth, not generated from file names. - You want the CLI to launch the browser and load the extension for you.
- You target Chrome, Edge, Firefox, and Safari from one tree, with per-browser
dist/output. - You want a bundler that still receives releases, and docs that your assistant can read through a docs MCP endpoint and
llms.txt(details).
Choose Plasmo if…
- You prefer file-system convention (
popup.tsx,contents/) over an explicit manifest. - You rely on CSUI, which mounts a React, Svelte, or Vue component into a page inside a shadow root for you.
- You use
@plasmohq/storageand@plasmohq/messaging, and your extension already works on the pinned toolchain.
At a glance
Mental model
Extension.js stays close to the platform. You author amanifest.json and reference real files. The CLI compiles, filters per browser, and ships. If you already understand how a browser extension is structured, the framework gets out of your way.
Plasmo abstracts the platform. A file named popup.tsx becomes the popup. A file under contents/ becomes a content script, and its matches come from an exported config object. Plasmo’s own docs describe it as “like Next.js for browser extensions”. The manifest is generated under the hood from your source files and from the manifest field in package.json.
Neither approach is universally better. The choice depends on whether you want to see your manifest or declare your manifest.
CLI surface
Extension.js
extension dev launches the browser on a temporary profile and loads the build. The argument can be a local path, a GitHub URL, or a ZIP archive. See Get started immediately.
Plasmo
plasmo dev writes build/chrome-mv3-dev and starts a live-reloading server. Loading that folder into the browser is a manual step: the Plasmo docs say “We plan to automate this in the future.” Every browser and manifest pair is one --target value.
Cross-browser strategy
Both frameworks ship one codebase to several browsers, with different mechanics:- Extension.js uses browser-prefixed manifest fields (
chrome:,firefox:,gecko:, and so on) inside onemanifest.json. The unprefixed keys apply everywhere, and prefixed keys land only in matching builds. One run with--browser=chrome,firefoxwritesdist/chromeanddist/firefox. - Plasmo builds one target per run. Browser differences live in environment variables: a
.env.firefoxfile,process.env.PLASMO_BROWSERin code, and env placeholders inside themanifestoverrides. A placeholder that resolves to nothing removes the field from the generated manifest.
chrome-mv3, firefox-mv2, and the experimental firefox-mv3. Its FAQ says Edge, Brave, and Opera “should work” because they are Chromium. Safari needs the safari-mv3 target plus a manual run of Apple’s converter. Extension.js names each of those browsers as a --browser value, and the Safari build runs the converter and xcodebuild for you.
Where Plasmo is ahead
Be honest about this before you move:- CSUI. Export a React, Svelte 3, or Vue 3 component from a content script. Plasmo mounts it in a shadow root, isolated from the host page’s styles. In Extension.js you write the container and the shadow root yourself, as shown in Content scripts.
- Store submission from the repo. Plasmo documents a GitHub Action, Browser Platform Publisher, that reads a
keys.jsonsecret and uploads to the stores. Extension.js builds the archives (Package an extension for store upload). The upload is left to your CI against each store’s API. Theextension publishcommand produces a share link on extension.dev, not a store submission. - Bundled helper libraries.
@plasmohq/storageand@plasmohq/messagingwrapchrome.storageandchrome.runtimemessaging. Extension.js has no equivalent packages. You call the browser APIs directly, which is also why@plasmohq/storagekeeps working after a migration.
Maintenance status
Check the signals yourself before you decide. These values come from npm and the GitHub API on 5 October 2026.
Plasmo still installs and still builds. Downloads stay high because existing projects keep it pinned. There has been no release and no commit on
main in over sixteen months. If your extension works today, you do not need to move. If you start a new extension, or need a bundler that still receives updates, read on.
Migration paths
If you are already on Plasmo and want to evaluate Extension.js, the typical migration touches four things:- Write a
manifest.jsonby hand, mapping each convention file to a manifest entry and merging themanifestfield frompackage.json. - Move content script
matchesfrom the exportedPlasmoCSConfigintocontent_scriptsin the manifest. - Rename
PLASMO_PUBLIC_*toEXTENSION_PUBLIC_*. - Replace
plasmo dev,plasmo build, andplasmo packagewithextension devandextension build --zip.
chrome.* calls copy over without changes. The full step-by-step guide, with the convention-to-manifest table and the CSUI replacement, is at Migrate from Plasmo.
When to choose Extension.js
- You want the manifest as the source of truth, not generated.
- You want the CLI to launch the browser and load the extension for you.
- You target Chrome, Edge, Firefox, and Safari from one tree and one command.
- You want a bundler that still receives releases.
- You want a docs MCP endpoint and
llms.txtfor your assistant.
When to choose Plasmo
- You prefer file-system convention over an explicit manifest.
- You depend on CSUI anchors and the shadow-root mounting that it does for you.
- You already publish through Browser Platform Publisher and your pinned toolchain still builds.

