Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ VitePress build. It is also what CI runs, on every pull request
- every `require("@cap2ui5/cds-plugin")` or `import { … } from
"@cap2ui5/cds-plugin"` **inside a code fence** names only what the package
really exports (and two dead packages are reported, in either form: the
port's `abap2UI5/…` and the plugin's withdrawn old name `cap2ui5`),
port's `abap2UI5/…` and the plugin's old name `cap2ui5`, a deprecated placeholder on npm),
- every `cds.requires.cap2ui5.<option>` is an option the plugin defines (and
0.1.0's `cds.cap2ui5.<option>` is reported as the deprecated place),
- every `1.x.y` release number is the pinned runtime release,
Expand Down Expand Up @@ -62,12 +62,18 @@ against the repos, don't guess):
| [abap2UI5/abap2UI5](https://github.com/abap2UI5/abap2UI5) | the framework itself, in ABAP. Downported and transpiled, it is published as `@abap2ui5/node-runtime` |
| [cap2UI5/docs](https://github.com/cap2UI5/docs) | this site |

On npm since 2026-09-29: `@cap2ui5/cds-plugin@0.3.1` (Node ≥ 22, peer
On npm: `@cap2ui5/cds-plugin@0.4.0` (since 2026-09-30; Node ≥ 22, peer
`@sap/cds` ≥ 9), published by the npm organisation `cap2ui5`, and
`@cap2ui5/samples@0.1.0`. The plugin pins `@abap2ui5/node-runtime@1.145.0`
(on npm since 2026-09-27) **exactly**. Up to 0.2.0 the plugin was the unscoped
package `cap2ui5`; it is withdrawn from npm (0.2.0 never reached it), so the
site names it only as the old name. What did NOT change name: the
`@cap2ui5/samples@0.2.0` (peer `^0.4.0`). The plugin pins
`@abap2ui5/node-runtime@1.146.0` **exactly**, the first runtime whose frontend
does the approuter's `X-CSRF-Token` handshake and has `accelerate( )`.
verify-refs reads that pin from `plugin/package.json`, not from the stand-in
`runtime/package.json`. Up to 0.2.0 the plugin was the unscoped
package `cap2ui5`. On npm that name is now only a deprecated placeholder,
`cap2ui5@0.0.1-placeholder` (since 2026-09-30, cap2UI5/cap2UI5#97): no `main`,
and a `cap2ui5` bin that prints where to go and exits 1. It installs nothing an
app can use (0.2.0 never reached npm), so the site names it only as the old
name. What did NOT change name: the
configuration `cds.requires.cap2ui5`, `cds add cap2ui5`, the bin `cap2ui5`
(`npx --no-install cap2ui5 abap2js`), the entity `cap2ui5.Drafts` and the
logger `cap2ui5`. The runtime package was renamed from
Expand Down Expand Up @@ -140,8 +146,8 @@ against the cap2UI5 repository.
be given a host-side registration (`defineExit`) instead. Boot the runtime
and check rather than porting a claim from abap2UI5's documentation.
- Write the translator as `npx --no-install cap2ui5 abap2js`, never a bare
`npx cap2ui5`. The unscoped npm name `cap2ui5` was withdrawn and is
anybody's now; a bare `npx cap2ui5` where the plugin is not installed
`npx cap2ui5`. The unscoped npm name `cap2ui5` holds only a placeholder;
a bare `npx cap2ui5` where the plugin is not installed
downloads and runs whatever npm has under it. `--no-install` runs the
project's own `@cap2ui5/cds-plugin` bin or fails. A page that shows the
command says it needs the plugin installed. Left as they are: `cds add
Expand Down
6 changes: 6 additions & 0 deletions HANDOVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ The reasoning behind all of it is in [ROADMAP.md](ROADMAP.md) §§8–25 and in

## Open — the approuter and CSRF

> **2026-09-30:** resolved in code. `@cap2ui5/cds-plugin` 0.4.0 pins runtime
> 1.146.0, which carries `5a1bd70`; the deployment page now says the generated
> catch-all route works, and keeps the extra route only for 0.3.x. What is left
> is to run it once behind an approuter bound to XSUAA or IAS — it is read from
> both sides' code, not measured. The text below is the state before.

`cds add approuter` generates a catch-all route with `"csrfProtection": true`,
and the frontend in runtime 1.145.0 sends no `X-CSRF-Token`, so behind that
route every roundtrip gets 403. The site documents the working setup — an
Expand Down
1 change: 1 addition & 0 deletions docs/.verify-refs-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ my_first_app # the class the getting-started walkthrough has you writ
my_app_name # stands for "your class" in the ?app_start= URL on the navigation page
zcl_my_app # stands for "your app class": the name you gave defineApp in the why-cap2UI5 pitch, and the ABAP class on the views page — an ABAP app is registered by its transpiled class, not by defineApp
ClassName # literal placeholder in the URL-parameter description
z2ui5_cl_smp_app_493 # abap2UI5's hello-world sample: a class in abap2UI5/samples and @cap2ui5/samples, not in abap2UI5's src

# --- apps of other repositories -------------------------------------------
# Registered with defineApp in their own repository, not in cap2UI5 - and apps,
Expand Down
25 changes: 21 additions & 4 deletions docs/api/app-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,27 @@ The wrapper awaits it either way.

### State is the fields

Every field with an initial value becomes part of the model and survives the
roundtrip. A field the plugin cannot type is left out and **named in a
warning**, never silently dropped. Unlike ABAP there is no `PROTECTED
SECTION`: every field is model.
Every field with an initial value is kept in the draft and survives the
roundtrip. It reaches the browser only once the app **binds** it —
`_bind()`, `_bind_edit()`, `_bind_path()`, a component or a cell — and from
then on it stays bound, as the framework does for an ABAP app. So a field no
view shows is neither sent to the browser nor taken back from it; there is no
`PROTECTED SECTION` to hide it, and none is needed. A field the plugin cannot
type is left out and **named in a warning**, never silently dropped.

Three names `defineApp` refuses, each with a message that says what to write
instead:

- **a camelCase field** — `isAdmin = false`. A field is an ABAP attribute, and
the runtime reads it by its lower-case name; write `is_admin`. Components of
a structure and the class's methods may be camelCase.
- **a `#private` member** used in `main()` or a method it calls. They run on a
proxy of the instance, which a private name does not reach. Use a plain
field — it is not sent to the browser unless bound — or a module-level
function.
- **an app name the runtime already has a class of** — the framework's own,
one of its apps, the plugin's. `defineApp("Z2UI5_CL_UTIL", …)` would replace
what every roundtrip runs.

Inside `main` and inside any method it calls, fields read and write as plain
values. A helper method that needs the client gets it as an ABAP app does —
Expand Down
18 changes: 9 additions & 9 deletions docs/examples/selection-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const { SELECT } = cds.ql;

defineApp("ZCL_ORDERS", class {
customer = "";
minTotal = 0;
onlyOpen = false;
min_total = 0;
only_open = false;
rows = t.table({ ID: 0, customer: "", total: t.packed(11, 2), open: false });
hits = 0;

Expand All @@ -33,8 +33,8 @@ defineApp("ZCL_ORDERS", class {
`<f:SimpleForm editable="true" layout="ResponsiveGridLayout">` +
`<f:content>` +
`<Label text="Customer"/><Input value="${client._bind("customer")}"/>` +
`<Label text="Minimum total"/><Input value="${client._bind("minTotal")}"/>` +
`<Label text="Open only"/><CheckBox selected="${client._bind("onlyOpen")}"/>` +
`<Label text="Minimum total"/><Input value="${client._bind("min_total")}"/>` +
`<Label text="Open only"/><CheckBox selected="${client._bind("only_open")}"/>` +
`</f:content></f:SimpleForm>` +

`<Button text="Go" type="Emphasized" press="${client._event("GO")}"/>` +
Expand All @@ -54,8 +54,8 @@ defineApp("ZCL_ORDERS", class {
if (client.check_on_event("GO")) {
const { Orders } = cds.entities("my.shop");
let q = SELECT.from(Orders).where`customer like ${"%" + this.customer + "%"}`;
if (this.minTotal) q = q.and`total >= ${this.minTotal}`;
if (this.onlyOpen) q = q.and`open = ${true}`;
if (this.min_total) q = q.and`total >= ${this.min_total}`;
if (this.only_open) q = q.and`open = ${true}`;

this.rows = await q;
this.hits = this.rows.length;
Expand All @@ -78,8 +78,8 @@ needs no `client.view_display()`. Render again only when the view's
| field | declared as | why |
|---|---|---|
| `customer` | `""` | `string` |
| `minTotal` | `0` | integer. A decimal threshold would be `t.packed(11, 2)` |
| `onlyOpen` | `false` | `abap_bool`; your code still sees `true`/`false` |
| `min_total` | `0` | integer. A decimal threshold would be `t.packed(11, 2)` |
| `only_open` | `false` | `abap_bool`; your code still sees `true`/`false` |
| `rows` | `t.table({…})` | an empty array carries no type |

A `CheckBox` binds `selected`, an `Input` binds `value` — ordinary UI5.
Expand All @@ -93,7 +93,7 @@ name containing a quote is a value and not a syntax error.

## Keeping the criteria

They persist for free. `customer`, `minTotal` and `onlyOpen` are declared
They persist for free. `customer`, `min_total` and `only_open` are declared
fields, so they are in the draft: come back to the app after a navigation and
the form is still filled in — including after a server restart.

Expand Down
9 changes: 6 additions & 3 deletions docs/guide/data-binding.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Data Binding

A field of your class is a bound model field. `client._bind("name")` gives you
the binding path to put in the view; the browser sends the value back, and the
framework applies it to the instance before your `main` runs.
A field of your class becomes a model field when the app binds it.
`client._bind("name")` gives you the binding path to put in the view; from
then on the field is sent to the browser, the browser sends the value back,
and the framework applies it to the instance before your `main` runs. A field
the app never binds stays on the server — kept in the draft, never sent, never
overwritten by what a browser posts.

```js
defineApp("ZCL_HELLO", class {
Expand Down
9 changes: 5 additions & 4 deletions docs/guide/ecosystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ abap2UI5 builds itself.
| `@abap2ui5/node-runtime` | abap2UI5: upstream's ABAP, downported and transpiled over open-abap, with the UI5 frontend embedded in the page its GET answers with — backend and frontend from one commit |
| `@cap2ui5/samples` | abap2UI5's samples as cap2UI5 apps. Optional: added to a project, they run beside its own apps — see [Apps from a package](./project-structure#apps-from-a-package) |

All three are on npm: `@cap2ui5/cds-plugin` 0.3.1 (Node ≥ 22, `@sap/cds` ≥ 9 as a peer),
`@abap2ui5/node-runtime` 1.145.0 (Node ≥ 22), which the plugin pins exactly,
and `@cap2ui5/samples` 0.1.0. `npm add @cap2ui5/cds-plugin` installs the first two — see the
All three are on npm: `@cap2ui5/cds-plugin` 0.4.0 (Node ≥ 22, `@sap/cds` ≥ 9 as a peer),
`@abap2ui5/node-runtime` 1.146.0 (Node ≥ 22), which the plugin pins exactly,
and `@cap2ui5/samples` 0.2.0, which needs plugin 0.4. `npm add @cap2ui5/cds-plugin` installs the first two — see the
[Quickstart](./getting-started). Up to 0.2.0 the plugin was the unscoped
package `cap2ui5`, which is withdrawn from npm.
package `cap2ui5`. On npm that name is now only a deprecated placeholder that
installs nothing usable; its `cap2ui5` command prints where to go instead.

## What the plugin does and does not own

Expand Down
15 changes: 11 additions & 4 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ Your own `server.js`, if you have one, is not touched. Nothing is generated
into your repository, and there are no frontend files to serve.

::: info Coming from the package `cap2ui5`
Up to 0.2.0 the plugin was the unscoped package `cap2ui5`, which is withdrawn
from npm. A project that has it swaps it:
Up to 0.2.0 the plugin was the unscoped package `cap2ui5`. On npm that name is
now only a deprecated placeholder that installs nothing usable. A project that
has it swaps it:

```bash
npm rm cap2ui5 && npm add @cap2ui5/cds-plugin
Expand Down Expand Up @@ -298,8 +299,14 @@ npm add -D @cap2ui5/samples
cds watch
```

The startup lines now list every sample beside `HELLO` and `BOOKS`, each under
its ABAP class name. As a devDependency the samples are there in development
The startup lines gain one line for the package, below `HELLO` and `BOOKS`:

```
[cap2ui5] - @cap2ui5/samples 71 apps - listed on CAP's start page, http://localhost:4004/
```

CAP's start page lists every sample under its ABAP class name, e.g.
`Z2UI5_CL_SMP_APP_493`, abap2UI5's hello world. As a devDependency the samples are there in development
only; a production start leaves them out. How a package brings apps is in
[Project Structure](./project-structure#apps-from-a-package), the list of
samples in the [cap2UI5/samples](https://github.com/cap2UI5/samples)
Expand Down
8 changes: 6 additions & 2 deletions docs/guide/migration-from-abap2ui5.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ fields can be written — `app.backend_event = "…"`, then
**`client.nav_app_call( app, fields )`** presets the called app's fields —
what an ABAP app does between `NEW` and `nav_app_call( )`.

**Every field is model.** There is no `PROTECTED SECTION`: every field with an
initial value is part of the model. A helper method that needs the client gets
**A bound field is model.** Every field with an initial value is kept in the
draft, and one the app binds is sent to the browser and written back — as
abap2UI5 does for an ABAP app's attributes. There is no `PROTECTED SECTION`,
and none is needed to keep a field out of the browser: don't bind it. Fields
are named in snake_case, as ABAP attributes are — `defineApp` refuses a
camelCase one. A helper method that needs the client gets
it as in ABAP, `this.client = client` in `main()`, without declaring it as a
field.

Expand Down
3 changes: 2 additions & 1 deletion docs/guide/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ defineApp("ZCL_ORDER", class {
});
```

All three survive. Structures and tables nest as deeply as you like — the model
All three survive — every declared field is kept in the draft, bound or not;
only what the app binds is also sent to the browser. Structures and tables nest as deeply as you like — the model
carries `ORDER.CUSTOMER.CITY` and `ORDER.LINES[].PRICE`, decimals included, and
the app reads the whole tree back as plain values on a later roundtrip.

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ same one:
```json
{
"cap2ui5": { "apps": "srv/apps" },
"peerDependencies": { "@cap2ui5/cds-plugin": "^0.3.0" }
"peerDependencies": { "@cap2ui5/cds-plugin": "^0.4.0" }
}
```

Expand Down
23 changes: 10 additions & 13 deletions docs/guide/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@ exported `z2ui5_cl_ui5_view_builder` and shipped TypeScript declarations;
0.3.0 added `npx --no-install cap2ui5 abap2js`, which translates an abap2UI5
app class into a cap2UI5 app line for line, and apps that come from a package —
[`@cap2ui5/samples`](https://github.com/cap2UI5/samples) is 71 of abap2UI5's
samples that way. The details are in the plugin's
samples that way. 0.4.0 sends the browser only the fields an app binds, gzips
the page and the roundtrips, and hosts runtime 1.146.0, whose frontend does the
approuter's CSRF token handshake and whose accelerations make a table of
thousands of rows usable. The details are in the plugin's
[CHANGELOG](https://github.com/cap2UI5/cap2UI5/blob/main/plugin/CHANGELOG.md).

## Known limits today

### Behind an approuter, the roundtrip path needs its own route
### Not yet run on BTP

The frontend in runtime `1.145.0` sends no `X-CSRF-Token`, and the route
`cds add approuter` generates demands one on every POST — so every roundtrip
is refused with `403` until the roundtrip path gets a route of its own with
`"csrfProtection": false`. The route, and why it is safe, are on
[Deployment](../reference/deployment#the-approuter-needs-one-extra-route-today).
abap2UI5/abap2UI5#2802 teaches the frontend the token handshake; it is merged,
but no abap2UI5 release carries it yet.
Behind an approuter, the route `cds add approuter` generates should work as it
is since 0.4.0: the frontend in runtime 1.146.0 does the `X-CSRF-Token`
handshake the approuter asks for. That is read from both sides' code, not yet
run end to end against XSUAA or IAS — see
[Deployment](../reference/deployment#the-approuter-and-its-csrf-token).

### The apps are JavaScript, and so are the errors

Expand Down Expand Up @@ -76,10 +77,6 @@ background pages may still carry the port's mechanics; if a page contradicts

## What's next

**Drop the approuter exception.** Once abap2UI5/abap2UI5#2802 is in an
upstream release, pin the plugin to it, and the route CAP generates works
unchanged.

**Teach `abap2js` more ABAP.** Some of its refusals say "not supported yet".

**Finish this documentation.**
Expand Down
9 changes: 5 additions & 4 deletions docs/guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,11 @@ refused without the payload being buffered. To open the route deliberately, set

The page loads, the first click fails with `403`, and the response carries
`x-csrf-token: Required`. That is the approuter, not the plugin: the route
`cds add approuter` generates has `"csrfProtection": true`, and the abap2UI5
frontend in the pinned runtime sends no CSRF token. Give the roundtrip path a
route of its own with `"csrfProtection": false` — the exact route, and why it
is safe, are in [Deployment](../reference/deployment#the-approuter-needs-one-extra-route-today).
`cds add approuter` generates has `"csrfProtection": true`. Since plugin 0.4.0
the frontend fetches and sends the token itself, so this means an older plugin:
the frontend in the runtime 0.3.x pins sends none. Upgrade to
0.4.0, or give the roundtrip path a route of its own — see
[Deployment](../reference/deployment#the-approuter-and-its-csrf-token).

## Two users see each other's state

Expand Down
Loading
Loading