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: +;;