L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

wxt

wxt

WXT is a Vite-based framework that builds browser extensions for Chrome, Firefox, Safari, and Edge from one codebase. Use when someone asks to "build a browser extension", "Chrome extension with React", "WXT framework", "cross- browser extension", "manifest v3 extension", "build Firefox extension", or "browser extension with TypeScript". Covers content scri…

Independent GitHub snapshot · Automated classification · Original documentation governs

Tterminalskills / skillsApache-2.0

Documentation de la Skill

GitHub

name: wxt description: >- WXT is a Vite-based framework that builds browser extensions for Chrome, Firefox, Safari, and Edge from one codebase. Use when someone asks to "build a browser extension", "Chrome extension with React", "WXT framework", "cross- browser extension", "manifest v3 extension", "build Firefox extension", or "browser extension with TypeScript". Covers content scripts, background workers, popup/options pages, storage, messaging, and publishing. license: Apache-2.0 compatibility: "WXT 0.21: Node.js 22+, Vite 6.3.4+, TypeScript 5.4+. Targets Chrome, Firefox, Safari, Edge. Supports React, Vue, Svelte, Solid." metadata: author: terminal-skills version: "1.1.0" category: development tags: ["browser-extension", "chrome", "firefox", "wxt", "manifest-v3"] repository: https://github.com/wxt-dev/wxt

WXT

Overview

WXT is a Vite-based framework for building browser extensions — its own tagline is "like Nuxt, but for web extensions." File-based entrypoints, hot reload, TypeScript-first, auto-imports. One codebase produces a separate build per browser: Manifest V3 for Chrome and Edge, Manifest V2 by default for Firefox and Safari (pass --mv3 to override). There is no manifest.json in the source tree — WXT generates it from wxt.config.ts and the entrypoint files.

WXT is still pre-1.0, so a change in the second digit (0.20 → 0.21) is a breaking release. This skill matches 0.21, which needs Node.js 22+, and makes vite a required peer dependency and web-ext an optional one (without web-ext the dev command no longer opens a browser).

When to Use

  • Building a browser extension for Chrome, Firefox, or all browsers
  • Want hot reload during development (not manual reload)
  • Need TypeScript + React/Vue/Svelte in your extension
  • Migrating from Manifest V2 to V3
  • Want one codebase that targets multiple browsers

Instructions

Setup

npx wxt@latest init code-explainer -t react --pm npm   # templates: vanilla, vue, react, svelte, solid
cd code-explainer
npm install    # postinstall runs `wxt prepare`, which generates .wxt/ (types, tsconfig)
npm run dev    # Opens Chrome with the extension loaded and hot reload

Without -t and --pm the init command asks for the template and package manager. To add WXT to an existing project: npm i -D wxt vite typescript, plus web-ext if the dev command should open a browser.

Project Structure

code-explainer/
├── entrypoints/
│   ├── popup/           # Popup UI (click extension icon)
│   │   ├── index.html
│   │   ├── main.tsx
│   │   └── App.tsx
│   ├── content.ts       # Content script (runs on web pages)
│   └── background.ts    # Service worker (background logic)
├── utils/               # Auto-imported helpers (also components/, hooks/, composables/)
├── public/
│   └── icon/            # 16.png, 32.png, 48.png, 96.png, 128.png — found automatically
├── .env                 # WXT_* and VITE_* variables, exposed on import.meta.env
├── wxt.config.ts
└── package.json

More content scripts go in files named {name}.content.ts. Other recognised entrypoint names include options, sidepanel, newtab and devtools (each a folder with an index.html).

Config and Permissions

WXT does not add permissions for you. Every API used below has to be declared, or it is undefined at runtime (storage throws):

// wxt.config.ts
import { defineConfig } from "wxt";

export default defineConfig({
  modules: ["@wxt-dev/module-react"],
  manifest: {
    name: "Code Explainer",
    permissions: ["storage", "alarms"],
    host_permissions: ["https://api.openai.com/*"],
  },
});

Write manifest keys in MV3 form (action, host_permissions); WXT converts them for MV2 builds. The manifest name and version default to the values in package.json, which also name the zip files — the template ships as wxt-react-starter 0.0.0, so edit both.

// utils/storage.ts — typed storage items, auto-imported everywhere
export const explainCount = storage.defineItem<number>("local:explainCount", { fallback: 0 });

Content Script

// entrypoints/content.ts — Runs on matched web pages; createShadowRootUi isolates the button's styles.
// DOM and extension API calls must stay inside main(): WXT imports this file in Node at build time.
export default defineContentScript({
  matches: ["https://github.com/*"],
  cssInjectionMode: "ui",
  async main(ctx) {
    const ui = await createShadowRootUi(ctx, {
      name: "code-explainer",
      position: "inline",
      anchor: "body",
      onMount(container) {
        const btn = document.createElement("button");
        btn.textContent = "Explain selection";
        btn.style.cssText = "position:fixed;right:16px;bottom:16px;z-index:9999";
        btn.onclick = async () => {
          const code = window.getSelection()?.toString().slice(0, 5000);
          if (!code) return;
          // Send to background for the API call
          const reply = await browser.runtime.sendMessage({ type: "EXPLAIN", code });
          alert(reply.error ?? reply.summary);
        };
        container.append(btn);
      },
    });
    ui.mount();
  },
});

Background Service Worker

// entrypoints/background.ts — Service worker: API calls, alarms, message routing. No DOM access.
// The main function cannot be async.
export default defineBackground(() => {
  // Since WXT 0.20 `browser` is the native API, not webextension-polyfill:
  // answer with sendResponse and return true instead of returning a promise.
  browser.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
    if (msg.type !== "EXPLAIN") return;
    explain(msg.code)
      .then((summary) => sendResponse({ summary }))
      .catch((err) => sendResponse({ error: String(err) }));
    return true; // keep the channel open until sendResponse runs
  });

  // Periodic tasks with alarms
  browser.alarms.create("refresh-badge", { periodInMinutes: 30 });
  browser.alarms.onAlarm.addListener(async (alarm) => {
    if (alarm.name !== "refresh-badge") return;
    const count = await explainCount.getValue();
    // MV2 builds (the Firefox default) only have browser.browserAction
    await (browser.action ?? browser.browserAction).setBadgeText({ text: count ? String(count) : "" });
  });
});

async function explain(code: string): Promise<string> {
  const apiKey = await storage.getItem<string>("local:apiKey");
  if (!apiKey) throw new Error("Save an API key in the popup first");
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
    body: JSON.stringify({
      model: import.meta.env.WXT_OPENAI_MODEL, // set in .env
      messages: [{ role: "user", content: `Explain this code:\n${code}` }],
    }),
  });
  if (!response.ok) throw new Error(`OpenAI API returned ${response.status}`);
  const data = await response.json();
  await explainCount.setValue((await explainCount.getValue()) + 1);
  return data.choices[0].message.content;
}

Popup UI (React)

// entrypoints/popup/App.tsx — Extension popup with React
import { useState, useEffect } from "react";

export default function App() {
  const [apiKey, setApiKey] = useState("");
  const [saved, setSaved] = useState(false);

  useEffect(() => {
    storage.getItem<string>("local:apiKey").then((key) => key && setApiKey(key));
  }, []);

  const save = async () => {
    await storage.setItem("local:apiKey", apiKey);
    setSaved(true);
    setTimeout(() => setSaved(false), 2000);
  };

  return (
    <div style={{ width: 300, padding: 16 }}>
      <h2>Code Explainer</h2>
      <input type="password" value={apiKey} placeholder="OpenAI API Key"
        onChange={(e) => setApiKey(e.target.value)} style={{ width: "100%" }} />
      <button onClick={save} style={{ marginTop: 8 }}>{saved ? "Saved" : "Save Key"}</button>
    </div>
  );
}

Build for Multiple Browsers

npm run dev            # Chrome with hot reload
npm run dev:firefox    # Firefox

# Production builds, one output directory per target
npm run build          # wxt build            → .output/chrome-mv3/
npm run build:firefox  # wxt build -b firefox → .output/firefox-mv2/
npx wxt build -b firefox --mv3   # → .output/firefox-mv3/
npx wxt build -b edge            # or -b safari

# Zip for store submission
npm run zip            # .output/code-explainer-1.0.0-chrome.zip
npm run zip:firefox    # ...-firefox.zip plus ...-sources.zip, which Firefox review requires

Branch on the target with import.meta.env.BROWSER, import.meta.env.FIREFOX or import.meta.env.MANIFEST_VERSION; limit an entrypoint to some browsers with include: ["firefox"] or exclude: ["chrome"].

Publish

npx wxt submit init   # asks for store credentials, writes .env.submit
npx wxt submit --dry-run \
  --chrome-zip .output/code-explainer-1.0.0-chrome.zip \
  --firefox-zip .output/code-explainer-1.0.0-firefox.zip \
  --firefox-sources-zip .output/code-explainer-1.0.0-sources.zip

Drop --dry-run to submit. The first listing in each store has to be created by hand. Edge accepts the Chrome zip via --edge-zip. Safari is not automated: build with -b safari and wrap the output with Xcode's safari-web-extension-packager.

Examples

Example 1: Block distracting sites during focus time

User prompt: "Build a Chrome extension that blocks Reddit and YouTube while I'm in a focus session."

Add a storage item and a second content script; the popup starts a session with focusUntil.setValue(Date.now() + 25 * 60_000).

// utils/storage.ts
export const focusUntil = storage.defineItem<number>("local:focusUntil", { fallback: 0 });
// entrypoints/blocker.content.ts
export default defineContentScript({
  matches: ["*://*.reddit.com/*", "*://*.youtube.com/*"],
  runAt: "document_start",
  async main() {
    const until = await focusUntil.getValue();
    if (Date.now() >= until) return;
    window.stop();
    const time = new Date(until).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
    document.documentElement.innerHTML = `<body><h1>Focus time. Back at ${time}.</h1></body>`;
  },
});

npm run build picks the file up by name and adds it to the generated manifest:

"content_scripts": [
  { "matches": ["*://*.reddit.com/*", "*://*.youtube.com/*"], "run_at": "document_start", "js": ["content-scripts/blocker.js"] }
]

Example 2: Ship the same extension to Firefox

User prompt: "My WXT extension works in Chrome. Package it for the Firefox add-on store too."

npx wxt zip -b firefox
✔ Zipped extension in 44 ms
  ├─ .output/code-explainer-1.0.0-firefox.zip  95.69 kB
  └─ .output/code-explainer-1.0.0-sources.zip  69.52 kB

The command also lists every file that went into the sources zip (hidden files such as .env are left out). The Firefox build is MV2: host_permissions are merged into permissions, action becomes browser_action, and the background runs as a script instead of a service worker. A new listing must first declare browser_specific_settings.gecko.data_collection_permissions in manifest (the build warns until it does; an explicit gecko.id is recommended). Upload both zips; the reviewers rebuild the extension from the sources zip, so check that npm i && npm run zip:firefox works inside it.

Guidelines

  • File-based entrypoints — file name and location determine the extension component
  • browser.* API — a plain alias for the browser's own browser/chrome global with @types/chrome types; the polyfill was removed in 0.20, so APIs a browser lacks are undefined (feature-detect with ?.)
  • No promise replies from onMessage — use sendResponse plus return true, or a messaging library such as @webext-core/messaging; Chrome only began accepting returned promises in version 148
  • Declare permissions yourself — storage, alarms, host_permissions and the rest go in manifest in wxt.config.ts
  • storage helper — keys carry their area prefix (local:, session:, sync:, managed:); prefer storage.defineItem for typed values with a fallback
  • Imports — helpers are auto-imported; for explicit imports use #imports (wxt/storage, wxt/client and wxt/sandbox no longer exist)
  • Hot reload works — npm run dev reloads content scripts and popup on save
  • MV3 service workers — background scripts are service workers (no persistent state)
  • Single-page sites — content scripts run only on full page loads; on sites like GitHub or YouTube match the whole origin and listen for wxt:locationchange with ctx.addEventListener
  • Secrets — anything in .env or the source is shipped inside the extension and readable by every user; take API keys from the user at runtime or proxy calls through your own server
  • Upgrading — install with --ignore-scripts, apply the steps in the upgrade guide, then run wxt prepare
  • Chrome Web Store API v1 stops working on 15 October 2026 — rerun wxt submit init and choose v2 (service-account auth)
  • When not to use — an existing extension with a working custom build gains little; Safari still needs Xcode on macOS
Skills enregistrées