# Modernize Ui5 App

> Convert a legacy UI5 freestyle JavaScript app (sync bootstrap, jQuery.sap.*, ES5, sap_belize) into a modern UI5 1.147 TypeScript app with sap.f.FlexibleColumnLayout, typed event handlers, ES modules, BaseController, and sap_horizon — with 5 documented critical traps up front. Use when asked to "modernize this UI5 app", "convert to UI5 TypeScript", "upgrade jQuery.sap to modern UI5", or "migrate freestyle UI5 to 1.147".

- Skill: `arc-mcp/modernize-ui5-app` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arc-mcp/modernize-ui5-app`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arc-mcp/modernize-ui5-app/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: arc-mcp (https://skillmd.com/u/arc-mcp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arc-mcp/modernize-ui5-app

---


# Modernize UI5 freestyle JS app ➜ UI5 TypeScript app

Convert a legacy UI5 freestyle JavaScript app (typical 2018–2021 era — sync bootstrap, JS
controllers, `jQuery.sap.*`, global formatter, ES5 patterns, no types, `sap_belize`) into a
modern UI5 TypeScript app on a recent 1.x release with async loading, manifest-driven
configuration, a proper `BaseController`, sap_horizon theme, ES modules, typed event handlers,
and clean `ui5-linter` + `tsc --noEmit` output. Runs side-by-side: the legacy app stays
untouched at `<source_app>/`; the modern app lands in `<modern_app>/`.

This skill is **one of two parallel UI paths** after the RAP backend lands. Pick this one if
the target architecture is a **freestyle TypeScript** app (custom controllers, manual binding,
explicit i18n). Pick `convert-ui5-to-fiori-elements.md` instead if the target is a
**Fiori Elements V4** app (annotation-driven; minimal custom code). Both start from the same
legacy JS app + the same V4 RAP service produced by `migrate-segw-to-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)
```

> **Path/namespace placeholders.** `<source_app>/`, `<modern_app>/`, `<source_namespace>`,
> `<modern_namespace>` are user-provided. Defaults: source is `legacy-*-app/` (sibling of the
> target); modern app namespace is derived from source by appending `.modern`.

---

## Which MCPs this skill uses (and which it doesn't)

| MCP | Used for | When |
|---|---|---|
| **UI5 MCP** (`mcp__SAPUI5_MCP_Server__*`) | Authoritative TS conversion guidelines, general UI5 guidelines, app scaffolding, API reference lookups, linter, manifest validator, version info | Throughout — this is the primary MCP for this skill |
| **sap-docs MCP** (`mcp__sap-docs__*`) | OData V4 binding patterns, draft handling, control documentation | When V2→V4 binding behaviour is non-obvious (e.g. composite key on draft, `$expand=_Tasks`, action invocation) |
| **arc-1 MCP** | OPTIONAL — service binding URL lookup, status-code semantics | Only if the V4 URL isn't readily available (e.g. you can't read the FE app's manifest); skip otherwise |
| **fiori-mcp** | NOT USED | This is a freestyle TS app, not Fiori Elements — no annotations to generate |

**There is no "convert to TS" tool** in the UI5 MCP. Conversion is mechanical and driven by
`mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines`, which returns the authoritative
playbook. You call it once at the start and follow it verbatim.

**Also use `mcp__sap-docs__search` during the run** whenever a UI5 best-practice is non-obvious
or contested (FCL routing, V4 binding semantics, draft handling, accessibility, theming). The
SAP help portal indexed there is authoritative and version-specific — quote it rather than
guessing.

---

## Critical traps — read these BEFORE writing any code

Past runs of this skill tripped on three issues that took multiple iterations to diagnose. They
are easy to avoid if you know about them up front; brutal if you don't. Even a strong LLM lost
~15-20 minutes debugging "blank page, no console error" because of Traps 1 + 2.

### Trap 1: FCL stays at "OneColumn" — only one column visible

**Symptom:** Both views are routed correctly, console is clean, accessibility tree shows
content for both columns — but visually only the left column renders.

**Root cause:** `sap.f.routing.Router` only changes the FCL `layout` property when the matched
route **explicitly declares** one. A route with no `layout` leaves FCL at its initial value
(`OneColumn`), so the mid / end columns stay hidden even though the router placed targets in
them.

**Fix:** **Every** FCL route MUST have a `layout` property. Not just detail/drill-down — also
the home / main / not-found route, every route that targets anything other than the
`beginColumnPages` aggregation alone.

```jsonc
"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
  }
]
```

### Trap 2: Blank page — DOM populated but every element has height: 0

**Symptom:** Console is clean, accessibility tree shows all content, but visually the page is
blank. DevTools confirms every nested element has `height: 0` cascading from the body's
component div.

**Root cause:** UI5's `ComponentSupport` module **strips the `data-sap-ui-component` attribute**
from the body's container div during processing. CSS selectors that match against that
attribute (`body > div[data-sap-ui-component] { height: 100% }`) stop matching after
ComponentSupport runs, so the wrapper div has no height and `data-height="100%"` resolves to
100% of 0.

**Fix:** Put `style="height: 100%"` **inline** on the component div in `index.html`. CSS
selectors keyed on `data-sap-ui-component` are not reliable here.

```html
<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>     <!-- ← REQUIRED inline, not in <style> -->
</body>
```

### Trap 3: `import Event from "sap/ui/base/Event"` is a code smell

**Symptom:** Strange casts like `(event.getParameters() as { listItem?: ListItemBase }).listItem`
appear in the controller. TypeScript can't see event parameters; you compensate with `as` casts
on `getParameter` or `getParameters` return values.

**Root cause:** Importing the generic `Event` type from `sap/ui/base/Event` forfeits the strong
typing UI5 ≥ 1.115 provides. UI5 generates `<Control>$<Event>Event` and
`<Control>$<Event>EventParameters` types for every control event — those are what to import.

**Fix:** If you find yourself writing `import Event from "sap/ui/base/Event"`, **stop and find
the right specific type**. Common ones for this skill's surface:

| Event source | Specific type | Module |
|---|---|---|
| `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` |

> **Important — ListBase inheritance:** `sap.m.List` and `sap.m.Table` **inherit** their
> selection / item-press / update / delete / swipe events from `sap.m.ListBase`. The TS event
> types live on `sap/m/ListBase`, not on `sap/m/List` or `sap/m/Table`. Confirmed in the SAP
> Help portal: "Both sap.m.List and sap.m.Table offer the same events, inheriting them from
> sap.m.ListBase." Don't go looking for `Table$SelectionChangeEvent` — it doesn't exist;
> use `ListBase$SelectionChangeEvent` from `sap/m/ListBase` and TypeScript accepts it for
> both controls' `selectionChange` events without casts.

If you can't find a specific event type for a UI5 ≥ 1.115 control, call
`mcp__SAPUI5_MCP_Server__get_api_reference(query="sap.m.<Control>#<eventName>")` to confirm
the typed name exists. Do not fall back to the generic `Event`.

### Trap 4: `onApprove(event: Button$PressEvent)` — declare with no parameter

**Symptom:** ESLint flags `_event` (or `event`) as `no-unused-vars`. You drop the parameter,
then ESLint flags the now-orphaned `import { Button$PressEvent }` as `no-unused-imports`. Two
edits, two re-lint cycles, one minute lost.

**Root cause:** UI5 button-press handlers typically don't use the event payload — `oCtx`,
`projectId`, etc. all come from `this.getView()` or from class fields populated by route
matching. Declaring `event: Button$PressEvent` is a knee-jerk reflex from the typed-events
rule that doesn't apply here.

**Fix:** declare press handlers with **no parameters** when you don't read the event:

```ts
// 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;
}
```

The same applies to `onNavBack`, `onItemPress` (when you derive the source from `this.byId`
not the event), and any other handler where the event payload is unused.

### Trap 5: Manifest v2 + missing `"type": "View"` — routes match but nothing renders

**Symptom:** Page is blank. FCL columns exist with the right widths but each NavContainer is
empty. Console is otherwise clean except for one easily-missed warning a millisecond after
each route match:

```
page stack is empty but should have been initialized -
application failed to provide a page to display
```

Routing logs show the route matches correctly (`"The route named 'main' did match"`), but no
view ever gets placed in any column.

**Root cause:** With `"_version": "2.0.0"` or higher (introduced in UI5 1.136 — the same
version we're targeting in this skill), the older routing keys `viewName`, `viewPath`,
`viewLevel` are **removed** and replaced by `name`, `path`, `level` — and a routing **target
no longer has an implicit `"type": "View"` default**. Without an explicit type, target
resolution silently produces nothing.

Source: SAP Help, *"Migration Information for Upgrading the Manifest File"*, the 2.0.0
(1.136) "Deprecated Manifest Entries" row: *"The routing properties ViewId, viewName,
viewPath and viewLevel can no longer be used. Please use the documented alternatives by
replacing them with the properties id, name, path and level, respectively along with adding
the `type: "view"`."*

**Fix:** in the `routing` block of `manifest.json`:

1. Add `"type": "View"` to `routing.config` (so it's the default for all targets in the
   block — saves repeating it).
2. Use `path` (NOT `viewPath`) for the view-folder namespace.
3. Use `name` (NOT `viewName`) on each target — and add `"type": "View"` per target too
   (belt + braces; some UI5 versions don't propagate the config default reliably).
4. Use `level` (NOT `viewLevel`) on each target.

```jsonc
"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
    }
}
```

This shape works in both manifest v1.x and v2.x — adding `type: "View"` is backwards-compatible
(it became available in 1.14.0 / UI5 1.62). The new key names (`name`/`path`/`level`) are also
backwards-compatible. So always emit the v2 shape, even if you're not sure whether the
project ends up on v1 or v2 — there's no downside.

**Quick diagnostic when you see this:** open the browser console, search for "page stack is
empty". If you find it: you have Trap 5. If you don't, you're probably looking at Trap 1
(missing `layout`) or Trap 2 (height cascade).

**Why the validators DON'T catch this** (verified empirically against `@ui5/linter@1.19.0`
and `@ui5/manifest@1.86.0`, the same versions wrapped by the UI5 MCP):

| Tool | Catches Trap 5? | Why |
|---|---|---|
| `run_ui5_linter` (`@ui5/linter`) | **No** | The `no-removed-manifest-property` rule exists but only checks `resources/js`, `rootView/async`, `routing/config/async`. Does NOT check routing targets for `viewName`/`viewPath`/`viewLevel`. The linter source reads `target.name ?? target.viewName` — it gracefully accepts either, without warning. |
| `run_manifest_validation` (`@ui5/manifest` schema + Ajv) | **No** | The schema marks `viewName`/`viewPath`/`viewLevel` as `"deprecated": true` with a description pointing to the v2 replacement, but Ajv with the MCP's `strict: false` config ignores the `deprecated` flag. The schema also has both `legacyTargetAddition` AND `actualTargetAdditionStandard` as alternatives side-by-side (no `_version`-conditional `if/then` branch), so a manifest with `viewName` validates as `isValid: true`. |
| `npm run ts-typecheck` | **No** | Manifest is JSON — TypeScript doesn't see it. |
| `eslint webapp` | **No** | ESLint doesn't model UI5 manifest semantics. |
| Browser runtime | **Indirectly** | The `"page stack is empty but should have been initialized"` console warning is the ONLY automated signal, and it only fires at runtime, not at lint time. |

**Implication for the skill:** the Phase 7 acceptance gates (linter / manifest validation /
ts-typecheck) WILL all report green for a v2 manifest with the deprecated routing keys. The
ONLY honest gate for this trap is the **Phase 8d browser-render verification** — that's why
Phase 8d is mandatory, not optional. Don't believe a "clean lint + clean validation" report
means the routing config is correct.

If you have the cycles to file an upstream issue, this is a clear gap in `@ui5/linter`'s
`no-removed-manifest-property` rule — adding `routing.targets[].viewName/viewPath/viewLevel`
to the checked-property list would close it.

---

## Self-help: when the skill doesn't have the answer

The skill captures common patterns, but every project has its own quirks. When you hit
something the skill doesn't cover, **investigate before guessing**. UI5 has a fragmented
middleware ecosystem and a deep type system; an educated guess often gets the wrong key name
or the wrong inheritance branch. The investigations below cost 30 seconds and save iterations.

### Pattern A — Middleware / proxy / build-tool config option

**Trigger:** you're setting a config key on a UI5 middleware (`ui5-middleware-simpleproxy`,
`ui5-middleware-livereload`, `fiori-tools-proxy`, `ui5-tooling-transpile`, anything in
`ui5.yaml`'s `customMiddleware` or `customTasks`).

**Why investigate:** UI5 middlewares silently ignore unknown configuration keys — no error, no
warning, but they do the wrong thing. The naming conventions differ across packages: TLS-skip
is `strictSSL: false` in `ui5-middleware-simpleproxy`, `ignoreCertErrors: true` in
`fiori-tools-proxy`, and other names elsewhere. Don't extrapolate from one to another.

**Recipe:**

```text
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>
```

If the README isn't local yet (pre-`npm install`), use WebFetch or `gh api` against the GitHub
mirror.

### Pattern B — UI5 control event TypeScript type

**Trigger:** you're typing an event handler parameter and you're not sure what the specific
`<Control>$<Event>Event` type is called or where it lives.

**Why investigate:** events are inherited — they're defined on a parent class and reused by
subclasses. Looking for `Table$SelectionChangeEvent` returns nothing because `selectionChange`
is defined on `ListBase`, not `Table`. The Trap 3 table covers the events this skill commonly
hits; for anything else, ask the UI5 MCP.

**Recipe:**

```text
mcp__SAPUI5_MCP_Server__get_api_reference(
  projectDir="<absolute target>",
  query="sap.m.<Control>#<eventName>"
)
```

Read the result for: (a) which class actually defines the event (= which module to import
from), (b) the canonical event type name. If `query` returns nothing, search broader
(`query="sap.m.<Control>"`) and inspect the event list; the type name is `<DefiningClass>$<EventName>Event`.

### Pattern C — OData V4 binding / draft / action semantic

**Trigger:** you're writing V4-specific code (composite keys, `$expand=_X` navigation, action
invocation, draft handling, batched updates) and you're not sure of the canonical pattern.

**Why investigate:** V4 differs from V2 in non-obvious ways, and the difference is
version-specific. Guessing usually costs a `tsc` cycle or a runtime "no metadata" error.

**Recipe:**

```text
mcp__sap-docs__search(
  query="<feature> OData V4 model UI5",
  sources=["sapui5","sap-help"],
  includeOnline=true
)
mcp__sap-docs__fetch(id="<best result id>")
```

Useful starting topics (search these terms verbatim):

- "Draft Handling with the OData V4 Model"
- "Operations" (V4 actions/functions)
- "Auto-`$expand` / `$select`"
- "Reducing the Number of Requests Required to Get the Properties of a Single Entity"
- "Reducing Roundtrips"
- "Filtering" + "Sorting"

### Pattern D — FCL routing behaviour surprise

**Trigger:** FCL renders the wrong column count, columns flicker, deep-link doesn't restore
layout, "Close" button absent.

**Why investigate:** `sap.f.routing.Router` is layout-driven. Almost every FCL surprise is a
missing or wrong `layout` value on the route — not a view-XML bug.

**Recipe:** open `manifest.json`, audit every entry under `routing.routes[]` for a `layout`
property. The home/main route needs one too (Trap 1). If you're not sure which `LayoutType`
enum value to use, search:

```text
mcp__sap-docs__search(query="sap.f.LayoutType FCL three-column")
```

### Pattern E — Page renders blank but DOM is populated

**Trigger:** accessibility tree shows content, console is clean, viewport is empty.

**Why investigate:** height cascading. Some ancestor element resolves to `height: 0` and
collapses every descendant. The trap is usually a `data-` attribute selector that doesn't
match because `ComponentSupport` stripped it (Trap 2), but other height-cascade variants exist
(e.g. `<body>` without `height: 100%`, a `Page` with implicit container width but no height).

**Recipe:** in browser DevTools, click the body, walk the descendant tree in the Elements
panel, watch the computed `height`. The first element with `0` is where the chain breaks.
Apply `style="height: 100%"` inline (preferred) or a CSS selector keyed on a non-stripped
attribute (`id`, `class` — never `data-sap-ui-*`).

### Pattern F — TypeScript compiles but ui5-linter fails

**Trigger:** `npm run ts-typecheck` is clean, but `ui5lint` flags issues — and `tsc` won't help
diagnose them.

**Why investigate:** ts-typecheck checks types; ui5-linter checks UI5-runtime concerns
(deprecated APIs, framework conventions, manifest cross-references, XML view binding
correctness). They're orthogonal. The right order is `eslint --fix` first (cleans mechanical
TS-level noise), then `ui5-linter` (catches the UI5-specific issues), then `tsc` (any
remaining type errors).

If `ui5-linter` complains about a finding you don't understand, request context:

```text
mcp__SAPUI5_MCP_Server__run_ui5_linter(
  projectDir="<absolute target>",
  filePatterns=["<the file>"],
  provideContextInformation=true
)
```

The `provideContextInformation: true` flag returns API-reference excerpts and documentation
links explaining each finding.

### Pattern G — UI5 best-practice you're not sure about

**Trigger:** "should I use Form or SimpleForm?", "should this be a JSONModel or a path?",
"is `core:require` the right way to load this formatter?", "should the manifest declare a
specific theme?"

**Why investigate:** SAPUI5 has version-specific guidelines. The MCP returns the authoritative
list for the project's version.

**Recipe — start with the two pinned guideline tools:**

```text
mcp__SAPUI5_MCP_Server__get_guidelines              # general UI5 dev rules
mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines   # TS-specific rules
```

If the answer isn't in those, escalate to sap-docs:

```text
mcp__sap-docs__search(query="<your question>")
```

### Pattern H — You've debugged the same problem twice this run

**Trigger:** you've written code, hit an error, fixed it, and the same shape of error
recurred on a different file or step. That's a signal the skill is missing a
generally-applicable pattern.

**Action:** capture it as a Run-Notes entry with: symptom, root cause, fix, and a one-line
"generic rule" extracted. After the run, propose adding it to the Critical Traps section if
it's likely to recur in future projects.

---

## Naming overrides for this project (deviation from older SAPUI5 JS conventions)

These two conventions **override** the older SAPUI5 JS Coding Guidelines. Apply silently — do
not preserve the legacy style during conversion.

| Convention | Older SAPUI5 JS Guidelines | This skill's rule |
|---|---|---|
| Variable prefixes | Hungarian recommended (`oModel`, `sQuery`, `iTotal`, `aFilters`, `mArgs`, `fHours`, `bFlag`) | **No Hungarian notation.** Use plain names: `model`, `query`, `total`, `filters`, `args`, `hours`, `flag`. TypeScript types make the prefix redundant. |
| "Main" entity name | "master" (e.g. `Master.view.xml`, i18n key `masterTitle`) | **"main"** everywhere — controller / view / route / target / file / i18n keys. Aligns with SAP's own [Inclusive Language guide](https://help.sap.com/docs/TERMINOLOGY/25cbeaaad3c24eba8ea10b579ce81aa1/83a23df24013403ea4c1fdd0107cc0fd.html) ("master branch → main branch"). |

Rename map for the typical SEGW-to-RAP demo surface:

| Legacy | Modern |
|---|---|
| `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 key `masterTitle` | `mainTitle` |
| i18n key `masterSearchPlaceholder` | `mainSearchPlaceholder` |
| i18n key `masterCount` | `mainCount` |
| `var oModel = ...` | `const model = ...` |
| `var sQuery = ...` | `const query = ...` |
| `var iTotal = ...` | `const total = ...` |
| `var aFilters = ...` | `const filters = ...` |
| `var oCtx = ...` | `const ctx = ...` (or `const context = ...` if clearer) |
| `var oList = ...` | `const list = ...` |
| `var oRouter = ...` | `const router = ...` |
| `var oEvent = ...` (parameter) | `event` |
| `var that = this;` | drop entirely; use arrow function |

**Exception:** keep a prefix only when dropping it would collide with a reserved word, a
same-named import, or a UI5 control name (e.g. local `const event = ...` collides with no
common UI5 import, so it's fine; but `const Date = ...` would shadow the global, so call it
`oDate` or rename it `workDate`).

Include a "Naming overrides:" line in the Phase 2 plan output so the user can confirm them for
this run.

**Atomic-rename tip:** the master → main rename touches at minimum 7 locations: file name, view
`controllerName` attribute, manifest route name, manifest target name + key + viewName, i18n
keys × 2 locale files, `BaseController.onNavBack` fallback, every `navTo("master", ...)` call
in any controller. To avoid re-edits, **grep up-front** to enumerate all hits, then edit in
one batch:

```text
Bash: grep -rn -E "master|Master" <target>/webapp
```

Review the output, decide which hits are renames (not, e.g., the word "master" inside a
sentence in the legacy German comments — those should be dropped anyway). Edit all in one
pass, then verify with a second grep that returns empty.

---

## Smart defaults (apply silently — do NOT ask before research)

| Setting | Default | Rationale |
|---|---|---|
| Source app | `<source_app>/` | The freestyle JS app under the workspace |
| Target app | `<modern_app>/` | Empty folder reserved for this skill's output |
| Target UI5 version | `1.147.2` | Latest 1.x at writing; matches the FE app in the demo; aligned with 2.0-API |
| Framework | `SAPUI5` | Matches the legacy app's ui5.yaml and the FE app's runtime |
| Language | TypeScript | Per `get_typescript_conversion_guidelines` |
| Types package | `@sapui5/types@1.147.2` | Required by UI5 MCP TS guidelines (NOT the older `sap-ui5-types` typo) |
| App namespace | Source namespace + `.modern` (e.g. `<source_namespace>.modern`) | Distinguishes from legacy in routing; keeps grep continuity |
| Theme | `sap_horizon` | UI5 1.108+ default; legacy `sap_belize` is deprecated |
| Layout | Translate `sap.m.SplitApp` ➜ `sap.f.FlexibleColumnLayout` (FCL) | Modern responsive default for main + detail |
| Naming | No Hungarian prefixes; "main" not "master" — see "Naming overrides" section above | Aligned with TS-idiomatic names + SAP Inclusive Language |
| Bootstrap | `data-sap-ui-async="true"` + `data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"` | Per UI5 guidelines; sync is deprecated and breaks 2.x |
| Manifest version | `_version: 1.60.0` or later | Required for `sap.app.dataSources` + declarative models |
| OData model | The V4 service produced by `migrate-segw-to-rap` (or any V4 service the user names), via dev-server proxy. Pattern: `/sap/opu/odata4/sap/<service_binding>/srvd/sap/<service_binding>/0001/` for SRVD-direct, or check `$metadata` of a sibling reference app if one exists | V4 demonstrates the modern pattern; same backend as any FE app you've already built |
| Routing | `sap.m.routing.Router` ➜ `sap.f.routing.Router` with per-FCL-column targets | FCL needs the f-router |
| BaseController | Required | Single source of truth for `getRouter`, `getModel`, `getResourceBundle`, `getOwnerComponent` |
| Event types | Use `<Control>$<Event>Event` (e.g. `Button$PressEvent`) | UI5 ≥ 1.115 supports them; UI5 guideline says **MUST** use |
| Formatters | OData types (`sap.ui.model.odata.type.*`) first; custom only for unique business logic | Per UI5 guideline §1 |
| Forms | `sap.ui.layout.form.Form` + `ColumnLayout` if any | Never `SimpleForm` (UI5 guideline §4) |
| Casts | Real control types (`as Button`), never `as any` / `as unknown as ...` | Per TS conversion §General Rules |
| Tests | OPA5 + QUnit skipped from first cut | Promoted to follow-up if Run 1 is green |
| Linter | `mcp__SAPUI5_MCP_Server__run_ui5_linter` → 0 findings | Hard acceptance criterion |
| Manifest validation | `mcp__SAPUI5_MCP_Server__run_manifest_validation` → 0 errors | Hard acceptance criterion |
| Type check | `npm run ts-typecheck` (script added to package.json) → 0 errors | Hard acceptance criterion |

## Input

The user provides **one of**:

- A relative path to the legacy app, e.g. `<source_app>/`
- A target folder name, e.g. `<modern_app>/` (skill infers source as the sibling `legacy-*`)
- Nothing — assume defaults (`<source_app>/` ➜ `<modern_app>/`)

If both folders exist and the target is non-empty, ask: **"`<target>/` already has content.
Wipe it and start over, or migrate into the existing structure?"** Default to wipe-and-rewrite
for the demo.

---

## Phase 0 — Preflight

### 0a. Legacy app readable

```text
Bash: cat <legacy>/webapp/manifest.json
Bash: ls <legacy>/webapp/{controller,view,model,fragment,i18n}
```

Assert: `manifest.json` parses; `webapp/controller/` and `webapp/view/` both exist; at least
one `*.controller.js` and one `*.view.xml` are present. If any of these fail, stop with
*"`<legacy>` does not look like a UI5 app — check the path."*

### 0b. UI5 MCP server reachable

```text
mcp__SAPUI5_MCP_Server__get_version_info(frameworkName="SAPUI5")
```

Assert: returns at least `1.147.x` in the version map. If the tool errors, stop with *"UI5
MCP server is not configured; configure it in `.cursor/mcp.json` before running this skill."*

### 0c. Pull the authoritative guidelines (do this BEFORE writing any code)

```text
mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines
mcp__SAPUI5_MCP_Server__get_guidelines
```

Read both responses fully. They are the source of truth for:

- dev-dependency versions (`@ui5/cli`, `typescript`, `ui5-tooling-transpile`, `ui5-middleware-livereload`, `typescript-eslint`)
- `tsconfig.json` shape (`target: es2023`, `module: es2022`, `types: ["@sapui5/types"]`, paths map)
- `ui5.yaml` shape (`ui5-tooling-transpile-task` + `ui5-tooling-transpile-middleware`)
- The **5-step code conversion sequence**: (1) class syntax, (2) ES modules, (3) type annotations, (4) casts for generic getters, (5) remaining issues
- The **`@namespace` JSDoc rule**: required immediately before each exported class for the back-transformation to re-add the UI5 class name
- The casts to avoid: never `any`, never `unknown as ...` — use real control types
- The control-event-type rule: import `Button$PressEvent` from `sap/m/Button`, not `Event` from `sap/ui/base/Event`

These tool responses are large and version-specific — do not paraphrase from memory; quote them
when you need to.

### 0d. npm + node available

```text
Bash: node --version && npm --version
```

Assert: Node 22+ (CLAUDE.md requirement) and npm 10+.

---

## Phase 1 — Discover the legacy app

Read every relevant file in `<legacy>/webapp/`. Classify findings as **blocker** (must fix for
modern UI5), **cleanup** (worst-practice but works), or **cosmetic** (style).

### 1a. Manifest scan

Pull `_version`, `sap.ui5.dependencies.minUI5Version`, `sap.ui5.rootView`,
`sap.ui5.dependencies.libs`, `sap.ui5.routing`, `sap.ui5.models`, `sap.ui5.contentDensities`,
`sap.ui5.resources`, theme references.

Common legacy patterns:

- `_version: 1.40.0` ➜ blocker (target 1.60+)
- `minUI5Version` < 1.108 ➜ blocker (no async guarantees)
- `routerClass: "sap.m.routing.Router"` with FCL target ➜ blocker
- Hard-coded service URLs in `sap.ui5.models` ➜ cleanup (move to `sap.app.dataSources`)
- `contentDensities: { compact: true, cozy: true }` ➜ cleanup (still supported, just move to manifest)
- `supportedThemes: ["sap_belize"]` ➜ cleanup (drop; sap_horizon is default)

### 1b. Component.js scan

Read `<legacy>/webapp/Component.js`. Look for:

- `jQuery.sap.require("...")` ➜ blocker (delete; use ES imports)
- Hardcoded service URL constant + `new sap.ui.model.odata.v2.ODataModel(...)` in `init` ➜ blocker (move model to manifest `sap.ui5.models[""]`)
- Manual `device` model creation ➜ keep (it's normal, but typed in TS)
- `sap.ui.model.BindingMode.TwoWay` for OData V4 ➜ blocker (V4 prefers `OneWay` reads + explicit edits)
- `useBatch: false` ➜ N/A for V4 (V4 always batches differently)
- Mixed concerns (formatter loaded via `jQuery.sap.require` in Component) ➜ blocker

### 1c. Controller scan (per controller)

For each `<legacy>/webapp/controller/<X>.controller.js`:

```text
Read: <legacy>/webapp/controller/<X>.controller.js
```

Flag:

- `var that = this;` followed by closures ➜ cleanup (arrow functions)
- `oCtx.getPath()` regex parsing (`.replace(/^\/Set\('/, "")`) ➜ blocker (use `oCtx.getProperty("Key")`)
- `sap.ui.getCore().byId(...)` ➜ cleanup (`this.byId(...)`)
- `sap.ui.getCore().getModel(...)` ➜ blocker (`this.getOwnerComponent()!.getModel(...)` with cast)
- `sap.ui.core.UIComponent.getRouterFor(this)` ➜ cleanup (move to BaseController)
- `sap.m.MessageBox` accessed as global ➜ blocker (`import MessageBox from "sap/m/MessageBox"`)
- `jQuery.sap.require(...)` ➜ blocker (ES import)
- Global formatter calls (`window.com.demo.formatter.X`) ➜ blocker (consolidate into formatter module, import in views via `core:require`)
- `oModel.callFunction("/X", {method:"POST", urlParameters: {...}})` (OData V2 function-import) ➜ blocker (translate to V4 action: `model.bindContext("/Action(...)").execute()`)
- `setTimeout(..., 500)` for "refresh after data arrives" ➜ blocker (use binding events, e.g. `dataReceived`)
- No JSDoc on parameters ➜ cosmetic (TS rewrite handles it)

### 1d. View scan (per view)

For each `<legacy>/webapp/view/<X>.view.xml`:

```text
Read: <legacy>/webapp/view/<X>.view.xml
```

Flag:

- Inline event handlers (`press="onSomething"`) referencing methods that don't exist ➜ blocker (will throw)
- `<core:Fragment fragmentName="...">` without async ➜ cleanup
- Deprecated controls (`sap.ui.commons.*`) ➜ blocker
- `sap.m.SplitApp` root ➜ blocker (translate to FCL per smart-defaults)
- Hardcoded strings instead of i18n keys ➜ cleanup
- Path-based formatters (`formatter: 'window.com.demo.X.statusText'`) ➜ blocker (convert to `core:require` of formatter module + `formatter: '.formatter.statusText'`)
- `enabled="{= ${Status} === 'D' }"` (expression binding) ➜ keep but adapt to V4 key (no `Status` ➜ `OverallStatus` etc., depending on the V4 model)

### 1e. Formatter scan

For `<legacy>/webapp/model/formatter.js`:

- `jQuery.sap.declare(...)` + `window.com.demo...` global namespace ➜ blocker (rewrite as ES module with `export function`)
- `sap.ui.core.format.DateFormat.getDateInstance(...)` instantiated per call ➜ cleanup (memoize once at module top-level)
- Per-call format object instantiation ➜ cleanup (instantiate once)
- Status / priority mapping switches ➜ keep but type as `string | undefined` ➜ `string`

### 1f. Build the findings report

Output a structured summary to the user:

```text
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 — Design plan + user approval

Print the migration plan in this exact format and STOP for `ok` / `edit` / question:

```text
Plan — modernize <legacy> ➜ <target>:

UI5 version:       1.147.2 SAPUI5 (TypeScript)
Types:             @sapui5/types@1.147.2
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.
```

Wait for `ok` before mutating anything in `<target>/`.

---

## Phase 3 — Scaffold the modern TS app

### 3a. Wipe target if non-empty

If the user confirmed wipe-and-rewrite:

```text
Bash: rm -rf <target>/* <target>/.[!.]*  # safely empty <target>/ while keeping the folder
```

### 3b. Scaffold via UI5 MCP

Call `create_ui5_app` directly into `<target>/` (NOT into a sub-folder):

```text
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` is intentionally **omitted** here — the V4 service is behind a proxy with
> credentials, so URL validation will fail. The data source is added manually in Phase 4.

### 3c. Verify the structure

```text
Bash: ls -la <target>/ && ls <target>/webapp/
```

Expected at root: `package.json`, `ui5.yaml`, `tsconfig.json`, `webapp/` with at minimum
`Component.ts`, `manifest.json`, `view/App.view.xml`, `controller/App.controller.ts`,
`index.html`, `i18n/i18n.properties`.

If `create_ui5_app` produces JS files instead of TS, fail loud — do NOT silently fall back to
manual scaffolding.

### 3d. Confirm dependencies match UI5 MCP guidelines

```text
Read: <target>/package.json
```

Cross-check against `get_typescript_conversion_guidelines` output. The expected dev-deps include
at minimum:

- `@ui5/cli`
- `typescript`
- `ui5-tooling-transpile`
- `ui5-middleware-livereload`
- `@sapui5/types` matching framework version

Add anything missing (e.g. add `ui5-middleware-simpleproxy` for OData proxying). Update versions
**only if** they're below the floor the guidelines specify; never downgrade.

Then run `npm install` again if the dep list changed.

Also confirm `"ts-typecheck": "tsc --noEmit"` exists in `scripts` (UI5 MCP guideline). Add if
missing.

### 3e. Warning — the scaffold itself uses Hungarian notation

The UI5 MCP scaffold templates (`Component.ts`, `BaseController.ts`, the default
`*.controller.ts`, `models.ts`) ship with Hungarian-prefixed parameter and local-variable
names (`sName`, `oModel`, `oParameters`, etc.). The Naming overrides section above applies to
the scaffold too — rename them as you read each scaffolded file, not just when porting from
the legacy controllers.

The fastest workflow: scaffold first, then do **one batch grep+rename pass** over the
scaffolded `webapp/` for the half-dozen common prefix

…(truncated)
