Migrate to Plasmo

Migrate to Plasmo Framework

The Plasmo Browser Extension Framework simplifies building browser extensions with its declarative approach. It eliminates boilerplate code and provides a file-based configuration system that is easy to understand, use and opt out of.

In this guide, we will walk you through transitioning from any extension project to Plasmo and highlight some key changes you need to make.

Install Plasmo CLI

The Plasmo framework's main driver is the Plasmo CLI. It is a Node.js package that contains a compiler, a bundler, a development server, and a packager tailor-made for browser extensions:

pnpm install plasmo

To start the development server, run plasmo dev. To build the extension, run plasmo build. To package the extension, run plasmo package.

manifest.json

Plasmo merges manifest.json into the package.json and abstracts away the most basic properties:

Manifest FieldAbstractions
iconsAuto generated with the icon.png in the assets directory
action, browser_actionspopup.tsx
options_uioptions.tsx
content_scriptscontents/*.{ts,tsx}, content.ts, content.tsx
backgroundbackground.ts
sandboxsandbox.tsx, Sandbox Pages
manifest_versionset by the --target build flag, default to 3
versionset by the version field in package.json
nameset by the displayName field in package.json
descriptionset by the description field in package.json
authorset by the author field in package.json
homepage_urlset by the homepage field in package.json

Plasmo centralizes common metadata between package.json and manifest.json and resolves any static file references (such as action, background, content scripts, and so on) automatically.

This enables you to focus on the metadata that matters, such as name, description, OAuth, declarative_net_request, and so on.

Furthermore, it enables the framework to provide even more powerful features, such as:

Popup, Options, New tab Pages

Plasmo removes the need to manually mount your React/Svelte/Vue components.

In a non-Plasmo extension project, to mount a React component, you'd create an index.html template and associated JavaScript code:

popup.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Popup</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="popup.js"></script>
  </body>
</html>
popup.jsx
import { createRoot } from "react"
 
import Popup from "./core/popup"
 
const root = document.getElementById("root")
createRoot(root).render(<Popup />)

If you wanted to add TypeScript, LESS, or SCSS, you would need to use Webpack, Parcel, or ESBuild, with extra plugins and loader setups.

With the Plasmo framework, it's much simpler. Add a popup.tsx or options.tsx file in the source code directory, exporting a default React component.

Plasmo will take care of the rest:

popup.tsx
import Popup from "./core/popup"
 
export default Popup

Plasmo has built-in support for CSS, SCSS, LESS, CSS Modules, and PostCSS plugins.

If you'd like to, for example, use SCSS, you can simply import the SCSS file in your React component:

popup.tsx
import Popup from "./core/popup"
 
import "./popup.scss"
 
export default Popup

You can mount a React component this way for the options page, new tab page, sandbox pages, and any other custom page.

Custom Pages

In non-Plasmo extension projects, creating custom pages typically requires additional HTML templates and associated JavaScript code:

tabs/custom.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Custom Page</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="./custom.js"></script>
  </body>
</html>
tabs/custom.js
import { createRoot } from "react"
 
import CustomPage from "./core/custom-page"
 
const root = document.getElementById("root")
createRoot(CustomPage).render(<PopupApp />)

With Plasmo, you can use the built-in tab pages feature to create custom pages easily. Add a tabs folder in you source code directory, and add your custom pages there, using .tsx files:

tabs/custom.tsx
import Hello from "./core/hello"
 
export default Hello

The page will be available at chrome-extension://<extension-id>/tabs/custom.html.

Content Scripts

In non-Plasmo extension projects, you'd specify your content scripts in the manifest.json file and write them in JavaScript:

manifest.json
{
  "content_scripts": [
    {
      "matches": ["https://*/*", "http://*/*"],
      "all_frames": true,
      "css": ["content.css"],
      "run_at": "document_start"
    }
  ]
}
content.js
console.log("Hello from content script!")

With Plasmo, your can specify your content script and its respective config in a file called content.ts, or inside a directory called contents - More on that later!

Here's what the content.ts file would look like:

content.ts
import type { PlasmoCSConfig } from "plasmo"
 
export const config: PlasmoCSConfig = {
  matches: ["https://*/*", "http://*/*"],
  all_frames: true,
  css: ["~content.css"]
}
 
console.log("Hello from content script!")

No more moving back and forth between your manifest.json and content.js files. An added benefit is that the config is fully typed and thus can provide valuable auto-completion from your IDE.

To add multiple content scripts, create a directory called contents and repeat the above steps. You can name your scripts whatever you'd like in this directory. They will all get added as content scripts with their own configs.

For example, let's say you want to add a content script that changes the background color of the page to green. You can do so by creating a file called emerald-splash.ts in the contents directory:

contents/emerald-splash.ts
document.body.style.backgroundColor = "green"

Since the content script file didn't export a config, Plasmo will use the default config:

{
  "matches": ["<all_urls>"]
}

Content Scripts UI

If you're injecting a React component into a web page, your extension likely has a lot of boilerplate code, creating Shadow DOMs, finding the right element to mount to, MutationObservers, and more.

We've abstracted all of this out so you can focus on building your component and not worry about the rest.

For example, let's say you want to inject a React component of a simple button into a web page. You can do so by creating a file called press-me.tsx in the contents directory:

contents/press-me.tsx
export default function PressMeCSUI() {
  return <button>Press me</button>
}

Plasmo will automatically inject this component into the page, and mount it to the root element of the page.

What if you want to inject the component into a specific element? You can do so by exporting a getInlineAnchor function from the file:

contents/press-me.tsx
import type { PlasmoGetInlineAnchor } from "plasmo"
 
export const getInlineAnchor: PlasmoGetInlineAnchor = async () =>
  document.querySelector("#pricing")

By returning an Element, Plasmo will mount the component adjacent to that element. You can completely customize the mounting behavior, how the component renders, and more.

This feature is called Content Scripts UI. To learn more about its API and how it works, check out the technical documentation on its lifecycle.

You can also specify the component's insert position relative to the element you're mounting it to:

contents/press-me.tsx
import type { PlasmoGetInlineAnchor } from "plasmo"
 
export const getInlineAnchor: PlasmoGetInlineAnchor = async () => ({
      element: document.querySelector("#pricing"),
      insertPosition: "afterend"
})

Background Service Worker

In non-Plasmo projects, creating a background service worker requires specifying the background property in the manifest.json file and writing the service worker code in JavaScript:

manifest.json
{
  "background": {
    "service_worker": "background.js"
  }
}
background.js
console.log("Hello from BGSW!")

In Plasmo, you specify the background service worker by creating a background.ts file:

background.ts
console.log("Hello from BGSW!")

You can import any modules that target the standard service worker runtime into background.ts. For example, you can add the bip39 module:

background.ts
import { generateMnemonic } from "bip39"
 
console.log(
  "Live now; make now always the most precious time. Now will never come again."
)
console.log(generateMnemonic())

Plasmo will automatically bundle the background.ts file and add it as a background service worker to the manifest.json file.

Environment Variables

If you're currently using environment variables, you can continue to do so in Plasmo.

The Plasmo framework supports environment variables out of the box with no additional setup required.

Opting Out

The Plasmo framework's abstraction philosophy is to remove the most common configuration and boilerplate code. This enables developers to work under a higher abstraction layer -- their chosen UI library/framework, such as React, Vue, and Svelte. This is the path to creating more powerful and beautiful extensions.

Opting out of Plasmo is as easy as removing the plasmo dependency from your package.json file and taking your components out onto your custom setups. This is possible because all the glue code generated by Plasmo is injected at the framework compiler and bundler layer, making your feature code extremely portable.

You can also use Plasmo as much or as little as needed. All chrome APIs are available, and you can use them directly in your feature code. For example, instead of using the messaging API, you can use the chrome.runtime.messaging API directly. The same goes for content scripts and extension pages.