generate-ppmplugin-manifest について
サードパーティ製ネイティブコントロールのリポジトリ向けスキルで、プラグインの manifest を検証し、対象プラットフォーム用のコピーをステージングします。通常はコミット済みの契約ファイルを保持し、存在しない場合はネイティブソースから生成できます。
主な機能
- 01manifest の構造と項目を検証
- 02対象プラットフォームに応じた entrypoints の設定
- 03ビルド用コピーのステージング
- 04ソースからの manifest 生成
活用シーン
- 01Android または iOS プラグイン出荷前の検証
- 02対象プラットフォーム向け manifest の準備
インストール
コマンドをコピーしてターミナルで実行します。
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 を読みます。スキーマ、導出テーブル、このスキルが適用する検証ルールの信頼できる情報源です。
マニフェストは 2 つ、契約は 1 つです。 ./manifest.json(リポジトリルート、コミット対象)は信頼できる契約の情報源であり、モジュールがサポートするすべてのプラットフォームを宣言します。ppmplugin/staging/manifest.json(gitignore 対象)は、このステージが生成するビルドコピーです。同じ契約ですが、entrypoints は実際に出荷するプラットフォームに絞り込まれています。ビルド/アセンブル用スキルが読み取るのは常にステージング済みコピーのみです。コミット済みのルートファイルは PCF と人間が読み取るものです。
このスキルで行わないこと
- コンパイルは行いません。Gradle も DEX も使用しません。それは
/build-android-binaryの役割です。 - バンドルの zip 化は行いません。それは
/assemble-ppmpluginの役割です。 - 宣言されたプラットフォームのバイナリが実際に存在するかは検証しません。意図された
entrypointsをステージングします。実際にステージングされたバイナリとの照合は/assemble-ppmpluginが行い、不一致があればゲートで停止します。 - 通常の経路では、コミット済みの
./manifest.jsonは書き換えず、読み取るだけです。フォールバックの場合(コミット済みマニフェストが存在しない場合)に限り、./manifest.jsonを書き込みます。src/、ios/、android/、PRD、package.jsonには、読み取り以外の操作を行いません。
ステップ 1 — 共通ドキュメントと前提条件ブロックを読む
shared/shared-instructions.md、shared/naming-conventions.md、およびshared/ppmplugin-format.mdを読みます。- このスキルではインストール、認証、ネットワーク通信を行いません。共通指示 §9.2 に従って、前提条件ゼロのブロックを出力します。
Prereq check — /generate-ppmplugin-manifest: skipped (skill does no installs / auth / network — failures surface at validation).
- 作業ディレクトリがサードパーティコントロールのリポジトリであることを確認します。
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)を出力して停止します。 .extension-state.mdを読みます。## ppmplugin (third-party controls)ブロックがある場合は、最後に選択されたターゲットと最後のマニフェスト書き込みを記録します(以下の再実行モードで使用します)。
ステップ 1.5 — ソースマニフェストの場所を確認する(どちらのモードか?)
このステージには 2 つのモードがあり、コミット済みの ./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を持つ.m(RCT_EXPORT_MODULEではありません)、および 1 つ以上のRCT_EXPORT_METHOD。
構造が不完全なプラットフォームは有効なターゲットではありません — 対象から除外し、不足している要素を明示してください。どちらのプラットフォームも構造上の要件を満たさない場合は、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 シェルでの検証がまだ必要です)。ユーザーが iOS/両方を対象にする場合、マニフェストはentrypoints.iosを宣言します。パッケージング時に iOS バイナリが配置されていなければ、/assemble-ppmpluginでは引き続きゲートされます。
次に、推測ではなく実際のファイルからフィールドを導出します(author-from-source モードのみ。validate-and-stage モードでは、./manifest.json から読み取ったフィールドを使ってステップ 3 に進みます)。
-
クラス名
<Pascal>— ネイティブモジュール(Android の<Pascal>Module.ktファイル名/そのgetName()="<Pascal>Module"、または iOS のRCT<Pascal>Module)か、.extension-state.mdの Identity ブロックから取得します。このトラックにはsrc/<Pascal>Extension.tsはありません。これをname、nativeModule、receivers[].nameの基準にします。 -
version—package.jsonのversion。 -
nativeModule— Android モジュールのoverride fun getName(): String = "<X>"を読み取ります。<X>をそのまま使用します。 -
packageClass—ReactPackageの.ktファイルを読み取ります。package <...>行とクラス名を組み合わせて FQN を作成します(例:com.powerapps.peninput.PenInputPackage)。 -
methods— Android モジュールをスキャンし(このスキルは OS に依存しないため、シェル固有のコマンドではなく Read/Grep ツールを使用)、@ReactMethodを探して、注釈付きの各関数名を収集します。件数をARCHITECTURE.md §3/PRD §4の操作と照合します。ドキュメントに記載された操作に対応する@ReactMethodがない場合は、警告として提示します(マニフェストにはバイナリが実際に公開する内容を反映しますが、不一致は通常、操作が接続されていないことを意味します)。 -
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-case と異なる場合(例:リポジトリ名がpowerapps-pdf-controlで、クラス名がPdfViewer→name: pdf-viewer)、.ppmpluginのファイル名がリポジトリ名と一致しないことをユーザーが把握できるよう、1 行の注記を表示します。正規プレフィックスのルールを満たすために必要です。receivers[].name— PCF がある場合は、デフォルト値をそのまま使わず、PCF から取得します。 兄弟 PCF(pcf/<…>/index.ts)が存在する場合は、ディスパッチキーのCOMPOSITE_KEY = "<name>/<receiver>"(または、それがバインドするReceiverKey)を grep し、その<receiver>の値を使用します。PCF はすでにその値を使ってディスパッチするためです。異なるレシーバー名を登録したマニフェストは、最初のディスパッチで失敗します(実際のバグ例:PCF はSnapshotにディスパッチする一方、マニフェストではデフォルトのDeviceInfoExtensionが指定されていた)。PCF がない場合に限り、<Pascal>Extensionにフォールバックします。いずれの場合も、選択した値を確認事項として提示します: 「PCF は<name>/<receiver>にディスパッチします。マニフェストはレシーバー<receiver>を登録します。よろしいですか?」(監査時に再確認します。/audit-ppmpluginのpcf-composite-key-matches-receiverを参照。)
receivers[]はランタイムのディスパッチ契約です(ppmplugin-format §2 — ランタイムのディスパッチ契約)。wrap ホストは、複合キー<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: "<step 4 の FQN>" } - iOS/Both →
entrypoints.ios = { framework: "<Pascal>Plugin", moduleClass: "RCT<Pascal>Module" }
- Android/Both →
ステップ 3 — ローカルで検証する(事前チェックゲート)
導出したマニフェストに対して、ppmplugin-format.md §4 のすべてのルールを実行します。それぞれについて、正確なルールとともに合格または不合格を示してください。
nameが^[a-z0-9][a-z0-9-]{0,63}$に一致する- 各
nativeModuleが、nameの標準プレフィックスで始まる —nameを-/_で分割し、各セグメントを PascalCase にして結合することで正確に算出します(ppmplugin-format §3)。Ordinal/大文字小文字を区別します。(helloworld→Helloworldとなる細かな違いに注意してください。往復変換が成立するよう、kebab(className)はキャメルケースの境界でハイフン区切りにする必要があります。) nativeModuleが予約済みプレフィックスで始まらない(§4 のリストは大文字小文字を区別しない)- 既知の互換性のない完全一致名が
nativeModuleに使われていない — §4 のローカル確認済みサブセット(DeviceInfo、AuthenticationHelper、NetworkClient、DataverseOfflineProvider、IntuneMAM)と一致する場合はブロックします(Ordinal、大文字小文字を区別)。このサブセットは網羅的ではありません。ローカル検証に合格しても、アップロードサービスがその名前を受け入れるとは限りません。モジュールの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は、wrap ホストが読み込むすべてのプラグイン間で共有される名前空間です)。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のいずれも含まれない(バンドルはネイティブのみです。これらはwrapランタイムが読み取らなくなった残存物です — §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)。承認されたら、次を行います。
.gitignoreにppmplugin/が含まれていることを確認します(なければ行を追加します)。manifest.jsonは追加しないでください(コミット済みのルートファイルは追跡対象のままにします)。- ステージ済みのビルド用コピー
ppmplugin/staging/manifest.jsonを書き込みます。これは、選択したターゲットに合わせてentrypointsを絞り込んだ契約です。/assemble-ppmpluginが ZIP 化するファイルです。 - ソースから作成するモードの場合のみ: リポジトリルートにコミットする
./manifest.json(すべての実行可能なプラットフォームを含む完全な契約)も書き込みます。これは、通常ならスキャフォールドが生成していた信頼できる唯一のソースです。検証してステージするモードでは、ステップ 1.5 でドリフト更新の提案をユーザーが承認した場合を除き、./manifest.jsonはそのままにします(すでにソースだからです)。 .extension-state.mdの## ppmplugin (third-party controls)ブロックを更新します(存在しない場合は作成します。スキーマはrepo-layout.mdを参照):Targetを設定し、Manifest: staged <ISO timestamp>とします。再実行モード(ステップ 1.5)とビルド/アセンブルのスキルは、この値を状態の参照に使います。
マニフェストのパスとともに DONE を返します。その後、AskUserQuestion を使って次のステップを提示します(shared-instructions §9.1)。選択したターゲットに対応するビルドスキルを提示します。
- Run /build-android-binary — ターゲットが Android または Both の場合(Kotlin モジュールをコンパイルして DEX を生成します)
- Run /build-ios-binary — ターゲットが iOS または Both の場合(Obj-C モジュールをコンパイルしてフレームワークを生成します。Mac 専用です)
- Run /assemble-ppmplugin — バイナリがすでに存在する場合のみ
- Stay — I'll review the manifest first
(Both ターゲットの場合は、両方のビルドスキルを提示します。AskUserQuestion の選択肢は最大 4 つです。)
ユーザーが Run /… の選択肢を選んだ場合は、同じターン内で Skill ツールを使ってただちにそのスキルを呼び出します(サブスキルの呼び出しについては shared-instructions §8 と §9.1「Execute, don't describe」を参照)。「準備ができたら実行してください」という指示を出して終了してはいけません。選択肢の選択は、実行のリクエストそのものです。「Stay」を選んだ場合のみ、ここで実行を終了します。
実例(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 のケバブケース)、nativeModule は PenInputModule(予約済みの単独名を避けるため Module サフィックスを付けています)。正規のプレフィックス PenInput で始まり、予約済みのプレフィックスではありません ✓。)