Turn the blog into a SPA using the SX-htmx engine (web/engine.sx) booting the
WASM OCaml kernel (same evaluator as the server) in-browser, with sx-boost
fragment-swapping every link into #content.
Server side DONE + verified:
- lib/host/static.sx: GET /static/** serves shared/static via the file-read
primitive (ctype by ext, traversal-guarded, 404 on missing). Wired into
serve.sh (module + route group). Tested: kernel JS + .wasm binary-exact.
- host/blog--page is now the SPA shell: full page = WASM boot scripts +
sx-boost=#content wrapper + #content; on SX-Request:true returns ONLY the
inner content fragment for the engine to swap. All 13 handlers thread req.
- docker-compose mounts ./shared/static.
- lib/host/playwright/spa-check.{spec.js,run-spa-check.sh}: boot/boost/swap/back.
Client side: the WASM kernel BOOTS (SxKernel object, data-sx-ready=true, web
stack loads). BLOCKER: the bundled .sxbc throw 'VM: unknown opcode 0' vs this
worktree's kernel -> .sx source fallback -> boot.sx source fails 'Expected
list, got string' -> process-boosted never binds links (boosted 0/N). Fix =
rebuild a consistent WASM bundle (recompile .sxbc against the kernel via
scripts/sx-build-all.sh); the browser wasm target isn't built here yet. See
plans/host-spa.md. Live NOT redeployed (stays on pre-SPA process).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
6.7 KiB
Bash
Executable File
163 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# host-on-sx live server launcher. Loads the kernel stdlib, the subsystem
|
|
# libraries, and the host modules into one sx_server process, then calls
|
|
# (host/serve PORT ...) which binds the native http-listen server to the
|
|
# Dream-shaped host app. Runs in the FOREGROUND (http-listen blocks), so this
|
|
# doubles as a container entrypoint and a local launcher.
|
|
#
|
|
# Usage:
|
|
# bash lib/host/serve.sh # serve on $HOST_PORT (default 8910)
|
|
# HOST_PORT=8920 bash lib/host/serve.sh # pick a port
|
|
#
|
|
# The module list is kept identical to lib/host/conformance.sh so what serves is
|
|
# exactly what the suites verify.
|
|
|
|
set -uo pipefail
|
|
# Project root: SX_PROJECT_DIR in containers (set to /app by the compose stack),
|
|
# else the git toplevel for local runs.
|
|
cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}"
|
|
|
|
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
|
fi
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
echo "ERROR: sx_server.exe not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
PORT="${HOST_PORT:-8910}"
|
|
|
|
# Modules: every load line from conformance.sh's MODULES list, minus the ledger
|
|
# (not needed to serve). server.sx supplies host/serve.
|
|
MODULES=(
|
|
"spec/stdlib.sx"
|
|
"lib/r7rs.sx"
|
|
"lib/apl/runtime.sx"
|
|
"lib/datalog/tokenizer.sx"
|
|
"lib/datalog/parser.sx"
|
|
"lib/datalog/unify.sx"
|
|
"lib/datalog/db.sx"
|
|
"lib/datalog/builtins.sx"
|
|
"lib/datalog/aggregates.sx"
|
|
"lib/datalog/strata.sx"
|
|
"lib/datalog/eval.sx"
|
|
"lib/datalog/api.sx"
|
|
"lib/datalog/magic.sx"
|
|
"lib/acl/schema.sx"
|
|
"lib/acl/facts.sx"
|
|
"lib/acl/engine.sx"
|
|
"lib/acl/explain.sx"
|
|
"lib/acl/audit.sx"
|
|
"lib/acl/federation.sx"
|
|
"lib/acl/api.sx"
|
|
"lib/relations/schema.sx"
|
|
"lib/relations/engine.sx"
|
|
"lib/relations/api.sx"
|
|
"lib/relations/explain.sx"
|
|
"lib/relations/federation.sx"
|
|
"lib/relations/tree.sx"
|
|
"lib/feed/normalize.sx"
|
|
"lib/feed/stream.sx"
|
|
"lib/feed/api.sx"
|
|
"lib/persist/event.sx"
|
|
"lib/persist/backend.sx"
|
|
"lib/persist/log.sx"
|
|
"lib/persist/kv.sx"
|
|
"lib/persist/api.sx"
|
|
"lib/persist/durable.sx"
|
|
"spec/render.sx"
|
|
"web/adapter-html.sx"
|
|
"lib/dream/types.sx"
|
|
"lib/dream/json.sx"
|
|
"lib/dream/auth.sx"
|
|
"lib/dream/error.sx"
|
|
"lib/dream/form.sx"
|
|
"lib/dream/session.sx"
|
|
"lib/dream/router.sx"
|
|
"lib/host/handler.sx"
|
|
"lib/host/middleware.sx"
|
|
"lib/host/session.sx"
|
|
"lib/host/auth.sx"
|
|
"lib/host/sxtp.sx"
|
|
"lib/host/router.sx"
|
|
"lib/host/static.sx"
|
|
"lib/host/feed.sx"
|
|
"lib/host/relations.sx"
|
|
"lib/host/blog.sx"
|
|
"lib/host/server.sx"
|
|
)
|
|
|
|
# Admin login credentials + session signing secret. Override via the container
|
|
# env; the in-source defaults are dev-only. The blog write routes are now GUARDED
|
|
# (session login or Bearer), so these gate publishing on blog.rose-ash.com.
|
|
ADMIN_USER="${SX_ADMIN_USER:-admin}"
|
|
ADMIN_PASS="${SX_ADMIN_PASSWORD:-letmein}"
|
|
SESSION_SECRET="${SX_SESSION_SECRET:-rose-ash-host-dev-secret-change-me}"
|
|
|
|
EPOCH=1
|
|
{
|
|
for M in "${MODULES[@]}"; do
|
|
echo "(epoch $EPOCH)"; echo "(load \"$M\")"; EPOCH=$((EPOCH+1))
|
|
done
|
|
# 100% serving JIT — NO host exclude. The serving-JIT perform-in-HO-callback
|
|
# miscompile (map/rest/drop wrong args → blank pages, empty picker) is fixed by
|
|
# two composing pieces: sx-vm-extensions 81177d0e resolves a callback's IO
|
|
# inline (instead of unwinding the native HO loop) WHEN a synchronous resolver
|
|
# is installed, and sx_server.ml's http-listen now installs that resolver (it
|
|
# mirrors cek_run_with_io exactly). So the whole request path — host app +
|
|
# Dream + Datalog — runs under JIT with no exclude. Verified: ephemeral durable
|
|
# server, 100% JIT, zero fallbacks, real content, picker lists candidates.
|
|
# Point the blog at the DURABLE file backend (persists under $SX_PERSIST_DIR),
|
|
# then idempotently seed a welcome post (sx_content = SX element markup, the
|
|
# editor's content model). Re-seeding is a no-op if the slug already exists.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-use-store! (persist/durable-backend))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Rebuild the relations graph from the durable edge store. lib/relations holds
|
|
# the graph in memory only, so without this, related/tags/types vanish on every
|
|
# restart even though the posts persist.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-edges!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Sessions on the DURABLE store, LAZILY: only a logged-in session (one that
|
|
# writes a field) persists, so a login survives a restart while anonymous /
|
|
# crawler traffic leaves no rows. host/session-init! bumps the per-boot epoch
|
|
# that keeps sids unique across restarts. Then the signing secret + admin
|
|
# credentials, and grant admin "edit" on "blog" so a logged-in session passes
|
|
# the ACL gate on the write routes.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-use-store! (persist/durable-backend))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-init!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/auth-set-admin! \\\"$ADMIN_USER\\\" \\\"$ADMIN_PASS\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(acl/load! (list (acl-grant \\\"$ADMIN_USER\\\" \\\"edit\\\" \\\"blog\\\")))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Idempotently seed a welcome post (sx_content = SX element markup, the editor's
|
|
# content model). Re-seeding is a no-op if the slug already exists.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"welcome\\\" \\\"Welcome to the SX host\\\" \\\"(article (h1 \\\\\\\"Welcome to the SX host\\\\\\\") (p \\\\\\\"Rendered by lib/host via render-to-html, from the durable SX store.\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the root type-posts (type, tag) — types ARE posts. Idempotent.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-types!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
# Anonymous reads (feed timeline + relations container reads + blog post detail)
|
|
# plus the GUARDED blog write routes: POST /new (editor form ingest), POST/PUT/
|
|
# DELETE /posts behind host/require-user (session login OR Bearer) + ACL. make-app
|
|
# auto-mounts /login + /logout and wraps everything in the signed-session
|
|
# middleware, so a browser logs in then publishes. The bearer resolver is a stub
|
|
# (no API tokens configured) — browser session is the live auth path for now.
|
|
# blog-routes LAST — its GET /:slug catch-all must not shadow the rest.
|
|
echo "(eval \"(host/serve $PORT (list host/static-routes host/feed-routes host/relations-routes (host/blog-write-routes (fn (tok) nil)) host/blog-routes))\")"
|
|
} | exec "$SX_SERVER"
|