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/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ff4181d --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,460 @@ +# 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`.* + +> [!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 + +**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.* + +--- + +## 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/.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/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 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);