CRXJS: a Vite plugin for Manifest V3, with Manifest V2 moved to another package
Build cross-browser extensions with native HMR and zero-config setup
At a glance
- What is it?
- CRXJS builds browser extensions through Vite with hot module replacement and generated web_accessible_resources, and its scaffolding command only works if you keep the @latest tag. The monorepo around it is pinned to older tooling than the extensions it produces, and it names two different pnpm versions in the same manifest.
- Who is it for?
- Use CRXJS if you are building a Manifest V3 extension and you want your content scripts to hot reload without losing extension state, and if you are willing to accept that Manifest V2 lives in a separate rollup plugin package. Run the scaffolder exactly as written, because dropping the @latest tag is the one step that fails quietly.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Dropping @latest is the one step that fails quietly
Scaffolding a project is one command.
npm create crxjs@latestThe README attaches an IMPORTANT note to it: @latest MUST NOT be omitted, because without the tag npm may resolve to a cached and outdated version of the package. Nothing in the output distinguishes the two cases. You get a project either way, built from a scaffolder that may be several releases behind, and the tag is the only thing telling npm to check.
That is worth internalising as a rule rather than a quirk. A scaffolding tool is pinned by whatever your npm cache happens to hold, and a cache that survived from an earlier project is the normal case on a developer machine that has built extensions before.
The package that command fetches is published to npm under the @crxjs scope, and the documentation site is crxjs.dev, which is also the homepage recorded in the manifest.
Manifest V2 support lives in a different package
The Vite plugin is Manifest V3 only. The note about where to go if you need MV2 points into the repository rather than at a separate site.
packages/rollup-plugin/README.mdSo the same project maintains two extension builders, one per bundler, and the split follows the manifest generation each one supports rather than a deprecation schedule. MV3 is what the Vite plugin targets, described as built for modern Chrome extensions with enhanced security.
What the Vite plugin adds on top of the bundler is worth being precise about, because three of the six feature claims are about work the plugin does to your manifest rather than to your code. web_accessible_resources entries are generated automatically, so you do not maintain that list by hand. Static assets are imported directly, meaning images and fonts are referenced in code rather than copied into a public directory. And hot module replacement is described as preserving extension state, with a specific claim that it works with content scripts, which is the part that changes how you debug a content script.
The manifest names two different pnpm versions
The root package.json of the monorepo declares the package manager field and then, separately, lists pnpm as an ordinary dependency at a different version.
"packageManager": "[email protected]",
"pnpm": "10.34.5"Two mechanisms, two versions, one repository. The packageManager field is what Corepack and modern tooling read to pin the toolchain, and it says 10.11.1. The dependency entry is what gets resolved into the lockfile, and it says 10.34.5. Whichever one your environment honours, the two are not the same, so a contributor and a CI run can disagree about which pnpm executed a script.
The rest of the root manifest follows the same pattern of a shared toolchain rather than per-package versions. Everything sits at the workspace root: eslint at 9.26.0, the TypeScript parser and plugin at 8.32.1, prettier at 2.x, typescript at 4.6, plus npm-run-all and eslint-plugin-react. A monorepo that shares one linter and one formatter across all of its packages is a reasonable choice, and it also means a single version bump touches every package.
Five playgrounds, and the default is the vanilla one
The playgrounds directory holds a working project per framework, and each has its own script at the root.
"play": "pnpm run play:vanilla",
"play:react": "pnpm -C playgrounds/react run dev",
"play:solid": "pnpm -C playgrounds/solid run dev",
"play:svelte": "pnpm -C playgrounds/svelte run dev",
"play:vue": "pnpm -C playgrounds/vue run dev",Running pnpm play starts the vanilla playground and nothing else, which is the smallest thing that proves the plugin works. Each of the other four is the same dev script pointed at a different directory, so testing the plugin against a framework is a matter of choosing the script rather than configuring anything.
The development instructions follow the same shape. Install pnpm, run pnpm install, build the plugin with pnpm build:vite-plugin, and then run the tests from inside packages/vite-plugin with pnpm run test. Building before running a playground matters, because the playground dev servers consume the plugin's built output rather than its source.
There is also a documentation entry point outside the site: the README points at a DeepWiki page for the repository as a way to learn more about how CRXJS is put together.
Two release tools are installed, one is wired up
The release script builds every package whose name matches a pattern and then hands off to Changesets.
"release": "pnpm --filter \"*plugin*\" build && changeset publish",
"release:vite-plugin": "cd ./packages/vite-plugin && pnpm release"The filter is what keeps the release scoped: anything named like a plugin gets built before publishing, and the rest of the workspace is left alone.
Alongside that, the development dependencies still include bumpp, which is the older increment-based release tool. It is not referenced by either release script. So the repository carries two ways to cut a release, one wired into the root script and one left in the dependency list, and only the Changesets path also produces the changelog entries that the .changeset directory collects.
Published tags follow the package, not the repository. The recent releases are named vite-plugin-v2.7.0, vite-plugin-v2.6.1 and vite-plugin-v2.5.0, and two of those were published on the same day, June 11, 2026. That granularity is the reason the release script filters by package name at all.
The shared toolchain is older than what it builds
The versions in the root manifest are the practical constraint for anyone hacking on the repository.
Prettier is pinned to the 2.x line with a separate jsdoc plugin at 0.4.2, TypeScript is at ^4.6.4, and the test runner in development is vitest at 0.34.6. The engine floor is low.
"engines": {
"node": ">=14"
}None of that blocks work. It does mean a contributor arriving from a current project has a different baseline than they expect: a two-major-version-old formatter, a TypeScript well behind what a new Vite plugin template ships, and a test runner that predates the current Vite releases. Two of the recent releases landed on the same day in June 2026, so the toolchain is not being held back by release pressure.
Repository maintenance is delegated rather than manual. renovate.json is present for dependency updates, and formatting rules live in .prettierrc.yaml with a .prettierignore, so the formatting baseline is shared by every package in the workspace.
One dependency is allowed to run an install script
The workspace pnpm settings say what the build is allowed to execute.
"onlyBuiltDependencies": [
"playwright-chromium"
]That is the whole list. Everything else is installed with its lifecycle scripts disabled, which is the default the tool applies and the reason this list has to be explicit. Playwright's Chromium download is allowed through, which tells you something about the test setup: the playgrounds are exercised against a real browser rather than a stub.
The other pnpm setting relaxes peer dependency resolution in a narrower way.
"peerDependencyRules": {
"ignoreMissing": [
"jest"
]
}Jest is declared as ignorable when it is missing, so something in the dependency graph still names it as a peer without requiring it. For an extension toolchain built around Vitest, that is a leftover rather than a requirement.
Two top level directories are also easy to miss. schema/ holds schema files, and design/ holds design assets, alongside .agents/ and a skills-lock.json. The repository is organised for agents as well as for people, and the README's own pointer for understanding the architecture is a generated wiki rather than a document in the tree.
Editorial conclusion
Use CRXJS if you are building a Manifest V3 extension and you want your content scripts to hot reload without losing extension state, and if you are willing to accept that Manifest V2 lives in a separate rollup plugin package. Run the scaffolder exactly as written, because dropping the @latest tag is the one step that fails quietly. Before you build on the repository itself, know what you are adopting: the manifest pins pnpm twice with two different versions, keeps Prettier 2 and TypeScript 4.6 as the shared toolchain, installs two release tools when only one is wired to the release script, and allows exactly one dependency to run an install script. Contributors should work in packages/vite-plugin and drive the playgrounds rather than adding a new build path at the root.
Frequently asked questions
How do I create a new CRXJS project?
Run npm create crxjs@latest. The README insists that the @latest tag must not be omitted, because without it npm may resolve to a cached and outdated version of the package and give you no indication that it did.
Does CRXJS support Manifest V2 extensions?
Not through the Vite plugin, which targets Manifest V3. If you need MV2, the README points to the rollup-plugin package in the same repository, packages/rollup-plugin.
What does CRXJS automate about the extension manifest?
It generates the web_accessible_resources entries automatically, and lets you import static assets such as images and fonts directly from code instead of copying them into a public directory.
How do I build and test the CRXJS vite plugin locally?
Install pnpm, run pnpm install, build with pnpm build:vite-plugin, then move into packages/vite-plugin and run pnpm run test. The playgrounds under playgrounds/ are built by that step, since they consume the plugin's output.
Which frameworks do the CRXJS playgrounds cover?
There is one playground per framework: vanilla, react, solid, svelte and vue. Each has its own script, and pnpm play on its own starts the vanilla one.
How are CRXJS releases published?
The release script builds every package whose name matches *plugin* and then runs changeset publish. Tags are named per package, such as vite-plugin-v2.7.0, so only the plugin packages are versioned by the root release command.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/crxjs-chrome-extension-tools)