From 4aa49e42e8655a74cfca4aaf272a5bb171797f54 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 15 Apr 2026 11:56:15 +0000 Subject: [PATCH] htmx demos working: activation, fetch, swap, OOB filtering, test runner page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - htmx-boot-subtree! wired into process-elements for auto-activation - Fixed cond compilation bug in hx-verb-info (Clojure-style flat cond) - Platform io-fetch upgraded: method/body/headers support, full response dict - Replaced perform IO ops with browser primitives (set-timeout, browser-confirm, etc) - SX→HTML rendering in hx-do-swap with OOB section filtering - hx-collect-params: collects input name/value for all methods - Handler naming: ex-{slug} convention, removed perform IO dependencies - Test runner page at (test.(applications.(htmx))) with iframe-based runner - Header "test" link on every page linking to test URL - Page file restructure: 285 files moved to URL-matching paths (a/b/c/index.sx) - page-functions.sx: ~100 component name references updated - _test added to skip_dirs, test- file prefix convention for test files Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 8 +- hosts/ocaml/browser/sx-platform.js | 83 +- lib/hyperscript/htmx.sx | 1211 ++++++++++++++++++++ shared/static/scripts/sx-test-runner.js | 434 ++++--- shared/static/wasm/sx-platform.js | 83 +- shared/static/wasm/sx/boot.sxbc | 4 +- shared/static/wasm/sx/hs-htmx.sx | 1211 ++++++++++++++++++++ shared/static/wasm/sx/hs-htmx.sxbc | 3 + shared/static/wasm/sx/module-manifest.json | 1079 ----------------- shared/static/wasm/sx/orchestration.sx | 19 +- shared/static/wasm/sx/orchestration.sxbc | 4 +- shared/static/wasm/sx_browser.bc.wasm.js | 4 +- sx/sx/applications/htmx/runner.sx | 176 +++ sx/sx/layouts/header.sx | 7 + sx/sx/page-functions.sx | 418 ++++--- web/orchestration.sx | 19 +- 16 files changed, 3201 insertions(+), 1562 deletions(-) create mode 100644 lib/hyperscript/htmx.sx create mode 100644 shared/static/wasm/sx/hs-htmx.sx create mode 100644 shared/static/wasm/sx/hs-htmx.sxbc delete mode 100644 shared/static/wasm/sx/module-manifest.json create mode 100644 sx/sx/applications/htmx/runner.sx diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index e9ce1e65..9cab15d3 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -2846,13 +2846,9 @@ let http_inject_shell_statics env static_dir sx_sxc = (* Hash each .sxbc module individually and add to the hash index. Each module's content is stored by hash; exported symbols map to the module hash. *) let sxbc_dir = static_dir ^ "/wasm/sx" in - let module_manifest_path = sxbc_dir ^ "/module-manifest.json" in + let module_manifest_path = sxbc_dir ^ "/module-manifest.sx" in let module_hashes : (string, string) Hashtbl.t = Hashtbl.create 32 in (* module key → hash *) (if Sys.file_exists module_manifest_path then begin - let manifest_src = In_channel.with_open_text module_manifest_path In_channel.input_all in - (* Simple JSON parse — extract "key": { "file": "...", "exports": [...] } *) - let exprs = try Sx_parser.parse_all ("(" ^ manifest_src ^ ")") with _ -> [] in - ignore exprs; (* The manifest is JSON, not SX — parse it manually *) (* Read each .sxbc file, hash it, store in hash_to_def *) if Sys.file_exists sxbc_dir && Sys.is_directory sxbc_dir then begin let files = Array.to_list (Sys.readdir sxbc_dir) in @@ -3485,7 +3481,7 @@ let http_mode port = (* Files to skip — declarative metadata, not needed for rendering *) let skip_files = ["primitives.sx"; "types.sx"; "boundary.sx"; "harness.sx"; "eval-rules.sx"; "vm-inline.sx"] in - let skip_dirs = ["tests"; "test"; "plans"; "essays"; "spec"; "client-libs"] in + let skip_dirs = ["tests"; "test"; "plans"; "essays"; "spec"; "client-libs"; "_test"] in let rec load_dir ?(base="") dir = if Sys.file_exists dir && Sys.is_directory dir then begin let entries = Sys.readdir dir in diff --git a/hosts/ocaml/browser/sx-platform.js b/hosts/ocaml/browser/sx-platform.js index f406a968..15c67d45 100644 --- a/hosts/ocaml/browser/sx-platform.js +++ b/hosts/ocaml/browser/sx-platform.js @@ -98,8 +98,33 @@ try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); } }, typeof arg === "number" ? arg : 0); } else if (opName === "io-fetch") { - fetch(typeof arg === "string" ? arg : "").then(function(r) { return r.text(); }).then(function(t) { - try { driveAsync(result.resume({ok: true, text: t})); } catch(e) { console.error("[sx] driveAsync:", e.message); } + var fetchUrl = typeof arg === "string" ? arg : ""; + var fetchMethod = (items && items[2]) || "GET"; + var fetchBody = items && items[3]; + var fetchHeaders = items && items[4]; + var fetchOpts = { method: typeof fetchMethod === "string" ? fetchMethod : "GET" }; + if (fetchBody && typeof fetchBody !== "boolean") { + fetchOpts.body = typeof fetchBody === "string" ? fetchBody : JSON.stringify(fetchBody); + } + if (fetchHeaders && typeof fetchHeaders === "object") { + var h = {}; + var keys = fetchHeaders._keys || Object.keys(fetchHeaders); + for (var fi = 0; fi < keys.length; fi++) { + var k = keys[fi], v = fetchHeaders[k]; + if (typeof k === "string" && typeof v === "string") h[k] = v; + } + fetchOpts.headers = h; + } + fetch(fetchUrl, fetchOpts).then(function(r) { + var hdrs = {}; + try { r.headers.forEach(function(v, k) { hdrs[k] = v; }); } catch(e) {} + return r.text().then(function(t) { + return { status: r.status, body: t, headers: hdrs, ok: r.ok }; + }); + }).then(function(resp) { + try { driveAsync(result.resume(resp)); } catch(e) { console.error("[sx] driveAsync:", e.message); } + }).catch(function(e) { + try { driveAsync(result.resume({status: 0, body: "", headers: {}, ok: false})); } catch(e2) { console.error("[sx] driveAsync:", e2.message); } }); } else if (opName === "io-navigate") { // navigation — don't resume @@ -273,7 +298,7 @@ } } // Content-addressed boot: script loaded from /sx/h/{hash}, not /static/wasm/. - // Fall back to /static/wasm/ base URL for module-manifest.json and .sx sources. + // Fall back to /static/wasm/ base URL for module-manifest.sx and .sx sources. if (!_baseUrl || _baseUrl.indexOf("/sx/h/") !== -1) { _baseUrl = "/static/wasm/"; } @@ -522,6 +547,22 @@ var _manifest = null; var _loadedLibs = {}; + /** + * Convert K.parse output (tagged {_type, ...} objects) to plain JS. + * SX nil (from empty lists `()`) becomes []. + */ + function sxDataToJs(v) { + if (v === null || v === undefined) return []; + if (typeof v !== "object") return v; + if (v._type === "list") return (v.items || []).map(sxDataToJs); + if (v._type === "dict") { + var out = {}; + for (var k in v) if (k !== "_type") out[k] = sxDataToJs(v[k]); + return out; + } + return v; + } + /** * Fetch and parse the module manifest (library deps + file paths). */ @@ -529,11 +570,14 @@ if (_manifest) return _manifest; try { var xhr = new XMLHttpRequest(); - xhr.open("GET", _baseUrl + "sx/module-manifest.json" + _cacheBust, false); + xhr.open("GET", _baseUrl + "sx/module-manifest.sx" + _cacheBust, false); xhr.send(); if (xhr.status === 200) { - _manifest = JSON.parse(xhr.responseText); - return _manifest; + var parsed = K.parse(xhr.responseText); + if (parsed && parsed.length > 0) { + _manifest = sxDataToJs(parsed[0]); + return _manifest; + } } } catch(e) {} console.warn("[sx-platform] No manifest found, falling back to full load"); @@ -699,6 +743,32 @@ } } + // Merge definitions from a new page's manifest (called during navigation) + function mergeManifest(el) { + if (!el) return; + try { + var incoming = JSON.parse(el.textContent); + var newDefs = incoming.defs || {}; + // Ensure base manifest is loaded + if (!_pageManifest) loadPageManifest(); + if (!_pageManifest) _pageManifest = { defs: {} }; + if (!_pageManifest.defs) _pageManifest.defs = {}; + for (var name in newDefs) { + _pageManifest.defs[name] = newDefs[name]; + _hashToName[newDefs[name]] = name; + } + // Merge hash store entries + if (incoming.store) { + if (!_pageManifest.store) _pageManifest.store = {}; + for (var h in incoming.store) { + _pageManifest.store[h] = incoming.store[h]; + } + } + } catch(e) { + console.warn("[sx] Failed to merge manifest:", e); + } + } + function resolveHash(hash) { // 1. In-memory cache if (_hashCache[hash]) return _hashCache[hash]; @@ -833,6 +903,7 @@ renderToHtml: function(expr) { return K.renderToHtml(expr); }, callFn: function(fn, args) { return K.callFn(fn, args); }, engine: function() { return K.engine(); }, + mergeManifest: function(el) { return mergeManifest(el); }, // Boot entry point (called by auto-init or manually) init: function() { if (typeof K.eval === "function") { diff --git a/lib/hyperscript/htmx.sx b/lib/hyperscript/htmx.sx new file mode 100644 index 00000000..ab32489e --- /dev/null +++ b/lib/hyperscript/htmx.sx @@ -0,0 +1,1211 @@ +;; htmx.sx — hx-* attributes as hyperscript sugar (htmx 4.0 compat) +;; +;; Every hx- attribute is syntactic sugar for a hyperscript event handler. +;; htmx-activate! scans hx-* attributes, builds the equivalent handler +;; from the same runtime primitives the hyperscript compiler emits, +;; and registers it via hs-on — same bytecode path, zero duplication. +;; +;; The translation: +;;