htmx demos working: activation, fetch, swap, OOB filtering, test runner page
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user