crxjs

bởi samber

CRXJS Chrome extension development — true HMR for popup, options, content scripts, side panels, manifest-driven builds, dynamic content script imports (`?script`, `?script&module`), and `defineManifest` for type-safe manifests. Uses Vite as its build tool. Use when the user mentions CRXJS, crxjs, @crxjs/vite-plugin, 'extension with hot reload', 'HMR for chrome extension', or wants to set up a CRXJS-based Chrome extension project with any framework (React, Vue, Svelte, Solid, Vanilla). Also...

npx skills add https://github.com/samber/cc-skills --skill 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).

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.

Thêm skills từ samber

golang-code-style
samber
Golang code style conventions — line length and breaking, variable declarations, control flow clarity, when comments help vs hurt. Use when writing or reviewing Go code, asking about style or clarity, or establishing project coding standards. Not for naming conventions (→ See `samber/cc-skills-golang@golang-naming` skill), linter configuration (→ See `samber/cc-skills-golang@golang-lint` skill), or doc comments (→ See `samber/cc-skills-golang@golang-documentation` skill).
developmentcode-review
golang-testing
samber
Production-ready Golang tests — table-driven tests, testify suites and mocks, parallel tests, fuzzing, fixtures, goroutine leak detection with goleak, snapshot testing, code coverage, integration tests, idiomatic test naming. Use when writing or reviewing Go tests, choosing a testing approach, setting up Go test CI, or debugging flaky/slow tests. For testify-specific APIs see `samber/cc-skills-golang@golang-stretchr-testify`; for measurement methodology see...
developmenttestingcode-review
golang-design-patterns
samber
Các mẫu thiết kế Golang theo phong cách bản địa — tùy chọn hàm, hàm khởi tạo, luồng lỗi và xếp tầng, quản lý tài nguyên và vòng đời, tắt máy an toàn, khả năng phục hồi, kiến trúc, tiêm phụ thuộc, xử lý dữ liệu, truyền phát, v.v. Áp dụng khi lựa chọn rõ ràng giữa các mẫu kiến trúc, triển khai tùy chọn hàm, thiết kế API hàm khởi tạo, thiết lập tắt máy an toàn, áp dụng các mẫu phục hồi, hoặc hỏi mẫu Go bản địa nào phù hợp với một vấn đề cụ thể.
developmentdesigncode-review
golang-error-handling
samber
Idiomatic Golang error handling — creation, wrapping with %w, errors.Is/As, errors.Join, custom error types, sentinel errors, panic/recover, the single handling rule, structured logging with slog, HTTP request logging middleware, and samber/oops for production errors. Built to make logs usable at scale with log aggregation 3rd-party tools. Apply when creating, wrapping, inspecting, or logging errors in Go code. For samber/oops specifics → See `samber/cc-skills-golang@golang-samber-oops`...
developmentcode-review
golang-performance
samber
Các mẫu và phương pháp tối ưu hiệu năng Golang - nếu X là điểm nghẽn, thì áp dụng Y. Bao gồm giảm cấp phát, hiệu quả CPU, bố trí bộ nhớ, tinh chỉnh GC, pooling, caching, và tối ưu đường dẫn nóng. Sử dụng khi profiling hoặc benchmark đã xác định được điểm nghẽn và bạn cần mẫu tối ưu phù hợp để khắc phục. Cũng sử dụng khi thực hiện đánh giá mã hiệu năng để đề xuất cải tiến hoặc benchmark có thể giúp xác định các cải thiện hiệu năng nhanh chóng. Không dành cho phương pháp đo lường (→...
developmentcode-review
golang-security
samber
Các phương pháp bảo mật tốt nhất và phòng ngừa lỗ hổng cho Golang. Bao gồm injection (SQL, lệnh, XSS), mật mã học, an toàn hệ thống tệp, bảo mật mạng, cookie, quản lý bí mật, an toàn bộ nhớ và ghi nhật ký. Áp dụng khi viết, xem xét hoặc kiểm tra mã Go về bảo mật, hoặc khi làm việc trên bất kỳ mã rủi ro nào liên quan đến mật mã, I/O, quản lý bí mật, xử lý đầu vào người dùng hoặc xác thực. Bao gồm cấu hình các công cụ bảo mật.
securitycode-reviewdevelopment
golang-database
samber
Hướng dẫn toàn diện về truy cập cơ sở dữ liệu Go — truy vấn tham số hóa, quét struct, cột NULL, giao dịch, mức cô lập, SELECT FOR UPDATE, connection pool, xử lý hàng loạt, truyền context và công cụ migration. Sử dụng khi viết, xem xét hoặc gỡ lỗi mã Golang tương tác với PostgreSQL, MariaDB, MySQL hoặc SQLite; để kiểm thử cơ sở dữ liệu; hoặc cho các câu hỏi về database/sql, sqlx hoặc pgx. KHÔNG tạo lược đồ cơ sở dữ liệu hoặc SQL migration.
developmentdatabase
golang-lint
samber
Các phương pháp linting tốt nhất và cấu hình golangci-lint cho các dự án Golang — chạy linters, cấu hình .golangci.yml, loại bỏ cảnh báo bằng chỉ thị nolint, diễn giải đầu ra lint, và lựa chọn linters. Sử dụng khi cấu hình golangci-lint, hỏi về cảnh báo lint hoặc loại bỏ nolint, thiết lập công cụ chất lượng mã, hoặc chọn linters. Cũng sử dụng khi người dùng đề cập đến golangci-lint, go vet, staticcheck, hoặc revive.
developmentcode-reviewtesting