From 37edb5f7db525911e0784028e06681abf86ba800 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 15:48:59 +0000 Subject: [PATCH 1/3] Add ecosystem analysis and improvement roadmap Full-ecosystem review of the six cap2UI5 repos plus upstream abap2UI5: current state, findings by area, and a four-phase improvement plan (P0 correctness/pipeline integrity, P1 security hardening, P2 npm distribution + DX, P3 structural debt, P4 product polish) with acceptance criteria and success metrics. Placed at the repo root (outside docs/) so it is a maintainer planning document, not a published site page, and stays inert for verify-refs. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL --- ROADMAP.md | 307 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..89cd4e2 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,307 @@ +# cap2UI5 — Analysis & Improvement Roadmap + +*Status: 2026-08-21 · produced from a full-ecosystem review of all six cap2UI5 +repos plus upstream `abap2UI5/abap2UI5` (each repo read at its current HEAD, +test suites executed, numbers measured rather than quoted). This is a +maintainer planning document, not a site page — it is deliberately outside +`docs/` so it is not published or scanned by `verify-refs`.* + +--- + +## 1. Where the project stands + +**What is genuinely strong** (worth saying first, because the plan below +should not destroy it): + +- The six-repo pipeline (upstream ABAP → `builder-abap2UI5-js` transpile → + core package → `builder-cap2UI5` app assembly → app repo → + `builder-cap2UI5-web` → GitHub Pages) is fully automated, diff-reviewable + (committed trees), self-healing on races (slot-vs-HEAD arbitration), and + watched by independent freshness/health crons. +- The builder scripts are defensively engineered where it counts: + `builder-cap2UI5`'s assemble guardrails (residual-string sweep, lock-drift + guard), `publish-cap.js`'s target-identity gate, the web build's shell + sanity gate and deterministic `BUILD_INFO.json`. +- The app's tests are behavioural security regressions (owner isolation, + 401s, port contract), not coverage theatre. The docs repo's + `verify-refs` checker is exemplary tooling, and its ignore file is a model + of how to keep exceptions reviewable. +- Documentation prose quality is unusually high across all repos + (AGENTS.md files, recorded policies, honest READMEs). + +**The five findings that matter most**, across everything reviewed: + +| # | Finding | Where | Severity | +|---|---|---|---| +| 1 | The pipeline is **red right now**: upstream's new favicon contract (``) is not emitted by the hand-ported `z2ui5_cl_ui5_http_handler.js`; 1 of 221 jest tests fails, `build_core` cannot commit `core/`, and the whole downstream chain stalls until a human intervenes | builder-abap2UI5-js | **P0 — blocking** | +| 2 | **BTP deployment is likely broken**: `mta.yaml` defines an `abap2UI5-srv` destination, but `app/z2ui5/xs-app.json` has no route to it — its catch-all sends `/rest/root/z2ui5` roundtrips to the static HTML5 repo, and no CI runs `mbt build` or a deploy smoke test to catch it | cap2UI5 | **P0** | +| 3 | **Dual draft-store model**: the async platform store is wired to CDS, but the ABAP-shaped instance path (`create/read/count_entries_total/cleanup`) silently falls back to an unbounded process-global in-memory store with a column set that does not exist in `db/schema.cds` — the start page's draft count and `cleanup()` operate on phantom data | cap2UI5 / core | **P0** | +| 4 | `assemble-core.js` **deletes modules that fail to load and still exits 0** — a missing `adapters/cap/node_modules` (never checked) silently guts the shipped package; the transpile/publish steps have no size floors | builder-abap2UI5-js | **P0** | +| 5 | **There is no consumable artifact**: the core package is `"private": true`, named `abap2UI5` (uppercase — npm would reject it), never versioned, never published; users onboard by cloning a generated repo they are told not to edit; README and AGENTS.md give opposite advice about `srv/app/` | ecosystem | **P1 — strategic** | + +Everything else in this document hangs off these five. + +--- + +## 2. Findings by area (condensed) + +### 2.1 builder-abap2UI5-js (transpiler + core build) + +- **Ratchet health**: `upstream-units.known-failures.json` has **113 entries** + (22 % of 510 executed upstream tests). The "list only shrinks" property + holds mechanically but not socially — the 2026-08 rename wave hand-added + +103 in one commit. Categories: 74 `port-deviation`, 17 `js-limit`, + 13 `port-gap` (5 concrete missing APIs), **6 `port-bug`**, 3 `async-boundary`. +- **The 6 `port-bug` entries are real shipped framework defects hidden in a + green build** — most notably `_bind` does not walk nested structures + (stops at level 1, three entries) and resolves attributes to the first + structure member. Nothing escalates these. +- **Transpiler**: `scripts/abap2js.js` is a single 4,001-line file; + `emitStatement` alone is ~1,266 lines. 39 unit tests / ~40 % statement + coverage of `scripts/`; real confidence comes only from full re-transpiles. + Several constructs emit **silently wrong semantics** rather than TODOs: + range-table `IN` ignores sign `E` (exclude → opposite result), unsupported + comparisons emit `false /* TODO */`, unresolved superclasses become empty + class stubs. +- **Sharpest fragility**: `clientSignature()` (`abap2js.js:710-740`) parses + the hand-written client port with a whitespace-sensitive regex to derive + the ABAP↔JS calling convention for all 104 samples; a reformat silently + breaks apps at runtime. No completeness test exists. +- **SRTTI family (`00/02/z2ui5_cl_srt_*`) is the convergence point of three + debt signals**: 76 of 127 lint warnings (`no-undef`, `constructor-super`), + 34 of 82 shipped `TODO(abap2js)` markers, 0 % coverage — and it is already + backlog item #1 in `docs/HANDOFF.md`. +- **Pipeline scripts that decide what ships are untested** (`assemble-core`, + `publish-core`, `mirror-input`, `transpile-tree`, `prepare-app`, + `patch-frontend`, `check-no-frozen`). `patch-frontend.js` has four silent + no-op paths (exact-literal and regex matches over upstream files that warn + and exit 0, one printing "already patched" when it patched nothing). + `check-port-drift.js` auto-accepts drift in default mode and mutates a + tracked baseline even on a report run; it currently shows 3 unreconciled + drifts, one of which is finding #1. +- **Sanity floors are far below reality**: units floor `>150` vs actual 510; + smoke floor vs actual 104; `transpile-tree.js` and `publish-core.js` have + none at all. +- The weekly **oracle** (`oracle-classify.js`) can *prove* which baseline + entries are achievable in JS, but writes only to an expiring Actions step + summary nobody reads. +- Stale numbers everywhere prose quotes a count (roadmap says baseline "low + twenties" vs 113; "19 suites/~225 tests" vs 21/221; TODO counts off by an + order of magnitude; `transpiler-roadmap.md` still describes the + pre-monorepo-split layout). + +### 2.2 cap2UI5 app + builder-cap2UI5 + +- **Auth**: the roundtrip is *not* open — `@(requires: 'authenticated-user')` + is enforced and regression-tested. But the `$XSAPPNAME.User` scope declared + in `xs-security.json` is never referenced, so role collections do nothing. +- **CSRF is off by default**: `server.js` answers `X-CSRF-Token: disabled`; + the core's gate is opt-in and never enabled; the check itself allows + requests with neither Origin nor Referer (fails open). +- **Input validation**: the action body is `@open type object {}` — no schema, + no explicit size limit (the express default applies by accident). Security + headers are applied to the GET bootstrap only, not POST responses or the + CDS-served webapp statics. `z2ui5_cl_ui5f_index_html.js` interpolates + config values into HTML unescaped (latent XSS for any exit that reflects + request data). +- **Draft table**: append-only, one row per roundtrip, `LargeString` payload; + retention job exists (24 h TTL) but is unindexed (full scan on HANA), runs + on every instance concurrently, and disagrees with the framework exit's + own 4 h `draft_exp_time_in_hours` — the two are unconnected. +- **Multi-instance**: sticky handlers, the in-memory port store and exit + config are all process-global; no session affinity is configured; >1 CF + instance means intermittent silent state loss. Two tabs of one user share + a sticky slot and clobber each other; the tab-close beacon kills the other + tab's state. +- **Concurrency**: no locking/versioning; concurrent roundtrips fork the + draft chain silently; interleaved sticky requests mutate one handler's + request JSON in place. +- **Hygiene gaps**: no eslint config in the app repo at all (`src/app/**` + is ignored even at source); `dependabot.yml` covers only `github-actions` + — npm deps (including `openui5-dist@1.113.0`, `@sap/cds ^10`) are never + updated; no `npm audit`/CodeQL/coverage; `deploy-check` deliberately skips + `mbt build` (which is why finding #2 is invisible); no CONTRIBUTING/issue + templates in the app repo's only hand-owned folder. +- ~14k LOC of the webapp is duplicated byte-identical at `core/app/z2ui5/webapp` + and `app/z2ui5/webapp` — two copies that can drift. +- builder-cap2UI5 itself is the strongest-engineered repo (496 script lines, + 604 test lines, guardrails designed in). Residual risks: the ordered + string-rewrite pairs (second pattern is a substring of the first — order + is enforced only by an after-the-fact guardrail), no composed + assemble+publish test, and no check that the *published* tree installs + and boots. + +### 2.3 Web build + deployed site + +- Toolchain is solid (28 unit tests, real end-to-end smoke, post-deploy live + smoke, daily health + 48 h freshness watchdogs). Deploy is current; all + 104 samples ship; bundle is 522 KB min / 116 KB gzip. +- **Risks**: unpinned OpenUI5 CDN bootstrap (up to 24 h blank-page window + between a bad release and the health cron; no fallback, no SRI, resolved + version recorded nowhere); registry can silently collapse to ~6 built-ins + if the samples dir moves (smoke only exercises `hi_world`); `build.mjs`'s + HTML patching (329 lines) has zero unit tests; sample auto-exclusions are + only a `console.warn`; interceptor ignores `AbortSignal`; no cache-busting + on the stable `z2ui5-web.js` filename; `crypto.randomUUID()` throws on + plain-HTTP LAN previews. +- **Missed product opportunity**: the playground has no sample browser — the + only entry to 104 samples is hand-typing `?app_start=…`. An in-browser + editor (register a user-authored class via the already-in-tab engine) + would be a genuinely differentiated feature vs. upstream. + +### 2.4 Docs + +- 32 pages / ~24k words, zero orphans, local search, good API coverage + (59/65 client methods), working deployment guide. +- **Gaps**: no roadmap page; no sample catalogue (the single highest-value + missing page — trivially generatable from the registry walk); migration + from abap2UI5 is a ~180-word buried section, not a page; zero coverage of + testing app classes, i18n, user-exit as an extension point, addons, FAQ, + release notes; no sitemap config. +- **Stale claims invisible to `verify-refs`**: playground bundle "~1.2 MB" + (real: 522 KB), "weekly" rebuild cadence (now event-driven per push), + arithmetic around the 12 MB comparison. `verify-refs` checks identifiers, + not numbers or method names, and exits 0 without a checkout (CI supplies + one, but nothing enforces that). +- The app README still teaches `_bind_edit` in its examples while upstream + has migrated every caller off it and schedules removal (~mid-2027). + +### 2.5 Upstream tracking + +- Upstream `main` moves fast (50 commits in 3 weeks) while releases are now + quarterly-or-slower; "merging to main is the release" for abapGit readers. + The port's pin is already 4 commits behind, and those 4 commits are an API + break (interface retirements into `src/99`, which the port excludes + entirely — retired classes simply *vanish* here rather than + deprecate-then-remove). +- Upstream's `docs/removal-plan.md` is the single best divergence-tracking + input for the port: 8 frontend custom controls already `// OBSOLETE:` + (should not be carried forward), `_bind_edit` and friends scheduled out, + the `eF('…')` legacy action parser blocked on the same `IS SUPPLIED` + transpiler defect the port's backlog names. +- The port pins `openui5-dist@1.113.0` — *between* upstream's classic floor + (1.71, heavily gated) and its v2 track (1.136+). None of upstream's + version gates protect the port: it sits in an unguarded middle, and + dependabot never proposes a bump. +- Upstream has explicitly parked a named action API + (`frontend-action-named-api`, deferred) — **the port must not invent one + either**; that design space is reserved upstream. + +--- + +## 3. The roadmap + +Four phases, ordered so that each unblocks the next. Within a phase, items +are independent workstreams. "Done when" lines are the acceptance criteria. + +### Phase 0 — Stop the bleeding (now; days) + +Correctness and pipeline-integrity fixes. Nothing else matters while the +pipeline can silently ship a wrong or gutted package — or nothing at all. + +| # | Item | Repo | Done when | +|---|---|---|---| +| 0.1 | Port the favicon contract into `z2ui5_cl_ui5_http_handler.js` | builder-abap2UI5-js | `npm test` green (221/221 minus deliberate skips); `build_core` commits and `trigger_cap` fires again | +| 0.2 | Add the `/rest/**` (and `/health`) route to the CAP srv destination in `app/z2ui5/xs-app.json`; add an `mbt build` (or CF deploy smoke) job to `deploy-check` | builder-cap2UI5 `src/` | a deployed approuter roundtrip reaches the CAP service; CI fails if the route disappears | +| 0.3 | Resolve the dual draft model: wire `engine.set_db_store` to CDS *or* delete the ABAP-shaped instance path from the CAP build; align `db/schema.cds` with whichever survives | builder-abap2UI5-js + builder-cap2UI5 | draft count / cleanup operate on the real table; no code path writes to the process-global `InMemoryStore` in the CAP app | +| 0.4 | `assemble-core.js`: hard-fail when `adapters/cap/node_modules` is missing; exit non-zero when any module is skipped/deleted; record the skip list in the report | builder-abap2UI5-js | a gutted assemble can no longer exit 0 | +| 0.5 | Raise sanity floors to near-reality (units ≥ ~480 of 510, smoke ≥ ~100 of 104) and add floors to `transpile-tree.js` and `publish-core.js` | builder-abap2UI5-js | losing ⅔ of the test corpus or publishing an empty tree turns CI red | +| 0.6 | `check-port-drift.js`: stop mutating the baseline in plain report mode; make default (nightly) mode fail or file a visible record instead of auto-accepting drift | builder-abap2UI5-js | drift requires an explicit `--update`; a report run leaves the tree clean | +| 0.7 | Promote the 6 `port-bug` ratchet entries (esp. the three nested-`_bind` failures) into tracked GitHub issues; link the baseline entries to them | builder-abap2UI5-js | every `port-bug` entry carries an issue URL; ratchet policy documented: `port-bug` may not be baselined without one | + +### Phase 1 — Security hardening (next; 1–2 weeks) + +The README's honesty about its own gaps is a strength — now close them. + +| # | Item | Repo | Done when | +|---|---|---|---| +| 1.1 | Turn `check_csrf_active` on by default; make `_check_csrf_rejected` fail **closed** when both Origin and Referer are absent; stop sending `X-CSRF-Token: disabled` | builder-abap2UI5-js (core) + builder-cap2UI5 | CSRF gate unit-tested (the function was made pure for exactly this) and exercised in the app suite | +| 1.2 | Enforce the `$XSAPPNAME.User` scope on the z2ui5 action (xs-security.json already declares it); document the role-collection step | builder-cap2UI5 `src/` | 403 without the role; README security table updated | +| 1.3 | Explicit request body-size cap on the roundtrip; apply `t_security_header` to POST responses and webapp statics | builder-cap2UI5 `src/` | headers asserted in tests for GET and POST | +| 1.4 | HTML-escape `title` and `t_add_config` interpolation in `z2ui5_cl_ui5f_index_html.js`; add an escaping helper for exits | builder-abap2UI5-js | reflected-input test passes | +| 1.5 | Draft-table ops: index `createdAt` (+`owner`); single-instance or jittered retention; reconcile the 24 h job with the exit's 4 h `draft_exp_time_in_hours` (one config, one owner) | builder-cap2UI5 `src/` | retention delete uses the index; one documented TTL knob | +| 1.6 | Document (README security section) the multi-instance/sticky limitation honestly; declare `instances: 1` in `mta.yaml` until affinity or a shared store exists | builder-cap2UI5 `src/` | no silent state-loss surprise for BTP adopters | +| 1.7 | CI hygiene: npm ecosystem in `dependabot.yml` (app + builders), `npm audit` gate, eslint config for the app repo (stop ignoring `src/app/**` at source), CodeQL on the builders | all | each repo's PR gate covers lint + audit | + +### Phase 2 — Become adoptable: distribution & DX (1–2 months) + +The strategic phase. Today there is no supported way to *start* a cap2UI5 +project except cloning a build artifact. This is the ceiling on adoption. + +| # | Item | Repo | Done when | +|---|---|---|---| +| 2.1 | **Decide and execute the npm question**: rename the core `abap2UI5` → `abap2ui5` (or a scope, e.g. `@cap2ui5/core`), drop `private`, adopt real versioning (mirror upstream's `X.Y.Z` + a port counter), publish from `build_core` on green. The name is baked into the 38-entry exports map, `path-map.js`, every generated `require()`, and all four adapter manifests — the cost only grows | builder-abap2UI5-js | `npm i` works from the public registry; nightly publishes carry a version bump; README stops calling an unpublished folder "the published package" | +| 2.2 | Ship a **`cds-plugin`** entry so `npm i` + zero config contributes the service, bootstrap and draft wiring to any CAP project — eliminating the hand-copied `srv/server.js` / `z2ui5-service.*` skeleton | builder-cap2UI5 `src/` → package | a fresh `cds init` project + one install + `Z2UI5_APP_DIRS` renders an app; the generated app repo becomes the *demo*, not the delivery mechanism | +| 2.3 | Hand-written `.d.ts` for the top of the API: `engine`, `z2ui5_if_app`, `client`, `z2ui5_cl_ui5_view_builder` (a fluent chain is the API most improved by IntelliSense) | builder-abap2UI5-js `src/` | types ship in the package; a TS consumer gets completion on the view-builder chain | +| 2.4 | Template/scaffold: a GitHub template repo or `cds add cap2ui5`; fix the `srv/app/` contradiction (README says "put apps here", AGENTS.md says "overwritten every publish") with one recommended layout | builder-cap2UI5 + docs | onboarding is: create from template → `npm i` → write one class | +| 2.5 | Docs to match: standalone migration-from-abap2UI5 page (per-construct mapping table), generated **sample catalogue** page (id → title → playground deep link → source link), user-exit extension-point page, testing-your-app page, FAQ; fix the stale playground numbers; add sitemap | docs | catalogue generated from the same walk `gen-registry.mjs` does; `verify-refs` extended to method names + a small numeric-claims check | +| 2.6 | README positioning: state the value proposition ("UI5 apps from pure backend code — no frontend build, no OData annotations") against Fiori Elements / plain UI5 / CAP+annotations; move the 220-line samples section into docs | builder-cap2UI5 `src/` | a first-time visitor knows in 30 seconds why this exists | + +### Phase 3 — Pay down structural debt (2–4 months, interleavable) + +| # | Item | Repo | Done when | +|---|---|---|---| +| 3.1 | **SRTTI first**: rewrite/complete `00/02/z2ui5_cl_srt_*` — it is simultaneously 76 lint warnings, 34 shipped TODOs, 0 % coverage and HANDOFF backlog item #1; follow with the `CALL TRANSFORMATION`/sxml shim in `z2ui5_cl_ui5_util_context` (36 TODOs) | builder-abap2UI5-js | lint `no-undef`/`constructor-super` bucket ≈ 0; `TODO(abap2js)` in shipped `core/` < 20; upstream `port-gap` entries for these classes delisted | +| 3.2 | Split `abap2js.js` (4,001 lines; `emitStatement` ~1,266) into modules (statements / expressions / OpenSQL / emit); raise transpiler unit coverage on the way; replace `clientSignature()`'s source-regex with reflection over the required client class, plus a map-completeness test | builder-abap2UI5-js | no function > ~300 lines; a client-port reformat cannot silently break the calling convention | +| 3.3 | Make silently-wrong emissions loud: range-table sign `E`, `false /* TODO */` comparisons, empty-superclass stubs → either implement or fail the transpile with a clear message (allow-list consciously) | builder-abap2UI5-js | no construct emits a wrong *answer* silently | +| 3.4 | Test the ship-deciding scripts (`assemble-core`, `publish-core`, `transpile-tree`, `patch-frontend` — make its four silent no-ops hard failures, `mirror-input` excludes) against temp fixtures, the way builder-cap2UI5 already does | builder-abap2UI5-js | every script that writes what ships has negative tests | +| 3.5 | Ratchet governance: `apps-smoke` entries get the same `category`+`why` schema (and the two "correct behaviour" entries move to an expected-diffs list, not a failures list); commit the weekly oracle classification to a tracked file so the 74 `port-deviation` entries become *proven*, diffable, and burn-downable | builder-abap2UI5-js | baseline changes are reviewable diffs with reasons; oracle output survives log retention | +| 3.6 | Fix jest coverage semantics (child-process suites are invisible → 21 % is structurally wrong): instrument the out-of-process runners or document/replace the number; add thresholds once it means something | builder-abap2UI5-js | the coverage number measures what actually runs | +| 3.7 | Upstream-divergence tracking as CI: a small gate that diffs upstream's `docs/removal-plan.md` obsolete list against what the port ships (the 8 `// OBSOLETE:` custom controls, `_bind_edit` in port README examples, retired interfaces) and turns red on new divergence | builder-abap2UI5-js | divergence is a red check, not archaeology | +| 3.8 | De-duplicate the 14k-LOC webapp (single source of truth, or a build-time copy with a byte-identity check); decide `openui5-dist` pin strategy (1.113.0 sits in an unguarded middle between upstream's 1.71 floor and 1.136+ v2 track) and let dependabot propose bumps behind the jest gate | builder-abap2UI5-js + builder-cap2UI5 | one webapp source; a documented, tested UI5 pin policy | +| 3.9 | Doc-number drift killer: a tiny `check-doc-numbers` script asserting the handful of counts quoted in AGENTS.md/roadmap/eslint comments (`` markers); rewrite `transpiler-roadmap.md` onto the post-split layout | builder-abap2UI5-js, docs | stale numbers become CI failures, not folklore | + +### Phase 4 — Product polish (opportunistic) + +| # | Item | Repo | Done when | +|---|---|---|---| +| 4.1 | Playground **sample browser**: landing page listing the 104 samples (titles + deep links + GitHub source) — the cookbook the docs already claim exists | builder-cap2UI5-web | a visitor finds and opens any sample without typing a class name | +| 4.2 | Playground **editor**: an in-tab pane that registers a user-authored app class via the already-loaded engine — a feature upstream cannot match without a WASM ABAP runtime | builder-cap2UI5-web | write-a-class → run, entirely in the browser | +| 4.3 | Web-build resilience: record the resolved OpenUI5 version in `BUILD_INFO.json`, add a bootstrap `onerror` fallback to a known-good version, registry-count floor + exclusion list in the artifact, cache-busted bundle filename, unit-test the extracted HTML patcher, honor `AbortSignal` in the interceptor | builder-cap2UI5-web | a bad OpenUI5 release degrades gracefully instead of 24 h of blank page; a silent registry collapse is impossible | +| 4.4 | PWA/offline for the playground (service worker precaching bundle + webapp, runtime-caching the CDN) | builder-cap2UI5-web | returning visitors work offline | +| 4.5 | Publish the four adapters (node/express/web) once 2.1 lands, with the shared-store caveat documented; make the adapter test gate hard-fail in CI when `node_modules` is missing instead of skipping green | builder-abap2UI5-js | adapters installable; a missing CI install step cannot pass silently | +| 4.6 | Public roadmap page in the docs (this document, maintained), plus release notes per core version once 2.1 exists | docs | users can see where the project is going | + +--- + +## 4. Sequencing rationale + +- **Phase 0 before everything**: items 0.1–0.6 are the difference between + "the pipeline ships what we think it ships" and "green means nothing". + They are each hours-to-a-day of work. +- **Phase 1 before Phase 2**: publishing to npm (2.1) multiplies the blast + radius of every security default; harden first, then distribute. +- **Phase 2 is where the strategy lives.** Everything in it serves one + sentence: *a CAP developer can adopt cap2UI5 with `npm i` in five minutes + without ever seeing the build pipeline.* The 2.1 rename decision gates + 2.2/2.3/4.5 and gets more expensive every month of generated history. +- **Phase 3.1 (SRTTI) is the single highest-leverage code change** in the + ecosystem: one body of work retires four independent debt signals and + unlocks several baselined upstream tests. +- The ratchet + oracle + drift-gate items (0.6, 0.7, 3.5, 3.7) together + change the port's relationship to upstream from *reactive archaeology* to + *contract with alarms* — which is what a fast-moving upstream `main` + (50 commits/3 weeks) demands. + +## 5. Measures of success + +| Metric | Today | Target (post-P3) | +|---|---|---| +| Pipeline test state | 1 red (blocked publish) | green, with floors near real corpus sizes | +| Units ratchet baseline | 113 (6 known product bugs inside) | < 90, zero `port-bug` category entries | +| Shipped `TODO(abap2js)` in `core/` | 82 | < 20 | +| Lint warnings (builder) | 127 | < 30, gate on error for fixed classes | +| Consumable artifact | none (`private`, unpublishable name) | versioned npm package + cds-plugin | +| Onboarding path | clone a generated repo | template / `npm i` in any CAP project | +| BTP deploy | untested, likely broken routing | CI-gated `mbt build` + deploy smoke | +| CSRF | off by default, fails open | on by default, fails closed, tested | +| Docs numeric claims | 3 known stale | asserted by CI | + +--- + +*Sources: full reads of AGENTS.md and code in `builder-abap2UI5-js`, +`builder-cap2UI5`, `cap2UI5`, `builder-cap2UI5-web`, `web-cap2UI5-build`, +`docs`, and upstream `abap2UI5/abap2UI5` (incl. `backlog/` and +`docs/removal-plan.md`); executed: the builder jest suite (216/221 + 1 fail), +eslint (127 warnings), coverage, and `verify-refs` (clean). File/line +references for every finding live in the review notes behind this document.* From 8e289d6025635ce9540c6240cd28486f21e081f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 16:41:12 +0000 Subject: [PATCH 2/3] Add the four missing pages, correct the stale numbers, close the CI gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audit found the docs' gaps were not wrong prose but absent prose: no roadmap, no sample index, migration buried in a comparison page, and the one extension point the framework has explained nowhere. It also found three numeric claims that had quietly stopped being true, because verify-refs checks identifiers and says nothing about numbers. STALE CLAIMS (measured, not quoted) - The playground bundle is 535 KB minified / 118 KB gzipped, not "~1.2 MB". The whole site is 1.1 MB. The "roughly a tenth" comparison against abap2UI5-web's ~12 MB is true of the site, not of the bundle (that one is a twentieth) — so the sentence now says which is which. - The site is not rebuilt "weekly and on demand": builder-cap2UI5-web builds on every push to main, and the sync pipeline pushes a trigger commit there after each framework change. The weekly cron is the safety net, not the schedule. - api/view-builder said "all 106 bundled samples"; there are 104 files. The number is now a link to the catalogue, which counts them itself. NEW PAGES - guide/samples.md — GENERATED by scripts/gen-samples.mjs from the sample gallery's own catalogue table (z2ui5_cl_smp_app_000.get_catalog), so it cannot disagree with what the playground offers: 97 catalogued samples in 23 categories, each linking into the playground and to its source, plus the 6 sub-apps that ship without a gallery entry. A hand-written index would have been a second copy of a list that changes on every sync. `npm run gen:samples` rewrites it; `--check` fails when it is out of date. The `group` field the task named turns out to be uniformly `samples` — the port carries only one of upstream's three sample repos — so the useful axis is `header`, and both are honoured (groups outside, categories inside). - guide/migration-from-abap2ui5.md — the ~180-word section extracted from vs-abap2ui5 and grown into a per-construct mapping: app class, views, binding, data structures, OpenSQL to CDS queries, cl_http_client to fetch, and a warning block on _bind/_bind_edit that names the trap in both directions (an app written against 1.143.0 relying on _bind writing back silently will not here). vs-abap2ui5 keeps a one-paragraph summary and links on. - guide/roadmap.md — written for users, not maintainers: what is limited today (no npm install path, single instance, the two pins, the frozen classes that are absent on purpose) and what is intended, with no dates. - guide/user-exit.md — the extension point two reference pages mentioned and none explained: discovery, the per-request context, every field of set_config_http_get and set_config_http_post, and the two properties of the CSRF gate worth knowing before relying on it (on by default since the 2026-08 pass, and lenient when there is nothing to compare). CORRECTIONS FOUND ON THE WAY `class my_exit extends z2ui5_if_exit` — the example in reference/configuration — throws at load time. z2ui5_if_exit is a frozen object of method names, not a base class; the framework matches an exit by duck typing. Both pages now show the form that runs, and say so. The same page's env table and CSRF default were also behind the code: retention now follows the framework's own draft expiry instead of contradicting it, and check_csrf_active ships true. CI verify-refs gains --require-checkout: without a checkout it still skips and exits 0 on a laptop, but check.yml now runs `npm run check:ci`, where a missing checkout is a failure. The workflow already checks cap2UI5 out — nothing made that step load-bearing, so removing it would have left the job green and checking nothing. Also: a sitemap in .vitepress/config.mjs (36 URLs at the deployed hostname), the new pages in the sidebar, and AGENTS.md documenting the generated page and the flag. `npm run check` green against a cap2UI5 checkout. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL --- .github/workflows/check.yml | 12 +- AGENTS.md | 20 ++- docs/.vitepress/config.mjs | 17 +- docs/api/view-builder.md | 3 +- docs/guide/migration-from-abap2ui5.md | 175 +++++++++++++++++++ docs/guide/playground.md | 14 +- docs/guide/roadmap.md | 155 +++++++++++++++++ docs/guide/samples.md | 242 ++++++++++++++++++++++++++ docs/guide/user-exit.md | 222 +++++++++++++++++++++++ docs/guide/vs-abap2ui5.md | 12 +- docs/index.md | 2 + docs/reference/configuration.md | 45 +++-- package.json | 4 +- scripts/gen-samples.mjs | 222 +++++++++++++++++++++++ scripts/verify-refs.mjs | 13 +- 15 files changed, 1122 insertions(+), 36 deletions(-) create mode 100644 docs/guide/migration-from-abap2ui5.md create mode 100644 docs/guide/roadmap.md create mode 100644 docs/guide/samples.md create mode 100644 docs/guide/user-exit.md create mode 100644 scripts/gen-samples.mjs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a280ede..09344fc 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -33,7 +33,9 @@ jobs: # and `require("abap2UI5/…")` on this site against a real checkout, so # without it the run proves only that the site builds — it skips itself # and exits 0 when the checkout is missing, which is exactly the silent - # pass this job exists to prevent. + # pass this job exists to prevent. Hence `check:ci` below, which passes + # --require-checkout: if this step is ever removed, renamed or fails to + # land the tree, the job goes red instead of quietly checking nothing. - name: Check out cap2UI5 (reference target) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -49,8 +51,10 @@ jobs: - run: npm ci # verify-refs + vitepress build, in that order: a dead reference is - # reported even when the site would have built fine around it. - - name: npm run check - run: npm run check + # reported even when the site would have built fine around it. Same two + # steps as the `npm run check` AGENTS.md tells a contributor to run — + # the :ci variant only refuses to skip the reference check. + - name: npm run check:ci + run: npm run check:ci env: CAP2UI5_DIR: .cap2ui5-ref diff --git a/AGENTS.md b/AGENTS.md index 8e14c6c..e87c12e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,25 @@ VitePress build. It is also what CI runs, on every pull request The verifier needs a checkout: `CAP2UI5_DIR=/path/to/cap2UI5`, or a sibling clone. It skips itself when there is none, so a green run without a checkout -proves only that the site builds. +proves only that the site builds. That leniency is right on a laptop and wrong +in CI, which does check cap2UI5 out — so CI runs `npm run check:ci`, the same +two steps with `verify-refs --require-checkout`, and a missing checkout is a +failure there rather than a silent pass. + +## Generated pages + +`docs/guide/samples.md` is **generated** — do not edit it by hand. It comes +from `scripts/gen-samples.mjs`, which reads the sample gallery's own catalogue +table out of `z2ui5_cl_smp_app_000` in a cap2UI5 checkout: + +``` +CAP2UI5_DIR=/path/to/cap2UI5 npm run gen:samples # rewrite the page +node scripts/gen-samples.mjs --check # fail if it is out of date +``` + +The result is committed like any other page, so the docs build without a +checkout and a reviewer sees the diff a sync causes. Regenerate it when the +sample set changes. Exceptions — placeholder class names, paths in other repos — go in `docs/.verify-refs-ignore`, **with a reason**. An unexplained entry there is diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index f252efe..3f01cbd 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -15,6 +15,13 @@ export default defineConfig({ cleanUrls: true, lastUpdated: true, + // Absolute URLs in sitemap.xml, same host as SITE_URL above — a search + // engine reads the file from the deployed origin, so a relative base is + // not enough to name a page. + sitemap: { + hostname: `${SITE_URL}/` + }, + ignoreDeadLinks: [ /^https?:\/\/localhost/, /\/LICENSE$/ @@ -76,7 +83,8 @@ export default defineConfig({ { text: 'Why cap2UI5?', link: '/guide/why-cap2ui5' }, { text: 'Try It in the Browser', link: '/guide/playground' }, { text: 'Quickstart', link: '/guide/getting-started' }, - { text: 'Project Structure', link: '/guide/project-structure' } + { text: 'Project Structure', link: '/guide/project-structure' }, + { text: 'Sample Catalogue', link: '/guide/samples' } ] }, { @@ -95,6 +103,7 @@ export default defineConfig({ text: 'Working With It', items: [ { text: 'Developer Tools', link: '/guide/devtools' }, + { text: 'The User Exit', link: '/guide/user-exit' }, { text: 'Troubleshooting', link: '/guide/troubleshooting' } ] }, @@ -102,14 +111,16 @@ export default defineConfig({ text: 'Comparison', items: [ { text: 'cap2UI5 vs. Fiori Elements', link: '/guide/vs-fiori-elements' }, - { text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' } + { text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' }, + { text: 'Migrating from abap2UI5', link: '/guide/migration-from-abap2ui5' } ] }, { text: 'Background', items: [ { text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' }, - { text: 'The Ecosystem', link: '/guide/ecosystem' } + { text: 'The Ecosystem', link: '/guide/ecosystem' }, + { text: 'Roadmap', link: '/guide/roadmap' } ] } ], diff --git a/docs/api/view-builder.md b/docs/api/view-builder.md index ad33ffb..70fa2cf 100644 --- a/docs/api/view-builder.md +++ b/docs/api/view-builder.md @@ -3,7 +3,8 @@ Views are built in JavaScript and rendered to UI5 XML. There is one builder: `z2ui5_cl_ui5_view_builder`. It is generic — it knows XML elements and attributes, not the UI5 control catalogue — which is why the transpiler -targets it and why all 106 bundled samples are written against it. +targets it and why every one of the +[bundled samples](../guide/samples) is written against it. Source: [`core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js). diff --git a/docs/guide/migration-from-abap2ui5.md b/docs/guide/migration-from-abap2ui5.md new file mode 100644 index 0000000..8f4cea5 --- /dev/null +++ b/docs/guide/migration-from-abap2ui5.md @@ -0,0 +1,175 @@ +# Migrating from abap2UI5 + +An abap2UI5 app and a cap2UI5 app are the same app in two languages. The wire +protocol is identical, the frontend is byte-identical, and the API is the same +method for method — so migration is mechanical rather than a redesign. What +changes is the language, the data access, and the outbound calls. + +This page is the per-construct mapping. For the wider comparison — why a +second implementation exists, which one to pick — see +[cap2UI5 vs. abap2UI5](./vs-abap2ui5). + +## The five steps + +1. **Rewrite the ABAP class as a JS class**, method for method. Or let the + transpiler do a first pass: in a + [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) + checkout, `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout` + emits JavaScript and marks what it could not translate as + `// TODO(abap2js)` comments instead of dropping it. +2. **Rebuild the view** if it was built on the retired `z2ui5_cl_xml_view` — + the one builder here is `z2ui5_cl_ui5_view_builder`. +3. **Convert data access** from OpenSQL to CDS queries. +4. **Convert outbound calls** from `cl_http_client` to `fetch` or + `cds.connect.to`. +5. **Drop the file into** [`srv/app/`](./project-structure#srv-app) and run it. + The same static frontend renders it without changes. + +## Construct by construct + +### The app class + +| abap2UI5 | cap2UI5 | +|---|---| +| `CLASS … DEFINITION PUBLIC` / `INTERFACES z2ui5_if_app` | `class … extends z2ui5_if_app` | +| `METHOD z2ui5_if_app~main` | `async main(client)` | +| `DATA name TYPE string` (class attribute) | `name = ""` (class field) | +| `me->name` | `this.name` | +| the class name is the ADT/CCDIR object name | the class name **must** match the file name — the RTTI lookup is name-based | + +App state is the instance's own fields in both worlds: whatever you assign to +`this` survives the roundtrip, because the framework serialises the instance +into the draft. See [App Lifecycle](./lifecycle). + +### Views + +| abap2UI5 | cap2UI5 | +|---|---| +| `z2ui5_cl_ui5_view_builder=>factory( )` | `z2ui5_cl_ui5_view_builder.factory()` | +| `z2ui5_cl_xml_view` (frozen upstream) | **not carried** — rebuild on `z2ui5_cl_ui5_view_builder` | +| `z2ui5_cl_xml_view_cc` (custom-control decorator) | **not carried** — the `z2ui5.cc` namespace is declared on the view like any other | +| the built-in popups `z2ui5_cl_pop_*` | **not carried** — see [Popups & Toasts](./popups) | +| `view->ele( n = 'View' ns = 'mvc' )` — named arguments | `view.ele({ n: "View", ns: "mvc" })` — one object | +| `)->a( n = 'title' v = 'Hello' )` chained with `->` | `.a({ n: "title", v: "Hello" })` chained with `.` | +| `view->stringify( )` | `view.stringify()` | + +Named arguments are the one place every line changes shape. ABAP names each +argument (`n = … v = …`); JavaScript passes a single object literal with the +same keys. Positional shorthand survives where ABAP had it: `view->ele( 'Page' )` +becomes `view.ele("Page")`. The methods themselves — +[`ele`, `tag`, `a`, `end`, `stringify`](../api/view-builder#methods) — are +unchanged. + +The absent classes are absent on purpose, not by omission: cap2UI5 does not +carry abap2UI5's frozen `src/99` package at all +([why](./vs-abap2ui5#commonalities)). An old sample that still compiles +upstream can therefore fail here on the import alone. + +### Binding + +| abap2UI5 | cap2UI5 | +|---|---| +| `client->_bind( name )` | `client._bind(this.name)` | +| `client->_bind_edit( name )` | `client._bind_edit(this.name)` | +| `client->_event( 'BUTTON_POST' )` | `client._event("BUTTON_POST")` | + +::: warning `_bind` vs `_bind_edit` — check which release your source targets +cap2UI5 pins abap2UI5 **1.142.0** (`static version` on `z2ui5_if_app`, in +`core/srv/z2ui5/02/z2ui5_if_app.js`). On that release the two calls are two +different bindings: + +- `_bind()` is **one-way** — the value is rendered, and changes in the browser + are not read back. +- `_bind_edit()` is **two-way** — the value is rendered *and* the frontend + writes it back into the attribute on the next roundtrip. An `Input` whose + value you intend to read needs this one. + +Upstream merged the two in **1.143.0**: from there on `_bind_edit()` is an +alias of `_bind()`, and everything is two-way. So an app written against +1.143.0 or later that relies on `_bind()` writing back **will silently not +write back here** — the field simply stays at its old value. Migrating such an +app means changing those calls to `_bind_edit()`. + +Coming the other way, from an older app: `_bind_edit()` keeps working on both +releases, which makes it the safe choice while the two releases coexist. +::: + +In both languages the binding call receives the attribute and the framework +works out *which* attribute that was. In JavaScript it does so by value +identity, so pass `this.name` itself — a copy, a literal or an expression can +resolve to a different attribute holding the same value. See +[Data Binding](./data-binding#reference-equality) for what to do when two +fields legitimately share a value. + +### Data structures + +| abap2UI5 | cap2UI5 | +|---|---| +| `TYPES: BEGIN OF ty_s_row … END OF ty_s_row` | no declaration — a plain object `{ … }` | +| `DATA s_row TYPE ty_s_row` | `s_row = { carrid: "", connid: "" }` | +| `DATA t_tab TYPE STANDARD TABLE OF ty_s_row` | `t_tab = []` | +| `APPEND s_row TO t_tab` | `t_tab.push(s_row)` | +| `READ TABLE t_tab INTO s_row INDEX 1` | `s_row = t_tab[0]` | +| `LOOP AT t_tab INTO s_row` | `for (const s_row of t_tab)` | +| `CLEAR` / `IS INITIAL` | `= ""` / `= []` / a falsy check | +| field names are case-insensitive, upper-cased in the model | **object keys are case-sensitive** — keep them lower-case and consistent | + +The last row is the one that bites. ABAP does not care whether you wrote +`CARRID` or `carrid`; JavaScript does, and a binding path that disagrees with +the object key by one character renders empty rather than failing. + +### Data access + +| abap2UI5 | cap2UI5 | +|---|---| +| `SELECT * FROM scarr INTO TABLE @DATA(t)` | `const t = await SELECT.from("my.Carriers")` | +| `SELECT SINGLE … WHERE id = @lv_id` | `await SELECT.one.from("my.Carriers").where({ id })` | +| `SELECT … UP TO 100 ROWS` | `.limit(100)` | +| `INSERT`/`UPDATE`/`DELETE` | `INSERT.into(…)`, `UPDATE(…)`, `DELETE.from(…)` | +| `sy-subrc` after the statement | an empty array / `undefined`, or a thrown error | +| implicit client handling, authorisation checks | CAP's `@requires` / `@restrict` on the service | + +CDS queries are **asynchronous**. That is the one structural change migration +forces: a method that reads data becomes `async`, and every caller up to +`main` has to `await` it. `main` is already `async` in cap2UI5, which is why +the chain terminates cleanly. + +### Outbound calls + +| abap2UI5 | cap2UI5 | +|---|---| +| `cl_http_client=>create_by_url( … )` | `await fetch(url, { … })` | +| `client->send( )` / `client->receive( )` | the awaited `fetch` promise | +| `lo_response->get_cdata( )` | `await res.text()` | +| `/ui2/cl_json=>deserialize( )` | `await res.json()` | +| RFC / service consumer / destination | `await cds.connect.to("service")` | +| SM59 destination | a CAP destination or a plain URL | + +See [External OData Call](../examples/external-odata) for a worked example. + +### Everything else + +| abap2UI5 | cap2UI5 | +|---|---| +| `sy-uname` | the identity provider — `engine.set_identity(…)`, see [Configuration](../reference/configuration#identity) | +| `sy-datum` / `sy-uzeit` | `new Date()` | +| `MESSAGE`/`cx_root` exceptions | `throw new Error(…)` / `try…catch` | +| `z2ui5_cl_ui5_user_exit` (config exit) | the same class, same two hooks — see [The User Exit](./user-exit) | +| DB table `Z2UI5_T_01` | CDS entity `cap2ui5.z2ui5_t_01`, see [Database Model](../reference/database) | +| transport / abapGit pull | `npm install`, `cds build`, `cf deploy` | + +## What does not need migrating + +- **The frontend.** cap2UI5 serves the same `app/z2ui5/webapp/` UI5 bundle + abap2UI5 does, custom controls included. Nothing you built on the frontend + side changes. +- **The wire protocol.** `POST /rest/root/z2ui5` with the same + `{ S_FRONT, XX, MODEL }` payload. A frontend cannot tell the two backends + apart, which is why the same app can be deployed in both worlds during a + transition. +- **The mental model.** Roundtrips, the draft chain, `check_on_init` / + `check_on_event` / `check_on_navigated`, `nav_app_call` — all identical. + +→ Next: the [**Sample Catalogue**](./samples) — the whole abap2UI5 demo +collection, already transpiled, is the largest set of before/after pairs +available. diff --git a/docs/guide/playground.md b/docs/guide/playground.md index d01547b..32c03c4 100644 --- a/docs/guide/playground.md +++ b/docs/guide/playground.md @@ -15,11 +15,11 @@ https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_ui5_app_hi_world https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_000 ``` -The `z2ui5_cl_smp_app_*` samples are the transpiled abap2UI5 demo collection — a browsable cookbook of tables, forms, popups, charts, and more. +The `z2ui5_cl_smp_app_*` samples are the transpiled abap2UI5 demo collection — a browsable cookbook of tables, forms, popups, charts, and more. The [**Sample Catalogue**](./samples) lists every one of them with a link straight into the playground. ## Wait — didn't you say the backend renders the view? -Yes, and that's exactly what makes this playground fun: cap2UI5's backend is **plain JavaScript with no hard CAP dependency in the hot path**. So the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) build bundles the entire backend — framework core plus all sample apps — into a single ~1.2 MB JS file and loads it **into the browser tab**: +Yes, and that's exactly what makes this playground fun: cap2UI5's backend is **plain JavaScript with no hard CAP dependency in the hot path**. So the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) build bundles the entire backend — framework core plus all sample apps — into a single ~520 KB JS file (~115 KB gzipped, which is what a visitor actually downloads) and loads it **into the browser tab**: ``` Browser tab @@ -37,21 +37,23 @@ Browser tab The frontend doesn't know the difference — it POSTs to the same endpoint and gets the same responses. Every roundtrip that would normally hit your CAP server is answered inside the tab. -For comparison: abap2UI5 has the same kind of twin ([abap2UI5-web](https://github.com/abap2UI5/abap2UI5-web)), but it has to ship an ABAP runtime and a WASM database to the browser (~12 MB). Because cap2UI5's backend is already JavaScript, the bundle here is roughly a tenth of that. +For comparison: abap2UI5 has the same kind of twin ([abap2UI5-web](https://github.com/abap2UI5/abap2UI5-web)), but it has to ship an ABAP runtime and a WASM database to the browser (~12 MB). Because cap2UI5's backend is already JavaScript, the whole site here — bundle, webapp, everything — is about 1.1 MB, roughly a tenth of that; the backend bundle alone is a twentieth. ## What it's good for - **Kicking the tires** before you install anything. -- **Browsing the sample apps** as a live catalog next to their source in [`core/srv/app/samples/`](https://github.com/cap2UI5/cap2UI5/tree/main/core/srv/app/samples). +- **Browsing the sample apps** as a live catalog next to their source in [`core/srv/app/samples/`](https://github.com/cap2UI5/cap2UI5/tree/main/core/srv/app/samples) — the [Sample Catalogue](./samples) is the index. - **Sharing a demo link** with colleagues. -## What it's *not* +## What it's *not* {#what-its-not} - **Not a production topology.** cap2UI5's security model — UI logic and state stay on the server — obviously doesn't apply when the "server" is shipped to the client. It's a demo artifact. - **Sessions live in the tab.** Reload = fresh state. On a real CAP server, drafts persist in the database. - **Server-only features are off.** Samples that call external OData services (e.g. Northwind) or need real CAP services/destinations won't work here. - **Internet still required** — UI5 itself loads from the SAP CDN. -The site is rebuilt automatically from the latest cap2UI5 sources (weekly and on demand) by the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) repository and published to [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build). +The site is rebuilt automatically from the latest cap2UI5 sources by the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) repository and published to [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build). The rebuild is **event-driven**: every push to builder-cap2UI5-web's `main` runs it, and cap2UI5's sync pipeline pushes a trigger commit there after each framework change — so the playground follows the framework rather than a calendar. A weekly cron remains as a safety net, and a manual run is always possible. + +Every deployment is one commit in [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build), and its `BUILD_INFO.json` names the exact cap2UI5 commit the site was built from — so you can always tell what you are looking at. → Ready for the real thing? Head to the [**Quickstart**](./getting-started) — you'll have the same apps running on a local CAP server in five minutes. diff --git a/docs/guide/roadmap.md b/docs/guide/roadmap.md new file mode 100644 index 0000000..17fde6b --- /dev/null +++ b/docs/guide/roadmap.md @@ -0,0 +1,155 @@ +# Roadmap + +Where cap2UI5 is going, and — more usefully — what is limited today. No dates: +this is a small project with an automated pipeline, and work lands when it +lands. What follows is the order of intent, not a schedule. + +::: tip How to read this +Anything under **Known limits today** is true of the current build. Anything +under **What's next** is intended, not promised. If a limit matters to your +project, it is worth checking the +[cap2UI5 issues](https://github.com/cap2UI5/cap2UI5/issues) before you build +around it. +::: + +## Known limits today + +### Getting a project started + +There is no supported way to *start* a cap2UI5 project other than cloning the +generated [cap2UI5](https://github.com/cap2UI5/cap2UI5) repository and writing +your apps into [`srv/app/`](./project-structure#srv-app). The framework is +vendored inside that repository at `core/`; it is not on the public npm +registry, so `npm install` cannot pull it into an existing CAP project. This is +the single biggest gap and the one most of the work below serves. + +### One process, one session + +State that survives a roundtrip lives in the draft table, but the *sticky* +handler that answers consecutive requests from one browser tab is held in the +process that answered the first one. There is no session affinity and no shared +handler store, so **running more than one instance can lose state +intermittently** — which is why the Cloud Foundry descriptor declares a single +instance. Horizontal scaling waits on either affinity or a shared sticky store. +Two tabs of the same user share one sticky slot, too, so a second tab can +disturb the first. + +Concurrency inside one session is not guarded either: two roundtrips answered at +the same time fork the draft chain silently rather than failing. + +### Pinned versions + +- **abap2UI5 1.142.0.** The port pins one upstream release; `static version` on + `z2ui5_if_app` says which. Upstream material written against a later release + can disagree with what runs here — most visibly `_bind` and `_bind_edit`, + which are still two different bindings on 1.142.0. See + [Migrating from abap2UI5](./migration-from-abap2ui5#binding). +- **OpenUI5 1.113.0**, served locally. Newer UI5 controls and properties are + not available. The pin is deliberately excluded from automated dependency + updates: moving it is a compatibility decision about which UI5 releases the + framework supports, not a routine bump. + +### Not everything upstream ships is here + +cap2UI5 deliberately does not carry abap2UI5's frozen legacy package, so +`z2ui5_cl_xml_view`, its custom-control decorator and the built-in `z2ui5_cl_pop_*` +popups do not exist here — an old upstream sample can fail on the import alone. +That is a decision, not a backlog item; the detail is on +[cap2UI5 vs. abap2UI5](./vs-abap2ui5). + +Beyond that, a number of transpiled framework internals still carry +`TODO(abap2js)` markers where an ABAP construct has no finished JavaScript +equivalent — mostly in runtime type introspection. The common paths are +exercised by the whole sample suite on every sync, but the corners are real. + +The one limit you are most likely to meet is not a TODO but a language +difference: because JavaScript has no reference to a field, `_bind` identifies +*which* attribute you meant by the value you passed, so two attributes holding +the same value are ambiguous. [Data Binding](./data-binding#reference-equality) +explains the workaround — pass the path explicitly. + +### BTP deployment + +The Cloud Foundry deployment descriptor ships and is documented +([Deployment](../reference/deployment)), but it is exercised by far less +automation than the local path. Treat a first BTP rollout as something to +verify end to end rather than assume. + +## What's next + +### Distribution — making `npm install` work + +The intent is that a CAP developer can adopt cap2UI5 without ever seeing the +build pipeline: install a published package into an existing project, add a +plugin entry, write one class. That means publishing the core to the public +registry under a real version, and shipping a CAP plugin that contributes the +service, the bootstrap and the draft wiring on its own — replacing today's +hand-copied `srv/server.js` and `srv/z2ui5-service.cds` skeleton. A template or +scaffold would then be the recommended way to start, and the generated +repository becomes the demo rather than the delivery mechanism. + +### Security defaults + +The principle is that the defaults should be safe when nobody configures +anything, and the work is closing what is left between that and what ships. +The 2026-08 pass moved several settings onto that footing: the CSRF +gate is on by default (see [The User Exit](./user-exit#csrf)), security headers +are applied to the roundtrip and OData responses rather than only to the +bootstrap page, the roundtrip has an explicit body-size cap, and the +authorisation scope the app declares is enforced on the endpoint — so a +deployment now needs the role assigned, not merely defined. + +What is *not* solved is the multi-instance state problem above: an honest +single-instance deployment is the current answer, not a shared store. The +[Deployment](../reference/deployment) page carries the operational detail as it +changes. + +### Types + +Hand-written type definitions for the top of the API — `client`, +`z2ui5_if_app`, `z2ui5_cl_ui5_view_builder` — so that editors complete the +builder chain instead of guessing. A fluent chain is the API most improved by +completion, and it is the one you write most. + +### Playground + +The [browser playground](./playground) runs the whole stack in a tab. Two things +would make it a genuinely better front door: a landing page that lists the +samples instead of requiring a hand-typed class name (the +[Sample Catalogue](./samples) is the documentation half of that), and an in-tab +editor that registers a class you wrote yourself — write a class, run it, +without installing anything. The engine already supports registering a class +directly, so this is a tooling question rather than a framework one. + +### Keeping up with abap2UI5 + +The port tracks a fast-moving upstream through an automated transpile pipeline. +The work here is less about features than about alarms: making divergence from +upstream a visible check rather than something discovered by a user, and +carrying upstream's own deprecations through instead of having classes quietly +appear and vanish between releases. + +## What is deliberately not planned + +- **A named frontend action API.** Upstream has parked that design space; the + port will not invent a competing one. +- **Carrying the frozen legacy classes.** See above — an npm package has no + installed base to protect, which is exactly why they were dropped. +- **Divergence from the wire protocol.** The frontend is shared with abap2UI5 + byte for byte, and staying protocol-compatible is what makes the same app + deployable in both worlds. + +## Where the real backlog lives + +This page is a summary written for people *using* cap2UI5. The working plans +live with the code, in each repository's issues and `AGENTS.md`: + +| | | +|---|---| +| the app, and the framework as delivered | [cap2UI5/cap2UI5](https://github.com/cap2UI5/cap2UI5) | +| transpiler and core build | [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) | +| app assembly and publishing | [builder-cap2UI5](https://github.com/cap2UI5/builder-cap2UI5) | +| the browser playground | [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) | +| this site | [cap2UI5/docs](https://github.com/cap2UI5/docs) | + +[The Ecosystem](./ecosystem) explains how those fit together. diff --git a/docs/guide/samples.md b/docs/guide/samples.md new file mode 100644 index 0000000..2dfdc59 --- /dev/null +++ b/docs/guide/samples.md @@ -0,0 +1,242 @@ + + + +# Sample Catalogue + +The framework ships 104 sample apps in `core/srv/app/samples/` — the abap2UI5 demo collection, transpiled to JavaScript. 97 of them are listed in the built-in gallery, and this page is that gallery's own table: it is generated from `z2ui5_cl_smp_app_000`, the sample browser the framework itself renders, so it cannot drift from what the playground offers. + +Every title links into the [browser playground](./playground) — no install, no server. The same apps run on your own CAP server under the same names: `?app_start=`, or `client.nav_app_call()` from an app of your own. + +→ Prefer to browse in the app itself? Open [`z2ui5_cl_smp_app_000`](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_000) — the same catalogue with a search field. + +Two markers are carried over from the upstream gallery: **(A)** marks a sample that drives the frontend through a client action, **(C)** one that uses a custom control from the `z2ui5.cc` namespace. + +## Categories + +[Basics](#basics) · [Binding](#binding) · [Browser](#browser) · [Control Behaviour](#control-behaviour) · [CSS](#css) · [Device](#device) · [Event](#event) · [File](#file) · [Focus](#focus) · [Formatter](#formatter) · [Grid Table](#grid-table) · [List](#list) · [Menu](#menu) · [Message](#message) · [Navigation](#navigation) · [Nested View](#nested-view) · [Popover](#popover) · [Popup](#popup) · [Scroll](#scroll) · [Table](#table) · [Templating](#templating) · [Timer](#timer) · [Tree](#tree) + +## Basics + +| Sample | Class | Topics | +|---|---|---| +| [Hello World, the Smallest App](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_493) | [`z2ui5_cl_smp_app_493`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_493.js) | hello world smallest first app minimal start here template | +| [Data Binding: Input and Button](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_494) | [`z2ui5_cl_smp_app_494`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_494.js) | binding _bind model attribute value input button serialize | +| [Lifecycle: Init, Event, Navigated](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_495) | [`z2ui5_cl_smp_app_495`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_495.js) | lifecycle roundtrip main dispatcher state serialize check_on_init check_on_event check_on_navigated | +| [Events, Views and Roundtrips](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_004) | [`z2ui5_cl_smp_app_004`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_004.js) | roundtrip restart second view uncaught error controller basics | +| [The Developer Tools (Ctrl+F12)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_496) | [`z2ui5_cl_smp_app_496`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_496.js) | developer tools devtools ctrl f12 debug inspect payload previous request response view xml view model source code log error adt export | + +## Binding + +| Sample | Class | Topics | +|---|---|---| +| [Currency Amounts (sap.ui.model.type.Currency)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_067) | [`z2ui5_cl_smp_app_067`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_067.js) | amount decimals leading zeros number format | +| [Dynamic Table Typed at Runtime (RTTI)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_061) | [`z2ui5_cl_smp_app_061`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_061.js) | generic data reference create data ddic dynamic itab | +| [Expression Binding, Types and Composite Parts](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_027) | [`z2ui5_cl_smp_app_027`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_027.js) | formatter parts conditional regexp visible enabled syntax | +| [Model setSizeLimit for Large Tables (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_071) | [`z2ui5_cl_smp_app_071`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_071.js) | combobox jsonmodel size limit large itab 100 entries | +| [Single Table Cell (tab_index)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_144) | [`z2ui5_cl_smp_app_144`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_144.js) | cell input internal table row field level | +| [Structure Fields and INCLUDEs](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_166) | [`z2ui5_cl_smp_app_166`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_166.js) | structure component include flat form level | +| [Types for Integer, Decimal, Date and Time](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_047) | [`z2ui5_cl_smp_app_047`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_047.js) | type conversion sum amount number field | + +## Browser + +| Sample | Class | Topics | +|---|---|---| +| [Copy to Clipboard (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_325) | [`z2ui5_cl_smp_app_325`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_325.js) | clipboard paste copy text area | +| [Local and Session Storage (A,C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_327) | [`z2ui5_cl_smp_app_327`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_327.js) | localstorage sessionstorage persist store_data offline | +| [Logout from the Client (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_361) | [`z2ui5_cl_smp_app_361`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_361.js) | logoff signout icf session end fiori launchpad | +| [Open a URL in a New Tab (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_073) | [`z2ui5_cl_smp_app_073`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_073.js) | url window open_new_tab link target | +| [Open Mail, Phone and SMS Links (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_316) | [`z2ui5_cl_smp_app_316`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_316.js) | mailto tel sms urlhelper redirect native link | +| [Reload the Page (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_492) | [`z2ui5_cl_smp_app_492`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_492.js) | reload refresh restart location_reload url | +| [Set the Tab Favicon (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_491) | [`z2ui5_cl_smp_app_491`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_491.js) | favicon icon tab image data uri | +| [Set the Tab Title (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_125) | [`z2ui5_cl_smp_app_125`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_125.js) | document.title tab caption headline set_title | +| [Soft Keyboard Mode on Mobile (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_352) | [`z2ui5_cl_smp_app_352`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_352.js) | mobile numeric keypad keyboard_set_mode phone input | + +## Control Behaviour + +| Sample | Class | Topics | +|---|---|---| +| [Expand a Panel by ID (setExpanded) (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_448) | [`z2ui5_cl_smp_app_448`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_448.js) | panel collapse expand setexpanded control_by_id whitelisted | +| [MultiInput with Tokens (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_078) | [`z2ui5_cl_smp_app_078`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_078.js) | multiinput token tokens suggestion custom control | +| [Open the PDF Viewer by ID (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_449) | [`z2ui5_cl_smp_app_449`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_449.js) | pdfviewer pdf document viewer popup control_by_id whitelisted | +| [Switch NavContainer Page by ID (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_088) | [`z2ui5_cl_smp_app_088`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_088.js) | navcontainer icontabbar icontabheader page switch control_by_id whitelisted | +| [Wizard with Steps (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_202) | [`z2ui5_cl_smp_app_202`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_202.js) | wizard step branching discardprogress setnextstep control_by_id | + +## CSS + +| Sample | Class | Topics | +|---|---|---| +| [Color Table Cells from the Backend](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_305) | [`z2ui5_cl_smp_app_305`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_305.js) | color background conditional formatting style data attribute | +| [FlexBox Layouts with Custom Classes](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_255) | [`z2ui5_cl_smp_app_255`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_255.js) | flexbox layout responsive navigation tile panel | +| [Ship Your Own CSS with the View](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_050) | [`z2ui5_cl_smp_app_050`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_050.js) | style stylesheet inline html class own design | + +## Device + +| Sample | Class | Topics | +|---|---|---| +| [Camera, Take Photos (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_306) | [`z2ui5_cl_smp_app_306`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_306.js) | camera photo picture webcam capture facing mode | +| [Device Model: Phone, Tablet, Desktop (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_445) | [`z2ui5_cl_smp_app_445`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_445.js) | sap.ui.device responsive orientation resize media model | +| [Frontend Info: UI5 Version, Theme, OS, Browser](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_122) | [`z2ui5_cl_smp_app_122`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_122.js) | client info ui5 version theme os user agent device | +| [Geolocation from the Browser (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_120) | [`z2ui5_cl_smp_app_120`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_120.js) | gps position latitude longitude altitude location | + +## Event + +| Sample | Class | Topics | +|---|---|---| +| [Control Objects in t_arg (FacetFilter)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_197) | [`z2ui5_cl_smp_app_197`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_197.js) | facetfilter filter object marshalling selected items | +| [Extra Arguments with t_arg](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_167) | [`z2ui5_cl_smp_app_167`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_167.js) | argument parameter payload event data fixed value | +| [Keyboard Shortcuts, Ctrl+S (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_471) | [`z2ui5_cl_smp_app_471`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_471.js) | shortcut hotkey ctrl key combination keyboard_shortcut | +| [Link with preventDefault (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_472) | [`z2ui5_cl_smp_app_472`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_472.js) | link href default action check_prevent_default | + +## File + +| Sample | Class | Topics | +|---|---|---| +| [Download to the Browser (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_186) | [`z2ui5_cl_smp_app_186`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_186.js) | export save base64 attachment xstring document | +| [Upload to the Backend (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_074) | [`z2ui5_cl_smp_app_074`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_074.js) | fileuploader base64 attachment import picture document | + +## Focus + +| Sample | Class | Topics | +|---|---|---| +| [Focus a Table Cell by Column and Row (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_421) | [`z2ui5_cl_smp_app_421`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_421.js) | table cell column row aggregation set_focus | +| [Jump to the Next Input on Enter (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_189) | [`z2ui5_cl_smp_app_189`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_189.js) | cursor enter tab next field form set_focus | +| [Set Focus and Select Text in an Input (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_133) | [`z2ui5_cl_smp_app_133`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_133.js) | cursor set_focus selection position textfield | + +## Formatter + +| Sample | Class | Topics | +|---|---|---| +| [ABAP Date and Time Strings (DATS/TIMS)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_450) | [`z2ui5_cl_smp_app_450`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_450.js) | dats tims conversion initial date 00000000 sy-datum | +| [Date Object for the DatePicker](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_457) | [`z2ui5_cl_smp_app_457`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_457.js) | datepicker datevalue javascript date object iso | +| [Date Objects for the PlanningCalendar](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_456) | [`z2ui5_cl_smp_app_456`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_456.js) | planningcalendar appointment javascript date object iso | +| [Inline Icons in a Text](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_466) | [`z2ui5_cl_smp_app_466`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_466.js) | icon glyph placeholder text status expandinlineicons | +| [When Not to Use One: Compute in ABAP](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_453) | [`z2ui5_cl_smp_app_453`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_453.js) | no formatter computed backend thin frontend prepare | + +## Grid Table + +| Sample | Class | Topics | +|---|---|---| +| [Events on Cell Level](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_160) | [`z2ui5_cl_smp_app_160`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_160.js) | cell enter row index event grid alv | +| [Full Example with sap.ui.table](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_070) | [`z2ui5_cl_smp_app_070`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_070.js) | grid alv dynamicpage column row action currency search sort filter | +| [Keep Column Filters on Refresh (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_143) | [`z2ui5_cl_smp_app_143`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_143.js) | column filter reset refresh uitableext grid alv | + +## List + +| Sample | Class | Topics | +|---|---|---| +| [Filter and Sort the Binding from ABAP (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_454) | [`z2ui5_cl_smp_app_454`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_454.js) | binding_call getbinding sorter filter follow_up_action | +| [Live Filter on the Client, No Roundtrip (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_455) | [`z2ui5_cl_smp_app_455`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_455.js) | binding_call live search client side no roundtrip filter | +| [StandardListItem, Highlight and Events](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_048) | [`z2ui5_cl_smp_app_048`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_048.js) | sap.m.list standardlistitem highlight infostate press selection | + +## Menu + +| Sample | Class | Topics | +|---|---|---| +| [Full Path of the Selected Item (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_473) | [`z2ui5_cl_smp_app_473`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_473.js) | menuitem nested submenu textpath controller path | +| [Menu Button with core:require](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_163) | [`z2ui5_cl_smp_app_163`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_163.js) | menubutton menuitem popover messagetoast require module | + +## Message + +| Sample | Class | Topics | +|---|---|---| +| [Message Model and MessageManager (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_467) | [`z2ui5_cl_smp_app_467`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_467.js) | messagemanager validation target field state central model | +| [MessageBox from SY, BAPIRET2 or Exception](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_008) | [`z2ui5_cl_smp_app_008`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_008.js) | t100 message class number exception cx_root error abend | +| [MessageBox, Types and Custom Actions](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_382) | [`z2ui5_cl_smp_app_382`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_382.js) | confirm warning error success information dialog action | +| [MessagePopover URL Policy (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_474) | [`z2ui5_cl_smp_app_474`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_474.js) | url policy link security validator relative allow deny | +| [MessageToast, Text and Duration](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_381) | [`z2ui5_cl_smp_app_381`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_381.js) | toast notification duration position animation | +| [MessageView and MessagePopover (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_452) | [`z2ui5_cl_smp_app_452`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_452.js) | messagepopover messageitem dialog grouped message list | + +## Navigation + +| Sample | Class | Topics | +|---|---|---| +| [Call and Leave Apps (nav_app_call)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_024) | [`z2ui5_cl_smp_app_024`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_024.js) | nav_app_call nav_app_leave sub app stack call back | +| [Data Loss Protection on Leaving (A,C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_279) | [`z2ui5_cl_smp_app_279`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_279.js) | dirty unsaved changes leave confirmation warning | +| [Return Data and Events to the Caller](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_488) | [`z2ui5_cl_smp_app_488`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_488.js) | r_data result get_app_prev return event payload | +| [Uncaught Error and Error Popup](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_464) | [`z2ui5_cl_smp_app_464`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_464.js) | exception dump error handling debugtool restart retry | + +## Nested View + +| Sample | Class | Topics | +|---|---|---| +| [Basic Example (nest_view_display)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_065) | [`z2ui5_cl_smp_app_065`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_065.js) | nest_view_display rerender model refresh sub view | +| [Embed Another App's View](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_104) | [`z2ui5_cl_smp_app_104`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_104.js) | sub app class embed instantiate another app rtti | +| [Master-Detail with FlexibleColumnLayout](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_097) | [`z2ui5_cl_smp_app_097`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_097.js) | fcl master detail list report two column split | +| [Three Columns with FlexibleColumnLayout](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_098) | [`z2ui5_cl_smp_app_098`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_098.js) | fcl three column detail detail deep navigation | + +## Popover + +| Sample | Class | Topics | +|---|---|---| +| [Basic Example with Placement](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_026) | [`z2ui5_cl_smp_app_026`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_026.js) | placement anchor button confirm cancel popover_display | +| [Open from a Table Row](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_052) | [`z2ui5_cl_smp_app_052`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_052.js) | list report dynamicpage row link details table | +| [Open Together with the View Build](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_490) | [`z2ui5_cl_smp_app_490`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_490.js) | initial render one roundtrip anchor button | +| [QuickView Contact Card](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_109) | [`z2ui5_cl_smp_app_109`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_109.js) | quickview contact card links grouped fields | +| [Select from a List](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_081) | [`z2ui5_cl_smp_app_081`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_081.js) | list selection placement anchor | +| [Toggle by ID (toggleBy) (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_465) | [`z2ui5_cl_smp_app_465`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_465.js) | toggleby open close control_by_id whitelisted | + +## Popup + +| Sample | Class | Topics | +|---|---|---| +| [Dialog inside a Dialog](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_161) | [`z2ui5_cl_smp_app_161`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_161.js) | nested stack popup in popup second dialog | +| [Element Binding to the Selected Row (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_470) | [`z2ui5_cl_smp_app_470`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_470.js) | element binding relative path aggregation dialog row | +| [Navigate between Dialogs (NavContainer) (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_170) | [`z2ui5_cl_smp_app_170`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_170.js) | navcontainer dialog pages back forward | +| [Value Help: Suggestions and F4 Dialog](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_009) | [`z2ui5_cl_smp_app_009`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_009.js) | f4 search help suggestion input dialog select | +| [Ways to Open a Dialog (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_012) | [`z2ui5_cl_smp_app_012`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_012.js) | dialog sub app destroy rerender background view | + +## Scroll + +| Sample | Class | Topics | +|---|---|---| +| [Scroll a Control into View (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_363) | [`z2ui5_cl_smp_app_363`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_363.js) | scroll_into_view control id validation jump | +| [Scroll to a Pixel Position (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_362) | [`z2ui5_cl_smp_app_362`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_362.js) | position pixel scroll_to restore refresh toolbar | + +## Table + +| Sample | Class | Topics | +|---|---|---| +| [Drag and Drop Rows (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_459) | [`z2ui5_cl_smp_app_459`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_459.js) | dnd dragdropinfo reorder rows move | +| [Editable Cells, Add and Delete Rows](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_011) | [`z2ui5_cl_smp_app_011`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_011.js) | edit input add row delete multiselect toolbar | +| [Filter Rows in the Backend](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_045) | [`z2ui5_cl_smp_app_045`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_045.js) | filter server side form growing where | +| [Large Table with Growing and ScrollContainer](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_006) | [`z2ui5_cl_smp_app_006`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_006.js) | growing 10000 rows sticky toolbar sort performance | +| [Live Search with Parallel Requests](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_059) | [`z2ui5_cl_smp_app_059`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_059.js) | live search parallel requests busy queue typing | +| [Search in the Backend (SearchField)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_053) | [`z2ui5_cl_smp_app_053`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_053.js) | search go enter server side where | +| [Selection Modes: Single and Multi Select](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_019) | [`z2ui5_cl_smp_app_019`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_019.js) | selectionmode none single multi segmentedbutton checkbox | + +## Templating + +| Sample | Class | Topics | +|---|---|---| +| [Build Columns Dynamically (template:repeat)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_173) | [`z2ui5_cl_smp_app_173`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_173.js) | template repeat runtime generated columns if then else | +| [Dynamic Content in a Nested View](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_176) | [`z2ui5_cl_smp_app_176`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_176.js) | template repeat runtime generated nested nest_view_display | + +## Timer + +| Sample | Class | Topics | +|---|---|---| +| [Progress Indicator during a Backend Call (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_064) | [`z2ui5_cl_smp_app_064`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_064.js) | progressindicator busy wait long running backend | +| [Refresh the View Every n Seconds (A)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_028) | [`z2ui5_cl_smp_app_028`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_028.js) | interval polling auto refresh follow_up_action seconds | + +## Tree + +| Sample | Class | Topics | +|---|---|---| +| [Drag and Drop Nodes (A,C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_461) | [`z2ui5_cl_smp_app_461`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_461.js) | dnd move node hierarchy binding context | +| [Editable Nodes with CustomTreeItem (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_463) | [`z2ui5_cl_smp_app_463`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_463.js) | customtreeitem rename input binding write back | +| [Inside a Dialog (C)](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_462) | [`z2ui5_cl_smp_app_462`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_462.js) | popup expand state hierarchy nodes | +| [Nested ABAP Table in a sap.m.Tree](https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_460) | [`z2ui5_cl_smp_app_460`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples/z2ui5_cl_smp_app_460.js) | hierarchy nodes nested json items | + +## Not in the gallery + +6 further classes ship in the same folder without a catalogue entry — they are the sub-apps the listed samples call (dialogs, detail views, nested apps), not entries in their own right. They run the same way if you name one directly: + +`z2ui5_cl_smp_app_020`, `z2ui5_cl_smp_app_025`, `z2ui5_cl_smp_app_105`, `z2ui5_cl_smp_app_112`, `z2ui5_cl_smp_app_469`, `z2ui5_cl_smp_app_489` + +## Where they come from + +The samples are machine-transpiled from the ABAP originals in [abap2UI5-samples](https://github.com/abap2UI5/abap2UI5-samples) by the [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) pipeline, and land in `core/srv/app/samples/` on every sync. That folder is pipeline-owned: edits to it are overwritten on the next publish. Copy a sample into [`srv/app/`](./project-structure#srv-app) before you change it. + +Some samples need a real server: those that call external OData services or CAP destinations will not work in the browser playground. See [what the playground is not](./playground#what-its-not). + +→ Ready to write your own? Start with [Hello World](../examples/hello-world), or read the [App Lifecycle](./lifecycle). diff --git a/docs/guide/user-exit.md b/docs/guide/user-exit.md new file mode 100644 index 0000000..a546c8a --- /dev/null +++ b/docs/guide/user-exit.md @@ -0,0 +1,222 @@ +# The User Exit + +Everything the framework renders or sends — the tab title, the theme, the tab +icon, the UI5 bootstrap URL, the Content-Security-Policy, the response headers, +how long drafts live, whether the CSRF gate is armed — comes from one config +object. The **user exit** is the one supported way to change it. + +It is a single class implementing `z2ui5_if_exit`, and it is the framework's +only extension point of this kind: there is no config file, no environment +variable for these values, and nothing to register. You write the class, the +framework finds it. + +## The smallest exit + +```js +// srv/app/my_exit.js +class my_exit { + + set_config_http_get(s_context, s_config) { + s_config.title = "Order Management"; + s_config.theme = "sap_horizon_dark"; + return s_config; + } + + set_config_http_post(s_context, s_config) { + return s_config; + } +} + +module.exports = my_exit; +``` + +That is the whole contract. Both methods must exist, but either may simply hand +back what it was given. + +::: warning `z2ui5_if_exit` is a contract, not a base class +Unlike `z2ui5_if_app`, it is **not** something to `extend` — it is a plain +object holding the method names, and `class my_exit extends z2ui5_if_exit` +throws at load time. The framework matches an exit by those method names alone. +If you want the check written down, assert it explicitly: + +```js +const z2ui5_if_exit = require("abap2UI5/z2ui5_if_exit"); +z2ui5_if_exit.check_implements(my_exit.prototype); // throws if one is missing +``` +::: + +## How the framework finds it + +`z2ui5_cl_ui5_user_exit` is the framework's own implementation of the same +interface. On the first request it scans the registered app directories for +**any other** class implementing `z2ui5_if_exit`, instantiates the one it +finds, and keeps it for the life of the process. + +- The scan uses the same app-directory search as app classes, so put the exit + where your apps are: [`srv/app/`](./project-structure#srv-app), or any + directory named in `Z2UI5_APP_DIRS` / registered via + `require("abap2UI5/register-apps")`. +- The file name must match the class name, as it must for app classes. +- **If more than one exit exists, exactly one wins** — the first by class name, + sorted. That is deliberate rather than a race: the chosen exit controls the + CSP and every security header, so which one wins must not depend on the order + the file system happens to return. Ship one exit. +- If none exists, the framework defaults apply unchanged. + +The framework runs its own defaults **first** and calls your exit **after**, so +`s_config` arrives fully populated. You override what you care about and leave +the rest alone. ABAP's `CHANGING` semantics map to either style in JavaScript: +mutate `s_config` in place, return it, or both — the framework accepts all +three. + +## The request context + +The first argument describes the request being answered: + +| Field | What it is | +|---|---| +| `path` | the request path | +| `method` | the HTTP method | +| `t_params` | the URL query parameters, as `[{ n, v }]` | +| `app_start` | the `?app_start=` value, lifted out of `t_params` for convenience | +| `session_id` | the session key the framework isolates state by | +| `tenant` | the tenant, when the platform supplies one | +| `body` | the raw request body | + +So the config can depend on the request — a different theme per app, for +instance: + +```js +set_config_http_get(s_context, s_config) { + if (s_context.app_start === "z2ui5_cl_smp_app_000") { + s_config.theme = "sap_horizon"; + } + return s_config; +} +``` + +The context is **per request**, not process-global: two roundtrips in flight at +the same time each see their own, so an exit cannot read the other request's +parameters. + +## `set_config_http_get` — the bootstrap page + +This hook builds the HTML page that boots UI5. It runs once per full page +load, not once per roundtrip. + +| Field | Default | Effect | +|---|---|---| +| `title` | `abap2UI5` | the browser tab title | +| `theme` | `sap_horizon` | any UI5 theme id | +| `favicon` | an inline SVG data URI | the tab icon; set your own, or clear it and the page emits no icon link at all (the browser then falls back to `/favicon.ico`) | +| `src` | `/resources/sap-ui-core.js` | the UI5 bootstrap — the locally served runtime by default, which is what keeps the stack working offline | +| `content_security_policy` | a full `` tag | see below | +| `t_security_header` | 7 headers | `[{ n, v }]`, applied to the bootstrap response | +| `t_add_config` | — | extra `data-sap-ui-*` bootstrap attributes, as `[{ n, v }]` | + +::: tip Escaping +Values are escaped for the context they land in — text, attribute or URI — so a +title with an apostrophe or an ampersand renders as written. Attribute *names* +in `t_add_config` are restricted rather than escaped: a name outside +`[A-Za-z_][A-Za-z0-9_:.-]*` is dropped. +::: + +### Content-Security-Policy + +The default policy allows `'unsafe-eval'` for one specific reason: the OpenUI5 +1.71 `ui5loader` evaluates module source as a string, and without it that +bootstrap fails with a CSP `EvalError`. Modern UI5 does not need it. If you pin +a modern release, tighten the policy: + +```js +set_config_http_get(s_context, s_config) { + s_config.content_security_policy = s_config.content_security_policy + .replace(" 'unsafe-eval'", ""); + return s_config; +} +``` + +The policy also allow-lists the public UI5 CDNs plus jsDelivr and cdnjs, and +permits `data:` — which is what makes the default inline-SVG favicon work. If +you serve everything locally, those hosts can go too. + +### Security headers + +The seven defaults are `cache-control: no-cache, no-store, must-revalidate`, +`Pragma: no-cache`, `Expires: 0`, `X-Content-Type-Options: nosniff`, +`X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin` +and a `Permissions-Policy` allowing geolocation, microphone and camera for the +same origin only. Replace or extend the array to add your own — HSTS, for +instance, on a deployment that terminates TLS itself. + +## `set_config_http_post` — the roundtrip + +This hook runs on the roundtrip endpoint, the POST that every user interaction +goes through. + +| Field | Default | Effect | +|---|---|---| +| `draft_exp_time_in_hours` | `4` | how long a draft stays resumable | +| `check_csrf_active` | `true` | the cross-origin POST gate | + +### Draft expiry + +```js +set_config_http_post(s_context, s_config) { + s_config.draft_exp_time_in_hours = 24; + return s_config; +} +``` + +A value of zero or less is ignored and the default restored — an expiry of +"never" is not something a typo should be able to configure. + +::: tip One knob, not two +The CAP app's retention job deletes draft rows on the same clock: it asks this +exit for `draft_exp_time_in_hours` and follows it, so raising the expiry here +also keeps the rows around longer. `Z2UI5_DRAFT_TTL_HOURS` overrides both when +it is set — see [Configuration](../reference/configuration). +::: + +### CSRF + +The gate compares the `Origin` header — falling back to `Referer` — against the +app's own `Host`, and rejects a mismatch with `403 Forbidden`. The frontend +posts from the same origin, so first-party traffic is unaffected. + +Two properties are worth knowing before you rely on it: + +- It is **on by default** since the 2026-08 security pass. It used to be + opt-in, and nothing opted in. If you are looking at a core vendored before + that change, `set_config_http_post` in + `core/srv/z2ui5/01/04/z2ui5_cl_ui5_user_exit.js` tells you which one you + have. +- It is **lenient when there is nothing to compare**: a request carrying + neither `Origin` nor `Referer` is allowed through, so that proxies which + strip those headers do not lock users out. It is a defence against + cross-origin form posts, not a complete CSRF defence, and it does not replace + authentication. + +Turn it off — in front of your own gateway, say — the same way you turn +anything else off: + +```js +set_config_http_post(s_context, s_config) { + s_config.check_csrf_active = false; + return s_config; +} +``` + +## What the exit is not for + +- **Not app configuration.** It is called for every request, for every app. App + state belongs on the app instance; see [App Lifecycle](./lifecycle). +- **Not a request filter.** It shapes config, it does not accept or reject + requests. Authentication and authorisation belong on the CAP service + (`@requires`, `@restrict`) — see [Deployment](../reference/deployment). +- **Not a place for per-user secrets.** The instance is created once and shared + by every request in the process; only the context argument is per request. + +→ Related: [Configuration](../reference/configuration) for environment +variables, identity and the draft store; [Architecture](../reference/architecture) +for where `z2ui5_cl_ui5_user_exit` sits in the stack. diff --git a/docs/guide/vs-abap2ui5.md b/docs/guide/vs-abap2ui5.md index a422986..3598326 100644 --- a/docs/guide/vs-abap2ui5.md +++ b/docs/guide/vs-abap2ui5.md @@ -151,14 +151,8 @@ A jest suite gates every sync — only a green build is committed. ## Migration ABAP → CAP -Because the wire format and API are compatible, migrating an existing abap2UI5 app to cap2UI5 is mechanical: +Because the wire format and API are compatible, migrating an existing abap2UI5 app to cap2UI5 is mechanical: rewrite the class method for method, convert data access from OpenSQL to CDS queries and outbound calls from `cl_http_client` to `fetch`, drop the file into `srv/app/`, run it. The same static frontend renders both without changes. -1. Rewrite the ABAP class as a JS class — method for method, with the two caveats above: a view built on the retired `z2ui5_cl_xml_view` has to be rebuilt on `z2ui5_cl_ui5_view_builder`, and two-way binding is `_bind_edit` on the release cap2UI5 pins. Or let the transpiler do a first pass: `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout` in a [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) checkout emits JavaScript, marking unsupported statements as `// TODO(abap2js)` comments instead of dropping them -2. Convert data access from OpenSQL to CDS queries -3. Convert external calls from `cl_http_client` to `fetch`/`cds.connect.to` -4. Drop the file into `srv/app/` (or a [registered app folder](./project-structure#srv-app)) -5. Run it — done. +→ [**Migrating from abap2UI5**](./migration-from-abap2ui5) is the full walkthrough, with a per-construct mapping table for classes, views, binding, structures, data access and outbound calls. -The same static frontend renders both without changes. - -→ Have a look at the [**examples**](../examples/hello-world) or jump straight to the [**API reference**](../api/client). +→ Or have a look at the [**examples**](../examples/hello-world) and jump straight to the [**API reference**](../api/client). diff --git a/docs/index.md b/docs/index.md index f538829..f623cbf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,4 +38,6 @@ features: - title: A live sample for every feature details: The transpiled abap2UI5 demo collection ships in core/srv/app/samples/ — and runs zero-install in the browser playground on GitHub Pages. icon: 🎨 + link: /guide/samples + linkText: Browse the sample catalogue --- diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 41b1bf1..e9539e7 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -9,16 +9,26 @@ framework renders or sends. | Variable | Default | Effect | |---|---|---| | `Z2UI5_APP_DIRS` | — | extra directories to search for app classes, separated by the platform path separator | -| `Z2UI5_DRAFT_TTL_HOURS` | `24` | how long draft rows are kept; `0` disables the retention job entirely | +| `Z2UI5_DRAFT_TTL_HOURS` | the exit's `draft_exp_time_in_hours` | how long draft rows are kept; `0` disables the retention job entirely | +| `Z2UI5_DRAFT_RETENTION_INSTANCE` | `0` | which Cloud Foundry instance runs the retention loop; `*` for all of them | | `PORT` | `4004` (CAP) | the port the server listens on | ```bash Z2UI5_APP_DIRS=/srv/my-apps:/srv/more-apps npx cds-serve -Z2UI5_DRAFT_TTL_HOURS=4 npx cds watch +Z2UI5_DRAFT_TTL_HOURS=24 npx cds watch ``` -An unparseable `Z2UI5_DRAFT_TTL_HOURS` falls back to 24 hours rather than -disabling cleanup — a typo must not silently turn retention off. +Retention has **one** clock, not two. Unset, `Z2UI5_DRAFT_TTL_HOURS` follows +the framework's own draft expiry — `draft_exp_time_in_hours` from the user exit, +4 hours by default — so rows are not deleted while the framework still considers +the session live. Set, it overrides both. An unparseable value falls back to the +framework expiry rather than disabling cleanup: a typo must not silently turn +retention off. + +Outside Cloud Foundry every process runs the retention loop, which is the right +answer for a single server. On CF only the instance whose `CF_INSTANCE_INDEX` +matches `Z2UI5_DRAFT_RETENTION_INSTANCE` does — the same hourly `DELETE` run by +N instances is the same work done N times. ## Registering app directories in code @@ -47,28 +57,40 @@ by scanning for an implementation — you do not register it anywhere. Its two methods are called after the framework defaults, so you receive a fully populated config object and change only what you care about. -```js -const z2ui5_if_exit = require("abap2UI5/z2ui5_if_exit"); +This section is the summary; [**The User Exit**](../guide/user-exit) is the +full treatment — discovery, the request context, and every field. -class my_exit extends z2ui5_if_exit { +```js +// srv/app/my_exit.js +class my_exit { set_config_http_get(s_context, s_config) { s_config.title = "My Application"; s_config.theme = "sap_horizon_dark"; return s_config; } + + set_config_http_post(s_context, s_config) { + return s_config; + } } module.exports = my_exit; ``` +`z2ui5_if_exit` is a contract, not a base class: an exit is any class carrying +both methods, and `extends z2ui5_if_exit` throws. Both must be present, even +when one only hands back what it was given. + ### What `set_config_http_get` controls | Field | Default | Notes | |---|---|---| | `title` | `abap2UI5` | the browser tab title | | `theme` | `sap_horizon` | any UI5 theme id | +| `favicon` | an inline SVG data URI | the tab icon; clear it and the page emits no icon link at all | | `src` | `/resources/sap-ui-core.js` | the UI5 bootstrap — the local runtime by default, so the stack works offline | | `content_security_policy` | a full `` tag | see below | | `t_security_header` | 7 headers | `[{n, v}]`, applied to the bootstrap response | +| `t_add_config` | — | extra `data-sap-ui-*` bootstrap attributes, as `[{n, v}]` | The default security headers are `cache-control: no-cache, no-store, must-revalidate`, `Pragma: no-cache`, `Expires: 0`, @@ -96,12 +118,15 @@ serve everything locally you can strip those hosts too. ### `set_config_http_post` -The same idea for the roundtrip response — the hook to add response headers -or to switch on the CSRF gate, which is **off by default**: +The same idea for the roundtrip. Two fields: `draft_exp_time_in_hours` (default +`4`) and `check_csrf_active`, the cross-origin POST gate, which is **on by +default** — turn it off only when something in front of the app already covers +it: ```js set_config_http_post(s_context, s_config) { - s_config.check_csrf_active = true; + s_config.draft_exp_time_in_hours = 24; + s_config.check_csrf_active = false; return s_config; } ``` diff --git a/package.json b/package.json index 015aeda..8702b21 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "verify": "node scripts/verify-refs.mjs", - "check": "npm run verify && npm run docs:build" + "check": "npm run verify && npm run docs:build", + "check:ci": "node scripts/verify-refs.mjs --require-checkout && npm run docs:build", + "gen:samples": "node scripts/gen-samples.mjs" }, "devDependencies": { "vitepress": "^1.6.3" diff --git a/scripts/gen-samples.mjs b/scripts/gen-samples.mjs new file mode 100644 index 0000000..e274702 --- /dev/null +++ b/scripts/gen-samples.mjs @@ -0,0 +1,222 @@ +#!/usr/bin/env node +/** + * gen-samples — generate docs/guide/samples.md from the shipped sample gallery. + * + * WHY THIS EXISTS + * --------------- + * cap2UI5 ships over a hundred sample apps, and until this page existed the + * only way into them was to already know a class name and hand-type it into + * `?app_start=`. A hand-written index would have been a second copy of a list + * that changes on every sync — stale within a week, and invisible to + * verify-refs until a class actually disappeared. + * + * So the page is generated from the gallery itself. `z2ui5_cl_smp_app_000` is + * the sample browser that ships with the framework; its `get_catalog()` holds + * one `{ group, header, sub, keywords, path, app }` entry per sample. That + * table is the same source the in-app gallery renders from, which makes it the + * only list that cannot disagree with what the playground actually offers. + * + * Usage: + * CAP2UI5_DIR=/path/to/cap2UI5 node scripts/gen-samples.mjs + * node scripts/gen-samples.mjs # defaults to ../cap2UI5 + * node scripts/gen-samples.mjs --check # fail if the page is out of date + * + * The result is committed like any other page: the docs must build without a + * cap2UI5 checkout, and a reviewer should see the diff a sync causes. + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const APP = path.resolve(process.env.CAP2UI5_DIR || path.join(ROOT, "..", "cap2UI5")); +const SAMPLES = path.join(APP, "core", "srv", "app", "samples"); +const GALLERY = "z2ui5_cl_smp_app_000"; +const OUT = path.join(ROOT, "docs", "guide", "samples.md"); +const CHECK = process.argv.includes("--check"); + +const PLAYGROUND = "https://cap2ui5.github.io/web-cap2UI5-build/"; +const SOURCE = "https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/app/samples"; + +if (!fs.existsSync(path.join(SAMPLES, `${GALLERY}.js`))) { + console.error(`gen-samples: no sample gallery at ${SAMPLES}/${GALLERY}.js`); + console.error(` set CAP2UI5_DIR or clone cap2UI5/cap2UI5 next to this repo.`); + process.exit(1); +} + +// The catalogue is one long array literal of object literals, all fields in a +// fixed order because the file is machine-generated by the transpiler. Parsing +// it with a regex is fine for exactly that reason — and if the shape ever +// changes, the count check below turns the surprise into a failure. +const ENTRY_RE = + /\{ group: `([^`]*)`, header: `([^`]*)`, sub: `([^`]*)`, keywords: `([^`]*)`, path: `([^`]*)`, app: `([^`]*)` \}/g; + +const src = fs.readFileSync(path.join(SAMPLES, `${GALLERY}.js`), "utf8"); +const entries = [...src.matchAll(ENTRY_RE)].map((m) => ({ + group: m[1], + header: m[2], + sub: m[3], + keywords: m[4], + app: m[6], +})); + +if (entries.length < 50) { + console.error(`gen-samples: only ${entries.length} catalogue entries parsed from ${GALLERY}.js`); + console.error(` the gallery table's shape probably changed — fix ENTRY_RE.`); + process.exit(1); +} + +const files = new Set( + fs + .readdirSync(SAMPLES) + .filter((f) => f.endsWith(".js")) + .map((f) => f.slice(0, -3)), +); + +const missing = entries.filter((e) => !files.has(e.app)); +if (missing.length) { + console.error(`gen-samples: the gallery lists ${missing.length} class(es) with no file:`); + for (const e of missing) console.error(` ${e.app}`); + process.exit(1); +} + +// The `group` field separates the upstream sample repositories (samples, +// controls, stack). Only one of them is carried into the port today, so +// grouping by it yields a single bucket — the useful axis is `header`, the +// category the in-app gallery prints above each block. Both are honoured: +// groups first (in catalogue order), categories inside them. +// +// One presentation liberty: the gallery numbers its introductory tiles +// `Basics I` … `Basics V`, one sample each. Five one-row tables is noise on a +// page you scroll rather than search, so they collapse into one `Basics` +// section — in catalogue order, which is the order they are meant to be read. +const categoryOf = (header) => (/^Basics\b/.test(header) ? `Basics` : header); + +const groups = []; +for (const e of entries) { + let g = groups.find((x) => x.name === e.group); + if (!g) groups.push((g = { name: e.group, categories: [] })); + const name = categoryOf(e.header); + let c = g.categories.find((x) => x.name === name); + if (!c) g.categories.push((c = { name, rows: [] })); + c.rows.push(e); +} + +const md = (s) => s.replace(/\|/g, "\\|"); +const anchor = (s) => + s + .toLowerCase() + .replace(/[^\w\- ]+/g, "") + .replace(/ /g, "-"); + +const categories = groups.flatMap((g) => g.categories); +const uncatalogued = [...files].filter((f) => f !== GALLERY && !entries.some((e) => e.app === f)).sort(); + +const out = []; +out.push(``); +out.push(``); +out.push(``); +out.push(`# Sample Catalogue`); +out.push(``); +out.push( + `The framework ships ${files.size} sample apps in \`core/srv/app/samples/\` — the ` + + `abap2UI5 demo collection, transpiled to JavaScript. ${entries.length} of them are ` + + `listed in the built-in gallery, and this page is that gallery's own table: it is ` + + `generated from \`${GALLERY}\`, the sample browser the framework itself renders, so it ` + + `cannot drift from what the playground offers.`, +); +out.push(``); +out.push( + `Every title links into the [browser playground](./playground) — no install, no server. ` + + `The same apps run on your own CAP server under the same names: ` + + `\`?app_start=\`, or \`client.nav_app_call()\` from an app of your own.`, +); +out.push(``); +out.push( + `→ Prefer to browse in the app itself? Open ` + + `[\`${GALLERY}\`](${PLAYGROUND}?app_start=${GALLERY}) — the same catalogue with a search field.`, +); +out.push(``); +out.push( + `Two markers are carried over from the upstream gallery: **(A)** marks a sample that ` + + `drives the frontend through a client action, **(C)** one that uses a custom control ` + + `from the \`z2ui5.cc\` namespace.`, +); +out.push(``); +out.push(`## Categories`); +out.push(``); +out.push( + categories.map((c) => `[${c.name}](#${anchor(c.name)})`).join(" · "), +); +out.push(``); + +for (const c of categories) { + out.push(`## ${c.name}`); + out.push(``); + out.push(`| Sample | Class | Topics |`); + out.push(`|---|---|---|`); + for (const r of c.rows) { + out.push( + `| [${md(r.sub)}](${PLAYGROUND}?app_start=${r.app}) ` + + `| [\`${r.app}\`](${SOURCE}/${r.app}.js) ` + + `| ${md(r.keywords)} |`, + ); + } + out.push(``); +} + +if (uncatalogued.length) { + out.push(`## Not in the gallery`); + out.push(``); + out.push( + `${uncatalogued.length} further classes ship in the same folder without a catalogue ` + + `entry — they are the sub-apps the listed samples call (dialogs, detail views, ` + + `nested apps), not entries in their own right. They run the same way if you name ` + + `one directly:`, + ); + out.push(``); + out.push(uncatalogued.map((a) => `\`${a}\``).join(", ")); + out.push(``); +} + +out.push(`## Where they come from`); +out.push(``); +out.push( + `The samples are machine-transpiled from the ABAP originals in ` + + `[abap2UI5-samples](https://github.com/abap2UI5/abap2UI5-samples) by the ` + + `[builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) pipeline, and ` + + `land in \`core/srv/app/samples/\` on every sync. That folder is pipeline-owned: ` + + `edits to it are overwritten on the next publish. Copy a sample into ` + + `[\`srv/app/\`](./project-structure#srv-app) before you change it.`, +); +out.push(``); +out.push( + `Some samples need a real server: those that call external OData services or CAP ` + + `destinations will not work in the browser playground. See ` + + `[what the playground is not](./playground#what-its-not).`, +); +out.push(``); +out.push( + `→ Ready to write your own? Start with [Hello World](../examples/hello-world), ` + + `or read the [App Lifecycle](./lifecycle).`, +); +out.push(``); + +const text = out.join("\n"); + +if (CHECK) { + const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, "utf8") : ""; + if (current !== text) { + console.error(`gen-samples: ${path.relative(ROOT, OUT)} is out of date — run npm run gen:samples`); + process.exit(1); + } + console.log(`gen-samples: OK — ${path.relative(ROOT, OUT)} matches ${APP}`); + process.exit(0); +} + +fs.writeFileSync(OUT, text); +console.log( + `gen-samples: wrote ${path.relative(ROOT, OUT)} — ` + + `${entries.length} catalogued samples in ${categories.length} categories ` + + `(${files.size} files in ${path.relative(APP, SAMPLES)})`, +); diff --git a/scripts/verify-refs.mjs b/scripts/verify-refs.mjs index 2296368..4e64bec 100644 --- a/scripts/verify-refs.mjs +++ b/scripts/verify-refs.mjs @@ -37,10 +37,14 @@ * CAP2UI5_DIR=/path/to/cap2UI5 node scripts/verify-refs.mjs * node scripts/verify-refs.mjs # defaults to ../cap2UI5 * node scripts/verify-refs.mjs --list # print the resolved inventory + * node scripts/verify-refs.mjs --require-checkout # missing checkout = error * * Exits 1 on the first broken claim, with the file and line to fix. When no * checkout is available it says so and exits 0 — a missing checkout is a - * setup gap, not a documentation defect. + * setup gap, not a documentation defect. That leniency is right on a laptop + * and wrong in CI: a workflow that checks cap2UI5 out and then silently loses + * it would go on passing while checking nothing. `--require-checkout` turns + * the skip into a failure, and .github/workflows/check.yml passes it. */ import fs from "node:fs"; import path from "node:path"; @@ -50,8 +54,15 @@ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const DOCS = path.join(ROOT, "docs"); const APP = path.resolve(process.env.CAP2UI5_DIR || path.join(ROOT, "..", "cap2UI5")); const LIST = process.argv.includes("--list"); +const REQUIRE_CHECKOUT = process.argv.includes("--require-checkout"); if (!fs.existsSync(path.join(APP, "package.json"))) { + if (REQUIRE_CHECKOUT) { + console.error(`verify-refs: no cap2UI5 checkout at ${APP} — required by --require-checkout.`); + console.error(` set CAP2UI5_DIR or clone cap2UI5/cap2UI5 next to this repo.`); + console.error(`\nSkipping here would mean the run checked nothing while reporting success.`); + process.exit(1); + } console.log(`verify-refs: no cap2UI5 checkout at ${APP} — skipping.`); console.log(` set CAP2UI5_DIR or clone cap2UI5/cap2UI5 next to this repo.`); process.exit(0); From 9677fe6cd3c863436d0ac6d1e32d4c05f42bca46 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 17:06:57 +0000 Subject: [PATCH 3/3] Record what the roadmap's execution actually found Phases 0-4 are implemented across the six repositories. ROADMAP.md gains a section recording what landed and -- more usefully -- where the analysis was WRONG. The original text is left as written so the corrections are visible rather than quietly edited away. Four findings did not survive contact with the code, including the document's own headline claim: - "_bind does not walk nested structures" is false. Five of the six port-bug ratchet entries are one inherent JS limit unreachable from a real app: the transpiled test passes a VALUE COPY of an empty string, so the lookup matches the first empty attribute. The app-facing API takes the member path and is correct at every depth, verified. The sixth was genuine and is fixed, so the baseline now holds zero port-bug entries. - The "dual draft-store model", called the largest correctness debt, is a documented cache composition: a miss falls through to the durable store. What was real is that it was unbounded, which is now fixed. - All five apps-smoke baseline entries describe CORRECT behaviour -- components and sub-apps that cannot start standalone -- not outstanding bugs. - The proposed stricter CSRF rule was implemented, found to contradict a published upstream contract and to break real callers, and reverted. It also records three defects a live CAP 9 probe found that no unit test could, two of which this work introduced -- which is the argument for the probe -- and what remains open (npm publish needs credentials; the SRTTI family; splitting abap2js.js; committing the oracle's classification). Also fixes reference/database.md, which described srv/draft-retention.js after that file moved into the framework package, and its now-stale TTL default. Caught by verify-refs, which is the check doing its job. npm run check green. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01T2gZuy95uKcT2zngQd1sAL --- ROADMAP.md | 153 +++++++++++++++++++++++++++++++++++++ docs/reference/database.md | 17 +++-- 2 files changed, 163 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 89cd4e2..ff4181d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,6 +6,13 @@ test suites executed, numbers measured rather than quoted). This is a maintainer planning document, not a site page — it is deliberately outside `docs/` so it is not published or scanned by `verify-refs`.* +> [!IMPORTANT] +> **Phases 0–4 have since been implemented.** See +> [§7 What was executed](#7-what-was-executed) for what landed, and — more +> usefully — for the four findings below that turned out to be **wrong** once +> someone tried to fix them. The analysis is left as written so the +> corrections are visible rather than quietly edited away. + --- ## 1. Where the project stands @@ -305,3 +312,149 @@ project except cloning a build artifact. This is the ceiling on adoption. `docs/removal-plan.md`); executed: the builder jest suite (216/221 + 1 fail), eslint (127 warnings), coverage, and `verify-refs` (clean). File/line references for every finding live in the review notes behind this document.* + +--- + +## 7. What was executed + +All five phases were implemented on the branch +`claude/cap2ui5-analysis-roadmap-kcf9qc` across six repositories. This section +records what landed and, more importantly, **where the analysis above was +wrong** — four of its findings did not survive contact with the code, and one +of them was the document's headline claim. + +### 7.1 Four corrections to the analysis + +**`_bind` nested-structure binding is NOT broken.** §2.1 reported six +`port-bug` ratchet entries as "real shipped framework defects hidden in a green +build", the worst being "`_bind` does not walk nested structures". Five of the +six are one inherent JS limitation, and it is not reachable from a real app: +`REF #( ms_struc-s_02-input )` transpiles to a *value copy* of an empty string, +so the lookup matches the first attribute that is also empty and answers +`{/MS_STRUC/INPUT}`. Every one of those tests initialises its fields to `""`, +which is why they all return the same wrong path. The app-facing API takes the +member path explicitly, and +`main_two_way(client, val, { name: "ms_struc-s_02-s_03-input" })` answers +`{/XX/MS_STRUC/S_02/S_03/INPUT}` — verified at every depth. The entries are now +categorised `js-limit` with that evidence. The sixth was genuine (a one-line +lifecycle-latch gap in `db_save`) and is fixed, so the baseline now holds +**zero** `port-bug` entries. The miscategorisation was the actual defect: it +had read as user-facing breakage for months. + +**The "dual draft-store model" is a cache, not a rival store.** §1/§2.2 called +it "the single largest correctness debt". `db_load` composes the two +deliberately — process buffer, then the synchronous store (transpiled ABAP +cannot `await`), then a fall-through to the durable CDS store. A miss is a +fall-through, not a wrong answer, which is also why back-navigation survives a +cold process. Nothing said so anywhere, which is why it read as a bug. What +*was* real: the cache was unbounded, holding every draft payload the process +had ever written. Now bounded (500 rows/table, oldest evicted), which is safe +precisely because of the fall-through. + +**Five of the five smoke-baseline entries describe correct behaviour.** §2.1 +counted them as outstanding failures. They are components and sub-apps that +cannot be started standalone — a confirm dialog that leaves immediately when +started with no event, sub-apps needing a parent, a sample that reads browser +device info a headless run cannot supply. They now carry `expected: true` and a +reason each. + +**The stricter CSRF rule was wrong and was reverted.** Phase 1 proposed failing +closed when neither `Origin` nor `Referer` is present. Implemented, it +contradicted a published upstream contract +(`ltcl_test_http_handler~test_csrf_no_headers` pins the lenient answer) and +broke callers that post without either header. Reverted, with the reasoning +recorded at the function: the vector it aimed at is closed a layer up, since +CDS accepts an action call only as `application/json` — which a cross-site +form cannot produce — and the approuter forwards a JWT. + +### 7.2 What a live probe found that no unit test could + +Booting a real CAP 9 server against the package (rather than trusting the test +suite) surfaced three defects that were invisible from inside: + +1. **`@sap/cds` was resolved from the wrong tree.** The package is normally a + `file:` dependency, npm symlinks those, and Node resolves from the *real* + path — so the plugin reported "@sap/cds not resolvable" while running inside + a live CAP server. +2. **The CSRF gate never saw a header.** It read `req.req || req._.req`; CAP 9 + exposes the express request as `req.http.req`. The gate was active, correct, + and passed empty strings — which its lenient branch reads as "nothing to + compare, allow". A cross-origin POST was answered **200** before the fix and + **403** after it, confirmed over real HTTP. The same bug meant user exits + never saw request context at all. +3. **A rejection was returned as a value**, so CDS serialised it as a + *successful* action result: HTTP 200 carrying `{status_code: 403}` in the + body. Every client that checks the status read a blocked request as + completed. + +None of the three could have been caught by the existing suites, and the first +two were introduced *by this work* — which is the argument for the probe. + +### 7.3 What landed, by phase + +**P0 — correctness and pipeline integrity.** Favicon contract ported (the +pipeline was red and the publish chain stalled); the missing approuter route +added with `test/approuter-routes.test.js` pinning it; `assemble-core` now +exits non-zero on a skipped file and refuses to run the load gate without its +dependencies; size floors added to transpile and publish and raised to near the +real corpus (units 150 → 480 of 511); `check-port-drift` no longer rewrites the +baseline on a report run. + +**P1 — security.** CSRF on by default; the `User` role enforced instead of bare +`authenticated-user`; security headers on the data endpoints; an explicit body +cap; HTML escaping for all exit-supplied config with a shipped +`abap2UI5/z2ui5_html` helper; retention reconciled to one TTL and one instance; +`createdAt`/`owner` indexed as HANA artifacts; npm added to dependabot; an +audit job on the shipped lock. + +**P2 — distribution.** The package now wires itself into CAP: `cds-plugin.js` +plus a shipped model and service, so a consumer writes two `using` lines and +installs. The generated app consumes the same `activate()` rather than +duplicating it — which also fixed retention starting twice. Hand-written +`.d.ts` verified with `tsc --strict` (and against deliberate errors). The npm +rename is **decided but not executed**: `docs/adr-001-npm-publishing.md` records +the choice (`@cap2ui5/core`) and `scripts/rename-package.js` performs it across +both repos (87 files on `--dry-run`). Renaming before anyone can publish would +break every example and pin for a benefit that does not exist until a +maintainer with credentials runs `npm publish`. + +**P3 — structural debt.** Three transpiler lowerings that returned *wrong +answers* now either work correctly or throw: `IN` ignored the range `sign`, so +an EXCLUDE line was evaluated as an include; `CP`/`NP` stripped the wildcards +and called `includes()`, so `A*Z` matched "ZA"; unknown operators emitted +`false`. `check-doc-numbers.js` verifies counts quoted in prose (every one had +drifted). `check-upstream-divergence.js` turns upstream's obsolescence list +into a baseline diff, so a retirement the port still ships fails the PR gate. + +**P4 — product polish.** The playground gained a sample browser (104 samples, +grouped and filterable — the only way in was previously typing a class name), +a registry floor and a recorded OpenUI5 version in `BUILD_INFO.json`, an +extracted and tested HTML patcher, and `AbortSignal` support. The docs gained +the sample catalogue, a standalone migration guide, a user-exit page, a roadmap +page and a sitemap; `verify-refs` can now fail closed. + +### 7.4 Numbers + +| | Before | After | +|---|---|---| +| builder-abap2UI5-js suite | 21 suites, 1 failing (publish blocked) | 26 suites, 269 passing | +| cap2UI5 app suite | 23 tests | 44 tests | +| builder-cap2UI5-web suite | 28 tests | 52 tests | +| Units ratchet | 113, incl. 6 `port-bug` | 111, **0** `port-bug` | +| `TODO(abap2js)` in shipped core | 82 | 72 | +| BTP roundtrip routing | unrouted (app could not work) | routed + tested | +| CSRF | opt-in, never enabled | on by default, 403 verified over HTTP | + +### 7.5 Still open + +- **`npm publish`** — needs a maintainer with registry credentials; everything + else is prepared (§7.3, P2). +- **The SRTTI family** (`00/02/z2ui5_cl_srt_*`) — still the convergence point + of the remaining lint warnings, most of the shipped `TODO(abap2js)` markers + and no coverage. It is the largest single piece of remaining work and was + scoped as its own project rather than attempted here. +- **Splitting `abap2js.js`** (~4,000 lines) — the comparison fixes landed + without it; the refactor itself remains. +- **The 74 `port-deviation` baseline entries** — the weekly oracle can *prove* + which are achievable in JS, but its output still expires with the Actions log + rather than being committed. diff --git a/docs/reference/database.md b/docs/reference/database.md index 6f7b825..7de1e09 100644 --- a/docs/reference/database.md +++ b/docs/reference/database.md @@ -106,23 +106,26 @@ The `managed` aspect requires no code patch in cap2UI5 — the engine ignores th ### Option 1: the retention job that already ships -You do not have to write this one — the app ships it as `srv/draft-retention.js`, -started from `srv/server.js` on the `served` event. It deletes rows past their -TTL once at startup and hourly after that, on an `unref`'d timer so it never -holds the process open. +You do not have to write this one — it ships inside the framework package +(`core/srv/cap/retention.js`) and is started by its `cds-plugin`, so it is +running in any project that installs cap2UI5, not only in this repository. It +deletes rows past their TTL once at startup and hourly after that, on an +`unref`'d timer so it never holds the process open. Configure it with one environment variable: | Variable | Default | Meaning | |---|---|---| -| `Z2UI5_DRAFT_TTL_HOURS` | `24` | how long a draft row is kept; `0` disables cleanup entirely | +| `Z2UI5_DRAFT_TTL_HOURS` | the framework's own `draft_exp_time_in_hours` (`4`) | how long a draft row is kept; `0` disables cleanup entirely | +| `Z2UI5_DRAFT_RETENTION_INSTANCE` | `0` | which Cloud Foundry instance runs the cleanup loop; `*` lets every instance run it | ```bash -Z2UI5_DRAFT_TTL_HOURS=4 npx cds watch # shorter retention +Z2UI5_DRAFT_TTL_HOURS=2 npx cds watch # shorter retention Z2UI5_DRAFT_TTL_HOURS=0 npx cds watch # keep everything (debugging) ``` -Anything unparseable falls back to 24 hours rather than disabling cleanup — +Anything unparseable falls back to the framework's own TTL rather than +disabling cleanup — a typo in the variable must not silently turn retention off. ### Option 2: DB-side job