modernize-ui5-app について
従来の UI5 フリースタイル JavaScript アプリを UI5 TypeScript アプリに変換し、元のアプリをそのまま残すワークフロースキルです。UI5 MCP のガイドと検証ツールを使い、モダン化を進めます。
主な機能
- 01JavaScript コントローラーを TypeScript に変換
- 02非同期読み込みと ES モジュールを設定
- 03マニフェスト駆動の構成を採用
- 04型付きイベントハンドラーを使用
- 05UI5 lint と TypeScript の検査を実行
活用シーン
- 01従来のフリースタイル UI5 アプリのモダン化
- 02UI5 JavaScript プロジェクトの TypeScript 移行
MCP 対応
変換ガイド、スキャフォールディング、API 検索、lint、マニフェスト検証に UI5 MCP を使います。必要に応じて UI5 と OData V4 の資料を調べるため sap-docs MCP も利用します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ arc-mcp/ arc-1 --skill modernize-ui5-appインストール後、Claude Code で/modernize-ui5-appを入力して読み込まれたか確認します。
詳細
UI5 のフリースタイル JS アプリを UI5 TypeScript アプリに近代化する
レガシーな UI5 フリースタイル JavaScript アプリ(典型的な 2018~2021 年頃のもの。同期ブートストラップ、JS コントローラー、jQuery.sap.*、グローバルフォーマッター、ES5 パターン、型なし、sap_belize)を、非同期読み込み、マニフェスト駆動の設定、適切な BaseController、sap_horizon テーマ、ES モジュール、型付きイベントハンドラーを備え、ui5-linter と tsc --noEmit の出力がクリーンな、最近の 1.x リリースの最新 UI5 TypeScript アプリに変換します。並行して稼働します。レガシーアプリは <source_app>/ にそのまま残し、最新アプリを <modern_app>/ に配置します。
このスキルは、RAP バックエンドの構築後に利用できる 2 つの並行 UI パスの 1 つ です。ターゲットアーキテクチャが フリースタイル TypeScript アプリ(カスタムコントローラー、手動バインディング、明示的な i18n)の場合は、こちらを選んでください。ターゲットが Fiori Elements V4 アプリ(アノテーション駆動で、カスタムコードが最小限)の場合は、代わりに convert-ui5-to-fiori-elements.md を選んでください。どちらも、migrate-segw-to-rap で生成された同じレガシー JS アプリと同じ V4 RAP サービスを出発点とします。
migrate-segw-to-rap.md (backend: SEGW V2 → RAP V4)
│
┌─────────────┴─────────────┐
▼ ▼
modernize-ui5-app.md convert-ui5-to-fiori-elements.md
(freestyle TS) (Fiori Elements V4)
パス/名前空間のプレースホルダー。
<source_app>/、<modern_app>/、<source_namespace>、<modern_namespace>はユーザー指定です。デフォルトでは、ソースはlegacy-*-app/(ターゲットの同階層)で、最新アプリの名前空間はソースに.modernを付けて生成します。
このスキルが使用する MCP(および使用しない MCP)
| MCP | 用途 | タイミング |
|---|---|---|
UI5 MCP(mcp__SAPUI5_MCP_Server__*) | 信頼できる TS 変換ガイドライン、一般的な UI5 ガイドライン、アプリのスキャフォールディング、API リファレンスの検索、リンター、マニフェストバリデーター、バージョン情報 | 全般 — このスキルの主要な MCP です |
sap-docs MCP(mcp__sap-docs__*) | OData V4 バインディングパターン、ドラフト処理、コントロールのドキュメント | V2→V4 のバインディング動作が自明でない場合(例:ドラフトでの複合キー、$expand=_Tasks、アクションの呼び出し) |
| arc-1 MCP | 任意 — サービスバインディング URL の検索、ステータスコードの意味 | V4 URL をすぐに確認できない場合のみ(例:FE アプリのマニフェストを読み取れない場合)。それ以外は省略してください |
| fiori-mcp | 使用しません | これはフリースタイル TS アプリであり、Fiori Elements ではないため、生成するアノテーションはありません |
UI5 MCP に「TS に変換」するツールはありません。変換は機械的に行い、信頼できる手順を返す mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines に従います。最初に一度だけ呼び出し、記載どおりに実行してください。
また、実行中に UI5 のベストプラクティスが自明でない場合や意見が分かれる場合は、mcp__sap-docs__search も使用してください(FCL ルーティング、V4 バインディングのセマンティクス、ドラフト処理、アクセシビリティ、テーマ設定など)。そこにインデックスされた SAP ヘルプポータルは信頼でき、バージョン固有の情報を提供します。推測せず、記載内容を引用してください。
重要な落とし穴 — コードを書く前にお読みください
このスキルの過去の実行では、診断に複数回の反復を要する問題が 3 つ発生しました。事前に把握していれば簡単に回避できますが、知らないと非常に厄介です。トラップ 1 と 2 が重なり、「画面が真っ白でコンソールエラーもない」問題のデバッグに、優秀な LLM でも約 15~20 分かかりました。
トラップ 1:FCL が「OneColumn」のまま — 列が 1 つしか表示されない
症状: 両方のビューへのルーティングは正しく、コンソールもクリーンで、アクセシビリティツリーには両方の列のコンテンツが表示されますが、画面上では左の列しか描画されません。
根本原因: sap.f.routing.Router が FCL の layout プロパティを変更するのは、一致したルートで明示的に指定されている場合だけです。layout のないルートでは、FCL は初期値の OneColumn のままとなり、ルーターがターゲットを配置していても、中央列/右端列は非表示のままです。
修正方法: FCL の すべての ルートに layout プロパティが 必須 です。詳細/ドリルダウンだけではありません。ホーム/メイン/見つからないページのルートを含め、beginColumnPages 集約だけを対象にしないルートにはすべて指定してください。
"routes": [
{
"pattern": "",
"name": "main",
"target": ["main", "welcome"],
"layout": "TwoColumnsMidExpanded" // ← REQUIRED — Welcome lives in mid column
},
{
"pattern": "Project/{projectId}",
"name": "detail",
"target": ["main", "detail"],
"layout": "TwoColumnsMidExpanded" // ← REQUIRED
}
]
罠 2:空白のページ — DOM は設定されているが、すべての要素の高さが 0
症状: コンソールにエラーはなく、アクセシビリティツリーにはすべてのコンテンツが表示されていますが、ページは視覚的に空白です。DevTools で確認すると、body のコンポーネント div からカスケードして、ネストされたすべての要素の height が 0 になっています。
根本原因: UI5 の ComponentSupport モジュールは、処理中に body のコンテナ div から data-sap-ui-component 属性を削除します。この属性に一致する CSS セレクター(body > div[data-sap-ui-component] { height: 100% })は、ComponentSupport の実行後に一致しなくなります。そのため、ラッパー div に高さが設定されず、data-height="100%" は 0 の 100% として解決されます。
修正方法: index.html のコンポーネント div に、インラインで style="height: 100%" を設定します。data-sap-ui-component に基づく CSS セレクターは、ここでは信頼できません。
<body class="sapUiBody" id="content">
<div
data-sap-ui-component
data-name="<source_namespace>.modern"
data-id="container"
data-height="100%"
style="height: 100%"></div>
</body>
罠 3:import Event from "sap/ui/base/Event" はコードのにおいです
症状: (event.getParameters() as { listItem?: ListItemBase }).listItem のような奇妙な型キャストがコントローラーに現れます。TypeScript ではイベントパラメーターを認識できないため、getParameter または getParameters の戻り値に as キャストを付けて補っています。
根本原因: sap/ui/base/Event から汎用の Event 型をインポートすると、UI5 ≥ 1.115 が提供する厳密な型付けを利用できなくなります。UI5 は、すべてのコントロールイベントに対して <Control>$<Event>Event 型と <Control>$<Event>EventParameters 型を生成します。インポートするのはこれらの型です。
修正方法: import Event from "sap/ui/base/Event" と書こうとしていることに気づいたら、いったん立ち止まり、適切な固有の型を探してください。このスキルの対象範囲でよく使う型は次のとおりです。
| イベントの発生元 | 固有の型 | モジュール |
|---|---|---|
sap.m.List / sap.m.Table の selectionChange | ListBase$SelectionChangeEvent | sap/m/ListBase |
sap.m.List / sap.m.Table の itemPress | ListBase$ItemPressEvent | sap/m/ListBase |
sap.m.List / sap.m.Table の updateFinished | ListBase$UpdateFinishedEvent | sap/m/ListBase |
sap.m.List / sap.m.Table の delete | ListBase$DeleteEvent | sap/m/ListBase |
sap.m.ListItemBase の press | ListItemBase$PressEvent | sap/m/ListItemBase |
sap.m.SearchField の liveChange | SearchField$LiveChangeEvent | sap/m/SearchField |
sap.m.Button の press | Button$PressEvent | sap/m/Button |
sap.ui.core.routing.Route の patternMatched | Route$PatternMatchedEvent | sap/ui/core/routing/Route |
重要 — ListBase の継承:
sap.m.Listとsap.m.Tableは、選択/項目の押下/更新/削除/スワイプの各イベントをsap.m.ListBaseから 継承します。TS イベント型はsap/m/Listやsap/m/Tableではなく、sap/m/ListBaseにあります。SAP Help ポータルで確認済みです。「sap.m.List と sap.m.Table はどちらも同じイベントを提供し、sap.m.ListBase から継承しています。」Table$SelectionChangeEventを探す必要はありません。これは存在しません。sap/m/ListBaseからListBase$SelectionChangeEventを使用すれば、キャストなしで両方のコントロールのselectionChangeイベントに対して TypeScript が受け入れます。
UI5 ≥ 1.115 のコントロールに対応する固有のイベント型が見つからない場合は、mcp__SAPUI5_MCP_Server__get_api_reference(query="sap.m.<Control>#<eventName>") を呼び出して、型名が存在することを確認してください。汎用の Event に置き換えないでください。
落とし穴 4:onApprove(event: Button$PressEvent) — パラメーターを宣言しない
症状: ESLint が _event(または event)に対して no-unused-vars を報告します。パラメーターを削除すると、今度は孤立した import { Button$PressEvent } に対して no-unused-imports が報告されます。2 回の編集、2 回の再 lint サイクルで、1 分を失います。
根本原因: UI5 のボタン押下ハンドラーでは、通常イベントペイロードを使用しません。oCtx、projectId などはすべて this.getView()、またはルートマッチングによって値が設定されたクラスフィールドから取得できます。event: Button$PressEvent を宣言するのは、ここでは当てはまらない型付きイベントのルールからくる反射的な習慣です。
修正方法: イベントを読み取らない場合は、押下ハンドラーをパラメーターなしで宣言します。
// CORRECT:
public async onApprove(): Promise<void> {
const projectId = this.projectId;
// ...
}
// WRONG — generates two lint errors that need two edits to fix:
public async onApprove(_event: Button$PressEvent): Promise<void> {
void _event; // dead code
const projectId = this.projectId;
}
onNavBack、onItemPress(イベントではなく this.byId からソースを取得する場合)、およびイベントペイロードを使用しないその他のハンドラーにも同じことが当てはまります。
落とし穴 5:Manifest v2 + "type": "View" の欠落 — ルートはマッチするが何も表示されない
症状: ページが空白になります。FCL の各列は正しい幅で存在しますが、各 NavContainer は空です。コンソールには、各ルートマッチの 1 ミリ秒後に見落としやすい警告が 1 つ表示される以外、何もありません。
page stack is empty but should have been initialized -
application failed to provide a page to display
ルーティングログではルートが正しくマッチしています("The route named 'main' did match")が、どの列にもビューが配置されません。
根本原因: "_version": "2.0.0" 以降(このスキルで対象としているものと同じ UI5 1.136 で導入)では、以前のルーティングキーである viewName、viewPath、viewLevel は削除され、name、path、level に置き換えられています。また、ルーティングのターゲットには、暗黙的なデフォルト "type": "View" がなくなりました。明示的な type がないと、ターゲットの解決時に何も表示されずに終わります。
出典:SAP Help、「Manifest File のアップグレードに関する移行情報」の 2.0.0(1.136)「非推奨の Manifest エントリー」の行:「ルーティングプロパティの ViewId、viewName、viewPath、viewLevel は使用できなくなりました。これらをそれぞれ id、name、path、level プロパティに置き換え、さらに type: "view" を追加して、記載されている代替手段を使用してください。」
修正方法: manifest.json の routing ブロックで、次のようにします。
routing.configに"type": "View"を追加します(ブロック内のすべてのターゲットのデフォルトになるため、繰り返し記述する手間を省けます)。- ビューフォルダーの名前空間には
path(viewPathではありません)を使用します。 - 各ターゲットには
name(viewNameではありません)を使用し、ターゲットごとにも"type": "View"を追加します(念のため。一部の UI5 バージョンでは config のデフォルトが確実に引き継がれません)。 - 各ターゲットには
level(viewLevelではありません)を使用します。
"routing": {
"config": {
"routerClass": "sap.f.routing.Router",
"type": "View", // ← REQUIRED in manifest v2
"viewType": "XML",
"path": "<ns>.view", // ← NOT "viewPath"
"async": true,
"controlId": "flexibleColumnLayout",
"controlAggregation": "beginColumnPages",
"bypassed": { "target": "notFound" }
},
"routes": [ /* ... layout property per Trap 1 ... */ ],
"targets": {
"main": {
"type": "View", // ← REQUIRED per target
"id": "main",
"name": "Main", // ← NOT "viewName"
"level": 1, // ← NOT "viewLevel"
"controlAggregation": "beginColumnPages"
},
// ... other targets
}
}
この形式は manifest v1.x と v2.x の両方で動作します。type: "View" の追加には下位互換性があります(1.14.0/UI5 1.62 で利用可能になりました)。新しいキー名(name/path/level)にも下位互換性があります。そのため、プロジェクトが v1 と v2 のどちらになるか確信がなくても、常に v2 の形式を出力してください。デメリットはありません。
この症状が出たときの簡単な診断方法: ブラウザーコンソールを開き、「page stack is empty」を検索します。見つかった場合は、落とし穴 5 です。見つからない場合は、落とし穴 1(layout の欠落)または落とし穴 2(高さのカスケード)を確認してください。
バリデーターで検出されない理由(UI5 MCP がラップしているものと同じバージョンの @ui5/[email protected] および @ui5/[email protected] で実証済み):
| ツール | Trap 5 を検出できますか? | 理由 |
|---|---|---|
run_ui5_linter (@ui5/linter) | いいえ | no-removed-manifest-property ルールは存在しますが、チェック対象は resources/js、rootView/async、routing/config/async のみです。ルーティングターゲットの viewName/viewPath/viewLevel はチェックしません。リンターのソースは target.name ?? target.viewName を読み取るため、どちらも警告なく受け入れます。 |
run_manifest_validation (@ui5/manifest スキーマ + Ajv) | いいえ | スキーマでは viewName/viewPath/viewLevel に "deprecated": true が設定され、説明には v2 での置き換え先が記載されていますが、MCP の strict: false 設定では Ajv が deprecated フラグを無視します。また、スキーマには legacyTargetAddition と actualTargetAdditionStandard の両方が代替案として並列で定義されており、_version に応じた if/then 分岐はありません。そのため、viewName を含むマニフェストも isValid: true として検証されます。 |
npm run ts-typecheck | いいえ | マニフェストは JSON であり、TypeScript からは認識されません。 |
eslint webapp | いいえ | ESLint は UI5 マニフェストのセマンティクスをモデル化しません。 |
| ブラウザーの実行時 | 間接的に | コンソール警告 "page stack is empty but should have been initialized" が唯一の自動検出シグナルですが、リンティング時ではなく実行時にのみ発生します。 |
スキルへの影響: フェーズ 7 の受け入れゲート(リンター/マニフェスト検証/ts-typecheck)は、非推奨のルーティングキーを含む v2 マニフェストに対してすべて成功と報告します。この Trap に対する唯一の信頼できるゲートは、フェーズ 8d のブラウザーレンダリング検証です。これがフェーズ 8d が任意ではなく必須である理由です。「リンティングも検証も問題なし」という報告を見ても、ルーティング設定が正しいとは限りません。
上流に issue を報告する時間があれば、これは @ui5/linter の no-removed-manifest-property ルールにおける明確な不備です。チェック対象プロパティのリストに routing.targets[].viewName/viewPath/viewLevel を追加すれば解消できます。
自分で解決する:スキルに答えがない場合
このスキルではよくあるパターンを扱いますが、プロジェクトごとに独自の癖があります。スキルで扱われていない問題に遭遇したら、推測する前に調査してください。UI5 のミドルウェアエコシステムは断片化しており、型システムも複雑です。根拠のある推測をしても、キー名や継承元を間違えることがよくあります。以下の調査は 30 秒で済み、手戻りを減らせます。
パターン A — ミドルウェア/プロキシ/ビルドツールの設定オプション
該当する場面: UI5 ミドルウェア(ui5-middleware-simpleproxy、ui5-middleware-livereload、fiori-tools-proxy、ui5-tooling-transpile、または ui5.yaml の customMiddleware や customTasks にあるもの)に設定キーを指定するとき。
調査が必要な理由: UI5 ミドルウェアは未知の設定キーを黙って無視します。エラーも警告も出さず、誤った動作をします。命名規則はパッケージによって異なります。TLS の検証を無効にする設定は、ui5-middleware-simpleproxy では strictSSL: false、fiori-tools-proxy では ignoreCertErrors: true、その他のパッケージでは別の名前です。あるパッケージの設定を別のパッケージにそのまま当てはめないでください。
手順:
Bash: cat <target>/node_modules/<package-name>/README.md
# or:
WebFetch: https://www.npmjs.com/package/<package-name>
WebFetch: https://github.com/<owner>/<package-name>
README がまだローカルにない場合(npm install 前)は、WebFetch または gh api を使って GitHub ミラーを参照してください。
パターン B — UI5 コントロールのイベントに使う TypeScript 型
該当する場面: イベントハンドラーのパラメーターに型を付けるときに、具体的な <Control>$<Event>Event 型の名前や定義場所がわからない場合。
調査が必要な理由: イベントは継承されます。親クラスで定義されたイベントがサブクラスで再利用されます。selectionChange は Table ではなく ListBase で定義されているため、Table$SelectionChangeEvent を探しても見つかりません。このスキルでよく扱うイベントは Trap 3 の表に記載されています。それ以外の場合は、UI5 MCP に問い合わせてください。
手順:
mcp__SAPUI5_MCP_Server__get_api_reference(
projectDir="<absolute target>",
query="sap.m.<Control>#<eventName>"
)
結果から、(a)イベントを実際に定義しているクラス(=インポート元のモジュール)、(b)正式なイベント型名を確認してください。query で何も返らない場合は、検索範囲を広げ(query="sap.m.<Control>")、イベント一覧を確認してください。型名は <DefiningClass>$<EventName>Event です。
パターン C — OData V4 のバインディング/ドラフト/アクションのセマンティクス
トリガー: 複合キー、$expand=_X ナビゲーション、アクションの呼び出し、ドラフト処理、一括更新など、V4 固有のコードを書いていて、標準的なパターンがわからない場合。
調査する理由: V4 は V2 と、すぐにはわかりにくい点で異なり、その違いはバージョン固有です。推測すると、たいてい tsc の実行サイクルが増えたり、実行時に「no metadata」エラーが発生したりします。
手順:
mcp__sap-docs__search(
query="<feature> OData V4 model UI5",
sources=["sapui5","sap-help"],
includeOnline=true
)
mcp__sap-docs__fetch(id="<best result id>")
検索語として、次のトピックから調べてください(以下の語句をそのまま検索します)。
- 「Draft Handling with the OData V4 Model」
- 「Operations」(V4 のアクション/関数)
- 「Auto-
$expand/$select」 - 「Reducing the Number of Requests Required to Get the Properties of a Single Entity」
- 「Reducing Roundtrips」
- 「Filtering」+「Sorting」
パターン D — FCL のルーティング動作に関する予想外の問題
トリガー: FCL の列数が正しくない、列がちらつく、ディープリンクでレイアウトが復元されない、「Close」ボタンが表示されない場合。
調査する理由: sap.f.routing.Router はレイアウト駆動型です。FCL の予想外の動作のほとんどは、ビュー XML の問題ではなく、ルートの layout 値が未設定か誤っていることが原因です。
手順: manifest.json を開き、routing.routes[] の各エントリーに layout プロパティがあるか確認します。ホーム/メインルートにも必要です(落とし穴 1)。使用する LayoutType の列挙値がわからない場合は、次を検索してください。
mcp__sap-docs__search(query="sap.f.LayoutType FCL three-column")
パターン E — ページが空白で表示されるが、DOM は生成されている
トリガー: アクセシビリティツリーにはコンテンツが表示され、コンソールにエラーはないのに、ビューポートが空の場合。
調査する理由: 高さの連鎖です。祖先要素の一部が height: 0 と解決され、すべての子孫要素が折りたたまれています。通常、この落とし穴の原因は ComponentSupport によって削除されたため一致しなくなった data- 属性セレクターです(落とし穴 2)。ただし、高さの連鎖に関する別のパターンもあります(例:height: 100% が設定されていない <body>、暗黙のコンテナ幅はあるが高さのない Page)。
手順: ブラウザーの DevTools で body をクリックし、Elements パネルで子孫ツリーをたどりながら、計算後の height を確認します。最初に 0 になる要素が、連鎖の途切れている箇所です。インラインで style="height: 100%" を適用するのが望ましい方法です。または、削除されない属性(id、class。data-sap-ui-* は使用しないでください)をキーにした CSS セレクターを使用します。
パターン F — TypeScript のコンパイルは成功するが、ui5-linter が失敗する
トリガー: npm run ts-typecheck は問題なく完了するのに、ui5lint が問題を検出し、tsc では診断できない場合。
調査する理由: ts-typecheck は型をチェックし、ui5-linter は UI5 の実行時に関する問題(非推奨 API、フレームワークの規約、manifest の相互参照、XML ビューのバインディングの正確性)をチェックします。両者の対象は別です。適切な順序は、まず eslint --fix(機械的な TS レベルのノイズを除去)、次に ui5-linter(UI5 固有の問題を検出)、最後に tsc(残りの型エラーを検出)です。
ui5-linter が、内容を理解できない指摘を報告した場合は、コンテキストをリクエストします。
mcp__SAPUI5_MCP_Server__run_ui5_linter(
projectDir="<absolute target>",
filePatterns=["<the file>"],
provideContextInformation=true
)
provideContextInformation: true フラグを設定すると、各指摘の説明となる API リファレンスの抜粋とドキュメントへのリンクが返されます。
パターン G — UI5 のベストプラクティスがわからない
トリガー: 「Form と SimpleForm のどちらを使うべきか」「これは JSONModel にすべきか、それともパスにすべきか」「このフォーマッターの読み込みには core:require が適切か」「manifest で特定のテーマを宣言すべきか」と迷う場合。
調査する理由: SAPUI5 のガイドラインはバージョン固有です。MCP はプロジェクトのバージョンに対応する正式な一覧を返します。
手順 — まず、固定されている 2 つのガイドラインツールから調べます。
mcp__SAPUI5_MCP_Server__get_guidelines # general UI5 dev rules
mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines # TS-specific rules
それでも答えが見つからない場合は、sap-docs に問い合わせます。
mcp__sap-docs__search(query="<your question>")
パターン H — 今回の実行中に同じ問題を 2 回デバッグした
トリガー: コードを書いてエラーに遭遇し、修正した後、別のファイルや手順で同じ種類のエラーが再発した場合。これは、スキルに汎用的に適用できるパターンが不足している兆候です。
対応: 症状、根本原因、修正内容、抽出した「汎用ルール」の 1 行を含む Run-Notes エントリーとして記録します。今回の実行後、今後のプロジェクトでも再発する可能性が高い場合は、「重大な落とし穴」セクションへの追加を提案します。
このプロジェクトの命名規則の上書き(旧 SAPUI5 JS 規約からの変更)
以下の 2 つの規約は、旧 SAPUI5 JS コーディングガイドラインを上書きします。暗黙的に適用し、変換時にレガシーのスタイルを引き継がないでください。
| 規約 | 旧 SAPUI5 JS コーディングガイドライン | このスキルのルール |
|---|---|---|
| 変数の接頭辞 | ハンガリアン記法を推奨(oModel、sQuery、iTotal、aFilters、mArgs、fHours、bFlag) | ハンガリアン記法は使いません。 シンプルな名前を使います:model、query、total、filters、args、hours、flag。TypeScript の型があるため、接頭辞は不要です。 |
| 「Main」エンティティ名 | 「master」(例:Master.view.xml、i18n キー masterTitle) | どこでも 「main」 を使います — コントローラー/ビュー/ルート/ターゲット/ファイル/i18n キー。SAP 独自のインクルーシブランゲージガイド(「master branch → main branch」)に合わせます。 |
一般的な SEGW から RAP へのデモの構成における名前の置き換え表:
| レガシー | モダン |
|---|---|
Master.controller.js | Main.controller.ts |
Master.view.xml | Main.view.xml |
controllerName="...controller.Master" | controllerName="...modern.controller.Main" |
route "name": "master" | route "name": "main" |
target "master" | target "main" |
i18n キー masterTitle | mainTitle |
i18n キー masterSearchPlaceholder | mainSearchPlaceholder |
i18n キー masterCount | mainCount |
var oModel = ... | const model = ... |
var sQuery = ... | const query = ... |
var iTotal = ... | const total = ... |
var aFilters = ... | const filters = ... |
var oCtx = ... | const ctx = ...(または、より明確な場合は const context = ...) |
var oList = ... | const list = ... |
var oRouter = ... | const router = ... |
var oEvent = ...(パラメーター) | event |
var that = this; | 完全に削除し、アロー関数を使います |
例外: 接頭辞を削除すると予約語、同名のインポート、または UI5 コントロール名と衝突する場合に限り、接頭辞を残します(ローカル変数の const event = ... は一般的な UI5 インポートと衝突しないため問題ありません。一方、const Date = ... はグローバルをシャドーイングするため、oDate または workDate という名前にします)。
この実行でユーザーが確認できるよう、Phase 2 の計画出力に「命名規則の上書き:」という行を含めてください。
アトミックな名前変更のヒント: master から main への名前変更では、最低でも 7 か所に影響します。ファイル名、ビューの controllerName 属性、manifest のルート名、manifest のターゲット名+キー+viewName、i18n キー(2 つのロケールファイル)、BaseController.onNavBack のフォールバック、すべてのコントローラー内の navTo("master", ...) 呼び出しです。編集のやり直しを避けるため、事前に grep を実行してすべての一致箇所を洗い出し、一括で編集してください。
Bash: grep -rn -E "master|Master" <target>/webapp
出力を確認し、どの一致箇所が名前変更の対象かを判断します(レガシーのドイツ語コメント内の文中にある「master」などは対象外で、そうしたコメントは削除する必要があります)。すべてを一括で編集したあと、2 回目の grep を実行し、一致箇所が何も返されないことを確認してください。
スマートデフォルト(暗黙に適用し、調査前に質問しないでください)
| 設定 | デフォルト | 理由 |
|---|---|---|
| ソースアプリ | <source_app>/ | ワークスペース内のフリースタイル JS アプリ |
| ターゲットアプリ | <modern_app>/ | このスキルの出力用に確保された空のフォルダー |
| ターゲット UI5 バージョン | 1.147.2 | 執筆時点で最新の 1.x。デモの FE アプリと一致し、2.0-API に準拠 |
| フレームワーク | SAPUI5 | レガシーアプリの ui5.yaml および FE アプリのランタイムと一致 |
| 言語 | TypeScript | get_typescript_conversion_guidelines に従う |
| 型パッケージ | @sapui5/[email protected] | UI5 MCP の TypeScript ガイドラインで必須(古い sap-ui5-types という誤記ではありません) |
| アプリの名前空間 | ソースの名前空間 + .modern(例:<source_namespace>.modern) | ルーティングでレガシーと区別し、grep の連続性を維持 |
| テーマ | sap_horizon | UI5 1.108 以降のデフォルト。レガシーの sap_belize は非推奨 |
| レイアウト | sap.m.SplitApp ➜ sap.f.FlexibleColumnLayout(FCL)に変換 | メインと詳細に適した、最新のレスポンシブなデフォルト |
| 命名 | ハンガリアン記法の接頭辞は使わず、「master」ではなく「main」を使用 — 上の「Naming overrides」セクションを参照 | TypeScript に適した命名と SAP Inclusive Language に準拠 |
| ブートストラップ | data-sap-ui-async="true" + data-sap-ui-on-init="module:sap/ui/core/ComponentSupport" | UI5 ガイドラインに従う。同期処理は非推奨で、2.x では動作しません |
| マニフェストのバージョン | _version: 1.60.0 以降 | sap.app.dataSources と宣言的モデルに必須 |
| OData モデル | migrate-segw-to-rap が生成する V4 サービス(またはユーザーが指定する任意の V4 サービス)を、開発サーバーのプロキシ経由で使用。SRVD 直結の場合のパターン:/sap/opu/odata4/sap/<service_binding>/srvd/sap/<service_binding>/0001/。または、兄弟に参照アプリがある場合は、その $metadata を確認 | V4 は最新のパターンを示します。すでに構築済みの任意の FE アプリと同じバックエンドを使用 |
| ルーティング | sap.m.routing.Router ➜ 列ごとの FCL ターゲットを持つ sap.f.routing.Router に変更 | FCL には f-router が必要 |
| BaseController | 必須 | getRouter、getModel、getResourceBundle、getOwnerComponent の唯一の信頼できる情報源 |
| イベント型 | <Control>$<Event>Event を使用(例:Button$PressEvent) | UI5 ≥ 1.115 でサポート。UI5 ガイドラインで必須 |
| フォーマッター | まず OData 型(sap.ui.model.odata.type.*)を使用し、独自のものは固有のビジネスロジックに限る | UI5 ガイドライン §1 に準拠 |
| フォーム | 使用する場合は sap.ui.layout.form.Form + ColumnLayout | SimpleForm は使用禁止(UI5 ガイドライン §4) |
| キャスト | 実際のコントロール型(as Button)を使用し、as any / as unknown as ... は使用しない | TypeScript 変換の「一般ルール」に準拠 |
| テスト | 初回リリースでは OPA5 + QUnit をスキップ | Run 1 が成功した場合、フォローアップとして実施 |
| リンター | mcp__SAPUI5_MCP_Server__run_ui5_linter → 検出事項 0 件 | 必須の受け入れ基準 |
| マニフェストの検証 | mcp__SAPUI5_MCP_Server__run_manifest_validation → エラー 0 件 | 必須の受け入れ基準 |
| 型チェック | npm run ts-typecheck(package.json にスクリプトを追加)→ エラー 0 件 | 必須の受け入れ基準 |
入力
ユーザーが指定するのは、次のいずれか 1 つです。
- レガシーアプリへの相対パス。例:
<source_app>/ - ターゲットフォルダー名。例:
<modern_app>/(スキルが同階層のlegacy-*をソースとして推定) - 何も指定しない場合 — デフォルト(
<source_app>/➜<modern_app>/)を使用
両方のフォルダーが存在し、ターゲットが空でない場合は、次のように質問します。「<target>/ にはすでに内容があります。
消去して最初からやり直しますか、それとも既存の構成に移行しますか?」 デモでは、消去して書き直すことをデフォルトにします。
フェーズ 0 — 事前確認
0a. レガシーアプリを読み取れること
Bash: cat <legacy>/webapp/manifest.json
Bash: ls <legacy>/webapp/{controller,view,model,fragment,i18n}
確認事項:manifest.json を解析できること、webapp/controller/ と webapp/view/ の両方が存在すること、少なくとも 1 つの *.controller.js と 1 つの *.view.xml があること。いずれかを満たさない場合は、「<legacy> は UI5 アプリのようには見えません — パスを確認してください。」 と表示して停止します。
0b. UI5 MCP サーバーに接続できること
mcp__SAPUI5_MCP_Server__get_version_info(frameworkName="SAPUI5")
確認事項:バージョンマップに少なくとも 1.147.x が返されること。ツールでエラーが発生した場合は、「UI5 MCP サーバーが設定されていません。このスキルを実行する前に、.cursor/mcp.json で設定してください。」 と表示して停止します。
0c. 権威あるガイドラインを取得する(コードを書く前に実行してください)
mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines
mcp__SAPUI5_MCP_Server__get_guidelines
両方の応答を最後まで読んでください。以下の正確な情報源です。
- 開発依存関係のバージョン(
@ui5/cli、typescript、ui5-tooling-transpile、ui5-middleware-livereload、typescript-eslint) tsconfig.jsonの構成(target: es2023、module: es2022、types: ["@sapui5/types"]、paths マップ)ui5.yamlの構成(ui5-tooling-transpile-task+ui5-tooling-transpile-middleware)- 5 ステップのコード変換手順:(1)クラス構文、(2)ES モジュール、(3)型注釈、(4)ジェネリック getter のキャスト、(5)残りの問題
@namespaceJSDoc ルール:バック変換で UI5 のクラス名を再追加できるよう、エクスポートする各クラスの直前に必ず記述します- 避けるべきキャスト:
anyは絶対に使わず、unknown as ...も絶対に使わないでください。実際のコントロール型を使用します - コントロールイベント型のルール:
sap/ui/base/EventのEventではなく、sap/m/ButtonからButton$PressEventをインポートします
これらのツール応答は長く、バージョン固有です。記憶を頼りに言い換えず、必要な箇所では引用してください。
0d. npm と node が使用可能であること
Bash: node --version && npm --version
確認:Node 22 以降(CLAUDE.md の要件)および npm 10 以降。
Phase 1 — レガシーアプリを調査する
<legacy>/webapp/ 内の関連ファイルをすべて読みます。見つかった内容を、blocker(最新の UI5 で動作させるために必ず修正)、cleanup(好ましくない慣例だが動作する)、cosmetic(スタイル)に分類します。
1a. マニフェストの確認
_version、sap.ui5.dependencies.minUI5Version、sap.ui5.rootView、sap.ui5.dependencies.libs、sap.ui5.routing、sap.ui5.models、sap.ui5.contentDensities、sap.ui5.resources、テーマの参照を確認します。
よくあるレガシーパターン:
_version: 1.40.0➜ blocker(1.60 以降を対象にします)minUI5Version< 1.108 ➜ blocker(非同期の保証がありません)- FCL ターゲットに
routerClass: "sap.m.routing.Router"を指定 ➜ blocker sap.ui5.modelsにハードコードされたサービス URL ➜ cleanup(sap.app.dataSourcesに移動します)contentDensities: { compact: true, cozy: true }➜ cleanup(引き続きサポートされていますが、マニフェストに移動します)supportedThemes: ["sap_belize"]➜ cleanup(削除します。デフォルトは sap_horizon です)
1b. Component.js の確認
<legacy>/webapp/Component.js を読みます。以下を確認します。
jQuery.sap.require("...")➜ blocker(削除し、ES import を使用します)init内のハードコードされたサービス URL 定数 +new sap.ui.model.odata.v2.ODataModel(...)➜ blocker(モデルをマニフェストのsap.ui5.models[""]に移動します)- 手動で作成された
deviceモデル ➜ 維持します(一般的な方法ですが、TS では型を付けます) - OData V4 に対する
sap.ui.model.BindingMode.TwoWay➜ blocker(V4 ではOneWay読み取りと明示的な編集を推奨します) useBatch: false➜ V4 では該当なし(V4 のバッチ処理は異なります)- 懸念事項の混在(Component 内で
jQuery.sap.requireにより formatter を読み込むなど)➜ blocker
1c. コントローラーの確認(コントローラーごと)
各 <legacy>/webapp/controller/<X>.controller.js について、以下を実行します。
Read: <legacy>/webapp/controller/<X>.controller.js
次の項目にフラグを付けます。
- クロージャの前に
var that = this;がある ➜ cleanup(アロー関数を使用します) oCtx.getPath()の正規表現解析(.replace(/^\/Set\('/, ""))➜ blocker(oCtx.getProperty("Key")を使用します)sap.ui.getCore().byId(...)➜ cleanup(this.byId(...)を使用します)sap.ui.getCore().getModel(...)➜ blocker(キャストを付けてthis.getOwnerComponent()!.getModel(...)を使用します)sap.ui.core.UIComponent.getRouterFor(this)➜ cleanup(BaseController に移動します)- グローバルとして
sap.m.MessageBoxにアクセス ➜ blocker(sap/m/MessageBoxからMessageBoxをインポートします) jQuery.sap.require(...)➜ blocker(ES import を使用します)- グローバル formatter 呼び出し(
window.com.demo.formatter.X)➜ blocker(formatter モジュールにまとめ、core:requireを使ってビューにインポートします) oModel.callFunction("/X", {method:"POST", urlParameters: {...}})(OData V2 の function-import)➜ blocker(V4 のアクションに変換します:model.bindContext("/Action(...)").execute())- 「データが届いた後に更新」するための
setTimeout(..., 500)➜ blocker(dataReceivedなど、バインディングイベントを使用します) - パラメーターに JSDoc がない ➜ cosmetic(TS への書き換えで対応します)
1d. ビューのスキャン(ビューごと)
各 <legacy>/webapp/view/<X>.view.xml について:
Read: <legacy>/webapp/view/<X>.view.xml
以下を検出します:
- 存在しないメソッドを参照するインラインイベントハンドラー(
press="onSomething")➜ ブロッカー(例外が発生します) - async を指定していない
<core:Fragment fragmentName="...">➜ クリーンアップ - 非推奨のコントロール(
sap.ui.commons.*)➜ ブロッカー - ルートが
<sap.m.SplitApp>➜ ブロッカー(smart-defaults に従って FCL に置き換えます) - i18n キーではなくハードコードされた文字列 ➜ クリーンアップ
- パスベースのフォーマッター(
formatter: 'window.com.demo.X.statusText')➜ ブロッカー(フォーマッターモジュールのcore:requireとformatter: '.formatter.statusText'に変換します) enabled="{= ${Status} === 'D' }"(式バインディング)➜ 維持しますが、V4 のキーに合わせて調整します(V4 モデルに応じて、StatusをOverallStatusなどに置き換えます)
1e. フォーマッターのスキャン
<legacy>/webapp/model/formatter.js について:
jQuery.sap.declare(...)とwindow.com.demo...のグローバル名前空間 ➜ ブロッカー(export functionを使った ES モジュールに書き換えます)- 呼び出しごとにインスタンス化される
sap.ui.core.format.DateFormat.getDateInstance(...)➜ クリーンアップ(モジュールのトップレベルで一度だけメモ化します) - 呼び出しごとのフォーマットオブジェクトのインスタンス化 ➜ クリーンアップ(一度だけインスタンス化します)
- ステータス/優先度のマッピング用 switch ➜ 維持しますが、型を
string | undefinedからstringに変更します
1f. 調査結果レポートの作成
ユーザーに構造化された概要を出力します:
Discovery — legacy app: <legacy>
Manifest version: 1.40.0 (target 1.60+)
UI5 version: 1.84.x (target 1.147.2)
Controllers: 3 (App, Master, Detail)
Views: 5 (App, Master, Detail, Welcome, NotFound)
Layout: sap.m.SplitApp (target sap.f.FlexibleColumnLayout)
Theme: sap_belize (target sap_horizon)
OData: V2 hardcoded in Component.init (target V4 via manifest dataSources)
Blockers (<n>):
- Component.js: hardcoded service URL + manual ODataModel construction
- Component.js: jQuery.sap.require(...)
- formatter.js: window.com.* global namespace
- Master.controller.js: getPath() regex parsing of V2 entity key
- Detail.controller.js: oModel.callFunction("/ApproveProject", ...) for V2 function-import
- Detail.controller.js: setTimeout(..., 500) for counter refresh
- *.view.xml: window.com.* formatter paths
- App.view.xml: SplitApp root
- ...
Cleanups (<n>):
- Master.controller.js: `var that = this;` pattern (4×)
- All controllers: `sap.ui.core.UIComponent.getRouterFor(this)` direct calls
- ...
Cosmetic (<n>):
- Missing JSDoc across controllers
- ...
Phase 2 — 設計計画 + ユーザーの承認
移行計画を次の形式どおりに出力し、ok/edit/質問を待ちます:
Plan — modernize <legacy> ➜ <target>:
UI5 version: 1.147.2 SAPUI5 (TypeScript)
Types: @sapui5/[email protected]
Namespace: <source_namespace>.modern
Theme: sap_horizon
Layout: sap.f.FlexibleColumnLayout (translated from SplitApp)
OData: V4 — <v4_service_url>
Proxy: ui5-middleware-simpleproxy → <sap_baseuri>
(auth via .env: UI5_MIDDLEWARE_SIMPLE_PROXY_{USERNAME,PASSWORD})
Naming overrides: - No Hungarian prefixes (TS types replace the hint)
- "main" instead of "master" for controller / view / route / i18n keys
(legacy Master.controller.js ➜ Main.controller.ts, etc.)
Files to generate in <target>/webapp:
Component.ts
manifest.json (v1.60.0)
index.html (ComponentSupport bootstrap, async, inline height fix)
controller/BaseController.ts
controller/App.controller.ts
controller/Main.controller.ts ← renamed from Master per naming overrides
controller/Detail.controller.ts
view/App.view.xml (FCL root)
view/Main.view.xml ← renamed from Master
view/Detail.view.xml
view/Welcome.view.xml
view/NotFound.view.xml
i18n/i18n.properties (translated keys: masterX → mainX) + i18n_en.properties
css/style.css (copied from legacy if non-empty)
model/models.ts (device-model helper only)
model/formatter.ts (consolidated; ES module export)
Files at <target>/ root:
package.json (with @ui5/cli, typescript, ui5-tooling-transpile, ui5-middleware-livereload,
ui5-middleware-simpleproxy, @sapui5/types)
ui5.yaml (specVersion 4.0; transpile + livereload + simpleproxy middleware)
tsconfig.json (target es2023, module es2022, strict, allowJs)
.env.example
.gitignore
V2 ➜ V4 binding migrations (generic — adapt to your service's entity names):
/<EntitySet> ➜ /<Entity> (drop "Set" suffix)
/<EntitySet>('K') ➜ /<Entity>(<Key>='K') (named-key style)
expand: '<Nav>' ➜ $expand=_<Nav> (V4 RAP prefixes assocs with _)
oModel.callFunction(...) ➜ model.bindContext("/<Action>(...)").execute()
fieldName remapping ➜ inspect $metadata; RAP often renames fields between V2/V4
Blockers being fixed: <count>
Cleanups applied: <count>
Cosmetic skipped: <count> (separate prettier pass if desired)
Tests in this skill: none (OPA5/QUnit follow-up)
Acceptance: ui5-linter clean + manifest validation clean + tsc clean + npm start
renders Master list
Type `ok` to proceed, `edit` to revise, or ask any question.
<target>/ 内のファイルを変更する前に、ok を待ちます。
Phase 3 — モダンな TS アプリのスキャフォールド
3a. 空でない場合はターゲットを消去
ユーザーが消去して書き換えることを承認した場合:
Bash: rm -rf <target>/* <target>/.[!.]* # safely empty <target>/ while keeping the folder
3b. UI5 MCP でスキャフォールド
create_ui5_app を <target>/ に直接実行します(サブフォルダーには実行しません):
mcp__SAPUI5_MCP_Server__create_ui5_app(
appNamespace = "<source_namespace>.modern",
basePath = "<absolute path to target>",
createAppDirectory = false,
framework = "SAPUI5",
frameworkVersion = "1.147.2",
typescript = true,
initializeGitRepository = false,
runNpmInstall = true
)
ここでは
oDataV4Urlを意図的に省略しています。V4 サービスは認証情報が必要なプロキシの背後にあるため、URL 検証が失敗します。データソースは Phase 4 で手動で追加します。
3c. 構成を確認
Bash: ls -la <target>/ && ls <target>/webapp/
ルートに必要なファイル:package.json、ui5.yaml、tsconfig.json、最低限 Component.ts、manifest.json、view/App.view.xml、controller/App.controller.ts、index.html、i18n/i18n.properties を含む webapp/。
create_ui5_app が TS ではなく JS ファイルを生成した場合は、明確に失敗させます。手動のスキャフォールドに黙って切り替えてはいけません。
3d. 依存関係が UI5 MCP のガイドラインに準拠していることを確認
Read: <target>/package.json
get_typescript_conversion_guidelines の出力と照合します。最低限、以下の dev-deps が必要です:
@ui5/clitypescriptui5-tooling-transpileui5-middleware-livereload- フレームワークのバージョンに一致する
@sapui5/types
不足しているもの(例:OData プロキシ用の ui5-middleware-simpleproxy)を追加します。バージョンを更新するのは、ガイドラインで指定された下限を下回っている場合に限ります。ダウングレードはしないでください。
その後、依存関係の一覧に変更があった場合は、再度 npm install を実行します。
また、scripts に "ts-typecheck": "tsc --noEmit" が存在すること(UI5 MCP ガイドライン)を確認します。なければ追加します。
3e. 警告 — スキャフォールド自体がハンガリアン記法を使用
UI5 MCP のスキャフォールドテンプレート(Component.ts、BaseController.ts、デフォルトの *.controller.ts、models.ts)には、ハンガリアン記法の接頭辞を付けたパラメーター名やローカル変数名(sName、oModel、oParameters など)が含まれています。上記の Naming overrides セクションはスキャフォールドにも適用されます。レガシーのコントローラーから移植する場合だけでなく、スキャフォールドの各ファイルを読む際にも名前を変更してください。
最も速いワークフローは、まずスキャフォールドを作成し、新しいビューごとのコントローラーを書く前に、スキャフォールドされた webapp/ 全体を対象に、よくある接頭辞(oModel、sName、oEvent、oParameters など)を grep して一括で名前を変更することです。これにより、拡張する予定の BaseController を、あらかじめモダンなスタイルにできます。
Phase 4 — manifest.json + ui5.yaml + プロキシの設定
4a. manifest.json を翻訳する
<target>/webapp/manifest.json を読み込み、レガシー版の固有情報をマージします。
-
sap.app.id=<source_namespace>.modernに設定します。 -
sap.app.title/sap.app.descriptionにi18nの値を設定します(すでに{{appTitle}}としてテンプレート化されています)。 -
レガシー版の
sap.app.iconsが空でなければ、コピーします。 -
ユーザーが指定した V4 サービス URL を使って
sap.app.dataSourcesを追加します(不明な場合は、バックエンドで使われている V4 バインディングパターンをmcp__sap-docs__searchで検索します。ABAP オンプレミス環境の SRVD-direct では、通常/sap/opu/odata4/sap/<service_binding>/srvd/sap/<service_binding>/0001/を使います)。"dataSources": { "mainService": { "uri": "<v4_service_url>", "type": "OData", "settings": { "odataVersion": "4.0", "localUri": "localService/metadata.xml" } } } -
sap.ui5.dependencies.minUI5Version="1.147.0"に設定し、ライブラリとしてsap.ui.core、sap.m、sap.f、sap.ui.layoutを指定します。 -
sap.ui5.models[""]を設定します。"": { "type": "sap.ui.model.odata.v4.ODataModel", "dataSource": "mainService", "settings": { "operationMode": "Server", "autoExpandSelect": true, "earlyRequests": true } } -
sap.ui5.rootViewを{ "viewName": "<ns>.view.App", "type": "XML", "id": "app" }に設定します。 -
ルーティング — すべてのルートに
layoutプロパティを必ず指定します(落とし穴 1)。また、すべてのターゲットにtype: "View"を指定し、viewName/viewPath/viewLevelではなくname/path/levelを使います(落とし穴 5)。"routing": { "config": { "routerClass": "sap.f.routing.Router", "type": "View", // ← REQUIRED in manifest v2 (Trap 5) "viewType": "XML", "path": "<ns>.view", // ← NOT "viewPath" "async": true, "controlId": "flexibleColumnLayout", "controlAggregation": "beginColumnPages", "bypassed": { "target": "notFound" } }, "routes": [ { "pattern": "", "name": "main", "target": ["main", "welcome"], "layout": "TwoColumnsMidExpanded" // ← REQUIRED (Trap 1) }, { "pattern": "<EntityRoute>/{<keyParam>}", "name": "detail", "target": ["main", "detail"], "layout": "TwoColumnsMidExpanded" // ← REQUIRED (Trap 1) } ], "targets": { "main": { "type": "View", "id": "main", "name": "Main", "level": 1, "controlAggregation": "beginColumnPages" }, "welcome": { "type": "View", "id": "welcome", "name": "Welcome", "level": 2, "controlAggregation": "midColumnPages" }, "detail": { "type": "View", "id": "detail", "name": "Detail", "level": 2, "controlAggregation": "midColumnPages" }, "notFound": { "type": "View", "id": "notFound", "name": "NotFound", "level": 3, "controlAggregation": "midColumnPages" } } }注:
type: "View"はconfigと各ターゲットの両方に指定します。configレベルのデフォルト値は引き継がれるはずですが、デフォルト値の解決方法における UI5 バージョン間の違いに備え、各ターゲットにも重複して指定しておくのが安全です。 -
sap.ui5.contentDensities={ "compact": true, "cozy": true }に設定します(宣言的な設定で問題ありません)。 -
supportedThemesを削除します(sap_horizon がデフォルトなので、明示的に固定する必要はありません)。
マージした manifest を書き込みます。
Write: <target>/webapp/manifest.json
検証します。
mcp__SAPUI5_MCP_Server__run_manifest_validation(manifestPath="<absolute path>/webapp/manifest.json")
次に進む前に、検出された問題をすべて修正します。
4b. ui5.yaml — プロキシの追加とトランスパイルミドルウェアの確認
<target>/ui5.yaml を開きます。Phase 3 の後に ui5-tooling-transpile-task と ui5-tooling-transpile-middleware がすでに追加されています(確認してください)。simpleproxy と livereload を追加します。
specVersion: "4.0"
metadata:
name: <source_namespace>.modern
type: application
framework:
name: SAPUI5
version: "1.147.2"
libraries:
- name: sap.m
- name: sap.ui.core
- name: sap.f
- name: sap.ui.layout
- name: themelib_sap_horizon
builder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
server:
customMiddleware:
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
- name: ui5-middleware-livereload
afterMiddleware: compression
- name: ui5-middleware-simpleproxy
afterMiddleware: compression
mountPath: /sap
configuration:
baseUri: "<sap_baseuri>/sap" # e.g. https://abap-host:50001/sap or https://my-cf-host
strictSSL: false # only if upstream uses a self-signed cert (dev trials)
query:
sap-client: "<client>" # e.g. "001"; omit if your service doesn't need it
設定キーの落とし穴: 正しいキーは
strictSSL: falseです。skipCertificateCheckやignoreCertErrorsではありません。これらのキーはほかのプロキシミドルウェア(fiori-tools-proxyではignoreCertErrorsを使用)にはありますが、ui5-middleware-simpleproxyは未知のキーをエラーなく無視するため、キーを間違えるとエラーメッセージなしで HTTPS ハンドシェイクに失敗します。ミドルウェアの README で確認し、デモワークスペースで動作することを確認済みです。
ui5 tooling が自動的に読み込むよう、package.json の ui5.dependencies に ui5-middleware-simpleproxy を追加します。
"ui5": { "dependencies": ["ui5-middleware-simpleproxy"] }
4c. .env のひな形
Write: <target>/.env.example
UI5_MIDDLEWARE_SIMPLE_PROXY_USERNAME=
UI5_MIDDLEWARE_SIMPLE_PROXY_PASSWORD=
対話形式で一度だけユーザーに依頼し、値を <target>/.env にコピーしてもらいます。.env はコミットしないでください。
4d. index.html — ComponentSupport ブートストラップとインラインの高さ(ひな形作成後に確認)
ひな形の index.html では、すでに ComponentSupport が使われているはずです。上記の落とし穴 2 に従って高さの処理を修正します。コンポーネントの <div> には、インラインで style="height: 100%" を必ず指定してください。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{appTitle}}</title>
<style>html,body,#content { height: 100%; margin: 0; }</style>
<script
id="sap-ui-bootstrap"
src="resources/sap-ui-core.js"
data-sap-ui-theme="sap_horizon"
data-sap-ui-resource-roots='{ "<source_namespace>.modern": "./" }'
data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"
data-sap-ui-compat-version="edge"
data-sap-ui-async="true"
data-sap-ui-frame-options="trusted">
</script>
</head>
<body class="sapUiBody" id="content">
<div
data-sap-ui-component
data-name="<source_namespace>.modern"
data-id="container"
data-height="100%"
style="height: 100%"></div>
</body>
</html>
注:
style="height: 100%"は必須です。[data-sap-ui-component]に対する CSS セレクターを使うほうがすっきり見えますが、ComponentSupport は処理中にその属性を削除するため、機能しません(落とし穴 2)。data-height="100%"だけでは不十分です。これは親要素の高さを基準に解決するComponentContainerの設定なので、ラップする div にもインラインスタイルが必要です。- ブートストラップ以外のインライン
<script>は使わないでください(CSP ルール、UI5 ガイドライン §1)。
Phase 5 — BaseController + Component.ts + App.view.xml
5a. BaseController.ts
Write: <target>/webapp/controller/BaseController.ts
import Controller from "sap/ui/core/mvc/Controller";
import UIComponent from "sap/ui/core/UIComponent";
import Router from "sap/f/routing/Router";
import Model from "sap/ui/model/Model";
import ResourceModel from "sap/ui/model/resource/ResourceModel";
import ResourceBundle from "sap/base/i18n/ResourceBundle";
import History from "sap/ui/core/routing/History";
/**
* @namespace <source_namespace>.modern.controller
*/
export default class BaseController extends Controller {
public getRouter(): Router {
return UIComponent.getRouterFor(this) as Router;
}
public getModel<T extends Model = Model>(name?: string): T {
return this.getView()!.getModel(name) as T;
}
public setModel(model: Model, name?: string): void {
this.getView()!.setModel(model, name);
}
public async getResourceBundle(): Promise<ResourceBundle> {
const i18n = this.getOwnerComponent()!.getModel("i18n") as ResourceModel;
return (await i18n.getResourceBundle()) as ResourceBundle;
}
public onNavBack(): void {
const previousHash = History.getInstance().getPreviousHash();
if (previousHash !== undefined) {
window.history.go(-1);
} else {
this.getRouter().navTo("master", {}, true);
}
}
}
5b. Component.ts
テンプレートには次のようなコードがあります。以下の点を確認してください。
import UIComponent from "sap/ui/core/UIComponent";
import Device from "sap/ui/Device";
import JSONModel from "sap/ui/model/json/JSONModel";
/**
* @namespace <source_namespace>.modern
*/
export default class Component extends UIComponent {
public static metadata = {
manifest: "json",
interfaces: ["sap.ui.core.IAsyncContentCreation"]
};
public init(): void {
super.init();
const deviceModel = new JSONModel(Device);
deviceModel.setDefaultBindingMode("OneWay");
this.setModel(deviceModel, "device");
this.getRouter().initialize();
}
public getContentDensityClass(): string {
return Device.support.touch ? "sapUiSizeCozy" : "sapUiSizeCompact";
}
}
(App.controller が使うため、レガシー版のコンポーネントから getContentDensityClass を引き継ぎます。)
5c. App.view.xml(FCL のルート)
<mvc:View
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.f"
controllerName="<source_namespace>.modern.controller.App"
displayBlock="true"
height="100%">
<FlexibleColumnLayout id="flexibleColumnLayout"
backgroundDesign="Solid"
layout="OneColumn"/>
</mvc:View>
5d. App.controller.ts
import BaseController from "./BaseController";
import Component from "../Component";
/**
* @namespace <source_namespace>.modern.controller
*/
export default class App extends BaseController {
public onInit(): void {
const oOwner = this.getOwnerComponent() as Component;
this.getView()!.addStyleClass(oOwner.getContentDensityClass());
}
}
フェーズ 6 — ビューごとの変換
レガシーな <View>.view.xml + <View>.controller.js ごとに、モダンな同等物を生成します。
Main(旧 Master) ➜ Detail ➜ Welcome ➜ NotFound の順に処理します。
コントローラーを書く前の注意事項: このスキルの冒頭にある命名規則の上書きを適用してください。ハンガリアン記法の接頭辞は使いません(
const list = ...とし、const oList = ...とはしません)。Master.controller.jsをMain.controller.tsに変更します。イベントには固有の型を使用し(トラップ 3)、sap/ui/base/EventのEventは決して使用しないでください。
各コントローラーで、get_typescript_conversion_guidelines の 5 ステップの TypeScript 変換に従ってください。
Class.extend()➜@namespaceJSDoc を直前に付けたclass extends ...。sap.ui.define([...], function(...))➜ ESimport+export default class。- メソッドのパラメーターとクラスプロパティに型注釈を追加します。
Button$PressEventのような固有のイベント型を使用し、sap/ui/base/Eventの単なるEventは決して使用しないでください。 - ジェネリック getter の戻り値をキャストします。
this.byId("x") as Table、this.getView()!.getModel() as ODataModel、event.getSource() as ColumnListItemのようにします。 - 残りの型エラーを修正します。
anyやunknown as ...より、実際の型を優先してください。
6a. ビューの変換パターン
-
<SplitApp>ルート ➜App.view.xml(FCL)に置き換えます。これは 5c で完了済みです。 -
ルーティング経由で、Master を FCL の
beginColumnPagesに、Detail をmidColumnPagesに配置します。 -
各ビュー内の
<Page>ラッパーはそのままにします。問題ありません。 -
controllerName="<source_namespace>.<X>"➜controllerName="<source_namespace>.modern.controller.<X>"に置き換えます。 -
フォーマッターについては、レガシーでは
formatter: 'window.com.demo.../statusText'(グローバルパス)を使用します。 モダンではcore:requireと相対フォーマッターを使用します。<List core:require="{ formatter: '<source_namespace>/modern/model/formatter' }" ...> <ObjectListItem title="{Title}" number="{ path: 'OverallStatus', formatter: 'formatter.statusText' }" numberState="{ path: 'OverallStatus', formatter: 'formatter.statusState' }" .../> </List>名前空間のパスでは、
core:require内で/(スラッシュ表記)を使用します。レガシーの「ドット」プロパティ名は、formatter.statusTextがcore:requireマップを基準に解決されるため、フォーマッターバインディングでも引き続き機能します。 -
V2 ➜ V4 のエンティティ/パス変換については、以下の 6c を参照してください。
-
sap.ui.commons.*の参照を削除します。 -
リンターで指摘された場合は、非推奨の
numberUnit、firstStatus、responsive、condensed、backgroundDesignを削除します(コントロールによって非推奨かどうかが異なる場合があります)。
6b. コントローラーの変換パターン(5 ステップの詳細)
各 <X>.controller.js について、次のようにします。
ステップ 1:クラス構文
変換前:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
"use strict";
return Controller.extend("com.demo.X.controller.Master", {
onInit: function() { /* ... */ }
});
});
変換後(@namespace 注釈が必要です):
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace com.demo.X.modern.controller
*/
export default class Master extends Controller {
public onInit(): void { /* ... */ }
}
(実際には Controller を直接ではなく、BaseController を継承します。6d を参照してください。)
ステップ 2:ES import
すべての sap.ui.define/sap.ui.require を ES import に置き換えます。動的な sap.ui.require(["sap/m/MessageBox"], cb) は import("sap/m/MessageBox").then(...) に置き換えます。jQuery.sap.require は決して使用しないでください。
ステップ 3:型付きイベントハンドラー(改めてトラップ 3 — 必ず読んでください)
変換前:
onPress: function (oEvent) {
var oItem = oEvent.getParameter("listItem");
var oCtx = oItem.getBindingContext();
}
変換後(ハンガリアン記法は使わず、固有のイベント型を使用):
import { List$SelectionChangeEvent } from "sap/m/List";
import ColumnListItem from "sap/m/ColumnListItem";
public onPress(event: List$SelectionChangeEvent): void {
const item = event.getParameter("listItem") as ColumnListItem;
const ctx = item.getBindingContext();
}
import Event from "sap/ui/base/Event" にフォールバックしてはいけません。 ジェネリック型を使うと、getParameter/getParameters を呼び出すたびに as キャストが必要になるためです。固有のイベント型が存在しないように思える場合は、まず検索してください。
mcp__SAPUI5_MCP_Server__get_api_reference(
projectDir="<target>",
query="sap.m.SearchField#liveChange"
)
OData V4 のコンテキスト対応イベントには、sap.ui.model.odata.v4.Context を使用します。
import V4Context from "sap/ui/model/odata/v4/Context";
const ctx = item.getBindingContext() as V4Context;
ステップ 4:ジェネリック getter のキャスト
import ODataModel from "sap/ui/model/odata/v4/ODataModel";
const model = this.getOwnerComponent()!.getModel() as ODataModel;
import Table from "sap/m/Table";
const tasksTable = this.byId("tasksTable") as Table;
(注:変数名は model/tasksTable であり、oModel/oTable ではありません。)
ステップ 5:V2 ➜ V4 バインディングの移行
ここが機械的には変換できない部分の中で最も大きな作業です。把握しておくべきパターンは 3 つあります。
(i) パス内のエンティティキー
V2 のパス:/<EntitySet>('K') — 単一引用符で囲んだ文字列キーで、Set サフィックスがあります。
V4 のパス:/<Entity>(<KeyField>='K') — Set サフィックスはなく、名前付きキー形式です。
V4 サービスがドラフト有効の場合(RAP 管理のほとんどのシナリオが該当します)、キーは
複合キーです:/<Entity>(<KeyField>='K',IsActiveEntity=true)。読み取り専用のフリースタイル TS アプリではアクティブな行を読むのが
デフォルトなので、通常は IsActiveEntity=true が適切です。
バインディングコンテキストから逆引きする場合は、パスを解析せずプロパティを読み取ります:
// V2: var sKey = oCtx.getPath().replace(/^\/Set\('/, "").replace(/'\)$/, "");
// V4: just read the property
const key = ctx.getProperty("<KeyField>") as string;
サービスがドラフト有効かどうかわからない場合は、次を検索してください:
mcp__sap-docs__search(query="Draft Handling with the OData V4 Model")
(ii) 展開
V2:parameters: { expand: "<Nav>" }(bindElement 内)。
V4:parameters: { $expand: "_<Nav>" } で渡す(アンダースコアに注意 — RAP V4 サービスでは通常、
関連名のプレフィックスに _ が付きます)。または、マニフェストのモデル設定で
autoExpandSelect: true に頼り、ビューのバインディングから UI5 に判定させます。
推奨: autoExpandSelect: true とパスベースのバインディング(items="{_<Nav>}")に頼ってください。
特別な理由がない限り、$expand を手動で記述しないでください。サービスのナビゲーション名を確認するには、
$metadata を取得して <NavigationProperty Name="_X"> を探します。
(iii) Function Import ➜ アクション
V2:
oModel.callFunction("/<Function>", {
method: "POST",
urlParameters: { <Key>: sKey },
success: function(oData) { /* ... */ },
error: function(oErr) { /* ... */ }
});
V4(バインドされたアクション — RAP で一般的なパターン):
import V4Context from "sap/ui/model/odata/v4/Context";
import ODataModel from "sap/ui/model/odata/v4/ODataModel";
const ctx = this.getView()!.getBindingContext() as V4Context;
const model = ctx.getModel() as ODataModel;
// Bound action: invoke relative to the entity context
const operation = model.bindContext("<Action.FQN>(...)", ctx);
try {
await operation.invoke("$auto"); // newer V4 API; older: .execute()
MessageToast.show("Action succeeded.");
ctx.refresh(); // pick up state changes
} catch (err) {
MessageBox.error((err as Error).message);
}
完全修飾アクション名(<Action.FQN>)はサービス固有です。RAP V4 では、
com.sap.gateway.srvd.<service_name>.v0001.<action_name> のような形式です。サービスでの名前を調べるには、
$metadata を確認し、<Action Name="X" IsBound="true"> を探します — 親の <Schema Namespace>
属性に .X を付けたものが FQN です。API の形式が不明な場合は、
mcp__sap-docs__search(query="OData V4 Operations action invocation UI5") を使用してください。
ステップ 6:プライベートフィールドをクラスプロパティに移動する
変更前(レガシー):
onInit: function () {
this._sProjectId = undefined; // implicit, later
}
変更後:
export default class Detail extends BaseController {
private projectId?: string; // no Hungarian; plain field name
public onInit(): void { /* ... */ }
}
6b.1 V4 のパス相対バインディングはコントローラーではなく XML ビューに記述する
親行の選択時に子テーブルのバインディングを更新する場合(例:「タスクを選択 → タイムエントリを再読み込み」)、V4 の標準的なパターンは次のとおりです。
-
親コンテキストのナビゲーション関連(
_<Nav>)を基準として、アイテムのバインディングをXML ビューで宣言します:<Table id="<childTable>" items="{ path: '_<Nav>', parameters: { $filter: 'IsActiveEntity eq true' } }"> <columns> <Column><Text text="{i18n>colA}" /></Column> </columns> <items> <ColumnListItem> <cells> <Text text="{<FieldA>}" /> </cells> </ColumnListItem> </items> </Table> -
コントローラーで親行が選択されたときにバインディングコンテキストを設定します — UI5 は 新しいコンテキストを基準に
_<Nav>関連を取得します:const childTable = this.byId("<childTable>") as Table; childTable.setBindingContext(parentContext);
コントローラー内で new ColumnListItem({ cells: [...] }) を使って ColumnListItem のテンプレート行を構築し、
続けて bindItems({ path, template, templateShareable }) を呼び出してはいけません。動作はしますが、
これは命令型の V2 パターンです。V4 では XML とコンテキストを使う方法のほうが簡潔で宣言的であり、UI5 が
想定する方法です。テンプレート行を割り当てるコントローラーメソッドは、いずれも避けるべき兆候です。
6c. フォーマッターの統合
レガシーのグローバル(window.com.demo...formatter.statusText など)を単一の ES モジュールにまとめます:
Write: <target>/webapp/model/formatter.ts
import DateFormat from "sap/ui/core/format/DateFormat";
// instantiate once — not per call
const oDateMedium = DateFormat.getDateInstance({ style: "medium" });
/**
* @namespace <source_namespace>.modern.model
*/
const formatter = {
statusText(sStatus?: string): string {
switch (sStatus) {
case "A": return "Approved";
case "D": return "Draft";
case "X": return "Cancelled";
default: return sStatus ?? "";
}
},
statusState(sStatus?: string): "Success" | "Warning" | "Error" | "None" {
if (sStatus === "A") return "Success";
if (sStatus === "X") return "Error";
if (sStatus === "D") return "Warning";
return "None";
},
// ...taskStatusText, taskStatusState, priorityText, priorityState, dateShort, hoursDecimal
dateShort(oDate?: Date): string {
return oDate ? oDateMedium.format(oDate) : "";
},
};
export default formatter;
ビューでは core:require="{ formatter: '<source_namespace>/modern/model/formatter' }" を参照し、
バインディングで formatter: 'formatter.statusText' を使用します(コントローラー相対の場合は
formatter: '.formatter.statusText' を使用できます — どちらも動作しますが、core:require 形式のほうがすっきりしており、
コントローラーフィールドも不要です)。
6d. ペアごとに記述する
Write: <target>/webapp/view/<X>.view.xml
Write: <target>/webapp/controller/<X>.controller.ts
各ペアの後、新しいファイルだけを対象にリンターを実行します:
mcp__SAPUI5_MCP_Server__run_ui5_linter(
projectDir="<absolute target>",
filePatterns=["webapp/controller/<X>.controller.ts", "webapp/view/<X>.view.xml"],
provideContextInformation=true
)
次に進む前に指摘事項を修正してください。ビューをまたいでリンターの負債を蓄積しないでください。
6e. i18n の移行(名前変更:masterX → mainX)
<legacy>/webapp/i18n/i18n.properties を <target>/webapp/i18n/i18n.properties にコピーし、
名前の上書きに伴う以下の変更を適用します。
| レガシーキー | モダンキー |
|---|---|
masterTitle | mainTitle |
masterSearchPlaceholder | mainSearchPlaceholder |
masterCount | mainCount |
モダン XML ビュー内の対応する {i18n>X} 参照も更新します。
さらに、同じ内容の <target>/webapp/i18n/i18n_en.properties も作成します(UI5 のガイドライン:
キーを追加した場合は、すべてのロケールファイルに反映してください)。レガシーアプリにドイツ語や
その他のロケールが含まれている場合は、同じ名前変更を適用して、それらもコピーします。
6f. Welcome および NotFound ビュー
シンプルな MessagePage です。XML を 1 対 1 で翻訳し、使用していない場合は xmlns:core を削除し、
レガシーから i18n キーを再利用してください。
Phase 7 — 検証
次の 4 つのゲートをこの順序ですべて実行してください。変換完了とする前に、4 つすべてに合格する必要があります。
重要 — これらのゲートで検出できないこと: ここでレポートがクリーンでも、アプリが 正しく表示されるとは限りません。特に、Trap 5(manifest v2 + 非推奨のルーティングキー/
type: "View"の欠落)は、以下のゲートをすべて通過しますが、実行時に空白ページが表示されます。 唯一信頼できる受け入れゲートは、Phase 8d のブラウザー描画チェックです。実証済みのツールカバー率の内訳は、 Trap 5 の表を参照してください。
7a. ESLint の自動修正(最初に実行 — 手動レビューの前)
Bash: cd <target> && npx eslint webapp --fix
この処理で、@typescript-eslint/no-unnecessary-type-assertion、no-unused-vars、および
no-unused-imports の指摘の大部分が自動的に解消されます。手動レビューの前に実行することで、
1 回分の反復を省けます(実行しない場合:tsc に合格 → 手動で lint をレビュー → 機械的な問題を修正 →
lint を再実行)。
自動修正後、手動での対応が残っているファイル(通常は 1~2 個)を再確認します。
7b. UI5 linter
mcp__SAPUI5_MCP_Server__run_ui5_linter(
projectDir="<absolute target>",
provideContextInformation=false # avoid bloating output on the full-project run
)
期待値:指摘ゼロ。誤検出はまれで、ほとんどの指摘にはツールの出力に実際の修正案が示されます。
提案された修正が正しいとユーザーが確認した後に限り、fix=true 引数を使用してください。
7c. Manifest の検証
mcp__SAPUI5_MCP_Server__run_manifest_validation(
manifestPath="<absolute target>/webapp/manifest.json"
)
期待値:エラーゼロ。未使用の i18n キーに関する警告は問題ありません。
7d. TypeScript の型チェック
Bash: cd <target> && npm run ts-typecheck
期待値:エラーゼロ。完了とする前に、すべてのエラーを修正してください。// @ts-ignore で抑制しないでください。
Phase 8 — スモークテスト
8a. インストールとサーバーの起動
Bash: cd <target> && npm install
Bash: cd <target> && (npm start &)
開発サーバーが起動するまで約 5 秒待ちます。
8b. HTTP プローブ(必要ですが、十分ではありません)
Bash: curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/index.html
Bash: curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/manifest.json
期待値:どちらも 200 を返します。
サーバーに関する注意点(UI5 ガイドライン §2): UI5 開発サーバーはデフォルトの index ファイルを配信しません。必ず
http://localhost:8080/index.htmlをプローブし、http://localhost:8080/は使用しないでください。
重要: HTTP 200 は、UI が表示されることを証明しません。Trap 1(FCL が OneColumn のままになる) と Trap 2(高さゼロの空白ページ)は、どちらも 200 OK を返します。8d のブラウザー描画チェックが、 唯一信頼できる受け入れゲートです。
8c. プロキシ経由の OData プローブ
Bash: curl -s "http://localhost:8080<v4_service_url>\$metadata" | head -5
# where <v4_service_url> is the path from sap.app.dataSources.mainService.uri
期待値:有効な <edmx:Edmx ...> XML。401 の場合は、.env のプロキシ認証情報が不足しているか、
間違っています。404 の場合は、manifest のサービスバインディング名が間違っています(正規 URL を
確認するため、FE アプリの manifest を再確認してください)。
8d. ブラウザーでのレンダリング検証(重大 — 実際の受け入れ基準です)
利用可能な MCP に応じて、以下のいずれかを実行します。利用可能な MCP がない場合は、 ユーザーに手動での確認を依頼してください。
オプション A — Chrome MCP(推奨):
mcp__Claude_in_Chrome__navigate(url="http://localhost:8080/index.html")
mcp__Claude_in_Chrome__get_page_text # must include project titles from V4
mcp__Claude_in_Chrome__javascript_tool(
code="document.querySelector('.sapFFCL').clientHeight"
)
# Expected: a number > 400 (a normal dev viewport is ~600-1000 tall)
オプション B — Claude_Preview MCP:
mcp__Claude_Preview__preview_start(url="http://localhost:8080/index.html")
mcp__Claude_Preview__preview_screenshot # confirm two columns visible
mcp__Claude_Preview__preview_eval(
expression="document.querySelector('.sapFFCL').clientHeight"
)
オプション C — 手動(どちらの MCP も利用できない場合):スキルの完了を宣言する前に、 ユーザーに URL を開き、2 列でレンダリングされていることを確認するよう依頼します。
検証項目:
- 1 列ではなく、2 列の FCL レイアウトになっていること。1 列の場合は、トラップ 1 に該当します。
.sapFFCL要素のclientHeightが 400 px を超えていること。0 またはほぼ 0 の場合は、トラップ 2 に該当します。- 左列:V4 バックエンドのレコードを含むメインリスト(件数とフィールドが、同じデータを表示する レガシーアプリのものと一致していること)。
- 右列:Welcome
IllustratedMessageプレースホルダー。 - 行をクリックすると、FCL が 2 列に切り替わり(または初期レイアウトによってはそのまま 2 列を維持し)、 中央列に詳細ビューが表示されること。
- 詳細ビューにエンティティのヘッダーと、レガシーアプリにあったタブやセクションが表示されること。
- アクションボタン(承認/送信/キャンセルボタンなど)が、レガシーアプリの式バインディングのルールに従って動作すること。つまり、エンティティが適切な状態の場合にのみ有効になること。
8e. 最終レポート
次を出力します:
Modernization complete — <target>
UI5 version: 1.147.2 SAPUI5 (TypeScript)
Theme: sap_horizon
Layout: sap.f.FlexibleColumnLayout
OData: V4 — <v4_service_url>
Views: <count> (all under FCL columns)
Controllers: <count> (all extend BaseController)
Linter: clean
Manifest: valid
TypeCheck: clean
Smoke: 200 OK on index.html + manifest.json + $metadata; UI renders Master list
UI5 MCP calls: <count> (get_typescript_conversion_guidelines, get_guidelines,
create_ui5_app, run_ui5_linter ×N, run_manifest_validation,
get_api_reference ×M where needed)
sap-docs: <count> (only when V2 ➜ V4 patterns needed lookup)
arc-1: <0 unless you needed to look up a V4 service binding URL>
What's next:
- Open http://localhost:8080/index.html. Master list should render with projects.
- Compare side-by-side with legacy app on its port and FE app on its port — three UIs,
one backend.
- Optional follow-up: add OPA5 page-object journey + QUnit formatter tests (see TS-conversion
guidelines §Test Conversion for the modern OPA pattern).
- Optional follow-up: add German i18n locale (i18n_de.properties).
エラー処理 — 既知のモード
| 症状 | 原因 | 対処法 |
|---|---|---|
| ページが空白で表示される。コンソールにエラーはなく、DOM は生成されているが、すべての要素の高さが 0 | UI5 ComponentSupport によって data-sap-ui-component 属性が取り除かれたため、その属性に依存する高さの CSS セレクターが一致しません(落とし穴 2)。 | index.html のコンポーネント <div> に、style="height: 100%" を直接インラインで追加してください。CSS セレクターは機能しません。 |
| ページが空白で、列も空。コンソールに「page stack is empty but should have been initialized」という警告が表示される | Manifest の _version が 2.0.0 以降(UI5 1.136 以降)で、ルーティングターゲットに明示的な "type": "View" がない、または非推奨の viewName/viewPath/viewLevel キーが使われています(落とし穴 5)。@ui5/linter も run_manifest_validation もこれを検出しないため、どちらも問題なしと報告します。 | routing.config と各ターゲットに "type": "View" を追加し、viewName → name、viewPath → path、viewLevel → level に変更してください。Lint や検証のゲートではなく、ブラウザーでのレンダリング(フェーズ 8d)で確認してください。 |
| FCL の列が 1 つしか表示されない。ルーティングはコンテンツを中央列/末尾列に配置するが、表示されない | 一致したルートに layout プロパティがありません(落とし穴 1)。 | ホーム/メインルートを含め、すべてのルートに "layout": "TwoColumnsMidExpanded"(または適切な値)を追加してください。ルーターは、一致したルートの layout 値からのみ FCL のレイアウトを更新します。 |
TypeScript に (event.getParameters() as {listItem: ListItem}).listItem のような型キャストを強制される | 特定の <Control>$<Event>Event ではなく、sap/ui/base/Event の汎用 Event がインポートされています(落とし穴 3)。 | 例のように、特定のイベント型に置き換えてください。import { List$SelectionChangeEvent } from "sap/m/List"。不明な場合は get_api_reference を使用してください。 |
create_ui5_app はエラーを返さないが、<target>/webapp/Component.ts が .js ファイルになっている | UI5 MCP が JS テンプレートにフォールバックしました。 | typescript: true を明示して再度呼び出してください。また、frameworkVersion がサポートされていること(1.96 以上であること)を確認してください。 |
新規スキャフォールドに対して run_ui5_linter が何百もの問題を検出する | テンプレートの言語が一致していません(JS でスキャフォールドされているが、TS が必要)。 | 上記と同様に、再度スキャフォールドしてください。 |
tsc --noEmit で sap/m/... の型がないというエラーが出る | @sapui5/types がインストールされていないか、バージョンが違います。 | cd <target> && npm install --save-dev @sapui5/[email protected] |
tsc --noEmit で event.getParameter("X") が unknown を返すというエラーが出る | sap/ui/base/Event の汎用 Event 型が使われています。 | コントロール固有のイベント型に置き換えてください。import { Button$PressEvent } from "sap/m/Button"。UI5 ≥ 1.115 が必要です(ガイドライン §1 のサブ項目)。 |
npm start が「port 8080 in use」というエラーで失敗する | レガシーアプリの npm start がすでに 8080 で実行されています。 | 先にレガシーアプリを停止するか、モダンアプリを別のポートで実行してください。ui5 serve -p 8081 |
| ブラウザーに空白のページが表示され、コンソールに「failed to load Component.js」と表示される | data-sap-ui-resource-roots と sap.app.id が一致していません。 | 両方に同じ完全修飾名前空間(<source_namespace>.modern)を指定してください。 |
| OData プロキシが 401 を返す | .env がないか、認証情報が正しくありません。 | cp .env.example .env を実行し、UI5_MIDDLEWARE_SIMPLE_PROXY_USERNAME と ..._PASSWORD を設定してください。 |
| OData プロキシが 502/証明書エラーを返す | 自己署名証明書を使う上流サーバーとの TLS ハンドシェイクに失敗しています。 | ui5-middleware-simpleproxy では strictSSL: false を設定してください(skipCertificateCheck は設定しないでください。このキーは黙って無視されます)。fiori-tools-proxy ではキーは ignoreCertErrors: true です。ミドルウェアの README を確認してください。セルフヘルプパターン A を参照してください。 |
| ナビゲーション時に FCL のレイアウトが切り替わらない | ルートごとの layout プロパティがないか、controlAggregation が正しく設定されていません。 | マスター ➜ beginColumnPages、詳細 ➜ midColumnPages とし、ルートに "layout": "TwoColumnsMidExpanded" を設定してください。 |
MessageBox.show(...) の実行時エラー:「not a function」 | インポートパスが間違っています。 | import MessageBox from "sap/m/MessageBox"; を使用してください。従来の sap.ui.commons パスは廃止されています。 |
| Lint がフォーマッター関数を「未使用」と判定する | TS でフォーマッターをインポートしていますが、ビューでは core:require を使用しています(Lint が常に相互参照をチェックするとは限りません)。 | エクスポートに JSDoc の @public を付けて抑制するか、コントローラーレベルで参照を追加してください(public formatter = formatter;)。 |
V4 の bindContext("/Action").execute() が「no metadata」で拒否される | サービスのメタデータがまだ読み込み中です。 | 最初のアクションの前に一度 await oModel.requestObject("/") を実行するか、oModel.attachOnce("metadataLoaded", ...) を使い、アクションボタンの実行をその読み込み完了まで待機させてください。 |
| V4 のバインディングが何も返さず、空のリストになる | 関連名が間違っています(V2 の <Nav> と V4 の _<Nav>)。 | RAP V4 サービスでは、関連名の先頭に _ が付きます。$metadata の <NavigationProperty Name=...> を確認し、正規の名前を特定してください。 |
/<EntitySet> にバインドされた V4 リストが 404 を返す | V2 のコレクション名が残っています。 | Set の接尾辞を削除し、/<Entity> を使用してください(エンティティセットではなく、エンティティ型に基づく名前です)。 |
npx tsc --noEmit で JQuery 型がないというエラーが出る | @types/jquery がありません。 | npm install --save-dev @types/jquery を実行し、tsconfig.types に @types/jquery が含まれていることを確認してください。 |
このスキルで明示的に対象外となる内容
- 新機能の追加。 これは翻訳スキルであり、レガシーアプリとの機能同等性のみを対象とします。レガシーアプリに検索/フィルター機能がなかった場合、モダンアプリにも追加しません。
- テスト。 OPA5 + QUnit のスキャフォールディングは後続作業とし、Run 1 の範囲を小さく保ちます。準備ができたら、TS 変換ガイドライン §Test Conversion でモダンな OPA5 のパターンを確認できます。
- カスタムコントロール。 レガシーアプリにはありません。使用している場合は、TS 変換ガイドライン §UI5 Control TypeScript Conversion を参照してください(
@ui5/ts-interface-generatorとコンストラクターシグネチャーの手動コピーによる追加手順です)。 - アクセシビリティ監査。 UI5 1.147 のコントロールはデフォルトでアクセシビリティに配慮されていますが、axe と手動でのスクリーンリーダーテストは別の成果物です。
- Fiori Launchpad との統合。 モダンアプリは単独で動作します。FLP タイルの作業は FE スキルまたは別の FLP スキルの対象です。
sap_horizonを超えるテーマのカスタマイズ。カスタムテーマにはthemelib_*ビルドが必要です。ui5 buildが行う範囲を超えるビルド時のバンドル化。Webpack/Vite は使用しません。
このスキルを実行する LLM 向けの注意事項
- コードを書く前に、必ず上記の「Critical Traps」セクションを読んでください。 過去の実行でデバッグ時間の大半を占めた問題は、
layoutのない FCL のルート、style="height: 100%"の指定漏れ、汎用的なEventのインポートの3つです。これらを読み飛ばすと、ほかの部分が完璧でも、15~30分のロスにつながります。 - 命名規則の上書きは、指摘せずに適用してください。 「master」ではなく「main」を使用し、ハンガリアン記法のプレフィックスは使わないでください。「一貫性」を理由にレガシーの命名を残さないでください。レガシーの命名も、刷新する対象の一部です。
- 必ず最初に
get_typescript_conversion_guidelinesとget_guidelinesを呼び出してください。 内容は大規模で、バージョン固有です。記憶を頼りに言い換えないでください。 - 実行中のベストプラクティスをその都度調べるには、sap-docs MCP を使用してください。 ルーティング設定、V4 のバインド動作、ドラフトのセマンティクス、FCL のレイアウト値などに不明点があれば、
mcp__sap-docs__searchで検索して、該当するトピックを取得してください。推測しないでください。 - ビューごとの粒度が重要です。 次のビューに進む前に、1つのビューを XML、コントローラー、フォーマッター、Lint まで完了してください。技術的負債をビュー間で蓄積させないでください。
- レガシー API 名が最新の移行先でなくなっている場合(コントロール名の変更やメソッドの置き換えなど)は、必ず
get_api_referenceを参照してください。 推測しないでください。 - ガイドラインで指定された依存関係のバージョンを、黙って下げないでください。 バージョンを上げる場合のみ変更してください。
anyやunknown as ...のキャストは使わないでください。 実際のコントロール型を使用してください。anyキャストを使うと、V2 と V4 のバインドに関する不具合が隠れ、実行時に「undefined.replace is not a function」というエラーとして表面化します。- OData V4 のパスのセマンティクスは V2 と異なります。
getPath().replace(/^\/Set/, "")のパターンをそのまま引き継がないでください。正しい方法はctx.getProperty("Key")です。 - 最終の Lint 実行前に、デバッグ用の
console.log文を削除してください。 レガシーアプリにはconsole.log("[X] ...")が多数あります。これらはレガシーの悪い慣行の例としては適切ですが、刷新後のバージョンでは使いません。 - 「var that = this;」と後続の .bind(that) のパターンは、完全に削除してください。 TS ではアロー関数が
thisを正しく処理します。 - レガシーの英語以外の JSDoc コメント("Worst-practice 2020-Mix..."、"manuell, weil immer schon so gemacht" など)は、そのまま残さず削除してください。 これらはレガシーの状態を説明するもので、刷新後のコードと矛盾します。最新のコントローラーの目的を簡潔な英語で記述してください。
- UI5 linter の前に
npx eslint webapp --fixを実行してください。 これにより、事後の Lint 対応の大半を占める3種類の指摘、no-unnecessary-type-assertion、no-unused-vars、no-unused-importsが自動で解消されます。この手順を省くと、1回分の反復サイクル(tsc は成功 → ui5lint は失敗 → 機械的な問題を修正)が余計に必要になります。 - Press イベントのハンドラーには、通常パラメーターはありません。
onApprove、onNavBack、onItemPress(イベントソースではなくthis.byIdを使用する場合)は、_event: Button$PressEventではなく()で宣言してください。後者では未使用のインポートが発生し、追加の編集が必要になります。上記の Trap 4 を参照してください。 - このスキルでは ARC-1 の使用は任意です。 V4 サービスバインド URL(例:
SAPRead type=SRVB name=<service_binding>)の検索や、バックエンドの CDS/動作定義に基づくステータスコードと列挙型の対応の確認が必要な場合にのみ使用してください。それ以外では省略してください。 - ここでは fiori-mcp は使用しません。 アノテーションを使う Fiori Elements 向けのものであり、手動コントロールを使うフリースタイルの TS アプリ向けではありません。
- 同じバックエンド用の兄弟参照アプリがある場合(例:同じ V4 サービスを使用してすでに構築した Fiori Elements アプリ)は、まずそのアプリの
manifest.jsonとui5.yamlを読んでください。 これらのファイルは、V4 URL、プロキシ設定、ナビゲーションの関連付け、認証に関する注意点について、正常に動作することが確認された参照情報です。値を再発見するより、コピーするほうが迅速です。 - サーバーに関する注意点: UI5 の開発サーバーには既定のインデックスがありません。必ず
/index.htmlを確認してください。 - UI5 ガイドライン §4 では
SimpleFormは禁止されています。 フォームが必要な場合はFormとColumnLayoutを使用してください(このレガシーアプリにはフォームがないため、この点は対応不要です)。 - 組み込みの書式設定(数値、日付、通貨)には、カスタムフォーマッターより先に OData 型(
sap.ui.model.odata.type.*)を使用してください。 カスタムフォーマッターは、組み込みの代替手段がないステータスコードからテキストへの変換など、固有のビジネスロジックにのみ使用してください。