npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-next

Quick 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

  1. Webpack finishes a build. The plugin diffs the emitted assets against the previous build.
  2. The diff classifies the change — manifest, background, content JS, content CSS, page, locale, or static asset.
  3. A typed message goes out over WebSocket (ws://localhost:9012) to every connected client. Schemas live in shared/messages.ts.
  4. Each client takes the minimum action: background reloads the extension, content scripts reload their host tabs, CSS swaps in place — no full reload.
  5. 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-next

2. Update the import — ExtensionReloaderExtReloader:

- 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.alarms ping so the SW never sleeps in dev (keepAliveInDev: true, on by default).
  • Manifest-declared CSS hot-swap — uses chrome.scripting.insertCSS to re-inject content_scripts[].css in place. Requires host_permissions covering the page URL (or activeTab); falls back to a tab reload if neither is granted.
  • Cross-browser — Chrome, Firefox, and Edge all supported via the webextension-polyfill types.

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 — plain background.service_worker. Load via chrome://extensions → "Load unpacked" → packages/demo/chrome/dist.
  • packages/demo/edge — same manifest as Chrome. Load via edge://extensions → "Load unpacked" → packages/demo/edge/dist.
  • packages/demo/firefoxservice_worker + scripts fallback + browser_specific_settings.gecko.id. Load via about: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-demo

Firefox 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 ./dist

Options:

-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 help

The 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.notifications path. 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-notifier as 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.