diff --git a/dune-project b/dune-project index bdbc7c57b..337b6c1e3 100644 --- a/dune-project +++ b/dune-project @@ -28,6 +28,7 @@ The client-side code is compiled to JS using Ocsigen Js_of_ocaml or to Wasm usin ppx_deriving (ppxlib (>= 0.15)) (js_of_ocaml-compiler (>= 6.0)) + wasm_of_ocaml-compiler (js_of_ocaml (>= 6.0)) (js_of_ocaml-lwt (>= 6.0)) (js_of_ocaml-ocamlbuild :build) diff --git a/eliom.opam b/eliom.opam index 0a5a47907..02b69bbbe 100644 --- a/eliom.opam +++ b/eliom.opam @@ -23,6 +23,7 @@ depends: [ "ppx_deriving" "ppxlib" {>= "0.15"} "js_of_ocaml-compiler" {>= "6.0"} + "wasm_of_ocaml-compiler" "js_of_ocaml" {>= "6.0"} "js_of_ocaml-lwt" {>= "6.0"} "js_of_ocaml-ocamlbuild" {build} diff --git a/pkg/distillery/templates/app.exe/Makefile.app b/pkg/distillery/templates/app.exe/Makefile.app index a26f95f10..5ad648b8c 100644 --- a/pkg/distillery/templates/app.exe/Makefile.app +++ b/pkg/distillery/templates/app.exe/Makefile.app @@ -67,6 +67,15 @@ install.static: $(TEST_PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).js | $(PREFIX)$( HASH=`md5sum _build/default/client/$(PROJECT_NAME).bc.js | cut -d ' ' -f 1` && \ install $(addprefix -o ,$(WWWUSER)) $(JS_PREFIX)_$$HASH.js $(PREFIX)$(ELIOMSTATICDIR) && \ ln -sf $(PROJECT_NAME)_$$HASH.js $(PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).js +ifeq ($(ENABLE_WASM),yes) + HASH_WASM=`md5sum _build/default/client/$(PROJECT_NAME).bc.wasm.js | cut -d ' ' -f 1` && \ + install $(addprefix -o ,$(WWWUSER)) $(JS_PREFIX)_$$HASH_WASM.wasm.js $(PREFIX)$(ELIOMSTATICDIR) && \ + ln -sf $(PROJECT_NAME)_$$HASH_WASM.wasm.js $(PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).wasm.js + if [ -d _build/default/client/$(PROJECT_NAME).bc.wasm.assets ]; then \ + cp -rf _build/default/client/$(PROJECT_NAME).bc.wasm.assets $(PREFIX)$(ELIOMSTATICDIR)/; \ + [ -z $(WWWUSER) ] || chown -R $(WWWUSER) $(PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).bc.wasm.assets; \ + fi +endif [ -z $(WWWUSER) ] || chown -R $(WWWUSER) $(PREFIX)$(FILESDIR) .PHONY: @@ -91,6 +100,14 @@ config-files: | $(TEST_PREFIX)$(ELIOMSTATICDIR) $(TEST_PREFIX)$(LIBDIR) HASH=`md5sum _build/default/client/$(PROJECT_NAME).bc.js | cut -d ' ' -f 1` && \ cp -f _build/default/client/$(PROJECT_NAME).bc.js $(JS_PREFIX)_$$HASH.js && \ ln -sf $(PROJECT_NAME)_$$HASH.js $(JS_PREFIX).js +ifeq ($(ENABLE_WASM),yes) + HASH_WASM=`md5sum _build/default/client/$(PROJECT_NAME).bc.wasm.js | cut -d ' ' -f 1` && \ + cp -f _build/default/client/$(PROJECT_NAME).bc.wasm.js $(JS_PREFIX)_$$HASH_WASM.wasm.js && \ + ln -sf $(PROJECT_NAME)_$$HASH_WASM.wasm.js $(JS_PREFIX).wasm.js + if [ -d _build/default/client/$(PROJECT_NAME).bc.wasm.assets ]; then \ + cp -rf _build/default/client/$(PROJECT_NAME).bc.wasm.assets $(TEST_PREFIX)$(ELIOMSTATICDIR)/; \ + fi +endif cp -f _build/default/$(PROJECT_NAME).cm* $(TEST_PREFIX)$(LIBDIR)/ all:: @@ -98,6 +115,9 @@ all:: js:: $(ENV_PSQL) dune build $(DUNE_OPTIONS) client/$(PROJECT_NAME).bc.js +ifeq ($(ENABLE_WASM),yes) + $(ENV_PSQL) dune build $(DUNE_OPTIONS) client/$(PROJECT_NAME).bc.wasm.js +endif byte:: js $(ENV_PSQL) dune build $(DUNE_OPTIONS) $(PROJECT_NAME)_main.bc diff --git a/pkg/distillery/templates/app.exe/Makefile.options b/pkg/distillery/templates/app.exe/Makefile.options index 6e6550394..2496462f1 100644 --- a/pkg/distillery/templates/app.exe/Makefile.options +++ b/pkg/distillery/templates/app.exe/Makefile.options @@ -73,5 +73,8 @@ CSS_PREFIX := $(LOCAL_STATIC_CSS)/${PROJECT_NAME} # JavaScript, ocsigenserver DEBUG := yes +# Enable WASM compilation (yes/no): Requires wasm_of_ocaml (enabled by default) +ENABLE_WASM := yes + ##---------------------------------------------------------------------- diff --git a/pkg/distillery/templates/app.exe/PROJECT_NAME.eliom b/pkg/distillery/templates/app.exe/PROJECT_NAME.eliom index 2f69b1386..d09e9404f 100644 --- a/pkg/distillery/templates/app.exe/PROJECT_NAME.eliom +++ b/pkg/distillery/templates/app.exe/PROJECT_NAME.eliom @@ -5,7 +5,11 @@ let%server application_name = "%%%PROJECT_NAME%%%" let%client application_name = Eliom_client.get_application_name () let%server () = - Ocsipersist_settings.set_db_file "local/var/data/%%%PROJECT_NAME%%%/%%%PROJECT_NAME%%%_db"; + Ocsipersist_settings.set_db_file "local/var/data/%%%PROJECT_NAME%%%/%%%PROJECT_NAME%%%_db" + (* Enable WebAssembly support with automatic browser detection. + The server will load the WASM version if the browser supports it, + with fallback to JavaScript otherwise. *) + ; Eliom_config.set_enable_wasm true (* Create a module for the application. See https://ocsigen.org/eliom/manual/clientserver-applications for more diff --git a/pkg/distillery/templates/app.exe/dune b/pkg/distillery/templates/app.exe/dune index 79f6fd711..30daabc8a 100644 --- a/pkg/distillery/templates/app.exe/dune +++ b/pkg/distillery/templates/app.exe/dune @@ -86,7 +86,7 @@ client (executables (names %%%PROJECT_NAME%%%) - (modes js byte) + (modes js byte wasm) (preprocess (pps js_of_ocaml-ppx @@ -106,9 +106,9 @@ --enable with-js-error --enable - use-js-string - --no-source-map)) - ; source maps are slow... + use-js-string)) + (wasm_of_ocaml + (flags :standard)) (libraries eliom.client)) (dynamic_include ../gen/dune.client)) @@ -122,6 +122,7 @@ %%%PROJECT_NAME%%%_main.exe client/%%%PROJECT_NAME%%%.bc client/%%%PROJECT_NAME%%%.bc.js + client/%%%PROJECT_NAME%%%.bc.wasm.js tools/check_modules.ml) (action (run ocaml -I +unix -I +str tools/check_modules.ml %%%PROJECT_NAME%%%))) diff --git a/pkg/distillery/templates/app.exe/dune-project b/pkg/distillery/templates/app.exe/dune-project index ba1d67e4a..cf94fbb75 100644 --- a/pkg/distillery/templates/app.exe/dune-project +++ b/pkg/distillery/templates/app.exe/dune-project @@ -1,10 +1,13 @@ -(lang dune 3.14) +(lang dune 3.17) (dialect (name "eliom-server") - (implementation (extension "eliom")) - (interface (extension "eliomi"))) + (implementation + (extension "eliom")) + (interface + (extension "eliomi"))) (wrapped_executables false) -(formatting (enabled_for ocaml "eliom-server")) +(formatting + (enabled_for ocaml "eliom-server")) diff --git a/pkg/distillery/templates/app.lib/Makefile.app b/pkg/distillery/templates/app.lib/Makefile.app index fbcf2ab9e..660e0e8c5 100644 --- a/pkg/distillery/templates/app.lib/Makefile.app +++ b/pkg/distillery/templates/app.lib/Makefile.app @@ -74,6 +74,15 @@ install.static: $(TEST_PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).js | $(PREFIX)$( HASH=`md5sum _build/default/client/$(PROJECT_NAME).bc.js | cut -d ' ' -f 1` && \ install $(addprefix -o ,$(WWWUSER)) $(JS_PREFIX)_$$HASH.js $(PREFIX)$(ELIOMSTATICDIR) && \ ln -sf $(PROJECT_NAME)_$$HASH.js $(PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).js +ifeq ($(ENABLE_WASM),yes) + HASH_WASM=`md5sum _build/default/client/$(PROJECT_NAME).bc.wasm.js | cut -d ' ' -f 1` && \ + install $(addprefix -o ,$(WWWUSER)) $(JS_PREFIX)_$$HASH_WASM.wasm.js $(PREFIX)$(ELIOMSTATICDIR) && \ + ln -sf $(PROJECT_NAME)_$$HASH_WASM.wasm.js $(PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).wasm.js + if [ -d _build/default/client/$(PROJECT_NAME).bc.wasm.assets ]; then \ + cp -rf _build/default/client/$(PROJECT_NAME).bc.wasm.assets $(PREFIX)$(ELIOMSTATICDIR)/; \ + [ -z $(WWWUSER) ] || chown -R $(WWWUSER) $(PREFIX)$(ELIOMSTATICDIR)/$(PROJECT_NAME).bc.wasm.assets; \ + fi +endif [ -z $(WWWUSER) ] || chown -R $(WWWUSER) $(PREFIX)$(FILESDIR) install.etc: $(TEST_PREFIX)$(ETCDIR)/$(PROJECT_NAME).conf | $(PREFIX)$(ETCDIR) install $< $(PREFIX)$(ETCDIR)/$(PROJECT_NAME).conf @@ -122,6 +131,11 @@ SED_ARGS += -e "s|%%FILESDIR%%|%%PREFIX%%$(FILESDIR)|g" SED_ARGS += -e "s|%%ELIOMSTATICDIR%%|%%PREFIX%%$(ELIOMSTATICDIR)|g" SED_ARGS += -e "s|%%APPNAME%%|$(shell basename `readlink $(JS_PREFIX).js` .js)|g" SED_ARGS += -e "s|%%CSSNAME%%|$(shell readlink $(CSS_PREFIX).css)|g" +ifeq ($(ENABLE_WASM),yes) + SED_ARGS += -e "s|%%WASMATTR%%|wasm=\"$(shell basename `readlink $(JS_PREFIX).wasm.js` .wasm.js).wasm.js\"|g" +else + SED_ARGS += -e "s|%%WASMATTR%%||g" +endif ifeq ($(DEBUG),yes) SED_ARGS += -e "s|%%DEBUGMODE%%|\|g" else @@ -156,6 +170,14 @@ config-files: | $(TEST_PREFIX)$(ELIOMSTATICDIR) $(TEST_PREFIX)$(LIBDIR) HASH=`md5sum _build/default/client/$(PROJECT_NAME).bc.js | cut -d ' ' -f 1` && \ cp -f _build/default/client/$(PROJECT_NAME).bc.js $(JS_PREFIX)_$$HASH.js && \ ln -sf $(PROJECT_NAME)_$$HASH.js $(JS_PREFIX).js +ifeq ($(ENABLE_WASM),yes) + HASH_WASM=`md5sum _build/default/client/$(PROJECT_NAME).bc.wasm.js | cut -d ' ' -f 1` && \ + cp -f _build/default/client/$(PROJECT_NAME).bc.wasm.js $(JS_PREFIX)_$$HASH_WASM.wasm.js && \ + ln -sf $(PROJECT_NAME)_$$HASH_WASM.wasm.js $(JS_PREFIX).wasm.js + if [ -d _build/default/client/$(PROJECT_NAME).bc.wasm.assets ]; then \ + cp -rf _build/default/client/$(PROJECT_NAME).bc.wasm.assets $(TEST_PREFIX)$(ELIOMSTATICDIR)/; \ + fi +endif cp -f _build/default/$(PROJECT_NAME).cm* $(TEST_PREFIX)$(LIBDIR)/ $(MAKE) $(CONFIG_FILES) $(TEST_CONFIG_FILES) PROJECT_NAME=$(PROJECT_NAME) @@ -164,6 +186,9 @@ all:: js:: $(ENV_PSQL) dune build $(DUNE_OPTIONS) client/$(PROJECT_NAME).bc.js +ifeq ($(ENABLE_WASM),yes) + $(ENV_PSQL) dune build $(DUNE_OPTIONS) client/$(PROJECT_NAME).bc.wasm.js +endif byte:: $(ENV_PSQL) dune build $(DUNE_OPTIONS) @$(PROJECT_NAME) diff --git a/pkg/distillery/templates/app.lib/Makefile.options b/pkg/distillery/templates/app.lib/Makefile.options index a65be2a0e..13d2a8e8b 100644 --- a/pkg/distillery/templates/app.lib/Makefile.options +++ b/pkg/distillery/templates/app.lib/Makefile.options @@ -73,5 +73,8 @@ CSS_PREFIX := $(LOCAL_STATIC_CSS)/${PROJECT_NAME} # JavaScript, ocsigenserver DEBUG := yes +# Enable WASM compilation (yes/no): Requires wasm_of_ocaml (enabled by default) +ENABLE_WASM := yes + ##---------------------------------------------------------------------- diff --git a/pkg/distillery/templates/app.lib/PROJECT_NAME.conf.in b/pkg/distillery/templates/app.lib/PROJECT_NAME.conf.in index 0f7b3af57..bc04fa06c 100644 --- a/pkg/distillery/templates/app.lib/PROJECT_NAME.conf.in +++ b/pkg/distillery/templates/app.lib/PROJECT_NAME.conf.in @@ -41,6 +41,8 @@ + + @@ -51,7 +53,7 @@ %%ELIOM_MODULES%% - + sitedata.secure_cookies <- v) secure_cookies; Option.iter (fun v -> sitedata.application_script <- v) application_script; + (* Always update enable_wasm: use provided value or current global default *) + sitedata.enable_wasm <- + Option.value enable_wasm ~default:!Eliommod.default_enable_wasm; Option.iter (fun v -> sitedata.cache_global_data <- v) global_data_caching; Option.iter (fun v -> sitedata.html_content_type <- Some v) html_content_type; Option.iter diff --git a/src/lib/eliom.server.mli b/src/lib/eliom.server.mli index 7d3cc9a8d..09d0144b6 100644 --- a/src/lib/eliom.server.mli +++ b/src/lib/eliom.server.mli @@ -32,6 +32,7 @@ val run : -> ?max_anonymous_services_per_session:int * bool -> ?secure_cookies:bool -> ?application_script:bool * bool + -> ?enable_wasm:bool -> ?global_data_caching:(string list * int) option -> ?html_content_type:string -> ?ignored_get_params:string * Re.re @@ -42,4 +43,10 @@ val run : (** [run ?app ()] run Eliom application [app] under current site. Use this to build a static executable without configuration file. Default value of [?app] is [default_app_name]. - Other optional values correspond to Eliom configuration for this site. *) + Other optional values correspond to Eliom configuration for this site. + + When [?enable_wasm] is set to [true], the server will generate a detection + script that loads the WASM version of your client code if the browser + supports WebAssembly, with automatic fallback to JavaScript otherwise. + If not specified, the global default is used + (see {!Eliom_config.set_enable_wasm}), which is [false]. *) diff --git a/src/lib/eliom_common.server.ml b/src/lib/eliom_common.server.ml index 993e9defd..c221dcd5c 100644 --- a/src/lib/eliom_common.server.ml +++ b/src/lib/eliom_common.server.ml @@ -493,7 +493,8 @@ and sitedata = ; mutable ipv6mask : int option * bool ; mutable application_script : bool (* defer *) * bool ; (* async *) - mutable cache_global_data : (string list * int) option + mutable enable_wasm : bool + ; mutable cache_global_data : (string list * int) option ; mutable html_content_type : string option ; mutable ignored_get_params : (string * Re.re) list ; mutable ignored_post_params : (string * Re.re) list diff --git a/src/lib/eliom_common.server.mli b/src/lib/eliom_common.server.mli index 22d3088f3..04de1afa2 100644 --- a/src/lib/eliom_common.server.mli +++ b/src/lib/eliom_common.server.mli @@ -558,7 +558,8 @@ and sitedata = ; mutable ipv6mask : int option * bool ; mutable application_script : bool (* defer *) * bool ; (* async *) - mutable cache_global_data : (string list * int) option + mutable enable_wasm : bool + ; mutable cache_global_data : (string list * int) option ; mutable html_content_type : string option ; mutable ignored_get_params : (string * Re.re) list ; mutable ignored_post_params : (string * Re.re) list diff --git a/src/lib/eliom_config.server.ml b/src/lib/eliom_config.server.ml index 059de8720..a9ef09e74 100644 --- a/src/lib/eliom_config.server.ml +++ b/src/lib/eliom_config.server.ml @@ -76,6 +76,8 @@ let set_max_volatile_groups_per_site v = let set_secure_cookies v = Eliommod.default_secure_cookies := v let set_application_script v = Eliommod.default_application_script := v +let set_enable_wasm v = Eliommod.default_enable_wasm := v +let get_enable_wasm () = !Eliommod.default_enable_wasm let set_cache_global_data v = Eliommod.default_cache_global_data := v let set_html_content_type v = Eliommod.default_html_content_type := Some v diff --git a/src/lib/eliom_config.server.mli b/src/lib/eliom_config.server.mli index 6e5e8d995..c58712d49 100644 --- a/src/lib/eliom_config.server.mli +++ b/src/lib/eliom_config.server.mli @@ -86,6 +86,23 @@ val set_max_anonymous_services_per_session : int -> unit val set_max_volatile_groups_per_site : int -> unit val set_secure_cookies : bool -> unit val set_application_script : bool * bool -> unit + +val set_enable_wasm : bool -> unit +(** Enable or disable WebAssembly support with automatic browser detection. + When enabled, Eliom will generate a script that detects WebAssembly + support in the browser and loads either the WASM version (.wasm.js) or + the JavaScript fallback (.js) accordingly. + + Disabled by default for backward compatibility. The Eliom templates + enable it explicitly. + + This setting can be overridden per-site using the [~enable_wasm] parameter + of {!Eliom.run} or the [] tag in the + configuration file. *) + +val get_enable_wasm : unit -> bool +(** Get the current global setting for WebAssembly support. *) + val set_cache_global_data : (Eliom_lib.Url.path * int) option -> unit val set_html_content_type : string -> unit val add_ignored_get_params : string * Re.re -> unit diff --git a/src/lib/eliom_registration.server.ml b/src/lib/eliom_registration.server.ml index 23ddbae65..0e67dc369 100644 --- a/src/lib/eliom_registration.server.ml +++ b/src/lib/eliom_registration.server.ml @@ -869,6 +869,14 @@ module type APP = sig -> unit -> [> `Script] Eliom_content.Html.elt + val wasm_detection_script : + ?defer:bool + -> ?async:bool + -> ?js_name:string + -> ?wasm_name:string + -> unit + -> [> `Script] Eliom_content.Html.elt + val application_name : string val is_initial_request : unit -> bool @@ -906,7 +914,64 @@ module App_base (App_param : Eliom_registration_sigs.APP_PARAM) = struct let eliom_appl_script_id : [`Script] Eliom_content.Html.Id.id = Eliom_content.Html.Id.new_elt_id ~global:true () - let application_script ?defer ?async () = + (* Generate an inline script for detecting and loading WASM/JS *) + let wasm_detection_script ?defer ?async ?js_name ?wasm_name () = + let defer', async' = + (Eliom_request_info.get_sitedata ()).Eliom_common.application_script + in + let defer = match defer with Some b -> b | None -> defer' in + let async = match async with Some b -> b | None -> async' in + let defer_str = if defer then "true" else "false" in + let async_str = if async then "true" else "false" in + (* Use provided filenames with hash, or default to application_name *) + let js_filename = + match js_name with + | Some name -> name + | None -> App_param.application_name ^ ".js" + in + let wasm_filename = + match wasm_name with + | Some name -> name + | None -> App_param.application_name ^ ".wasm.js" + in + (* Generate full URIs for both JS and WASM versions using make_uri *) + let js_uri = + Eliom_content.Html.F.string_of_uri + (Eliom_content.Html.F.make_uri + ~service:(Eliom_service.static_dir ()) + [js_filename]) + in + let wasm_uri = + Eliom_content.Html.F.string_of_uri + (Eliom_content.Html.F.make_uri + ~service:(Eliom_service.static_dir ()) + [wasm_filename]) + in + let script_content = + Printf.sprintf + "(function() {\n\ var script = document.createElement('script');\n\ script.defer = %s;\n\ script.async = %s;\n\ \n\ if (window?.WebAssembly?.JSTag) {\n\ script.src = '%s';\n\ } else {\n\ script.src = '%s';\n\ }\n\ \n\ document.head.appendChild(script);\n})();" + defer_str async_str wasm_uri js_uri + in + Eliom_content.Html.Id.create_named_elt ~id:eliom_appl_script_id + (Eliom_content.Html.F.script + (Eliom_content.Html.F.cdata_script script_content)) + + let wasm_detection_script = + (wasm_detection_script + : ?defer:_ + -> ?async:_ + -> ?js_name:_ + -> ?wasm_name:_ + -> _ + -> [`Script] Eliom_content.Html.elt + :> ?defer:_ + -> ?async:_ + -> ?js_name:_ + -> ?wasm_name:_ + -> _ + -> [> `Script] Eliom_content.Html.elt) + + let js_only_application_script ?defer ?async () = let defer', async' = (Eliom_request_info.get_sitedata ()).Eliom_common.application_script in @@ -919,11 +984,17 @@ module App_base (App_param : Eliom_registration_sigs.APP_PARAM) = struct Eliom_content.Html.Id.create_named_elt ~id:eliom_appl_script_id (Eliom_content.Html.D.js_script ~a ~uri: - (Eliom_content.Html.D.make_uri + (Eliom_content.Html.F.make_uri ~service:(Eliom_service.static_dir ()) [App_param.application_name ^ ".js"]) ()) + (* Automatically return WASM detection script or JS-only script based on config *) + let application_script ?defer ?async () = + if (Eliom_request_info.get_sitedata ()).Eliom_common.enable_wasm + then wasm_detection_script ?defer ?async () + else js_only_application_script ?defer ?async () + let application_script = (application_script : ?defer:_ -> ?async:_ -> _ -> [`Script] Eliom_content.Html.elt @@ -1070,7 +1141,9 @@ module App_base (App_param : Eliom_registration_sigs.APP_PARAM) = struct let head_elts = if List.exists is_eliom_appl_script head_elts then head_elts - else head_elts @ [application_script ()] + else + (* application_script now automatically detects WASM *) + head_elts @ [application_script ()] in let head_elts = appl_data_script @@ -1181,6 +1254,7 @@ module App (App_param : Eliom_registration_sigs.APP_PARAM) = struct let is_initial_request = Base.is_initial_request let application_script = Base.application_script + let wasm_detection_script = Base.wasm_detection_script include Eliom_mkreg.Make (Base) diff --git a/src/lib/eliom_registration.server.mli b/src/lib/eliom_registration.server.mli index 4fb0feb39..1e2f109fc 100644 --- a/src/lib/eliom_registration.server.mli +++ b/src/lib/eliom_registration.server.mli @@ -120,6 +120,43 @@ module type APP = sig do not include this script in the [] node of your page, it will be automatically added at the end of the [] node. *) + val wasm_detection_script : + ?defer:bool + -> ?async:bool + -> ?js_name:string + -> ?wasm_name:string + -> unit + -> [> `Script] Eliom_content.Html.elt + (** The function [wasm_detection_script ()] returns an inline [