crxjs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CRXJS

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
).
CRXJS是一款Chrome扩展开发工具,可为弹窗、选项页、内容脚本和侧边栏提供真正的HMR。它读取你的清单文件自动生成扩展输出,处理内容脚本注入,并管理服务工作线程的构建。本质上它是一个Vite插件(
@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
  • :
    @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

Quick start

快速开始

bash
undefined
bash
undefined

Scaffold new project (picks framework interactively)

搭建新项目(交互式选择框架)

npm create crxjs@latest
npm create crxjs@latest

Or add to existing Vite project

或添加到现有Vite项目

npm install @crxjs/vite-plugin -D
undefined
npm install @crxjs/vite-plugin -D
undefined

Vite config by framework

各框架的Vite配置

CRXJS is added as a Vite plugin. The setup varies slightly per framework.
CRXJS作为Vite插件添加,不同框架的设置略有差异。

React

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 })],
});
Use
@vitejs/plugin-react
(not
plugin-react-swc
) for best HMR compatibility. If you must use SWC, cast the manifest:
typescript
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;
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 })],
});
为获得最佳HMR兼容性,请使用
@vitejs/plugin-react
(而非
plugin-react-swc
)。如果必须使用SWC,请对清单进行类型转换:
typescript
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;

Vue

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 })],
});
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

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
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 })],
});

Vanilla TypeScript

原生TypeScript

typescript
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
  plugins: [crx({ manifest })],
});
typescript
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
  plugins: [crx({ manifest })],
});

defineManifest — type-safe dynamic manifest

defineManifest — 类型安全的动态清单

Instead of a static JSON file, use CRXJS's
defineManifest
for dynamic values and full TypeScript autocompletion:
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",
  side_panel: { default_path: "src/sidepanel/index.html" },
  icons: {
    "16": "public/icons/icon16.png",
    "48": "public/icons/icon48.png",
    "128": "public/icons/icon128.png",
  },
}));
Import in vite.config.ts:
typescript
import manifest from "./manifest";
// ... crx({ manifest })
无需使用静态JSON文件,可使用CRXJS的
defineManifest
实现动态值和完整的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",
  side_panel: { default_path: "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 })

Type declarations

类型声明

Add to a
src/vite-env.d.ts
or
src/crxjs.d.ts
:
typescript
/// <reference types="@crxjs/vite-plugin/client" />
This enables types for
?script
and
?script&module
imports.
添加到
src/vite-env.d.ts
src/crxjs.d.ts
typescript
/// <reference types="@crxjs/vite-plugin/client" />
这将启用
?script
?script&module
导入的类型支持。

HMR behavior by context

不同上下文的HMR行为

ContextHMRHow it works
PopupFull HMRWebSocket-based, state preserved
Options pageFull HMRSame as popup
Side panelFull HMRSame as popup
Content script (manifest)True HMRCRXJS injects loader + HMR client
Content script (dynamic)True HMRVia
?script
import
Service workerAuto-reloadChanges trigger full extension reload
Main world scriptsNo HMRSkipped 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.
上下文HMR支持工作原理
弹窗完整HMR基于WebSocket,保留状态
选项页完整HMR与弹窗相同
侧边栏完整HMR与弹窗相同
清单中配置的内容脚本真正HMRCRXJS注入加载器 + HMR客户端
动态内容脚本真正HMR通过
?script
导入实现
服务工作线程自动重载变更触发扩展完整重载
主世界脚本无HMR被CRXJS加载器跳过
内容脚本的HMR之所以可行,是因为CRXJS会生成一个加载器脚本,导入HMR预编译代码、HMR客户端以及你的实际脚本——无需整页重载即可实现真正的模块级HMR,这是CRXJS的核心优势。

Dynamic content script imports

动态内容脚本导入

For content scripts injected programmatically (not in manifest), CRXJS provides special import suffixes:
typescript
// background.ts — ?script gives you a resolved path for executeScript
import contentScript from "./content?script";

chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id! },
    files: [contentScript],
  });
});
For main world injection (no HMR):
typescript
import mainWorldScript from "./inject?script&module";

await chrome.scripting.executeScript({
  target: { tabId },
  world: "MAIN",
  files: [mainWorldScript],
});
对于通过编程方式注入(而非在清单中配置)的内容脚本,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 plugin options

CRXJS插件选项

typescript
crx({
  manifest,
  browser: "chrome", // 'chrome' | 'firefox'
  contentScripts: {
    injectCss: true, // auto-inject CSS for content scripts
    hmrTimeout: 5000, // HMR connection timeout (ms)
  },
});
typescript
crx({
  manifest,
  browser: "chrome", // 'chrome' | 'firefox'
  contentScripts: {
    injectCss: true, // 自动为内容脚本注入CSS
    hmrTimeout: 5000, // HMR连接超时时间(毫秒)
  },
});

Development workflow

开发流程

bash
undefined
bash
undefined

Start dev server (outputs to dist/ with HMR)

启动开发服务器(输出到dist/目录,支持HMR)

npm run dev
npm run dev

1. Open chrome://extensions

1. 打开chrome://extensions

2. Enable "Developer mode"

2. 启用「开发者模式」

3. Click "Load unpacked"

3. 点击「加载已解压的扩展程序」

4. Select the dist/ directory

4. 选择dist/目录

5. Edit code — popup/content scripts update instantly via HMR

5. 编辑代码——弹窗/内容脚本通过HMR即时更新

6. Service worker changes trigger automatic extension reload

6. 服务工作线程变更会触发扩展自动重载


After loading once, subsequent `npm run dev` sessions reconnect automatically. No need to re-load the extension unless manifest.json changes.

加载一次后,后续的`npm run dev`会话会自动重新连接。除非manifest.json变更,否则无需重新加载扩展。

Production build

生产构建

bash
npm run build    # outputs to dist/
The dist/ directory is ready to zip and upload to Chrome Web Store:
bash
cd dist && zip -r ../extension.zip .
Disable Vite's module preload to avoid CWS rejection of inline scripts:
typescript
build: {
  modulePreload: false;
}
bash
npm run build    # 输出到dist/目录
dist/目录可直接压缩后上传至Chrome网上应用店:
bash
cd dist && zip -r ../extension.zip .
禁用Vite的模块预加载,避免内联脚本被Chrome网上应用店拒绝:
typescript
build: {
  modulePreload: false;
}

Known issues and workarounds

已知问题与解决方案

Tailwind CSS HMR in content scripts

内容脚本中的Tailwind CSS HMR问题

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.
新增的Tailwind类可能无法触发内容脚本的CSS更新。解决方案:添加新工具类后重启开发服务器。v2.4.0版本已优化但未完全解决。确保配置中
injectCss: true

WebSocket connection errors (
ws://localhost:undefined/
)

WebSocket连接错误(
ws://localhost:undefined/

Cause: port mismatch between dev server and HMR config. Fix: explicitly set both to the same value:
typescript
server: {
  port: 5173,
  strictPort: true,
  hmr: { port: 5173 },
}
原因:开发服务器端口与HMR配置不匹配。修复:显式将两者设置为相同值:
typescript
server: {
  port: 5173,
  strictPort: true,
  hmr: { port: 5173 },
}

"Manifest version 2 is deprecated" warning

「Manifest版本2已弃用」警告

If you see this, your manifest is being interpreted as MV2. Fix: ensure
"manifest_version": 3
is set.
如果看到此提示,说明你的清单被识别为MV2。修复:确保设置
"manifest_version": 3

Content scripts not injecting on file:// URLs

内容脚本无法在file:// URL上注入

Chrome requires the user to enable "Allow access to file URLs" in the extension settings at chrome://extensions. CRXJS cannot change this.
Chrome要求用户在chrome://extensions的扩展设置中启用「允许访问文件URL」。CRXJS无法修改此设置。

HMR stops working after Chrome update

Chrome更新后HMR停止工作

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的HMR依赖注入连接到开发服务器WebSocket的内容脚本。Chrome安全更新偶尔会破坏此功能。修复:升级到最新版本的CRXJS,它会跟进Chrome的变更。

CRXJS vs alternatives

CRXJS与竞品对比

FeatureCRXJSWXTPlasmo
Content script HMRTrue HMRFile-based reloadPartial
Framework supportAny Vite frameworkAnyReact-focused
Abstraction levelThin (Vite plugin)Full frameworkFull framework
Messaging helpersNone (use chrome.* directly)Built-inBuilt-in
Storage wrappersNoneBuilt-inBuilt-in
Cross-browserChrome + FirefoxChrome + Firefox + SafariChrome + Firefox
File-based routingNoYesYes
Learning curveLow (know Vite, know CRXJS)MediumMedium
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.
特性CRXJSWXTPlasmo
内容脚本HMR真正HMR文件级重载部分支持
框架支持任意Vite框架任意框架侧重React
抽象层级轻量(Vite插件)完整框架完整框架
消息通信助手无(直接使用chrome.* API)内置内置
存储封装内置内置
跨浏览器支持Chrome + FirefoxChrome + Firefox + SafariChrome + Firefox
文件路由
学习曲线低(懂Vite即可)中等中等
选择CRXJS的场景:你希望尽量贴近原生Chrome API,且最看重内容脚本的HMR功能。CRXJS不会过度干预——无魔法路由、无封装API,仅为你的代码提供HMR支持。
选择WXT的场景:你需要约定式规范、内置工具和多浏览器支持。
选择Plasmo的场景:你专注于React开发,需要最高层级的抽象。

Project structure (recommended)

推荐项目结构

my-extension/
├── src/
│   ├── background/
│   │   └── index.ts
│   ├── content/
│   │   ├── index.ts
│   │   └── styles.css
│   ├── popup/
│   │   ├── index.html        <- CRXJS resolves HTML entry points
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── options/
│   │   ├── index.html
│   │   └── main.tsx
│   ├── sidepanel/
│   │   ├── index.html
│   │   └── main.tsx
│   └── shared/
│       ├── messages.ts
│       └── storage.ts
├── public/
│   └── icons/
├── manifest.ts               <- or manifest.json
├── vite.config.ts
├── tsconfig.json
└── package.json
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.
my-extension/
├── src/
│   ├── background/
│   │   └── index.ts
│   ├── content/
│   │   ├── index.ts
│   │   └── styles.css
│   ├── popup/
│   │   ├── index.html        <- CRXJS会自动解析HTML入口
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── options/
│   │   ├── index.html
│   │   └── main.tsx
│   ├── sidepanel/
│   │   ├── index.html
│   │   └── main.tsx
│   └── shared/
│       ├── messages.ts
│       └── storage.ts
├── public/
│   └── icons/
├── manifest.ts               <- 或manifest.json
├── vite.config.ts
├── tsconfig.json
└── package.json
CRXJS会自动解析清单中引用的HTML文件。你的popup.html可以使用标准的
<script type="module" src="./main.tsx">
,完全兼容。
如果你在CRXJS中遇到bug或异常行为,请在github.com/crxjs/chrome-extension-tools/issues提交问题。