Package Exports
- extforge
- extforge/logger
Readme
ExtForge
⚡ Lightning-fast browser extension build system with first-class HMR, interactive scaffolding, and cross-browser support.
ExtForge is a zero-config build system for Manifest V3 browser extensions. It scaffolds new projects, runs an esbuild-powered dev server with hot module reloading, generates per-browser manifests, and packages your extension for the Chrome, Firefox, Safari, and Edge stores — all from a single CLI.
npx extforge init my-extension
cd my-extension
npm run devFeatures
- 🔥 Hot reload that actually works. Edit your side panel, popup, content script, or background service worker — ExtForge rebuilds and pushes updates over WebSocket. CSS swaps without a reload; JS triggers a tab reload; manifest and background changes do a full extension reload.
- 🌐 Cross-browser by default. A single config emits a manifest tailored to Chrome, Firefox, Safari, and Edge. Browser-specific quirks (background
service_workervs.scripts,host_permissionsplacement, etc.) are handled automatically. - 📦 Manifest V3 first. Built around MV3 from day one. Service workers as ESM modules. Side panel, action popup, content scripts, and page-world injected scripts as separate entry points.
- 🚀 Powered by esbuild. Cold-start build in tens of milliseconds. Incremental rebuilds in single digits.
- ⚛️ Framework-agnostic. First-class React + Tailwind support, but you can use Vue, Svelte, Solid, or vanilla JS/TS.
- 🧱 Scaffolding.
extforge initwalks you through framework, CSS, and target-browser choices and writes a working starter. - 📋 Validation.
extforge validatechecks for common manifest mistakes, missing icons, and structural problems before they hit the store. - 🗜️ Packaging.
extforge packageproduces store-ready.ziparchives per browser.
Installation
# Per-project (recommended)
npm install -D extforge
# Global
npm install -g extforgeRequires Node.js ≥ 20.
Quick start
Scaffold a new extension
npx extforge init my-extension
cd my-extension
npm install
npm run devThe interactive prompt asks for:
- Project name
- Framework (React, Vue, Svelte, Solid, vanilla)
- CSS approach (Tailwind, vanilla, none)
- Target browsers
- Which entry points to generate (popup, side panel, content script, background, injected)
Pass --defaults to skip prompts.
Load the extension
After npm run dev, open chrome://extensions, enable Developer mode, click Load unpacked, and select dist/chrome/. (Or dist/firefox/, dist/edge/, dist/safari/.)
The dev server prints the path to load when it starts.
Project layout
ExtForge expects a conventional layout — no glue code, no entry-point manifest:
my-extension/
├── extforge.config.ts # config (browsers, manifest, framework, css)
├── icons/ # icon-16.png, icon-32.png, icon-48.png, icon-128.png
├── src/
│ ├── background.ts # service worker (MV3)
│ ├── content.ts # content script (runs in page DOM)
│ ├── injected.ts # page-world script (optional)
│ ├── injected/ # OR multiple page-world scripts
│ │ ├── feature-a.ts
│ │ └── feature-b.ts
│ ├── ui/
│ │ ├── popup/
│ │ │ ├── index.html
│ │ │ └── index.tsx # action popup
│ │ └── sidepanel/
│ │ ├── index.html
│ │ └── index.tsx # side panel
│ └── styles/
│ └── globals.css # Tailwind / global CSS
└── public/ # static assets copied verbatim to dist/Any entry point you don't need, you simply don't create. ExtForge auto-discovers what's there.
Configuration
extforge.config.ts at the project root:
import { defineConfig } from 'extforge';
export default defineConfig({
browsers: ['chrome', 'firefox', 'safari', 'edge'],
framework: 'react',
css: 'tailwind',
manifest: {
manifestVersion: 3,
name: 'My Extension',
version: '0.1.0',
description: 'Does something useful',
icons: {
'16': 'icons/icon-16.png',
'48': 'icons/icon-48.png',
'128': 'icons/icon-128.png',
},
action: {
defaultPopup: 'ui/popup/index.html',
defaultIcon: { '16': 'icons/icon-16.png', '48': 'icons/icon-48.png' },
},
background: { entrypoint: 'background/index.js' },
contentScripts: [
{ matches: ['<all_urls>'], js: ['content/index.js'], runAt: 'document_start' },
],
sidePanel: { defaultPath: 'ui/sidepanel/index.html' },
permissions: {
required: ['storage', 'activeTab', 'scripting'],
optional: [],
host: ['<all_urls>'],
},
webAccessibleResources: [
{ resources: ['injected.js'], matches: ['<all_urls>'] },
],
},
});ExtForge converts this single declaration into a per-browser manifest.json at build time, applying browser-specific transformations (e.g. background.scripts for Firefox, host_permissions placement, etc.).
CLI
extforge init [name] # Scaffold a new extension project
extforge dev # Start dev server with HMR
extforge build # Production build (all browsers)
extforge build --browser # Build a single browser target
extforge build --dev # Dev-mode build (no minify, sourcemaps)
extforge validate # Validate config and project structure
extforge package # Create dist .zip archives for stores
extforge icons # Generate PNG icons from icons/icon.svgextforge dev
Flags:
--browser <chrome|firefox|safari|edge>— target (default:chrome)--port <n>— HMR WebSocket port (default:35729; auto-bumps if busy)--host <h>— HMR host (default:localhost)
What it does:
- Reserves a free WebSocket port.
- Builds your extension once into
dist/<browser>/, embedding an HMR client into your background, popup, side panel, and content bundles. - Starts a
ws://server and a file watcher onsrc/,public/,icons/, andextforge.config.ts. - On change: rebuilds (incremental when possible) and broadcasts the update kind. CSS swaps in place; JS reloads the open tab/page; manifest or background changes call
chrome.runtime.reload().
extforge build
Default: builds all browsers in config.browsers (or all four if unset). Pass --browser <name> for a single target. --dev produces an unminified, sourcemap-included build.
extforge package
Zips each dist/<browser>/ directory into packages/<name>-<browser>-v<version>.zip. Run after extforge build.
extforge icons
Renders icons/icon.svg to PNGs at 16/32/48/128. Requires sharp-cli (preferred) or cairosvg (fallback).
How HMR works
ExtForge injects a small WebSocket client as an esbuild banner into each ESM entry (background, side panel, popup) at dev-time. When you save a file, the server:
- Classifies the change (
css/js/assets/manifest/full-reload). - Rebuilds incrementally.
- Broadcasts a JSON payload to all connected clients.
Clients react based on context:
- Service worker context — receives
full-reload/manifestand callschrome.runtime.reload(). - Window context (popup, side panel, content script) — receives
cssand swaps<link>href timestamps; receivesjsand reloads the page; receivesfull-reloadand callschrome.runtime.reload().
Notes:
- After loading the extension in your browser the first time, you must reload it from
chrome://extensionsfor the freshly bundled HMR client to start. - The injected page-world script (
src/injected.ts) is intentionally excluded from HMR client injection to keep page-world clean.
Programmatic API
ExtForge exposes its core building blocks:
import {
build, buildAll, createBuildContext,
createHMRServer,
validateProject,
generateManifest,
loadExtForgeConfig,
} from 'extforge';See src/core/index.ts for the full surface.
Example — programmatic dev server:
import { createHMRServer, loadExtForgeConfig, createLogger } from 'extforge';
const config = await loadExtForgeConfig(process.cwd());
const server = createHMRServer({
projectRoot: process.cwd(),
config,
browser: 'chrome',
port: 35729,
logger: createLogger({ scope: 'my-app' }),
});
await server.start();Comparison
| ExtForge | Plasmo | WXT | CRXJS | |
|---|---|---|---|---|
| Manifest V3 | ✅ | ✅ | ✅ | ✅ |
| Cross-browser manifest | ✅ | ✅ | ✅ | ⚠️ |
| HMR | ✅ esbuild + ws | ✅ | ✅ Vite | ✅ Vite |
| Bundler | esbuild | Parcel | Vite | Vite |
| Framework-agnostic | ✅ | ✅ | ✅ | ✅ |
| Init scaffolder | ✅ interactive | ✅ | ✅ | ❌ |
| Zero-config | ✅ | ✅ | ✅ | ❌ |
ExtForge optimizes for build speed and explicit configuration. If you prefer Vite-based tooling, WXT is excellent. If you want filesystem-based routing of entry points with magic conventions, try Plasmo. ExtForge gives you esbuild speed and a single typed config.
Contributing
PRs welcome. Please open an issue first for non-trivial changes.
git clone https://github.com/arshad-shah/extforge.git
cd extforge
pnpm install
pnpm build
pnpm testTo test against a sample project, link locally:
cd extforge && pnpm link --global
cd ../my-extension && pnpm link --global extforgeLicense
MIT © Arshad Shah