Document SX rendering pipeline, add missing sx_docs mount, loud error on missing component
- CLAUDE.md: add SX rendering pipeline overview, service sx/ vs sxc/ convention, dev container mount convention - docker-compose.dev.yml: add missing ./sx/sx:/app/sx bind mount for sx_docs (root cause of "Unknown component: ~sx-layout-full") - async_eval.py: add evaluation modes table to module docstring; log error when async_eval_slot_to_sx can't find a component instead of silently falling through to client-side serialization - helpers.py: remove debug logging from render_to_sx_with_env Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
24
CLAUDE.md
24
CLAUDE.md
@@ -108,6 +108,26 @@ cd artdag/l1 && mypy app/types.py app/routers/recipes.py tests/
|
|||||||
- Silent SSO: `prompt=none` OAuth flow for automatic cross-app login
|
- Silent SSO: `prompt=none` OAuth flow for automatic cross-app login
|
||||||
- ActivityPub: RSA signatures, per-app virtual actor projections sharing same keypair
|
- ActivityPub: RSA signatures, per-app virtual actor projections sharing same keypair
|
||||||
|
|
||||||
|
### SX Rendering Pipeline
|
||||||
|
|
||||||
|
The SX system renders component trees defined in s-expressions. The same AST can be evaluated in different modes depending on where the server/client rendering boundary is drawn:
|
||||||
|
|
||||||
|
- `render_to_html(name, **kw)` — server-side, produces HTML. Used by route handlers returning full HTML.
|
||||||
|
- `render_to_sx(name, **kw)` — server-side, produces SX wire format. Component calls stay **unexpanded** (serialized for client-side rendering by sx.js).
|
||||||
|
- `render_to_sx_with_env(name, env, **kw)` — server-side, **expands the top-level component** then serializes children as SX wire format. Used by layout components that need Python context (auth state, fragments, URLs) resolved server-side.
|
||||||
|
- `sx_page(ctx, page_sx)` — produces the full HTML shell (`<!doctype html>...`) with component definitions, CSS, and page SX inlined for client-side boot.
|
||||||
|
|
||||||
|
See the docstring in `shared/sx/async_eval.py` for the full evaluation modes table.
|
||||||
|
|
||||||
|
### Service SX Directory Convention
|
||||||
|
|
||||||
|
Each service has two SX-related directories:
|
||||||
|
|
||||||
|
- **`{service}/sx/`** — service-specific component definitions (`.sx` files with `defcomp`). Loaded at startup by `load_service_components()`. These define layout components, reusable UI fragments, etc.
|
||||||
|
- **`{service}/sxc/`** — page definitions and Python rendering logic. Contains `defpage` definitions (client-routed pages) and the Python functions that compose headers, layouts, and page content.
|
||||||
|
|
||||||
|
Shared components live in `shared/sx/templates/` and are loaded by `load_shared_components()` in the app factory.
|
||||||
|
|
||||||
### Art DAG
|
### Art DAG
|
||||||
|
|
||||||
- **3-Phase Execution:** Analyze → Plan → Execute (tasks in `artdag/l1/tasks/`)
|
- **3-Phase Execution:** Analyze → Plan → Execute (tasks in `artdag/l1/tasks/`)
|
||||||
@@ -130,6 +150,10 @@ cd artdag/l1 && mypy app/types.py app/routers/recipes.py tests/
|
|||||||
| likes | (internal only) | 8009 |
|
| likes | (internal only) | 8009 |
|
||||||
| orders | orders.rose-ash.com | 8010 |
|
| orders | orders.rose-ash.com | 8010 |
|
||||||
|
|
||||||
|
## Dev Container Mounts
|
||||||
|
|
||||||
|
Dev bind mounts in `docker-compose.dev.yml` must mirror the Docker image's COPY paths. When adding a new directory to a service (e.g. `{service}/sx/`), add a corresponding volume mount (`./service/sx:/app/sx`) or the directory won't be visible inside the dev container. Hypercorn `--reload` watches for Python file changes; `.sx` file hot-reload is handled by `reload_if_changed()` in `shared/sx/jinja_bridge.py`.
|
||||||
|
|
||||||
## Key Config Files
|
## Key Config Files
|
||||||
|
|
||||||
- `docker-compose.yml` / `docker-compose.dev.yml` — service definitions, env vars, volumes
|
- `docker-compose.yml` / `docker-compose.dev.yml` — service definitions, env vars, volumes
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ services:
|
|||||||
- ./sx/bp:/app/bp
|
- ./sx/bp:/app/bp
|
||||||
- ./sx/services:/app/services
|
- ./sx/services:/app/services
|
||||||
- ./sx/content:/app/content
|
- ./sx/content:/app/content
|
||||||
|
- ./sx/sx:/app/sx
|
||||||
- ./sx/path_setup.py:/app/path_setup.py
|
- ./sx/path_setup.py:/app/path_setup.py
|
||||||
- ./sx/entrypoint.sh:/usr/local/bin/entrypoint.sh
|
- ./sx/entrypoint.sh:/usr/local/bin/entrypoint.sh
|
||||||
- ./sx/__init__.py:/app/__init__.py:ro
|
- ./sx/__init__.py:/app/__init__.py:ro
|
||||||
|
|||||||
@@ -10,6 +10,28 @@ control flow (``if``, ``let``, ``map``, ``when``). The sync
|
|||||||
collect-then-substitute resolver can't handle data dependencies between
|
collect-then-substitute resolver can't handle data dependencies between
|
||||||
I/O results and control flow, so handlers need inline async evaluation.
|
I/O results and control flow, so handlers need inline async evaluation.
|
||||||
|
|
||||||
|
Evaluation modes
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The same component AST can be evaluated in different modes depending on
|
||||||
|
where the rendering boundary is drawn (server vs client). Five modes
|
||||||
|
exist across the codebase:
|
||||||
|
|
||||||
|
Function Expands components? Output Used for
|
||||||
|
-------------------- ------------------- -------------- ----------------------------
|
||||||
|
_eval (sync) Yes Python values register_components, Jinja sx()
|
||||||
|
_arender (async) Yes HTML render_to_html
|
||||||
|
_aser (async) No — serializes SX wire format render_to_sx
|
||||||
|
_aser_component Yes, one level SX wire format render_to_sx_with_env (layouts)
|
||||||
|
sx.js renderDOM Yes DOM nodes Client-side
|
||||||
|
|
||||||
|
_aser deliberately does NOT expand ~component calls — it serializes them
|
||||||
|
as SX wire format so the client can render them. But layout components
|
||||||
|
(used by render_to_sx_with_env) need server-side expansion because they
|
||||||
|
depend on Python context (auth state, fragments, etc.). That's what
|
||||||
|
_aser_component / async_eval_slot_to_sx provides: expand the top-level
|
||||||
|
component body server-side, then serialize its children for the client.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
from shared.sx.async_eval import async_render
|
from shared.sx.async_eval import async_render
|
||||||
@@ -1021,6 +1043,16 @@ async def async_eval_slot_to_sx(
|
|||||||
if result is None or result is NIL:
|
if result is None or result is NIL:
|
||||||
return ""
|
return ""
|
||||||
return serialize(result)
|
return serialize(result)
|
||||||
|
else:
|
||||||
|
import logging
|
||||||
|
logging.getLogger("sx.eval").error(
|
||||||
|
"async_eval_slot_to_sx: component %s not found in env "
|
||||||
|
"(will fall through to _aser and serialize unexpanded — "
|
||||||
|
"client will see 'Unknown component'). "
|
||||||
|
"Check that the .sx file is loaded and the service's sx/ "
|
||||||
|
"directory is bind-mounted in docker-compose.dev.yml.",
|
||||||
|
head.name,
|
||||||
|
)
|
||||||
# Fall back to normal async_eval_to_sx
|
# Fall back to normal async_eval_to_sx
|
||||||
result = await _aser(expr, env, ctx)
|
result = await _aser(expr, env, ctx)
|
||||||
if isinstance(result, SxExpr):
|
if isinstance(result, SxExpr):
|
||||||
|
|||||||
Reference in New Issue
Block a user