Skip to main content
dev 是日常浏览器扩展开发的命令:watch 模式、启动浏览器,以及感知上下文的更新行为。 dev 会运行开发流水线并监听你的项目文件。它会根据变更类型采取不同的更新策略:热模块替换(HMR)、硬重载,或者当变更需要时进行完整重启。

什么时候使用 dev

  • 构建特性并实时验证改动。
  • 在一个或多个浏览器目标里调试扩展行为。
生产产物用 build;生产构建 + 启动用 start;只运行已有构建产物用 preview 如果你的扩展放在 monorepo / 子模块里,请查看 extension.config.* 如何加载 env 文件(包括工作区根的回退):环境变量

dev 命令的能力

用法

如果省略路径,Extension.js 会使用当前工作目录。你也可以传入一个 GitHub tree URL(例如 https://github.com/user/repo/tree/main/path)。Extension.js 会下载该仓库并在本地副本上以开发模式运行。

最常用的 flag

下面这些覆盖了 80% 的场景。其余的请跳到 完整参考

参数与 flag

有两个已废弃的别名从 --help 中隐藏,但仍然可用:
  • --wait-format <pretty|json> 映射到 --output,并在 stderr 上警告一次。请把脚本迁移到 --output
  • --author--author-mode 映射到 --debug

Safari flag

这些 flag 只适用于 safariwebkit-based 目标。和其他目标一起传入其中任何一个都会以 E_INVALID_OPTION 退出,所以拼错不会静默地不生效。 对于 Safari 目标,dev 还会在第一次打包前运行工具链预检。缺少 Xcode 会以 E_SAFARI_TOOLCHAIN 快速失败。 从 4.1.20 起,Safari 会话就是一个完整的 dev 会话。你在 Safari 设置中启用扩展之后,extension logs 可以读取它,控制通道会连上,每次保存都会在 Safari 中重载扩展。详见 构建 Safari 扩展

父进程看门狗

--parent-pid 供派生 dev 的 harness 与 agent 使用,宿主崩溃不会泄漏服务器。它的值必须是正整数,其他任何值都会以 E_INVALID_OPTION 退出。看门狗每 2 秒轮询一次父进程。父进程消失后,dev 服务器会通过 SIGTERM 关闭;如果清理卡住,还有 5 秒的硬退出兜底。

端口如何确定

--port 是一个请求,而不是保证。请求的端口被占用时,dev 服务器会向上寻找最近的空闲端口。--port 0 会向 OS 要一个任意空闲端口。请从 ready.json 读取实际绑定的端口,而不是你传入的那个 flag。

自动化元数据(推荐用于脚本 / agent)

dev 运行时,Extension.js 会向这些位置写入机器可读的元数据:
  • dist/extension-js/<browser>/ready.json
  • dist/extension-js/<browser>/events.ndjson(每行一个 JSON)
对于自动化(Playwright、持续集成(CI)、AI agent),请优先使用这些文件,而不是解析终端日志。 ready.json 当作就绪契约来用:
  • 启动期间 status: "starting"
  • 构建编译完成时 status: "ready"
  • 启动 / 编译失败时 status: "error"
  • 会话关闭后 status: "stopped",因此一个已经死掉的会话不会再宣称自己 ready
  • service worker 连接后为 runtime: "attached"(并带上 executorAttachedAt)。act verb 应该等待它,而不是等待 ready
  • 从 4.1.20 起,最后一个已连接的扩展上下文断开后为 runtime: "detached"(并带上 executorDetachedAt)。executorAttachedAt 会作为来源记录保留,重新连接后该值会回到 "attached"
  • runId 唯一标识一次运行时会话
  • startedAt 标记运行时会话的起始时间戳
  • command 记录产生它的命令(devstartpreviewbuild
  • toolchainVersionextensionNameextensionVersion 记录是哪个 Extension.js 版本、为哪个扩展产出了这棵目录树——终端滚屏丢失后,这个文件仍是一份构建回执
  • port 是实际绑定的 dev 服务器端口,host 是客户端可以拨号的可连接主机(见绑定主机与可连接主机
  • controlPort / instanceId 定位 extension logs 与 act verb 使用的控制桥
  • cdpPort(Chromium)与 rdpPort(Gecko)暴露浏览器调试端口
  • profilePathbrowserPidextensionId 由浏览器启动器在启动后写入
完整的 schema、错误状态与两阶段就绪规则见 ready.json 契约 events.ndjson 只属于当前这次运行:新一次运行开始时文件会重置,且每条记录都盖有本次运行的 runId(与 ready.json 一致),消费者不会看到上一个会话的事件与当前会话交错。 会话行为异常时,运行 extension doctor——它按顺序检查契约、控制通道、令牌、执行器与浏览器,并指出第一个失败的环节和修复方法。

阻止浏览器启动

两个 flag 听起来很像,作用却不同:
--no-browser 是无头、CI 与远程工作的运行模式,也是 Playwright E2E 工作流所依赖的 flag。它还有配置写法 commands.dev.noBrowser: true,以及环境变量写法 EXTENSION_CLI_NO_BROWSER=1 --no-open 只是启动时的一个细节。当你希望浏览器停在它原本显示的页面上,而不让 Extension.js 在上面为你的扩展打开一个标签页时,就用它。devstartpreview 都接受这两个 flag。

--no-browser 与就绪同步

--no-browser 只会禁用浏览器启动,但完整的 dev 循环仍然保留。dev 服务器依旧监听你的文件,并在每次重建后通过控制桥向扩展的 service worker 广播一次重载,因此即使没有被启动的浏览器在驱动,你的改动也会生效:
  • content script 的改动会就地重新注入到已经打开的匹配标签页(service worker 会用新的构建产物运行 chrome.scripting.executeScript),所以保存后页面就会更新,不需要手动刷新。之后新打开的标签页也会拿到新的构建产物,因为 service worker 会动态重新注册 content script(chrome.scripting.registerContentScripts)。
  • service worker / manifest 的改动会重启扩展。
所以在无头、持续集成(CI)以及远程 / dev container 工作流里,--no-browser 的表现和普通的 dev 会话一样:把构建好的 dist/<browser> 加载进任何你能控制的浏览器,它就会在你保存时持续更新。(如果你想要一个永不重载的静态 dev bundle,请用 --no-reload,见下文。) --no-browser 并不会在编译完成之前阻塞外部 runner。 对于 Playwright / CI / AI 工作流:
  1. extension dev --no-browser 作为长期运行的进程。
  2. extension dev --wait --browser=<browser> 作为就绪闸门。
  3. 只有在 status: "ready" 之后才启动外部浏览器自动化。
--wait 面向第二个进程(或 CI 步骤),在 error / 超时时以非零状态退出。 当 --wait 看到一个来自已死进程的过期 ready.jsonpid 已不存在)时,它会继续等待一个活跃的生产者。 --wait 需要一个本地项目路径。传入远程 URL 会以 E_ARGS 退出。 如果你在同一条命令中同时传入 --wait--no-browser--wait 优先。命令会以 wait-only 模式运行。 从 4.1.21 开始,当 --wait 在生产者还在写入 ready.json 时读到它,会把这个写了一半的文件当作又一种暂态,继续轮询。到 4.1.20 为止,一次撕裂读取会让等待以 JSON 解析错误失败。始终无法解析的文件仍会以 E_READY_TIMEOUT 结束,超时消息会点明原因:The last read of the file failed to parse as JSON,后面跟着解析器自己的错误。

--output json 得到机器输出

--output json 会在 stdout 打印 schema-1 信封,每行一个 JSON 对象:
  • 普通的 dev 运行会在启动时打印一个 status: "started" 帧。它带有项目路径、浏览器列表、请求的端口以及 dev 服务器的 piddev 不会自行结束,所以后面不会再有结果帧。实时状态请读 ready.json
  • dev --wait 运行成功时会打印一个 status: "ready" 帧。它的 value.results 数组按浏览器给出完整的就绪契约。
  • 失败时会在进程以 1 退出之前打印一个带 error.code(例如 E_READY_TIMEOUT)的 ok: false 帧。

--no-reload:得到一个干净的 dev bundle

--no-reload 会跳过 content script 重注入包装器与重建时的重载派发。dev 下的 dist 会更接近生产 bundle,文件变更时已打开的标签页也不会被打扰。你需要自己重载扩展或页面来看到改动。 --no-reload 仅在 extension dev 上受支持。把它传给 startpreviewbuild 会以错误退出。内部上它会设置 EXTENSION_NO_RELOAD=true,方便 develop 进程在 CLI argv 之外读取。 dev 构建会输出 cheap-module-source-map 类型的 source map,描述的是你的源码:原始的 TypeScript 与精确的行号,覆盖 content script、经典多文件分组、background 以及两个 manifest 版本下的页面。不会使用任何 eval 变体,因此 bundle 在你自己的 CSP 下运行。

日志 flag

这些 flag 仍是实验性的,可能在小版本之间变化。

共享的全局选项

也支持 全局 flag

monorepo 与工作区根

你可以把 dev(以及 build)指向一个 monorepo 的根目录,而不是扩展包本身。Extension.js 会检测工作区根,并自动解析其中的扩展包:
如果只找到一个扩展包,Extension.js 会解析它并打印:
如果存在多个候选,会列出来让你指向具体那一个:

pnpm 工作区成员从根目录安装

从 4.1.18 开始,当项目是 pnpm 工作区的成员且依赖缺失时,自动安装会从工作区根目录运行,而不是从包目录运行。安装范围过滤为该成员及其工作区依赖:
锁文件和 linker 布局仍然属于工作区,所以结果与在根目录执行 pnpm install 一致。安装运行之前,会话会打印一行 info:

示例

运行一个本地扩展

从 GitHub 运行一个远程扩展

把 GitHub tree URL 作为参数传入,即可在本地开发远程扩展:

在 Firefox 中运行

依次在多个浏览器中运行

在 Docker 或 dev container 中运行

当你在 Docker、dev container 或 GitHub Codespaces 中运行时,把 dev 服务器绑定到 0.0.0.0,宿主机才能访问到它:
搭配 --port 0 让 OS 自动选择一个可用端口:

绑定主机与可连接主机

--host 是 dev 服务器绑定的地址。浏览器(HMR 客户端与重载桥)需要的是一个它真正能连接的地址,两者并不总是同一个值:
  • --host 0.0.0.0 会绑定所有网络接口,但 0.0.0.0 并不是一个可连接的地址。Extension.js 会自动改为向浏览器公布 127.0.0.1,这正是常见的端口转发式 Docker / dev container / Codespaces 场景所需要的目标:浏览器跑在宿主机上,端口被转发到容器里。
  • 对于真正的远程场景(浏览器与 dev 服务器不在同一台机器上),请用 --public-host 传入浏览器能访问到的地址(局域网 IP 或主机名)。它会被传播到 HMR 客户端 URL、ready.json 以及烘焙进扩展里的重载桥。
--host 本身已经是一个具体地址时(例如 --host 192.168.1.50),这个值本来就可连接,会被直接使用。只有在绑定主机与浏览器面向的主机不同的时候,才需要 --public-host

用 Brave 作为自定义二进制运行

最佳实践

  • 浏览器兼容性: 在不同浏览器中测试你的扩展,确保在每个目标上都能工作。
  • polyfill: dev 中 polyfill 默认开启,所以 browser.* 调用在 Chromium-based 浏览器里也能工作。想要未经处理的原始 bundle 时,传 --no-polyfill
  • 自动化可靠性:dev 当作 watch 模式的伴侣(--no-browser + dev --wait);把 start 当作生产的伴侣(--no-browser + start --wait)。脚本与 CI 自动化使用 --output=json

下一步