Web extension module for def-forms + modifier-key clicks + CSSX SSR fix

Move defhandler/defquery/defaction/defpage/defrelation from hardcoded
evaluator dispatch to web/web-forms.sx extension module, registered via
register-special-form!. Adapters updated to use definition-form? and
dynamically extended form-name lists.

Fix modifier-key clicks (ctrl-click → new tab) in three click handlers:
bindBoostLink, bindClientRouteClick, and orchestration.sx bind-event.
Add event-modifier-key? primitive (eventModifierKey_p for transpiler).

Fix CSSX SSR: ~cssx/flush no longer drains the collected bucket on the
server, so the shell template correctly emits CSSX rules in <head>.

Add missing server-side DOM stubs (create-text-node, dom-append, etc.)
and SSR passthrough for portal/error-boundary/promise-delayed.

Passive event listeners for touch/wheel/scroll to fix touchpad scrolling.

97/97 Playwright demo tests + 4/4 isomorphic SSR tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 10:01:41 +00:00
parent 8ccf5f7c1e
commit 8a08de26cd
11 changed files with 318 additions and 48 deletions

View File

@@ -125,11 +125,9 @@ let io_batch_mode = ref false
let io_queue : (int * string * value list) list ref = ref []
let io_counter = ref 0
(** Module-level scope stacks — shared between make_server_env (aser
scope-push!/pop!) and step-sf-context (via get-primitive "scope-peek"). *)
(** Request cookies — set by Python bridge before each page render.
get-cookie reads from here on the server; set-cookie is a no-op
(server can't set response cookies from SX — that's the framework's job). *)
(* Request cookies — set by Python bridge before each page render.
get-cookie reads from here on the server; set-cookie is a no-op
(server can't set response cookies from SX — that's the framework's job). *)
let _request_cookies : (string, string) Hashtbl.t = Hashtbl.create 8
let () = Sx_primitives.register "get-cookie" (fun args ->
@@ -144,6 +142,8 @@ let () = Sx_primitives.register "set-cookie" (fun _args ->
(* No-op on server — cookies are set via HTTP response headers *)
Nil)
(* Module-level scope stacks — shared between make_server_env (aser
scope-push!/pop!) and step-sf-context (via get-primitive "scope-peek"). *)
let _scope_stacks : (string, value list) Hashtbl.t = Hashtbl.create 8
let () = Sx_primitives.register "scope-push!" (fun args ->
@@ -496,6 +496,10 @@ let make_server_env () =
bind "dom-text-content" (fun _args -> String "");
bind "dom-set-text-content" (fun _args -> Nil);
bind "dom-body" (fun _args -> Nil);
bind "dom-create-element" (fun _args -> Nil);
bind "dom-append" (fun _args -> Nil);
bind "create-text-node" (fun _args -> Nil);
bind "render-to-dom" (fun _args -> Nil);
(* Raw HTML — platform primitives for adapter-html.sx *)
bind "make-raw-html" (fun args ->
@@ -617,6 +621,16 @@ let make_server_env () =
Sx_ref.eval_expr m.m_body (Env body_env)
| _ -> raise (Eval_error "expand-macro: expected (macro args env)"));
(* Expose register-special-form! and *custom-special-forms* to SX code
(used by web-forms.sx and adapter form-classification functions) *)
bind "register-special-form!" (fun args ->
match args with
| [String name; handler] ->
ignore (Sx_ref.register_special_form (String name) handler);
Nil
| _ -> raise (Eval_error "register-special-form!: expected (name handler)"));
ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms);
(* Register <> as a special form — evaluates all children, returns list *)
ignore (Sx_ref.register_special_form (String "<>") (NativeFn ("<>", fun args ->
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))));
@@ -725,10 +739,8 @@ let make_server_env () =
bind "set-response-status" (fun args -> io_request "set-response-status" args);
bind "set-response-header" (fun args -> io_request "set-response-header" args);
(* Application constructs — no-ops in the kernel, but needed so
handler/page files load successfully (their define forms get evaluated) *)
ignore (Sx_ref.register_special_form (String "defhandler") (NativeFn ("defhandler", fun _args -> Nil)));
ignore (Sx_ref.register_special_form (String "defpage") (NativeFn ("defpage", fun _args -> Nil)));
(* defhandler/defpage/defquery/defaction/defrelation are registered by
web-forms.sx via register-special-form!, no longer hardcoded here. *)
bind "cond-scheme?" (fun args ->
match args with
@@ -1504,6 +1516,7 @@ let cli_mode mode =
Filename.concat base "render.sx";
Filename.concat web_base "adapter-html.sx";
Filename.concat web_base "adapter-sx.sx";
Filename.concat web_base "web-forms.sx";
] in
(* Load spec files for all CLI modes that need rendering *)
(if mode = "aser" || mode = "aser-slot" || mode = "render" then
@@ -1588,6 +1601,7 @@ let test_mode () =
Filename.concat web_base "signals.sx";
Filename.concat web_base "adapter-html.sx";
Filename.concat web_base "adapter-sx.sx";
Filename.concat web_base "web-forms.sx";
] in
cli_load_files env files;
(* Register JIT *)