Safari 仅支持 macOS,且需要完整的 Xcode 应用。build → 转换 →
xcodebuild → 打开的流水线覆盖 build 与 dev;不支持 preview 与
start,目前也没有实时重载。--browser=safari,可以把你发往 Chrome 和 Firefox 的同一个扩展转换为 Safari App Extension。Extension.js 会打包你的代码,运行 Apple 的 safari-web-extension-converter,再用 xcodebuild 编译生成的 app,并引导你启用它。
前置要求
Safari 仅支持 macOS,并且需要 完整的 Xcode app——不能只装 Command Line Tools。转换器(safari-web-extension-converter)与 xcodebuild 都包含在 Xcode.app 中。
extension build / dev --browser=safari 会在打包之前就快速失败,并给出指引,而不是后续抛出一个含糊的错误。
产物
extension build --browser=safari 会在你项目旁创建:
整条流水线端到端运行:打包 → 转换 →
xcodebuild,并且在 dev(或
build --open)下还会 打开 app → 引导启用。普通的 build
在打包后就停下,并打印 open 命令供你自己执行。
name(例如 React Sidebar Example → bundle id dev.extensionjs.React-Sidebar-Example)。工程默认面向 macOS。
app 身份与打包选项
dev 与 build 都接受身份覆盖参数(仅对 Safari 目标生效):
不带
--bundle-id 时,Extension.js 会推导出 dev.extensionjs.<name>,其中 <name> 是经过清洗的 app 名称(连续的非字母数字字符会变成连字符)。你自己提供的 bundle id 必须符合反向 DNS 形状:至少两段以点分隔、由字母、数字和连字符组成、且每段以字母开头。不合法的值会在打包前被拒绝。
同样的选项也可以写在 extension.config.js 里(CLI 标志优先):
在其他平台上构建 web 扩展产物
extension build --browser=safari 在 Linux 和 Windows 上同样可用:它会产出完整的
dist/safari web 扩展产物,并带着一条警告 跳过 Xcode 打包步骤。
这样 CI 可以在任意平台构建载荷,之后再由一台 Mac(或 macOS runner)完成转换 +
xcodebuild 的部分。dev --browser=safari 仍然需要装有 Xcode 的
macOS,因为没有打包步骤的 Safari 开发循环没有任何东西可运行。
在 Safari 中启用扩展
如何启用扩展取决于 app 是如何签名的。app 打开时(dev,或
build --open),Extension.js 会打印与你这次构建相匹配的步骤,并在 macOS
注册扩展后给出确认。
已签名的构建(推荐)
带上--development-team 和你的 Apple Developer team id,app 就会用你自己的证书签名:
- Safari ▸ 设置 ▸ 扩展 ▸ 开启你的扩展。
content_scripts.matches 和 host_permissions
里都声明了 <all_urls>,情况也是如此——这正是从 Chrome
过来的人最容易意外的地方,因为在 Chrome 里安装即授予。
在同一个面板的 权限(Permissions) 下,使用:
- 在每个网站上始终允许…(Always Allow on Every Website…) 适合你正在迭代的开发扩展。它会持久保存,只需授予一次。
- 编辑网站…(Edit Websites…) 只允许你正在测试的那些主机。
xcrun security find-identity -v -p codesigning。证书名称里那个十位字符的代码就是你的 team id,它也出现在你 Apple Developer 账号的 Membership 页面上。
临时签名的构建(没有 Apple Developer 账号)
不带--development-team 时构建为临时签名,Safari 会把它视为未签名。它仍然可以运行,只是需要三步:
- Safari ▸ 设置 ▸ 高级 ▸ 勾选 “显示网页开发者功能”。
- Safari ▸ 开发 ▸ 允许未签名的扩展(每次重启 Safari 时这一项都会被重置)。
- Safari ▸ 设置 ▸ 扩展 ▸ 开启你的扩展。
- 按上面所述授予网站访问权限。只是启用并不能让内容脚本运行。
“允许未签名的扩展” 每次启动 Safari 时都会被重置,而且无法脚本化,也无法保存到偏好设置里,所以每次重启都要重来这三步。
如果你有 Apple Developer 账号,
--development-team
在日常开发中就已经值回票价了,而不只是发布时才有用。使用 dev 进行开发
extension dev --browser=safari 会运行一个 watch 循环:
- 首次编译——完整流程:转换、构建、打开 app,并打印启用步骤。
- 之后每次保存——增量
xcodebuild重新同步(通常几秒钟),用刚刚重新构建的dist/safari更新 app 的资源。
Xcode 工程何时会重新生成
Xcode 工程只生成一次,之后的重新同步都复用它。是否过期由一个指纹文件dist/safari-xcode/.manifest-fingerprint 决定。v2
指纹记录了归一化后的 manifest.json 内容,以及身份输入:app
名称、bundle id 和仅 macOS 设置。当存储的指纹不再匹配,或你传入
--force-regenerate 时,转换器会再次运行。纯外观性的 manifest
改动(键顺序、空白)不会触发它。
重新生成会替换整个工程:你在 Xcode 里做的定制(entitlements、capabilities、新增的文件或
target)都会被 丢弃。只有这几项签名设置会被自动保留:DEVELOPMENT_TEAM、CODE_SIGN_STYLE
和 PROVISIONING_PROFILE_SPECIFIER。每次要重新生成一个已存在的工程时,Extension.js
都会先发出警告;如果你在 Xcode 里做过定制,请先备份。删除 dist/safari-xcode 可以从头来过。
bundle id 是如何被强制生效的
Apple 的转换器会根据 app 名称推导父 app 的 id,而不是原样采用--bundle-identifier。每次转换之后,Extension.js 都会重写生成的
project.pbxproj 中两处 PRODUCT_BUNDLE_IDENTIFIER:app target 拿到你的 bundle
id,扩展 target 拿到 <bundle-id>.Extension。这样保留的是你配置的身份,而不是转换器猜出来的那个。
xcodebuild 具体做了什么
编译步骤使用 Release 配置,derived data 写入
dist/safari-xcode/.derived——这个目录值得加进 .gitignore。签名设置取决于
--development-team。带 team id 时,构建会传入
DEVELOPMENT_TEAM=<id>、CODE_SIGN_STYLE=Automatic 和
-allowProvisioningUpdates,因此 Xcode 无需打开就能生成所需的 provisioning
profile。不带时则传入临时签名设置(CODE_SIGN_IDENTITY=-、CODE_SIGNING_REQUIRED=NO、CODE_SIGNING_ALLOWED=YES),这样即使没有
Apple Developer 账号,内嵌的 .appex 也能通过校验。对仅 macOS
的工程,scheme 名就是你的 app 名称;对通用工程则是 <App> (macOS)。
注册确认
打开 app 之后,Extension.js 会轮询pluginkit
查询扩展的注册状态,大约在 5 秒内尝试 6 次,然后打印确认或一条”尚未注册”的提示。在
--no-open(以及不带 --open 的普通 build)下,app 根本不会启动,因此还谈不上注册。这时轮询会被跳过,CLI 改为打印
open 命令。
在 Safari 中调试
Safari 不支持--logs 集中式日志(它没有自动化通道),但 Web Inspector 覆盖了每一种扩展上下文:
- 后台 / service worker:Safari ▸ 开发 ▸ Web Extension Background Content ▸ 你的扩展。
- popup / options / sidebar 页面:打开对应界面,然后右键 ▸ 检查元素(或 开发 ▸ 你的 Mac ▸ 该页面)。
- 内容脚本:检查宿主页面,扩展的脚本上下文会出现在 Sources 标签页的 Extension Scripts 下。
xcrun / xcodebuild
输出的末尾部分,限制在最后 50 行和 8 KB 以内,让诊断信息保持可读。传入
--debug(或设置 EXTENSION_DEBUG=true)可以改为实时输出完整的工具日志。转换器的兼容性警告(Safari
不支持的 manifest 键)会在打包过程中以警告形式呈现。
引擎目标
safari 有一个引擎别名 webkit-based,与 chromium-based 和 gecko-based 平行:
命令支持情况
preview 与 start 的存在是为了在正在运行的浏览器中启动你的扩展。Safari 要求上面那一步带有安全限制的手动启用,所以这两个命令会拒绝 Safari 目标,并把你引导到 build。在 --output json 下,preview 以 E_COMMAND_UNSUPPORTED_FOR_TARGET 失败(Safari 是受支持的浏览器,只是这个命令没有 Safari 路径),start 以 E_UNSUPPORTED_BROWSER 失败。
限制
- 打包仅限 macOS。 Xcode 步骤需要 macOS 与完整的 Xcode app。(在其他平台上
build仍会产出dist/safari;dev则需要 macOS。) - 没有实时重载。 重新构建很快,但你需要在 Safari 中刷新才能应用更新。
- 首次启用为手动操作。 把扩展打开、以及授予网站访问权限,都是 Safari 的安全控制项,无法自动化。在临时签名的构建上,允许未签名的扩展是第三个同样规则的控制项。
- 签名止步于开发阶段。
--development-team用你的开发证书为本地 app 签名。用于分发的签名、公证(notarization)与 App Store 提交是这条工作流之外的另一步(见下文)。 - 仅产出 macOS 目标。 目前这条工作流不会生成 iOS 应用。
dev 之后:发布到 App Store
上面的 Safari 工作流最终得到的是一个本地签名的 app——临时签名或开发签名。把它分发出去(上架 Mac App Store,或作为经过公证的直接下载)是另一条流水线,Extension.js 计划通过 extension.dev 平台提供。在那之前,请遵循 Apple 自己的指南:- 分发你的 Safari web 扩展(Apple Developer Program、签名、App Store Connect)。
- 非 App Store 分发请参考 公证 macOS 软件。
- 设置你自己的
--bundle-id(反向 DNS,用你拥有的域名),从第一次构建开始。bundle id 就是扩展在 Apple 平台上的身份。 - 在 Xcode 里设置一次你的
DEVELOPMENT_TEAM:它会随工程重新生成自动保留,CODE_SIGN_STYLE和PROVISIONING_PROFILE_SPECIFIER也一样。
最佳实践
- 其他目标照常构建:Safari 是增量加入的——继续在
chromium/firefox中迭代,需要验证 Safari 时再运行--browser=safari。 - 使用按浏览器划分的字段 来处理真正的行为差异。Safari 会解析 chromium 系的前缀(
chromium:、chrome:、edge:),而在 Safari 目标上,带safari:/webkit:前缀的键优先于它们——--browser=safari和--browser=webkit-based都是如此。 - 保留生成的工程,除非你确实需要一个全新的工程——重新生成会丢弃被保留的签名设置之外的所有 Xcode 侧定制。
下一步
- 查看所有 支持的浏览器。
- 使用 按浏览器划分的 manifest 字段。
- 了解 多平台构建。

