webpack-ext-reloader-next
v1.0.2
Published
Webpack extension reloader for Chrome, Firefox, and Edge. Auto-reloads your browser extension on rebuild. MV3-ready successor to webpack-ext-reloader.
Downloads
367
Maintainers
Readme
Why this exists
The original webpack-ext-reloader is unmaintained and broken on Manifest
V3 — its WebSocket dies the moment Chrome puts the service worker to
sleep, and it doesn't reconnect. This package is the modern successor.
It picks a port, watches your build, and reloads the extension over a self-healing WebSocket that survives service-worker eviction. No configuration required for the common case.
Install
pnpm add -D webpack-ext-reloader-next
# or: npm i -D webpack-ext-reloader-next
# or: yarn add -D webpack-ext-reloader-nextQuick start
Drop one line into your webpack config. Pick the form that matches your setup:
CommonJS (webpack.config.js or .cjs):
const { ExtReloader } = require("webpack-ext-reloader-next");
module.exports = {
mode: "development",
// ...your config
plugins: [new ExtReloader()],
};ES Modules (webpack.config.mjs, or with "type": "module"):
import { ExtReloader } from "webpack-ext-reloader-next";
export default {
mode: "development",
// ...your config
plugins: [new ExtReloader()],
};TypeScript (webpack.config.ts):
import { ExtReloader, type ExtReloaderOptions } from "webpack-ext-reloader-next";
import type { Configuration } from "webpack";
const config: Configuration = {
mode: "development",
// ...your config
plugins: [new ExtReloader()],
};
export default config;The default export is also available if you prefer
import ExtReloader from "webpack-ext-reloader-next".
That's it. The plugin reads your manifest.json, infers your background
/ content / popup entries, and starts a reload server on a free port
starting at 9012. Run webpack --watch, load the unpacked build into
Chrome, and edit a file — the extension reloads on its own.
The plugin is automatically a no-op when
mode === "production".
What it does on each kind of change
| You edited | What happens |
|------------|--------------|
| manifest.json | Full extension reload (chrome.runtime.reload) |
| _locales/** | Full extension reload |
| Background service worker | Full extension reload |
| Popup / options / devtools page | Reload the open extension page only |
| Content script JS | Reload tabs matching the script's matches |
| Content script or page CSS | Hot-swap the <link> tag — no reload |
| Static asset (image, etc.) | Nothing (just rebuild) |
The CSS hot-swap is the one feature no other webpack extension reloader
ships. You edit a content-script stylesheet, the matching <link> is
swapped out with a cache-buster, and your Gmail (or whatever) tab keeps
its scroll position and form state.
Migrating from webpack-ext-reloader
If you're already using the original webpack-ext-reloader (or
webpack-extension-reloader, or @reorx/webpack-ext-reloader),
migration is mostly a rename.
1. Swap the dependency:
pnpm remove webpack-ext-reloader
pnpm add -D webpack-ext-reloader-next2. Update the import — ExtensionReloader → ExtReloader:
- const ExtensionReloader = require("webpack-ext-reloader");
+ const { ExtReloader } = require("webpack-ext-reloader-next");
module.exports = {
plugins: [
- new ExtensionReloader({
- port: 9090,
- reloadPage: true,
- entries: {
- contentScript: "content",
- background: "background",
- extensionPage: "popup",
- },
- }),
+ new ExtReloader(),
],
};The entries mapping is no longer required — the plugin reads your
manifest.json and infers them. You can still pass entries to
override.
3. Delete any manual chrome.runtime.reload() workaround you added
because the old plugin's WebSocket died on MV3. The new client
reconnects on every Chrome event, so the SW eviction cycle is handled
automatically.
Option name compatibility:
| webpack-ext-reloader option | webpack-ext-reloader-next |
|-------------------------------|------------------------------|
| port | port (default 9012, auto-increments) |
| reloadPage | reloadPage (same default) |
| entries | entries (now optional) |
| manifest | manifest (now optional) |
| — | keepAliveInDev (new, default true) |
| — | notifications.* (new) |
| — | logLevel (new) |
That's the whole migration for the 90% case.
Manifest V3 specifics
- Service worker reconnection — clients reconnect on every Chrome event so the WebSocket survives the SW eviction cycle.
- No
eval— nothing in the injected client violates the MV3 CSP. - Smart keep-alive — opt-in
chrome.alarmsping so the SW never sleeps in dev (keepAliveInDev: true, on by default). - Cross-browser — Chrome, Firefox, and Edge all supported via the
webextension-polyfilltypes.
Demo
Three MV3 demo extensions live under packages/demo/ —
one per target browser. They're identical except for the manifest:
packages/demo/chrome— plainbackground.service_worker. Load viachrome://extensions→ "Load unpacked" →packages/demo/chrome/dist.packages/demo/edge— same manifest as Chrome. Load viaedge://extensions→ "Load unpacked" →packages/demo/edge/dist.packages/demo/firefox—service_worker+scriptsfallback +browser_specific_settings.gecko.id. Load viaabout:debugging#/runtime/this-firefox→ "Load Temporary Add-on" →packages/demo/firefox/dist/manifest.json.
git clone https://github.com/graybearo/webpack-ext-reloader-next
cd webpack-ext-reloader-next
pnpm install
pnpm --filter chrome-demo dev # or firefox-demo / edge-demoFirefox MV3 still gates background.service_worker behind the
extensions.backgroundServiceWorker.enabled pref, so any Firefox MV3
manifest also needs a background.scripts fallback — that's what the
firefox-demo shows.
Standalone CLI (no webpack)
If you build with something other than webpack — esbuild, rollup, a custom script — you can still use the same reload protocol via the CLI. It watches your extension's output directory, classifies changes, and broadcasts to clients you've added to your extension.
npx webpack-ext-reloader-next --dist ./distOptions:
-p, --port <number> WebSocket port (default: auto from 9012)
--manifest <path> Path to manifest.json (default: probe in --dist)
-d, --dist <path> Directory to watch (default: ./dist)
-h, --help Show helpThe CLI runs the same diff classifier as the webpack plugin, so reload
behavior is identical. You're responsible for getting the client scripts
into your extension build (see packages/plugin/src/client/ for the
sources).
OS notifications
Optional. If you npm install node-notifier (an optionalDependency),
the plugin fires OS-level notifications from the dev server, useful
when you're in another window and the build breaks:
'errors'(default) — first error after a streak of successes, and on recovery.'all'— every reload + every error.false— disabled.
new ExtReloader({ notifications: { osNotifications: "errors" } });If node-notifier isn't installed (unavailable on some CI environments),
the plugin silently skips OS notifications. Everything else works
normally.
We deliberately don't ship a
chrome.notificationspath. It would require consumers to add the"notifications"permission to their manifest just for a dev-time feature, which is poor practice. The action badge, in-page toast, and full-screen error overlay already cover browser-side feedback without permission pollution.
Compared to alternatives
| Feature | this | webpack-ext-reloader | @reorx/webpack-ext-reloader | webpack-extension-reloader-v3-manifest |
|---|:---:|:---:|:---:|:---:|
| MV3 service worker reconnection | ✅ | ❌ | ⚠️ partial | ⚠️ partial |
| Smart routing (per-entry reload) | ✅ | ⚠️ basic | ⚠️ basic | ⚠️ basic |
| CSS hot-swap (no reload) | ✅ | ❌ | ❌ | ❌ |
| Build-error overlay | ✅ | ❌ | ❌ | ❌ |
| Zero-config | ✅ | ❌ | ❌ | ❌ |
| Last release | active | 2024 | 2023 | 2023 |
Honest comparison, not marketing — these are all real plugins solving the same problem.
Options
Every option is optional. The defaults work for most projects.
| Option | Type | Default | Notes |
|--------|------|---------|-------|
| manifest | string | auto | Path to manifest.json. Probes ./manifest.json, ./src/manifest.json, ./public/manifest.json, ./static/manifest.json. |
| entries | object | auto | Override entry detection. Shape: { background, contentScript, extensionPage }. |
| port | number | 9012 | WebSocket port. Auto-increments if taken. |
| maxRetries | number | 50 | How many times the SW retries when the dev server is unreachable. -1 = retry forever. Retries are silent (HTTP-probed before each WS attempt), so a high cap doesn't pollute the console. |
| reloadPage | boolean | true | Reload host tabs when content scripts change. |
| keepAliveInDev | boolean | true | Use chrome.alarms to keep the MV3 SW alive in dev. |
| notifications.toast | boolean | true | Show a small "reloaded" toast in host pages. |
| notifications.errorOverlay | boolean | true | Render a full-screen overlay on build errors. |
| notifications.osNotifications | 'all' \| 'errors' \| false | 'errors' | OS-level notifications. |
| notifications.badge | boolean | true | Color-code the extension's action badge by WS state. |
| logLevel | 'silent' \| 'normal' \| 'verbose' | 'normal' | Terminal output verbosity. |
| force | boolean | false | Run even when mode === "production". |
Roadmap
v0.1 ships everything below.
- [x] WebSocket server with auto-port selection (starts at 9012)
- [x] Resilient client with exponential-backoff reconnection
- [x] Auto-config: reads
manifest.json, no entry mapping required - [x] Smart reload classifier (manifest / background / page / tabs / CSS)
- [x] CSS hot-swap — no reload, no state loss
- [x] Build-error overlay in content scripts and pages
- [x] Reload toast on host pages
- [x] Action-badge state indicator (green / yellow / red)
- [x] Manifest watcher — picks up changes outside the webpack graph
- [x] Dual ESM + CJS + TypeScript webpack configs
- [x] OS notifications (
node-notifieras an optional peer dep) - [x] Standalone CLI (
npx webpack-ext-reloader-next) for non-webpack toolchains
Future:
- [ ] Multi-compiler support
- [ ] rspack / vite plugin parity
Contributing
PRs welcome. See CONTRIBUTING.md for setup, commit conventions (we use Conventional Commits + semantic-release, so commit type matters), and the code style.
License
MIT — see LICENSE.
