npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cap2ui5/cds-plugin

v0.5.0

Published

abap2UI5 for CAP: a cds-plugin that hosts the transpiled abap2UI5 runtime, keeps its drafts in a CDS entity and lets apps be written as plain JavaScript classes

Readme

@cap2ui5/cds-plugin

abap2UI5 as a CAP plugin. Build SAPUI5 apps inside your CAP backend, as plain JavaScript classes — no frontend project, no manifest.json, no second build pipeline.

The framework underneath is not a port: it is abap2UI5's own ABAP, downported and transpiled by @abaplint/transpiler over open-abap and published by abap2UI5 as @abap2ui5/node-runtime. Its drafts are a CDS entity in your database, under your authorization; your apps read your entities with cds.ql.

Pre-release. The plugin is tested end to end (wire, restart, concurrency, browser) but the API can still move before 1.0.

Install

npm i @cap2ui5/cds-plugin
cds add cap2ui5      # optional: a first app in srv/apps/hello.js

Up to 0.2.0 the package was called cap2ui5. A project that has it swaps it - npm rm cap2ui5 && npm add @cap2ui5/cds-plugin - and its app modules require @cap2ui5/cds-plugin; everything it configured keeps its name.

Requires Node.js 22 or later and @sap/cds 9 or 10 (CI tests both, on Node 22 and 24).

That is the installation. On the next cds serve the roundtrip route (/rest/root/z2ui5, /sap/bc/z2ui5) exists - its page carries the whole UI5 frontend - and cds deploy creates cap2ui5.Drafts next to your own entities. Your server.js, if you have one, is untouched.

An app is one file

// srv/apps/books.js
import cds from "@sap/cds";
import { defineApp, t } from "@cap2ui5/cds-plugin";

const { SELECT } = cds.ql;

defineApp("BOOKS", class {
  search = "";
  books  = t.table({ ID: 0, title: "", author: "", price: t.packed(9, 2) });

  async main(client) {                  // async only because THIS app does I/O
    if (client.check_on_navigated()) {
      client.view_display(`<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" displayBlock="true" height="100%">
        <Page title="Books">
          <SearchField value="${client._bind("search")}" search="${client._event("SEARCH")}"/>
          <Table items="${client._bind("books")}"> … <Text text="{TITLE}"/> … </Table>
        </Page></mvc:View>`);
    } else if (client.check_on_event("SEARCH")) {
      const { Books } = cds.entities("my.bookshop");
      this.books = await SELECT.from(Books).where`title like ${"%" + this.search + "%"}`;
      client.message_toast_display(`${this.books.length} found`);
    }
  }
});

cds watch prints the address of every app of the project, one line for each package that brings apps (see Apps from a package), and the user to log in as:

[cap2ui5] - BOOKS             http://localhost:4004/sap/bc/z2ui5?app_start=BOOKS
[cap2ui5] - @cap2ui5/samples  71 apps - listed on CAP's start page, http://localhost:4004/
[cap2ui5] - development login: alice (empty password)

Only in development; a production profile prints neither. CAP's start page at / lists the same addresses under "Web Applications".

A project from cds init + cds add nodejs is an ES module project, hence import. In a CommonJS project, or in a .cjs file, require("@cap2ui5/cds-plugin") returns the same names.

The client is z2ui5_if_client, by its own names

main( client ) receives what an ABAP app's z2ui5_if_app~main( client ) receives, spelled the JavaScript way: client->check_app_prev_stack( ) is client.check_app_prev_stack(). A method's preferred parameter is its one positional argument, and parameters by name are one object with the ABAP names. The constants are there too. So an ABAP app ports line by line, and abap2UI5's documentation of a method is the documentation of the JavaScript one.

| ABAP | JavaScript | |---|---| | client->check_on_navigated( ) | client.check_on_navigated() | | client->check_on_event( `GO` ) | client.check_on_event("GO") | | client->get_event_arg( 1 ) | client.get_event_arg(1) | | client->_bind( name ), _bind( s_order-customer ) | client._bind("name"), client._bind("s_order-customer") | | client->_bind( val = t_tab path = abap_true ) | client._bind({ val: "t_tab", path: true }) | | client->_event( val = `GO` t_arg = VALUE #( ( `x` ) ) ) | client._event({ val: "GO", t_arg: ["x"] }) | | client->follow_up_action( val = z2ui5_if_client=>cs_event-set_title t_arg = … ) | client.follow_up_action({ val: z2ui5_if_client.cs_event.set_title, t_arg: [ … ] }) | | client->view_display( view->stringify( ) ) | client.view_display(view.stringify()) | | client->message_box_display( text = … type = `error` ) | client.message_box_display({ text: …, type: "error" }) | | client->nav_app_call( NEW zcl_other( ) ) | client.nav_app_call("ZCL_OTHER") | | client->nav_app_leave( event = … r_data = … ) | client.nav_app_leave({ event, r_data }) | | client->get( )-r_event_data | client.get().r_event_data |

Every method of the interface is there under its name - nav.test.mjs holds the client to the interface, so none is missing and none is invented. The constants are read from the runtime, and so is which of them there are: abap2UI5's page transitions - cs_transition, and view_display( )'s transition and transition_back - come with abap2UI5 1.146.0, the runtime this version pins; on an older runtime (1.145.0) a call that sets one is refused, naming the runtime, rather than dropped unseen:

client.view_display({ val: view.stringify(), transition: client.cs_transition.slide });

What is JavaScript's own, and why:

  • A field is bound by its name, client._bind("name"): ABAP's _bind( ) finds the attribute by reference, which a JavaScript value cannot carry. A cell of a table is { val: column, tab: "t_tab", tab_index: 2 }.
  • Some answers come after main( ). The framework's calls are asynchronous here, so main( ) stays synchronous by resolving what it can before it runs and the rest after it: what _event( ), a _bind( ) with options, and follow_up_action( ) in a view attribute return is a placeholder that becomes the wire after main( ) - embed it as it is. An async main works as well, for an app that does I/O.
  • client.get_app( id ) answers the app behind a draft id as a handle whose fields can be written - app.backend_event = "…", then client.nav_app_leave(app) - but not read: it is loaded after main( ). Reading the other app is client.get_app_prev( ), as plain values.
  • client.nav_app_call( app, fields ) presets the called app's fields, what an ABAP app does between NEW and nav_app_call( ).
  • client.set_session_stateful( ) is not supported and throws; the interface's obsolete *_model_update( ) do nothing, as they do in ABAP. (A transpiled ABAP app can go stateful: its session is kept per CAP user, in the process - a restart or a second instance loses it.) client.raw is the transpiled z2ui5_if_client itself, asynchronous.

check_on_navigated( ), not check_on_init( ), is the render branch. check_on_init( ) is the first roundtrip of this app instance; check_on_navigated( ) is also true every time the app gets the screen back — a called app leaving, a value help closing, a bookmark restored.

State: strings, numbers, booleans, t.packed(l, d), t.char(n), t.numc(n), t.date(), t.time(), a plain object (a structure), t.table({ …one row… }) — and those nest, up to 8 levels. Component names are UPPERCASE in the model. The whole instance is persisted to cap2ui5.Drafts after every roundtrip and rebuilt before the next, so state survives a restart. As in ABAP, only what the app binds is part of the model: a field goes to the browser, and the browser can write it, once main( ) has bound it with client._bind( ) (or _bind_edit( ), _bind_path( ), a component or a cell of it) - and stays bound for the rest of the session. A field no view binds - a price, a role flag, a counter - stays on the server, in the draft; a MODEL the browser sends for it is ignored. A helper that needs the client gets it as an ABAP app does, this.client = client in main( ), without declaring it as a field.

A field is an ABAP attribute, so its name is lower case - is_admin, not isAdmin, which defineApp( ) refuses: ABAP names are not case-sensitive and the runtime reads the attribute by its lower-case name. A structure's components and the app's methods may be camelCase. main( ) and its helpers run on a proxy that reads the fields as plain values, which a #private member does not reach - using one is refused with a message; a plain field is private enough, as it is not sent to the browser unless it is bound. t.bool( ) reads as a boolean, t.char(n) without its padding and t.float( ) as a number, as the plain true, "" and 0.5 do. t.date( ) and t.time( ) store and read YYYYMMDD and HHMMSS, as ABAP does, and take a cds.Date's YYYY-MM-DD and a cds.Time's HH:MM:SS as well; the model carries both as ISO, as the framework serializes a D and a T. null and undefined clear a field, as CLEAR does; a value a field cannot take - a text into a number, a string into a table, a JavaScript Date into a t.date( ) - is refused with the field's name.

Types: the package ships TypeScript declarations (index.d.ts). In a JavaScript app, annotate the client for completion and checked field names: /** @param {import("@cap2ui5/cds-plugin").Client<{ search: string }>} client */.

Building a view with z2ui5_cl_ui5_view_builder

A view is built the way an ABAP app builds one, with abap2UI5's own view builder, under its own name and called as the client is:

const { defineApp, z2ui5_cl_ui5_view_builder } = require("@cap2ui5/cds-plugin");

defineApp("HELLO", class {
  name = "";

  main(client) {

    if (client.check_on_navigated()) {

      const view = z2ui5_cl_ui5_view_builder.factory()
          .ele({ n: "View", ns: "mvc" })
              .a({ n: "xmlns", v: "sap.m" })
              .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" })

              .ele("Shell")
                  .ele("Page")
                      .a({ n: "title", v: "Hello" })

                      .tag("Input")
                          .a({ n: "value", v: client._bind("name") })
                      .tag("Text")
                          .a({ n: "text", t: "{shown as typed}" })
                      .tag("Button")
                          .a({ n: "text", v: "Go" })
                          .a({ n: "press", v: client._event("GO") });

      client.view_display(view.stringify());

    } else if (client.check_on_event("GO")) {

      client.message_box_display(`Hello ${this.name}`);

    }

  }
});

| | | |---|---| | z2ui5_cl_ui5_view_builder.factory() | an empty root; open the mvc:View and declare its xmlns yourself | | ele(n), ele({ n, ns }) | add a child element and descend into it | | tag(n), tag({ n, ns }) | add a child element and stay: the form for a leaf | | a({ n, v }), a({ n, b }), a({ n, t }) | an attribute on the element the chain points at: the child just added, or the node itself while it has none. Exactly one of v (written as it is: bindings, events, constant text), b (a boolean, rendered true/false) and t (text rendered literally, so a { in user input is shown rather than read as a binding) | | end() | ascend to the parent | | stringify() | the XML - rendered after main( ), so hand it to view_display( ) as it is; outside an app, await it | | z2ui5_cl_ui5_view_builder.escape_literal(val) | the literal escaping of t, for one part of a value that also carries a binding |

The chain is recorded while main( ) runs and rendered after it, by the transpiled z2ui5_cl_ui5_view_builder the runtime carries. The XML, its escaping and its refusals (an end() past the root, a duplicate attribute, an invalid name) are therefore exactly those of the same chain in an ABAP app. A refusal answers the roundtrip with the framework's error, naming the app. ViewBuilder is the same class, for code that prefers a JavaScript name.

An ABAP app, translated: cap2ui5 abap2js

Because the client and the view builder are abap2UI5's own, an abap2UI5 app class translates into a cap2UI5 app line for line - and the package does it:

npm add -D @abaplint/core      # once: the ABAP parser it reads with
npx --no-install cap2ui5 abap2js src/z2ui5_cl_my_app.clas.abap --out srv/apps

The parser, @abaplint/core, is an optional peer dependency of the plugin: 8 MB that only translating needs, so a project that serves apps does not install it. Without it the command, and abap2js( ) in code, say how to add it.

--no-install makes npx run the cap2ui5 of the project's @cap2ui5/cds-plugin or fail - never download one. Without it, npx cap2ui5 where the plugin is not installed fetches and runs whatever npm has under the unscoped name cap2ui5, and that name is no longer this package's. In a package.json script the command is cap2ui5 abap2js …, which runs the installed one as well.

z2ui5_cl_my_app.clas.abap becomes srv/apps/z2ui5_cl_my_app.js, registered as Z2UI5_CL_MY_APP, so ?app_start= is the same on both sides. The module starts where the ABAP statement starts and breaks where the ABAP breaks: a view chain keeps one call per line, VALUE #( ) one row per line, comments come along and texts are never touched. A directory translates every class in it.

| option | | |---|---| | --out <dir> | where the modules go, srv/apps by default | | --lib <dir> | where other classes a class names are read from - zcl_other=>ty_s_row (repeatable; the inputs' directories by default) | | --origin <text> | writes // @origin <text> <input path> into each module | | --esm, --cjs | the module format; by default what the package.json nearest to --out declares | | --check | writes nothing and exits 1 when a module is missing or would change - for CI |

It knows the part of ABAP an abap2UI5 app is written in - attributes and TYPES, VALUE #( ), COND/SWITCH, string templates, IF/CASE/DO, the client's and the view builder's calls - and refuses everything else with file, row and column (refused: z2ui5_cl_x.clas.abap:41:7 - LOOP AT ... ASSIGNING / REFERENCE INTO writes through the row - not supported yet) rather than guess. What it writes where JavaScript differs from ABAP:

  • _bind( s_order-customer ) is _bind("s_order-customer"); a _bind( ) of anything but an attribute or its component is refused.
  • nav_app_call( NEW zcl_other( ) ) is nav_app_call("ZCL_OTHER").
  • abap_bool is a boolean; where ABAP turns it into a string - a string template, CONV string( ), a t_arg row - it is "X" or "", as ABAP prints it.
  • TYPE p, n, d, t fields are t.packed( ), t.numc( ), t.date( ), t.time( ); a structure TYPES is a module constant, and INCLUDE TYPE of it that constant, spread.
  • A local ABAP scopes to the method and JavaScript would scope to a block - declared in a WHEN, or used after the IF/LOOP that declares it - is declared on top of the method, starting with the value ABAP gives it.
  • CONV string( ) of a number is String( ), without ABAP's trailing sign position ("0 ").

In code, abap2js(source, { file, lib, origin, format }) answers { name, code } and throws an Abap2jsError with file, row and col. How far "line for line" goes was measured on abap2UI5's samples: each translated sample served beside its original, transpiled, in one cap2UI5 server, and every roundtrip compared - view, model and actions.

Apps from a package

A package can bring apps with it: add it to the project, and its apps run beside the project's own - as abap2UI5's samples run in the system they are pulled into. npm add @cap2ui5/samples does exactly that with abap2UI5's samples.

The package says where its app modules are, in its package.json, and names the plugin as a peer dependency, so that its apps and the project's run on the same one:

"cap2ui5": { "apps": "srv/apps" },
"peerDependencies": { "@cap2ui5/cds-plugin": "^0.3.0" }

The range moves in lockstep with the plugin's minor version, and stays that narrow on purpose: ^0.3.0 is 0.3.x only, and while the plugin is 0.x a minor release may break the API an app is written against (the CHANGELOG says so, and the release after 0.3.1 does: only bound fields are sent to the browser, and a camelCase field is refused). A range like >=0.3.0 <1 would let npm install a plugin the package's apps have never run on, and they would break where they are used instead of at npm install. So a package with apps publishes a release for each plugin minor, with the range that names it; from 1.0 on, ^1.0.0 is the range to use. A range the project's plugin does not satisfy makes npm refuse the install or bring a second, nested copy of the plugin - which the log names.

The plugin finds such a package the way CAP finds its plugins: among the project's dependencies, and outside production (NODE_ENV other than production) among its devDependencies too - so npm add -D brings a package's apps to development only. Its directory is loaded like the project's own apps directory, after it, and the log names every package it loaded apps from. An app of a name the project has already stays the project's, and the log says so. A directory outside the package, or one the installed package does not have - a files entry forgotten - is skipped with a warning.

Agents: the MCP endpoint

AI agents - Claude Code, any client that speaks the Model Context Protocol over HTTP - can operate the project's apps: read a screen as data, fill its fields, fire its events. Through the app's own main( ), as the CAP user who calls, with that user's drafts and roles; there is no second API and no technical user. It is the same idea, the same four tools and the same answers as abap2UI5's MCP server (for the transpiled backend in development) and abap2UI5's ABAP agent addon (inside an SAP system).

It is off until the project switches it on:

"cds": { "requires": { "cap2ui5": { "agent": true } } }

| key under agent | default | | |---|---|---| | path | /rest/root/z2ui5/mcp | where the endpoint answers | | apps | none | apps the project opts in - names, or patterns with *; how a transpiled ABAP app opts in | | confirm | none | events an agent hands to a human: "EVENT" for every app, "APP:EVENT" for one, * as a wildcard | | forbidden | none | events an agent never fires, written the same way | | retention | 90 | days a row of the audit log cap2ui5.AgentLog is kept; 0 or false keeps every row - see Audit retention |

A key the endpoint does not know, or a value of the wrong type, is refused at start - "forbiden" would otherwise leave the event open, and a "retention": "90d" read as "forever" or as "now" would be a wrong guess for an audit log either way. Any object switches the endpoint on, so CDS_REQUIRES_CAP2UI5_AGENT_RETENTION=30 alone does too.

An app opts in where it is defined, and classifies its events there:

defineApp("ZCL_JS_BOOKS", class { /* ... */ }, {
  agent: { description: "Search the bookshop's books", events: { ADD: "confirm", "DELETE*": "forbidden" } },
});

agent: true opts in with every event allowed; agent: false keeps the app out whatever apps says - for an app that administers something. An event is allowed, confirm or forbidden; the app's word and the settings' are both applied and the stricter wins, so a project can tighten what an app allows but not loosen it. A setting "APP:EVENT" covers the app on the screen and the app the session was started with, so a framework popup an app calls (z2ui5_cl_pop_to_confirm) is covered by that app's rules.

Connecting a client - Claude Code, for one:

claude mcp add --transport http bookshop http://localhost:4004/rest/root/z2ui5/mcp \
  --header "Authorization: Basic $(printf '%s' 'alice:' | base64)"

In production the header is the token your identity provider issues (XSUAA, IAS) - pass it as a header, CAP does not implement MCP's own authorization discovery.

The tools

| tool | input | answer | |---|---|---| | app_list | filter? | { count, apps: [{ app, source: "app" \| "config", description? }], hint } | | app_start | app, values?, max_rows? (0-200, default 20) | snapshot | | app_describe | session, max_rows? | the snapshot of the last answer - no roundtrip | | app_act | session, values?, event?, args?, row?, max_rows? | the next snapshot |

The answer is agent snapshot v1, specified in the MCP server's docs/agent-snapshot.md: fields (id, model path, label, kind, value, editable, choices), actions (event, arguments, label, enabled, row scope), tables (columns, the first rows, selection), messages, texts, unsupported, and pending. It is not reimplemented here: the plugin carries the MCP server's own viewxml.mjs, snapshot.mjs and appclient.mjs, vendored unchanged at a recorded commit (lib/agent/vendor/, npm run agent-vendor), and adds one key - an action an app or the settings classify confirm or forbidden carries "policy", as in the ABAP addon. values address a field by id, model path or name, a table cell as "<table>/<row>/<COLUMN>"; event is an event name or an action id; values without an event stay pending, as typing does in the browser. Every refusal is a tool result with isError and a sentence naming what is allowed, and a refused act sends nothing.

Security model

  • Off by default, and an app is reachable only when it opted in or the settings name it.
  • The real user, always. The endpoint runs behind CAP's middlewares and the same roles guard as the roundtrip route: 401 with the auth strategy's challenge for anonymous callers, 403 for a user without the role. It admits authenticated users only, even where roles lets anybody into the UI. A tool call runs the route's own roundtrip handler in process, inside the MCP request, so the draft store binds every draft to that user, and the app's own checks see that user.
  • confirm and forbidden events are never fired by an agent. A confirm refusal answers the link that hands the screen to a human: <origin>/rest/root/z2ui5#/app/<APP>/<session>. Opened in the browser by the same user, the framework restores the session's draft - the state the agent prepared - and the human checks it and presses the button. Anybody else gets a fresh app: the draft store serves a draft to its owner only. Values the agent left pending are not in the draft; the refusal lists them.
  • Sessions are owner-scoped. Each user has a client of their own; a session id of another user's is "unknown", exactly as a missing one.
  • Audit: every call writes a row to cap2ui5.AgentLog - time, user, the MCP client's User-Agent, tool, app, the session in and out, event, arguments, the outcome (ok, refused, confirm, forbidden, error) and the refusal. The values an agent entered are never stored, only which fields it filled. Like cap2ui5.Drafts the entity is in the model and in no service; to show it, expose it in a service of your own with a @restrict such as where: 'owner = $user'. Rows older than retention days are deleted - see below.
  • Browser-side abuse is refused: a request whose Origin names another host gets 403, and only Content-Type: application/json is read (415), so a web page cannot drive the endpoint with a user's cookies.

Sessions and restarts

A session is the app's draft - in cap2ui5.Drafts, owned by the user, expiring with the draft expiry - plus what the client remembers of the screen: the views in their slots, the pending edits, the last answer app_describe reads. That memory is the process's (20 sessions per user), and a restart loses it while the drafts survive. So a session the server no longer knows is restored from its draft, the way the handover link restores it for a human: the framework renders the app from the draft (check_on_navigated( ) is true). app_describe answers the restored screen, under a new session id; app_act sends nothing and names the new id, since the screen the agent chose its action on is not in front of it now. Edits pending before the restart are lost, as a browser tab's are. Only a session the audit log shows the same user's agent reached, which is the newest of its line and whose draft has not expired, is restored.

Audit retention

A row of cap2ui5.AgentLog is kept retention days - 90 unless the setting says otherwise - and then deleted, the way the draft store expires drafts: one DELETE of everything older than a cutoff, run on the way of an ordinary request rather than by a timer of the plugin's own. The drafts are swept on every app start; the log is swept by an agent call, at most once an hour per process and tenant, so the endpoint pays for one DELETE an hour. A sweep that fails is logged as a warning and tried again an hour later; it never fails the agent's call.

"retention": 0 (or false) keeps every row. A project that prefers a scheduled job of its own sets that, so the endpoint leaves the log alone, and runs the same DELETE from the job:

const { purgeAgentLog } = require("@cap2ui5/cds-plugin");
const deleted = await purgeAgentLog({ days: 90 });   // without `days`: the setting's retention, else 90

It deletes every user's rows older than the cutoff and answers how many. It runs in cds.context's transaction where there is one - in a multitenant application, call it per tenant inside cds.tx({ tenant }, ...) (the endpoint's own sweep follows the caller's tenant).

A session is restored after a restart only from its audit rows, so a retention shorter than the draft expiry (draft_exp_time_in_hours, 4 hours unless the user exit changes it) would lose sessions whose drafts are still there - which takes an expiry of weeks.

Configure

Under cds.requires.cap2ui5 - in package.json, a .cdsrc.json, a profile or CDS_REQUIRES_CAP2UI5_* environment variables, like any CAP setting:

"cds": {
  "requires": {
    "cap2ui5": { "roles": ["admin"], "body_parser": { "limit": "20mb" } }
  }
}

| key | default | | |---|---|---| | apps | srv/apps | the directory scanned for app modules | | roles | ["authenticated-user"] | who may call: a role, or a list of roles any one of which lets the user in, as with CAP's @requires; any or null allows anonymous callers | | routes | /sap/bc/z2ui5, /rest/root/z2ui5 | where the roundtrip answers | | body_parser.limit | CAP's cds.server.body_parser.limit, else 10mb | the largest roundtrip body; a larger one gets 413 | | compression | true | gzip for the page and the roundtrips, where the browser accepts it; false leaves compressing to a proxy in front | | accelerate | true | calls the runtime's accelerate( ) where it has one - the releases after 1.145.0, see Performance; false runs the runtime's own code | | agent | off | the MCP endpoint for AI agents - true or { path, apps, confirm, forbidden, retention }, see Agents |

"cap2ui5": false switches the plugin off: no route, and no cap2ui5.Drafts or cap2ui5.AgentLog table in the model.

The route runs behind CAP's own middlewares and answers like a CAP service. Whatever cds.requires.auth is configured to identifies the user. A caller who is not logged in gets 401 with that strategy's login challenge. A user who lacks the role gets 403.

0.1.0 read its settings from a top-level cds.cap2ui5, with requires for the roles. Those settings still apply, and the log names the new place.

Everything the framework decides about a response — the Content-Security-Policy, the security headers, the UI5 bootstrap URL, the theme, the draft expiry, the CSRF gate — comes from the user exit, defineExit({ onPage, onRoundtrip }), one per project.

In production

  • Who may call: set roles to the roles your identity provider grants (XSUAA scopes, IAS groups). The default, authenticated-user, is CAP's own default for production.
  • Behind the approuter: route the paths in routes to the CAP backend with the approuter's authentication, like the service paths. The approuter protects routes with an X-CSRF-Token by default (csrfProtection). @abap2ui5/node-runtime 1.145.0, the runtime this version pins, does not fetch that token yet, so set "csrfProtection": false on these routes. The framework runs its own CSRF check: it compares Origin/Referer with the host, and the user exit configures it. The token handshake comes with the abap2UI5 release after 1.145.0 (abap2UI5#2802).
  • Body size: a roundtrip carries the app's whole model. cds.server.body_parser.limit, CAP's global limit, applies here too; body_parser.limit above overrides it for this route.
  • Compression: the route gzips what it answers where the browser accepts gzip - the page and every roundtrip of 1 kB or more - as the framework asks the ICF to on an ABAP system. Behind an approuter or an ingress that compresses too, nothing is compressed twice: it finds Content-Encoding set and passes the body on. "compression": false leaves the work to it and saves the CPU here.
  • Logs: the plugin logs through cds.log('cap2ui5'), so production gets JSON records with the request's correlation id. Set the level with cds.log.levels.cap2ui5.
  • Database: cap2ui5.Drafts and cap2ui5.AgentLog are part of the model, so cds deploy and cds build --production create them like any other table (.hdbtable for SAP HANA). The agent log's table is there whether the endpoint is on or not - the database is built from the model where cds build runs, the endpoint is switched on where the server runs, often by an environment variable the build never sees; ADR-009 has the reasons. It is written only while the endpoint is on, and its rows are deleted after agent.retention days (90). A draft is deleted after the user exit's draft_exp_time_in_hours, 4 hours unless the exit changes it.
  • Multitenancy (MTX): not tested yet. The draft store reads and writes through cds.run, which follows cds.context. The drafts should therefore land in each tenant's database like any other row, but no test proves it.

Performance

A roundtrip carries the app's model: the framework restores it from the draft, runs the app and answers it whole. So what a roundtrip costs grows with the model - one editable table of n rows, measured with npm run bench -- --rows <n> in this repository (Node 22, a shared 4-core machine; take the ratios, not the seconds):

| rows | answer, plain / gzip | start / edit one cell, 1.145.0 | with the runtime's accelerations | |---:|---|---|---| | 500 | 45 / 6 kB | 2.7 s / 4.3 s | 0.9 s / 1.4 s | | 1000 | 90 / 11 kB | 6.0 s / 12.1 s | 0.6 s / 1.4 s | | 2000 | 181 / 21 kB | 19.6 s / 43.5 s | 1.6 s / 2.9 s | | 8000 | 727 / 82 kB | - | 3.3 s / 16.4 s |

  • The runtime's accelerations. On 1.145.0 the time grows with n², not in abap2UI5's ABAP but in two places of @abaplint/runtime: a LOOP ... WHERE over a sorted primary key reads every row, and CP rescans the rest of the draft's XML for every token the parser reads. @abap2ui5/node-runtime fixes both with accelerate( ) from the release after 1.145.0 - the right column, measured before that release - and the plugin calls it when the server starts; the log then says runtime accelerations active. "accelerate": false switches them off.
  • Node 24 is recommended. CAP keeps cds.context in an AsyncLocalStorage, and on Node 22 that costs the transpiled framework - which awaits at nearly every method call - about as much time again as its own work. Node 24 keeps it in the async context frame, which costs next to nothing. On Node 22.7 and later, NODE_OPTIONS=--experimental-async-context-frame does the same: measured back to back, it halved the times in the table, with the accelerations and without (2000 rows, an edit with them: 3.4 s, then 2.0 s).
  • NODE_COMPILE_CACHE. The runtime is about 800 transpiled modules, and importing them takes 0.7 s on every start. With NODE_COMPILE_CACHE set to a directory, Node keeps their compiled code there and the import takes 0.5 s - worth it for cds watch, which restarts on every save.
  • Compression. The page carries the whole UI5 frontend, 358 kB - 83 kB gzipped - and the route gzips it and every roundtrip of 1 kB or more where the browser accepts gzip (see In production).

Documentation

cap2ui5.github.io/docs — the guide, the API reference, the examples and the architecture, including why the exit is registered rather than discovered and where UI5 itself comes from.

Support

Bugs and questions: github.com/cap2UI5/cap2UI5/issues. What changed between versions: CHANGELOG.md.

License

MIT