extension-toolchain

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "extension-toolchain" with this command: npx skills add phrazzld/claude-config/phrazzld-claude-config-extension-toolchain

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

WXT Plasmo CRXJS

Frameworks All React-focused All

Setup Batteries-included Opinionated Manual

DX Excellent Excellent Great

HMR Yes Yes Best

Auto-publish Yes Yes No

Learning Curve Low Low Medium

Flexibility High Medium Highest

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Web3

mobile-toolchain

No summary provided by upstream source.

Repository SourceNeeds Review
Web3

toolchain-preferences

No summary provided by upstream source.

Repository SourceNeeds Review
Web3

crypto-gains

No summary provided by upstream source.

Repository SourceNeeds Review