host: extract the relate picker into a content-addressed ~relate-picker component
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>
This commit is contained in:
2026-06-29 15:17:30 +00:00
parent db4809b01e
commit b21ae05e8f
12 changed files with 133 additions and 20 deletions

View File

@@ -71,6 +71,11 @@ cp "$ROOT/shared/sx/templates/tw-layout.sx" "$DIST/sx/"
cp "$ROOT/shared/sx/templates/tw-type.sx" "$DIST/sx/"
cp "$ROOT/shared/sx/templates/tw.sx" "$DIST/sx/"
# 9b. Host app components (content-addressed, client-expanded on boosted nav).
# Listed in the host's data-sx-manifest "boot" array so the client eager-loads
# them after the web stack — see lib/host/static.sx + sx-platform.js loadWebStack.
cp "$ROOT/lib/host/sx/relate-picker.sx" "$DIST/sx/"
# 10. Hyperscript
for f in tokenizer parser compiler runtime integration htmx; do
cp "$ROOT/lib/hyperscript/$f.sx" "$DIST/sx/hs-$f.sx"

View File

@@ -48,6 +48,8 @@ const SOURCE_MAP = {
'boot.sx': 'web/boot.sx',
'tw-layout.sx': 'web/tw-layout.sx', 'tw-type.sx': 'web/tw-type.sx', 'tw.sx': 'web/tw.sx',
'text-layout.sx': 'lib/text-layout.sx',
// Host app components (content-addressed, client-expanded on boosted nav).
'relate-picker.sx': 'lib/host/sx/relate-picker.sx',
};
let synced = 0;
for (const [dist, src] of Object.entries(SOURCE_MAP)) {
@@ -87,6 +89,8 @@ const FILES = [
'hs-tokenizer.sx', 'hs-parser.sx', 'hs-compiler.sx', 'hs-runtime.sx',
'hs-worker.sx', 'hs-prolog.sx',
'hs-integration.sx', 'hs-htmx.sx',
// Host app components — standalone defcomps, no inter-module deps.
'relate-picker.sx',
'boot.sx',
];

View File

@@ -646,6 +646,18 @@
// Load entry point itself (boot.sx — not a library, just defines + init)
loadBytecodeFile("sx/" + entry.file) || loadSxFile("sx/" + entry.file.replace(/\.sxbc$/, '.sx'));
// App components: the page's data-sx-manifest "boot" array lists app-specific
// modules (e.g. ~relate-picker) to eager-load after the web stack, so their
// defcomps are registered before a boosted fragment references them. Loaded
// content-addressed, the same as any module.
var pageM = loadPageManifest();
if (pageM && pageM.boot && pageM.boot.length) {
for (var b = 0; b < pageM.boot.length; b++) {
var bf = pageM.boot[b];
loadBytecodeFile("sx/" + bf) || loadSxFile("sx/" + bf.replace(/\.sxbc$/, '.sx'));
}
}
if (K.endModuleLoad) K.endModuleLoad();
var count = Object.keys(_loadedLibs).length + 1; // +1 for entry
var dt = Math.round(performance.now() - t0);