From 7f757654b658d049564f1725f54fde781575e304 Mon Sep 17 00:00:00 2001 From: udo Date: Thu, 11 Jun 2026 13:44:24 +0000 Subject: [PATCH] fix: harden UCE runtime and starter --- README.md | 10 +- RECOMMENDATIONS.md | 492 + WASM-PROPOSAL.md | 496 + docs/patches/sqlite-3.46.1-vendor.md | 34 + docs/react-developer-affordances-todo.md | 147 + scripts/build_linux.sh | 15 +- site/demo/collections.uce | 41 + site/demo/index.uce | 2 + site/demo/sqlite.uce | 54 + site/doc/areas/ob.txt | 1 + site/doc/areas/sqlite.txt | 8 + site/doc/areas/string.txt | 6 + site/doc/areas/types.txt | 7 + site/doc/areas/uri.txt | 8 + site/doc/pages/0_Request.txt | 449 +- site/doc/pages/3_C++ Preprocessor.txt | 16 + site/doc/pages/coming_from_react.txt | 69 + site/doc/pages/dtree_filter.txt | 26 + site/doc/pages/dtree_group_by.txt | 26 + site/doc/pages/dtree_keys.txt | 26 + site/doc/pages/dtree_map.txt | 26 + site/doc/pages/dtree_omit.txt | 26 + site/doc/pages/dtree_pick.txt | 26 + site/doc/pages/dtree_values.txt | 26 + site/doc/pages/list_every.txt | 26 + site/doc/pages/list_filter.txt | 18 + site/doc/pages/list_find.txt | 26 + site/doc/pages/list_map.txt | 19 + site/doc/pages/list_some.txt | 26 + site/doc/pages/list_sort.txt | 26 + site/doc/pages/list_unique.txt | 26 + site/doc/pages/map.txt | 31 + site/doc/pages/mysql_query.txt | 11 +- site/doc/pages/parse_query.txt | 21 + site/doc/pages/request_base_url.txt | 16 + site/doc/pages/request_context_params.txt | 28 + site/doc/pages/request_query_path.txt | 27 + site/doc/pages/request_query_route.txt | 29 + site/doc/pages/request_script_url.txt | 16 + site/doc/pages/route_path_is_safe.txt | 24 + site/doc/pages/route_path_normalize.txt | 20 + site/doc/pages/route_path_sanitize.txt | 34 + site/doc/pages/sqlite_affected_rows.txt | 19 + site/doc/pages/sqlite_connect.txt | 28 + site/doc/pages/sqlite_disconnect.txt | 14 + site/doc/pages/sqlite_error.txt | 15 + site/doc/pages/sqlite_insert_id.txt | 19 + site/doc/pages/sqlite_query.txt | 37 + site/examples/uce-starter/README.md | 18 +- .../components/auth/oauth-client.uce | 4 +- .../components/basic/cookie-consent.uce | 1 - .../uce-starter/components/basic/notfound.uce | 15 + .../uce-starter/components/data/widgets.uce | 17 +- .../components/example/theme-switcher.uce | 5 +- .../components/gauges/arcgauge.uce | 6 +- .../components/gauges/needlegauge.uce | 6 +- .../components/gauges/progressbar.uce | 6 +- .../components/theme/account_links.uce | 9 +- .../uce-starter/components/theme/assets.uce | 39 + .../uce-starter/components/theme/footer.uce | 5 - .../components/theme/global_controls.uce | 4 - .../uce-starter/components/theme/head.uce | 13 +- .../components/theme/page_shell.uce | 20 - .../components/theme/standard_nav.uce | 6 +- .../components/theme/web_affordances.uce | 18 + .../components/workspace/primitives.uce | 8 +- site/examples/uce-starter/config/settings.uce | 6 + site/examples/uce-starter/index.uce | 62 +- site/examples/uce-starter/js/u-macrobars.js | 2 +- site/examples/uce-starter/lib/app.uce | 385 +- .../uce-starter/themes/common/page.blank.uce | 3 +- .../uce-starter/themes/common/page.json.uce | 8 +- .../uce-starter/themes/dark/page.html.uce | 11 +- .../uce-starter/themes/light/page.html.uce | 11 +- .../themes/localfirst/page.html.uce | 25 +- site/examples/uce-starter/themes/page.uce | 22 + .../themes/portal-dark/page.html.uce | 11 +- .../themes/portal-light/page.html.uce | 11 +- .../themes/retro-gaming/page.html.uce | 11 +- .../uce-starter/views/account/login.uce | 9 +- .../uce-starter/views/account/logout.uce | 5 +- .../uce-starter/views/account/profile.uce | 9 +- .../uce-starter/views/account/register.uce | 7 +- .../uce-starter/views/auth/callback.uce | 13 +- site/examples/uce-starter/views/auth/demo.uce | 11 +- .../views/auth/store-oauth-session.uce | 15 +- site/examples/uce-starter/views/dashboard.uce | 11 +- site/examples/uce-starter/views/features.uce | 9 +- site/examples/uce-starter/views/gauges.uce | 6 +- site/examples/uce-starter/views/index.uce | 20 +- site/examples/uce-starter/views/page1.uce | 9 +- .../uce-starter/views/page2-section1.uce | 5 +- site/examples/uce-starter/views/page2.uce | 7 +- .../uce-starter/views/theme-preview.uce | 14 +- site/examples/uce-starter/views/themes.css | 33 - site/examples/uce-starter/views/themes.uce | 22 +- .../uce-starter/views/workspace/index.uce | 19 +- site/index.uce | 3 +- site/tests/core.uce | 45 +- site/tests/http.uce | 4 +- site/tests/index.uce | 32 +- site/tests/manifest.txt | 17 + site/tests/preprocessor.uce | 7 + site/tests/services.uce | 11 + site/tests/sqlite.uce | 93 + site/tests/testlib.h | 23 + src/3rdparty/sqlite/sqlite3.c | 257679 +++++++++++++++ src/3rdparty/sqlite/sqlite3.h | 13425 + src/3rdparty/sqlite/sqlite3ext.h | 719 + src/lib/compiler-parser.cpp | 113 + src/lib/compiler.cpp | 121 +- src/lib/compiler.h | 2 + src/lib/dtree.cpp | 9 +- src/lib/dtree.h | 4 +- src/lib/functionlib.cpp | 253 +- src/lib/functionlib.h | 27 +- src/lib/mysql-connector.cpp | 79 +- src/lib/mysql-connector.h | 2 + src/lib/sqlite-connector.cpp | 353 + src/lib/sqlite-connector.h | 34 + src/lib/types.h | 1 + src/lib/uce_lib.cpp | 7 +- src/lib/uce_lib.h | 1 + src/lib/uri.cpp | 167 +- src/lib/uri.h | 11 + src/linux_fastcgi.cpp | 100 +- tests/plugins/uce_http_smoke.py | 19 + tests/plugins/uce_site_suite.py | 126 +- 128 files changed, 276200 insertions(+), 872 deletions(-) create mode 100644 RECOMMENDATIONS.md create mode 100644 WASM-PROPOSAL.md create mode 100644 docs/patches/sqlite-3.46.1-vendor.md create mode 100644 docs/react-developer-affordances-todo.md create mode 100644 site/demo/collections.uce create mode 100644 site/demo/sqlite.uce create mode 100644 site/doc/areas/sqlite.txt create mode 100644 site/doc/pages/coming_from_react.txt create mode 100644 site/doc/pages/dtree_filter.txt create mode 100644 site/doc/pages/dtree_group_by.txt create mode 100644 site/doc/pages/dtree_keys.txt create mode 100644 site/doc/pages/dtree_map.txt create mode 100644 site/doc/pages/dtree_omit.txt create mode 100644 site/doc/pages/dtree_pick.txt create mode 100644 site/doc/pages/dtree_values.txt create mode 100644 site/doc/pages/list_every.txt create mode 100644 site/doc/pages/list_filter.txt create mode 100644 site/doc/pages/list_find.txt create mode 100644 site/doc/pages/list_map.txt create mode 100644 site/doc/pages/list_some.txt create mode 100644 site/doc/pages/list_sort.txt create mode 100644 site/doc/pages/list_unique.txt create mode 100644 site/doc/pages/map.txt create mode 100644 site/doc/pages/request_base_url.txt create mode 100644 site/doc/pages/request_context_params.txt create mode 100644 site/doc/pages/request_query_path.txt create mode 100644 site/doc/pages/request_query_route.txt create mode 100644 site/doc/pages/request_script_url.txt create mode 100644 site/doc/pages/route_path_is_safe.txt create mode 100644 site/doc/pages/route_path_normalize.txt create mode 100644 site/doc/pages/route_path_sanitize.txt create mode 100644 site/doc/pages/sqlite_affected_rows.txt create mode 100644 site/doc/pages/sqlite_connect.txt create mode 100644 site/doc/pages/sqlite_disconnect.txt create mode 100644 site/doc/pages/sqlite_error.txt create mode 100644 site/doc/pages/sqlite_insert_id.txt create mode 100644 site/doc/pages/sqlite_query.txt create mode 100644 site/examples/uce-starter/components/basic/notfound.uce create mode 100644 site/examples/uce-starter/components/theme/assets.uce delete mode 100644 site/examples/uce-starter/components/theme/page_shell.uce create mode 100644 site/examples/uce-starter/components/theme/web_affordances.uce create mode 100644 site/examples/uce-starter/themes/page.uce create mode 100644 site/tests/manifest.txt create mode 100644 site/tests/sqlite.uce create mode 100644 src/3rdparty/sqlite/sqlite3.c create mode 100644 src/3rdparty/sqlite/sqlite3.h create mode 100644 src/3rdparty/sqlite/sqlite3ext.h create mode 100644 src/lib/sqlite-connector.cpp create mode 100644 src/lib/sqlite-connector.h diff --git a/README.md b/README.md index b61c994..5985b59 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ The current build expects: - PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu) - standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support +SQLite is vendored under `src/3rdparty/sqlite/` and compiled by `scripts/build_linux.sh`; no system SQLite package is required. + The binary is written to: ```bash @@ -80,9 +82,11 @@ Useful helpers for that data model include: - `json_encode(String)` for emitting JavaScript-safe string literals directly - `ascii_safe_name(String)` for conservative ASCII identifier normalization - `path_join(base, child)` for filesystem-style path assembly +- `sqlite_connect()`, `sqlite_query()`, and related helpers for embedded SQLite storage with named prepared parameters - `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows - `gz_compress()` and `gz_uncompress()` for gzip-format byte strings - `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers +- `map()`, `filter()`, `list_unique()`, `dtree_filter()`, `dtree_map()`, `dtree_pick()`, and related helpers for route/menu/card data shaping near render code Named component handlers are also supported: @@ -206,15 +210,19 @@ The current error page includes: - request URI - resolved script path +- generated C++ path when available - high-level error summary +- source/generated excerpts and raw compiler output paths for template/component/unit failure modes - signal number and name when applicable - a native backtrace +Compile failures are also formatted with the source path, generated C++ path, compile-output artifact path, a nearby source/generated excerpt when a line can be identified, and the raw compiler output. + This recovery path currently covers normal request handling. It is not yet the universal recovery path for every runtime subsystem. ## Docs And Tests -The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. +The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. Developers coming from React, Next, or Remix should start with `site/doc/pages/coming_from_react.txt` / `/doc/index.uce?p=coming_from_react` for the concept map and starter-router notes. Useful entry points: diff --git a/RECOMMENDATIONS.md b/RECOMMENDATIONS.md new file mode 100644 index 0000000..5b0acde --- /dev/null +++ b/RECOMMENDATIONS.md @@ -0,0 +1,492 @@ +# UCE Code Review — Full Findings (2026-06-11) + +Scope: the pending working-tree changes (73 modified tracked files plus new +untracked sources — notably `src/lib/sqlite-connector.cpp/.h` and the +uce-starter theme/router rework). + +Method: seven independent review angles (line-by-line diff scan, +removed-behavior audit, cross-file call tracing, reuse, simplification, +efficiency, altitude), followed by a verification pass on the correctness +candidates. Status legend: + +- **Confirmed** — verified against the working tree, decisive lines quoted. +- **Plausible** — surfaced by a review angle, not individually re-verified. +- **Refuted** — investigated and found not to be an issue (kept here so + nobody re-flags it). + +--- + +## Part 1 — Correctness (all confirmed) + +### 1.1 XSS: island props break out of single-quoted attribute + +**File:** `site/examples/uce-starter/components/theme/web_affordances.uce:12` + +The island component emits JSON props inside a single-quoted attribute +(`data-props=''`), but `html_escape()` +(`src/lib/functionlib.cpp:936`) escapes only `& < > "`, and `json_encode` / +`json_escape` (`src/lib/dtree.cpp:794`) never escape apostrophes. Any `'` in a +prop value terminates the attribute. + +**Failure:** a view passes user-influenced prop text containing an apostrophe, +e.g. `x' autofocus onfocus='alert(1)` — the attribute terminates early and +attacker-controlled attributes/event handlers land on the div. Even benign +values like `Don't` silently truncate the payload. + +**Fix:** use a double-quoted attribute (html_escape covers `"`), or extend +`html_escape` to escape `'` as `'`. + +**Status:** fixed, but fix not verified yet. + +### 1.2 Crash: positional `?` placeholders in sqlite_query + +**File:** `src/lib/sqlite-connector.cpp:86` + +`bind_params` constructs a `String` directly from +`sqlite3_bind_parameter_name(stmt, i)`, which returns NULL for nameless +positional `?` parameters. `std::string(nullptr)` is UB and crashes before the +`if(name == "") continue;` guard on the next line can run. + +**Failure:** any page calling +`sqlite_query(db, "select * from notes where id = ?", params)` with a bare `?` +instead of `:name` segfaults the worker (500 recovery page). + +**Fix:** remove support for positional placeholders from sqlite and mysql API in favor of named placeholders, adjust the documentation accordingly. + +**Status:** fixed, but fix not verified yet. + +### 1.3 HTTP parsing: split_http_headers assumes lines[0] is the request line + +**File:** `src/lib/functionlib.cpp:754` + +The rewritten parser unconditionally treats the first line as the HTTP request +line. The old parser located the first colon-free line, which tolerated a +leading CRLF (RFC 7230 §3.5) and header-only input. Two failure modes: + +- The direct-HTTP caller (`src/fastcgi/src/fcgicc.cc:693`) passes the raw + buffer unstripped — a client sending a stray leading CRLF before + `GET /x.uce HTTP/1.1` gets the request line silently dropped (empty + `REQUEST_METHOD`, request fails). +- Header-only input (`Host: example.test\nX-Token: abc`, e.g. page code + parsing a raw header block) parses `Host:` as `REQUEST_METHOD` and loses + `HTTP_HOST` entirely. The function is directly callable from .uce pages. + +**Fix:** skip leading empty lines before consuming the request line, and only +treat a colon-free first line as the request line. + +**Status:** fixed, but fix not verified yet. + +### 1.4 Silent data loss: multi-statement SQL drops everything after the first `;` + +**File:** `src/lib/sqlite-connector.cpp:178` + +`SQLite::query` passes `0` for `pzTail` to `sqlite3_prepare_v2` and never +checks for remaining SQL, so multi-statement strings execute only the first +statement and still report `ok`. + +**Failure:** a migration page runs +`sqlite_query(db, "create table t(id integer); insert into t values(1);")` — +only the CREATE executes, the INSERT is silently dropped, `sqlite_error()` +says `ok`, leaving the database half-migrated with no error signal. + +**Fix:** capture `pzTail` and either loop over remaining statements or raise +an error when trailing SQL is present. + +**Status:** fixed, but fix not verified yet. + +### 1.5 Diagnostics regression: fault backtrace captured after siglongjmp + +**File:** `src/linux_fastcgi.cpp:895` + +The in-handler `capture_backtrace_string` call (`request_fault_trace`) was +deleted; the trace is now captured after `siglongjmp` back in +`handle_complete`, where the faulting stack has already been unwound. No +capture remains in `on_request_fault_signal`. + +**Failure:** a `.uce` page null-derefs → SIGSEGV → the error page's Trace +shows only `handle_complete`/`main` frames. The faulting unit's frames are +gone, making crash reports undiagnosable beyond the signal number (the README +still advertises a native backtrace of the failure). + +**Fix:** restore the capture inside `on_request_fault_signal` (on the faulting +stack) and hand the result across the longjmp, as the previous code did. + +**Status:** fixed, but fix not verified yet. + +### 1.7 Memory leak: SQLite wrapper objects never freed by request cleanup + +**File:** `src/lib/sqlite-connector.cpp:248` + +`cleanup_sqlite_connections()` closes the raw `sqlite3` handles tracked in +`resources.sqlite_connections` but never deletes the heap-allocated `SQLite` +wrappers from `sqlite_connect` (`new` without `delete`; no arena allocator is +compiled in). `sqlite_disconnect` itself is correct (`delete db` after +unregistering). Note: this mirrors a pre-existing identical leak in the MySQL +connector (`cleanup_mysql_connections`, `mysql-connector.cpp:298-304`). + +**Failure:** a page calls `sqlite_connect()` per request and relies on +end-of-request cleanup instead of `sqlite_disconnect()` — exactly what the +cleanup path exists for. One wrapper leaks per request; unbounded RSS growth +in the long-lived FastCGI worker. + +**Fix:** track the wrapper objects (not raw handles) in +`resources.sqlite_connections` and delete them in cleanup; fix the MySQL +connector the same way, or factor a shared registry (see 5.1). + +**Status:** fixed, but fix not verified yet. + +### 1.8 Asset shims emit stylesheets mid-`` + +**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8` +(also `theme_assets.uce`, `gauges/assets.uce`) + +The new ONCE-based asset shims fire inside the view's `ob_start()` capture in +`index.uce` (lines 67-85: view is captured into `fragments["main"]`, then +`themes/page` renders), so their `` output is baked +into the main fragment and spliced into the content div — inside ``, +not ``. + +**Failure:** every marketing/theme/gauges page ships its stylesheet mid-body +(FOUC, invalid-ish markup). The deleted `page_shell` asset registry rendered +these in ``. While this behavior is often okay in practice, we should +improve on this to encourage cleaner output. + +**Fix:** Part A: restore a registration mechanism: record component and asset output +that's intended as once per page in context.call["fragments"]["once"] by default +and the page template component can then explicitly slot this in where appropriate. + +Part B: Modify the preprocessor so directives support attributes like this: +ONCE(Request& context) +@fragment my-fragment-name +{ + ... +} + +Which will then automatically (in this example) slot the output into +context.call["fragments"]["my-fragment-name"] instead of the default slot name "once". +In the future we'll introduce more attributes with this syntax. Using this flexible mechanism for the fragment slot +name, we can leave it up to the page template where to slot in what. ONCE, COMPONENT, +and RENDER should support the fragment attribute. + +**Status:** fixed, but fix not verified yet. + +### 1.9 Error page "Generated C++" hint prints a nonexistent path + +**File:** `src/linux_fastcgi.cpp:79` + +`render_request_failure` computes the hint as +`path_join(BIN_DIRECTORY, SCRIPT_FILENAME) + ".cpp"`, but `path_join` returns +an absolute child unchanged (`src/lib/sys.cpp:179`), while the compiler builds +the real path by plain concatenation `BIN_DIRECTORY + src_path` +(`src/lib/compiler.cpp:631-636`). SCRIPT_FILENAME is always absolute in the +FastCGI path. + +**Failure:** every runtime failure page prints e.g. +`Generated C++: /var/www/site/page.uce.cpp` — the BIN_DIRECTORY prefix is +silently dropped and the path never exists (real file: +`/var/cache/uce/work/var/www/site/page.uce.cpp`). + +**Fix:** export one helper from `compiler.cpp` that maps a source file to its +generated artifact (`su->pre_path + "/" + su->pre_file_name` — the new +`compiler_format_compile_failure` in this same changeset already computes it +correctly) and use it in both places. + +**Status:** fixed, but fix not verified yet. + +### 1.10 O(n²) and per-row deep copies in dtree_map / dtree_filter + +**File:** `src/lib/functionlib.cpp:182` + +Both helpers call `tree.is_list()` inside the per-element callback — +`is_list()` (`src/lib/dtree.cpp:165`) walks the entire map, making the +operation O(n²) — even though both already compute `is_list()` once before +the loop. Additionally, `DTree::each` (`dtree.h:22`) takes the callback +element by value (`DTree t`), deep-copying every subtree per iteration. + +**Failure:** mapping over a 1000-row `sqlite_query` result does ~1M +key-validation checks plus a full deep copy of each row tree. + +**Fix:** hoist `is_list()` into a `bool` local reused in the callback; change +`each()` to pass `const DTree&`. + +**Status:** fixed, but fix not verified yet. + +### 1.11 Proactive compiler child can std::terminate on lock failure + +**File:** `src/lib/compiler.cpp:288` + +`compiler_with_registry_lock` now throws `std::runtime_error` when the +registry lock file can't be opened (previously: warning + proceed unlocked). +`run_proactive_compiler` (`src/linux_fastcgi.cpp:1270-1372`) calls +`compiler_list_known_units` / `compiler_set_known_units` with no try/catch +anywhere in the chain, so a transient failure (fd exhaustion, disk full) +aborts the forked child via `std::terminate`. + +**Blast radius is small:** the proactive compiler runs in a forked child that +the main loop respawns each iteration, and request-path callers are protected +by the try/catch in `handle_complete`. Still, a catch-and-log around the +proactive scan is cheap insurance. + +**Status:** fixed, but fix not verified yet. + +--- + +## Part 2 — Reuse / duplication (plausible unless noted) + +### 2.1 request_query_path() duplicates request_query_route() + +**File:** `src/lib/uri.cpp:311` + +`request_query_path()` copy-pastes `request_query_route()`'s (line 325) +first-keyless-segment scan verbatim — two identical "find first &-part +without =" loops plus `route_path_sanitize` calls that must be kept in sync. +It also has **zero callers** in `src/` or `site/` (only a doc page references +it). + +**Fix:** implement as +`return request_query_route(context, default_path)["l_path"].to_string();`. + +**Status:** fixed, but fix not verified yet. + +### 2.2 list_filter / list_map re-implement the generic filter / map templates + +**File:** `src/lib/functionlib.cpp:71` + +`StringList` is `typedef std::vector` (`types.h:52`), so the existing +`filter` template (`functionlib.h:68`, declared ~10 lines above the new +`list_*` declarations) already does exactly what `list_filter` does. Two +filter implementations now live in the same module; behavior fixes must be +applied twice. + +**Fix:** drop it and point the doc page at `filter`); same consideration for `list_map`. + +**Status:** fixed, but fix not verified yet. + +### 2.3 Hand-rolled redirects instead of the redirect() helper + +**Files:** `site/examples/uce-starter/views/account/login.uce:13`, +`logout.uce:6`, `profile.uce:8`, `site/demo/sqlite.uce:21-22` + +Four call sites inline `context.set_status(302, "Found"); +context.header["Location"] = ...` instead of calling the existing runtime +helper `redirect(String url, s32 code = 302)` (`src/lib/uri.cpp:414`). This +replaced the single `app_redirect()` helper the changeset deleted. + +**Note — security aspect refuted (see 6.1):** headers are sanitized centrally +on write-out, so this is a pure reuse nit, not a vulnerability. + +**Fix:** use `redirect(app_link("account/profile", context))` or restore one +`app_redirect` wrapper. + +**Status:** fixed, but fix not verified yet. + +### 2.5 Asset tag boilerplate repeated across ~a dozen files + +**Files:** `components/example/marketing_assets.uce:5`, +`components/example/theme_assets.uce`, `components/gauges/assets.uce`, +plus inline `` +blocks in `components/theme/head.uce`, `components/data/widgets.uce`, +`components/workspace/primitives.uce`, `views/dashboard.uce` + +About a dozen hand-written copies of the stylesheet/script tag pattern around +`app_asset_url()`; a versioning or attribute change (defer/integrity) touches +every file. The marketing and theme shims differ only in one CSS path. + +**Fix:** collapse the three shims into one parameterizable assets component. + +**Status:** fixed, but fix not verified yet. + +--- + +## Part 3 — Simplification (plausible) + +### 3.1 request_query_route() emits a derivable "rejected" flag + +**File:** `src/lib/uri.cpp:347` + +The route tree contains both `route["valid"]` and `route["rejected"]` where +`rejected` is exactly `!valid`, and nothing in the codebase reads +`"rejected"`. Redundant derivable state doubles the invariant surface — a +future path that sets one flag without the other produces a route tree that +lies. + +**Fix:** drop the `"rejected"` key; callers needing it can write +`!route["valid"].to_bool()`. + +**Status:** fixed, but fix not verified yet. + +### 3.2 Starter router carries write-only diagnostic state + +**File:** `site/examples/uce-starter/index.uce:38` + +The router rewrite adds candidate `kind`/`matched` fields and +`context.call["route"]["candidates"]/["resolved"]/["missed"]`, none of which +is read by any component, theme, or test. It also uses `dtree_filter` to +`file_exists`-stat every candidate after the first match is already found. +~55 lines plus a builder helper replace what the deleted `app_resolve_view` +did with three early-return `file_exists` checks. + +**Fix:** remove unused parts. + +**Status:** fixed, but fix not verified yet. + +### 3.4 Single-link shim components where a direct ONCE block suffices + +**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8` +(and `theme_assets.uce`) + +These are single-`` shim files with empty COMPONENT bodies, invoked via +`print(component(...))` of an empty string. Routed views are mutually +exclusive per request, so the cross-unit dedup the shims provide can never +trigger; they exist only to host one ONCE line at the cost of two extra files +and an indirection on every view. `dashboard.uce:3-6` already demonstrates +the simpler form (ONCE block directly in the view). `gauges/assets.uce` is +the justified case (multiple sibling components per page) and can stay. + +**Status:** fixed, but fix not verified yet. + +### 3.5 Compile-failure artifact file is self-referential + +**File:** `src/lib/compiler.cpp:847` + +`compile_shared_unit()` overwrites `su->compiler_messages` with the formatted +failure report, then writes that report into `compile_output_file_name` — the +same artifact the report's own "Compile output:" line +(`compiler_format_compile_failure`, line 800) points readers at. No copy of +the raw, unformatted compiler output survives for tooling to parse. + +**Fix:** keep raw messages as the stored/recorded artifact content and format +only at the print/response boundary. + +**Status:** fixed, but fix not verified yet. + +--- + +## Part 4 — Efficiency (plausible unless noted) + +### 4.1 QUERY_STRING parsed twice per request + +**File:** `src/linux_fastcgi.cpp:744` + +`prepare_request_body_maps` calls `request_populate_context_params` (which +splits QUERY_STRING on `&` + uri-decodes inside `request_query_route`) and +then `parse_query(QUERY_STRING)` re-splits and re-decodes the identical +string on the next line. Runs on every HTTP request, CLI invocation, and +websocket event. Route params are also computed eagerly for requests that +never read `ROUTE_*`/`BASE_URL`. + +**Fix:** parse QUERY_STRING once into `request.get` first and derive the +route token from that single pass, or populate the route params lazily. + +**Status:** fixed, but fix not verified yet. + +### 4.2 Repeated normalization in request_populate_context_params + +**File:** `src/lib/uri.cpp:351` + +Per request: `route_path_normalize` runs three times on the same path +(directly, inside `route_path_sanitize`, and inside `route_path_is_safe`), +`request_script_url` is computed twice (for SCRIPT_URL and again inside +`request_base_url`), and `route_path_normalize` (line 255) strips slashes via +`path = path.substr(1)` in a while loop — O(n²) copies per slash run. + +**Fix:** normalize once and pass the normalized string down; compute +script_url into a local used by both params; replace the substr loops with +`find_first_not_of("/")` / `find_last_not_of("/")` and a single substr. + +**Status:** fixed, but fix not verified yet. + +### 4.4 collect_rows rebuilds column-name strings for every row + +**File:** `src/lib/sqlite-connector.cpp:118` + +For R rows × C columns the row loop does R×C `sqlite3_column_name` calls plus +R×C String heap constructions and map inserts keyed on the fresh string, +though column names are invariant across rows. A 10k-row, 8-column result +builds 80k redundant name strings per query. + +**Fix:** build a `std::vector names(column_count)` once before the +step loop and index it inside (`row[names[i]]`). + +**Status:** fixed, but fix not verified yet. + +### 4.5 bind_params copies the params map twice and binds SQLITE_TRANSIENT + +**File:** `src/lib/sqlite-connector.cpp:95` + +`SQLite::query` takes `StringMap` by value and passes it by value again to +`bind_params`, then binds with `SQLITE_TRANSIENT`, forcing SQLite to memcpy +each value a third time — even though the copied map outlives the statement +(it lives until after `sqlite3_finalize`). + +**Fix:** pass `const StringMap&` through `query()`/`bind_params` and bind +with `SQLITE_STATIC` (or at least drop the two by-value map copies). + +**Status:** fixed, but fix not verified yet. + +### 4.6 Per-request connection opens re-run pragmas; busy_timeout set twice + +**File:** `src/lib/sqlite-connector.cpp:15` + +`connect()` calls `sqlite3_busy_timeout(5000)` and then +`apply_default_pragmas` runs `PRAGMA busy_timeout = 5000` again (pure +duplicate). `PRAGMA journal_mode = WAL` is persistent in the database file +but re-issued on every open. Because cleanup closes all handles at request +end, a page like `site/demo/sqlite.uce` pays `sqlite3_open_v2` + 4 pragmas + +`CREATE TABLE IF NOT EXISTS` on every request. + +**Fix:** drop the redundant busy_timeout pragma; cache open +connections per worker keyed by path across requests (resetting state at +request end) so pragmas and schema checks run once per worker. + +**Status:** fixed, but fix not verified yet. + +--- + +## Part 5 — Altitude / design (plausible unless noted) + +### 5.3 Test suites hand-registered in three parallel lists — already drifting + +**Files:** `site/tests/index.uce:15`, `tests/plugins/uce_site_suite.py` + +A test page must be registered in three places: the `site/tests/*.uce` file +itself, a `site_tests_card()` line in `index.uce`, and a tuple in +`uce_site_suite.py`. The new `sqlite.uce` was added to all three by hand. The +lists have already drifted: `site/tests/call_helpers.uce` exists on disk but +appears in neither `index.uce` nor any plugin list, and +`security_headers.uce` is covered only by the security-smoke plugin, not the +index cards — new suites can silently fall out of the dashboard and/or CI. + +**Fix:** enumerate `site/tests/*.uce` with ls()/glob in both `index.uce` and +`uce_site_suite.py`, with title/tags metadata declared once (in the test page +or a single shared manifest). + +**Status:** fixed, but fix not verified yet. + +### 5.4 compiler_developer_hints() couples the runtime to clang's English message text + +**File:** `src/lib/compiler.cpp:778` + +The hint table pattern-matches hardcoded English clang diagnostic substrings +("no member named", "expected ';'", ...). A clang version bump, a switch to +gcc (COMPILE_SCRIPT is user-configurable server config), or localized +diagnostics silently degrades every hint to the generic fallback, and each +new error class means hand-extending an if-chain in runtime C++. + +**Fix:** the excerpt mechanism added in the same change +(`compiler_format_compile_failure`'s source/generated excerpts + artifact +paths) already carries the diagnostic value; make the hints a data-driven +table or drop them in favor of the excerpts. + +**Status:** fixed, but fix not verified yet. + +### 5.5 Generated-artifact path computed by hand in two places + +**File:** `src/linux_fastcgi.cpp:79` vs `src/lib/compiler.cpp:631-636` + +The deeper-fix framing of 1.9: the moment the compiler's artifact layout or +naming changes (hashing, per-config subdirs), any hand-recomputed path goes +stale. One exported source-file → artifact-path helper in `compiler.cpp`, +used by both the compile-failure formatter and the runtime failure page. + +**Status:** fixed, but fix not verified yet. diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md new file mode 100644 index 0000000..a5ced4c --- /dev/null +++ b/WASM-PROPOSAL.md @@ -0,0 +1,496 @@ +# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE + +- **Status:** proposal / design draft (2026-06-11) +- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` → + `dlopen`) with per-unit WebAssembly modules executed in a per-request, + runtime-linked workspace, exposing the same API surface to page code. +- **Origin:** design discussion 2026-06-11; incorporates the post-mortem of the + earlier per-invocation arena attempt (preserved in + `src/lib/_scratchpad.cpp`). + +--- + +## 1. Motivation + +Two long-standing structural problems and one strategic opportunity share a +single root cause: **request code shares an address space and an allocator +with the runtime.** + +1. **The arena attempt failed for a structural reason.** The + `GLOBAL_ARENA_ALLOCATOR` design in `_scratchpad.cpp` swapped the global + `operator new`/`delete` against a `current_memory_arena`. A single global + allocator cannot distinguish request-lifetime allocations from + process-lifetime ones: during a request, server-lifetime structures + (compile registry, sessions, config trees, unit statics) also allocate. + Under the arena those dangle on reset; with `delete` as a no-op, + system-allocated objects released mid-request leak. The lifetime + distinction lives in the type system and call graph, not at the allocator + boundary — fixing it in-process means threading PMR allocators through + `String`, `DTree`, and every container. + +2. **Fault recovery is best-effort, not sound.** The current + SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips + destructors, can resume over corrupted worker state, and (see + RECOMMENDATIONS.md 1.5) cannot reliably produce a useful backtrace. + A faulting unit *can* have scribbled on runtime memory before the signal. + +3. **UCE can only ever host fully-trusted code.** A `.uce` page is arbitrary + native code with full process privileges. Multi-tenant or user-supplied + page hosting is structurally impossible in the native model. + +A WASM execution model deletes the shared-fate fact itself rather than +patching around it: + +- **Arena by construction:** each request runs in its own linear memory, + dropped wholesale at request end. Request-lifetime memory physically cannot + outlive the request; server-lifetime state physically cannot live inside + it. The `_scratchpad.cpp` design becomes correct because the boundary is + structural, not typological. +- **Sound recovery:** a trap (null deref, OOB, stack exhaustion) is a defined + host-side error that unwinds cleanly, with a precise guest stack trace, and + the unit cannot have touched host memory. Render error page, drop + workspace, keep serving — actually correct, not hopeful. +- **Capability security:** page code gets exactly the imported API surface + and nothing else. Multi-tenant hosting becomes possible. +- **Secondary wins:** architecture-independent unit artifacts (no clang + required on prod), safe module unload/replace (vs. never-safe `dlclose`), + first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU + timeout), per-request memory stats for free. + +**Accepted costs:** ~1.2–2× compute slowdown vs. native (still far ahead of +interpreted runtimes); a real ABI/membrane design; ownership of a custom +loader; toolchain rough edges (§10). + +--- + +## 2. Rejected alternatives (recorded so they stay rejected) + +- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` and + threading allocators through the entire codebase; the failed global-swap + shortcut is the only cheap version and it is unsound (§1.1). +- **One instance per component.** UCE components are function calls, not + RPCs: callees receive the context **by reference**, mutate `context.call`, + share the `ob_*` capture stack and `ONCE` dedup state. Per-component + instances force serialize/copy/deserialize of the context on every + `component()` call (a dozen+ per page in the starter), require + host-mediation of the ob stack, and silently change reference semantics to + copy semantics. Rejected. +- **One linked module per app ("the blob").** Introduces an "app" concept + UCE does not have, makes every edit a global relink, turns the artifact + cache into a build graph — webpack's worst traits without its benefits. + Rejected. The file stays the unit. +- **Eager pre-loading of known units at worker warm-up.** Rejected; loading + is strictly lazy, on first explicit call, preserving current semantics. + +--- + +## 3. Architecture overview + +### 3.1 Execution model + +``` +host process (linux_fastcgi, per worker) +│ +├─ vendored wasm runtime (§10.1) +├─ unit artifact cache: one PIC .wasm per unit (replaces per-unit .so) +├─ loader (§6): dylink parsing, base allocation, GOT resolution, +│ symbol registry, ABI stamp check +├─ core snapshot: "core module, initialized" memory+table image +│ +└─ per request: WORKSPACE + ├─ linear memory (CoW-born from core snapshot) + ├─ shared funcref table + ├─ core module instance (uce_lib + libc compiled to wasm) + ├─ unit module instances (loaded lazily, on first call, incl. mid-request) + └─ host handle table (sqlite/mysql/file/socket handles + closers) +``` + +- **Workspace = request.** Born from the core snapshot, dropped at request + end. Memory drop is the arena; handle-table drop is resource cleanup + (this generalizes and replaces the per-connector + `cleanup_*_connections()` pattern — RECOMMENDATIONS.md 1.7 / 5.1 become + structurally unrepresentable). +- **One unit = one PIC wasm module.** Compile, cache, and invalidation + granularity stay per-file. The `.uce → C++` translation pipeline is + unchanged; only the compile target changes + (`clang --target=wasm32-wasi -fPIC` + `wasm-ld -shared`). +- **Strictly lazy loading.** A unit's module is instantiated into a + workspace the first time that workspace calls it — including mid-request. + This is the wasm equivalent of today's compile-and-`dlopen`-on-first-hit + and requires no restart, no fallback path. Placement memoization + (deterministic bases so repeat instantiation is cheaper) is a permitted + optimization; it must not change the loading policy. +- **In-flight isolation.** Module versions are immutable; a recompiled unit + becomes a new module. Running workspaces keep what they loaded; new + workspaces get the new version. Old modules are dropped when unreferenced + (safe unload — impossible with `dlclose`). + +### 3.2 Memory model + +- **One heap, one allocator, one DTree implementation** — all owned by the + core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT; + the loader **rejects any unit module that defines rather than imports + them** (two allocators on one heap is the one fatal misconfiguration). +- **Bump allocator option.** Because the workspace heap is dropped wholesale, + the core's allocator may be a bump allocator with no-op free — the + `_scratchpad.cpp` design, now correct by construction. Per-deployment + flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base + (replaces the tracking `operator new` in `types.h`). +- **Unit statics reset per request** (workspace is born from the core + snapshot, which does not include unit data; unit data segments initialize + at unit load within the workspace). This is *more* shared-nothing than + today, where `.so` statics persist across requests within a worker. + Cross-request state must use explicit host facilities (sessions, caches). + **Breaking change — must be called out in docs and checked against the + site/ tree during Phase 5.** + +### 3.3 The host membrane + +Exactly three currencies cross between host and workspace: + +1. **scalars** (i32/i64/f64), +2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are + placed via the core's exported allocator), +3. **handles** (opaque `u32` indices into the per-workspace host handle + table; each entry carries a closer callback). + +Everything pointer-shaped stays on its own side. Hostcall surface budget: +30–60 functions (§5.1). Host errors return as error values; traps are +reserved for unit faults. Nothing throws across the membrane. + +**DTrees cross the membrane only as the versioned wire encoding** (§5.3), +at exactly three sites: request context in (once), response out (once), +bulk I/O results in (per query, optional vs. cursor-style hostcalls). + +### 3.4 DTree inside the workspace: no serialization, ever + +Within a workspace, all modules share one address space, one toolchain, one +set of headers — the C/C++ ABI is intact across module boundaries. Therefore: + +- A `DTree` is a pointer (an i32 offset into linear memory). +- `component(path, context)` resolves path → table index (host registry or + guest-resident map) and `call_indirect`s, passing the context pointer. + Reference semantics, mutation visibility, shared ob stack, working `ONCE` + — identical to today. +- Function pointers are shared-table indices, valid across modules: virtual + calls, `std::function` callbacks (`dtree_map` lambdas) work across units. +- Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module + symbols — the same shape of overhead native PIC pays through the PLT/GOT, + i.e. what dlopened `.so` units pay today. + +Encode/decode is **not** part of internal component calls. It exists only at +the membrane (§3.3) and on the cross-instance plane (§4). + +### 3.5 The DTree C ABI (load-bearing, build it first) + +The stable contract of the workspace is a **C ABI**, not the C++ class: +the core exports `extern "C"` accessors over an opaque `uce_dtree*` (§5.2), +plus the string/ob/print helpers. C++ units may bypass it and use the class +directly (same headers, zero cost — a private fast path). Every other +workspace language uses the C surface. + +This ABI is versioned: every unit artifact carries a custom section +(`uce.abi`: core ABI version + toolchain fingerprint); the loader refuses +stale units and triggers lazy recompilation (units are lazily compiled +anyway, so this costs nothing structurally). + +**Phase 1 of the implementation plan is to introduce this C ABI in the +current native runtime** — it is useful immediately (plugin surface, +testability) and de-risks the rest. + +--- + +## 4. Two component-call planes / multi-language support + +The design stratifies languages by one question: *can the toolchain produce a +PIC linear-memory module that adopts a foreign allocator?* + +**Plane A — workspace peers** (C++, C, Rust, Zig, …): +- Join the workspace as PIC modules importing core symbols. +- Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator + parameter) and must not unwind across boundaries (`panic=abort` / + catch-at-edge). +- Access DTrees through the C ABI: pointer semantics, no copies, ns-scale + calls. Idiomatic wrappers per language (e.g. Rust `DTree<'request>` — + the borrow checker enforces the arena invariant). + +**Plane B — runtime-carrying languages** (JS, Python, Go, C#, …): +- Their GC/runtime owns its memory; they run as **separate instances** + within the request and communicate through the host using the wire + encoding and handles. Bindings choose per-access hostcalls or bulk + subtree hydration into native dicts/objects — a tuning decision, not an + architectural fork. + +**The semantic rule (enforced by the loader, not by convention):** +cross-plane components do **not** receive the mutable context. They get an +explicit interface — props in (copied by definition), rendered output and +declared results back. Plane A keeps the full "here's the world, mutate it" +contract. A `component()` call must never silently change mutation semantics +based on the callee's implementation language. + +The cross-instance call mechanism is shared by: Plane B units, and future +cross-trust-boundary components (multi-tenant). Serialization boundaries and +isolation boundaries are the same lines, by design. + +--- + +## 5. ABI sketches (to be finalized in Phase 0/1) + +### 5.1 Hostcall surface (grouped; target ≤ 60 functions) + +``` +request: uce_host_ctx_read(buf) → len // wire-encoded context, once +response: uce_host_respond(status, hdrs_buf, body_buf) + uce_host_stream_write(buf) // chunked/streaming path +log: uce_host_log(level, buf) +sqlite: uce_host_sqlite_connect(path_buf) → handle | err + uce_host_sqlite_query(handle, sql_buf, params_buf) → result_buf | err + uce_host_sqlite_cursor_*(...) // optional row-cursor variant + uce_host_sqlite_insert_id/affected/error/disconnect(handle) +mysql: (same shape; existing connector APIs are already handle-shaped) +files: uce_host_file_read/write/stat/list(path_buf, ...) // policy-gated +session: uce_host_session_get/set(key_buf, val_buf) +http: uce_host_http_request(req_buf) → handle/result_buf // outbound +misc: uce_host_time(), uce_host_random(buf), uce_host_env(key_buf) +loader: uce_host_component_resolve(path_buf) → table_index // may load (§6) +ws: uce_host_ws_send(buf), event delivery via render entry re-invocation +``` + +Conventions: all errors as result codes + `uce_host_last_error(buf)`; +inbound buffers placed via the core's exported `uce_alloc`; no hostcall +traps on bad input (clamp/error instead). + +### 5.2 DTree C ABI (core exports; sketch) + +```c +typedef struct uce_dtree uce_dtree; // opaque; workspace-owned + +uce_dtree* uce_dtree_root(void); // request context +uce_dtree* uce_dtree_get(uce_dtree*, const char* key, size_t klen); // create-on-write +uce_dtree* uce_dtree_find(uce_dtree*, const char* key, size_t klen); // NULL if absent +const char* uce_dtree_value(uce_dtree*, size_t* len); +void uce_dtree_set_value(uce_dtree*, const char* v, size_t vlen); +size_t uce_dtree_count(uce_dtree*); +int uce_dtree_is_list(uce_dtree*); +/* iteration */ +uce_dtree_iter uce_dtree_iter_begin(uce_dtree*); +int uce_dtree_iter_next(uce_dtree*, uce_dtree_iter*, + const char** key, size_t* klen, uce_dtree** child); +/* encode/decode at the membrane */ +size_t uce_dtree_encode(uce_dtree*, char* buf, size_t cap); // → UCEB1 +uce_dtree* uce_dtree_decode(const char* buf, size_t len); +/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close, + uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */ +``` + +No unwinding across this surface; C++ exceptions are caught at the edge and +surfaced as error returns where fallible. + +### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only) + +Length-prefixed binary tree; **not** JSON. Sketch (finalize against DTree's +actual fields — value + ordered children): + +``` +node := value children +value := varint len, bytes (utf-8) +children := varint count, count × ( key: varint len + bytes, node ) +flags := one leading byte per node reserved (bit0: is_list hint) +header := "UCEB" u8 version +``` + +This encoding is a **versioned protocol** from day one (header byte). It is +the Plane B contract and the membrane format; internal calls never see it. + +### 5.4 Unit module contract + +``` +custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id } +imports: env.memory, env.__indirect_function_table, + env.__memory_base, env.__table_base, + GOT.mem.* / GOT.func.* (resolved by loader), + core symbols (malloc, uce_dtree_*, uce_print, ...) +exports: uce_unit_setup, uce_unit_render, + uce_unit_component, uce_unit_websocket + (same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET + dlsym symbols in compiler.cpp) +forbidden: defining malloc/free/operator new, own memory, start fn + with side effects beyond data init +``` + +--- + +## 6. The loader (host-side, custom, load-bearing) + +Owned code, ~1–2k lines, vendored-runtime-adjacent. Reference logic: +Emscripten's dylink loader (the ABI is the stable, battle-tested part; the +server-side loader is what doesn't exist off the shelf). + +Per `load(unit)` into a workspace: + +1. Fetch compiled module from artifact cache (compile on miss — today's + lazy-compile path, retargeted). +2. Verify `uce.abi` stamp against the core; on mismatch, recompile unit. +3. Verify import discipline (no allocator/runtime definitions; §3.2). +4. Parse `dylink.0`: data size/alignment, table slots needed. +5. Allocate `__memory_base` (bump within workspace data region) and + `__table_base` (append to shared table). +6. Instantiate with bases; resolve `GOT.*` imports against the workspace + symbol registry (core symbols + previously loaded units); register the + unit's exports. +7. Register entry points in the path → table-index dispatch map. + +`uce_host_component_resolve(path)` consults the dispatch map and calls +`load()` on miss — this is how lazy, programmatic, mid-request loading works +with no special cases. + +Placement memoization (optional, later): record each unit's first-assigned +bases; reuse across workspaces so instantiation is cheaper and snapshot +growth (below) stays consistent. Does not change the lazy policy. + +**Core snapshot:** the only pre-built state is "core module, initialized" — +memory bytes + table state captured once per core build. Workspaces are born +from it via CoW (`mmap(MAP_PRIVATE)` of the snapshot image; the host owns +the Memory object, so OS-level CoW is available). No units are pre-fed. + +--- + +## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp) + +``` +1. accept request +2. workspace = birth_from_core_snapshot() (CoW, ~µs) +3. write wire-encoded context into workspace; core decodes → context DTree +4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr) +5. component(path) inside guest → resolve hostcall → (lazy load) → + call_indirect — reference semantics throughout +6. I/O via hostcalls; resources land in the workspace handle table +7. on return: encode response/headers out; write FastCGI response + on trap: defined error → render error page with guest stack trace; + workspace state is irrelevant because… +8. drop workspace: linear memory gone (arena), handle table closed + (generalized resource cleanup), instances released +``` + +CPU limit: epoch/fuel interruption → same path as trap. +Memory limit: linear memory max → allocation failure / trap → same path. + +--- + +## 8. What carries over unchanged + +- The `.uce → C++` translation, parser, and page semantics. +- The lazy compile-on-first-request model and per-unit artifact caching + (different artifact format). +- The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they + move behind hostcalls with the same `.uce`-visible signatures. +- The site tree, docs, demo, and the network test suite + (`tests/run_network_tests.py`) — which becomes the parity harness (§9). +- nginx/FastCGI front-end integration, worker model, websocket event flow + (events re-enter via the websocket entry point; note: a workspace-per-event + or workspace-per-connection decision is an open question, §11). + +--- + +## 9. Implementation plan + +Phases are sequential; each has an exit criterion. No phase except 0 and 2's +scaffolding produces throwaway work. + +**Phase 0 — toolchain & runtime spike (timeboxed).** +Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated +unit; cross-module C++ calls with shared memory/table; exceptions decision +(wasm EH vs. error-code discipline at unit boundaries — pick one, record it); +vendored runtime selection. Candidates: **WAMR** (C, small, designed for +embedding, easiest to vendor and patch — fits the project's vendoring +practice) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier +to vendor/patch). Selection criteria: imported-memory + shared-table support, +AOT artifact quality, patchability. Exit: a two-module (core stub + unit +stub) hello-world linked at runtime by a minimal loader, in the chosen +vendored runtime. + +**Phase 1 — DTree C ABI in the native runtime.** +Introduce `uce_dtree_*` (§5.2) and the UCEB1 codec in `src/lib/`, used +natively. Zero wasm dependency; immediately testable; freezes the contract +everything else builds on. Exit: codec round-trip + accessor tests in the +existing suite; ABI doc checked in. + +**Phase 2 — core module + membrane.** +Compile `uce_lib` (+ wasi-libc) to wasm as the core module; implement the +hostcall surface (§5.1) in the host; temporary scaffolding allowed: one +statically-linked unit + core to validate codegen and membrane without the +loader. Exit: one real `.uce` page (e.g. `site/tests/core.uce`) renders +correctly through the membrane. Scaffolding is marked throwaway. + +**Phase 3 — the loader + workspace.** +Implement §6 in full: dylink parsing, base allocation, GOT resolution, +ABI/import verification, lazy mid-request loading, path dispatch. Per-request +workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4). +Exit: the uce-starter renders end-to-end with components loading lazily; +`tests/run_network_tests.py --match starter` passes against the wasm worker. + +**Phase 4 — production mechanics.** +Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap → +error-page path with guest stack traces (this supersedes the +signal/longjmp machinery and closes RECOMMENDATIONS.md 1.5 structurally); +handle-table cleanup (closes 1.7/5.1); artifact/ABI versioning end-to-end. +Exit: kill-tests (deliberate null-deref page, infinite-loop page, OOM page) +produce clean error pages and an unharmed worker. + +**Phase 5 — parity & performance.** +Full network suite green on the wasm worker; differential native-vs-wasm runs +on the site tree; audit `site/` for cross-request-static reliance (§3.2 +breaking change); benchmark suite (template-heavy page, sqlite page, +component-heavy starter page) with budgets: ≤2× native page latency, +workspace birth ≤100µs, internal component call overhead within 10× native +call cost. Exit: numbers published in this document; go/no-go for default +backend. + +**Phase 6 — second plane (deferred until wanted).** +Cross-instance call mechanism (props-in/output-out, UCEB1), first Plane B +language binding, loader enforcement of the cross-plane context rule (§4). +Plane A second language (Rust) as the cheaper first polyglot proof. + +The native `.so` backend remains in-tree and selectable until Phase 5's +go/no-go; both backends share the Phase 1 C ABI. + +--- + +## 10. Risks & mitigations + +| Risk | Mitigation | +|---|---| +| wasi-sdk PIC / shared-library maturity (least-trodden toolchain path) | Phase 0 spike before any commitment; pin toolchain versions; statically link libc into the core and export from there (avoid shared wasi-libc entirely) | +| C++ exceptions × PIC × wasm EH | Phase 0 decision point; fallback is error-code discipline at unit entry points (units already have a uniform entry shape) | +| Custom loader correctness (GOT, bases, relocation) | Small, contained (~1–2k lines); crib logic from Emscripten's reference loader; fuzz with adversarial modules; loader rejects > loader guesses | +| Vendored runtime patches drift from upstream | Same practice as vendored SQLite: provenance + patch files under `docs/patches/`; pin upstream tag; tests gate upgrades | +| Performance regression beyond budget | Phase 5 gates; bump allocator and placement memoization in reserve; native backend retained | +| Multi-module DWARF / debugging story | Trap stack traces cover the production case (better than today); accept weaker interactive debugging; keep native backend for local deep-debugging | +| Unit-statics semantic change breaks existing pages | Phase 5 audit of `site/`; documented migration note; host-side cache facility if a real need surfaces | + +## 11. Open questions + +1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.) +2. **WebSocket granularity:** workspace per event (pure arena, statics reset + per event) or per connection (state across events, bounded lifetime)? + Leaning per-event for consistency; needs a look at current WS page usage. +3. **UCEB1 final layout** vs. DTree's actual field set (value/children/list + flag) — finalize in Phase 1. +4. **Cursor vs. bulk** as the default for `sqlite_query` results at the + membrane (offer both; pick the default after Phase 5 benchmarks). +5. **Streaming output:** today's ob model buffers; does the membrane expose + `uce_host_stream_write` from day one or post-MVP? +6. **Core snapshot rebuild cadence** once placement memoization lands + (dead-base reclamation policy). + +## 12. Summary + +The module is the **unit** (file-grained, lazily compiled, lazily loaded — +including mid-request). The instance set is the **workspace** (one per +request; shared memory + table; born from a core-only CoW snapshot; dropped +wholesale — the arena, done right). The contract is the **DTree C ABI** +inside the workspace (pointer semantics, no serialization) and the **UCEB1 +wire encoding + handles** at every true address-space boundary (host +membrane, Plane B languages, future trust boundaries). Serialization +boundaries and isolation boundaries are the same lines; component calls stay +function calls; and the file stays the unit. diff --git a/docs/patches/sqlite-3.46.1-vendor.md b/docs/patches/sqlite-3.46.1-vendor.md new file mode 100644 index 0000000..e2c318d --- /dev/null +++ b/docs/patches/sqlite-3.46.1-vendor.md @@ -0,0 +1,34 @@ +# SQLite 3.46.1 Vendor Import + +Imported the official SQLite amalgamation without local source modifications. + +- Version: SQLite 3.46.1 +- Upstream archive: https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip +- Import date: 2026-05-29 +- Vendored files: + - `src/3rdparty/sqlite/sqlite3.c` + - `src/3rdparty/sqlite/sqlite3.h` + - `src/3rdparty/sqlite/sqlite3ext.h` + +## Checksums + +```text +77823cb110929c2bcb0f5d48e4833b5c59a8a6e40cdea3936b99e199dbbe5784 sqlite-amalgamation-3460100.zip +6c35bc5f7f85eac9c49928bacbb02bb694b547aabf69197e058cca245ad80e83 sqlite3.c +89b62c671c5964e137409ce034941b7b05a3af2c9875aba41f47f9483d0c2515 sqlite3.h +b184dd1586d935133d37ad76fa353faf0a1021ff2fdedeedcc3498fff74bbb94 sqlite3ext.h +``` + +## Runtime compile flags + +The amalgamation is included by `src/lib/uce_lib.cpp` with: + +```cpp +#define SQLITE_THREADSAFE 1 +#define SQLITE_OMIT_LOAD_EXTENSION 1 +#define SQLITE_DQS 0 +#define SQLITE_DEFAULT_FOREIGN_KEYS 1 +#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1 +``` + +No patch diff is needed because the vendored SQLite files are unmodified. diff --git a/docs/react-developer-affordances-todo.md b/docs/react-developer-affordances-todo.md new file mode 100644 index 0000000..e154a1a --- /dev/null +++ b/docs/react-developer-affordances-todo.md @@ -0,0 +1,147 @@ +# React Developer Affordances Todo + +## Objective + +Add practical value for developers coming from React frameworks while preserving UCE's server-first C++ model. Defer component syntax/children work, avoid global head/assets/islands in the runtime, and focus on function-library data helpers, diagnostics, docs, examples, demos, and a starter-local router with starter-local asset/island components. + +## Success Criteria + +- [x] Function library has useful collection/data-shaping helpers with docs and tests. +- [x] Compile/runtime diagnostics are more helpful, especially for generated-code and common preprocessor mistakes. +- [x] Docs include a concise React/Next/Remix orientation guide. +- [x] Starter example uses a centralized hierarchical/file-based router in `index.uce` efficiently. +- [x] Starter-local asset/island affordances live as component handlers in the starter, not global runtime APIs. +- [x] Network tests and relevant build checks pass on `k-uce`. + +## Current State + +- Status: complete +- Last updated: 2026-05-28 +- Source of truth: `/root/mount_ssh/k-uce-root-htdocs-uce` +- Runtime/live target: `k-uce:/Code/uce.openfu.com/uce`; rebuilt and restarted `uce.service` on `k-uce`. + +## Goal Tree + +Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded + +- [x] G1: Add collection/data helpers to function library + - Why: React-framework developers routinely shape arrays/objects near render code. + - Done when: helpers are declared, implemented, documented, and covered by tests. + - Verify: build plus focused site/network tests. + - [x] G1.1: Identify current `StringList`/`DTree` idioms and choose helper surface. + - [x] G1.2: Implement minimal high-value helpers without broad template complexity. + - [x] G1.3: Add docs and examples for helpers. + - [x] G1.4: Add/extend tests. +- [x] G2: Improve developer diagnostics + - Why: React frameworks win by making failures easy to act on. + - Done when: compile/runtime error output includes actionable context and docs mention debugging flow. + - Verify: intentional broken page surfaces improved message. + - [x] G2.1: Inspect current compiler/runtime error rendering path. + - [x] G2.2: Add source excerpt / generated path / common-hint text where appropriate. + - [x] G2.3: Document diagnostics. +- [x] G3: Add React/Next/Remix orientation docs + - Why: mapping familiar concepts reduces onboarding cost without adding syntax. + - Done when: docs page exists and is linked from docs/README/demo surfaces. + - Verify: docs page renders live. +- [x] G4: Starter-local router and starter affordances + - Why: User specifically wants hierarchical/file-based routing beautifully in starter `index.uce`. + - Done when: starter routes go through a central router in `index.uce`, and starter-local asset/island component handlers exist and are used where sensible. + - Verify: key starter routes render 200. + - [x] G4.1: Inspect current starter routing. + - [x] G4.2: Refactor to clear route table / hierarchical file resolution in `index.uce`. + - [x] G4.3: Add starter-local `COMPONENT:asset` / `COMPONENT:island` style handlers in one unit. + - [x] G4.4: Use them efficiently in starter pages/layout. +- [x] G5: Demos and examples + - Why: Affordances must be visible to developers, not hidden in APIs. + - Done when: docs/demo/tests expose examples. + - Verify: demo URLs return 200 and tests pass. +- [x] G6: Verification and project docs + - Done when: build/test commands are run on `k-uce`, project notes updated, and adversarial review completed. + +## Execution Queue + +Complete. + +## Decisions + +- 2026-05-28: Defer component children/slots and JSX-like preprocessor syntax. +- 2026-05-28: Do not add global runtime head/assets/islands APIs; implement asset/island as starter-local components. +- 2026-05-28: Do not add a generic runtime file-based-routing system; demonstrate hierarchical/file routing inside starter `index.uce`. +- 2026-05-28: Keep collection helpers explicit (`list_*`, `dtree_*`) instead of overloading generic names such as `map`/`sort`. + +## Assumptions + +- Current source-of-truth mount is live-editable; runtime validation requires SSH to `k-uce`. +- Existing site tests are the right place for function-library coverage. + +## Blockers and Risks + +- No current blockers. +- Future risk: if UCE grows a full parser or component-tag syntax, keep this pass's explicit component/router APIs as a stable lower-level fallback. + +## Evidence and Verification Log + +- 2026-05-28: Created plan after reviewing README, preprocessor docs, and function library headers. +- 2026-05-28: `ssh k-uce 'cd /Code/uce.openfu.com/uce && bash scripts/build_linux.sh'` succeeded. +- 2026-05-28: Restarted `uce.service` on `k-uce`. +- 2026-05-28: `tests/run_network_tests.py --match core` passed. +- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dtree_group_by`. +- 2026-05-28: Full internal network suite passed, 25/25. +- 2026-05-28: Temporary broken `/tests/diagnostic-probe.uce` returned `500` with formatted `UCE compile error` diagnostics; source and cache artifacts were removed afterward. + +## Change Log + +- 2026-05-28: Created initial goal tree. +- 2026-05-28: Implemented helpers, diagnostics, docs, demo, starter router, starter-local web affordance components, tests, and validation. + +## Follow-up Cleanup 2026-05-29 + +- Removed duplicate route cleanup from `starter_router_candidates()` because `app_make_route()` is the single normalization point for `l_path`. +- Weeded out nearby duplicate/obsolete starter code: + - `starter_router_add_candidate(...)` now owns repeated candidate tree construction. + - Removed unused `app_resolve_view()` / `starter_resolve_view()` after moving routing into starter `index.uce`. + - Removed unused legacy registered asset rendering functions from `lib/app.uce`; registered assets now render through `components/theme/web_affordances.uce`. + - `app_init()` now reuses `app_base_url(context)` instead of repeating base URL derivation. + - `web_affordances.uce` now uses one `starter_render_asset_group(...)` loop for CSS and JS. +- Verification: rebuilt on `k-uce`, restarted `uce.service`, checked key starter routes, and ran `tests/run_network_tests.py --match core` successfully. + +## Follow-up Routed Views 2026-05-29 + +- Changed starter route dispatch from `unit_render(...)` to `component(...)`. +- Converted all routed `site/examples/uce-starter/views/*.uce` files to `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are central-router-only views. +- Updated the starter README and verified key starter routes. No service restart was required because only `.uce`/docs changed. + +## Follow-up Canonical Starter URLs 2026-05-29 + +- Canonicalized starter self-links from `/examples/uce-starter/index.uce?...` to `/examples/uce-starter/?...` with `app_canonical_script_url(...)`. +- Updated the starter README to show canonical directory URLs. +- Touched the starter front controller to force the `#load`ed helper change into the cached generated unit. +- Verified canonical/direct starter routes and checked generated self-links. No service restart was required. + +## Follow-up Not Found Component 2026-05-29 + +- Moved `starter_router_render_not_found` markup into `components/basic/notfound.uce`. +- Router now delegates 404 body rendering through `component("components/basic/notfound", props, context)`. +- Verified missing routes return `404` and normal dashboard route returns `200`. No service restart required. + +## Follow-up Page Shell Component 2026-05-29 + +- Moved app page rendering into `themes/page.uce` as a component. +- Removed `app_render_page`, `app_theme_page_component`, and `starter_render_page` from `lib/app.uce`. +- Page template resolution now follows `context.call["app"]["page_type"]`: current theme first, common fallback second. +- Verified representative HTML routes and the JSON page-type fallback. No service restart required. + +## Follow-up Deep Starter Context Cleanup 2026-05-29 + +- Removed repeated `starter_boot(context)` calls; root `index.uce` is the boot point. +- Removed `context.call["starter"]` duplicated state and JSON side-channel state. +- JSON routes now use only `context.call["app"]["page_type"]` plus normal captured output. +- Simplified `themes/page.uce` and `themes/common/page.json.uce` accordingly. +- Replaced `starter_*` alias helper usage with direct `app_*` helpers and removed alias wrappers except `StarterUser`. +- Verified key routes and core tests. No service restart required. + +## Follow-up Route Context Flattening 2026-05-29 + +- Flattened `context.call["app"]["route"]` to `context.call["route"]`. +- Moved former `context.call["app"]["router"]` metadata into `context.call["route"]`. +- Verified representative starter HTML, 404, and JSON routes. No service restart required. diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh index d45faae..0a6cea8 100755 --- a/scripts/build_linux.sh +++ b/scripts/build_linux.sh @@ -17,8 +17,19 @@ FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math" LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`" SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\"" -echo "Compliling executable..." -time -p $COMPILER src/linux_fastcgi.cpp $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1 +echo "Compiling SQLite..." +clang -g -O2 -fPIC \ + -DSQLITE_THREADSAFE=1 \ + -DSQLITE_OMIT_LOAD_EXTENSION=1 \ + -DSQLITE_DQS=0 \ + -DSQLITE_DEFAULT_FOREIGN_KEYS=1 \ + -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \ + -c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1 +if [ $? -ne 0 ]; then exit 1; fi + + +echo "Compiling executable..." +time -p $COMPILER src/linux_fastcgi.cpp bin/sqlite3.o $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1 if [ $? -eq 0 ] then diff --git a/site/demo/collections.uce b/site/demo/collections.uce new file mode 100644 index 0000000..27c4adc --- /dev/null +++ b/site/demo/collections.uce @@ -0,0 +1,41 @@ +#include "demo_guard.h" + +RENDER(Request& context) +{ + DTree p; + p.set(context.params); + + StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"}; + StringList unique_routes = list_unique(routes); + StringList sorted_routes = list_sort(unique_routes); + StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); }); + + DTree cards; + DTree card; + card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear(); + card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear(); + card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card); + + DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); }); + DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); }); + DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); }); + + <> + + + + + +

UCE Test SuiteCollection Docs →

+

Collection Helpers

+

Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.

+

StringList route labels

+

DTree app card titles

+

Grouped cards

+
+ Request Parameters +
+
+ + +} diff --git a/site/demo/index.uce b/site/demo/index.uce index 83b678b..211934d 100644 --- a/site/demo/index.uce +++ b/site/demo/index.uce @@ -42,6 +42,7 @@ RENDER(Request& context) + @@ -60,6 +61,7 @@ RENDER(Request& context) +
Advanced
diff --git a/site/demo/sqlite.uce b/site/demo/sqlite.uce new file mode 100644 index 0000000..f05d9de --- /dev/null +++ b/site/demo/sqlite.uce @@ -0,0 +1,54 @@ +#include "demo_guard.h" + +RENDER(Request& context) +{ + if(!test_demo_request_allowed(context)) + { + test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database"); + return; + } + + String db_path = "/tmp/uce-demo-sqlite.sqlite"; + SQLite* db = sqlite_connect(db_path); + sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)"); + + if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "") + { + StringMap params; + params["body"] = trim(context.post["body"]); + params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S"); + sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params); + sqlite_disconnect(db); + redirect("sqlite.uce", 303); + return; + } + + DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10"); + String error = sqlite_error(db); + ?> + + + + + +

UCE Demo / SQLite

+
+

This demo stores rows in with runtime SQLite helpers and named prepared parameters.

+
+ + +
+
+
+
+ +
+ # + +
+ +
+ + types +request_context_params +request_script_url +request_base_url +request_query_path +request_query_route set_status component +component_render unit_render -ws_message +unit_call session_start +set_cookie +parse_query +parse_multipart +ws_message +ws_connection_id +ws_connections +ws_send +0_DTree +StringMap +UploadedFile :content -`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.props`, and `context.connection`. +`Request& context` is the request-local state object passed into every UCE handler: -## Core Fields +```cpp +RENDER(Request& context) { ... } +COMPONENT(Request& context) { ... } +WS(Request& context) { ... } +CLI(Request& context) { ... } +SERVE_HTTP(Request* req) { ... } +``` -- `ServerState* server`: current server state -- `StringMap params`: FastCGI server parameters -- `StringMap get`: current request GET variables -- `StringMap post`: current request POST variables -- `StringMap cookies`: cookies sent by the browser -- `StringMap session`: current session data -- `String session_id`: session cookie ID -- `String session_name`: session cookie name -- `DTree cfg`: request-local configuration tree -- `DTree call`: invocation-local structured data for helpers such as component and unit calls -- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket -- `std::vector uploaded_files`: files uploaded in the current request -- `StringMap header`: headers to send back to the browser -- `StringList set_cookies`: cookies queued for the response -- `u64 random_seed`: current request noise seed -- `u64 random_index`: current request noise index position +It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics. -## Response Control +## Handler Lifetime -`context.set_status(s32 code[, String reason])` sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes. +A fresh `Request` is created for each HTTP/CLI/custom-server request. Component and unit calls normally share that same object, so state placed on `context.call`, `context.header`, `context.session`, or `context.cfg` is visible to later components in the same request. -## Flags And Stats +For WebSockets, each incoming message is delivered as its own `Request`, but `context.connection` points at broker-owned per-socket state that persists for the lifetime of that WebSocket connection. -- `bool flags.log_request`: controls whether the request should be logged -- `u32 stats.bytes_written` -- `f64 stats.time_init` -- `f64 stats.time_start` -- `f64 stats.time_end` +`ONCE(Request& context)` hooks run once per request, per resolved unit file, before the first `RENDER`, `COMPONENT`, `CLI`, or matching entrypoint from that unit. -## Common Usage Notes +## Incoming Request Maps -- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads. -- `context.props` carries invocation data for component calls and unit calls. -- `context.in` carries the current request body, and for `WS(Request& context)` it is the current WebSocket message payload. -- `context.params["WS_..."]` exposes the current WebSocket message metadata directly on the request parameter map. -- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection. +### `context.params` — server/runtime parameters -`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context. +Type: `StringMap` + +This is the low-level parameter map from FastCGI/direct HTTP plus UCE-populated convenience fields. It is closest to PHP `$_SERVER`. + +Common CGI/FastCGI-style keys include: + +- `REQUEST_METHOD`: `GET`, `POST`, etc. +- `REQUEST_URI`: raw request URI where available +- `DOCUMENT_URI`: normalized request path where available +- `SCRIPT_NAME`: script path where available +- `SCRIPT_FILENAME`: resolved filesystem path of the active UCE unit +- `QUERY_STRING`: raw query string +- `DOCUMENT_ROOT`: web root used by the frontend/backend +- `CONTENT_TYPE`: request body content type +- `CONTENT_LENGTH`: request body length +- `HTTP_COOKIE`: raw cookie header +- `HTTP_HOST`, `HTTP_USER_AGENT`, `HTTP_ACCEPT`, and other `HTTP_...` headers supplied by the frontend + +UCE also populates convenience route/link fields before handlers run: + +- `SCRIPT_URL`: canonical script URL; `/index.uce` is collapsed to the containing directory URL +- `BASE_URL`: canonical directory URL for the script +- `ROUTE_PATH`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied; empty when unsafe input was rejected +- `ROUTE_PAGE`: first segment of `ROUTE_PATH` +- `ROUTE_PATH_RAW`: normalized but untrusted route input, for diagnostics only +- `ROUTE_VALID`: `1` when route input is safe, `0` when the supplied route was rejected + +See `request_context_params`, `request_script_url`, `request_base_url`, `request_query_path`, `request_query_route`, and `route_path_sanitize`. + +### `context.get` + +Type: `StringMap` + +Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`. + +```cpp +String theme = first(context.get["theme"], "default"); +``` + +For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`. + +### `context.post` + +Type: `StringMap` + +Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`. + +```cpp +if(context.params["REQUEST_METHOD"] == "POST") + String email = context.post["email"]; +``` + +### `context.cookies` + +Type: `StringMap` + +Cookies sent by the client, parsed from `HTTP_COOKIE`. `set_cookie()` also updates this map after queuing a response cookie, so later code in the same request can observe the new value. + +### `context.in` + +Type: `String` + +Raw request body. For WebSocket handlers, this is the current message payload. + +Use this for JSON APIs: + +```cpp +DTree body = json_decode(context.in); +``` + +### `context.uploaded_files` + +Type: `std::vector` + +Each `UploadedFile` contains: + +- `file_name`: original submitted filename +- `tmp_name`: temporary server-side upload path +- `size`: uploaded byte count + +Use this with multipart form posts. + +## Session State + +### `context.session` + +Type: `StringMap` + +Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data. + +```cpp +session_start(); +context.session["user_id"] = "42"; +``` + +At the end of a successful request, modified session data is saved automatically if a session is active. + +### `context.session_id` and `context.session_name` + +The active session ID and cookie name after `session_start()`. + +Related helpers: + +- `session_start()` +- `session_destroy()` +- `session_id_create()` +- `set_cookie()` + +## Per-request Structured Trees + +### `context.call` + +Type: `DTree` + +General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components. + +Examples from front-controller style apps: + +```cpp +context.call["route"] = request_query_route(context); +context.call["app"]["page_type"] = "html"; +context.call["fragments"]["main"] = captured_html; +``` + +Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`). + +### `context.cfg` + +Type: `DTree` + +Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup: + +```cpp +context.cfg = get_config(); +``` + +This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`. + +Use `get_by_path()` for non-mutating deep reads: + +```cpp +String site_name = context.cfg.get_by_path("site/name").to_string(); +``` + +### `context.props` + +Type: `DTree` + +Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns. + +```cpp +DTree props; +props["title"] = "Dashboard"; +print(component("components/card", props, context)); +``` + +### `context.connection` + +Type: `DTree` + +WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket. + +```cpp +context.connection["message_count"] = context.connection["message_count"].to_u64() + 1; +``` + +Only meaningful for WebSocket handlers. + +## Response State + +### `context.response_code` + +Type: `String` + +The raw status line. Usually use `context.set_status(...)` instead of writing this directly. + +### `context.header` + +Type: `StringMap` + +Response headers to emit. Header names are case-sensitive as written. + +```cpp +context.header["Content-Type"] = "application/json"; +context.header["Location"] = "/info/"; +``` + +### `context.set_cookies` + +Type: `StringList` + +Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this directly. + +### `context.set_status(code[, reason])` + +Sets the HTTP response status and `context.flags.status`. + +```cpp +context.set_status(404, "Not Found"); +context.set_status(302, "Found"); +context.header["Location"] = app_link("dashboard", context); +``` + +Related helpers: + +- `redirect(url[, code])` +- `set_cookie(...)` + +## Output Buffers + +### `context.ob_stack` and `context.ob` + +Internal output-buffer stack. Most code should use helpers instead of touching these directly: + +- `print(...)` +- `out(...)` +- `ob_start()` +- `ob_get()` +- `ob_get_close()` +- `ob_close()` + +Common capture pattern: + +```cpp +ob_start(); +print(component("views/dashboard", context)); +String html = ob_get_close(); +context.call["fragments"]["main"] = html; +``` + +### `context.out` and `context.err` + +Runtime output/error artifacts used by some transports and failure paths. Normal page rendering should use `print()` / output buffers. + +## Request Flags + +`context.flags` contains runtime booleans and the numeric status: + +- `log_request`: whether the request should be logged +- `is_finished`: internal completion marker +- `status`: numeric HTTP status set by `set_status()` +- `output_closed`: internal transport state +- `params_closed`: internal transport state +- `input_closed`: internal transport state + +Most page code only reads `flags.status`, if anything. + +## Request Stats + +`context.stats` contains counters/timing for the request: + +- `bytes_written` +- `time_init` +- `time_start` +- `time_end` +- `mem_high` +- `mem_alloc` +- `invoke_count` + +These are useful for diagnostics, demos, and runtime instrumentation. + +## Random / Noise State + +- `random_seed` +- `random_index` + +Used by UCE noise/random helpers to provide request-local deterministic progression. + +Related helpers include functions in the noise/hash area such as `gen_int`, `gen_float`, `gen_noise64`, and `gen_sha1`. + +## WebSocket Fields + +In `WS(Request& context)`, the runtime mirrors WebSocket metadata into `context.params` and `context.resources`. + +Convenience `context.params` keys include: + +- `WS_MESSAGE` +- `WS_CONNECTION_ID` +- `WS_SCOPE` +- `WS_CONNECTION_COUNT` +- `WS_OPCODE` +- `WS_MESSAGE_TYPE` +- `WS_DOCUMENT_URI` + +Prefer WebSocket helper functions where possible: + +- `ws_message()` +- `ws_connection_id()` +- `ws_scope()` +- `ws_opcode()` +- `ws_is_binary()` +- `ws_connections()` +- `ws_connection_count()` +- `ws_send()` +- `ws_send_to()` +- `ws_close()` + +Use `context.connection` for per-socket structured state. + +## Runtime / Resource Fields + +### `context.server` + +Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at: + +```cpp +context.server->config["KEY"] +``` + +This is a `StringMap`, separate from app-owned `context.cfg`. + +### `context.resources` + +Internal runtime resources and transport state. Includes sockets, MySQL handles, WebSocket state, current unit file, and parser buffers. Application code should normally use public helpers instead of editing this directly. + +Notable fields: + +- `is_websocket` +- `is_cli` +- `websocket_connection_id` +- `websocket_scope` +- `websocket_scope_connection_ids` +- `current_unit_file` + +## Common Patterns + +### Minimal page + +```cpp +RENDER(Request& context) +{ + <>

Hello

+} +``` + +### JSON endpoint + +```cpp +RENDER(Request& context) +{ + context.header["Content-Type"] = "application/json"; + DTree response; + response["ok"].set_bool(true); + print(json_encode(response)); +} +``` + +### Redirect + +```cpp +RENDER(Request& context) +{ + context.set_status(302, "Found"); + context.header["Location"] = "/info/"; +} +``` + +### Component props + +```cpp +DTree props; +props["title"] = "Welcome"; +print(component("components/card", props, context)); +``` + +### Front-controller route + +```cpp +context.call["route"] = request_query_route(context); +String route_path = context.call["route"]["l_path"].to_string(); +``` + +Or use runtime-populated params directly: + +```cpp +String route_path = context.params["ROUTE_PATH"]; +``` ## Related Concepts -- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, and `http_response_code()` -- JavaScript / Node.js: Express `req` and `res`, Fetch `Request`, `Headers`, cookies or session middleware, and per-connection state in WebSocket handlers +- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()` +- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state diff --git a/site/doc/pages/3_C++ Preprocessor.txt b/site/doc/pages/3_C++ Preprocessor.txt index f2cc616..ce54879 100644 --- a/site/doc/pages/3_C++ Preprocessor.txt +++ b/site/doc/pages/3_C++ Preprocessor.txt @@ -26,6 +26,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi - Inside a literal block, `` emits `print(expression);` without HTML escaping. - `#load "other.uce"` injects another UCE unit at compile time. - `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`. +- `ONCE`, `RENDER`, and `COMPONENT` may be followed by a preprocessor attribute line such as `@fragment head` before the opening `{`. The handler's output is then captured and appended to `context.call["fragments"]["head"]` instead of being emitted at the call site. `ONCE` defaults to `@fragment once` when no fragment is specified. - `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler. - `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata. @@ -42,6 +43,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi - `` becomes `print(...);` and is intended for trusted markup or already-escaped content. - `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`. - Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file. +- `@fragment slot-name` lines immediately following `ONCE`, `RENDER`, or `COMPONENT` are removed and replaced with an output-capture guard at the start of the handler body. - Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions. - Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers. - The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`. @@ -130,6 +132,18 @@ ONCE(Request& context) } ``` +One-time page assets captured for a template-controlled slot: + +```cpp +ONCE(Request& context) +@fragment head +{ + ?>`. + ## Rules - Literal mode can start on either `<>` or `?>`. @@ -155,6 +169,8 @@ ONCE(Request& context) - Inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor. - Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems. +- Compile failures are reported with the source path, generated C++ path, compile-output artifact path, an excerpt when UCE can identify a line, and the raw compiler output from the configured compile script. +- Runtime request failures include the request/script path, generated C++ path, a hint about inspecting template delimiters and recent component/unit calls, and a native trace when available. - If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`. ## Related Concepts diff --git a/site/doc/pages/coming_from_react.txt b/site/doc/pages/coming_from_react.txt new file mode 100644 index 0000000..78c509e --- /dev/null +++ b/site/doc/pages/coming_from_react.txt @@ -0,0 +1,69 @@ +:title +Coming from React, Next, or Remix + +:sig +UCE orientation for React-framework developers + +:see +1_RENDER +1_COMPONENT +component +unit_render +3_C++ Preprocessor +map +filter +dtree_filter + +:content +UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly. + +## Concept Map + +- `RENDER(Request& context)` is the page/server-render entrypoint. +- `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)` are server-rendered components. +- `context.props` is the component invocation payload, similar to props. +- `context.call` is request-local scratch state shared across units during one request. +- `context.cfg` is structured app/config data. +- `ONCE(Request& context)` is per-request setup for a unit before its first render/component entry. +- `INIT(Request& context)` is worker-local setup when a unit is loaded. +- `` is escaped interpolation; prefer it for user-visible text. +- `` is trusted raw markup output, closer to a deliberate `dangerouslySetInnerHTML` decision. +- `unit_render()` renders another page unit; `component()` returns component HTML as a string. + +## Routes and Layouts + +UCE does not require a framework-level router. A front controller can keep routing explicit and app-local. The starter example demonstrates this in `site/examples/uce-starter/index.uce`: it resolves a request path by checking: + +1. `views/.uce` +2. `views//index.uce` +3. parent index handlers such as `views/workspace/index.uce` with the last segment as a route parameter + +That keeps file-based and hierarchical routing in normal UCE code instead of hiding it in the runtime. + +## Data Shaping Near Render Code + +The function library includes small collection helpers for common route/menu/card transformations: + +```cpp +auto visible = filter(routes, [](String route) { return(route != "admin"); }); +auto labels = map(visible, [](String route) { return(to_upper(route)); }); +DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); }); +DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); }); +``` + +Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern. + +## Assets and Islands + +Global runtime APIs for assets and islands are intentionally not part of UCE core. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The only starter web-affordance helper left is `COMPONENT:island` in `components/theme/web_affordances.uce` for small progressive-enhancement modules. This keeps app policy in the app without an asset registry layer. + +## Debugging + +When a unit fails to compile, UCE reports the source path, generated C++ path, compile-output artifact, a source/generated excerpt when it can identify a line, and the raw compiler output. The generated C++ under `BIN_DIRECTORY` is the source of truth for what the configured compiler actually saw. + +## What Not To Expect + +- No client-side virtual DOM is built into UCE. +- No global file-router is imposed by the runtime. +- No JSX-like component tags are required for this workflow. +- Component children/slot syntax is intentionally deferred; use explicit props and component calls for now. diff --git a/site/doc/pages/dtree_filter.txt b/site/doc/pages/dtree_filter.txt new file mode 100644 index 0000000..7cedab5 --- /dev/null +++ b/site/doc/pages/dtree_filter.txt @@ -0,0 +1,26 @@ +:title +dtree_filter + +:sig +DTree dtree_filter(DTree tree, function f) + +:see +StringList +0_DTree +filter + +:content +Keeps children for which f returns true. List-like input stays list-like. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +DTree visible = dtree_filter(items, [](DTree item, String key) { return(item["hidden"].to_bool() == false); }); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dtree_group_by.txt b/site/doc/pages/dtree_group_by.txt new file mode 100644 index 0000000..310c44d --- /dev/null +++ b/site/doc/pages/dtree_group_by.txt @@ -0,0 +1,26 @@ +:title +dtree_group_by + +:sig +DTree dtree_group_by(DTree tree, function f) + +:see +StringList +0_DTree +filter + +:content +Groups children into list-like buckets by the string returned from f. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); }); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dtree_keys.txt b/site/doc/pages/dtree_keys.txt new file mode 100644 index 0000000..9175a83 --- /dev/null +++ b/site/doc/pages/dtree_keys.txt @@ -0,0 +1,26 @@ +:title +dtree_keys + +:sig +StringList dtree_keys(DTree tree) + +:see +StringList +0_DTree +filter + +:content +Returns map keys from a DTree. Scalar values produce an empty list. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +StringList keys = dtree_keys(context.cfg["menu"]); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dtree_map.txt b/site/doc/pages/dtree_map.txt new file mode 100644 index 0000000..597fc5d --- /dev/null +++ b/site/doc/pages/dtree_map.txt @@ -0,0 +1,26 @@ +:title +dtree_map + +:sig +DTree dtree_map(DTree tree, function f) + +:see +StringList +0_DTree +filter + +:content +Transforms each child. List-like input stays list-like; map input keeps keys. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +DTree titles = dtree_map(items, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); }); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dtree_omit.txt b/site/doc/pages/dtree_omit.txt new file mode 100644 index 0000000..91a1f30 --- /dev/null +++ b/site/doc/pages/dtree_omit.txt @@ -0,0 +1,26 @@ +:title +dtree_omit + +:sig +DTree dtree_omit(DTree tree, StringList keys) + +:see +StringList +0_DTree +filter + +:content +Copies a DTree map except for selected keys. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +DTree safe_user = dtree_omit(user, {"password_hash"}); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dtree_pick.txt b/site/doc/pages/dtree_pick.txt new file mode 100644 index 0000000..e4b530b --- /dev/null +++ b/site/doc/pages/dtree_pick.txt @@ -0,0 +1,26 @@ +:title +dtree_pick + +:sig +DTree dtree_pick(DTree tree, StringList keys) + +:see +StringList +0_DTree +filter + +:content +Copies only selected keys from a DTree map. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +DTree public_user = dtree_pick(user, {"name", "avatar"}); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dtree_values.txt b/site/doc/pages/dtree_values.txt new file mode 100644 index 0000000..b8dcd91 --- /dev/null +++ b/site/doc/pages/dtree_values.txt @@ -0,0 +1,26 @@ +:title +dtree_values + +:sig +DTree dtree_values(DTree tree) + +:see +StringList +0_DTree +filter + +:content +Returns child values as a list-like DTree. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +DTree menu_items = dtree_values(context.cfg["menu"]); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/list_every.txt b/site/doc/pages/list_every.txt new file mode 100644 index 0000000..5e26b8a --- /dev/null +++ b/site/doc/pages/list_every.txt @@ -0,0 +1,26 @@ +:title +list_every + +:sig +bool list_every(StringList items, function f) + +:see +StringList +0_DTree +filter + +:content +Returns true when every item matches. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +bool all_named = list_every(routes, [](String s) { return(s != ""); }); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/list_filter.txt b/site/doc/pages/list_filter.txt new file mode 100644 index 0000000..705fea3 --- /dev/null +++ b/site/doc/pages/list_filter.txt @@ -0,0 +1,18 @@ +:title +list_filter + +:sig +Use filter(StringList items, function f) + +:see +filter +StringList + +:content +`list_filter()` was removed because `StringList` is `std::vector` and the generic `filter()` helper covers the same behavior without a second implementation to maintain. + +Use: + +```cpp +auto visible = filter(routes, [](String s) { return(s != "admin"); }); +``` diff --git a/site/doc/pages/list_find.txt b/site/doc/pages/list_find.txt new file mode 100644 index 0000000..3cc88d0 --- /dev/null +++ b/site/doc/pages/list_find.txt @@ -0,0 +1,26 @@ +:title +list_find + +:sig +String list_find(StringList items, function f, String fallback = "") + +:see +StringList +0_DTree +filter + +:content +Returns the first matching item or fallback. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +String route = list_find(routes, [](String s) { return(str_starts_with(s, "dashboard")); }, "index"); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/list_map.txt b/site/doc/pages/list_map.txt new file mode 100644 index 0000000..3a918fb --- /dev/null +++ b/site/doc/pages/list_map.txt @@ -0,0 +1,19 @@ +:title +list_map + +:sig +Use map(StringList items, function f) + +:see +map +filter +StringList + +:content +`list_map()` was removed because `StringList` is `std::vector` and the generic `map()` helper covers the same behavior without a second implementation to maintain. + +Use: + +```cpp +auto upper = map(names, [](String s) { return(to_upper(s)); }); +``` diff --git a/site/doc/pages/list_some.txt b/site/doc/pages/list_some.txt new file mode 100644 index 0000000..3fbfa8a --- /dev/null +++ b/site/doc/pages/list_some.txt @@ -0,0 +1,26 @@ +:title +list_some + +:sig +bool list_some(StringList items, function f) + +:see +StringList +0_DTree +filter + +:content +Returns true when any item matches. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +bool has_dashboard = list_some(routes, [](String s) { return(s == "dashboard"); }); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/list_sort.txt b/site/doc/pages/list_sort.txt new file mode 100644 index 0000000..8aabcde --- /dev/null +++ b/site/doc/pages/list_sort.txt @@ -0,0 +1,26 @@ +:title +list_sort + +:sig +StringList list_sort(StringList items) + +:see +StringList +0_DTree +filter + +:content +Returns a sorted copy of the list. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +auto sorted = list_sort(tags); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/list_unique.txt b/site/doc/pages/list_unique.txt new file mode 100644 index 0000000..0f97094 --- /dev/null +++ b/site/doc/pages/list_unique.txt @@ -0,0 +1,26 @@ +:title +list_unique + +:sig +StringList list_unique(StringList items) + +:see +StringList +0_DTree +filter + +:content +Returns the first occurrence of each string, preserving input order. + +These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. + +```cpp +auto tags = list_unique({"uce", "docs", "uce"}); +``` + +Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. + +## Related Concepts + +- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. +- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/map.txt b/site/doc/pages/map.txt new file mode 100644 index 0000000..98272b6 --- /dev/null +++ b/site/doc/pages/map.txt @@ -0,0 +1,31 @@ +:title +map + +:sig +StringList map(StringList items, function f) +vector map(vector items, function f) + +:params +items : list of items to transform +f : a function that returns the transformed value for each item +return value : a new list containing the transformed values + +:see +>string +filter +StringList +0_DTree + +:content +Returns a new list by calling `f` for each item in `items`. + +```cpp +auto upper = map(names, [](String s) { return(to_upper(s)); }); +``` + +Use `filter()` when you want to keep only matching items, and `map()` when you want to transform each item. + +## Related Concepts + +- JavaScript / React: `Array.prototype.map()` +- PHP: `array_map()` diff --git a/site/doc/pages/mysql_query.txt b/site/doc/pages/mysql_query.txt index 88372e3..298f69f 100644 --- a/site/doc/pages/mysql_query.txt +++ b/site/doc/pages/mysql_query.txt @@ -13,7 +13,16 @@ return value : a list of rows returned from executing the query :content Executes a MySQL query and returns the resulting data, if any. -`params` provides the query parameter values used by the statement. +`params` provides the query parameter values used by the statement. Use named `:name` placeholders only; positional `?` placeholders are rejected. + +```cpp +StringMap params; +params["email"] = "ada@example.test"; +DTree rows = mysql_query(m, + "select id, email from users where email = :email", + params +); +``` The result is returned as a `DTree`, which makes it easy to iterate through rows and read fields with the usual `DTree` accessors. diff --git a/site/doc/pages/parse_query.txt b/site/doc/pages/parse_query.txt index 3698940..77084f2 100644 --- a/site/doc/pages/parse_query.txt +++ b/site/doc/pages/parse_query.txt @@ -7,10 +7,31 @@ return value : a StringMap containing the parameters :see >uri +encode_query +request_context_params :content Decodes a query-string fragment such as `a=b&c=d` into a `StringMap`. +Parsing rules: + +- pairs are separated on `&` +- each pair is split on the first raw `=` only +- both key and value are URL-decoded +- keyless flags such as `preview` are present with an empty string value +- empty pairs, including a trailing `&`, are ignored +- repeated keys use the last value seen + +Examples: + +```cpp +StringMap q = parse_query("alpha=1&token=a%3Db&preview&empty="); +// q["alpha"] == "1" +// q["token"] == "a=b" +// q["preview"] == "" +// q["empty"] == "" +``` + This is useful when you need to work with URL parameter data outside the normal request parsing flow. Related: diff --git a/site/doc/pages/request_base_url.txt b/site/doc/pages/request_base_url.txt new file mode 100644 index 0000000..c6d0370 --- /dev/null +++ b/site/doc/pages/request_base_url.txt @@ -0,0 +1,16 @@ +:title +request_base_url + +:sig +String request_base_url(Request& context) + +:see +request_script_url +request_query_route + +:content +Returns the canonical directory base URL for the current script. This is useful for front-controller apps that generate links to sibling assets or query-routed pages. + +```cpp +String base_url = request_base_url(context); +``` diff --git a/site/doc/pages/request_context_params.txt b/site/doc/pages/request_context_params.txt new file mode 100644 index 0000000..a83d61a --- /dev/null +++ b/site/doc/pages/request_context_params.txt @@ -0,0 +1,28 @@ +:title +Request Context Params + +:sig +SCRIPT_URL, BASE_URL, ROUTE_PATH, ROUTE_PAGE, ROUTE_PATH_RAW, ROUTE_VALID + +:see +request_script_url +request_base_url +request_query_path +request_query_route +route_path_sanitize +route_path_is_safe +0_Request + +:content +UCE populates several convenience request parameters before invoking page, component, CLI, custom HTTP, or WebSocket handlers: + +- `context.params["SCRIPT_URL"]`: canonical script URL, with `/index.uce` collapsed to the containing directory URL +- `context.params["BASE_URL"]`: canonical directory URL for the script +- `context.params["ROUTE_PATH"]`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied +- `context.params["ROUTE_PAGE"]`: first segment of `ROUTE_PATH` +- `context.params["ROUTE_PATH_RAW"]`: normalized but not trusted route input, for diagnostics only +- `context.params["ROUTE_VALID"]`: `1` when the supplied/defaulted route is safe, `0` when the supplied route was rejected + +`ROUTE_PATH` is safe to compose under an application-controlled route root. Unsafe route input such as `..`, `.`, empty interior segments, backslashes, dots in filenames, or other non route-segment characters is rejected by the runtime and yields an empty `ROUTE_PATH` with `ROUTE_VALID=0`. + +These are useful for front-controller apps that use URLs like `/?dashboard` or `/?workspace/projects` while still accepting ordinary named query parameters. diff --git a/site/doc/pages/request_query_path.txt b/site/doc/pages/request_query_path.txt new file mode 100644 index 0000000..dd73d26 --- /dev/null +++ b/site/doc/pages/request_query_path.txt @@ -0,0 +1,27 @@ +:title +request_query_path + +:sig +String request_query_path(Request& context, String default_path = "index") + +:see +request_query_route +request_context_params +route_path_sanitize +route_path_is_safe +parse_query + +:content +Returns the first keyless query-string segment as a sanitized route path. + +For a request such as `/?workspace/projects&theme=dark`, this returns `workspace/projects`. + +When no keyless route is supplied, the result is `default_path`. When unsafe route input is supplied, the result is an empty string. Unsafe route input includes `.` or `..` segments, empty interior segments, backslashes, dots in filenames, or any character outside ASCII letters, digits, `_`, and `-`. + +```cpp +String route_path = request_query_path(context); +if(route_path == "") + context.set_status(404, "Not Found"); +``` + +The runtime-populated `context.params["ROUTE_PATH"]` uses the same sanitizer. diff --git a/site/doc/pages/request_query_route.txt b/site/doc/pages/request_query_route.txt new file mode 100644 index 0000000..3f528dc --- /dev/null +++ b/site/doc/pages/request_query_route.txt @@ -0,0 +1,29 @@ +:title +request_query_route + +:sig +DTree request_query_route(Request& context, String default_path = "index") + +:see +request_query_path +request_context_params +route_path_sanitize +request_script_url + +:content +Builds a small route tree from the first keyless query-string segment. + +Fields: + +- `raw_path`: normalized but untrusted route input, for diagnostics only +- `l_path`: sanitized full route path, or empty string when input was rejected +- `page`: first path segment of `l_path`, or empty string when input was rejected +- `valid`: boolean; true when `l_path` is safe + +```cpp +DTree route = request_query_route(context); +if(route["valid"].to_bool()) + print("route=", route["l_path"].to_string()); +``` + +This supports front-controller apps that use URLs such as `/?dashboard` or `/?workspace/projects` while still allowing ordinary named query parameters alongside the route. The returned `l_path` is already sanitized for composing under an application-controlled route root. diff --git a/site/doc/pages/request_script_url.txt b/site/doc/pages/request_script_url.txt new file mode 100644 index 0000000..3a48351 --- /dev/null +++ b/site/doc/pages/request_script_url.txt @@ -0,0 +1,16 @@ +:title +request_script_url + +:sig +String request_script_url(Request& context) + +:see +request_base_url +request_query_route + +:content +Returns the request script URL from `DOCUMENT_URI` / `SCRIPT_NAME`, canonicalizing a front-controller URL ending in `/index.uce` to the containing directory URL. + +```cpp +String script_url = request_script_url(context); +``` diff --git a/site/doc/pages/route_path_is_safe.txt b/site/doc/pages/route_path_is_safe.txt new file mode 100644 index 0000000..747e100 --- /dev/null +++ b/site/doc/pages/route_path_is_safe.txt @@ -0,0 +1,24 @@ +:title +route_path_is_safe + +:sig +bool route_path_is_safe(String path) + +:see +route_path_sanitize +route_path_normalize +request_query_path +request_query_route + +:content +Returns whether a normalized route path is safe to use as a route-derived file path segment. + +A safe route path contains only non-empty segments made from ASCII letters, digits, `_`, and `-`. The segments `.` and `..` are rejected. + +```cpp +route_path_is_safe("workspace/projects"); // true +route_path_is_safe("workspace/../admin"); // false +route_path_is_safe("view.uce"); // false +``` + +Most request code should use runtime-populated `context.params["ROUTE_PATH"]` or `request_query_route()` instead of calling this directly. diff --git a/site/doc/pages/route_path_normalize.txt b/site/doc/pages/route_path_normalize.txt new file mode 100644 index 0000000..a2bc0ba --- /dev/null +++ b/site/doc/pages/route_path_normalize.txt @@ -0,0 +1,20 @@ +:title +route_path_normalize + +:sig +String route_path_normalize(String path) + +:see +route_path_sanitize +route_path_is_safe +request_query_path +request_query_route + +:content +Trims whitespace and removes leading/trailing `/` from a route path without deciding whether the path is safe. + +```cpp +route_path_normalize(" /workspace/projects/ "); // "workspace/projects" +``` + +For route data that may be used to compose file paths, prefer `route_path_sanitize()` or runtime-populated `context.params["ROUTE_PATH"]`. diff --git a/site/doc/pages/route_path_sanitize.txt b/site/doc/pages/route_path_sanitize.txt new file mode 100644 index 0000000..febe103 --- /dev/null +++ b/site/doc/pages/route_path_sanitize.txt @@ -0,0 +1,34 @@ +:title +route_path_sanitize + +:sig +String route_path_sanitize(String path, String default_path = "index") + +:see +route_path_is_safe +route_path_normalize +request_query_path +request_query_route +request_context_params + +:content +Normalizes and validates a route path for file-backed routing. + +Rules: + +- leading and trailing `/` are removed +- an empty path becomes `default_path` +- every path segment must be non-empty +- `.` and `..` segments are rejected +- only ASCII letters, digits, `_`, and `-` are accepted inside a segment +- unsafe input returns an empty string + +Use this instead of manually checking app routes before composing file paths. + +```cpp +route_path_sanitize("/workspace/projects/"); // "workspace/projects" +route_path_sanitize("../secret"); // "" +route_path_sanitize(""); // "index" +``` + +`request_query_path()`, `request_query_route()`, and the runtime-populated `ROUTE_PATH` params already use this sanitizer. diff --git a/site/doc/pages/sqlite_affected_rows.txt b/site/doc/pages/sqlite_affected_rows.txt new file mode 100644 index 0000000..780bea1 --- /dev/null +++ b/site/doc/pages/sqlite_affected_rows.txt @@ -0,0 +1,19 @@ +:sig +u32 sqlite_affected_rows(SQLite* db) + +:params +db : pointer to an active SQLite connection +return value : number of rows changed by the most recent statement + +:see +>sqlite +sqlite_query +sqlite_insert_id + +:content +Returns the number of rows changed by the most recent insert, update, or delete statement on the connection. + +```cpp +sqlite_query(db, "update users set visits = visits + 1 where id = :id", params); +print(sqlite_affected_rows(db)); +``` diff --git a/site/doc/pages/sqlite_connect.txt b/site/doc/pages/sqlite_connect.txt new file mode 100644 index 0000000..12b22c0 --- /dev/null +++ b/site/doc/pages/sqlite_connect.txt @@ -0,0 +1,28 @@ +:sig +SQLite* sqlite_connect(String path) + +:params +path : filesystem path to the SQLite database file +return value : pointer to a SQLite connection struct + +:see +>sqlite +sqlite_query +sqlite_error +sqlite_disconnect + +:content +Opens an SQLite database file and returns a connection handle. + +UCE opens SQLite with serialized/full-mutex connection mode, sets a busy timeout, enables foreign keys, and applies WAL-oriented defaults: + +```sql +PRAGMA busy_timeout = 5000; +PRAGMA foreign_keys = ON; +PRAGMA journal_mode = WAL; +PRAGMA synchronous = NORMAL; +``` + +SQLite itself handles file locking and one-writer/many-reader concurrency. UCE does not add a separate app-level database lock. + +Connections are registered for request cleanup, but explicit `sqlite_disconnect()` is still preferred when you are done with the handle. diff --git a/site/doc/pages/sqlite_disconnect.txt b/site/doc/pages/sqlite_disconnect.txt new file mode 100644 index 0000000..8cb2537 --- /dev/null +++ b/site/doc/pages/sqlite_disconnect.txt @@ -0,0 +1,14 @@ +:sig +void sqlite_disconnect(SQLite* db) + +:params +db : pointer to an active SQLite connection + +:see +>sqlite +sqlite_connect + +:content +Closes an SQLite connection and deletes the UCE connection wrapper. + +UCE also cleans up SQLite connections that remain open at request end, but explicit disconnects keep resource lifetime local and clear. diff --git a/site/doc/pages/sqlite_error.txt b/site/doc/pages/sqlite_error.txt new file mode 100644 index 0000000..3daeda1 --- /dev/null +++ b/site/doc/pages/sqlite_error.txt @@ -0,0 +1,15 @@ +:sig +String sqlite_error(SQLite* db) + +:params +db : pointer to an SQLite connection +return value : latest connector/SQLite status message + +:see +>sqlite +sqlite_query + +:content +Returns the latest SQLite connector status or error message. + +Successful calls usually set the message to `ok` or `connected`. Failed prepare, bind, step, pragma, or open operations include connector context plus SQLite's own error text. diff --git a/site/doc/pages/sqlite_insert_id.txt b/site/doc/pages/sqlite_insert_id.txt new file mode 100644 index 0000000..093d775 --- /dev/null +++ b/site/doc/pages/sqlite_insert_id.txt @@ -0,0 +1,19 @@ +:sig +u64 sqlite_insert_id(SQLite* db) + +:params +db : pointer to an active SQLite connection +return value : last inserted rowid + +:see +>sqlite +sqlite_query +sqlite_affected_rows + +:content +Returns SQLite's last inserted rowid for the connection after an insert statement. + +```cpp +sqlite_query(db, "insert into notes(body) values(:body)", params); +u64 id = sqlite_insert_id(db); +``` diff --git a/site/doc/pages/sqlite_query.txt b/site/doc/pages/sqlite_query.txt new file mode 100644 index 0000000..8b307af --- /dev/null +++ b/site/doc/pages/sqlite_query.txt @@ -0,0 +1,37 @@ +:sig +DTree sqlite_query(SQLite* db, String q) +DTree sqlite_query(SQLite* db, String q, StringMap params) + +:params +db : pointer to an active SQLite connection +q : SQL statement +params : optional named parameter map +return value : list of result rows as a DTree + +:see +>sqlite +sqlite_connect +sqlite_error +sqlite_insert_id +sqlite_affected_rows +0_DTree + +:content +Executes one SQLite statement and returns result rows as a `DTree` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement. + +Use named parameters with `:name` placeholders only. Positional `?` placeholders and SQLite's other named marker forms (`@name`, `$name`) are rejected so UCE SQLite queries use the same placeholder style as the MySQL helper. UCE binds parameters with SQLite prepared statements; it does not substitute values into the SQL string. + +```cpp +SQLite* db = sqlite_connect("/tmp/app.sqlite"); + +StringMap params; +params["email"] = "ada@example.test"; +DTree rows = sqlite_query(db, + "select id, email from users where email = :email", + params +); +``` + +Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DTree values. Blob values are returned as byte strings. + +For statements that do not return rows, inspect `sqlite_affected_rows()` or `sqlite_insert_id()` after the call. diff --git a/site/examples/uce-starter/README.md b/site/examples/uce-starter/README.md index 991a46a..81517dc 100644 --- a/site/examples/uce-starter/README.md +++ b/site/examples/uce-starter/README.md @@ -17,13 +17,19 @@ Theme rendering is split the same way as the PHP starter: - `themes/common/page.json.uce` - `themes//page.html.uce` -Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` resolves the routed view, captures the `main` fragment, then hands off once into `themes/common/page.*.uce` or `themes//page.html.uce`, matching the PHP starter's page-layer flow without an extra shell implementation. +Those page templates are the authoritative shell layer, and in the UCE port they are proper `COMPONENT(...)` units rather than standalone page entrypoints. `index.uce` owns the app-local router, captures the `main` fragment, then hands off to `themes/page.uce`. That component resolves `context.call["app"]["page_type"]` by checking `themes//page..uce` first and then `themes/common/page..uce`, matching the PHP starter's page-layer flow without an extra shell implementation. -The example uses query-string routing in the same style as the PHP starter: +The router is intentionally normal UCE code instead of a runtime feature. It checks `views/.uce`, then `views//index.uce`, then parent index handlers such as `views/workspace/index.uce` with the last segment stored as `context.call["route"]["param"]`. Matching view files are invoked with `component()`, and view files expose `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are intended to render only through the central router. This gives the starter hierarchical/file-based routing while keeping policy in the app. -- `index.uce` -- `index.uce?page1` -- `index.uce?themes&theme=portal-dark` -- `index.uce?workspace/projects` +Starter-local web affordances live in `components/theme/web_affordances.uce`; currently this provides `COMPONENT:island` for progressive enhancement without adding global UCE runtime APIs. Component-specific CSS/JS is emitted by `ONCE(Request& context)` in the component unit that owns it, or by a small shared asset component when multiple sibling components need the same files. The router's 404 body is also a normal component at `components/basic/notfound.uce`, keeping page fragments out of the front controller. + +The example uses query-string routing in the same style as the PHP starter, but the canonical public URL is the directory path because `index.uce` is reached through nginx's default index/try-file behavior. Links generated by the starter therefore target: + +- `/examples/uce-starter/` +- `/examples/uce-starter/?page1` +- `/examples/uce-starter/?themes&theme=portal-dark` +- `/examples/uce-starter/?workspace/projects` + +Direct requests to `/examples/uce-starter/index.uce` still work, but self-links are canonicalized back to `/examples/uce-starter/`. The demo account pages use a small file-backed user store under `/tmp/uce-starter-data/` with session-based login state. diff --git a/site/examples/uce-starter/components/auth/oauth-client.uce b/site/examples/uce-starter/components/auth/oauth-client.uce index 557c1cc..263d081 100644 --- a/site/examples/uce-starter/components/auth/oauth-client.uce +++ b/site/examples/uce-starter/components/auth/oauth-client.uce @@ -4,7 +4,7 @@ COMPONENT(Request& context) { String title = first(context.props["title"].to_string(), "Sign in with OAuth"); String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method"); - String callback_url = first(context.props["callback_url"].to_string(), starter_link("auth/callback", context)); + String callback_url = first(context.props["callback_url"].to_string(), app_link("auth/callback", context)); DTree services = context.props["services"]; if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "") @@ -46,7 +46,7 @@ COMPONENT(Request& context) String service_name = service["name"].to_string(); String scope = service["scope"].to_string(); String auth_url = service["auth_url"].to_string(); - String store_url = starter_link("auth/store-oauth-session", context); + String store_url = app_link("auth/store-oauth-session", context); ?>
-

">Register

+

">Register

} diff --git a/site/examples/uce-starter/views/account/logout.uce b/site/examples/uce-starter/views/account/logout.uce index cfee83b..d8c3e50 100644 --- a/site/examples/uce-starter/views/account/logout.uce +++ b/site/examples/uce-starter/views/account/logout.uce @@ -1,8 +1,7 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); StarterUser(context).logout(); - starter_redirect("account/login", context); + redirect(app_link("account/login", context)); } diff --git a/site/examples/uce-starter/views/account/profile.uce b/site/examples/uce-starter/views/account/profile.uce index 33257ac..148381b 100644 --- a/site/examples/uce-starter/views/account/profile.uce +++ b/site/examples/uce-starter/views/account/profile.uce @@ -1,15 +1,14 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); StarterUser users(context); if(!users.is_signed_in()) { - starter_redirect("account/login", context); + redirect(app_link("account/login", context)); return; } - context.call["starter"]["page_title"] = "Profile"; + context.call["app"]["page_title"] = "Profile"; DTree user = users.current(); String roles = ""; user["roles"].each([&](DTree role, String key) { @@ -22,6 +21,6 @@ RENDER(Request& context)

Email:

Roles:

Created:

-

">Logout

+

">Logout

} diff --git a/site/examples/uce-starter/views/account/register.uce b/site/examples/uce-starter/views/account/register.uce index e4bc7ab..01dd732 100644 --- a/site/examples/uce-starter/views/account/register.uce +++ b/site/examples/uce-starter/views/account/register.uce @@ -1,9 +1,8 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Register"; + context.call["app"]["page_title"] = "Register"; StarterUser users(context); DTree result; if(context.params["REQUEST_METHOD"] == "POST") @@ -11,7 +10,7 @@ RENDER(Request& context) <>

Register

- +

diff --git a/site/examples/uce-starter/views/auth/callback.uce b/site/examples/uce-starter/views/auth/callback.uce index e2fd61e..09ec87f 100644 --- a/site/examples/uce-starter/views/auth/callback.uce +++ b/site/examples/uce-starter/views/auth/callback.uce @@ -1,9 +1,8 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "OAuth Callback"; + context.call["app"]["page_title"] = "OAuth Callback"; String code = context.get["code"]; String state = context.get["state"]; String error = context.get["error"]; @@ -18,8 +17,8 @@ RENDER(Request& context)

Authentication Failed

@@ -33,8 +32,8 @@ RENDER(Request& context)

State:

diff --git a/site/examples/uce-starter/views/auth/demo.uce b/site/examples/uce-starter/views/auth/demo.uce index 9f50958..4c85b99 100644 --- a/site/examples/uce-starter/views/auth/demo.uce +++ b/site/examples/uce-starter/views/auth/demo.uce @@ -1,10 +1,11 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Auth"; - starter_register_css("views/marketing.css", context); + DTree asset_props; + asset_props["css"]["0"] = "views/marketing.css"; + print(component("../../components/theme/assets", asset_props, context)); + context.call["app"]["page_title"] = "Auth"; DTree props; props["title"] = "Sign In to Your Account"; @@ -13,7 +14,7 @@ RENDER(Request& context) props["github_client_id"] = "YOUR_GITHUB_CLIENT_ID"; props["discord_client_id"] = "YOUR_DISCORD_CLIENT_ID"; props["twitch_client_id"] = "YOUR_TWITCH_CLIENT_ID"; - props["callback_url"] = starter_link("auth/callback", context); + props["callback_url"] = app_link("auth/callback", context); <>

Authentication Demo

diff --git a/site/examples/uce-starter/views/auth/store-oauth-session.uce b/site/examples/uce-starter/views/auth/store-oauth-session.uce index 63ef19e..5c679a0 100644 --- a/site/examples/uce-starter/views/auth/store-oauth-session.uce +++ b/site/examples/uce-starter/views/auth/store-oauth-session.uce @@ -1,21 +1,24 @@ #load "../../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_type"] = "json"; + context.call["app"]["page_type"] = "json"; + DTree response; DTree body = json_decode(context.in); if(context.params["REQUEST_METHOD"] == "POST" && body["oauth_service"].to_string() != "" && body["oauth_state"].to_string() != "") { context.session["oauth_service"] = body["oauth_service"].to_string(); context.session["oauth_state"] = body["oauth_state"].to_string(); - context.call["starter"]["json"]["status"] = "success"; + response["status"] = "success"; } else { context.set_status(400, "Bad Request"); - context.call["starter"]["json"]["status"] = "error"; - context.call["starter"]["json"]["message"] = "Invalid input"; + response["status"] = "error"; + response["message"] = "Invalid input"; } + print(json_encode(response)); } + + diff --git a/site/examples/uce-starter/views/dashboard.uce b/site/examples/uce-starter/views/dashboard.uce index 6e3ae20..073a999 100644 --- a/site/examples/uce-starter/views/dashboard.uce +++ b/site/examples/uce-starter/views/dashboard.uce @@ -1,10 +1,13 @@ #load "../lib/app.uce" -RENDER(Request& context) +ONCE(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Dashboard"; - starter_register_css("views/dashboard.css", context); + ?>" />

Features

This page exists in the UCE port so the starter menu resolves cleanly as a full route, while still reusing the same marketing components as the home page.

diff --git a/site/examples/uce-starter/views/gauges.uce b/site/examples/uce-starter/views/gauges.uce index ed8af5e..33c9fe0 100644 --- a/site/examples/uce-starter/views/gauges.uce +++ b/site/examples/uce-starter/views/gauges.uce @@ -1,10 +1,8 @@ #load "../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Gauges"; - starter_register_css("themes/common/css/gauges.css", context); + context.call["app"]["page_title"] = "Gauges"; f64 pi = 3.14159265358979323846; DTree props; diff --git a/site/examples/uce-starter/views/index.uce b/site/examples/uce-starter/views/index.uce index ec1dfcb..821948a 100644 --- a/site/examples/uce-starter/views/index.uce +++ b/site/examples/uce-starter/views/index.uce @@ -1,10 +1,11 @@ #load "../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Home"; - starter_register_css("views/marketing.css", context); + DTree asset_props; + asset_props["css"]["0"] = "views/marketing.css"; + print(component("../components/theme/assets", asset_props, context)); + context.call["app"]["page_title"] = "Home"; DTree props; @@ -79,13 +80,13 @@ RENDER(Request& context) ?>

Starter Themes

The original light and dark starter skins remain available in the same gallery for side-by-side checks.

- ">Open Gallery + ">Open Gallery
@@ -98,9 +99,16 @@ RENDER(Request& context) props["secondary_text"] = "View GitHub"; print(component("../components/example/marketing_blocks:CTA_SECTION", props, context)); + DTree island_props; + island_props["name"] = "StarterGuidelines"; + island_props["id"] = "starter-guidelines-island"; + island_props["props"]["route"] = context.call["route"]["l_path"].to_string(); + island_props["props"]["enhancement"] = "progressive"; + <>

Component Development Guidelines

+
🏷️Use semantic HTML5 elements
🎨Implement CSS custom properties for theming
diff --git a/site/examples/uce-starter/views/page1.uce b/site/examples/uce-starter/views/page1.uce index 520f6f9..71d1a86 100644 --- a/site/examples/uce-starter/views/page1.uce +++ b/site/examples/uce-starter/views/page1.uce @@ -1,10 +1,11 @@ #load "../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Components"; - starter_register_css("views/marketing.css", context); + DTree asset_props; + asset_props["css"]["0"] = "views/marketing.css"; + print(component("../components/theme/assets", asset_props, context)); + context.call["app"]["page_title"] = "Components"; <>

Component System

diff --git a/site/examples/uce-starter/views/page2-section1.uce b/site/examples/uce-starter/views/page2-section1.uce index 8720bca..a35e3ad 100644 --- a/site/examples/uce-starter/views/page2-section1.uce +++ b/site/examples/uce-starter/views/page2-section1.uce @@ -1,9 +1,8 @@ #load "../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_type"] = "blank"; + context.call["app"]["page_type"] = "blank"; <> - Page 2 Section 1 loaded
UCE starter AJAX fragment response
diff --git a/site/examples/uce-starter/views/page2.uce b/site/examples/uce-starter/views/page2.uce index a73c189..58d5f5a 100644 --- a/site/examples/uce-starter/views/page2.uce +++ b/site/examples/uce-starter/views/page2.uce @@ -1,16 +1,15 @@ #load "../lib/app.uce" -RENDER(Request& context) +COMPONENT(Request& context) { - starter_boot(context); - context.call["starter"]["page_title"] = "Ajaxy"; + context.call["app"]["page_title"] = "Ajaxy"; <>

Ajax Demo

This is the content of Page 2. You can add more information here.

Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.


- +