From 176e67e5b7d6694b0dec749cfbfa2ad849b9876f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 18 Jul 2024 03:54:03 +0200 Subject: [PATCH] switch to a Document trait and introduce Script/Head/Style/Meta components (#2635) * switch to a Document trait and introduce Script/Head/Style/Meta components * Fix desktop title * Insert special elements into the head during the inital SSR render * Make all head component attributes optional * hydrate head elements * improve the server streaming head warning * Document fullstack head hydration approach * deduplicate head elements by href * move Link into head::Link * document head components * add meta and title examples * Fix a few doc examples * fix formatting * Add title to playwright tests * serde is optional on web, but it is enabled by hydrate * remove leftover console log --- Cargo.lock | 484 ++++++++-------- Cargo.toml | 7 + examples/meta.rs | 36 ++ examples/title.rs | 22 + packages/core/src/virtual_dom.rs | 1 - packages/desktop/Cargo.toml | 4 +- packages/desktop/src/config.rs | 4 +- packages/desktop/src/{eval.rs => document.rs} | 12 +- packages/desktop/src/lib.rs | 2 +- packages/desktop/src/protocol.rs | 2 +- packages/desktop/src/webview.rs | 7 +- packages/dioxus/Cargo.toml | 4 +- packages/fullstack/Cargo.toml | 6 +- packages/fullstack/src/document/mod.rs | 4 + packages/fullstack/src/document/server.rs | 140 +++++ packages/fullstack/src/document/web.rs | 58 ++ packages/fullstack/src/hooks/server_cached.rs | 28 +- packages/fullstack/src/html_storage/mod.rs | 6 +- packages/fullstack/src/launch.rs | 15 +- packages/fullstack/src/lib.rs | 2 + packages/fullstack/src/render.rs | 44 +- packages/fullstack/src/serve_config.rs | 11 +- packages/html/Cargo.toml | 13 +- packages/html/assets/script.js | 0 packages/html/assets/style.css | 0 packages/html/build.rs | 9 + packages/html/docs/head.md | 59 ++ .../eval.rs => html/src/document/bindings.rs} | 9 +- packages/html/src/{ => document}/eval.rs | 53 +- packages/html/src/document/head.rs | 520 ++++++++++++++++++ packages/html/src/document/mod.rs | 114 ++++ packages/html/src/elements.rs | 8 + packages/{interpreter => html}/src/js/eval.js | 0 packages/html/src/js/hash.txt | 1 + packages/html/src/js/head.js | 1 + .../src/js/native_eval.js | 0 packages/html/src/lib.rs | 11 +- packages/html/src/ts/.gitignore | 3 + packages/{interpreter => html}/src/ts/eval.ts | 0 packages/html/src/ts/head.ts | 19 + .../src/ts/native_eval.ts | 0 packages/html/tsconfig.json | 18 + packages/interpreter/Cargo.toml | 3 +- packages/interpreter/build.rs | 94 +--- packages/interpreter/src/js/README.md | 1 - packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/lib.rs | 3 - packages/lazy-js-bundle/Cargo.toml | 11 + packages/lazy-js-bundle/src/lib.rs | 202 +++++++ packages/liveview/Cargo.toml | 2 +- packages/liveview/src/eval.rs | 26 +- packages/liveview/src/lib.rs | 2 +- packages/playwright-tests/fullstack.spec.js | 30 +- .../playwright-tests/fullstack/src/main.rs | 1 + .../playwright-tests/nested-suspense.spec.js | 3 + .../nested-suspense/src/main.rs | 15 + packages/playwright-tests/web.spec.js | 4 + packages/playwright-tests/web/src/main.rs | 1 + packages/router/src/components/link.rs | 1 + packages/router/src/history/liveview.rs | 8 +- packages/ssr/Cargo.toml | 2 +- packages/web/Cargo.toml | 6 +- packages/web/src/{eval.rs => document.rs} | 22 +- packages/web/src/hydration/deserialize.rs | 12 + packages/web/src/lib.rs | 10 +- 65 files changed, 1703 insertions(+), 495 deletions(-) create mode 100644 examples/meta.rs create mode 100644 examples/title.rs rename packages/desktop/src/{eval.rs => document.rs} (90%) create mode 100644 packages/fullstack/src/document/mod.rs create mode 100644 packages/fullstack/src/document/server.rs create mode 100644 packages/fullstack/src/document/web.rs create mode 100644 packages/html/assets/script.js create mode 100644 packages/html/assets/style.css create mode 100644 packages/html/build.rs create mode 100644 packages/html/docs/head.md rename packages/{interpreter/src/eval.rs => html/src/document/bindings.rs} (87%) rename packages/html/src/{ => document}/eval.rs (60%) create mode 100644 packages/html/src/document/head.rs create mode 100644 packages/html/src/document/mod.rs rename packages/{interpreter => html}/src/js/eval.js (100%) create mode 100644 packages/html/src/js/hash.txt create mode 100644 packages/html/src/js/head.js rename packages/{interpreter => html}/src/js/native_eval.js (100%) create mode 100644 packages/html/src/ts/.gitignore rename packages/{interpreter => html}/src/ts/eval.ts (100%) create mode 100644 packages/html/src/ts/head.ts rename packages/{interpreter => html}/src/ts/native_eval.ts (100%) create mode 100644 packages/html/tsconfig.json delete mode 100644 packages/interpreter/src/js/README.md create mode 100644 packages/lazy-js-bundle/Cargo.toml create mode 100644 packages/lazy-js-bundle/src/lib.rs rename packages/web/src/{eval.rs => document.rs} (89%) diff --git a/Cargo.lock b/Cargo.lock index 93fb68dfc..d278f4949 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,7 +236,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -258,7 +258,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -356,8 +356,8 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "zstd 0.13.1", - "zstd-safe 7.1.0", + "zstd 0.13.2", + "zstd-safe 7.2.0", ] [[package]] @@ -497,7 +497,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -552,13 +552,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -665,7 +665,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "itoa 1.0.11", "matchit", @@ -787,7 +787,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -812,7 +812,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "pin-project-lite", "rustls 0.21.12", @@ -986,9 +986,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8" +checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499" [[package]] name = "bitvec" @@ -1050,7 +1050,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "syn_derive", ] @@ -1227,7 +1227,7 @@ checksum = "aa7015584550945f11fdfb7af113d30e2727468ec281c1d7f28cc1019196c25d" dependencies = [ "anyhow", "auth-git2", - "clap 4.5.8", + "clap 4.5.9", "console", "dialoguer", "env_logger 0.11.3", @@ -1259,19 +1259,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "cargo-lock" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72" -dependencies = [ - "petgraph", - "semver", - "serde", - "toml 0.7.8", - "url", -] - [[package]] name = "cargo-platform" version = "0.1.8" @@ -1281,20 +1268,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo_metadata" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7daec1a2a2129eeba1644b220b4647ec537b0b5d4bfd6876fcc5a540056b592" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "cargo_metadata" version = "0.18.1" @@ -1309,16 +1282,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cargo_toml" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3f9629bc6c4388ea699781dc988c2b99766d7679b151c81990b4fa1208fafd3" -dependencies = [ - "serde", - "toml 0.8.14", -] - [[package]] name = "cargo_toml" version = "0.18.0" @@ -1337,9 +1300,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.104" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" dependencies = [ "jobserver", "libc", @@ -1403,7 +1366,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1456,9 +1419,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -1466,9 +1429,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -1485,7 +1448,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -1825,7 +1788,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.8", + "clap 4.5.9", "criterion-plot 0.5.0", "futures", "is-terminal", @@ -1970,7 +1933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2001,7 +1964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2056,7 +2019,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2073,14 +2036,14 @@ checksum = "4b2c1c1776b986979be68bb2285da855f8d8a35851a769fca8740df7c3d07877" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -2088,27 +2051,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2170,7 +2133,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2245,7 +2208,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2257,7 +2220,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2271,10 +2234,10 @@ dependencies = [ "axum-server", "brotli 5.0.0", "cargo-generate", - "cargo_metadata 0.18.1", - "cargo_toml 0.18.0", + "cargo_metadata", + "cargo_toml", "chrono", - "clap 4.5.8", + "clap 4.5.9", "colored 2.1.0", "ctrlc", "dioxus-autofmt", @@ -2292,7 +2255,7 @@ dependencies = [ "futures-util", "headers 0.3.9", "html_parser", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-rustls 0.26.0", "hyper-util", "ignore", @@ -2312,7 +2275,7 @@ dependencies = [ "serde", "serde_json", "subprocess", - "syn 2.0.68", + "syn 2.0.70", "tar", "tauri-bundler", "tempfile", @@ -2335,8 +2298,8 @@ dependencies = [ name = "dioxus-cli-config" version = "0.5.2" dependencies = [ - "cargo_toml 0.18.0", - "clap 4.5.8", + "cargo_toml", + "clap 4.5.9", "dirs", "once_cell", "serde", @@ -2409,7 +2372,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.68", + "syn 2.0.70", "tokio", "trybuild", ] @@ -2477,6 +2440,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing-subscriber", ] [[package]] @@ -2498,7 +2462,7 @@ dependencies = [ "base64 0.21.7", "bytes", "ciborium", - "clap 4.5.8", + "clap 4.5.9", "dioxus", "dioxus-cli-config", "dioxus-desktop", @@ -2511,8 +2475,9 @@ dependencies = [ "dioxus_server_macro", "futures-channel", "futures-util", + "generational-box 0.5.2", "http 1.1.0", - "hyper 1.4.0", + "hyper 1.4.1", "once_cell", "parking_lot", "pin-project", @@ -2590,6 +2555,8 @@ dependencies = [ "async-trait", "dioxus", "dioxus-core 0.5.2", + "dioxus-core-macro", + "dioxus-hooks", "dioxus-html-internal-macro", "dioxus-rsx", "dioxus-web", @@ -2599,6 +2566,8 @@ dependencies = [ "generational-box 0.5.2", "js-sys", "keyboard-types", + "lazy-js-bundle", + "manganis", "rfd", "rustversion", "serde", @@ -2608,6 +2577,7 @@ dependencies = [ "tokio", "tracing", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] @@ -2618,7 +2588,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "trybuild", ] @@ -2629,7 +2599,7 @@ dependencies = [ "dioxus-core 0.5.2", "dioxus-html", "js-sys", - "md5", + "lazy-js-bundle", "serde", "sledgehammer_bindgen", "sledgehammer_utils", @@ -2774,7 +2744,7 @@ dependencies = [ "proc-macro2", "quote", "slab", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2788,7 +2758,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.68", + "syn 2.0.70", "tracing", ] @@ -2907,7 +2877,7 @@ dependencies = [ "proc-macro2", "quote", "server_fn_macro", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -2984,7 +2954,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3065,7 +3035,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3086,7 +3056,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3107,7 +3077,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3218,7 +3188,7 @@ checksum = "3a82608ee96ce76aeab659e9b8d3c2b787bffd223199af88c674923d861ada10" dependencies = [ "execute-command-macro", "execute-command-tokens", - "generic-array 1.0.0", + "generic-array 1.1.0", ] [[package]] @@ -3238,7 +3208,7 @@ checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b" dependencies = [ "execute-command-tokens", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3397,7 +3367,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3580,7 +3550,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -3751,9 +3721,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe739944a5406424e080edccb6add95685130b9f160d5407c639c7df0c5836b0" +checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a" dependencies = [ "typenum", ] @@ -3877,9 +3847,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.31.3" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f52455500a0fac1fd62a1cf42d9121cfddef8cb3ded2f9e7adb5775deb1fc9" +checksum = "d9b8ee65074b2bbb91d9d97c15d172ea75043aefebf9869b5b329149dc76501c" dependencies = [ "bstr 1.9.1", "gix-date", @@ -3995,9 +3965,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.42.2" +version = "0.42.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe2dc4a41191c680c942e6ebd630c8107005983c4679214fdb1007dcf5ae1df" +checksum = "25da2f46b4e7c2fa7b413ce4dffb87f69eaf89c2057e386491f4c55cadbfe386" dependencies = [ "bstr 1.9.1", "gix-actor", @@ -4132,7 +4102,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4443,7 +4413,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -4841,9 +4811,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -4865,9 +4835,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -4892,7 +4862,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.29", + "hyper 0.14.30", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -4906,7 +4876,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "log", "rustls 0.22.4", @@ -4925,9 +4895,9 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -4941,7 +4911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.30", "native-tls", "tokio", "tokio-native-tls", @@ -4955,7 +4925,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-util", "native-tls", "tokio", @@ -4974,7 +4944,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.4.0", + "hyper 1.4.1", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -5251,7 +5221,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -5527,7 +5497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcb3baf2360eb25ad31f0ada3add63927ada6db457791979b82ac199f835cb9" dependencies = [ "cargo-platform", - "cargo_metadata 0.18.1", + "cargo_metadata", "cfg-expr", "petgraph", "semver", @@ -5565,6 +5535,10 @@ dependencies = [ "log", ] +[[package]] +name = "lazy-js-bundle" +version = "0.5.2" + [[package]] name = "lazy_static" version = "1.5.0" @@ -5806,7 +5780,7 @@ checksum = "915f6d0a2963a27cd5205c1902f32ddfe3bc035816afd268cf88c0fc0f8d287e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -5933,28 +5907,23 @@ dependencies = [ [[package]] name = "manganis-cli-support" version = "0.2.4" -source = "git+https://github.com/DogeDark/dioxus-collect-assets/#6b6077150c854af6497d8b96d3a070635ab9b6c4" +source = "git+https://github.com/DogeDark/dioxus-collect-assets/#ad09a549775957c5ce177274cfbb6dc76c976c10" dependencies = [ "anyhow", - "cargo-lock", - "cargo_metadata 0.17.0", - "cargo_toml 0.16.3", "image 0.25.1", "imagequant", "lightningcss", "manganis-common 0.2.4 (git+https://github.com/DogeDark/dioxus-collect-assets/)", "mozjpeg", "object", - "petgraph", "png", "railwind", "ravif", "rayon", - "reqwest 0.11.27", + "reqwest 0.12.5", "rustc-hash", "serde", "serde_json", - "toml 0.7.8", "tracing", "url", ] @@ -5979,13 +5948,13 @@ dependencies = [ [[package]] name = "manganis-common" version = "0.2.4" -source = "git+https://github.com/DogeDark/dioxus-collect-assets/#6b6077150c854af6497d8b96d3a070635ab9b6c4" +source = "git+https://github.com/DogeDark/dioxus-collect-assets/#ad09a549775957c5ce177274cfbb6dc76c976c10" dependencies = [ "anyhow", "base64 0.21.7", "home", "infer 0.11.0", - "reqwest 0.11.27", + "reqwest 0.12.5", "serde", "toml 0.7.8", "tracing", @@ -6001,7 +5970,7 @@ dependencies = [ "manganis-common 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "tracing-subscriber", ] @@ -6177,11 +6146,12 @@ dependencies = [ [[package]] name = "mozjpeg" -version = "0.10.7" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3bcd16df874918cb91d9ca985d7fde466bdc3b0983bf83c33395033568e510" +checksum = "8c1e1f1b1de2ee6ef673e8c76666b93794120fa6ec1cb4f535c129ea6f32731d" dependencies = [ "arrayvec", + "bytemuck", "libc", "mozjpeg-sys", "rgb", @@ -6457,7 +6427,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -6606,9 +6576,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "opaque-debug" @@ -6618,9 +6588,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" dependencies = [ "is-wsl", "libc", @@ -6650,7 +6620,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -6818,7 +6788,7 @@ dependencies = [ "libc", "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6920,7 +6890,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -7048,7 +7018,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -7095,7 +7065,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -7302,7 +7272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -7394,7 +7364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -7596,9 +7566,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67376f469e7e7840d0040bbf4b9b3334005bb167f814621326e4c7ab8cd6e944" +checksum = "c6ba61c28ba24c0cf8406e025cb29a742637e3f70776e61c27a8a8b72a042d12" dependencies = [ "avif-serialize", "imgref", @@ -7754,7 +7724,7 @@ dependencies = [ "hickory-resolver", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-rustls 0.24.2", "hyper-tls 0.5.0", "ipnet", @@ -7796,13 +7766,14 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "hyper-rustls 0.27.2", "hyper-tls 0.6.0", "hyper-util", @@ -7865,9 +7836,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.40" +version = "0.8.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" +checksum = "1aee83dc281d5a3200d37b299acd13b81066ea126a7f16f0eae70fc9aed241d9" dependencies = [ "bytemuck", ] @@ -7891,13 +7862,13 @@ dependencies = [ [[package]] name = "rhai_codegen" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59aecf17969c04b9c0c5d21f6bc9da9fec9dd4980e64d1871443a476589d8c86" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -8003,7 +7974,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -8091,29 +8062,31 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.5", "subtle", "zeroize", ] [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ + "log", "once_cell", + "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.5", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" dependencies = [ "openssl-probe", "rustls-pemfile 2.1.2", @@ -8159,9 +8132,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "ring", "rustls-pki-types", @@ -8321,9 +8294,9 @@ checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -8361,13 +8334,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -8420,7 +8393,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -8446,9 +8419,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.2" +version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079f3a42cd87588d924ed95b533f8d30a483388c4e400ab736a7058e34f16169" +checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" dependencies = [ "base64 0.22.1", "chrono", @@ -8464,14 +8437,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.2" +version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc03aad67c1d26b7de277d51c86892e7d9a0110a2fe44bf6b26cc569fba302d6" +checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -8488,7 +8461,7 @@ dependencies = [ "gloo-net 0.5.0", "http 1.1.0", "http-body-util", - "hyper 1.4.0", + "hyper 1.4.1", "inventory", "js-sys", "once_cell", @@ -8519,7 +8492,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "xxhash-rust", ] @@ -8530,7 +8503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00783df297ec85ea605779f2fef9cbec98981dffe2e01e1a9845c102ee1f1ae6" dependencies = [ "server_fn_macro", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -8698,12 +8671,12 @@ dependencies = [ [[package]] name = "sledgehammer_bindgen_macro" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd941cc539bd3dc694edaf9d0c4e1221d02baa67c6b45ec04fad1024d9e8139" +checksum = "edc90d3e8623d29a664cd8dba5078b600dd203444f00b9739f744e4c6e7aeaf2" dependencies = [ "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -9178,9 +9151,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", @@ -9196,7 +9169,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -9313,9 +9286,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" [[package]] name = "tauri-bundler" @@ -9476,7 +9449,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -9554,9 +9527,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -9600,7 +9573,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -9640,7 +9613,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", ] @@ -9706,7 +9679,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.15", ] [[package]] @@ -9755,9 +9728,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ "indexmap 2.2.6", "serde", @@ -9845,7 +9818,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -9927,9 +9900,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a5f13f11071020bb12de7a16b925d2d58636175c20c11dc5f96cb64bb6c9b3" +checksum = "5b1e5645f2ee8025c2f1d75e1138f2dd034d74e6ba54620f3c569ba2a2a1ea06" dependencies = [ "dissimilar", "glob", @@ -10076,17 +10049,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64 0.22.1", "log", "native-tls", "once_cell", - "rustls 0.22.4", + "rustls 0.23.11", "rustls-pki-types", - "rustls-webpki 0.102.4", "url", "webpki-roots 0.26.3", ] @@ -10123,9 +10095,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", "serde", @@ -10271,7 +10243,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "wasm-bindgen-shared", ] @@ -10337,7 +10309,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10380,7 +10352,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -10612,7 +10584,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -10700,7 +10672,7 @@ dependencies = [ "windows-core 0.54.0", "windows-implement 0.53.0", "windows-interface 0.53.0", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10710,7 +10682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ "windows-core 0.57.0", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10719,7 +10691,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10729,7 +10701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ "windows-result", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10741,7 +10713,7 @@ dependencies = [ "windows-implement 0.57.0", "windows-interface 0.57.0", "windows-result", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10752,7 +10724,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -10763,7 +10735,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -10774,7 +10746,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -10785,7 +10757,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -10794,7 +10766,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10821,7 +10793,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10856,18 +10828,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -10876,7 +10848,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10893,9 +10865,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -10911,9 +10883,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -10929,15 +10901,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -10953,9 +10925,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -10971,9 +10943,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -10989,9 +10961,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -11007,9 +10979,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -11144,9 +11116,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" +checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" [[package]] name = "yansi" @@ -11201,7 +11173,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "zvariant_utils", ] @@ -11233,7 +11205,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] [[package]] @@ -11273,11 +11245,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 7.1.0", + "zstd-safe 7.2.0", ] [[package]] @@ -11292,18 +11264,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.11+zstd.1.5.6" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", @@ -11356,7 +11328,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", "zvariant_utils", ] @@ -11368,5 +11340,5 @@ checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.70", ] diff --git a/Cargo.toml b/Cargo.toml index 3ff3bf75c..2573e7d98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,12 +30,17 @@ members = [ "packages/fullstack", "packages/server-macro", "packages/static-generation", + "packages/lazy-js-bundle", + + # Fullstack examples "packages/fullstack/examples/axum-hello-world", "packages/fullstack/examples/axum-router", "packages/fullstack/examples/axum-streaming", "packages/fullstack/examples/axum-desktop", "packages/fullstack/examples/axum-auth", "packages/fullstack/examples/hackernews", + + # Static generation examples "packages/static-generation/examples/simple", "packages/static-generation/examples/router", "packages/static-generation/examples/github-pages", @@ -85,6 +90,7 @@ dioxus-hot-reload = { path = "packages/hot-reload", version = "0.5.0" } dioxus-fullstack = { path = "packages/fullstack", version = "0.5.0" } dioxus-static-site-generation = { path = "packages/static-generation", version = "0.5.0" } dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0", default-features = false } +lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.5.0" } tracing = "0.1.37" tracing-futures = "0.2.5" toml = "0.8" @@ -173,6 +179,7 @@ reqwest = { version = "0.11.9", features = ["json"], optional = true } http-range = { version = "0.1.5", optional = true } ciborium = { version = "0.2.1", optional = true } base64 = { version = "0.21.0", optional = true } +tracing-subscriber = "0.3.17" [dev-dependencies] dioxus = { workspace = true, features = ["router"] } diff --git a/examples/meta.rs b/examples/meta.rs new file mode 100644 index 000000000..fae916258 --- /dev/null +++ b/examples/meta.rs @@ -0,0 +1,36 @@ +//! This example shows how to add metadata to the page with the Meta component + +use dioxus::prelude::*; + +fn main() { + tracing_subscriber::fmt::init(); + launch(app); +} + +fn app() -> Element { + rsx! { + // You can use the Meta component to render a meta tag into the head of the page + // Meta tags are useful to provide information about the page to search engines and social media sites + // This example sets up meta tags for the open graph protocol for social media previews + Meta { + property: "og:title", + content: "My Site", + } + Meta { + property: "og:type", + content: "website", + } + Meta { + property: "og:url", + content: "https://www.example.com", + } + Meta { + property: "og:image", + content: "https://example.com/image.jpg", + } + Meta { + name: "description", + content: "My Site is a site", + } + } +} diff --git a/examples/title.rs b/examples/title.rs new file mode 100644 index 000000000..ebe258963 --- /dev/null +++ b/examples/title.rs @@ -0,0 +1,22 @@ +//! This example shows how to set the title of the page or window with the Title component + +use dioxus::prelude::*; + +fn main() { + tracing_subscriber::fmt::init(); + launch(app); +} + +fn app() -> Element { + let mut count = use_signal(|| 0); + + rsx! { + div { + // You can set the title of the page with the Title component + // In web applications, this sets the title in the head. On desktop, it sets the window title + Title { "My Application (Count {count})" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + } + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index c6e100eb7..8b406f2df 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -918,7 +918,6 @@ impl VirtualDom { "Calling {} listeners", listeners.len() ); - tracing::info!("Listeners: {:?}", listeners); for listener in listeners.into_iter().rev() { if let AttributeValue::Listener(listener) = listener { self.runtime.rendering.set(false); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index bd9f3a809..b2a061af2 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -15,9 +15,9 @@ dioxus-html = { workspace = true, features = [ "serialize", "native-bind", "mounted", - "eval", + "document", ] } -dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "eval"] } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-cli-config = { workspace = true, features = ["read-config"] } generational-box = { workspace = true } diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 2e51b81fd..1db7a0be0 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -211,8 +211,8 @@ impl Config { /// Sets the menu the window will use. This will override the default menu bar. /// /// > Note: Menu will be hidden if - /// [`with_decorations`](tao::window::WindowBuilder::with_decorations) - /// is set to false and passed into [`with_window`](Config::with_window) + /// > [`with_decorations`](tao::window::WindowBuilder::with_decorations) + /// > is set to false and passed into [`with_window`](Config::with_window) #[allow(unused)] pub fn with_menu(mut self, menu: impl Into>) -> Self { #[cfg(not(any(target_os = "ios", target_os = "android")))] diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/document.rs similarity index 90% rename from packages/desktop/src/eval.rs rename to packages/desktop/src/document.rs index 1f69d1b47..541b87314 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/document.rs @@ -1,23 +1,27 @@ -use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; +use dioxus_html::document::{Document, EvalError, Evaluator}; use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use crate::{query::Query, DesktopContext}; /// Represents the desktop-target's provider of evaluators. -pub struct DesktopEvalProvider { +pub struct DesktopDocument { pub(crate) desktop_ctx: DesktopContext, } -impl DesktopEvalProvider { +impl DesktopDocument { pub fn new(desktop_ctx: DesktopContext) -> Self { Self { desktop_ctx } } } -impl EvalProvider for DesktopEvalProvider { +impl Document for DesktopDocument { fn new_evaluator(&self, js: String) -> GenerationalBox> { DesktopEvaluator::create(self.desktop_ctx.clone(), js) } + + fn set_title(&self, title: String) { + self.desktop_ctx.window.set_title(&title); + } } /// Represents a desktop-target's JavaScript evaluator. diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 90e0040a6..5d489f551 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -7,9 +7,9 @@ mod app; mod assets; mod config; mod desktop_context; +mod document; mod edits; mod element; -mod eval; mod event_handlers; mod events; mod file_upload; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 862c93552..f46a4a493 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,5 @@ use crate::{assets::*, edits::EditQueue}; -use dioxus_interpreter_js::eval::NATIVE_EVAL_JS; +use dioxus_html::document::NATIVE_EVAL_JS; use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; use dioxus_interpreter_js::NATIVE_JS; use std::path::{Component, Path, PathBuf}; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index c007e194f..5f8c8d7aa 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,11 +1,11 @@ use crate::menubar::DioxusMenu; use crate::{ - app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider, + app::SharedContext, assets::AssetHandlerRegistry, document::DesktopDocument, edits::EditQueue, file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config, DesktopContext, DesktopService, }; use dioxus_core::{ScopeId, VirtualDom}; -use dioxus_html::prelude::EvalProvider; +use dioxus_html::document::Document; use futures_util::{pin_mut, FutureExt}; use std::{rc::Rc, task::Waker}; use wry::{RequestAsyncResponder, WebContext, WebViewBuilder}; @@ -214,8 +214,7 @@ impl WebviewInstance { file_hover, )); - let provider: Rc = - Rc::new(DesktopEvalProvider::new(desktop_context.clone())); + let provider: Rc = Rc::new(DesktopDocument::new(desktop_context.clone())); dom.in_runtime(|| { ScopeId::ROOT.provide_context(desktop_context.clone()); diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 187404424..c20e3d070 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -33,7 +33,7 @@ axum = { workspace = true, optional = true } dioxus-hot-reload = { workspace = true, optional = true } [features] -default = ["macro", "html", "hot-reload", "signals", "hooks", "launch", "mounted", "file_engine", "eval"] +default = ["macro", "html", "hot-reload", "signals", "hooks", "launch", "mounted", "file_engine", "document"] minimal = ["macro", "html", "signals", "hooks", "launch"] signals = ["dep:dioxus-signals"] macro = ["dep:dioxus-core-macro"] @@ -42,7 +42,7 @@ hooks = ["dep:dioxus-hooks"] hot-reload = ["dep:dioxus-hot-reload", "dioxus-web?/hot_reload", "dioxus-fullstack?/hot-reload"] mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"] file_engine = ["dioxus-web?/file_engine"] -eval = ["dioxus-web?/eval", "dioxus-html?/eval"] +document = ["dioxus-web?/document", "dioxus-html?/document"] launch = ["dep:dioxus-config-macro"] router = ["dep:dioxus-router"] diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 58b90aef3..69e02f5fb 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -20,6 +20,7 @@ axum = { workspace = true, features = ["ws", "macros"], optional = true } tower-http = { workspace = true, optional = true, features = ["fs"] } dioxus-lib = { workspace = true } +generational-box = { workspace = true } # Dioxus + SSR dioxus-ssr = { workspace = true, optional = true } @@ -71,19 +72,18 @@ tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], option dioxus = { workspace = true, features = ["fullstack"] } [features] -default = ["hot-reload", "panic_hook"] +default = ["hot-reload", "panic_hook", "document", "file_engine", "mounted"] panic_hook = ["dioxus-web?/panic_hook"] hot-reload = ["dioxus-web?/hot_reload", "dioxus-hot-reload/serve"] mounted = ["dioxus-web?/mounted"] file_engine = ["dioxus-web?/file_engine"] -eval = ["dioxus-web?/eval"] +document = ["dioxus-web?/document"] web = ["dep:dioxus-web", "dep:web-sys"] desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"] mobile = ["dep:dioxus-mobile"] default-tls = ["server_fn/default-tls"] rustls = ["server_fn/rustls"] axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"] -static-site-generation = [] server = [ "server_fn/ssr", "dioxus_server_macro/server", diff --git a/packages/fullstack/src/document/mod.rs b/packages/fullstack/src/document/mod.rs new file mode 100644 index 000000000..37efdc544 --- /dev/null +++ b/packages/fullstack/src/document/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "server")] +pub(crate) mod server; +#[cfg(feature = "web")] +pub(crate) mod web; diff --git a/packages/fullstack/src/document/server.rs b/packages/fullstack/src/document/server.rs new file mode 100644 index 000000000..efcea5884 --- /dev/null +++ b/packages/fullstack/src/document/server.rs @@ -0,0 +1,140 @@ +//! On the server, we collect any elements that should be rendered into the head in the first frame of SSR. +//! After the first frame, we have already sent down the head, so we can't modify it in place. The web client +//! will hydrate the head with the correct contents once it loads. + +use std::cell::RefCell; + +use dioxus_lib::{html::document::*, prelude::*}; +use dioxus_ssr::Renderer; +use generational_box::GenerationalBox; + +#[derive(Default)] +struct ServerDocumentInner { + streaming: bool, + title: Option, + meta: Vec, + link: Vec, + script: Vec, +} + +/// A Document provider that collects all contents injected into the head for SSR rendering. +#[derive(Default)] +pub(crate) struct ServerDocument(RefCell); + +impl ServerDocument { + pub(crate) fn render( + &self, + to: &mut impl std::fmt::Write, + renderer: &mut Renderer, + ) -> std::fmt::Result { + fn lazy_app(props: Element) -> Element { + props + } + let myself = self.0.borrow(); + let element = rsx! { + if let Some(title) = myself.title.as_ref() { + title { title: "{title}" } + } + {myself.meta.iter().map(|m| rsx! { {m} })} + {myself.link.iter().map(|l| rsx! { {l} })} + {myself.script.iter().map(|s| rsx! { {s} })} + }; + + let mut dom = VirtualDom::new_with_props(lazy_app, element); + dom.rebuild_in_place(); + + // We don't hydrate the head, so we can set the pre_render flag to false to save a few bytes + let was_pre_rendering = renderer.pre_render; + renderer.pre_render = false; + renderer.render_to(to, &dom)?; + renderer.pre_render = was_pre_rendering; + + Ok(()) + } + + pub(crate) fn start_streaming(&self) { + self.0.borrow_mut().streaming = true; + } + + pub(crate) fn warn_if_streaming(&self) { + if self.0.borrow().streaming { + tracing::warn!("Attempted to insert content into the head after the initial streaming frame. Inserting content into the head only works during the initial render of SSR outside before resolving any suspense boundaries."); + } + } + + /// Write the head element into the serialized context for hydration + /// We write true if the head element was written to the DOM during server side rendering + pub(crate) fn serialize_for_hydration(&self) { + let serialize = crate::html_storage::serialize_context(); + serialize.push(&!self.0.borrow().streaming); + } +} + +impl Document for ServerDocument { + fn new_evaluator(&self, js: String) -> GenerationalBox> { + NoOpDocument.new_evaluator(js) + } + + fn set_title(&self, title: String) { + self.warn_if_streaming(); + self.serialize_for_hydration(); + self.0.borrow_mut().title = Some(title); + } + + fn create_meta(&self, props: MetaProps) { + self.warn_if_streaming(); + self.serialize_for_hydration(); + self.0.borrow_mut().meta.push(rsx! { + meta { + name: props.name, + charset: props.charset, + http_equiv: props.http_equiv, + content: props.content, + property: props.property, + } + }); + } + + fn create_script(&self, props: ScriptProps) { + self.warn_if_streaming(); + self.serialize_for_hydration(); + let children = props.script_contents(); + self.0.borrow_mut().script.push(rsx! { + script { + src: props.src, + defer: props.defer, + crossorigin: props.crossorigin, + fetchpriority: props.fetchpriority, + integrity: props.integrity, + nomodule: props.nomodule, + nonce: props.nonce, + referrerpolicy: props.referrerpolicy, + r#type: props.r#type, + {children} + } + }); + } + + fn create_link(&self, props: head::LinkProps) { + self.warn_if_streaming(); + self.serialize_for_hydration(); + self.0.borrow_mut().link.push(rsx! { + link { + rel: props.rel, + media: props.media, + title: props.title, + disabled: props.disabled, + r#as: props.r#as, + sizes: props.sizes, + href: props.href, + crossorigin: props.crossorigin, + referrerpolicy: props.referrerpolicy, + fetchpriority: props.fetchpriority, + hreflang: props.hreflang, + integrity: props.integrity, + r#type: props.r#type, + blocking: props.blocking, + } + }) + } +} diff --git a/packages/fullstack/src/document/web.rs b/packages/fullstack/src/document/web.rs new file mode 100644 index 000000000..768816138 --- /dev/null +++ b/packages/fullstack/src/document/web.rs @@ -0,0 +1,58 @@ +#![allow(unused)] +//! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server. + +use dioxus_lib::events::Document; +use dioxus_web::WebDocument; + +fn head_element_written_on_server() -> bool { + dioxus_web::take_server_data() + .ok() + .flatten() + .unwrap_or_default() +} + +pub(crate) struct FullstackWebDocument; + +impl Document for FullstackWebDocument { + fn new_evaluator( + &self, + js: String, + ) -> generational_box::GenerationalBox> { + WebDocument.new_evaluator(js) + } + + fn set_title(&self, title: String) { + if head_element_written_on_server() { + return; + } + WebDocument.set_title(title); + } + + fn create_meta(&self, props: dioxus_lib::prelude::MetaProps) { + if head_element_written_on_server() { + return; + } + WebDocument.create_meta(props); + } + + fn create_script(&self, props: dioxus_lib::prelude::ScriptProps) { + if head_element_written_on_server() { + return; + } + WebDocument.create_script(props); + } + + fn create_style(&self, props: dioxus_lib::prelude::StyleProps) { + if head_element_written_on_server() { + return; + } + WebDocument.create_style(props); + } + + fn create_link(&self, props: dioxus_lib::prelude::head::LinkProps) { + if head_element_written_on_server() { + return; + } + WebDocument.create_link(props); + } +} diff --git a/packages/fullstack/src/hooks/server_cached.rs b/packages/fullstack/src/hooks/server_cached.rs index 2f518cd04..499db0362 100644 --- a/packages/fullstack/src/hooks/server_cached.rs +++ b/packages/fullstack/src/hooks/server_cached.rs @@ -22,27 +22,29 @@ use serde::{de::DeserializeOwned, Serialize}; /// ``` pub fn use_server_cached( server_fn: impl Fn() -> O, +) -> O { + use_hook(|| server_cached(server_fn)) +} + +pub(crate) fn server_cached( + value: impl FnOnce() -> O, ) -> O { #[cfg(feature = "server")] { - let serialize = crate::html_storage::use_serialize_context(); - use_hook(|| { - let data = server_fn(); - serialize.push(&data); - data - }) + let serialize = crate::html_storage::serialize_context(); + let data = value(); + serialize.push(&data); + data } #[cfg(all(not(feature = "server"), feature = "web"))] { - use_hook(|| { - dioxus_web::take_server_data() - .ok() - .flatten() - .unwrap_or_else(server_fn) - }) + dioxus_web::take_server_data() + .ok() + .flatten() + .unwrap_or_else(value) } #[cfg(not(any(feature = "server", feature = "web")))] { - use_hook(server_fn) + value() } } diff --git a/packages/fullstack/src/html_storage/mod.rs b/packages/fullstack/src/html_storage/mod.rs index 3e65dddf3..08da4fc28 100644 --- a/packages/fullstack/src/html_storage/mod.rs +++ b/packages/fullstack/src/html_storage/mod.rs @@ -32,7 +32,11 @@ impl SerializeContext { } pub(crate) fn use_serialize_context() -> SerializeContext { - use_hook(|| has_context().unwrap_or_else(|| provide_context(SerializeContext::default()))) + use_hook(serialize_context) +} + +pub(crate) fn serialize_context() -> SerializeContext { + has_context().unwrap_or_else(|| provide_context(SerializeContext::default())) } #[derive(serde::Serialize, serde::Deserialize, Default)] diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index b26eb2e87..5dc4262e9 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -50,12 +50,23 @@ pub fn launch( #[allow(unused)] pub fn launch( root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, + #[allow(unused_mut)] mut contexts: Vec< + Box Box + Send + Sync>, + >, platform_config: Config, ) { let contexts = Arc::new(contexts); - let factory = virtual_dom_factory(root, contexts); + let mut factory = virtual_dom_factory(root, contexts); let cfg = platform_config.web_cfg.hydrate(true); + + #[cfg(feature = "document")] + let factory = move || { + let mut vdom = factory(); + vdom.provide_root_context(std::rc::Rc::new(crate::document::web::FullstackWebDocument) + as std::rc::Rc); + vdom + }; + dioxus_web::launch::launch_virtual_dom(factory(), cfg) } diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 36ce0dd10..fec63e5b1 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -18,6 +18,8 @@ pub mod launch; pub use config::*; +#[cfg(feature = "document")] +mod document; #[cfg(feature = "server")] mod render; #[cfg(feature = "server")] diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index 494d5c5da..82d6ae7ca 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -160,8 +160,34 @@ impl SsrRendererPool { let join_handle = spawn_platform(move || async move { let mut virtual_dom = virtual_dom_factory(); + #[cfg(feature = "document")] + let document = std::rc::Rc::new(crate::document::server::ServerDocument::default()); + #[cfg(feature = "document")] + virtual_dom.provide_root_context(document.clone() as std::rc::Rc); + + // poll the future, which may call server_context() + tracing::info!("Rebuilding vdom"); + with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place()); let mut pre_body = String::new(); + + if let Err(err) = wrapper.render_head(&mut pre_body) { + _ = into.start_send(Err(err)); + return; + } + + #[cfg(feature = "document")] + { + // Collect any head content from the document provider and inject that into the head + if let Err(err) = document.render(&mut pre_body, &mut renderer) { + _ = into.start_send(Err(err.into())); + return; + } + + // Enable a warning when inserting contents into the head during streaming + document.start_streaming(); + } + if let Err(err) = wrapper.render_before_body(&mut pre_body) { _ = into.start_send(Err(err)); return; @@ -206,10 +232,6 @@ impl SsrRendererPool { }; } - // poll the future, which may call server_context() - tracing::info!("Rebuilding vdom"); - with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place()); - // Render the initial frame with loading placeholders let mut initial_frame = renderer.render(&virtual_dom); @@ -364,6 +386,18 @@ impl FullstackHTMLTemplate { } impl FullstackHTMLTemplate { + /// Render any content before the head of the page. + pub fn render_head( + &self, + to: &mut R, + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + let ServeConfig { index, .. } = &self.cfg; + + to.write_str(&index.head)?; + + Ok(()) + } + /// Render any content before the body of the page. pub fn render_before_body( &self, @@ -371,7 +405,7 @@ impl FullstackHTMLTemplate { ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { let ServeConfig { index, .. } = &self.cfg; - to.write_str(&index.pre_main)?; + to.write_str(&index.close_head)?; Ok(()) } diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index 03008b0d2..ced773842 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -107,13 +107,19 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { post_main.1.to_string(), ); + let (head, close_head) = pre_main.split_once("").unwrap_or_else(|| { + panic!("Failed to find closing tag after id=\"{root_id}\" in index.html.") + }); + let (head, close_head) = (head.to_string(), "".to_string() + close_head); + let (post_main, after_closing_body_tag) = post_main.split_once("").unwrap_or_else(|| { panic!("Failed to find closing tag after id=\"{root_id}\" in index.html.") }); IndexHtml { - pre_main, + head, + close_head, post_main: post_main.to_string(), after_closing_body_tag: "".to_string() + after_closing_body_tag, } @@ -121,7 +127,8 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { #[derive(Clone)] pub(crate) struct IndexHtml { - pub(crate) pre_main: String, + pub(crate) head: String, + pub(crate) close_head: String, pub(crate) post_main: String, pub(crate) after_closing_body_tag: String, } diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 6b73326e4..bc6cfb063 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -11,12 +11,15 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true } +dioxus-core-macro = { workspace = true } dioxus-rsx = { workspace = true, optional = true } dioxus-html-internal-macro = { workspace = true } +dioxus-hooks = { workspace = true } generational-box = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } js-sys = { version = "0.3.56", optional = true } euclid = "0.22.7" enumset = "1.1.2" @@ -51,14 +54,18 @@ features = [ "CompositionEvent", ] +[build-dependencies] +lazy-js-bundle = { workspace = true } + [dev-dependencies] serde_json = "1" dioxus = { workspace = true } dioxus-web = { workspace = true } tokio = { workspace = true, features = ["time"] } +manganis = { workspace = true } [features] -default = ["serialize", "mounted", "eval", "file-engine"] +default = ["serialize", "mounted", "document", "file-engine"] serialize = [ "dep:serde", "dep:serde_json", @@ -76,7 +83,7 @@ mounted = [ "web-sys?/ScrollBehavior", "web-sys?/HtmlElement", ] -eval = [ +document = [ "dep:serde", "dep:serde_json" ] @@ -87,7 +94,7 @@ file-engine = [ "web-sys?/FileList", "web-sys?/FileReader" ] -wasm-bind = ["dep:web-sys", "dep:wasm-bindgen"] +wasm-bind = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] native-bind = ["dep:tokio", "file-engine"] hot-reload-context = ["dep:dioxus-rsx", "dioxus-rsx/hot_reload_traits"] html-to-rsx = [] diff --git a/packages/html/assets/script.js b/packages/html/assets/script.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/html/assets/style.css b/packages/html/assets/style.css new file mode 100644 index 000000000..e69de29bb diff --git a/packages/html/build.rs b/packages/html/build.rs new file mode 100644 index 000000000..88da265a4 --- /dev/null +++ b/packages/html/build.rs @@ -0,0 +1,9 @@ +fn main() { + // If any TS files change, re-run the build script + lazy_js_bundle::LazyTypeScriptBindings::new() + .with_watching("./src/ts") + .with_binding("./src/ts/eval.ts", "./src/js/eval.js") + .with_binding("./src/ts/native_eval.ts", "./src/js/native_eval.js") + .with_binding("./src/ts/head.ts", "./src/js/head.js") + .run(); +} diff --git a/packages/html/docs/head.md b/packages/html/docs/head.md new file mode 100644 index 000000000..94dcb9937 --- /dev/null +++ b/packages/html/docs/head.md @@ -0,0 +1,59 @@ +# Modifying the Head + +Dioxus includes a series of components that render into the head of the page: + +- [Title](crate::Title) +- [Meta](crate::Meta) +- [head::Link](crate::head::Link) +- [Script](crate::Script) +- [Style](crate::Style) + +Each of these components can be used to add extra information to the head of the page. For example, you can use the `Title` component to set the title of the page, or the `Meta` component to add extra metadata to the page. + +## Limitations + +Components that render into the head of the page do have a few key limitations: + +- With the exception of the `Title` component, all components that render into the head cannot be modified after the first time they are rendered. +- Components that render into the head will not be removed even after the component is removed from the tree. + +## Example + +```rust, no_run +# use dioxus::prelude::*; +fn RedirectToDioxusHomepageWithoutJS() -> Element { + rsx! { + // You can use the meta component to render a meta tag into the head of the page + // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds + Meta { + http_equiv: "refresh", + content: "10;url=https://dioxuslabs.com", + } + } +} +``` + +## Fullstack Rendering + +Head components are compatible with fullstack rendering, but only head components that are rendered in the initial render (before suspense boundaries resolve) will be rendered into the head. + +If you have any important metadata that you want to render into the head, make sure to render it outside of any pending suspense boundaries. + +```rust, no_run +# use dioxus::prelude::*; +# #[component] +# fn LoadData(children: Element) -> Element { unimplemented!() } +fn App() -> Element { + rsx! { + // This will render in SSR + Title { "My Page" } + SuspenseBoundary { + fallback: |_| rsx! { "Loading..." }, + LoadData { + // This will only be rendered on the client after hydration so it may not be visible to search engines + Meta { name: "description", content: "My Page" } + } + } + } +} +``` diff --git a/packages/interpreter/src/eval.rs b/packages/html/src/document/bindings.rs similarity index 87% rename from packages/interpreter/src/eval.rs rename to packages/html/src/document/bindings.rs index 81f2e0f97..ad8ef1606 100644 --- a/packages/interpreter/src/eval.rs +++ b/packages/html/src/document/bindings.rs @@ -1,13 +1,14 @@ /// Code for the Dioxus channel used to communicate between the dioxus and javascript code -pub const NATIVE_EVAL_JS: &str = include_str!("./js/native_eval.js"); +#[cfg(feature = "native-bind")] +pub const NATIVE_EVAL_JS: &str = include_str!("../js/native_eval.js"); -#[cfg(feature = "webonly")] +#[cfg(feature = "wasm-bind")] #[wasm_bindgen::prelude::wasm_bindgen] pub struct JSOwner { _owner: Box, } -#[cfg(feature = "webonly")] +#[cfg(feature = "wasm-bind")] impl JSOwner { pub fn new(owner: impl std::any::Any) -> Self { Self { @@ -16,7 +17,7 @@ impl JSOwner { } } -#[cfg(feature = "webonly")] +#[cfg(feature = "wasm-bind")] #[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")] extern "C" { pub type WebDioxusChannel; diff --git a/packages/html/src/eval.rs b/packages/html/src/document/eval.rs similarity index 60% rename from packages/html/src/eval.rs rename to packages/html/src/document/eval.rs index f8179b09e..44da18426 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/document/eval.rs @@ -1,18 +1,14 @@ #![allow(clippy::await_holding_refcell_ref)] -#![doc = include_str!("../docs/eval.md")] +#![doc = include_str!("../../docs/eval.md")] use dioxus_core::prelude::*; -use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; +use generational_box::GenerationalBox; use std::future::{poll_fn, Future, IntoFuture}; use std::pin::Pin; use std::rc::Rc; use std::task::{Context, Poll}; -/// A struct that implements EvalProvider is sent through [`ScopeState`]'s provide_context function -/// so that [`eval`] can provide a platform agnostic interface for evaluating JavaScript code. -pub trait EvalProvider { - fn new_evaluator(&self, js: String) -> GenerationalBox>; -} +use super::document; /// The platform's evaluator. pub trait Evaluator { @@ -42,55 +38,22 @@ type EvalCreator = Rc UseEval>; /// has access to most, if not all of your application data.** #[must_use] pub fn eval_provider() -> EvalCreator { - let eval_provider = consume_context::>(); + let eval_provider = document(); Rc::new(move |script: &str| UseEval::new(eval_provider.new_evaluator(script.to_string()))) as Rc UseEval> } -#[doc = include_str!("../docs/eval.md")] +#[doc = include_str!("../../docs/eval.md")] #[doc(alias = "javascript")] pub fn eval(script: &str) -> UseEval { - let eval_provider = dioxus_core::prelude::try_consume_context::>() - // Create a dummy provider that always hiccups when trying to evaluate - // That way, we can still compile and run the code without a real provider - .unwrap_or_else(|| { - struct DummyProvider; - impl EvalProvider for DummyProvider { - fn new_evaluator(&self, _js: String) -> GenerationalBox> { - tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle."); - UnsyncStorage::owner().insert(Box::new(DummyEvaluator)) - } - } - - struct DummyEvaluator; - impl Evaluator for DummyEvaluator { - fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> { - Err(EvalError::Unsupported) - } - fn poll_recv( - &mut self, - _context: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Err(EvalError::Unsupported)) - } - fn poll_join( - &mut self, - _context: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Err(EvalError::Unsupported)) - } - } - - Rc::new(DummyProvider) as Rc - }); - - UseEval::new(eval_provider.new_evaluator(script.to_string())) + let document = use_hook(document); + UseEval::new(document.new_evaluator(script.to_string())) } /// A wrapper around the target platform's evaluator that lets you send and receive data from JavaScript spawned by [`eval`]. /// -#[doc = include_str!("../docs/eval.md")] +#[doc = include_str!("../../docs/eval.md")] #[derive(Clone, Copy)] pub struct UseEval { evaluator: GenerationalBox>, diff --git a/packages/html/src/document/head.rs b/packages/html/src/document/head.rs new file mode 100644 index 000000000..1d5766fca --- /dev/null +++ b/packages/html/src/document/head.rs @@ -0,0 +1,520 @@ +#![doc = include_str!("../../docs/head.md")] + +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +use dioxus_core::{prelude::*, DynamicNode}; +use dioxus_core_macro::*; + +/// Warn the user if they try to change props on a element that is injected into the head +#[allow(unused)] +fn use_update_warning(value: &T, name: &'static str) { + #[cfg(debug_assertions)] + { + let cloned_value = value.clone(); + let initial = use_hook(move || value.clone()); + + if initial != cloned_value { + tracing::warn!("Changing the props of `{name}` is not supported "); + } + } +} + +fn extract_single_text_node(children: &Element, component: &str) -> Option { + let vnode = match children { + Element::Ok(vnode) => vnode, + Element::Err(err) => { + tracing::error!("Error while rendering {component}: {err}"); + return None; + } + }; + // The title's children must be in one of two forms: + // 1. rsx! { "static text" } + // 2. rsx! { "title: {dynamic_text}" } + match vnode.template.get() { + // rsx! { "static text" } + Template { + roots: &[TemplateNode::Text { text }], + node_paths: &[], + attr_paths: &[], + .. + } => Some(text.to_string()), + // rsx! { "title: {dynamic_text}" } + Template { + roots: &[TemplateNode::Dynamic { id }], + node_paths: &[&[0]], + attr_paths: &[], + .. + } => { + let node = &vnode.dynamic_nodes[id]; + match node { + DynamicNode::Text(text) => Some(text.value.clone()), + _ => { + tracing::error!("Error while rendering {component}: The children of {component} must be a single text node. It cannot be a component, if statement, loop, or a fragment"); + None + } + } + } + _ => { + tracing::error!( + "Error while rendering title: The children of title must be a single text node" + ); + None + } + } +} + +#[derive(Clone, Props, PartialEq)] +pub struct TitleProps { + /// The contents of the title tag. The children must be a single text node. + children: Element, +} + +/// Render the title of the page. On web renderers, this will set the [title](crate::elements::title) in the head. On desktop, it will set the window title. +/// +/// Unlike most head components, the Title can be modified after the first render. Only the latest update to the title will be reflected if multiple title components are rendered. +/// +/// +/// The children of the title component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the title will not be updated. +/// +/// # Example +/// +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// fn App() -> Element { +/// rsx! { +/// // You can use the Title component to render a title tag into the head of the page or window +/// Title { "My Page" } +/// } +/// } +/// ``` +#[component] +pub fn Title(props: TitleProps) -> Element { + let children = props.children; + let Some(text) = extract_single_text_node(&children, "Title") else { + return rsx! {}; + }; + + // Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server + let document = use_hook(document); + let last_text = use_hook(|| { + // Set the title initially + document.set_title(text.clone()); + Rc::new(RefCell::new(text.clone())) + }); + + // If the text changes, update the title + let mut last_text = last_text.borrow_mut(); + if text != *last_text { + document.set_title(text.clone()); + *last_text = text; + } + + rsx! {} +} + +/// Props for the [`Meta`] component +#[derive(Clone, Props, PartialEq)] +pub struct MetaProps { + pub property: Option, + pub name: Option, + pub charset: Option, + pub http_equiv: Option, + pub content: Option, +} + +impl MetaProps { + pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> { + let mut attributes = Vec::new(); + if let Some(property) = &self.property { + attributes.push(("property", property.clone())); + } + if let Some(name) = &self.name { + attributes.push(("name", name.clone())); + } + if let Some(charset) = &self.charset { + attributes.push(("charset", charset.clone())); + } + if let Some(http_equiv) = &self.http_equiv { + attributes.push(("http-equiv", http_equiv.clone())); + } + if let Some(content) = &self.content { + attributes.push(("content", content.clone())); + } + attributes + } +} + +/// Render a [`meta`](crate::elements::meta) tag into the head of the page. +/// +/// # Example +/// +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// fn RedirectToDioxusHomepageWithoutJS() -> Element { +/// rsx! { +/// // You can use the meta component to render a meta tag into the head of the page +/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds +/// Meta { +/// http_equiv: "refresh", +/// content: "10;url=https://dioxuslabs.com", +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[component] +pub fn Meta(props: MetaProps) -> Element { + use_update_warning(&props, "Meta {}"); + + use_hook(|| { + let document = document(); + document.create_meta(props); + }); + + rsx! {} +} + +#[derive(Clone, Props, PartialEq)] +pub struct ScriptProps { + /// The contents of the script tag. If present, the children must be a single text node. + pub children: Element, + /// Scripts are deduplicated by their src attribute + pub src: Option, + pub defer: Option, + pub crossorigin: Option, + pub fetchpriority: Option, + pub integrity: Option, + pub nomodule: Option, + pub nonce: Option, + pub referrerpolicy: Option, + pub r#type: Option, +} + +impl ScriptProps { + pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> { + let mut attributes = Vec::new(); + if let Some(defer) = &self.defer { + attributes.push(("defer", defer.to_string())); + } + if let Some(crossorigin) = &self.crossorigin { + attributes.push(("crossorigin", crossorigin.clone())); + } + if let Some(fetchpriority) = &self.fetchpriority { + attributes.push(("fetchpriority", fetchpriority.clone())); + } + if let Some(integrity) = &self.integrity { + attributes.push(("integrity", integrity.clone())); + } + if let Some(nomodule) = &self.nomodule { + attributes.push(("nomodule", nomodule.to_string())); + } + if let Some(nonce) = &self.nonce { + attributes.push(("nonce", nonce.clone())); + } + if let Some(referrerpolicy) = &self.referrerpolicy { + attributes.push(("referrerpolicy", referrerpolicy.clone())); + } + if let Some(r#type) = &self.r#type { + attributes.push(("type", r#type.clone())); + } + attributes + } + + pub fn script_contents(&self) -> Option { + extract_single_text_node(&self.children, "Script") + } +} + +/// Render a [`script`](crate::elements::script) tag into the head of the page. +/// +/// +/// If present, the children of the script component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the script will not be added. +/// +/// +/// Any scripts you add will be deduplicated by their `src` attribute (if present). +/// +/// # Example +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// fn LoadScript() -> Element { +/// rsx! { +/// // You can use the Script component to render a script tag into the head of the page +/// Script { +/// src: manganis::mg!(file("./assets/script.js")), +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[component] +pub fn Script(props: ScriptProps) -> Element { + use_update_warning(&props, "Script {}"); + + use_hook(|| { + if let Some(src) = &props.src { + if !should_insert_script(src) { + return; + } + } + + let document = document(); + document.create_script(props); + }); + + rsx! {} +} + +#[derive(Clone, Props, PartialEq)] +pub struct StyleProps { + /// Styles are deduplicated by their href attribute + pub href: Option, + pub media: Option, + pub nonce: Option, + pub title: Option, + /// The contents of the style tag. If present, the children must be a single text node. + pub children: Element, +} + +impl StyleProps { + pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> { + let mut attributes = Vec::new(); + if let Some(href) = &self.href { + attributes.push(("href", href.clone())); + } + if let Some(media) = &self.media { + attributes.push(("media", media.clone())); + } + if let Some(nonce) = &self.nonce { + attributes.push(("nonce", nonce.clone())); + } + if let Some(title) = &self.title { + attributes.push(("title", title.clone())); + } + attributes + } + + pub fn style_contents(&self) -> Option { + extract_single_text_node(&self.children, "Title") + } +} + +/// Render a [`style`](crate::elements::style) tag into the head of the page. +/// +/// If present, the children of the style component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the style will not be added. +/// +/// # Example +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// fn RedBackground() -> Element { +/// rsx! { +/// // You can use the style component to render a style tag into the head of the page +/// // This style tag will set the background color of the page to red +/// Style { +/// r#" +/// body {{ +/// background-color: red; +/// }} +/// "#, +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[component] +pub fn Style(props: StyleProps) -> Element { + use_update_warning(&props, "Style {}"); + + use_hook(|| { + if let Some(href) = &props.href { + if !should_insert_style(href) { + return; + } + } + let document = document(); + document.create_style(props); + }); + + rsx! {} +} + +use super::*; + +#[derive(Clone, Props, PartialEq)] +pub struct LinkProps { + pub rel: Option, + pub media: Option, + pub title: Option, + pub disabled: Option, + pub r#as: Option, + pub sizes: Option, + /// Links are deduplicated by their href attribute + pub href: Option, + pub crossorigin: Option, + pub referrerpolicy: Option, + pub fetchpriority: Option, + pub hreflang: Option, + pub integrity: Option, + pub r#type: Option, + pub blocking: Option, +} + +impl LinkProps { + pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> { + let mut attributes = Vec::new(); + if let Some(rel) = &self.rel { + attributes.push(("rel", rel.clone())); + } + if let Some(media) = &self.media { + attributes.push(("media", media.clone())); + } + if let Some(title) = &self.title { + attributes.push(("title", title.clone())); + } + if let Some(disabled) = &self.disabled { + attributes.push(("disabled", disabled.to_string())); + } + if let Some(r#as) = &self.r#as { + attributes.push(("as", r#as.clone())); + } + if let Some(sizes) = &self.sizes { + attributes.push(("sizes", sizes.clone())); + } + if let Some(href) = &self.href { + attributes.push(("href", href.clone())); + } + if let Some(crossorigin) = &self.crossorigin { + attributes.push(("crossOrigin", crossorigin.clone())); + } + if let Some(referrerpolicy) = &self.referrerpolicy { + attributes.push(("referrerPolicy", referrerpolicy.clone())); + } + if let Some(fetchpriority) = &self.fetchpriority { + attributes.push(("fetchPriority", fetchpriority.clone())); + } + if let Some(hreflang) = &self.hreflang { + attributes.push(("hrefLang", hreflang.clone())); + } + if let Some(integrity) = &self.integrity { + attributes.push(("integrity", integrity.clone())); + } + if let Some(r#type) = &self.r#type { + attributes.push(("type", r#type.clone())); + } + if let Some(blocking) = &self.blocking { + attributes.push(("blocking", blocking.clone())); + } + attributes + } +} + +/// Render a [`link`](crate::elements::link) tag into the head of the page. +/// +/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different. +/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page. +/// +/// # Example +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// fn RedBackground() -> Element { +/// rsx! { +/// // You can use the meta component to render a meta tag into the head of the page +/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds +/// head::Link { +/// href: manganis::mg!(file("./assets/style.css")), +/// rel: "stylesheet", +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[doc(alias = "")] +#[component] +pub fn Link(props: LinkProps) -> Element { + use_update_warning(&props, "Link {}"); + + use_hook(|| { + if let Some(href) = &props.href { + if !should_insert_link(href) { + return; + } + } + let document = document(); + document.create_link(props); + }); + + rsx! {} +} + +fn get_or_insert_root_context() -> T { + match ScopeId::ROOT.has_context::() { + Some(context) => context, + None => { + let context = T::default(); + ScopeId::ROOT.provide_context(context.clone()); + context + } + } +} + +#[derive(Default, Clone)] +struct LinkContext(DeduplicationContext); + +fn should_insert_link(href: &str) -> bool { + get_or_insert_root_context::() + .0 + .should_insert(href) +} + +#[derive(Default, Clone)] +struct ScriptContext(DeduplicationContext); + +fn should_insert_script(src: &str) -> bool { + get_or_insert_root_context::() + .0 + .should_insert(src) +} + +#[derive(Default, Clone)] +struct StyleContext(DeduplicationContext); + +fn should_insert_style(href: &str) -> bool { + get_or_insert_root_context::() + .0 + .should_insert(href) +} + +#[derive(Default, Clone)] +struct DeduplicationContext(Rc>>); + +impl DeduplicationContext { + fn should_insert(&self, href: &str) -> bool { + let mut set = self.0.borrow_mut(); + let present = set.contains(href); + if !present { + set.insert(href.to_string()); + true + } else { + false + } + } +} diff --git a/packages/html/src/document/mod.rs b/packages/html/src/document/mod.rs new file mode 100644 index 000000000..ab3a662dd --- /dev/null +++ b/packages/html/src/document/mod.rs @@ -0,0 +1,114 @@ +// API inspired by Reacts implementation of head only elements. We use components here instead of elements to simplify internals. + +use std::{ + rc::Rc, + task::{Context, Poll}, +}; + +use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; + +mod bindings; +#[allow(unused)] +pub use bindings::*; +mod eval; +pub use eval::*; + +pub mod head; +pub use head::{Meta, MetaProps, Script, ScriptProps, Style, StyleProps, Title, TitleProps}; + +fn format_attributes(attributes: &[(&str, String)]) -> String { + let mut formatted = String::from("["); + for (key, value) in attributes { + formatted.push_str(&format!("[{key:?}, {value:?}],")); + } + if formatted.ends_with(',') { + formatted.pop(); + } + formatted.push(']'); + formatted +} + +fn create_element_in_head( + tag: &str, + attributes: &[(&str, String)], + children: Option, +) -> String { + let helpers = include_str!("../js/head.js"); + let attributes = format_attributes(attributes); + let children = children + .map(|c| format!("\"{c}\"")) + .unwrap_or("null".to_string()); + format!(r#"{helpers};window.createElementInHead("{tag}", {attributes}, {children});"#) +} + +/// A provider for document-related functionality. By default most methods are driven through [`eval`]. +pub trait Document { + /// Create a new evaluator for the document that evaluates JavaScript and facilitates communication between JavaScript and Rust. + fn new_evaluator(&self, js: String) -> GenerationalBox>; + + /// Set the title of the document + fn set_title(&self, title: String) { + self.new_evaluator(format!("document.title = {title:?};")); + } + + fn create_meta(&self, props: MetaProps) { + let attributes = props.attributes(); + let js = create_element_in_head("meta", &attributes, None); + self.new_evaluator(js); + } + + fn create_script(&self, props: ScriptProps) { + let attributes = props.attributes(); + let js = create_element_in_head("script", &attributes, props.script_contents()); + self.new_evaluator(js); + } + + fn create_style(&self, props: StyleProps) { + let attributes = props.attributes(); + let js = create_element_in_head("style", &attributes, props.style_contents()); + self.new_evaluator(js); + } + + fn create_link(&self, props: head::LinkProps) { + let attributes = props.attributes(); + let js = create_element_in_head("link", &attributes, None); + self.new_evaluator(js); + } +} + +/// The default No-Op document +pub struct NoOpDocument; + +impl Document for NoOpDocument { + fn new_evaluator(&self, _js: String) -> GenerationalBox> { + tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle."); + UnsyncStorage::owner().insert(Box::new(NoOpEvaluator)) + } +} + +struct NoOpEvaluator; +impl Evaluator for NoOpEvaluator { + fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> { + Err(EvalError::Unsupported) + } + fn poll_recv( + &mut self, + _context: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Err(EvalError::Unsupported)) + } + fn poll_join( + &mut self, + _context: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Err(EvalError::Unsupported)) + } +} + +/// Get the document provider for the current platform or a no-op provider if the platform doesn't document functionality. +pub fn document() -> Rc { + dioxus_core::prelude::try_consume_context::>() + // Create a NoOp provider that always logs an error when trying to evaluate + // That way, we can still compile and run the code without a real provider + .unwrap_or_else(|| Rc::new(NoOpDocument) as Rc) +} diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 4be34e568..4f5ef5c71 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -655,6 +655,11 @@ builder_constructors! { title: String DEFAULT, // FIXME r#type: Mime "type", integrity: String DEFAULT, + disabled: Bool DEFAULT, + referrerpolicy: ReferrerPolicy DEFAULT, + fetchpriority: FetchPriority DEFAULT, + blocking: Blocking DEFAULT, + r#as: As "as", }; /// Build a @@ -665,6 +670,7 @@ builder_constructors! { content: String DEFAULT, http_equiv: String "http-equiv", name: Metadata DEFAULT, + property: Metadata DEFAULT, }; /// Build a @@ -1261,6 +1267,8 @@ builder_constructors! { nonce: Nonce DEFAULT, src: Uri DEFAULT, text: String DEFAULT, + fetchpriority: String DEFAULT, + referrerpolicy: String DEFAULT, r#async: Bool "async", r#type: String "type", // TODO could be an enum diff --git a/packages/interpreter/src/js/eval.js b/packages/html/src/js/eval.js similarity index 100% rename from packages/interpreter/src/js/eval.js rename to packages/html/src/js/eval.js diff --git a/packages/html/src/js/hash.txt b/packages/html/src/js/hash.txt new file mode 100644 index 000000000..051bb006a --- /dev/null +++ b/packages/html/src/js/hash.txt @@ -0,0 +1 @@ +[10372071913661173523, 8375185156499858125, 4813754958077120784] \ No newline at end of file diff --git a/packages/html/src/js/head.js b/packages/html/src/js/head.js new file mode 100644 index 000000000..f4d2c362a --- /dev/null +++ b/packages/html/src/js/head.js @@ -0,0 +1 @@ +var createElementInHead=function(tag,attributes,children){const element=document.createElement(tag);for(let[key,value]of attributes)element.setAttribute(key,value);if(children)element.appendChild(document.createTextNode(children));document.head.appendChild(element)};window.createElementInHead=createElementInHead; diff --git a/packages/interpreter/src/js/native_eval.js b/packages/html/src/js/native_eval.js similarity index 100% rename from packages/interpreter/src/js/native_eval.js rename to packages/html/src/js/native_eval.js diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 4fd2a8d64..3f1646912 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -47,8 +47,8 @@ pub use elements::*; pub use events::*; pub use render_template::*; -#[cfg(feature = "eval")] -pub mod eval; +#[cfg(feature = "document")] +pub mod document; pub mod extensions { pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension}; @@ -57,9 +57,12 @@ pub mod extensions { pub mod prelude { pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension}; + #[cfg(feature = "document")] + pub use crate::document::{ + self, document, eval, head, Document, Meta, MetaProps, Script, ScriptProps, Style, + StyleProps, Title, TitleProps, UseEval, + }; pub use crate::elements::extensions::*; - #[cfg(feature = "eval")] - pub use crate::eval::*; pub use crate::events::*; pub use crate::point_interaction::*; pub use keyboard_types::{self, Code, Key, Location, Modifiers}; diff --git a/packages/html/src/ts/.gitignore b/packages/html/src/ts/.gitignore new file mode 100644 index 000000000..68b79b0d3 --- /dev/null +++ b/packages/html/src/ts/.gitignore @@ -0,0 +1,3 @@ +# please dont accidentally run tsc and commit your js in this dir. +*.js + diff --git a/packages/interpreter/src/ts/eval.ts b/packages/html/src/ts/eval.ts similarity index 100% rename from packages/interpreter/src/ts/eval.ts rename to packages/html/src/ts/eval.ts diff --git a/packages/html/src/ts/head.ts b/packages/html/src/ts/head.ts new file mode 100644 index 000000000..d01c0aaff --- /dev/null +++ b/packages/html/src/ts/head.ts @@ -0,0 +1,19 @@ +// Helper functions for working with the document head + +function createElementInHead( + tag: string, + attributes: [string, string][], + children: string | null +): void { + const element = document.createElement(tag); + for (const [key, value] of attributes) { + element.setAttribute(key, value); + } + if (children) { + element.appendChild(document.createTextNode(children)); + } + document.head.appendChild(element); +} + +// @ts-ignore +window.createElementInHead = createElementInHead; diff --git a/packages/interpreter/src/ts/native_eval.ts b/packages/html/src/ts/native_eval.ts similarity index 100% rename from packages/interpreter/src/ts/native_eval.ts rename to packages/html/src/ts/native_eval.ts diff --git a/packages/html/tsconfig.json b/packages/html/tsconfig.json new file mode 100644 index 000000000..11cda68e3 --- /dev/null +++ b/packages/html/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "lib": [ + "ES2015", + "DOM", + "dom", + "dom.iterable", + "ESNext" + ], + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + }, + "exclude": [ + "**/*.spec.ts" + ] +} diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index f6e2eb3aa..4112c1f07 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -26,7 +26,7 @@ dioxus-core = { workspace = true, optional = true } dioxus-html = { workspace = true, optional = true } [build-dependencies] -md5 = "0.7.0" +lazy-js-bundle = { workspace = true } [features] default = [] @@ -42,4 +42,3 @@ webonly = [ ] binary-protocol = ["sledgehammer", "dep:dioxus-core", "dep:dioxus-html"] minimal_bindings = [] -eval = [] diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index cf06194b1..6dcd7617c 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -1,84 +1,14 @@ -use std::collections::hash_map::DefaultHasher; -use std::path::PathBuf; -use std::{hash::Hasher, process::Command}; - fn main() { - // If any TS changes, re-run the build script - let watching = std::fs::read_dir("./src/ts").unwrap(); - let ts_paths: Vec<_> = watching - .into_iter() - .flatten() - .map(|entry| entry.path()) - .filter(|path| path.extension().map(|ext| ext == "ts").unwrap_or(false)) - .collect(); - for path in &ts_paths { - println!("cargo:rerun-if-changed={}", path.display()); - } - - // Compute the hash of the ts files - let hash = hash_ts_files(ts_paths); - - // If the hash matches the one on disk, we're good and don't need to update bindings - let fs_hash_string = std::fs::read_to_string("src/js/hash.txt"); - let expected = fs_hash_string - .as_ref() - .map(|s| s.trim()) - .unwrap_or_default(); - if expected == hash.to_string() { - return; - } - - // Otherwise, generate the bindings and write the new hash to disk - // Generate the bindings for both native and web - gen_bindings("common", "common"); - gen_bindings("native", "native"); - gen_bindings("core", "core"); - gen_bindings("eval", "eval"); - gen_bindings("native_eval", "native_eval"); - gen_bindings("hydrate", "hydrate"); - gen_bindings("initialize_streaming", "initialize_streaming"); - - std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); -} - -/// Hashes the contents of a directory -fn hash_ts_files(mut files: Vec) -> u64 { - // Different systems will read the files in different orders, so we sort them to make sure the hash is consistent - files.sort(); - let mut hash = DefaultHasher::new(); - for file in files { - let contents = std::fs::read_to_string(file).unwrap(); - // windows + git does a weird thing with line endings, so we need to normalize them - for line in contents.lines() { - hash.write(line.as_bytes()); - } - } - hash.finish() -} - -// okay...... so tsc might fail if the user doesn't have it installed -// we don't really want to fail if that's the case -// but if you started *editing* the .ts files, you're gonna have a bad time -// so..... -// we need to hash each of the .ts files and add that hash to the JS files -// if the hashes don't match, we need to fail the build -// that way we also don't need -fn gen_bindings(input_name: &str, output_name: &str) { - // If the file is generated, and the hash is different, we need to generate it - let status = Command::new("bun") - .arg("build") - .arg(format!("src/ts/{input_name}.ts")) - .arg("--outfile") - .arg(format!("src/js/{output_name}.js")) - .arg("--minify-whitespace") - .arg("--minify-syntax") - .status() - .unwrap(); - - if !status.success() { - panic!( - "Failed to generate bindings for {}. Make sure you have tsc installed", - input_name - ); - } + // If any TS files change, re-run the build script + lazy_js_bundle::LazyTypeScriptBindings::new() + .with_watching("./src/ts") + .with_binding("./src/ts/common.ts", "./src/js/common.js") + .with_binding("./src/ts/native.ts", "./src/js/native.js") + .with_binding("./src/ts/core.ts", "./src/js/core.js") + .with_binding("./src/ts/hydrate.ts", "./src/js/hydrate.js") + .with_binding( + "./src/ts/initialize_streaming.ts", + "./src/js/initialize_streaming.js", + ) + .run(); } diff --git a/packages/interpreter/src/js/README.md b/packages/interpreter/src/js/README.md deleted file mode 100644 index 851ef6751..000000000 --- a/packages/interpreter/src/js/README.md +++ /dev/null @@ -1 +0,0 @@ -this files are generated - do not edit them! diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index cbfbc11f6..863cfa274 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -6628058622237003340 \ No newline at end of file +[6449103750905854967, 12029349297046688094, 14626980229647476238, 8716623267269178440, 5336385715226370016, 14456089431355876478, 3589298972260118311, 2745859031945642653, 5638004933879392817] \ No newline at end of file diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index ba726b5fe..ce862e81b 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -24,9 +24,6 @@ pub mod unified_bindings; #[cfg(feature = "sledgehammer")] pub use unified_bindings::*; -#[cfg(feature = "eval")] -pub mod eval; - // Common bindings for minimal usage. #[cfg(all(feature = "minimal_bindings", feature = "webonly"))] pub mod minimal_bindings { diff --git a/packages/lazy-js-bundle/Cargo.toml b/packages/lazy-js-bundle/Cargo.toml new file mode 100644 index 000000000..77d2dab56 --- /dev/null +++ b/packages/lazy-js-bundle/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "lazy-js-bundle" +version = { workspace = true } +edition = "2021" +authors = ["Jonathan Kelley"] +description = "A codegen library to bundle TypeScript into JavaScript without requiring a bundler to be installed" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus" +keywords = ["ts", "codegen", "typescript", "javascript", "wasm"] diff --git a/packages/lazy-js-bundle/src/lib.rs b/packages/lazy-js-bundle/src/lib.rs new file mode 100644 index 000000000..36dae8da4 --- /dev/null +++ b/packages/lazy-js-bundle/src/lib.rs @@ -0,0 +1,202 @@ +use std::collections::hash_map::DefaultHasher; +use std::path::{Path, PathBuf}; +use std::{hash::Hasher, process::Command}; + +struct Binding { + input_path: PathBuf, + output_path: PathBuf, +} + +/// A builder for generating TypeScript bindings lazily +#[derive(Default)] +pub struct LazyTypeScriptBindings { + binding: Vec, + minify_level: MinifyLevel, + watching: Vec, +} + +impl LazyTypeScriptBindings { + /// Create a new builder for generating TypeScript bindings that inputs from the given path and outputs javascript to the given path + pub fn new() -> Self { + Self::default() + } + + /// Add a binding to generate + pub fn with_binding( + mut self, + input_path: impl AsRef, + output_path: impl AsRef, + ) -> Self { + let input_path = input_path.as_ref(); + let output_path = output_path.as_ref(); + + self.binding.push(Binding { + input_path: input_path.to_path_buf(), + output_path: output_path.to_path_buf(), + }); + + self + } + + /// Set the minify level for the bindings + pub fn with_minify_level(mut self, minify_level: MinifyLevel) -> Self { + self.minify_level = minify_level; + self + } + + /// Watch any .js or .ts files in a directory and re-generate the bindings when they change + // TODO: we should watch any files that get bundled by bun by reading the source map + pub fn with_watching(mut self, path: impl AsRef) -> Self { + let path = path.as_ref(); + self.watching.push(path.to_path_buf()); + self + } + + /// Run the bindings + pub fn run(&self) { + // If any TS changes, re-run the build script + let mut watching_paths = Vec::new(); + for path in &self.watching { + if let Ok(dir) = std::fs::read_dir(path) { + for entry in dir.flatten() { + let path = entry.path(); + if path + .extension() + .map(|ext| ext == "ts" || ext == "js") + .unwrap_or(false) + { + watching_paths.push(path); + } + } + } else { + watching_paths.push(path.to_path_buf()); + } + } + for path in &watching_paths { + println!("cargo:rerun-if-changed={}", path.display()); + } + + // Compute the hash of the input files + let hashes = hash_files(watching_paths); + + // Try to find a common prefix for the output files and put the hash in there otherwise, write it to src/binding_hash.txt + let mut hash_location: Option = None; + for path in &self.binding { + match hash_location { + Some(current_hash_location) => { + let mut common_path = PathBuf::new(); + for component in path + .output_path + .components() + .zip(current_hash_location.components()) + { + if component.0 != component.1 { + break; + } + common_path.push(component.0); + } + hash_location = + (common_path.components().next().is_some()).then_some(common_path); + } + None => { + hash_location = Some(path.output_path.clone()); + } + }; + } + let hash_location = hash_location.unwrap_or_else(|| PathBuf::from("./src/js")); + std::fs::create_dir_all(&hash_location).unwrap(); + let hash_location = hash_location.join("hash.txt"); + + // If the hash matches the one on disk, we're good and don't need to update bindings + let fs_hash_string = std::fs::read_to_string(&hash_location); + let expected = fs_hash_string + .as_ref() + .map(|s| s.trim()) + .unwrap_or_default(); + let hashes_string = format!("{hashes:?}"); + if expected == hashes_string { + return; + } + + // Otherwise, generate the bindings and write the new hash to disk + for path in &self.binding { + gen_bindings(&path.input_path, &path.output_path, self.minify_level); + } + + std::fs::write(hash_location, hashes_string).unwrap(); + } +} + +/// The level of minification to apply to the bindings +#[derive(Copy, Clone, Debug, Default)] +pub enum MinifyLevel { + /// Don't minify the bindings + None, + /// Minify whitespace + Whitespace, + /// Minify whitespace and syntax + #[default] + Syntax, + /// Minify whitespace, syntax, and identifiers + Identifiers, +} + +impl MinifyLevel { + fn as_args(&self) -> &'static [&'static str] { + match self { + MinifyLevel::None => &[], + MinifyLevel::Whitespace => &["--minify-whitespace"], + MinifyLevel::Syntax => &["--minify-whitespace", "--minify-syntax"], + MinifyLevel::Identifiers => &[ + "--minify-whitespace", + "--minify-syntax", + "--minify-identifiers", + ], + } + } +} + +/// Hashes the contents of a directory +fn hash_files(mut files: Vec) -> Vec { + // Different systems will read the files in different orders, so we sort them to make sure the hash is consistent + files.sort(); + let mut hashes = Vec::new(); + for file in files { + let mut hash = DefaultHasher::new(); + let Ok(contents) = std::fs::read_to_string(file) else { + continue; + }; + // windows + git does a weird thing with line endings, so we need to normalize them + for line in contents.lines() { + hash.write(line.as_bytes()); + } + hashes.push(hash.finish()); + } + hashes +} + +// okay...... so bun might fail if the user doesn't have it installed +// we don't really want to fail if that's the case +// but if you started *editing* the .ts files, you're gonna have a bad time +// so..... +// we need to hash each of the .ts files and add that hash to the JS files +// if the hashes don't match, we need to fail the build +// that way we also don't need +fn gen_bindings(input_path: &Path, output_path: &Path, minify_level: MinifyLevel) { + // If the file is generated, and the hash is different, we need to generate it + let status = Command::new("bun") + .arg("build") + .arg(input_path) + .arg("--outfile") + .arg(output_path) + .args(minify_level.as_args()) + .status() + .unwrap(); + + if !status.success() { + panic!( + "Failed to generate bindings for {:?}. Make sure you have bun installed", + input_path + ); + } +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index a8bd4da98..5463a2948 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -22,7 +22,7 @@ tokio-stream = { version = "0.1.11", features = ["net"] } tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" -dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] } +dioxus-html = { workspace = true, features = ["serialize", "document", "mounted"] } rustc-hash = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } diff --git a/packages/liveview/src/eval.rs b/packages/liveview/src/eval.rs index c7dca0c3d..aeb2e8729 100644 --- a/packages/liveview/src/eval.rs +++ b/packages/liveview/src/eval.rs @@ -1,48 +1,48 @@ use dioxus_core::ScopeId; -use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; +use dioxus_html::document::{Document, EvalError, Evaluator}; use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use std::rc::Rc; use crate::query::{Query, QueryEngine}; -/// Provides the DesktopEvalProvider through [`cx.provide_context`]. +/// Provides the LiveviewDocument through [`ScopeId::provide_context`]. pub fn init_eval() { let query = ScopeId::ROOT.consume_context::().unwrap(); - let provider: Rc = Rc::new(DesktopEvalProvider { query }); + let provider: Rc = Rc::new(LiveviewDocument { query }); ScopeId::ROOT.provide_context(provider); } -/// Reprints the desktop-target's provider of evaluators. -pub struct DesktopEvalProvider { +/// Reprints the liveview-target's provider of evaluators. +pub struct LiveviewDocument { query: QueryEngine, } -impl EvalProvider for DesktopEvalProvider { +impl Document for LiveviewDocument { fn new_evaluator(&self, js: String) -> GenerationalBox> { - DesktopEvaluator::create(self.query.clone(), js) + LiveviewEvaluator::create(self.query.clone(), js) } } -/// Reprents a desktop-target's JavaScript evaluator. -pub(crate) struct DesktopEvaluator { +/// Reprents a liveview-target's JavaScript evaluator. +pub(crate) struct LiveviewEvaluator { query: Query, } -impl DesktopEvaluator { - /// Creates a new evaluator for desktop-based targets. +impl LiveviewEvaluator { + /// Creates a new evaluator for liveview-based targets. pub fn create(query_engine: QueryEngine, js: String) -> GenerationalBox> { let query = query_engine.new_query(&js); // We create a generational box that is owned by the query slot so that when we drop the query slot, the generational box is also dropped. let owner = UnsyncStorage::owner(); let query_id = query.id; - let query = owner.insert(Box::new(DesktopEvaluator { query }) as Box); + let query = owner.insert(Box::new(LiveviewEvaluator { query }) as Box); query_engine.active_requests.slab.borrow_mut()[query_id].owner = Some(owner); query } } -impl Evaluator for DesktopEvaluator { +impl Evaluator for LiveviewEvaluator { /// # Panics /// This will panic if the query is currently being awaited. fn poll_join( diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 458f29dcd..71d543911 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -85,7 +85,7 @@ fn handle_edits_code() -> String { .replace("export", ""); while let Some(import_start) = interpreter.find("import") { let import_end = interpreter[import_start..] - .find(|c| c == ';' || c == '\n') + .find([';', '\n']) .map(|i| i + import_start) .unwrap_or_else(|| interpreter.len()); interpreter.replace_range(import_start..import_end, ""); diff --git a/packages/playwright-tests/fullstack.spec.js b/packages/playwright-tests/fullstack.spec.js index 52585aea0..98f7623a2 100644 --- a/packages/playwright-tests/fullstack.spec.js +++ b/packages/playwright-tests/fullstack.spec.js @@ -1,34 +1,38 @@ // @ts-check -const { test, expect } = require('@playwright/test'); +const { test, expect } = require("@playwright/test"); -test('button click', async ({ page }) => { - await page.goto('http://localhost:3333'); +test("button click", async ({ page }) => { + await page.goto("http://localhost:3333"); await page.waitForTimeout(1000); // Expect the page to contain the counter text. - const main = page.locator('#main'); - await expect(main).toContainText('hello axum! 12345'); + const main = page.locator("#main"); + await expect(main).toContainText("hello axum! 12345"); + // Expect the title to contain the counter text. + await expect(page).toHaveTitle("hello axum! 12345"); // Click the increment button. - let button = page.locator('button.increment-button'); + let button = page.locator("button.increment-button"); await button.click(); // Expect the page to contain the updated counter text. - await expect(main).toContainText('hello axum! 12346'); + await expect(main).toContainText("hello axum! 12346"); + // Expect the title to contain the updated counter text. + await expect(page).toHaveTitle("hello axum! 12346"); }); -test('fullstack communication', async ({ page }) => { - await page.goto('http://localhost:3333'); +test("fullstack communication", async ({ page }) => { + await page.goto("http://localhost:3333"); await page.waitForTimeout(1000); // Expect the page to contain the counter text. - const main = page.locator('#main'); - await expect(main).toContainText('Server said: ...'); + const main = page.locator("#main"); + await expect(main).toContainText("Server said: ..."); // Click the increment button. - let button = page.locator('button.server-button'); + let button = page.locator("button.server-button"); await button.click(); // Expect the page to contain the updated counter text. - await expect(main).toContainText('Server said: Hello from the server!'); + await expect(main).toContainText("Server said: Hello from the server!"); }); diff --git a/packages/playwright-tests/fullstack/src/main.rs b/packages/playwright-tests/fullstack/src/main.rs index 5438cdc60..95631b08e 100644 --- a/packages/playwright-tests/fullstack/src/main.rs +++ b/packages/playwright-tests/fullstack/src/main.rs @@ -17,6 +17,7 @@ fn app() -> Element { rsx! { h1 { "hello axum! {count}" } + Title { "hello axum! {count}" } button { class: "increment-button", onclick: move |_| count += 1, "Increment" } button { class: "server-button", diff --git a/packages/playwright-tests/nested-suspense.spec.js b/packages/playwright-tests/nested-suspense.spec.js index 8a0aaac5a..d3d9f7607 100644 --- a/packages/playwright-tests/nested-suspense.spec.js +++ b/packages/playwright-tests/nested-suspense.spec.js @@ -17,6 +17,9 @@ test("nested suspense resolves", async ({ page }) => { "The robot becomes sentient and says hello world" ); + // And expect the title to have resolved on the client + await expect(page).toHaveTitle("The robot says hello world"); + // And more loading text for the nested suspense await expect(mainMessageDiv).toContainText("Loading 1..."); await expect(mainMessageDiv).toContainText("Loading 2..."); diff --git a/packages/playwright-tests/nested-suspense/src/main.rs b/packages/playwright-tests/nested-suspense/src/main.rs index 288ab5bde..c8e275ff9 100644 --- a/packages/playwright-tests/nested-suspense/src/main.rs +++ b/packages/playwright-tests/nested-suspense/src/main.rs @@ -17,6 +17,10 @@ fn main() { fn app() -> Element { rsx! { + SuspenseBoundary { + fallback: move |_| rsx! {}, + LoadTitle {} + } MessageWithLoader { id: 0 } } } @@ -33,6 +37,17 @@ fn MessageWithLoader(id: usize) -> Element { } } +#[component] +fn LoadTitle() -> Element { + let title = use_server_future(move || server_content(0))?() + .unwrap() + .unwrap(); + + rsx! { + Title { "{title.title}" } + } +} + #[component] fn Message(id: usize) -> Element { let message = use_server_future(move || server_content(id))?() diff --git a/packages/playwright-tests/web.spec.js b/packages/playwright-tests/web.spec.js index be31defff..7daf122b8 100644 --- a/packages/playwright-tests/web.spec.js +++ b/packages/playwright-tests/web.spec.js @@ -7,6 +7,8 @@ test("button click", async ({ page }) => { // Expect the page to contain the counter text. const main = page.locator("#main"); await expect(main).toContainText("hello axum! 0"); + // Expect the title to contain the counter text. + await expect(page).toHaveTitle("hello axum! 0"); // Click the increment button. let button = page.locator("button.increment-button"); @@ -14,6 +16,8 @@ test("button click", async ({ page }) => { // Expect the page to contain the updated counter text. await expect(main).toContainText("hello axum! 1"); + // Expect the title to contain the updated counter text. + await expect(page).toHaveTitle("hello axum! 1"); }); test("svg", async ({ page }) => { diff --git a/packages/playwright-tests/web/src/main.rs b/packages/playwright-tests/web/src/main.rs index 58a0ec174..ae68f15d3 100644 --- a/packages/playwright-tests/web/src/main.rs +++ b/packages/playwright-tests/web/src/main.rs @@ -9,6 +9,7 @@ fn app() -> Element { rsx! { div { "hello axum! {num}" + Title { "hello axum! {num}" } button { class: "increment-button", onclick: move |_| num += 1, "Increment" } } svg { circle { cx: 50, cy: 50, r: 40, stroke: "green", fill: "yellow" } } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 66c35d93b..09bd8b7ca 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -198,6 +198,7 @@ impl Debug for LinkProps { /// # r#"A fully configured link"# /// # ); /// ``` +#[doc(alias = "")] #[allow(non_snake_case)] pub fn Link(props: LinkProps) -> Element { let LinkProps { diff --git a/packages/router/src/history/liveview.rs b/packages/router/src/history/liveview.rs index 036d5d84c..790b9d98e 100644 --- a/packages/router/src/history/liveview.rs +++ b/packages/router/src/history/liveview.rs @@ -1,6 +1,7 @@ use super::HistoryProvider; use crate::routable::Routable; use dioxus_lib::prelude::*; +use document::UseEval; use serde::{Deserialize, Serialize}; use std::sync::{Mutex, RwLock}; use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc}; @@ -38,11 +39,6 @@ where last_visited: usize, } -#[derive(Serialize, Deserialize)] -struct SessionStorage { - liveview: Option, -} - enum Action { GoBack, GoForward, @@ -172,7 +168,7 @@ where let updater_callback: Arc>> = Arc::new(RwLock::new(Arc::new(|| {}))); - let eval_provider = consume_context::>(); + let eval_provider = document(); let create_eval = Rc::new(move |script: &str| { UseEval::new(eval_provider.new_evaluator(script.to_string())) diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index a54fb7c3f..9f4756847 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-html = { workspace = true, features = ["eval"]} +dioxus-html = { workspace = true, features = ["document"]} dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true } dioxus-interpreter-js = { workspace = true } generational-box = { workspace = true } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index de6dc3e20..dbd09af60 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -54,15 +54,15 @@ features = [ ] [features] -default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"] +default = ["panic_hook", "mounted", "file_engine", "hot_reload", "document"] panic_hook = ["dep:console_error_panic_hook"] -hydrate = ["web-sys/Comment", "ciborium"] +hydrate = ["web-sys/Comment", "ciborium", "dep:serde"] mounted = ["web-sys/Element", "dioxus-html/mounted"] file_engine = [ "dioxus-html/file-engine", ] hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location", "dep:serde_json", "dep:serde", "dioxus-core/serialize"] -eval = ["dioxus-html/eval", "dioxus-interpreter-js/eval", "dep:serde-wasm-bindgen", "dep:serde_json"] +document = ["dioxus-html/document", "dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"] [dev-dependencies] dioxus = { workspace = true, default-features = true } diff --git a/packages/web/src/eval.rs b/packages/web/src/document.rs similarity index 89% rename from packages/web/src/eval.rs rename to packages/web/src/document.rs index c4a8a61a4..3efedf050 100644 --- a/packages/web/src/eval.rs +++ b/packages/web/src/document.rs @@ -1,5 +1,7 @@ -use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; -use dioxus_interpreter_js::eval::{JSOwner, WeakDioxusChannel, WebDioxusChannel}; +use dioxus_core::ScopeId; +use dioxus_html::document::{ + Document, EvalError, Evaluator, JSOwner, WeakDioxusChannel, WebDioxusChannel, +}; use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use js_sys::Function; use serde_json::Value; @@ -8,15 +10,17 @@ use std::pin::Pin; use std::{rc::Rc, str::FromStr}; use wasm_bindgen::prelude::*; -/// Provides the WebEvalProvider through [`cx.provide_context`]. -pub fn init_eval() { - let provider: Rc = Rc::new(WebEvalProvider); - dioxus_core::ScopeId::ROOT.provide_context(provider); +/// Provides the WebEvalProvider through [`ScopeId::provide_context`]. +pub fn init_document() { + let provider: Rc = Rc::new(WebDocument); + if ScopeId::ROOT.has_context::>().is_none() { + ScopeId::ROOT.provide_context(provider); + } } -/// Represents the web-target's provider of evaluators. -pub struct WebEvalProvider; -impl EvalProvider for WebEvalProvider { +/// The web-target's document provider. +pub struct WebDocument; +impl Document for WebDocument { fn new_evaluator(&self, js: String) -> GenerationalBox> { WebEvaluator::create(js) } diff --git a/packages/web/src/hydration/deserialize.rs b/packages/web/src/hydration/deserialize.rs index fb580be6c..86e9ed35d 100644 --- a/packages/web/src/hydration/deserialize.rs +++ b/packages/web/src/hydration/deserialize.rs @@ -80,9 +80,21 @@ impl HTMLDataCursor { } /// An error that can occur when trying to take data from the server +#[derive(Debug)] pub enum TakeDataError { /// Deserializing the data failed DeserializationError(ciborium::de::Error), /// No data was available DataNotAvailable, } + +impl std::fmt::Display for TakeDataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e), + Self::DataNotAvailable => write!(f, "DataNotAvailable"), + } + } +} + +impl std::error::Error for TakeDataError {} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ffac6bbb0..38eee7a73 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -35,8 +35,10 @@ pub mod launch; mod mutations; pub use event::*; -#[cfg(feature = "eval")] -mod eval; +#[cfg(feature = "document")] +mod document; +#[cfg(feature = "document")] +pub use document::WebDocument; #[cfg(all(feature = "hot_reload", debug_assertions))] mod hot_reload; @@ -60,8 +62,8 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! { let mut dom = virtual_dom; - #[cfg(feature = "eval")] - dom.in_runtime(eval::init_eval); + #[cfg(feature = "document")] + dom.in_runtime(document::init_document); #[cfg(feature = "panic_hook")] if web_config.default_panic_hook {