> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Plasmo to Extension.js

> Step-by-step migration from a Plasmo browser extension to Extension.js. Map file conventions to manifest.json, convert env vars, and keep your React code.

[Plasmo](https://www.plasmo.com) 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.

| Signal | Value |
| - | - |
| Last npm release | 0.90.5, published 17 May 2025 |
| Last commit on `main` | 17 May 2025 |
| Open issues | 371 |
| Downloads in the last month | about 2.3 million |

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

```bash theme={null}
npm install extension@latest --save-dev
npm uninstall plasmo
```

## 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:

| 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 |

HTML entrypoints are plain HTML files that load your React component. A popup looks like:

```html popup/index.html theme={null}
<!doctype html>
<html>
  <body>
    <div id="root"></div>
    <script src="./index.tsx" type="module"></script>
  </body>
</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:

```ts contents/inline.tsx theme={null}
import type { PlasmoCSConfig } from "plasmo";

export const config: PlasmoCSConfig = {
  matches: ["https://example.com/*"],
};
```

In Extension.js, the matches live in the manifest and the file is a plain module:

```json manifest.json theme={null}
{
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],
      "js": ["content/inline.tsx"]
    }
  ]
}
```

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](/docs/implementation-guide/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](/docs/features/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](/docs/implementation-guide/messaging).

## Step 5: update package.json scripts

```json theme={null}
{
  "scripts": {
    "dev": "extension dev",
    "build": "extension build",
    "start": "extension start"
  }
}
```

Where Plasmo used `--target=firefox-mv2`, Extension.js targets browsers directly:

```bash theme={null}
extension build --browser=chrome,firefox --zip
```

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

```bash theme={null}
extension dev --browser=chrome
```

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](/docs/implementation-guide/icons).
* **`~` import alias:** Plasmo aliases `~` to the project root. Map it in `tsconfig.json` paths, or switch to relative imports. See [Path resolution](/docs/features/path-resolution).
* **Storage hooks:** `useStorage` from `@plasmohq/storage/hook` works unchanged, because it only depends on React and `chrome.storage`.

## See also

* [Migrate from WXT](/docs/migrate/from-wxt)
* [Migrate from CRXJS](/docs/migrate/from-crxjs)
* [Extension.js vs WXT](/docs/compare/extension-js-vs-wxt)
* [Cross-browser compatibility](/docs/features/cross-browser-compatibility)
* [Reload and HMR](/docs/features/reload-and-hmr)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.