关于 experience-ui-bundle-2gp-deploy
此参考型 skill 指导将现有 UI Bundle 制作成 Salesforce 2GP,并在其他 org 安装、升级或卸载。它面向已有项目和可构建 bundle 的开发者,按具体请求提供对应步骤,而非要求完整执行整个发布流程。
主要功能
- 01选择 managed 或 unlocked 包类型
- 02准备现有 bundle 以便打包
- 03创建 2GP 包及版本
- 04在订阅 org 安装、升级或卸载
- 05排查打包与安装问题
适用场景
- 01将现有 UI Bundle 制作为 2GP
- 02为现有包创建新版本
- 03将包安装到订阅 org
- 04诊断包构建或升级故障
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ forcedotcom/ sf-skills --skill experience-ui-bundle-2gp-deploy安装后在 Claude Code 中输入/experience-ui-bundle-2gp-deploy确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/CustomApplication.app-meta.xmlPermissionSet.permissionset-meta.xmlscripts/find-bundle-package-dir.sh详细介绍
为现有 UI Bundle 打包(2GP)
介绍如何将当前项目中已存在的 UI Bundle(位于
<packageDir>/uiBundles/<name>/ 下,其中 <packageDir> 是 sfdx-project.json 中的包
目录,通常为 force-app/main/default)打包为第二代包(2GP),然后在另一个组织中安装/升级/卸载。
**这是参考知识,不是要从头到尾执行的操作手册。**用户已经有一个项目和一个已构建(或可构建)的 Bundle。理解用户的意图,只应用相应部分:
**只回答用户问的内容。**提供用户所需部分的命令、命令面向的组织,以及任何确实存在的注意事项——仅此而已。用户只询问一个步骤时,不要重述其他部分、重新解释类型表,也不要复述完整的“构建→创建→安装→提升”流程。调试问题需要的是修复方法,而不是打包教程;安装问题需要的是 sf package install 命令,以及订阅方组织与 Dev Hub 的区别,而不是第 1 部分和第 2 部分。简洁即正确。
| 用户想要…… | 转到 |
|---|---|
| 决定使用托管包还是非托管包 | 选择一种类型 |
| 让 Bundle 可打包/配置 CustomApplication | 第 1 部分 |
| 创建包或新版本 | 第 2 部分 |
| 安装/升级/卸载/提升 | 第 3 部分 |
| 调试故障 | 第 4 部分 + 故障排除 |
此技能用于打包和跨组织分发(sf package …)。若要将 Bundle 作为普通源代码部署到一个组织(sf project deploy …),请改用
experience-ui-bundle-deploy。此处绝不要使用 sf project generate 或
sf template generate ui-bundle——项目和 Bundle 已经存在。
MyReactApp/force-app/force-app/main/default 都是占位符;请将其替换为用户实际的 Bundle 名称及其 <packageDir>,并替换下文中出现的所有位置。确定 <packageDir> 时必须采用确定性方法——在包含多个包的项目中绝不要猜测 [0]——使用:
packageDir="$(scripts/find-bundle-package-dir.sh <bundleName>)" # walks packageDirectories; picks the entry whose tree contains uiBundles/<bundleName>/
第 0 步——确认组织(在操作任何组织之前执行)
不要假定默认组织。询问用户,或读取 sf org list,然后重述你将使用的组织:
- Dev Hub(
devhub)——创建包、构建版本和部署源代码的组织。始终必需。 - 订阅方组织(
subscriber)——安装包的组织。仅安装/升级/卸载时必需。
sf org list # connected orgs + default Dev Hub
sf org list --json | jq -r '.result.nonScratchOrgs[]?.alias'
规则:
- 仅创建任务(包或版本)→ 一个 Dev Hub 就够了;不要询问订阅方组织。
- 安装任务→ 确认两个组织,并确认哪个组织对应哪个角色。在 Dev Hub 中误操作安装是常见且棘手的错误。
将下文所有 devhub/subscriber 替换为实际别名。
ID 说明(打包): 0Ho… 包 · 04t… 可安装版本
(SubscriberPackageVersionId)· 05i… Package2Version · 08c… 版本创建
请求 · 0Hf… 安装请求 · 06y… 卸载请求。
ID 说明(运行时,在调试故障订阅方组织时有用): 9YE… UI
Bundle 行 · 9YF… UIBundleApplication 关联记录 · 02u… CustomApplication /
TabSet · 0Zu… ManagedContentSpace(工作区)· 0ap… ManagedContentChannel
(WEB_APP)。安装后 App Launcher 中缺少磁贴,几乎总是因为其中某项缺失或配置不当。
前提条件 — 人人都会忘记的 2GP 开关
2GP 需要在 Dev Hub 中手动启用一个 Setup 开关,任何 CLI 命令或 元数据部署都无法切换它。Setup → Dev Hub,以下两项都要启用:
- Enable Dev Hub,以及
- Enable Unlocked Packages and Second-Generation Managed Packages ← 真正的门槛。
在启用 #2 之前,sf package create 会返回 NOT_FOUND,任何 Package2 查询
都会返回 sObject type 'Package2' is not supported。没有 CLI 变通方法 —
启用该开关。开始前先验证:
# clean "0 records" = 2GP ON; "sObject type 'Package2' is not supported" = toggle OFF
sf data query --target-org devhub --use-tooling-api --query "SELECT Id FROM Package2 LIMIT 1"
sf org display --target-org devhub --json | jq '.result.isDevHub'
选择一种类型
这三种都是 2GP(使用相同的 sf package CLI)。创建前先选好 — 这会决定
命名空间、安装时 Bundle 的命名方式,以及是否能共存。
| Managed | Unlocked — namespaced | Unlocked — org-dependent | |
|---|---|---|---|
| 命名空间 | 必需 | 必需 | 无(空字符串 "") |
| 源代码可见性 | 隐藏(受 IP 保护) | 可见/可编辑 | 可见/可编辑 |
| 安装后的名称 | ns__Name | ns__Name | 裸名称 Name(扁平) |
| 能否与本地同名 Bundle 共存 | 可以(按命名空间过滤) | 可以(按命名空间过滤) | 不可以 — 会冲突 |
| 升级行为 | 干净替换(锁定) | 替换,覆盖订阅者的修改 | 替换,覆盖订阅者的修改 |
| 升级失败时的回滚风险 | 有 | 有 | 无 |
| 典型用途 | ISV/AppExchange 分发 | 与 org 无关的共享,源码开放 | 包依赖目标 org 中已有的元数据 |
带命名空间的类型(managed、unlocked-namespaced)需要先注册命名空间, 并将其关联到此 Dev Hub(App Launcher → Namespace Registries)。没有 已注册的命名空间?请使用org-dependent unlocked — 它不需要命名空间。
关联方式(命名空间 ⇄ Dev Hub)
命名空间位于拥有它的独立 Developer Edition(DE)org 中; Dev Hub 通过关联的注册项借用它。具体步骤如下:
- 注册一个 DE org,并在其中注册命名空间(Setup → Package Manager → Namespace Registrations)。
- 在 Dev Hub 中,打开 App Launcher → Namespace Registries → Link Namespace, 使用 DE org 的凭据登录,将命名空间关联到此 Dev Hub。
- 在
sfdx-project.json中将namespace设置为已关联的命名空间标识。如果此处的 值未关联到目标 Dev Hub,sf package version create会因命名空间错误而失败(请参阅故障排除)。
一个 DE org 可以包含多个命名空间,一个 Dev Hub 也可以关联多个 DE org — 因此,单个 Dev Hub 可以在多个命名空间下构建包。命名空间会在创建版本时锁定, 并随构建版本中的每一条 UI Bundle 记录一起保留;之后无法更改。
运行时模型 — 为什么风格很重要
回答常规问题时,你不必解释这些内容。用户询问“为什么”时再详细说明:为什么托管包会隐藏源代码、为什么命名空间安装后名称是 ns__Name、为什么有些 URL 看起来不同,或者为什么无锁定升级会清除他们的修改。
- 来源隔离。 每个已安装的 UI Bundle 都从自己的
*.salesforce.app来源渲染,与salesforce.com核心 UI 不同。分层如下:salesforce.com— 第一方核心 UI*.salesforce.app— 由 AFS 托管的第二方 Bundle(无命名空间)<ns>.salesforce.app— 第三方/命名空间化(托管包 + 命名空间化无锁定包) 每个命名空间都有自己的子域名,因此来自不同包的两个 Bundle 可以共存,不会发生跨来源干扰。
- IP 保护仅适用于托管包。 对于托管包,在订阅者组织中,
getSourceZip()会返回 null — 编译后的dist/以不透明内容存储,不会交还给订阅者。对于无锁定包(命名空间化或依赖组织),订阅者可以完整读取所提供的二进制文件。 - 安装语义。 托管包和命名空间化无锁定包会以
ns__Name的名称安装,并可与同名本地 Bundle 共存。依赖组织的无锁定包没有命名空间 — 它会以裸名称Name安装,并与具有相同开发者名称的本地 Bundle 冲突。 - 增量升级。 使用
sf package install安装较新的04t…时,平台会将每个传入dist/资源的内容索引哈希与已存储内容进行比较,并跳过哈希未变化的资源 — 补丁若只涉及一个 Bundle,就只会重写该 Bundle 中已更改的文件。开发者拥有的构件(dist/、ui-bundle.json、ISV 基础权限集)会被替换;订阅者拥有的状态(订阅者创建的权限集、自定义元数据、已配置的域)会被保留。 - 紧急停用开关。 Setup → Security → Multi-Framework Domains → 禁用已配置的域。会立即返回 404;元数据仍保持已安装状态,且可以恢复。
第 1 部分 — 让现有 Bundle 可打包
准备项目,使其可以打包并在组织中使用。只应用请求所需的更改。
1a. 在 sfdx-project.json 中设置 API 版本 + 命名空间
此处的 namespace 决定你可以构建哪种风格(参见上表),因此请谨慎设置 — 没有安全的默认值。将 <ns> 替换为用户实际注册的命名空间;依赖组织时使用 ""。
# namespaced (managed / unlocked-namespaced): <ns> MUST be registered & linked to this Dev Hub
node -e "const fs=require('fs'),f='sfdx-project.json',j=JSON.parse(fs.readFileSync(f)); j.sourceApiVersion='68.0'; j.namespace='<ns>'; fs.writeFileSync(f,JSON.stringify(j,null,2))"
# org-dependent unlocked: no namespace
node -e "const fs=require('fs'),f='sfdx-project.json',j=JSON.parse(fs.readFileSync(f)); j.sourceApiVersion='68.0'; j.namespace=''; fs.writeFileSync(f,JSON.stringify(j,null,2))"
cat sfdx-project.json # confirm namespace + sourceApiVersion before packaging
- 托管包/命名空间化无锁定包 →
namespace= 已注册且已关联的命名空间。 - 依赖组织的无锁定包 → 将
namespace留空,即""。 - 设置未注册到此 Dev Hub 的命名空间,会导致后续构建失败(参见故障排除)。
1b. 构建 Bundle — 打包前必须存在 dist/
cd force-app/main/default/uiBundles/MyReactApp # the real bundle dir
npm install --no-audit --no-fund
npm run build
cd -
在 dist/ 存在之前就进行打包或部署,应用虽然可以安装,但渲染结果会是空白 — Bundle 会随包一同发布其构建产物。务必先构建。
1c. 配置 CustomApplication(仅当此包必须作为 Salesforce 应用启动时)
如果此包已通过其他方式引用(嵌入在
FlexiPage、Experience Cloud 站点等中),请跳过此步骤。否则,读取
<SKILL_DIR>/assets/CustomApplication.app-meta.xml(其中 <SKILL_DIR> 是此技能自身目录的
绝对路径),将每个 MyReactApp 替换为实际的捆绑包开发者名称,并将结果写入
用户项目中的 <packageDir>/applications/。在 <uiBundle> 中填写捆绑包的
开发者名称——在同一包内无需前缀;跨命名空间时,它会解析为 ns__Name(有命名空间)或 c__Name(无命名空间)。
应用启动器磁贴实际关注的三个字段——正确设置后,已安装的订阅者不会看到损坏的磁贴:
<uiType>Lightning</uiType>——应用启动器渲染磁贴所必需<navType>Standard</navType>——标准导航容器<formFactors>Large</formFactors>——桌面设备规格(仅在安装时验证,因此缺失或错误的值虽然能通过部署,却会隐藏磁贴)
mkdir -p force-app/main/default/applications
# then write the substituted template to:
# force-app/main/default/applications/<BundleName>.app-meta.xml
1d. 将源代码部署到 Dev Hub(以便在 package create 之前创建元数据)
sf project deploy start --source-dir force-app --target-org devhub --api-version 68.0 --wait 30
1e. 通过权限集授予应用可见性(仅当 1c 添加了 CustomApplication,且应用必须无需手动点击 Setup 即可访问时)
读取 <SKILL_DIR>/assets/PermissionSet.permissionset-meta.xml,将
MyReactApp 替换为实际的捆绑包名称(包括 <application> 和标签中的内容),
将结果写入用户项目,然后部署并分配:
mkdir -p force-app/main/default/permissionsets
# write the substituted template to:
# force-app/main/default/permissionsets/<BundleName>_Access.permissionset-meta.xml
sf project deploy start --source-dir force-app/main/default/permissionsets/MyReactApp_Access.permissionset-meta.xml --target-org devhub --api-version 68.0 --wait 30
sf org assign permset --name MyReactApp_Access --target-org devhub
第 2 部分 — 创建包(仅限 Dev Hub)
不涉及订阅者组织。sf package create 只运行一次(注册
0Ho… 容器);之后可反复构建可安装的 04t… 版本。选择上文选定的一种风格:
# managed
sf package create --name MyReactApp --package-type Managed --path force-app --target-dev-hub devhub
# unlocked, namespaced (namespace comes from sfdx-project.json)
sf package create --name MyReactApp --package-type Unlocked --path force-app --target-dev-hub devhub
# unlocked, org-dependent (no namespace)
sf package create --name MyReactApp --package-type Unlocked --org-dependent --path force-app --target-dev-hub devhub
然后构建一个版本:
sf package version create --package MyReactApp --installation-key-bypass --wait 20 --target-dev-hub devhub
# a specific/patch version instead:
sf package version create --package MyReactApp --version-number 1.0.1 --wait 20 --target-dev-hub devhub
--version-number 1.0.0.NEXT 会自动递增构建号;固定版本号 1.0.1 则会锁定
版本号。--installation-key-bypass 会构建不受保护的版本(安装时无需密钥);省略此选项并传入 --installation-key <key>,即可限制安装。
稳健地创建版本(应对 Dev Hub 队列缓慢)
构建在队列中等待时,--wait 可能超时,导致请求句柄丢失。以异步方式提交,捕获 08c… ID,然后轮询:
REQ=$(sf package version create --package MyReactApp --installation-key-bypass \
--skip-validation --target-dev-hub devhub --json | jq -r '.result.Id')
echo "request: $REQ"
while :; do
J=$(sf package version create report -i "$REQ" --target-dev-hub devhub --json)
ST=$(echo "$J" | jq -r '.result[0].Status'); echo "status: $ST"
case "$ST" in
Success) echo "$J" | jq -r '.result[0].SubscriberPackageVersionId'; break;;
Error) echo "$J" | jq -r '.result[0].Error[]? // "build failed"'; break;;
esac
sleep 30
done
--skip-validation 速度更快,但会生成测试版(无法提升,且测试版不能升级测试版——参见第 3 部分)。要生成可发布的构建,请去掉此选项。可随时恢复队列中的构建:
sf package version create report -i 08c… --target-dev-hub devhub
第 3 部分 — 安装 / 升级 / 卸载 / 提升
先确认订阅者别名(步骤 0)。 此处所有操作都会访问 订阅者——只有提升操作是在 Dev Hub 上运行。
# fresh install
sf package install --package 04t… --target-org subscriber --wait 10
# add --installation-key <key> if the version was built with one
# add --publish-wait 10 to wait for the version to finish publishing
# upgrade (newer version over the old)
sf package install --package 04t…v2 --target-org subscriber --upgrade-type Mixed --wait 10
# --upgrade-type: Mixed (default) | DeprecateOnly | Delete (destructive — care)
# uninstall
sf package uninstall --package 04t… --target-org subscriber --wait 20
# promote a managed version to released/immutable — runs on the DEV HUB, irreversible
sf package version promote --package 04t… --target-dev-hub devhub
测试版不能升级测试版。 将 --skip-validation(测试版)v0.2 安装到测试版 v0.1 上会失败,并提示 "Cannot upgrade beta package."。要么提升 v0.1(托管包),要么先卸载 v0.1,再安装 v0.2。
未锁定包的升级会覆盖订阅者对捆绑包所做的编辑。 依赖组织的风格在升级失败时无法回滚;有命名空间的风格则可以。
稳健地安装(确认安装确实完成)
sf package install --wait 可能在请求仍处于 IN_PROGRESS 状态时以退出代码 0 结束——这会造成假成功。请验证:
sf package install --package 04t… --target-org subscriber --wait 20 --no-prompt
sf package installed list --target-org subscriber --json \
| jq -r '.result[]? | select(.SubscriberPackageVersionId=="04t…") | .SubscriberPackageVersionId'
如果没有输出 → 服务端仍在处理;请等待几分钟后再运行 sf package installed list 轮询,然后再判断安装是否失败。
第 4 部分 — 调试 / 检查
大多是只读操作。可使用以下命令诊断故障或检查状态。
# Dev Hub state
sf org display --target-org devhub --json | jq '{isDevHub:.result.isDevHub, user:.result.username, instance:.result.instanceUrl, api:.result.apiVersion}'
# Is 2GP on? (the #1 root cause)
sf data query --target-org devhub --use-tooling-api --query "SELECT Id, Name, NamespacePrefix, ContainerOptions FROM Package2"
# Packages & versions on the Dev Hub
sf package list --target-dev-hub devhub
sf package version list --packages MyReactApp --target-dev-hub devhub --verbose
# Version-create failures — status + Error[]
sf package version create list --target-dev-hub devhub
sf package version create report -i 08c… --target-dev-hub devhub
sf data query --target-org devhub --use-tooling-api \
--query "SELECT Id, Status, Package2Id, Error FROM Package2VersionCreateRequest ORDER BY CreatedDate DESC LIMIT 5"
# A version's details
sf package version report --package 04t… --target-dev-hub devhub
# What's installed in the subscriber
sf package installed list --target-org subscriber --json \
| jq -r '.result[]? | "\(.SubscriberPackageName) \(.SubscriberPackageVersionNumber) \(.SubscriberPackageVersionId)"'
# Install/uninstall stuck IN_PROGRESS
sf package install report --request-id 0Hf… --target-org subscriber
sf package uninstall report --request-id 06y… --target-org subscriber
# Deploy failures (before you can even package)
sf project deploy start --source-dir force-app --target-org devhub --dry-run --wait 30
sf project deploy report --target-org devhub
# Bundle renders blank — confirm built assets shipped
ls -la force-app/main/default/uiBundles/MyReactApp/dist
故障排查
| 症状 | 原因/修复方法 |
|---|---|
sObject type 'Package2' is not supported | 2GP 开关已关闭——前往 Setup → Dev Hub,启用 "Unlocked & Second-Gen Managed Packages"(手动操作,没有 CLI 修复方法)。 |
sf package create → NOT_FOUND | 同上——尚未配置 2GP。启用开关,然后重新认证。 |
启用后 isDevHub: false/null | CLI 登录凭据已缓存——重新认证。以 Package2 查询和 package create 为准,不要依赖缓存的标志。 |
version create 卡住/--wait 超时 | 构建已排队。使用异步提交,然后通过 version create report -i 08c… 轮询;稍后使用相同 ID 继续。 |
install --wait 以 0 退出,但应用缺失 | 服务端状态仍为 IN_PROGRESS。通过 sf package installed list 确认;等待几分钟后再次轮询。 |
| "Cannot upgrade beta package" | Beta 版本不能升级 Beta 版本。先发布 v0.1(managed),或先卸载它,再安装 v0.2。 |
| managed/带命名空间的构建出现命名空间错误 | 命名空间尚未注册/关联到此 Dev Hub(App Launcher → Namespace Registries),或改用组织依赖型 unlocked(无命名空间)。 |
| 应用已安装,但显示空白 | 部署/打包前未构建 Bundle——运行 npm run build,确认存在 dist/,重新部署并重建版本。 |
| 安装到了错误的组织 | 第 0 步确认的别名有误。重新检查 sf org list;subscriber ≠ devhub。 |
| 组织依赖型 Bundle 与本地 Bundle 冲突 | 两者都使用不带前缀(null-prefix)的名称。使用带命名空间的版本,或重命名。 |
备注
- 先确认组织。 始终需要 Dev Hub;只有安装/升级/ 卸载时才需要 subscriber。仅创建任务不需要询问 subscriber。
- 完整运行的顺序: 构建 Bundle → 部署源代码 →
package create(仅一次)→package version create(每次发布)→install→promote(仅限 managed)。 - 关于 Salesforce 内部打包问题,请咨询权威渠道 #packaging。
- 权威外部文档(供深入参考):
- Second-Generation Managed Packaging Developer Guide — https://developer.salesforce.com/docs/atlas.en-us.pkg2_dev.meta/pkg2_dev/sfdx_dev_dev2gp.htm (managed/AppExchange 版本:工作流、组件、分发、推送升级、 1GP→2GP 差异)。
- Unlocked packages 使用相同的
sf packageCLI;关于带命名空间的 unlocked 和 组织依赖型版本,请参阅同一指南中的 "Unlocked Packages" 部分。