# CRXJS: Vite and Rollup plugins for Chrome extensions with real HMR

> CRXJS is a set of build plugins that turn a normal Vite or Rollup project into a Manifest V3 browser extension, including hot module replacement that survives content script reloads. It is for extension developers who already know their bundler and do not want a framework-specific scaffold.

**crxjs/chrome-extension-tools** — Build cross-browser extensions with native HMR and zero-config setup

- Repository: https://github.com/crxjs/chrome-extension-tools
- Website: https://crxjs.dev
- Stars: 4,178 · Forks: 246
- Language: TypeScript
- License: not declared
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/crxjs-chrome-extension-tools

## The manifest bookkeeping that CRXJS removes

A Manifest V3 extension is not a web app with a different entry point. The manifest declares a background service worker, content scripts, and a list of web_accessible_resources that must name every file the page is allowed to fetch from the extension origin. When a bundler hashes output filenames, those declarations have to be rewritten after every build. Doing that by hand is the part of extension work that breaks silently: the extension loads, the popup renders, and a content script fails to fetch an image that no longer exists under the name in the manifest.

CRXJS targets that gap. The README describes the plugin as zero configuration with intelligent defaults, and lists automatic generation of web_accessible_resources manifest entries as a feature. The intended user is someone who already has a Vite project and a manifest, not someone starting from a blank folder. If you want a scaffold, the README offers npm create crxjs@latest, but the plugin itself assumes you bring the manifest.

## How the Vite plugin sits between your source and the built extension

The repository is a pnpm workspace. packages/ holds the plugins, playgrounds/ holds runnable example extensions for React, Solid, Svelte, Vue and vanilla, and schema/ sits at the top level. The root package.json exposes build:vite-plugin, which delegates to packages/vite-plugin, and a set of play scripts that map one to one onto the playground directories.

The mechanism is a Vite plugin: it reads your manifest, treats the entries named there as additional build inputs, and emits a manifest whose file references match the hashed output. That is why static asset imports work in extension code, and why web_accessible_resources entries can be generated rather than listed. Hot module replacement is the second half. The README claims true HMR that preserves extension state and, in a parenthetical, says it works with content scripts. Content scripts are the hard case for any dev server, because they run in the page context rather than the extension context, so this is the claim worth checking against the playgrounds before you commit to the toolchain.

The README also notes that the plugin is built for Manifest V3 and directs anyone who needs MV2 to the rollup-plugin package. The two packages are separate, and that split is the clearest statement of scope in the repository.

## Installing CRXJS and running a first extension

The README gives one entry point for a new project. The @latest tag is called out in a warning block, because npm may otherwise resolve a cached and outdated version.

```bash
npm create crxjs@latest
```

After the scaffold exists, the plugin is installed as a normal dev dependency and registered in the Vite config. The README does not print that config block, and the repository files do not show one either, so there is no snippet to copy here. What the README does print is the repository's own development flow, and that is the only sequence of commands it commits to.

```bash
pnpm install
pnpm build:vite-plugin
pnpm play
```

pnpm play is an alias for the vanilla playground; the root package.json also defines play:react, play:solid, play:svelte and play:vue for the other framework examples. What you should see is the playground extension building and a dev server starting. Load the unpacked output from chrome://extensions with developer mode on, and the extension should appear without a manual rebuild step.

Tests live in packages/vite-plugin and run from that directory with the command the README gives.

```bash
cd packages/vite-plugin
pnpm run test
```

## Where the plugin stops being the right tool

The README is explicit that MV2 support lives in the rollup-plugin package, not the Vite one. An extension that still ships a background page rather than a service worker is outside the advertised target of the vite-plugin, and the README does not describe a migration path between the two packages.

The zero-config claim has a boundary too. The plugin generates web_accessible_resources entries and handles static asset imports, but it does not author your manifest for you, and the README does not document rollback behaviour for a generated manifest that turns out wrong. There is also no published licence in the repository metadata I can see, which matters if you are vendoring the plugin rather than installing it from npm.

Finally, HMR in a browser extension is a development convenience with a real cost: the dev server and the extension have to agree on how modules are addressed, and a content script that runs in a page you do not control is the least predictable place for that. The README asserts it works there. It does not describe the failure modes when it does not.

## CRXJS against writing the manifest by hand or using a framework scaffold

The real alternative is not another plugin. It is a plain Vite build plus a hand-maintained manifest, or a framework-specific extension starter that bundles its own build pipeline.

Hand-rolling keeps you on Vite's normal output naming, which means either giving up content hashes in filenames or writing a small script that rewrites the manifest after each build. That script is the thing CRXJS replaces, and for a small extension with one content script and no dynamic assets it is maybe thirty lines. The trade is control: you know exactly which files land in web_accessible_resources because you wrote the list.

A framework scaffold makes the opposite trade. It picks your UI library, your routing and your build layout, and you inherit its upgrade cadence alongside the extension logic. CRXJS stays at the bundler layer, which is why the same repository ships playgrounds for React, Solid, Svelte, Vue and vanilla rather than a single opinionated template. If you have already chosen a UI stack and a Vite config, the plugin slots underneath both.

## Release cadence, licence and what an upgrade costs

The repository is not archived, and the last push was on 2026-09-13, so work is recent. Recent releases are all on the vite-plugin line: vite-plugin-v2.5.0 and vite-plugin-v2.6.1 on 2026-06-11, and vite-plugin-v2.7.0 on 2026-06-19. The root package.json pins pnpm@10.11.1 as the package manager and declares node >=14 in engines, which is a floor rather than a recommendation.

Versioning is handled with changesets. The .changeset/ directory and the release script, which runs a build filtered to plugin packages and then changeset publish, mean upgrade notes are generated from the changeset files rather than written by hand. RELEASES.md at the top level is where that history lands. For an extension author, the practical cost of an upgrade is re-checking that the generated manifest still matches what your extension expects, because that file is the plugin's output and not something you edit.

The repository metadata does not state a licence. The README asks for sponsorship and links a Discord server, but neither substitutes for a licence file. If you are shipping a commercial extension, resolve that before you depend on the package.

## Conclusion

Adopt CRXJS if you already build with Vite and want HMR in background and content scripts without hand-writing manifest entries for every asset. Do not adopt it if you need Manifest V2, since the README points MV2 users to the separate rollup-plugin package, or if you want a framework scaffold rather than a plugin you wire into an existing Vite config. Verify first that the vite-plugin version you install matches the manifest version you target, and read RELEASES.md for the changeset entries that describe what moved between vite-plugin 2.5.0, 2.6.1 and 2.7.0 before upgrading a working extension.

## FAQ

### Does CRXJS support Manifest V2?

The Vite plugin is built for Manifest V3. The README directs anyone who needs MV2 support to the separate rollup-plugin package in the same repository.

### How do I create a new CRXJS project?

The README gives npm create crxjs@latest as the entry point, and warns that @latest must not be omitted because npm may otherwise resolve a cached and outdated version of the package.

### Does CRXJS hot module replacement work with content scripts?

The README lists true hot module replacement that preserves extension state and adds that it works with content scripts. It does not describe what happens when that replacement fails.

### What licence does CRXJS use?

The repository metadata provided does not state a licence. The README links documentation, Discord and a sponsorship page but does not name licence terms.

### How do I run the CRXJS playgrounds and tests?

Clone the repository, install pnpm, run pnpm install, then pnpm build:vite-plugin. The playgrounds live under playgrounds/ and run with pnpm play; tests run with pnpm run test from inside packages/vite-plugin.

## Sources

- [crxjs/chrome-extension-tools on GitHub](https://github.com/crxjs/chrome-extension-tools)
- [Issues](https://github.com/crxjs/chrome-extension-tools/issues)
- [Project website](https://crxjs.dev)
- [README](https://github.com/crxjs/chrome-extension-tools/blob/main/README.md)
- [Releases](https://github.com/crxjs/chrome-extension-tools/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/crxjs-chrome-extension-tools
