Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
The declarative picker markup is now a reusable SX component
(lib/host/sx/relate-picker.sx, defcomp ~relate-picker &key slug kind) instead of
inline markup in the editor. It is a CONTENT-ADDRESSED, CLIENT-EXPANDED component:
- Server: on a full page load render-page expands ~relate-picker server-side
(SEO / no-JS), exactly as before.
- Client: on a boosted SPA nav the edit body serialises to the compact
(~relate-picker :slug … :kind …), and the CLIENT expands it. The component
module is compiled to a content-addressed .sxbc, served immutably from
/sx/h/{hash}, and listed in the page's data-sx-manifest "boot" array so the
client eager-loads it after the web stack — registering its defcomp before any
boosted fragment references it.
Wiring:
- lib/host/sx/relate-picker.sx — the component.
- lib/host/blog.sx — editor emits (~relate-picker :slug s :kind k); the inline
form markup is gone.
- lib/host/static.sx — host/static-manifest-json emits boot:["relate-picker.sxbc"]
(the previously-empty boot array, now used as designed).
- hosts/ocaml/browser/sx-platform.js — loadWebStack eager-loads the page manifest's
boot[] modules (content-addressed) after the web stack.
- bundle.sh + compile-modules.js — copy/compile the component to .sxbc.
- serve.sh + conformance.sh — load the component module server-side.
This gives the host an app-component system: app defcomps shipped to the client by
hash, the same machinery as the kernel modules — the picker is the first, and it's
the model for publishing components externally.
Tests: conformance 272/272 (server expansion); relate-picker.spec.js 6/6 incl. the
boosted-nav populate (proves client-side component load + expansion) and the
error/retry case. WASM stack rebuilt (relate-picker.sxbc @ 6818110a).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
5.3 KiB
Plaintext
119 lines
5.3 KiB
Plaintext
;; lib/host/static.sx — serve the client kernel + assets so the blog can boot the
|
|
;; SX-htmx hypermedia engine (web/engine.sx) and run as a SPA. The native
|
|
;; http-listen host reads files with the `file-read` primitive (no perform), so
|
|
;; GET /static/** maps to a file under the static root (default "shared/static",
|
|
;; resolved against the server cwd — mount ./shared/static there in the container).
|
|
;;
|
|
;; Also wires the CONTENT-ADDRESSED module cache the SX client expects: GET
|
|
;; /sx/h/{hash} serves a web-stack .sxbc by its content hash (immutable, never
|
|
;; stale — a deploy changes the content → changes the hash → a fresh URL), and a
|
|
;; <script data-sx-manifest> mapping {file -> hash} makes the client's
|
|
;; loadBytecodeFile take the content-addressed branch (localStorage + immutable)
|
|
;; instead of the path + max-age=3600 branch.
|
|
;; Depends on lib/dream/types.sx (dream-response/-html-status/-param) + router.
|
|
|
|
(define host/static-root "shared/static")
|
|
(define host/static-use-root! (fn (r) (set! host/static-root r)))
|
|
|
|
;; content-type by file extension; default to octet-stream.
|
|
(define host/static--ctype
|
|
(fn (path)
|
|
(cond
|
|
((ends-with? path ".js") "application/javascript; charset=utf-8")
|
|
((ends-with? path ".mjs") "application/javascript; charset=utf-8")
|
|
((ends-with? path ".css") "text/css; charset=utf-8")
|
|
((ends-with? path ".json") "application/json; charset=utf-8")
|
|
((ends-with? path ".map") "application/json; charset=utf-8")
|
|
((ends-with? path ".svg") "image/svg+xml")
|
|
((ends-with? path ".png") "image/png")
|
|
((ends-with? path ".woff2") "font/woff2")
|
|
((ends-with? path ".wasm") "application/wasm")
|
|
(true "application/octet-stream"))))
|
|
|
|
;; A content-hashed filename (e.g. js_of_ocaml-651f6707.wasm, or anything under
|
|
;; /sx/h/) is immutable; everything else gets a modest max-age (mutable bundle).
|
|
(define host/static--cache-control
|
|
(fn (rel)
|
|
(if (ends-with? rel ".wasm")
|
|
"public, max-age=31536000, immutable"
|
|
"public, max-age=3600")))
|
|
|
|
;; reject empty, absolute, or traversal paths.
|
|
(define host/static--safe?
|
|
(fn (rel)
|
|
(and (> (len rel) 0)
|
|
(not (starts-with? rel "/"))
|
|
(not (string-contains? rel "..")))))
|
|
|
|
;; Serve one asset by its path relative to the static root. file-read THROWS on a
|
|
;; missing file, so gate on file-exists? first and return a 404 instead.
|
|
(define host/static-serve
|
|
(fn (rel)
|
|
(if (not (host/static--safe? rel))
|
|
(dream-html-status 403 "Forbidden")
|
|
(let ((path (str host/static-root "/" rel)))
|
|
(if (not (file-exists? path))
|
|
(dream-html-status 404 "Not Found")
|
|
(dream-response 200
|
|
{:content-type (host/static--ctype rel)
|
|
:cache-control (host/static--cache-control rel)}
|
|
(file-read path)))))))
|
|
|
|
;; ── content-addressed module cache (/sx/h/{hash}) ───────────────────
|
|
;; Each web-stack .sxbc carries its content hash in its head: (sxbc 1 "HASH" ...).
|
|
;; Index every .sxbc by that hash at startup so the client can fetch each module
|
|
;; immutably + localStorage-cached, and never stale.
|
|
(define host/static--sxh->path (dict)) ;; hash -> filepath
|
|
(define host/static--file->hash (dict)) ;; "dom.sxbc" -> hash
|
|
|
|
;; the embedded hash from a .sxbc head: (sxbc 1 "HASH" ... -> "HASH"
|
|
(define host/static--sxbc-hash
|
|
(fn (head) (nth (split head "\"") 1)))
|
|
|
|
(define host/static-build-sxh-index!
|
|
(fn ()
|
|
(for-each
|
|
(fn (path)
|
|
(let ((h (host/static--sxbc-hash (substr (file-read path) 0 60)))
|
|
(base (last (split path "/"))))
|
|
(dict-set! host/static--sxh->path h path)
|
|
(dict-set! host/static--file->hash base h)))
|
|
(file-glob (str host/static-root "/wasm/sx/*.sxbc")))))
|
|
|
|
;; GET /sx/h/{hash} -> the .sxbc content, immutable (content-addressed).
|
|
(define host/static-sxh-serve
|
|
(fn (hash)
|
|
(let ((path (get host/static--sxh->path hash)))
|
|
(if (nil? path)
|
|
(dream-html-status 404 "Not Found")
|
|
(dream-response 200
|
|
{:content-type "text/sx; charset=utf-8"
|
|
:cache-control "public, max-age=31536000, immutable"}
|
|
(file-read path))))))
|
|
|
|
;; the data-sx-manifest JSON for the shell: {"modules": {"dom.sxbc": "hash", ...}}.
|
|
;; The client's loadBytecodeFile reads manifest.modules[file] -> hash -> /sx/h/.
|
|
;; App components the client must eager-load (after the web stack) so their
|
|
;; defcomps are registered before a boosted fragment references them. Loaded
|
|
;; content-addressed via the modules map below, the same as any web-stack module.
|
|
(define host/static--boot-modules (list "relate-picker.sxbc"))
|
|
|
|
(define host/static-manifest-json
|
|
(fn ()
|
|
(str "{\"v\":1,\"boot\":["
|
|
(join "," (map (fn (m) (str "\"" m "\"")) host/static--boot-modules))
|
|
"],\"defs\":{},\"modules\":{"
|
|
(join ","
|
|
(map (fn (k) (str "\"" k "\":\"" (get host/static--file->hash k) "\""))
|
|
(keys host/static--file->hash)))
|
|
"}}")))
|
|
|
|
;; Route group: GET /static/** (path) + GET /sx/h/** (content-addressed). A plain
|
|
;; route LIST (like host/feed-routes); host/serve combines + flattens the groups.
|
|
(define host/static-routes
|
|
(list
|
|
(dream-get "/static/**"
|
|
(fn (req) (host/static-serve (dream-param req "**"))))
|
|
(dream-get "/sx/h/**"
|
|
(fn (req) (host/static-sxh-serve (dream-param req "**"))))))
|