mcpField
E
experience-ui-bundle-2gp-deploy

experience-ui-bundle-2gp-deploy

github.com/forcedotcom云与 DevOps1.1k2.0k 次安装1 天前更新
代码仓库

关于 experience-ui-bundle-2gp-deploy

此参考型 skill 指导将现有 UI Bundle 制作成 Salesforce 2GP,并在其他 org 安装、升级或卸载。它面向已有项目和可构建 bundle 的开发者,按具体请求提供对应步骤,而非要求完整执行整个发布流程。

主要功能

  1. 01选择 managed 或 unlocked 包类型
  2. 02准备现有 bundle 以便打包
  3. 03创建 2GP 包及版本
  4. 04在订阅 org 安装、升级或卸载
  5. 05排查打包与安装问题

适用场景

  1. 01将现有 UI Bundle 制作为 2GP
  2. 02为现有包创建新版本
  3. 03将包安装到订阅 org
  4. 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.xml
PermissionSet.permissionset-meta.xml
scripts/
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,以下两项都要启用:

  1. Enable Dev Hub,以及
  2. 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 的命名方式,以及是否能共存。

ManagedUnlocked — namespacedUnlocked — org-dependent
命名空间必需必需无(空字符串 "")
源代码可见性隐藏(受 IP 保护)可见/可编辑可见/可编辑
安装后的名称ns__Namens__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 通过关联的注册项借用它。具体步骤如下:

  1. 注册一个 DE org,并在其中注册命名空间(Setup → Package Manager → Namespace Registrations)。
  2. 在 Dev Hub 中,打开 App Launcher → Namespace Registries → Link Namespace, 使用 DE org 的凭据登录,将命名空间关联到此 Dev Hub。
  3. 在 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 supported2GP 开关已关闭——前往 Setup → Dev Hub,启用 "Unlocked & Second-Gen Managed Packages"(手动操作,没有 CLI 修复方法)。
sf package create → NOT_FOUND同上——尚未配置 2GP。启用开关,然后重新认证。
启用后 isDevHub: false/nullCLI 登录凭据已缓存——重新认证。以 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。
  • 权威外部文档(供深入参考):

评论

支持 Markdown