Skip to content
extension-toolchain logo

Extension Toolchain

extension-toolchain

Apply modern browser extension toolchain patterns: WXT (default), Plasmo, CRXJS for Chrome/Firefox/Safari extensions. Use when building browser extensions, choosing extension frameworks, or discussing manifest v3 patterns.

SKILL.md

Full skill instructions

Extension Toolchain

Modern browser extension development with Manifest V3, focusing on framework-agnostic solutions.

Recommended Stack: WXT (Default)

Why WXT (2025):

  • Framework-agnostic (React, Vue, Svelte, SolidJS, Vanilla)
  • Vite-powered (fast HMR, optimized builds)
  • Auto-reload on code changes (content scripts too!)
  • TypeScript-first with excellent type generation
  • Automated publishing to stores
  • Manifest V3 by default
# Create new extension
npm create wxt@latest

# Choose your framework
? Select a template:
  > vanilla
    react
    vue
    svelte
    solid

# Start development
cd my-extension
npm run dev         # Chrome (default)
npm run dev:firefox # Firefox
npm run dev:edge    # Edge
npm run dev:safari  # Safari (experimental)

When to Use WXT

✅ Multi-framework teams (framework-agnostic) ✅ Need cross-browser compatibility ✅ Want modern DX (HMR, TypeScript, auto-reload) ✅ Publishing to multiple stores ✅ Complex extensions with multiple entry points

Alternative: Plasmo

Best for React developers:

  • Next.js-like file-based routing
  • Automatic code splitting
  • Built-in remote code bundling
  • Very opinionated (React-centric)
# Create Plasmo extension
npm create plasmo

# Start development
npm run dev

When to Use Plasmo

✅ React-only team ✅ Want Next.js-like DX ✅ Need remote code bundling ✅ Prefer opinionated frameworks

Alternative: CRXJS (Vite Plugin)

Minimal, unopinionated:

  • Just a Vite plugin (you control everything)
  • Best-in-class HMR (especially for content scripts)
  • Lightweight, minimal overhead
  • Requires more manual setup
# Add to existing Vite project
npm install @crxjs/​vite-plugin -D

When to Use CRXJS

✅ Want maximum control ✅ Already using Vite ✅ Minimal tooling preference ✅ Expert developer team

Toolchain Comparison

WXTPlasmoCRXJS
FrameworksAllReact-focusedAll
SetupBatteries-includedOpinionatedManual
DXExcellentExcellentGreat
HMRYesYesBest
Auto-publishYesYesNo
Learning CurveLowLowMedium
FlexibilityHighMediumHighest

Project Structure (WXT)

my-extension/
├── entrypoints/
│   ├── background.ts      # Service worker
│   ├── content.ts         # Content script
│   ├── popup/             # Extension popup
│   │   ├── index.html
│   │   └── main.tsx
│   └── options/           # Options page
│       ├── index.html
│       └── main.tsx
├── components/            # Shared UI components
├── utils/                 # Shared utilities
├── public/                # Static assets
│   └── icon.png          # Extension icon
├── wxt.config.ts         # WXT configuration
└── package.json

Manifest V3 Essentials

// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
  manifest: {
    name: 'My Extension',
    version: '1.0.0',
    permissions: ['storage', 'tabs'],
    host_permissions: ['https://*.example.com/​*'],
    action: {
      default_title: 'My Extension',
    },
  },
})

Key Manifest V3 Changes

  • Service Workers replace background pages (no DOM access)
  • host_permissions separate from permissions
  • scripting API for dynamic content script injection
  • No remotely hosted code (bundle everything)
  • Limited executeScript capabilities

Communication Patterns

Popup ↔ Background

// popup/​main.tsx
import browser from 'webextension-polyfill'

const response = await browser.runtime.sendMessage({
  type: 'GET_DATA',
  payload: { key: 'value' },
})

// background.ts
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GET_DATA') {
    // Process and respond
    sendResponse({ data: 'result' })
  }
  return true // Keep channel open for async response
})

Content Script ↔ Background

// content.ts
import browser from 'webextension-polyfill'

// Send message to background
const result = await browser.runtime.sendMessage({
  type: 'ANALYZE_PAGE',
  url: window.location.href,
})

// background.ts
browser.runtime.onMessage.addListener(async (message) => {
  if (message.type === 'ANALYZE_PAGE') {
    const analysis = await analyzePage(message.url)
    return { analysis }
  }
})

Content Script ↔ Page (Web Page)

// content.ts - inject into page context
const script = document.createElement('script')
script.src = browser.runtime.getURL('injected.js')
document.head.appendChild(script)

// Listen for messages from page
window.addEventListener('message', (event) => {
  if (event.source !== window) return
  if (event.data.type === 'FROM_PAGE') {
    // Handle message from page
  }
})

// injected.js (runs in page context, has access to page's window/​DOM)
window.postMessage({ type: 'FROM_PAGE', data: 'value' }, '*')

Storage Patterns

// Using chrome.storage.sync (syncs across devices)
import browser from 'webextension-polyfill'

// Save
await browser.storage.sync.set({ key: 'value' })

// Load
const { key } = await browser.storage.sync.get('key')

// Listen for changes
browser.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'sync' && changes.key) {
    console.log('Value changed:', changes.key.newValue)
  }
})

Essential Libraries

# Cross-browser compatibility
npm install webextension-polyfill

# State Management
npm install zustand

# Forms
npm install react-hook-form zod

# UI Components (if using React)
npm install @radix-ui/​react-* # Headless components

Testing Strategy

# Install testing libraries
npm install --save-dev vitest @testing-library/​react @testing-library/​user-event
npm install --save-dev @wxt-dev/​testing

Example test:

// popup/​main.test.tsx
import { render, screen } from '@testing-library/​react'
import { describe, it, expect } from 'vitest'
import Popup from './​main'

describe('Popup', () => {
  it('renders heading', () => {
    render(<Popup />)
    expect(screen.getByRole('heading')).toBeInTheDocument()
  })
})

Quality Gates Integration

# .github/​workflows/​extension-ci.yml
name: Extension CI

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/​checkout@v4
      - uses: actions/​setup-node@v4
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build

      - name: Upload build artifact
        uses: actions/​upload-artifact@v4
        with:
          name: extension-build
          path: .output/

Publishing Automation

# Build for all browsers
npm run build              # Chrome
npm run build:firefox      # Firefox
npm run build:safari       # Safari

# Zip for submission
npm run zip                # All stores

# Or use WXT's publish command (requires API keys)
wxt publish --chrome --firefox

Store submission setup:

// wxt.config.ts
export default defineConfig({
  zip: {
    artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
  },
  manifest: {
    name: '__MSG_extName__',
    description: '__MSG_extDescription__',
    default_locale: 'en',
  },
})

Performance Best Practices

  • Lazy load content scripts: Only inject when needed
  • Use storage efficiently: Minimize sync storage writes
  • Debounce frequent operations: Especially in content scripts
  • Minimize background script work: Use alarms/​events, not intervals
  • Optimize bundle size: Code splitting, tree shaking

Security Considerations

// Content Security Policy
manifest: {
  content_security_policy: {
    extension_pages: "script-src 'self'; object-src 'self'"
  }
}

// Validate messages
browser.runtime.onMessage.addListener((message) => {
  // Always validate message structure
  if (typeof message !== 'object' || !message.type) {
    return
  }

  // Type guard
  if (message.type === 'EXPECTED_TYPE') {
    // Process
  }
})

// Never inject user content directly into DOM
// Use textContent, not innerHTML
element.textContent = userInput // Safe
element.innerHTML = userInput   // XSS vulnerability!

Common Gotchas

Service Worker Lifecycle:

  • Service workers can be terminated anytime
  • Use chrome.storage for persistence, not in-memory state
  • Set up event listeners at top level (not inside async functions)

Content Script Isolation:

  • Content scripts run in isolated world
  • No direct access to page's JavaScript
  • Must use postMessage to communicate with page

Manifest V3 Restrictions:

  • No eval() or new Function()
  • No inline scripts in HTML
  • All code must be bundled
  • Limited service worker APIs

Recommendation Flow

New browser extension:
├─ Multi-framework team → WXT ✅
├─ React-only team → Plasmo
└─ Want maximum control → CRXJS

Existing extension (Manifest V2):
└─ Migrate to WXT (handles V2→V3 migration)

When agents design browser extensions, they should:

  • Default to WXT for new projects (framework-agnostic, best DX)
  • Use Manifest V3 (V2 deprecated in 2024)
  • Apply quality-gates skill for testing/​CI setup
  • Use webextension-polyfill for cross-browser compatibility
  • Follow Content Security Policy strictly
  • Plan for service worker lifecycle (no persistent background page)
  • Use chrome.storage for state persistence
  • Validate all messages between components

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View