CRXJS
CRXJS is a Chrome extension development tool that provides true HMR for popup, options, content scripts, and side panels. It reads your manifest to auto-generate the extension output, handles content script injection, and manages the service worker build. Under the hood it is a Vite plugin (@crxjs/vite-plugin).
Current status
- - Package:
@crxjs/vite-plugin (v2.x stable, latest v2.4.0 as of March 2026) - Scaffolding:
npm create crxjs@latest (always use @latest) - Maintained by: @Toumash and @FliPPeDround (since mid-2025)
- GitHub: github.com/crxjs/chrome-extension-tools (~4k stars)
- Vite compatibility: v3 through v8-beta
Quick start
CODEBLOCK0
Vite config by framework
CRXJS is added as a Vite plugin. The setup varies slightly per framework.
React
CODEBLOCK1
Use @vitejs/plugin-react (not plugin-react-swc) for best HMR compatibility. If you must use SWC, cast the manifest:
CODEBLOCK2
Vue
CODEBLOCK3
Svelte
CODEBLOCK4
Vanilla TypeScript
CODEBLOCK5
defineManifest — type-safe dynamic manifest
Instead of a static JSON file, use CRXJS's defineManifest for dynamic values and full TypeScript autocompletion:
CODEBLOCK6
Import in vite.config.ts:
CODEBLOCK7
Type declarations
Add to a src/vite-env.d.ts or src/crxjs.d.ts:
CODEBLOCK8
This enables types for ?script and ?script&module imports.
HMR behavior by context
| Context | HMR | How it works |
|---|
| Popup | Full HMR | WebSocket-based, state preserved |
| Options page |
Full HMR | Same as popup |
| Side panel | Full HMR | Same as popup |
| Content script (manifest) | True HMR | CRXJS injects loader + HMR client |
| Content script (dynamic) | True HMR | Via
?script import |
| Service worker | Auto-reload | Changes trigger full extension reload |
| Main world scripts | No HMR | Skipped by CRXJS loader |
Content script HMR works because CRXJS generates a loader script that imports an HMR preamble, the HMR client, and your actual script — enabling real module-level HMR without full page reload. This is CRXJS's main differentiator.
Dynamic content script imports
For content scripts injected programmatically (not in manifest), CRXJS provides special import suffixes:
CODEBLOCK9
For main world injection (no HMR):
CODEBLOCK10
CRXJS plugin options
CODEBLOCK11
Development workflow
CODEBLOCK12
After loading once, subsequent npm run dev sessions reconnect automatically. No need to re-load the extension unless manifest.json changes.
Production build
CODEBLOCK13
The dist/ directory is ready to zip and upload to Chrome Web Store:
CODEBLOCK14
Disable Vite's module preload to avoid CWS rejection of inline scripts:
CODEBLOCK15
Known issues and workarounds
Tailwind CSS HMR in content scripts
New Tailwind classes may not trigger CSS updates in content scripts. Workaround: restart dev server after adding new utility classes. Improved in v2.4.0 but not fully resolved. Ensure injectCss: true in config.
WebSocket connection errors (ws://localhost:undefined/)
Cause: port mismatch between dev server and HMR config. Fix: explicitly set both to the same value:
CODEBLOCK16
"Manifest version 2 is deprecated" warning
If you see this, your manifest is being interpreted as MV2. Fix: ensure "manifest_version": 3 is set.
Content scripts not injecting on file:// URLs
Chrome requires the user to enable "Allow access to file URLs" in the extension settings at chrome://extensions. CRXJS cannot change this.
HMR stops working after Chrome update
CRXJS's HMR relies on injecting a content script that connects to the dev server's WebSocket. Chrome security updates occasionally break this. Fix: update to the latest CRXJS version, which tracks Chrome changes.
CRXJS vs alternatives
| Feature | CRXJS | WXT | Plasmo |
|---|
| Content script HMR | True HMR | File-based reload | Partial |
| Framework support |
Any Vite framework | Any | React-focused |
| Abstraction level | Thin (Vite plugin) | Full framework | Full framework |
| Messaging helpers | None (use chrome.\* directly) | Built-in | Built-in |
| Storage wrappers | None | Built-in | Built-in |
| Cross-browser | Chrome + Firefox | Chrome + Firefox + Safari | Chrome + Firefox |
| File-based routing | No | Yes | Yes |
| Learning curve | Low (know Vite, know CRXJS) | Medium | Medium |
Choose CRXJS when: you want minimal abstraction over raw Chrome APIs and value content script HMR above all. CRXJS stays out of the way — no magic routing, no wrapper APIs, just your code with HMR.
Choose WXT when: you want conventions, built-in utilities, and cross-browser support.
Choose Plasmo when: you're React-focused and want the highest-level abstraction.
Project structure (recommended)
CODEBLOCK17
CRXJS resolves HTML files referenced in the manifest automatically. Your popup.html can use standard <script type="module" src="./main.tsx"> and it works.
If you encounter a bug or unexpected behavior in CRXJS, open an issue at github.com/crxjs/chrome-extension-tools/issues.
CRXJS
CRXJS 是一款 Chrome 扩展开发工具,为弹出窗口、选项页、内容脚本和侧边栏提供真正的 HMR(热模块替换)。它能读取你的清单文件自动生成扩展输出,处理内容脚本注入,并管理工作线程的构建。底层是一个 Vite 插件(@crxjs/vite-plugin)。
当前状态
- - 包名:@crxjs/vite-plugin(v2.x 稳定版,截至 2026 年 3 月最新版本为 v2.4.0)
- 脚手架:npm create crxjs@latest(始终使用 @latest)
- 维护者:@Toumash 和 @FliPPeDround(自 2025 年中起)
- GitHub:github.com/crxjs/chrome-extension-tools(约 4k 星标)
- Vite 兼容性:v3 到 v8-beta
快速开始
bash
脚手架新项目(交互式选择框架)
npm create crxjs@latest
或添加到现有 Vite 项目
npm install @crxjs/vite-plugin -D
按框架分类的 Vite 配置
CRXJS 作为 Vite 插件添加。不同框架的配置略有差异。
React
typescript
// vite.config.ts
import { defineConfig } from vite;
import react from @vitejs/plugin-react;
import { crx } from @crxjs/vite-plugin;
import manifest from ./manifest.json;
export default defineConfig({
plugins: [react(), crx({ manifest })],
});
使用 @vitejs/plugin-react(而非 plugin-react-swc)以获得最佳 HMR 兼容性。如果必须使用 SWC,请对清单进行类型转换:
typescript
import { ManifestV3Export } from @crxjs/vite-plugin;
const manifest = manifestJson as ManifestV3Export;
Vue
typescript
import vue from @vitejs/plugin-vue;
import { crx } from @crxjs/vite-plugin;
import manifest from ./manifest.json;
export default defineConfig({
plugins: [vue(), crx({ manifest })],
});
Svelte
typescript
import { svelte } from @sveltejs/vite-plugin-svelte;
import { crx } from @crxjs/vite-plugin;
import manifest from ./manifest.json;
export default defineConfig({
plugins: [svelte(), crx({ manifest })],
});
原生 TypeScript
typescript
import { crx } from @crxjs/vite-plugin;
import manifest from ./manifest.json;
export default defineConfig({
plugins: [crx({ manifest })],
});
defineManifest — 类型安全的动态清单
使用 CRXJS 的 defineManifest 替代静态 JSON 文件,支持动态值和完整的 TypeScript 自动补全:
typescript
// manifest.ts
import { defineManifest } from @crxjs/vite-plugin;
import pkg from ./package.json;
export default defineManifest((config) => ({
manifest_version: 3,
name: config.command === serve ? [DEV] ${pkg.name} : pkg.name,
version: pkg.version,
description: pkg.description,
permissions: [storage, activeTab, scripting],
action: {
default_popup: src/popup/index.html,
default_icon: {
16: public/icons/icon16.png,
48: public/icons/icon48.png,
},
},
background: {
service_worker: src/background/index.ts,
type: module,
},
content_scripts: [
{
matches: [https:///],
js: [src/content/index.ts],
css: [src/content/styles.css],
},
],
options_page: src/options/index.html,
sidepanel: { defaultpath: src/sidepanel/index.html },
icons: {
16: public/icons/icon16.png,
48: public/icons/icon48.png,
128: public/icons/icon128.png,
},
}));
在 vite.config.ts 中导入:
typescript
import manifest from ./manifest;
// ... crx({ manifest })
类型声明
添加到 src/vite-env.d.ts 或 src/crxjs.d.ts:
typescript
///
这为 ?script 和 ?script&module 导入启用了类型支持。
按上下文的 HMR 行为
| 上下文 | HMR | 工作原理 |
|---|
| 弹出窗口 | 完整 HMR | 基于 WebSocket,状态保持 |
| 选项页 |
完整 HMR | 与弹出窗口相同 |
| 侧边栏 | 完整 HMR | 与弹出窗口相同 |
| 内容脚本(清单) | 真正 HMR | CRXJS 注入加载器 + HMR 客户端 |
| 内容脚本(动态) | 真正 HMR | 通过 ?script 导入 |
| 服务工作线程 | 自动重载 | 更改触发完整扩展重载 |
| 主世界脚本 | 无 HMR | 被 CRXJS 加载器跳过 |
内容脚本 HMR 之所以有效,是因为 CRXJS 生成了一个加载器脚本,该脚本导入 HMR 前导码、HMR 客户端和你的实际脚本——实现了真正的模块级 HMR,无需完整页面重载。这是 CRXJS 的主要差异化优势。
动态内容脚本导入
对于以编程方式注入的内容脚本(不在清单中),CRXJS 提供了特殊的导入后缀:
typescript
// background.ts — ?script 为你提供 executeScript 的解析路径
import contentScript from ./content?script;
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id! },
files: [contentScript],
});
});
用于主世界注入(无 HMR):
typescript
import mainWorldScript from ./inject?script&module;
await chrome.scripting.executeScript({
target: { tabId },
world: MAIN,
files: [mainWorldScript],
});
CRXJS 插件选项
typescript
crx({
manifest,
browser: chrome, // chrome | firefox
contentScripts: {
injectCss: true, // 自动注入内容脚本的 CSS
hmrTimeout: 5000, // HMR 连接超时时间(毫秒)
},
});
开发工作流
bash
启动开发服务器(输出到 dist/,带 HMR)
npm run dev
1. 打开 chrome://extensions
2. 启用开发者模式
3. 点击加载已解压的扩展程序
4. 选择 dist/ 目录
5. 编辑代码 — 弹出窗口/内容脚本通过 HMR 即时更新
6. 服务工作线程更改触发自动扩展重载
加载一次后,后续的 npm run dev 会话会自动重新连接。除非 manifest.json 发生变化,否则无需重新加载扩展。
生产构建
bash
npm run build # 输出到 dist/
dist/ 目录可直接压缩并上传到 Chrome 网上应用商店:
bash
cd dist && zip -r ../extension.zip .
禁用 Vite 的模块预加载以避免内联脚本被 CWS 拒绝:
typescript
build: {
modulePreload: false;
}
已知问题及解决方法
Tailwind CSS 在内容脚本中的 HMR
新的 Tailwind 类可能不会触发内容脚本中的 CSS 更新。解决方法:添加新的工具类后重启开发服务器。在 v2.4.0 中有所改进但未完全解决。确保配置中设置了 injectCss: true。
WebSocket 连接错误(ws://localhost:undefined/)
原因:开发服务器和 HMR 配置之间的端口不匹配。修复:将两者显式设置为相同值:
typescript
server: {
port: 5173,
strictPort: true,
hmr: { port: 5173 },
}
Manifest version 2 is deprecated 警告
如果看到此警告,说明你的清单被解释为 MV2。修复:确保设置了 manifest_version: 3。
内容脚本未在 file:// URL 上注入
Chrome 要求用户在 chrome://extensions 的扩展设置中启用允许访问文件网址。CRXJS 无法更改此设置。
Chrome 更新后 HMR 停止工作
CRXJS 的 HMR 依赖于注入一个连接到开发服务器 WebSocket 的内容脚本。Chrome 安全更新偶尔会破坏此功能