C

crxjs

by @samberv
4.4(120)

CRXJS 是一个用于 Chrome 扩展开发的 Vite 插件,提供真正的热模块替换(HMR),支持弹窗、选项页、内容脚本和侧边面板。它通过读取 manifest 文件自动生成扩展输出,简化了开发流程,显著提升了开发效率和体验。

chrome-extensionvite-pluginhmrfrontend-developmentdeveloper-toolsGitHub
安装方式
npx skills add https://github.com/samber/cc-skills --skill crxjs
compare_arrows

Before / After 效果对比

1 组
使用前

在没有这个技能之前,开发者在修改 Chrome 扩展代码后,需要手动刷新扩展或等待缓慢的页面重载才能看到更改,这导致了漫长的开发周期和低效的调试过程,尤其是在处理内容脚本时。

使用后

此技能通过提供真正的热模块替换(HMR),使得开发者在修改代码后能即时看到效果,无需手动刷新,极大地缩短了开发迭代时间,提升了调试效率和开发体验。

SKILL.md

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

# Scaffold new project (picks framework interactively)
npm create crxjs@latest

# Or add to existing Vite project
npm install @crxjs/vite-plugin -D

Vite config by framework

CRXJS is added as a Vite plugin. The setup varies slightly per framework.

React

// 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:

import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;

Vue

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

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

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

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

defineManifest — type-safe dynamic manifest

Instead of a static JSON file, use CRXJS's defineManifest for dynamic values and full TypeScript autocompletion:

// 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:

import manifest from "./manifest";
// ... crx({ manifest })

Type declarations

Add to a src/vite-env.d.ts or src/crxjs.d.ts:

/// <reference types="@crxjs/vite-plugin/client" />

This enables types for ?script and ?script&module imports.

HMR behavior by context

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.

Dynamic content script imports

For content scripts injected programmatically (not in manifest), CRXJS provides special import suffixes:

// 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):

import mainWorldScript from "./inject?script&module";

await chrome.scripting.executeScript({
  target: { tabId },
  world: "MAIN",
  files: [mainWorldScript],
});

CRXJS plugin options

crx({
  manifest,
  browser: "chrome", // 'chrome' | 'firefox'
  contentScripts: {
    injectCss: true, // auto-inject CSS for content scripts
    hmrTimeout: 5000, // HMR connection timeout (ms)
  },
});

Development workflow

# Start dev server (outputs to dist/ with HMR)
npm run dev

# 1. Open chrome://extensions
# 2. Enable "Developer mode"
# 3. Click "Load unpacked"
# 4. Select the dist/ directory
# 5. Edit code — popup/content scripts update instantly via HMR
# 6. Service worker changes trigger automatic extension reload

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

Production build

npm run build    # outputs to dist/

The dist/ directory is ready to zip and upload to Chrome Web Store:

cd dist && zip -r ../extension.zip .

Disable Vite's module preload to avoid CWS rejection of inline scripts:

build: {
  modulePreload: false;
}

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:

server: {
  port: 5173,
  strictPort: true,
  hmr: { port: 5173 },
}

"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

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.

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.

用户评价 (0)

发表评价

效果
易用性
文档
兼容性

暂无评价

统计数据

安装量3.9K
评分4.4 / 5.0
版本
更新日期2026年10月23日
对比案例1 组

用户评分

4.4(120)
5
37%
4
43%
3
13%
2
5%
1
2%

为此 Skill 评分

0.0

兼容平台

🤖claude-code

时间线

创建2026年6月25日
最后更新2026年10月23日
🎁 Agent 知识卡片
调研问卷