跳转到主要内容
CRXJS 是用于 Chrome 扩展的 Vite 插件。如果你已经超出了只面向 Chrome 的范畴,希望获得一等的 Firefox 输出,或者遇到了重载循环的怪问题,本指南可以把一个典型的 CRXJS 项目迁移到 Extension.js,而无需重写 UI 代码。
你来这里是因为在 Vite 8 上 vite build 报错 [crx:manifest-post] Content script fileName is undefined?在迁移前请先查看 专门的修复 页面 了解变通方案。

哪些变,哪些不变

不变的: 你的 React/Vue/Svelte 组件、你的 Tailwind 配置、你的测试、你的 chrome.* API 调用。 会变的:
  • vite.config.ts + @crxjs/vite-plugin 变成 extension.config.js(或干脆不要配置)。
  • manifest.config.ts(TypeScript 模块)变成普通的 manifest.json,可配合 按浏览器前缀的键
  • vite / vite build 脚本变成 extension dev / extension build
  • Extension.js 的 dev 取代 Vite 开发服务器,内置浏览器启动、配置文件管理和按目标的重载。

第 1 步:安装 Extension.js

npm install extension@latest --save-dev
npm uninstall @crxjs/vite-plugin vite
如果你的项目也在非扩展场景(例如一个营销站点)中使用 Vite,请把 Vite 保留在那个 workspace 范围内。

第 2 步:转换 manifest

CRXJS 使用一个 TypeScript 模块:
manifest.config.ts
import { defineManifest } from "@crxjs/vite-plugin";
import pkg from "./package.json";

export default defineManifest({
  manifest_version: 3,
  name: pkg.name,
  version: pkg.version,
  action: { default_popup: "src/popup/index.html" },
  background: { service_worker: "src/background/index.ts" },
  content_scripts: [{ matches: ["<all_urls>"], js: ["src/content/index.ts"] }],
});
在 Extension.js 中,写一个真实的 manifest.json 并引用真实的文件:
manifest.json
{
  "manifest_version": 3,
  "name": "my-extension",
  "version": "1.0.0",
  "action": { "default_popup": "popup/index.html" },
  "background": { "service_worker": "background/index.ts" },
  "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content/index.ts"] }]
}
注意:
  • 在 manifest 中文件扩展名仍然是 .ts/.tsx。Extension.js 会在构建时编译它们。
  • 如果你扁平化了目录,去掉 src/ 前缀。Extension.js 会遵循你的 manifest 声明的路径。
  • 对于按浏览器的取值,请使用 带前缀的键firefox:browser_specific_settings),而不是按构建维护多份 manifest。

第 3 步:更新 package.json 脚本

把:
{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}
替换为:
{
  "scripts": {
    "dev": "extension dev",
    "build": "extension build",
    "start": "extension start"
  }
}
完整命令集见 命令参考

第 4 步:删除 vite.config.ts

如果你的 vite.config.ts 只是为接 CRXJS 而存在,直接删掉。如果里面还有其他插件,把等价配置迁移到 extension.config.js,或者用 Rspack 配置 做更高级的打包器定制。

第 5 步:处理 HMR 差异

CRXJS 的 HMR 通过 Vite 的 WebSocket 推送更新。Extension.js 使用一种不同的模型,文档见 重载与 HMR
  • popup、options、devtools 页面:HMR。
  • Content scripts:精准重载。
  • 背景 service worker:完整重启。
如果你的代码依赖 Vite 的 import.meta.hot 来处理 content script 逻辑,请把那些分支替换为普通的模块代码。Extension.js 在你的源代码之外编排重载。

第 6 步:跨浏览器输出

CRXJS 面向 Chromium。要同时开始输出 Firefox:
extension build --browser=chrome,firefox --zip
你会得到 dist/chromedist/firefox,含按浏览器正确的 manifest,以及可直接上架 Chrome Web Store 和 addons.mozilla.org 的 .zip 归档。参见 跨浏览器兼容性

第 7 步:验证

extension dev --browser=chrome
验证 popup、options、content script 和背景行为。然后验证 Firefox:
extension dev --browser=firefox
如果你的代码调用 chrome.* 而你希望同一份源在 Firefox 上无需改动就能运行,请使用 --polyfill

常见坑点

  • Service worker 的 import 语句: 当你的背景入口使用 ESM 语法时,Extension.js 会自动设置 type: "module"。CRXJS 也是这么做的。无需额外处理。
  • web_accessible_resources 的类型: Manifest V3 使用 [{resources, matches}] 块。两个框架都会输出正确的形状;按原样复制现有条目即可。
  • 带哈希的资源路径: Extension.js 在 dist/<browser> 下使用稳定的路径。如果你的代码硬编码了 Vite 风格的带哈希文件名,请替换为相对于 manifest 的路径。

参见