webpack-ext-reloader-next
v1.0.4
Published
Webpack extension reloader for Chrome, Firefox, and Edge. Auto-reloads your browser extension on rebuild. MV3-ready successor to webpack-ext-reloader.
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".
Multiple content scripts or pages? Pass entries arrays — the inferred
defaults assume one of each:
new ExtReloader({
entries: {
contentScript: ["gmail-content", "inbox-content"],
extensionPage: ["popup", "options"],
},
});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 in place — no reload |
| Static asset (image, etc.) | Nothing (just rebuild) |
The CSS hot-swap is the one feature no other webpack extension reloader
ships. Edit a stylesheet — whether it's import-ed from JS (cloned
<link> with a cache-buster) or declared in manifest.json under
content_scripts[].css (re-injected via chrome.scripting.insertCSS) —
and your Gmail (or whatever) tab keeps its scroll position and form
state.
How it works
- Webpack finishes a build. The plugin diffs the emitted assets against the previous build.
- The diff classifies the change — manifest, background, content JS, content CSS, page, locale, or static asset.
- A typed message goes out over WebSocket (
ws://localhost:9012) to every connected client. Schemas live inshared/messages.ts. - Each client takes the minimum action: background reloads the extension, content scripts reload their host tabs, CSS swaps in place — no full reload.
- The service worker reconnects on every Chrome event, so SW eviction never breaks the connection.
The protocol is bundler-agnostic. Today it ships with a webpack 5 plugin and a standalone CLI; rspack and Vite are on the roadmap.
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). - Manifest-declared CSS hot-swap — uses
chrome.scripting.insertCSSto re-injectcontent_scripts[].cssin place. Requireshost_permissionscovering the page URL (oractiveTab); falls back to a tab reload if neither is granted. - Cross-browser — Chrome, Firefox, and Edge all supported via the
webextension-polyfilltypes.
Try the demo extensions
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". |
Troubleshooting
Port 9012 already in use. The plugin auto-increments to the next
free port and prints the chosen one on startup. Pin a port with
new ExtReloader({ port: 9050 }) if you need it stable.
WebSocket gets CORS-blocked from the service worker. Your manifest's
host_permissions doesn't include http://localhost:*/*. Add it for
dev builds only:
host_permissions: process.env.IS_LOCAL
? ["http://localhost:*/*", "http://127.0.0.1:*/*", ...]
: [...]The omission is the most common reason the SW never connects in custom
extensions — declaring strict host_permissions without localhost
trips Chrome's CORS check on the WebSocket handshake.
CSS edits don't apply visibly. If your CSS is declared in
manifest.json under content_scripts[].css, the plugin re-injects via
chrome.scripting.insertCSS. That needs either host_permissions
covering the page URL or activeTab. Without one of those, the swap is
a no-op and you'll see a tab reload instead — still visible, just less
smooth.
Service worker keeps disconnecting. Default is
keepAliveInDev: true (uses chrome.alarms) — turn it off if it's
noisy in your console. Otherwise check that the manifest still grants
alarms permission.
Firefox MV3 doesn't load the service worker. Firefox gates
background.service_worker behind the
extensions.backgroundServiceWorker.enabled pref. Add
background.scripts as a fallback alongside service_worker — see
packages/demo/firefox/src/manifest.json.
Webpack config edits don't propagate. The reloader watches built
assets, not the webpack config itself. Stop and restart webpack --watch
after editing webpack.config.*.
FAQ
Does this support true HMR (state preservation across reloads)?
No. MV3's CSP forbids eval, and webpack's HMR pipeline depends on it.
Reloads are fast enough that HMR isn't worth the maintenance cost. The
one exception is CSS, which we hot-swap without a reload.
Manifest V2 support? No. MV3-only. MV2 is on Chrome's deprecation timeline and we don't carry the weight of supporting both.
Why webpack 5 only — what about rspack or Vite? That's where the demand is today. The protocol is bundler-agnostic, so rspack/Vite parity is a refactor, not a rewrite — on the roadmap once there's a real user asking.
Will this run in production builds?
No. The plugin auto-disables when mode === "production". Use
force: true if you really must (you don't).
Why no chrome.notifications for in-extension dev alerts?
It would force consumers to add the "notifications" permission to
their manifest just for a dev-time feature. The action badge, in-page
toast, and full-screen error overlay cover the same ground without
permission pollution.
Does it conflict with webpack-dev-server?
No — they don't overlap. webpack-dev-server serves a web app over
HTTP; this plugin runs a tiny WebSocket alongside webpack --watch
specifically for extension reload. You generally won't run both.
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.
