mcpField
G
generate-ppmplugin-manifest

generate-ppmplugin-manifest

github.com/microsoft开发1.1k71 次安装今天更新
代码仓库

关于 generate-ppmplugin-manifest

用于第三方原生控件仓库的技能,验证插件 manifest.json,并按待发布平台生成暂存副本。它区分已提交的契约文件与构建用副本,缺少源文件时也可从原生代码生成。

主要功能

  1. 01验证 manifest.json 结构与字段
  2. 02按目标平台调整 entrypoints
  3. 03写入暂存构建副本
  4. 04无源清单时从代码生成

适用场景

  1. 01发布 Android 或 iOS 插件前检查清单
  2. 02为指定平台准备暂存 manifest.json

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/microsoft/power-platform-skills --skill generate-ppmplugin-manifest

安装后在 Claude Code 中输入/generate-ppmplugin-manifest确认已加载。

详细介绍

/generate-ppmplugin-manifest

暂存放入 .ppmplugin 包中的 manifest.json——这是 wrap 运行时和上传服务读取的小型描述文件,用于识别插件并将调用路由至插件。在常规流程中,已提交的 ./manifest.json 已存在(由 /generate-native-extension 在所描述的代码旁创建);此阶段负责验证该文件、将其 entrypoints 与要发布的目标平台协调一致,并写入暂存构建副本 ppmplugin/staging/manifest.json。这是“确保字符串正确”的关卡:执行迅速,无需构建工具,并可在任何 Android 构建开始前捕获常见的上传失败。如果仓库中没有 ./manifest.json(手工编写的原生模块跳过了脚手架),此阶段会从源代码创建该文件作为回退方案——参见步骤 2。

阅读 shared/ppmplugin-format.md——这是本技能所执行的架构、派生表和验证规则的权威来源。

两个清单,一份契约。 ./manifest.json(仓库根目录,已提交)是事实来源契约——它声明模块支持的所有平台。ppmplugin/staging/manifest.json(被 git 忽略)是此阶段生成的构建副本——契约相同,但 entrypoints 会精简为实际要发布的平台。构建/组装技能只会读取暂存副本;PCF 和人员读取的是已提交的根目录文件。

此技能不会做什么

  • 不会编译任何内容——不运行 Gradle,也不生成 DEX。这由 /build-android-binary 负责。
  • 不会压缩该包——这由 /assemble-ppmplugin 负责。
  • 不会验证已声明平台的二进制文件是否实际存在——它会暂存预期的 entrypoints;/assemble-ppmplugin 会将它们与实际暂存的二进制文件进行协调,并在不匹配时拦截。
  • 常规路径下不会重写已提交的 ./manifest.json——只会读取它。仅在回退情况下(不存在已提交的清单)才会写入 ./manifest.json。除读取外,绝不会改动 src/、ios/、android/、PRD 或 package.json。

步骤 1 — 阅读共享文档和先决条件部分

  1. 阅读 shared/shared-instructions.md、shared/naming-conventions.md 和 shared/ppmplugin-format.md。
  2. 此技能不会执行安装、身份验证或网络操作。打印零先决条件部分(依据 shared-instructions §9.2):
Prereq check — /generate-ppmplugin-manifest: skipped (skill does no installs / auth / network — failures surface at validation).
  1. 确认当前工作目录是第三方控件仓库:存在 package.json(名称为仅供开发使用、私有的普通 <kebab>-control——不是已发布的 @powerapps/extension-* 作用域;此流程发布的是二进制文件,而非 npm 包——参见 repo-layout.md),并且存在 android/ 和/或 ios/ 原生模块。如果不是,则停止并提示 NEEDS_CONTEXT: not a third-party-control repo (no package.json / native module)。
  2. 阅读 .extension-state.md——如果其中包含 ## ppmplugin (third-party controls) 部分,记录上次选择的目标平台和上次写入清单的记录(下文的重新运行模式会用到)。

步骤 1.5 — 定位源清单(我们处于哪种模式?)

此阶段有两种模式,取决于已提交的 ./manifest.json 是否存在:

  • 验证并暂存模式(正常流程)。 仓库根目录中存在 ./manifest.json(由 /generate-native-extension 编写)。这是事实来源——不要重新编写它。读取并验证它(步骤 3),将其 entrypoints 与选定目标协调一致(仅步骤 2 → 目标),并写入暂存副本(步骤 4)。跳过步骤 2 的字段推导——契约已编写完成;你要做的是验证并暂存它,而不是重新生成它。(可以选择从 Android 模块源代码重新推导 methods;如果发现它们与 ./manifest.json 不一致——例如在脚手架生成后手动添加了 @ReactMethod——则展示差异并提出更新已提交文件;绝不要静默重写。)

  • 从源代码创建模式(备用)。 不存在 ./manifest.json——这是跳过脚手架、手动编写的原生模块。根据源代码机械地推导每个字段(完整执行步骤 2),然后写入已提交的 ./manifest.json 和暂存副本(步骤 4)。

在构建会话中重新运行。 如果上次运行已生成暂存副本 ppmplugin/staging/manifest.json,不要盲目覆盖——比较目标/入口点与已提交源文件的差异,并通过 AskUserQuestion 询问:更新(根据当前 ./manifest.json + 目标重新暂存)/ 保持原样(报告并停止)。将目标默认设为 .extension-state.md 中上次选择的目标;不要重复询问已回答的问题。


步骤 2 — 确定目标;仅在从源代码创建模式下推导字段

以下结构预检和目标选择在两种模式下都会执行(你始终需要了解哪些平台可用,以及要发布哪些平台)。字段推导小节(条目 1–6 + 计算部分)仅在从源代码创建模式下执行(步骤 1.5)——在正常的验证并暂存模式下,字段已保存在 ./manifest.json 中;从中读取字段并跳过推导,只保留目标选择。

首先,运行结构预检。 ppmplugin 路径要求采用规范的 PAM 扩展布局——即 /generate-native-extension 生成的结构(参见 shared/repo-layout.md)。手工创建、外部来源或已偏离规范的仓库可能缺少某些部分;现在就准确指出缺少什么,避免之后出现难以理解的 Gradle/Xcode 错误,或生成内容错误却未被察觉的清单。打印一个清晰可见的 ✓/✗ 区块:

  • 通用: package.json(仅用于开发的 <kebab>-control 名称——此流程中没有 @powerapps/extension-* 作用域,也没有 src/ TS 层);可从原生模块(android/.../<Pascal>Module.kt / ios/RCT<Pascal>Module.m)、./manifest.json 或 .extension-state.md 解析出类名。
  • Android(如果存在 android/):android/build.gradle;一个继承 ReactContextBaseJavaModule 且包含 override fun getName() 的 Kotlin 模块类;一个 ReactPackage 类;至少 1 个 @ReactMethod。
  • iOS(如果存在 ios/):声明 <RCTBridgeModule> 的 ios/RCT<Pascal>Module.h;包含 + (NSString *)moduleName(不是 RCT_EXPORT_MODULE)和至少 1 个 RCT_EXPORT_METHOD 的 .m 文件。

结构不完整的平台不是有效目标——将其排除,并说明缺少哪个元素。如果两个平台的结构都不完整,则以 NEEDS_CONTEXT: repo doesn't match the expected PAM-extension layout — missing <list> 为内容停止,并指向 shared/repo-layout.md。(在暂存副本上操作可以保护源文件;但它不会补出缺失的模块——这正是进行此预检的原因。) 然后确定此捆绑包面向哪些平台(仅从结构完整的平台中选择)——这将决定声明哪些 entrypoints:

  • 通过 AskUserQuestion 确认:仅 Android / 仅 iOS / 两者——只提供预检通过的目标平台。默认选择当前已有的平台;如果是在 Update 模式下重新运行,则默认选择 .extension-state.md 中记录的先前目标平台。
  • 注意可用性:/build-android-binary 已稳定;/build-ios-binary 仍处于 v0 阶段(仅限 Mac;**已知限制:**其 React-Core 弱链接配置仍需通过实际 PAM/wrap shell 验证)。如果用户选择 iOS/两者,清单将声明 entrypoints.ios;如果打包时尚未暂存 iOS 二进制,/assemble-ppmplugin 仍会进行拦截。

然后根据实际文件而非假设推导字段(仅限 author-from-source 模式——在 validate-and-stage 模式下,跳到步骤 3,从 ./manifest.json 读取字段):

  1. 类名 <Pascal>——从原生模块(Android 的 <Pascal>Module.kt 文件名 / 其 getName() = "<Pascal>Module",或 iOS 的 RCT<Pascal>Module)或 .extension-state.md 的 Identity 部分获取。此流程中没有 src/<Pascal>Extension.ts。这是 name、nativeModule、receivers[].name 的依据。

  2. version——package.json 中的 version。

  3. nativeModule——读取 Android 模块的 override fun getName(): String = "<X>"。原样使用 <X>。

  4. packageClass——读取 ReactPackage 的 .kt 文件:将其 package <...> 行与类名组合起来 → FQN(例如 com.powerapps.peninput.PenInputPackage)。

  5. methods——使用 Read/Grep 工具(而非特定于 shell 的命令;此技能与操作系统无关)扫描 Android 模块中的 @ReactMethod,并收集每个带注解的函数名。将数量与 ARCHITECTURE.md §3 / PRD §4 中记录的操作进行交叉核对;如果某项已记录的操作没有对应的 @ReactMethod,请将其作为警告指出(清单反映的是二进制实际公开的内容,但不匹配通常意味着某项操作尚未接入)。

  6. iOS 入口点字段(仅当目标平台为 iOS / 两者时)——读取 ios/RCT<Pascal>Module.h 获取类名(@interface RCT<Pascal>Module : NSObject <RCTBridgeModule>)→ 这就是 entrypoints.ios.moduleClass。读取 ios/RCT<Pascal>Module.m 中的 + (NSString *)moduleName { return @"<X>"; },并断言 <X> 与步骤 3 中的 nativeModule 相同——这是两个平台上的同一个桥接符号;若不匹配,说明 iOS 和 Android 模块存在不一致。(.m 不得使用 RCT_EXPORT_MODULE——该宏的 +load 注册对框架的 dlopen 扁平命名空间不可见,因此模块不会在设备上加载。)

然后根据 ppmplugin-format.md §3 计算:

  • name = kebab(className)——**根据类名计算,而不是根据功能/仓库名称。**如果 kebab(className) 与仓库的功能 kebab 不同(例如仓库为 powerapps-pdf-control,但类名为 PdfViewer → name: pdf-viewer),请打印一行说明,让用户知道 .ppmplugin 文件名不会与仓库名称一致。这是通过规范前缀规则所必需的。
  • receivers[].name——如果存在 PCF,则从 PCF 获取,不要盲目使用默认值。如果存在同级 PCF(pcf/<…>/index.ts),搜索其中的分发键——COMPOSITE_KEY = "<name>/<receiver>"(或其绑定的 ReceiverKey)——并使用该 <receiver> 值,因为 PCF 已经会向其分发;如果清单注册了不同的接收器名称,首次分发时就会失败(实际问题:PCF 分发到 Snapshot,而清单默认使用了 DeviceInfoExtension)。只有在不存在 PCF 时,才回退为 <Pascal>Extension。无论哪种情况,都要将选定的值作为确认项向用户确认: “PCF 分发到 <name>/<receiver>;清单将注册接收器 <receiver>——请确认?”(Audit 会重新检查此项——参见 /audit-ppmplugin pcf-composite-key-matches-receiver。)

receivers[] 是运行时分发契约(ppmplugin-format §2 — 运行时分发契约)。封装宿主通过复合键 <name>/<receivers[].name> 将调用路由到 NativeModules.<nativeModule>.<method> —— 纯原生包中没有 TS handleMessageAsync / sendAsync 层。因此,nativeModule 必须等于模块的 getName(),且 methods 中的每个条目都必须是真实的 @ReactMethod / RCT_EXPORT_METHOD 名称(宿主会直接调用该方法;如果方法不存在,设备上会报错 method '<m>' not found)。methods 必须从模块源码推导,切勿猜测。

  • entrypoints —— 仅声明选定的目标:
    • Android / Both → entrypoints.android = { dex: "<Pascal>Plugin.dex", packageClass: "<FQN from step 4>" }
    • iOS / Both → entrypoints.ios = { framework: "<Pascal>Plugin", moduleClass: "RCT<Pascal>Module" }

步骤 3 —— 在本地验证(预检关卡)

对推导出的清单运行 ppmplugin-format.md §4 中的每条规则。逐条给出通过或失败结果,并注明确切规则:

  • name 符合 ^[a-z0-9][a-z0-9-]{0,63}$
  • 每个 nativeModule 都以 name 的规范前缀开头——精确计算方式为:将 name 按 - / _ 拆分,将每段转换为 PascalCase,再拼接(ppmplugin-format §3),按序号区分大小写。(注意 helloworld → Helloworld 这一细微之处;kebab(className) 必须在 camelCase 边界处插入连字符,才能保证往返转换成立。)
  • 没有任何 nativeModule 以保留前缀开头(§4 中不区分大小写的列表)
  • 没有任何 nativeModule 是已知的不兼容精确名称——若匹配 §4 中本地检查的子集(DeviceInfo、AuthenticationHelper、NetworkClient、DataverseOfflineProvider、IntuneMAM),则阻止(按序号区分大小写)。该子集并不完整:本地通过并不保证上传服务会接受该名称。将模块的 getName()(以及 iOS +moduleName)重命名为非保留形式——添加 Module 后缀(DeviceInfo → DeviceInfoModule)或供应商前缀(ContosoDeviceInfo)——然后重新推导 nativeModule 以匹配。
  • nativeModule 看起来像通用平台名词 → 警告。 如果 nativeModule 匹配 ^(Device|Network|File|Audio|Camera|Sensor|Location|Storage|Notification|Bluetooth|Wifi|Media|Photo|Contact|Calendar|Battery),则警告:通用的单名词名称既容易与上传服务冲突,也容易发生名称碰撞(NativeModules 是封装宿主加载的所有插件共用的命名空间)。建议添加 Module 后缀或供应商前缀。展示警告;若用户是有意为之,则允许继续。
  • methods 非空;数量 ≤32;每项符合 ^[a-zA-Z_$][a-zA-Z0-9_$]{0,127}$
  • 每个 receivers[].name 都符合相同的 JS 标识符正则表达式
  • 不得包含 SDK 时代 / JS 层字段——清单中不得包含 entrypoints.js、entrypoints.ts、extension.js、extension.hbc、extensionClassName、jsLayer(该包仅含原生代码;这些字段是泄漏信息,封装运行时已不再读取——§4)。由于此技能负责生成清单,因此不会输出这些字段;但重新运行模式会读取现有清单,所以必须对此进行断言。

将结果打印为可见区块:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 manifest.json validation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 🟢 ✓ name 'pen-input' — valid kebab identifier
 🟢 ✓ nativeModule 'PenInputModule' starts with canonical prefix 'PenInput'
 🟢 ✓ nativeModule 'PenInputModule' — no reserved prefix
 🟢 ✓ nativeModule 'PenInputModule' — not a known reserved name
 🟢 ✓ methods ['capturePenInput'] — non-empty, valid identifiers
 🟢 ✓ receiver name 'PenInputExtension' — valid identifier
 🟢 ✓ no SDK-era / JS-layer fields (native-only manifest)
 🟢 7 checks passed, 0 failed.
 ⓘ Known incompatible-name subset checked locally; the upload service may apply additional checks.

如果任何检查失败,则停止,并输出 BLOCKED: manifest validation — <rule> 和具体修复方案(例如,“重命名 Kotlin 模块的 getName(),使其以 PenInput 开头”)。不要写入无效清单。


第 4 步 — 确认门槛 + 写入

显示清单(经过验证的源文件 + 与目标平台协调后的 entrypoints),并等待确认(shared-instructions §7.1)。获批后:

  1. 确保 .gitignore 中包含 ppmplugin/(若不存在,则追加该行)。不要添加 manifest.json(已提交的根目录文件应保持跟踪状态)。
  2. 写入暂存构建副本 ppmplugin/staging/manifest.json — 将 entrypoints 精简为所选目标平台的契约。这是 /assemble-ppmplugin 打包的内容。
  3. 仅限从源文件创作模式: 还要在仓库根目录写入已提交的 ./manifest.json(完整契约,包含所有可行平台)— 这是脚手架通常会生成的事实来源。在验证并暂存模式下,不要改动 ./manifest.json(它已是源文件),除非用户在第 1.5 步接受了漂移更新提议。
  4. 更新 .extension-state.md 中的 ## ppmplugin (third-party controls) 区块(若不存在则创建 — 架构见 repo-layout.md):设置 Target、Manifest: staged <ISO timestamp>。重新运行模式(第 1.5 步)和构建/组装技能会读取此状态。

返回 DONE 及清单路径。然后通过 AskUserQuestion 提供后续步骤(shared-instructions §9.1),为所选目标平台提供构建技能:

  • 运行 /build-android-binary — 若目标为 Android 或 Both(编译 Kotlin 模块 → DEX)
  • 运行 /build-ios-binary — 若目标为 iOS 或 Both(编译 Obj-C 模块 → framework;仅限 Mac)
  • 运行 /assemble-ppmplugin — 仅当二进制文件已存在时
  • 暂不操作 — 我先检查清单

(目标为 Both 时,列出这两项构建技能。每个 AskUserQuestion 最多提供 4 个选项。)

用户选择 Run /… 选项时,必须在同一轮中立即通过 Skill 工具调用相应技能(子技能调用,见 shared-instructions §8 + §9.1“执行,而非描述”)。不要打印“准备好后运行”之类的指示后就停止 — 选择该选项即表示请求运行。只有选择“暂不操作”才会结束运行。


操作示例(pen-input)

{
  "name": "pen-input",
  "version": "0.1.4",
  "abi": { "compatibleShells": ">=1.0.0", "builtAgainst": "1.0.0" },
  "entrypoints": {
    "android": { "dex": "PenInputPlugin.dex", "packageClass": "com.powerapps.peninput.PenInputPackage" }
  },
  "receivers": [
    { "name": "PenInputExtension", "nativeModule": "PenInputModule", "methods": ["capturePenInput"] }
  ]
}

(此示例仅针对 Android,因此省略 entrypoints.ios。若目标为 Both,还会包含 "ios": { "framework": "PenInputPlugin", "moduleClass": "RCTPenInputModule" }。name = pen-input = 类 PenInput 的 kebab-case 名称;nativeModule = PenInputModule(添加 Module 后缀以避免使用受保留的裸名称),并以规范前缀 PenInput 开头 ✓;不属于受保留前缀 ✓。)

评论

支持 Markdown