Browse Source

Move the document trait into a separate crate (#3035)

* add a default head method through eval
* remove the old document trait
* implement document for each platform
* pull out document into a dedicated crate to cut down on shared dependencies
---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
Evan Almloff 8 months ago
parent
commit
519ec9d294
91 changed files with 1109 additions and 1040 deletions
  1. 23 0
      Cargo.lock
  2. 25 23
      Cargo.toml
  3. 1 1
      example-projects/ecommerce-site/src/components/loading.rs
  4. 1 1
      example-projects/ecommerce-site/src/main.rs
  5. 2 2
      example-projects/file-explorer/src/main.rs
  6. 1 1
      example-projects/fullstack-hackernews/src/main.rs
  7. 1 1
      examples/all_events.rs
  8. 1 1
      examples/calculator.rs
  9. 1 1
      examples/calculator_mutable.rs
  10. 1 1
      examples/clock.rs
  11. 1 1
      examples/control_focus.rs
  12. 1 1
      examples/counters.rs
  13. 2 2
      examples/crm.rs
  14. 1 1
      examples/dynamic_asset.rs
  15. 3 3
      examples/eval.rs
  16. 1 1
      examples/file_upload.rs
  17. 1 1
      examples/flat_router.rs
  18. 1 1
      examples/global.rs
  19. 1 1
      examples/image_generator_openai.rs
  20. 1 1
      examples/link.rs
  21. 5 5
      examples/meta.rs
  22. 1 1
      examples/overlay.rs
  23. 1 1
      examples/read_size.rs
  24. 1 1
      examples/reducer.rs
  25. 1 1
      examples/resize.rs
  26. 1 1
      examples/router.rs
  27. 1 1
      examples/title.rs
  28. 1 1
      examples/todomvc.rs
  29. 1 1
      examples/weather_app.rs
  30. 1 1
      examples/window_event.rs
  31. 1 1
      packages/cli/src/builder/prepare_html.rs
  32. 1 1
      packages/core/src/scheduler.rs
  33. 1 1
      packages/core/tests/suspense.rs
  34. 1 1
      packages/desktop/Cargo.toml
  35. 6 6
      packages/desktop/headless_tests/eval.rs
  36. 1 1
      packages/desktop/headless_tests/rendering.rs
  37. 1 1
      packages/desktop/headless_tests/utils.rs
  38. 3 7
      packages/desktop/src/document.rs
  39. 1 1
      packages/desktop/src/js/hash.txt
  40. 2 2
      packages/desktop/src/ts/native_eval.ts
  41. 2 1
      packages/desktop/src/webview.rs
  42. 2 1
      packages/dioxus-lib/Cargo.toml
  43. 3 0
      packages/dioxus-lib/src/lib.rs
  44. 2 1
      packages/dioxus/Cargo.toml
  45. 1 1
      packages/dioxus/src/launch.rs
  46. 11 0
      packages/dioxus/src/lib.rs
  47. 22 0
      packages/document/Cargo.toml
  48. 1 0
      packages/document/assets/script.js
  49. 1 0
      packages/document/assets/style.css
  50. 0 0
      packages/document/build.rs
  51. 10 10
      packages/document/docs/eval.md
  52. 4 4
      packages/document/docs/head.md
  53. 156 0
      packages/document/src/document.rs
  54. 126 0
      packages/document/src/elements/link.rs
  55. 74 0
      packages/document/src/elements/meta.rs
  56. 124 0
      packages/document/src/elements/mod.rs
  57. 113 0
      packages/document/src/elements/script.rs
  58. 93 0
      packages/document/src/elements/style.rs
  59. 57 0
      packages/document/src/elements/title.rs
  60. 36 0
      packages/document/src/error.rs
  61. 74 0
      packages/document/src/eval.rs
  62. 0 0
      packages/document/src/js/hash.txt
  63. 0 0
      packages/document/src/js/head.js
  64. 31 0
      packages/document/src/lib.rs
  65. 0 0
      packages/document/src/ts/.gitignore
  66. 0 0
      packages/document/src/ts/eval.ts
  67. 0 0
      packages/document/src/ts/head.ts
  68. 18 0
      packages/document/tsconfig.json
  69. 4 9
      packages/fullstack/src/document/server.rs
  70. 7 14
      packages/fullstack/src/document/web.rs
  71. 5 8
      packages/fullstack/src/render.rs
  72. 1 1
      packages/hooks/docs/side_effects.md
  73. 1 5
      packages/html/Cargo.toml
  74. 0 0
      packages/html/assets/script.js
  75. 0 0
      packages/html/assets/style.css
  76. 0 134
      packages/html/src/document/eval.rs
  77. 0 562
      packages/html/src/document/head.rs
  78. 0 162
      packages/html/src/document/mod.rs
  79. 0 8
      packages/html/src/lib.rs
  80. 2 1
      packages/liveview/Cargo.toml
  81. 3 7
      packages/liveview/src/eval.rs
  82. 1 1
      packages/playwright-tests/fullstack/src/main.rs
  83. 1 1
      packages/playwright-tests/nested-suspense/src/main.rs
  84. 5 5
      packages/playwright-tests/web/src/main.rs
  85. 4 5
      packages/router/src/history/liveview.rs
  86. 1 0
      packages/static-generation/src/ssg.rs
  87. 2 1
      packages/web/Cargo.toml
  88. 1 1
      packages/web/src/devtools.rs
  89. 6 14
      packages/web/src/document.rs
  90. 1 1
      packages/web/src/js/hash.txt
  91. 1 1
      packages/web/src/ts/eval.ts

+ 23 - 0
Cargo.lock

@@ -2380,6 +2380,7 @@ dependencies = [
  "dioxus-core-macro",
  "dioxus-desktop",
  "dioxus-devtools",
+ "dioxus-document",
  "dioxus-fullstack",
  "dioxus-hooks",
  "dioxus-html",
@@ -2579,6 +2580,7 @@ dependencies = [
  "dioxus-cli-config",
  "dioxus-core",
  "dioxus-devtools",
+ "dioxus-document",
  "dioxus-hooks",
  "dioxus-html",
  "dioxus-interpreter-js",
@@ -2638,6 +2640,24 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "dioxus-document"
+version = "0.6.0-alpha.2"
+dependencies = [
+ "dioxus",
+ "dioxus-core",
+ "dioxus-core-macro",
+ "dioxus-core-types",
+ "dioxus-html",
+ "futures-channel",
+ "futures-util",
+ "generational-box",
+ "lazy-js-bundle",
+ "serde",
+ "serde_json",
+ "tracing",
+]
+
 [[package]]
 name = "dioxus-examples"
 version = "0.6.0-alpha.2"
@@ -2815,6 +2835,7 @@ dependencies = [
  "dioxus-config-macro",
  "dioxus-core",
  "dioxus-core-macro",
+ "dioxus-document",
  "dioxus-hooks",
  "dioxus-html",
  "dioxus-rsx",
@@ -2830,6 +2851,7 @@ dependencies = [
  "dioxus-cli-config",
  "dioxus-core",
  "dioxus-devtools",
+ "dioxus-document",
  "dioxus-html",
  "dioxus-interpreter-js",
  "futures-channel",
@@ -3074,6 +3096,7 @@ dependencies = [
  "dioxus-core",
  "dioxus-core-types",
  "dioxus-devtools",
+ "dioxus-document",
  "dioxus-html",
  "dioxus-interpreter-js",
  "dioxus-signals",

+ 25 - 23
Cargo.toml

@@ -1,39 +1,40 @@
 [workspace]
 resolver = "2"
 members = [
-    "packages/dioxus",
-    "packages/dioxus-lib",
-    "packages/core",
-    "packages/core-types",
-    "packages/cli",
+    "packages/autofmt",
+    "packages/check",
     "packages/cli-config",
-    "packages/core-macro",
+    "packages/cli",
     "packages/config-macro",
-    "packages/router-macro",
+    "packages/core-macro",
+    "packages/core-types",
+    "packages/core",
+    "packages/desktop",
+    "packages/devtools-types",
+    "packages/devtools",
+    "packages/dioxus-lib",
+    "packages/dioxus",
+    "packages/document",
     "packages/extension",
-    "packages/router",
-    "packages/html",
-    "packages/html-internal-macro",
+    "packages/fullstack",
+    "packages/generational-box",
     "packages/hooks",
-    "packages/web",
-    "packages/ssr",
-    "packages/desktop",
-    "packages/mobile",
+    "packages/html-internal-macro",
+    "packages/html",
     "packages/interpreter",
+    "packages/lazy-js-bundle",
     "packages/liveview",
-    "packages/autofmt",
-    "packages/check",
-    "packages/rsx",
+    "packages/mobile",
+    "packages/router-macro",
+    "packages/router",
     "packages/rsx-hotreload",
     "packages/rsx-rosetta",
-    "packages/generational-box",
-    "packages/signals",
-    "packages/devtools",
-    "packages/devtools-types",
-    "packages/fullstack",
+    "packages/rsx",
     "packages/server-macro",
+    "packages/signals",
+    "packages/ssr",
     "packages/static-generation",
-    "packages/lazy-js-bundle",
+    "packages/web",
 
     # Full project examples
     "example-projects/fullstack-hackernews",
@@ -76,6 +77,7 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.6.0-alpha.0" }
 dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.0" }
 dioxus-router = { path = "packages/router", version = "0.6.0-alpha.0" }
 dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.0" }
+dioxus-document = { path = "packages/document", version = "0.6.0-alpha.0", default-features = false }
 dioxus-html = { path = "packages/html", version = "0.6.0-alpha.0", default-features = false }
 dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.0" }
 dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.0" }

+ 1 - 1
example-projects/ecommerce-site/src/components/loading.rs

@@ -3,7 +3,7 @@ use dioxus::prelude::*;
 #[component]
 pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
     rsx! {
-        head::Link {
+        document::Link {
             rel: "stylesheet",
             href: asset!("./public/loading.css")
         }

+ 1 - 1
example-projects/ecommerce-site/src/main.rs

@@ -17,7 +17,7 @@ mod api;
 fn main() {
     dioxus::launch(|| {
         rsx! {
-            head::Link {
+            document::Link {
                 rel: "stylesheet",
                 href: asset!("./public/tailwind.css")
             }

+ 2 - 2
example-projects/file-explorer/src/main.rs

@@ -21,12 +21,12 @@ fn app() -> Element {
     let mut files = use_signal(Files::new);
 
     rsx! {
-        head::Link {
+        document::Link {
             rel: "stylesheet",
             href: asset!("./assets/fileexplorer.css")
         }
         div {
-            head::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
+            document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
             header {
                 i { class: "material-icons icon-menu", "menu" }
                 h1 { "Files: " {files.read().current()} }

+ 1 - 1
example-projects/fullstack-hackernews/src/main.rs

@@ -36,7 +36,7 @@ pub fn App() -> Element {
 #[component]
 fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
     rsx! {
-        head::Link { rel: "stylesheet", href: asset!("./assets/hackernews.css") }
+        document::Link { rel: "stylesheet", href: asset!("./assets/hackernews.css") }
         div { display: "flex", flex_direction: "row", width: "100%",
             div {
                 width: "50%",

+ 1 - 1
examples/all_events.rs

@@ -26,7 +26,7 @@ fn app() -> Element {
     };
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         div { id: "container",
             // focusing is necessary to catch keyboard events
             div { id: "receiver", tabindex: 0,

+ 1 - 1
examples/calculator.rs

@@ -54,7 +54,7 @@ fn app() -> Element {
     };
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         div { id: "wrapper",
             div { class: "app",
                 div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,

+ 1 - 1
examples/calculator_mutable.rs

@@ -29,7 +29,7 @@ fn app() -> Element {
     let mut state = use_signal(Calculator::new);
 
     rsx! {
-        head::Link { rel: "stylesheet", href: asset!("./examples/assets/calculator.css") }
+        document::Link { rel: "stylesheet", href: asset!("./examples/assets/calculator.css") }
         div { id: "wrapper",
             div { class: "app",
                 div {

+ 1 - 1
examples/clock.rs

@@ -36,7 +36,7 @@ fn app() -> Element {
     );
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         div { id: "app",
             div { id: "title", "Carpe diem 🎉" }
             div { id: "clock-display", "{time}" }

+ 1 - 1
examples/control_focus.rs

@@ -40,7 +40,7 @@ fn app() -> Element {
     });
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         h1 { "Input Roulette" }
         button { onclick: move |_| running.toggle(), "Toggle roulette" }
         div { id: "roulette-grid",

+ 1 - 1
examples/counters.rs

@@ -16,7 +16,7 @@ fn app() -> Element {
     let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
 
         div { id: "controls",
             button { onclick: move |_| counters.write().push(0), "Add counter" }

+ 2 - 2
examples/crm.rs

@@ -20,13 +20,13 @@ fn main() {
         }))
         .launch(|| {
             rsx! {
-                head::Link {
+                document::Link {
                     rel: "stylesheet",
                     href: asset!("https://unpkg.com/purecss@2.0.6/build/pure-min.css"), 
                     integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
                     crossorigin: "anonymous"
                 }
-                head::Link { rel: "stylesheet", href: asset!("./examples/assets/crm.css") }
+                document::Link { rel: "stylesheet", href: asset!("./examples/assets/crm.css") }
                 h1 { "Dioxus CRM Example" }
                 Router::<Route> {}
             }

+ 1 - 1
examples/dynamic_asset.rs

@@ -24,7 +24,7 @@ fn app() -> Element {
     });
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         h1 { "Dynamic Assets" }
         img { src: "/logos/logo.png" }
     }

+ 3 - 3
examples/eval.rs

@@ -19,7 +19,7 @@ fn app() -> Element {
         // The `eval` is available in the prelude - and simply takes a block of JS.
         // Dioxus' eval is interesting since it allows sending messages to and from the JS code using the `await dioxus.recv()`
         // builtin function. This allows you to create a two-way communication channel between Rust and JS.
-        let mut eval = eval(
+        let mut eval = document::eval(
             r#"
                 dioxus.send("Hi from JS!");
                 let msg = await dioxus.recv();
@@ -29,10 +29,10 @@ fn app() -> Element {
         );
 
         // Send a message to the JS code.
-        eval.send("Hi from Rust!".into()).unwrap();
+        eval.send("Hi from Rust!").unwrap();
 
         // Our line on the JS side will log the message and then return "hello world".
-        let res = eval.recv().await.unwrap();
+        let res: String = eval.recv().await.unwrap();
 
         // This will print "Hi from JS!" and "Hi from Rust!".
         println!("{:?}", eval.await);

+ 1 - 1
examples/file_upload.rs

@@ -43,7 +43,7 @@ fn app() -> Element {
     };
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
 
         h1 { "File Upload Example" }
         p { "Drop a .txt, .rs, or .js file here to read it" }

+ 1 - 1
examples/flat_router.rs

@@ -14,7 +14,7 @@ const STYLE: &str = asset!("./examples/assets/flat_router.css");
 fn main() {
     dioxus::launch(|| {
         rsx! {
-            head::Link { rel: "stylesheet", href: STYLE }
+            document::Link { rel: "stylesheet", href: STYLE }
             Router::<Route> {}
         }
     })

+ 1 - 1
examples/global.rs

@@ -18,7 +18,7 @@ fn main() {
 
 fn app() -> Element {
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         Increment {}
         Decrement {}
         Reset {}

+ 1 - 1
examples/image_generator_openai.rs

@@ -36,7 +36,7 @@ fn app() -> Element {
     });
 
     rsx! {
-        head::Link { rel: "stylesheet", href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
+        document::Link { rel: "stylesheet", href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
         div { class: "container",
             div { class: "columns",
                 div { class: "column",

+ 1 - 1
examples/link.rs

@@ -16,7 +16,7 @@ fn main() {
 
 fn app() -> Element {
     rsx! (
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         Router::<Route> {}
     )
 }

+ 5 - 5
examples/meta.rs

@@ -12,23 +12,23 @@ fn app() -> Element {
         // 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 {
+        document::Meta {
             property: "og:title",
             content: "My Site",
         }
-        Meta {
+        document::Meta {
             property: "og:type",
             content: "website",
         }
-        Meta {
+        document::Meta {
             property: "og:url",
             content: "https://www.example.com",
         }
-        Meta {
+        document::Meta {
             property: "og:image",
             content: "https://example.com/image.jpg",
         }
-        Meta {
+        document::Meta {
             name: "description",
             content: "My Site is a site",
         }

+ 1 - 1
examples/overlay.rs

@@ -22,7 +22,7 @@ fn app() -> Element {
     _ = use_global_shortcut("cmd+g", move || show_overlay.toggle());
 
     rsx! {
-        head::Link {
+        document::Link {
             rel: "stylesheet",
             href: asset!("./examples/assets/overlay.css"),
         }

+ 1 - 1
examples/read_size.rs

@@ -28,7 +28,7 @@ fn app() -> Element {
     };
 
     rsx!(
-        head::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
+        document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
         div {
             width: "50%",
             height: "50%",

+ 1 - 1
examples/reducer.rs

@@ -17,7 +17,7 @@ fn app() -> Element {
     let mut state = use_signal(|| PlayerState { is_playing: false });
 
     rsx!(
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         h1 {"Select an option"}
 
         // Add some cute animations if the radio is playing!

+ 1 - 1
examples/resize.rs

@@ -15,7 +15,7 @@ fn app() -> Element {
     let mut dimensions = use_signal(Size2D::zero);
 
     rsx!(
-        head::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
+        document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
         div {
             width: "50%",
             height: "50%",

+ 1 - 1
examples/router.rs

@@ -13,7 +13,7 @@ const STYLE: &str = asset!("./examples/assets/router.css");
 fn main() {
     dioxus::launch(|| {
         rsx! {
-            head::Link { rel: "stylesheet", href: STYLE }
+            document::Link { rel: "stylesheet", href: STYLE }
             Router::<Route> {}
         }
     });

+ 1 - 1
examples/title.rs

@@ -14,7 +14,7 @@ fn app() -> Element {
         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})" }
+            document::Title { "My Application (Count {count})" }
             button { onclick: move |_| count += 1, "Up high!" }
             button { onclick: move |_| count -= 1, "Down low!" }
         }

+ 1 - 1
examples/todomvc.rs

@@ -63,7 +63,7 @@ fn app() -> Element {
     };
 
     rsx! {
-        head::Link { rel: "stylesheet", href: STYLE }
+        document::Link { rel: "stylesheet", href: STYLE }
         section { class: "todoapp",
             TodoHeader { todos }
             section { class: "main",

+ 1 - 1
examples/weather_app.rs

@@ -19,7 +19,7 @@ fn app() -> Element {
     let current_weather = use_resource(move || async move { get_weather(&country()).await });
 
     rsx! {
-        head::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
+        document::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
         div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
             div { class: "flex items-center justify-center flex-row",
                 div { class: "flex items-start justify-center flex-row",

+ 1 - 1
examples/window_event.rs

@@ -26,7 +26,7 @@ fn main() {
 
 fn app() -> Element {
     rsx!(
-        head::Link { href: "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel: "stylesheet" }
+        document::Link { href: "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel: "stylesheet" }
         Header {}
         div { class: "container mx-auto",
             div { class: "grid grid-cols-5",

+ 1 - 1
packages/cli/src/builder/prepare_html.rs

@@ -156,7 +156,7 @@ impl BuildRequest {
                 };
                 match variant {
                     ResourceType::Style => format!(
-                        "    head::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
+                        "    document::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
                         path.display()
                     ),
                     ResourceType::Script => {

+ 1 - 1
packages/core/src/scheduler.rs

@@ -32,7 +32,7 @@
 //!     use_effect(move || {
 //!         let id = id.read();
 //!         // This will panic if the id is not written to the DOM before the effect is run
-//!         eval(format!(r#"document.getElementById("{id}").innerHTML = "Hello World";"#));
+//!         document::eval(format!(r#"document.getElementById("{id}").innerHTML = "Hello World";"#));
 //!     });
 //!
 //!     rsx! {

+ 1 - 1
packages/core/tests/suspense.rs

@@ -517,7 +517,7 @@ fn nested_suspense_resolves_client() {
         let title = use_resource(move || async_content(0)).suspend()?();
 
         rsx! {
-            Title { "{title.title}" }
+            document::Title { "{title.title}" }
         }
     }
 

+ 1 - 1
packages/desktop/Cargo.toml

@@ -15,8 +15,8 @@ dioxus-html = { workspace = true, features = [
     "serialize",
     "mounted",
     "file_engine",
-    "document",
 ] }
+dioxus-document = { workspace = true }
 dioxus-signals = { workspace = true, optional = true }
 dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
 dioxus-cli-config = { workspace = true }

+ 6 - 6
packages/desktop/headless_tests/eval.rs

@@ -16,23 +16,23 @@ static EVALS_RETURNED: GlobalSignal<usize> = Signal::global(|| 0);
 fn app() -> Element {
     // Double 100 values in the value
     use_future(|| async {
-        let mut eval = eval(
+        let mut eval = document::eval(
             r#"for (let i = 0; i < 100; i++) {
             let value = await dioxus.recv();
             dioxus.send(value*2);
         }"#,
         );
         for i in 0..100 {
-            eval.send(serde_json::Value::from(i)).unwrap();
-            let value = eval.recv().await.unwrap();
-            assert_eq!(value, serde_json::Value::from(i * 2));
+            eval.send(i).unwrap();
+            let value: i32 = eval.recv().await.unwrap();
+            assert_eq!(value, i * 2);
             EVALS_RECEIVED.with_mut(|x| *x += 1);
         }
     });
 
     // Make sure returning no value resolves the future
     use_future(|| async {
-        let eval = eval(r#"return;"#);
+        let eval = document::eval(r#"return;"#);
 
         eval.await.unwrap();
         EVALS_RETURNED.with_mut(|x| *x += 1);
@@ -40,7 +40,7 @@ fn app() -> Element {
 
     // Return a value from the future
     use_future(|| async {
-        let eval = eval(
+        let eval = document::eval(
             r#"
         return [1, 2, 3];
         "#,

+ 1 - 1
packages/desktop/headless_tests/rendering.rs

@@ -16,7 +16,7 @@ fn use_inner_html(id: &'static str) -> Option<String> {
         spawn(async move {
             tokio::time::sleep(std::time::Duration::from_millis(500)).await;
 
-            let res = eval(&format!(
+            let res = document::eval(&format!(
                 r#"let element = document.getElementById('{}');
                 return element.innerHTML"#,
                 id

+ 1 - 1
packages/desktop/headless_tests/utils.rs

@@ -50,7 +50,7 @@ pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'sta
                 "#
             );
 
-            eval(&js).await.unwrap();
+            document::eval(&js).await.unwrap();
         });
     })
 }

+ 3 - 7
packages/desktop/src/document.rs

@@ -1,4 +1,4 @@
-use dioxus_html::document::{Document, EvalError, Evaluator};
+use dioxus_document::{Document, Eval, EvalError, Evaluator};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 
 use crate::{query::Query, DesktopContext};
@@ -18,17 +18,13 @@ impl DesktopDocument {
 }
 
 impl Document for DesktopDocument {
-    fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
-        DesktopEvaluator::create(self.desktop_ctx.clone(), js)
+    fn eval(&self, js: String) -> Eval {
+        Eval::new(DesktopEvaluator::create(self.desktop_ctx.clone(), js))
     }
 
     fn set_title(&self, title: String) {
         self.desktop_ctx.window.set_title(&title);
     }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
-    }
 }
 
 /// Represents a desktop-target's JavaScript evaluator.

+ 1 - 1
packages/desktop/src/js/hash.txt

@@ -1 +1 @@
-[11927251734412729446]
+[14101548031762241351]

+ 2 - 2
packages/desktop/src/ts/native_eval.ts

@@ -2,7 +2,7 @@ import {
   Channel,
   DioxusChannel,
   WeakDioxusChannel,
-} from "../../../html/src/ts/eval";
+} from "../../../document/src/ts/eval";
 
 // In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
 declare global {
@@ -86,5 +86,5 @@ export class NativeDioxusChannel extends DioxusChannel {
   }
 
   // Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
-  async rustRecv(): Promise<any> {}
+  async rustRecv(): Promise<any> { }
 }

+ 2 - 1
packages/desktop/src/webview.rs

@@ -13,8 +13,9 @@ use crate::{
     Config, DesktopContext, DesktopService,
 };
 use dioxus_core::{Runtime, ScopeId, VirtualDom};
+use dioxus_document::Document;
 use dioxus_hooks::to_owned;
-use dioxus_html::{prelude::Document, HasFileData, HtmlEvent, PlatformEventData};
+use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
 use futures_util::{pin_mut, FutureExt};
 use std::cell::OnceCell;
 use std::sync::Arc;

+ 2 - 1
packages/dioxus-lib/Cargo.toml

@@ -13,6 +13,7 @@ rust-version = "1.79.0"
 [dependencies]
 dioxus-core = { workspace = true }
 dioxus-html = { workspace = true, optional = true }
+dioxus-document = { workspace = true, optional = true }
 dioxus-core-macro = { workspace = true, optional = true }
 dioxus-config-macro = { workspace = true, optional = true }
 dioxus-hooks = { workspace = true, optional = true }
@@ -26,7 +27,7 @@ dioxus = { workspace = true }
 default = ["macro", "html", "signals", "hooks"]
 signals = ["dep:dioxus-signals"]
 macro = ["dep:dioxus-core-macro", "dep:dioxus-rsx", "dep:dioxus-config-macro"]
-html = ["dep:dioxus-html"]
+html = ["dep:dioxus-html", "dep:dioxus-document"]
 hooks = ["dep:dioxus-hooks"]
 
 [package.metadata.docs.rs]

+ 3 - 0
packages/dioxus-lib/src/lib.rs

@@ -16,6 +16,9 @@ pub mod events {
 #[cfg(feature = "html")]
 pub use dioxus_html as html;
 
+#[cfg(feature = "html")]
+pub use dioxus_document as document;
+
 #[cfg(feature = "macro")]
 pub use dioxus_rsx as rsx;
 

+ 2 - 1
packages/dioxus/Cargo.toml

@@ -13,6 +13,7 @@ rust-version = "1.79.0"
 [dependencies]
 dioxus-core = { workspace = true }
 dioxus-html = { workspace = true, default-features = false, optional = true }
+dioxus-document = { workspace = true, optional = true }
 dioxus-core-macro = { workspace = true, optional = true }
 dioxus-config-macro = { workspace = true, optional = true }
 dioxus-hooks = { workspace = true, optional = true }
@@ -44,7 +45,7 @@ devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/de
 mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
 file_engine = ["dioxus-web?/file_engine"]
 asset = ["dep:manganis", "dioxus-core/manganis"]
-document = ["dioxus-web?/document", "dioxus-html?/document"]
+document = ["dioxus-web?/document", "dioxus-document"]
 
 launch = ["dep:dioxus-config-macro"]
 router = ["dep:dioxus-router"]

+ 1 - 1
packages/dioxus/src/launch.rs

@@ -330,7 +330,7 @@ fn web_launch(
                 #[cfg(all(feature = "static-generation", not(feature = "fullstack")))]
                 use dioxus_static_site_generation::document;
                 let document = std::rc::Rc::new(document::web::FullstackWebDocument)
-                    as std::rc::Rc<dyn crate::prelude::Document>;
+                    as std::rc::Rc<dyn crate::prelude::document::Document>;
                 vdom.provide_root_context(document);
             }
             vdom

+ 11 - 0
packages/dioxus/src/lib.rs

@@ -50,6 +50,10 @@ pub mod events {
     pub use dioxus_html::prelude::*;
 }
 
+#[cfg(feature = "document")]
+#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
+pub use dioxus_document as document;
+
 #[cfg(feature = "html")]
 #[cfg_attr(docsrs, doc(cfg(feature = "html")))]
 pub use dioxus_html as html;
@@ -59,6 +63,13 @@ pub use dioxus_html as html;
 pub use dioxus_core_macro as core_macro;
 
 pub mod prelude {
+    #[cfg(feature = "document")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
+    pub use dioxus_document as document;
+
+    #[cfg(feature = "launch")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
+    pub use crate::launch::*;
 
     #[cfg(feature = "hooks")]
     #[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]

+ 22 - 0
packages/document/Cargo.toml

@@ -0,0 +1,22 @@
+[package]
+name = "dioxus-document"
+edition = "2021"
+version.workspace = true
+
+[dependencies]
+dioxus-core = { workspace = true }
+dioxus-core-types = { workspace = true }
+dioxus-core-macro = { workspace = true }
+dioxus-html = { workspace = true }
+tracing = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+futures-channel = { workspace = true }
+futures-util.workspace = true
+generational-box.workspace = true
+
+[build-dependencies]
+lazy-js-bundle = { workspace = true }
+
+[dev-dependencies]
+dioxus = { workspace = true }

+ 1 - 0
packages/document/assets/script.js

@@ -0,0 +1 @@
+// this script is included as an asset!() for a test

+ 1 - 0
packages/document/assets/style.css

@@ -0,0 +1 @@
+/* this stylesheet is included as an asset!() for a test */

+ 0 - 0
packages/html/build.rs → packages/document/build.rs


+ 10 - 10
packages/html/docs/eval.md → packages/document/docs/eval.md

@@ -1,6 +1,6 @@
 # Communicating with JavaScript
 
-You can use the `eval` function to execute JavaScript code in your application with the desktop, mobile, web or liveview renderers. Eval takes a block of JavaScript code (that may be asynchronous) and returns a `UseEval` object that you can use to send data to the JavaScript code and receive data from it.
+You can use the `eval` function to execute JavaScript code in your application with the desktop, mobile, web or liveview renderers. Eval takes a block of JavaScript code (that may be asynchronous) and returns a `Eval` object that you can use to send data to the JavaScript code and receive data from it.
 
 <div class="warning">
 
@@ -18,7 +18,7 @@ fn App() -> Element {
         button {
             onclick: move |_| async move {
                 // Eval is a global function you can use anywhere inside Dioxus. It will execute the given JavaScript code.
-                let result = eval(r#"console.log("Hello World");
+                let result = document::eval(r#"console.log("Hello World");
                 return "Hello World";"#);
 
                 // You can use the `await` keyword to wait for the result of the JavaScript code.
@@ -32,7 +32,7 @@ fn App() -> Element {
 
 ## Sending data to JavaScript
 
-When you execute JavaScript code with `eval`, you can pass data to it by formatting the value into the JavaScript code or sending values to the `UseEval` channel.
+When you execute JavaScript code with `eval`, you can pass data to it by formatting the value into the JavaScript code or sending values to the `Eval` channel.
 
 ```rust
 use dioxus::prelude::*;
@@ -43,7 +43,7 @@ fn app() -> Element {
             onclick: move |_| {
                 // You can pass initial data to the eval function by formatting it into the JavaScript code.
                 const LOOP_COUNT: usize = 10;
-                let eval = eval(&format!(r#"for(let i = 0; i < {LOOP_COUNT}; i++) {{
+                let eval = document::eval(&format!(r#"for(let i = 0; i < {LOOP_COUNT}; i++) {{
                     // You can receive values asynchronously with the the `await dioxus.recv()` method.
                     let value = await dioxus.recv();
                     console.log("Received", value);
@@ -51,7 +51,7 @@ fn app() -> Element {
 
                 // You can send values from rust to the JavaScript code with the `send` method on the object returned by `eval`.
                 for i in 0..LOOP_COUNT {
-                    eval.send(i.into()).unwrap();
+                    eval.send(i).unwrap();
                 }
             },
             "Log Count"
@@ -62,7 +62,7 @@ fn app() -> Element {
 
 ## Sending data from JavaScript
 
-The `UseEval` struct also contains methods for receiving values you send from JavaScript. You can use the `dioxus.send()` method to send values to the JavaScript code and the `UseEval::recv()` method to receive values from the JavaScript code.
+The `Eval` struct also contains methods for receiving values you send from JavaScript. You can use the `dioxus.send()` method to send values to the JavaScript code and the `Eval::recv()` method to receive values from the JavaScript code.
 
 ```rust
 use dioxus::prelude::*;
@@ -72,14 +72,14 @@ fn app() -> Element {
         button {
             onclick: move |_| async move {
                 // You can send values from rust to the JavaScript code by using the `send` method on the object returned by `eval`.
-                let mut eval = eval(r#"for(let i = 0; i < 10; i++) {
+                let mut eval = document::eval(r#"for(let i = 0; i < 10; i++) {
                     // You can send values asynchronously with the `dioxus.send()` method.
                     dioxus.send(i);
                 }"#);
 
                 // You can receive values from the JavaScript code with the `recv` method on the object returned by `eval`.
                 for _ in 0..10 {
-                    let value = eval.recv().await.unwrap();
+                    let value: i32 = eval.recv().await.unwrap();
                     println!("Received {}", value);
                 }
             },
@@ -104,12 +104,12 @@ const SCRIPT: &str = r#"
 
 fn app() -> Element {
     // ❌ You shouldn't run eval in the body of a component. This will run before the component has been mounted
-    // eval(SCRIPT);
+    // document::eval(SCRIPT);
 
     // ✅ You should run eval inside an effect or event. This will run after the component has been mounted
     use_effect(move || {
         spawn(async {
-            let count = eval(SCRIPT).await;
+            let count = document::eval(SCRIPT).await;
             println!("Count is {:?}", count);
         });
     });

+ 4 - 4
packages/html/docs/head.md → packages/document/docs/head.md

@@ -4,7 +4,7 @@ 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)
+- [document::Link](crate::document::Link)
 - [Script](crate::Script)
 - [Style](crate::Style)
 
@@ -25,7 +25,7 @@ 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 {
+        document::Meta {
             http_equiv: "refresh",
             content: "10;url=https://dioxuslabs.com",
         }
@@ -46,12 +46,12 @@ If you have any important metadata that you want to render into the head, make s
 fn App() -> Element {
     rsx! {
         // This will render in SSR
-        Title { "My Page" }
+        document::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" }
+                document::Meta { name: "description", content: "My Page" }
             }
         }
     }

+ 156 - 0
packages/document/src/document.rs

@@ -0,0 +1,156 @@
+use std::sync::Arc;
+
+use super::*;
+
+/// A context for the document
+pub type DocumentContext = Arc<dyn Document>;
+
+fn format_string_for_js(s: &str) -> String {
+    let escaped = s
+        .replace('\\', "\\\\")
+        .replace('\n', "\\n")
+        .replace('\r', "\\r")
+        .replace('"', "\\\"");
+    format!("\"{escaped}\"")
+}
+
+fn format_attributes(attributes: &[(&str, String)]) -> String {
+    let mut formatted = String::from("[");
+    for (key, value) in attributes {
+        formatted.push_str(&format!(
+            "[{}, {}],",
+            format_string_for_js(key),
+            format_string_for_js(value)
+        ));
+    }
+    if formatted.ends_with(',') {
+        formatted.pop();
+    }
+    formatted.push(']');
+    formatted
+}
+
+fn create_element_in_head(
+    tag: &str,
+    attributes: &[(&str, String)],
+    children: Option<String>,
+) -> String {
+    let helpers = include_str!("./js/head.js");
+    let attributes = format_attributes(attributes);
+    let children = children
+        .as_deref()
+        .map(format_string_for_js)
+        .unwrap_or("null".to_string());
+    let tag = format_string_for_js(tag);
+    format!(r#"{helpers};window.createElementInHead({tag}, {attributes}, {children});"#)
+}
+
+/// A provider for document-related functionality.
+///
+/// Provides things like a history API, a title, a way to run JS, and some other basics/essentials used
+/// by nearly every platform.
+///
+/// An integration with some kind of navigation history.
+///
+/// Depending on your use case, your implementation may deviate from the described procedure. This
+/// is fine, as long as both `current_route` and `current_query` match the described format.
+///
+/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
+/// The described behaviors are designed to mimic a web browser, which most users should already
+/// know. Deviations might confuse them.
+pub trait Document: 'static {
+    /// Run `eval` against this document, returning an [`Eval`] that can be used to await the result.
+    fn eval(&self, js: String) -> Eval;
+
+    /// Set the title of the document
+    fn set_title(&self, title: String) {
+        self.eval(format!("document.title = {title:?};"));
+    }
+
+    /// Create a new element in the head
+    fn create_head_element(
+        &self,
+        name: &str,
+        attributes: &[(&str, String)],
+        contents: Option<String>,
+    ) {
+        self.eval(create_element_in_head(name, attributes, contents));
+    }
+
+    /// Create a new meta tag in the head
+    fn create_meta(&self, props: MetaProps) {
+        let attributes = props.attributes();
+        self.create_head_element("meta", &attributes, None);
+    }
+
+    /// Create a new script tag in the head
+    fn create_script(&self, props: ScriptProps) {
+        let attributes = props.attributes();
+        match (&props.src, props.script_contents()) {
+            // The script has inline contents, render it as a script tag
+            (_, Ok(contents)) => self.create_head_element("script", &attributes, Some(contents)),
+            // The script has a src, render it as a script tag without a body
+            (Some(_), _) => self.create_head_element("script", &attributes, None),
+            // The script has neither contents nor src, log an error
+            (None, Err(err)) => err.log("Script"),
+        }
+    }
+
+    /// Create a new style tag in the head
+    fn create_style(&self, props: StyleProps) {
+        let mut attributes = props.attributes();
+        match (&props.href, props.style_contents()) {
+            // The style has inline contents, render it as a style tag
+            (_, Ok(contents)) => self.create_head_element("style", &attributes, Some(contents)),
+            // The style has a src, render it as a link tag
+            (Some(_), _) => {
+                attributes.push(("type", "text/css".into()));
+                self.create_head_element("link", &attributes, None)
+            }
+            // The style has neither contents nor src, log an error
+            (None, Err(err)) => err.log("Style"),
+        };
+    }
+
+    /// Create a new link tag in the head
+    fn create_link(&self, props: LinkProps) {
+        let attributes = props.attributes();
+        self.create_head_element("link", &attributes, None);
+    }
+}
+
+/// A document that does nothing
+#[derive(Default)]
+pub struct NoOpDocument;
+
+impl Document for NoOpDocument {
+    fn eval(&self, _: String) -> Eval {
+        let owner = generational_box::Owner::default();
+        let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
+        Eval::new(boxed)
+    }
+}
+
+/// An evaluator that does nothing
+#[derive(Default)]
+pub 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 std::task::Context<'_>,
+    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
+        std::task::Poll::Ready(Err(EvalError::Unsupported))
+    }
+
+    fn poll_join(
+        &mut self,
+        _context: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
+        std::task::Poll::Ready(Err(EvalError::Unsupported))
+    }
+}

+ 126 - 0
packages/document/src/elements/link.rs

@@ -0,0 +1,126 @@
+use super::*;
+use crate::document;
+use dioxus_html as dioxus_elements;
+
+#[non_exhaustive]
+#[derive(Clone, Props, PartialEq)]
+pub struct LinkProps {
+    pub rel: Option<String>,
+    pub media: Option<String>,
+    pub title: Option<String>,
+    pub disabled: Option<bool>,
+    pub r#as: Option<String>,
+    pub sizes: Option<String>,
+    /// Links are deduplicated by their href attribute
+    pub href: Option<String>,
+    pub crossorigin: Option<String>,
+    pub referrerpolicy: Option<String>,
+    pub fetchpriority: Option<String>,
+    pub hreflang: Option<String>,
+    pub integrity: Option<String>,
+    pub r#type: Option<String>,
+    pub blocking: Option<String>,
+    #[props(extends = link, extends = GlobalAttributes)]
+    pub additional_attributes: Vec<Attribute>,
+}
+
+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
+///         document::Link {
+///             href: asset!("./assets/style.css"),
+///             rel: "stylesheet",
+///         }
+///     }
+/// }
+/// ```
+///
+/// <div class="warning">
+///
+/// Any updates to the props after the first render will not be reflected in the head.
+///
+/// </div>
+#[doc(alias = "<link>")]
+#[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);
+    });
+
+    VNode::empty()
+}
+
+#[derive(Default, Clone)]
+struct LinkContext(DeduplicationContext);
+
+fn should_insert_link(href: &str) -> bool {
+    get_or_insert_root_context::<LinkContext>()
+        .0
+        .should_insert(href)
+}

+ 74 - 0
packages/document/src/elements/meta.rs

@@ -0,0 +1,74 @@
+use super::*;
+use crate::document;
+use dioxus_html as dioxus_elements;
+
+#[non_exhaustive]
+/// Props for the [`Meta`] component
+#[derive(Clone, Props, PartialEq)]
+pub struct MetaProps {
+    pub property: Option<String>,
+    pub name: Option<String>,
+    pub charset: Option<String>,
+    pub http_equiv: Option<String>,
+    pub content: Option<String>,
+    #[props(extends = meta, extends = GlobalAttributes)]
+    pub additional_attributes: Vec<Attribute>,
+}
+
+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
+///         document::Meta {
+///             http_equiv: "refresh",
+///             content: "10;url=https://dioxuslabs.com",
+///         }
+///     }
+/// }
+/// ```
+///
+/// <div class="warning">
+///
+/// Any updates to the props after the first render will not be reflected in the head.
+///
+/// </div>
+#[component]
+#[doc(alias = "<meta>")]
+pub fn Meta(props: MetaProps) -> Element {
+    use_update_warning(&props, "Meta {}");
+
+    use_hook(|| {
+        let document = document();
+        document.create_meta(props);
+    });
+
+    VNode::empty()
+}

+ 124 - 0
packages/document/src/elements/mod.rs

@@ -0,0 +1,124 @@
+#![doc = include_str!("../../docs/head.md")]
+
+use std::{cell::RefCell, collections::HashSet, rc::Rc};
+
+use dioxus_core::{prelude::*, DynamicNode};
+use dioxus_core_macro::*;
+
+mod link;
+pub use link::*;
+mod meta;
+pub use meta::*;
+mod script;
+pub use script::*;
+mod style;
+pub use style::*;
+mod title;
+pub use title::*;
+
+/// Warn the user if they try to change props on a element that is injected into the head
+#[allow(unused)]
+fn use_update_warning<T: PartialEq + Clone + 'static>(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 ");
+        }
+    }
+}
+
+/// An error that can occur when extracting a single text node from a component
+pub enum ExtractSingleTextNodeError<'a> {
+    /// The node contained an render error, so we can't extract the text node
+    RenderError(&'a RenderError),
+    /// There was only one child, but it wasn't a text node
+    NonTextNode,
+    /// There is multiple child nodes
+    NonTemplate,
+}
+
+impl ExtractSingleTextNodeError<'_> {
+    /// Log a warning depending on the error
+    pub fn log(&self, component: &str) {
+        match self {
+            ExtractSingleTextNodeError::RenderError(err) => {
+                tracing::error!("Error while rendering {component}: {err}");
+            }
+            ExtractSingleTextNodeError::NonTextNode => {
+                tracing::error!(
+                    "Error while rendering {component}: The children of {component} must be a single text node"
+                );
+            }
+            ExtractSingleTextNodeError::NonTemplate => {
+                tracing::error!(
+                    "Error while rendering {component}: The children of {component} must be a single text node"
+                );
+            }
+        }
+    }
+}
+
+fn extract_single_text_node(children: &Element) -> Result<String, ExtractSingleTextNodeError<'_>> {
+    let vnode = match children {
+        Element::Ok(vnode) => vnode,
+        Element::Err(err) => {
+            return Err(ExtractSingleTextNodeError::RenderError(err));
+        }
+    };
+    // The title's children must be in one of two forms:
+    // 1. rsx! { "static text" }
+    // 2. rsx! { "title: {dynamic_text}" }
+    match vnode.template {
+        // rsx! { "static text" }
+        Template {
+            roots: &[TemplateNode::Text { text }],
+            node_paths: &[],
+            attr_paths: &[],
+            ..
+        } => Ok(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) => Ok(text.value.clone()),
+                _ => Err(ExtractSingleTextNodeError::NonTextNode),
+            }
+        }
+        _ => Err(ExtractSingleTextNodeError::NonTemplate),
+    }
+}
+
+fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
+    match ScopeId::ROOT.has_context::<T>() {
+        Some(context) => context,
+        None => {
+            let context = T::default();
+            ScopeId::ROOT.provide_context(context.clone());
+            context
+        }
+    }
+}
+
+#[derive(Default, Clone)]
+struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
+
+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
+        }
+    }
+}

+ 113 - 0
packages/document/src/elements/script.rs

@@ -0,0 +1,113 @@
+use super::*;
+use crate::document;
+use dioxus_html as dioxus_elements;
+
+#[non_exhaustive]
+#[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<String>,
+    pub defer: Option<bool>,
+    pub crossorigin: Option<String>,
+    pub fetchpriority: Option<String>,
+    pub integrity: Option<String>,
+    pub nomodule: Option<bool>,
+    pub nonce: Option<String>,
+    pub referrerpolicy: Option<String>,
+    pub r#type: Option<String>,
+    #[props(extends = script, extends = GlobalAttributes)]
+    pub additional_attributes: Vec<Attribute>,
+}
+
+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()));
+        }
+        if let Some(src) = &self.src {
+            attributes.push(("src", src.clone()));
+        }
+        attributes
+    }
+
+    pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
+        extract_single_text_node(&self.children)
+    }
+}
+
+/// 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
+///         document::Script {
+///             src: asset!("./assets/script.js"),
+///         }
+///     }
+/// }
+/// ```
+///
+/// <div class="warning">
+///
+/// Any updates to the props after the first render will not be reflected in the head.
+///
+/// </div>
+#[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);
+    });
+
+    VNode::empty()
+}
+
+#[derive(Default, Clone)]
+struct ScriptContext(DeduplicationContext);
+
+fn should_insert_script(src: &str) -> bool {
+    get_or_insert_root_context::<ScriptContext>()
+        .0
+        .should_insert(src)
+}

+ 93 - 0
packages/document/src/elements/style.rs

@@ -0,0 +1,93 @@
+use super::*;
+use crate::document;
+use dioxus_html as dioxus_elements;
+
+#[non_exhaustive]
+#[derive(Clone, Props, PartialEq)]
+pub struct StyleProps {
+    /// Styles are deduplicated by their href attribute
+    pub href: Option<String>,
+    pub media: Option<String>,
+    pub nonce: Option<String>,
+    pub title: Option<String>,
+    /// The contents of the style tag. If present, the children must be a single text node.
+    pub children: Element,
+    #[props(extends = style, extends = GlobalAttributes)]
+    pub additional_attributes: Vec<Attribute>,
+}
+
+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) -> Result<String, ExtractSingleTextNodeError<'_>> {
+        extract_single_text_node(&self.children)
+    }
+}
+
+/// 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
+///         document::Style {
+///             r#"
+///                 body {{
+///                     background-color: red;
+///                 }}
+///             "#
+///         }
+///     }
+/// }
+/// ```
+///
+/// <div class="warning">
+///
+/// Any updates to the props after the first render will not be reflected in the head.
+///
+/// </div>
+#[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);
+    });
+
+    VNode::empty()
+}
+
+#[derive(Default, Clone)]
+struct StyleContext(DeduplicationContext);
+
+fn should_insert_style(href: &str) -> bool {
+    get_or_insert_root_context::<StyleContext>()
+        .0
+        .should_insert(href)
+}

+ 57 - 0
packages/document/src/elements/title.rs

@@ -0,0 +1,57 @@
+use crate::document;
+
+use super::*;
+
+#[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
+///         document::Title { "My Page" }
+///     }
+/// }
+/// ```
+#[component]
+#[doc(alias = "<title>")]
+pub fn Title(props: TitleProps) -> Element {
+    let children = props.children;
+    let text = match extract_single_text_node(&children) {
+        Ok(text) => text,
+        Err(err) => {
+            err.log("Title");
+            return VNode::empty();
+        }
+    };
+
+    // 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;
+    }
+
+    VNode::empty()
+}

+ 36 - 0
packages/document/src/error.rs

@@ -0,0 +1,36 @@
+use std::error::Error;
+use std::fmt::Display;
+
+/// Represents an error when evaluating JavaScript
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum EvalError {
+    /// The platform does not support evaluating JavaScript.
+    Unsupported,
+
+    /// The provided JavaScript has already been ran.
+    Finished,
+
+    /// The provided JavaScript is not valid and can't be ran.
+    InvalidJs(String),
+
+    /// Represents an error communicating between JavaScript and Rust.
+    Communication(String),
+
+    /// Represents an error serializing or deserializing the result of an eval
+    Serialization(serde_json::Error),
+}
+
+impl Display for EvalError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            EvalError::Unsupported => write!(f, "EvalError::Unsupported - eval is not supported on the current platform"),
+            EvalError::Finished => write!(f, "EvalError::Finished - eval has already ran"),
+            EvalError::InvalidJs(_) => write!(f, "EvalError::InvalidJs - the provided javascript is invalid"),
+            EvalError::Communication(_) => write!(f, "EvalError::Communication - there was an error trying to communicate with between javascript and rust"),
+            EvalError::Serialization(_) => write!(f, "EvalError::Serialization - there was an error trying to serialize or deserialize the result of an eval"),
+        }
+    }
+}
+
+impl Error for EvalError {}

+ 74 - 0
packages/document/src/eval.rs

@@ -0,0 +1,74 @@
+#![doc = include_str!("../docs/eval.md")]
+
+use crate::error::EvalError;
+use generational_box::GenerationalBox;
+use std::future::{poll_fn, Future, IntoFuture};
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+#[doc = include_str!("../docs/eval.md")]
+pub struct Eval {
+    evaluator: GenerationalBox<Box<dyn Evaluator>>,
+}
+
+impl Eval {
+    /// Create this eval from a dynamic evaluator
+    pub fn new(evaluator: GenerationalBox<Box<dyn Evaluator + 'static>>) -> Self {
+        Self { evaluator }
+    }
+
+    /// Wait until the javascript task is finished and return the result
+    pub async fn join<T: serde::de::DeserializeOwned>(self) -> Result<T, EvalError> {
+        let json_value = poll_fn(|cx| match self.evaluator.try_write() {
+            Ok(mut evaluator) => evaluator.poll_join(cx),
+            Err(_) => Poll::Ready(Err(EvalError::Finished)),
+        })
+        .await?;
+        serde_json::from_value(json_value).map_err(EvalError::Serialization)
+    }
+
+    /// Send a message to the javascript task
+    pub fn send(&self, data: impl serde::Serialize) -> Result<(), EvalError> {
+        match self.evaluator.try_read() {
+            Ok(evaluator) => {
+                evaluator.send(serde_json::to_value(data).map_err(EvalError::Serialization)?)
+            }
+            Err(_) => Err(EvalError::Finished),
+        }
+    }
+
+    /// Receive a message from the javascript task
+    pub async fn recv<T: serde::de::DeserializeOwned>(&mut self) -> Result<T, EvalError> {
+        let json_value = poll_fn(|cx| match self.evaluator.try_write() {
+            Ok(mut evaluator) => evaluator.poll_recv(cx),
+            Err(_) => Poll::Ready(Err(EvalError::Finished)),
+        })
+        .await?;
+        serde_json::from_value(json_value).map_err(EvalError::Serialization)
+    }
+}
+
+impl IntoFuture for Eval {
+    type Output = Result<serde_json::Value, EvalError>;
+    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        Box::pin(self.join().into_future())
+    }
+}
+
+/// The platform's evaluator.
+pub trait Evaluator {
+    /// Sends a message to the evaluated JavaScript.
+    fn send(&self, data: serde_json::Value) -> Result<(), EvalError>;
+    /// Receive any queued messages from the evaluated JavaScript.
+    fn poll_recv(
+        &mut self,
+        context: &mut Context<'_>,
+    ) -> Poll<Result<serde_json::Value, EvalError>>;
+    /// Gets the return value of the JavaScript
+    fn poll_join(
+        &mut self,
+        context: &mut Context<'_>,
+    ) -> Poll<Result<serde_json::Value, EvalError>>;
+}

+ 0 - 0
packages/html/src/js/hash.txt → packages/document/src/js/hash.txt


+ 0 - 0
packages/html/src/js/head.js → packages/document/src/js/head.js


+ 31 - 0
packages/document/src/lib.rs

@@ -0,0 +1,31 @@
+use std::rc::Rc;
+
+mod document;
+mod elements;
+mod error;
+mod eval;
+
+pub use document::*;
+pub use elements::*;
+pub use error::*;
+pub use eval::*;
+
+/// Get the document provider for the current platform or a no-op provider if the platform doesn't document functionality.
+pub fn document() -> Rc<dyn Document> {
+    match dioxus_core::prelude::try_consume_context::<Rc<dyn Document>>() {
+        Some(document) => document,
+        None => {
+            tracing::error!(
+                "Unable to find a document in the renderer. Using the default no-op document."
+            );
+            Rc::new(NoOpDocument)
+        }
+    }
+}
+
+/// Evaluate some javascript in the current document
+#[doc = include_str!("../docs/eval.md")]
+#[doc(alias = "javascript")]
+pub fn eval(script: &str) -> Eval {
+    document().eval(script.to_string())
+}

+ 0 - 0
packages/html/src/ts/.gitignore → packages/document/src/ts/.gitignore


+ 0 - 0
packages/html/src/ts/eval.ts → packages/document/src/ts/eval.ts


+ 0 - 0
packages/html/src/ts/head.ts → packages/document/src/ts/head.ts


+ 18 - 0
packages/document/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"
+    ]
+}

+ 4 - 9
packages/fullstack/src/document/server.rs

@@ -4,9 +4,8 @@
 
 use std::cell::RefCell;
 
-use dioxus_lib::{html::document::*, prelude::*};
+use dioxus_lib::{document::*, prelude::*};
 use dioxus_ssr::Renderer;
-use generational_box::GenerationalBox;
 use once_cell::sync::Lazy;
 use parking_lot::RwLock;
 
@@ -71,8 +70,8 @@ impl ServerDocument {
 }
 
 impl Document for ServerDocument {
-    fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
-        NoOpDocument.new_evaluator(js)
+    fn eval(&self, js: String) -> Eval {
+        NoOpDocument.eval(js)
     }
 
     fn set_title(&self, title: String) {
@@ -151,7 +150,7 @@ impl Document for ServerDocument {
         }
     }
 
-    fn create_link(&self, props: head::LinkProps) {
+    fn create_link(&self, props: LinkProps) {
         self.warn_if_streaming();
         self.serialize_for_hydration();
         self.0.borrow_mut().link.push(rsx! {
@@ -173,8 +172,4 @@ impl Document for ServerDocument {
             }
         })
     }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
-    }
 }

+ 7 - 14
packages/fullstack/src/document/web.rs

@@ -1,7 +1,7 @@
 #![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_lib::document::*;
 use dioxus_web::WebDocument;
 
 fn head_element_written_on_server() -> bool {
@@ -15,11 +15,8 @@ fn head_element_written_on_server() -> bool {
 pub struct FullstackWebDocument;
 
 impl Document for FullstackWebDocument {
-    fn new_evaluator(
-        &self,
-        js: String,
-    ) -> generational_box::GenerationalBox<Box<dyn dioxus_lib::prelude::document::Evaluator>> {
-        WebDocument.new_evaluator(js)
+    fn eval(&self, js: String) -> Eval {
+        WebDocument.eval(js)
     }
 
     fn set_title(&self, title: String) {
@@ -29,35 +26,31 @@ impl Document for FullstackWebDocument {
         WebDocument.set_title(title);
     }
 
-    fn create_meta(&self, props: dioxus_lib::prelude::MetaProps) {
+    fn create_meta(&self, props: MetaProps) {
         if head_element_written_on_server() {
             return;
         }
         WebDocument.create_meta(props);
     }
 
-    fn create_script(&self, props: dioxus_lib::prelude::ScriptProps) {
+    fn create_script(&self, props: ScriptProps) {
         if head_element_written_on_server() {
             return;
         }
         WebDocument.create_script(props);
     }
 
-    fn create_style(&self, props: dioxus_lib::prelude::StyleProps) {
+    fn create_style(&self, props: StyleProps) {
         if head_element_written_on_server() {
             return;
         }
         WebDocument.create_style(props);
     }
 
-    fn create_link(&self, props: dioxus_lib::prelude::head::LinkProps) {
+    fn create_link(&self, props: LinkProps) {
         if head_element_written_on_server() {
             return;
         }
         WebDocument.create_link(props);
     }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
-    }
 }

+ 5 - 8
packages/fullstack/src/render.rs

@@ -1,7 +1,9 @@
 //! A shared pool of renderers for efficient server side rendering.
+use crate::document::ServerDocument;
 use crate::streaming::{Mount, StreamingRenderer};
 use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
 use dioxus_isrg::{CachedRender, RenderFreshness};
+use dioxus_lib::document::Document;
 use dioxus_ssr::Renderer;
 use futures_channel::mpsc::Sender;
 use futures_util::{Stream, StreamExt};
@@ -165,6 +167,7 @@ impl SsrRendererPool {
         let join_handle = spawn_platform(move || async move {
             let mut virtual_dom = virtual_dom_factory();
             let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
+            virtual_dom.provide_root_context(document.clone());
             virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
 
             // poll the future, which may call server_context()
@@ -431,11 +434,8 @@ impl FullstackHTMLTemplate {
         let ServeConfig { index, .. } = &self.cfg;
 
         let title = {
-            let document: Option<std::rc::Rc<dyn Document>> =
+            let document: Option<std::rc::Rc<ServerDocument>> =
                 virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
-            let document: Option<&crate::document::server::ServerDocument> = document
-                .as_ref()
-                .and_then(|document| document.as_any().downcast_ref());
             // Collect any head content from the document provider and inject that into the head
             document.and_then(|document| document.title())
         };
@@ -448,11 +448,8 @@ impl FullstackHTMLTemplate {
         }
         to.write_str(&index.head_after_title)?;
 
-        let document: Option<std::rc::Rc<dyn dioxus_lib::prelude::document::Document>> =
+        let document: Option<std::rc::Rc<ServerDocument>> =
             virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
-        let document: Option<&crate::document::server::ServerDocument> = document
-            .as_ref()
-            .and_then(|document| document.as_any().downcast_ref());
         if let Some(document) = document {
             // Collect any head content from the document provider and inject that into the head
             document.render(to)?;

+ 1 - 1
packages/hooks/docs/side_effects.md

@@ -16,7 +16,7 @@ fn MyComponent() -> Element {
         let count = count.read();
 
         // You can use the count value to update the DOM manually
-        eval(&format!(
+        document::eval(&format!(
             r#"var c = document.getElementById("dioxus-canvas");
 var ctx = c.getContext("2d");
 ctx.font = "30px Arial";

+ 1 - 5
packages/html/Cargo.toml

@@ -41,7 +41,7 @@ tokio = { workspace = true, features = ["time"] }
 manganis = { workspace = true }
 
 [features]
-default = ["serialize", "mounted", "document", "file_engine"]
+default = ["serialize", "mounted", "file_engine"]
 serialize = [
     "dep:serde",
     "dep:serde_json",
@@ -51,10 +51,6 @@ serialize = [
     "dioxus-core/serialize"
 ]
 mounted = []
-document = [
-    "dep:serde",
-    "dep:serde_json"
-]
 file_engine = [
     "dep:async-trait",
 ]

+ 0 - 0
packages/html/assets/script.js


+ 0 - 0
packages/html/assets/style.css


+ 0 - 134
packages/html/src/document/eval.rs

@@ -1,134 +0,0 @@
-#![allow(clippy::await_holding_refcell_ref)]
-#![doc = include_str!("../../docs/eval.md")]
-
-use dioxus_core::prelude::*;
-use generational_box::GenerationalBox;
-use std::error::Error;
-use std::fmt::Display;
-use std::future::{poll_fn, Future, IntoFuture};
-use std::pin::Pin;
-use std::rc::Rc;
-use std::task::{Context, Poll};
-
-use super::document;
-
-/// The platform's evaluator.
-pub trait Evaluator {
-    /// Sends a message to the evaluated JavaScript.
-    fn send(&self, data: serde_json::Value) -> Result<(), EvalError>;
-    /// Receive any queued messages from the evaluated JavaScript.
-    fn poll_recv(
-        &mut self,
-        context: &mut Context<'_>,
-    ) -> Poll<Result<serde_json::Value, EvalError>>;
-    /// Gets the return value of the JavaScript
-    fn poll_join(
-        &mut self,
-        context: &mut Context<'_>,
-    ) -> Poll<Result<serde_json::Value, EvalError>>;
-}
-
-type EvalCreator = Rc<dyn Fn(&str) -> UseEval>;
-
-/// Get a struct that can execute any JavaScript.
-///
-/// # Safety
-///
-/// Please be very careful with this function. A script with too many dynamic
-/// parts is practically asking for a hacker to find an XSS vulnerability in
-/// it. **This applies especially to web targets, where the JavaScript context
-/// has access to most, if not all of your application data.**
-#[must_use]
-pub fn eval_provider() -> EvalCreator {
-    let eval_provider = document();
-
-    Rc::new(move |script: &str| UseEval::new(eval_provider.new_evaluator(script.to_string())))
-        as Rc<dyn Fn(&str) -> UseEval>
-}
-
-#[doc = include_str!("../../docs/eval.md")]
-#[doc(alias = "javascript")]
-pub fn eval(script: &str) -> UseEval {
-    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")]
-#[derive(Clone, Copy)]
-pub struct UseEval {
-    evaluator: GenerationalBox<Box<dyn Evaluator>>,
-}
-
-impl UseEval {
-    /// Creates a new UseEval
-    pub fn new(evaluator: GenerationalBox<Box<dyn Evaluator + 'static>>) -> Self {
-        Self { evaluator }
-    }
-
-    /// Sends a [`serde_json::Value`] to the evaluated JavaScript.
-    pub fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
-        match self.evaluator.try_read() {
-            Ok(evaluator) => evaluator.send(data),
-            Err(_) => Err(EvalError::Finished),
-        }
-    }
-
-    /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
-    pub async fn recv(&mut self) -> Result<serde_json::Value, EvalError> {
-        poll_fn(|cx| match self.evaluator.try_write() {
-            Ok(mut evaluator) => evaluator.poll_recv(cx),
-            Err(_) => Poll::Ready(Err(EvalError::Finished)),
-        })
-        .await
-    }
-
-    /// Gets the return value of the evaluated JavaScript.
-    pub async fn join(self) -> Result<serde_json::Value, EvalError> {
-        poll_fn(|cx| match self.evaluator.try_write() {
-            Ok(mut evaluator) => evaluator.poll_join(cx),
-            Err(_) => Poll::Ready(Err(EvalError::Finished)),
-        })
-        .await
-    }
-}
-
-impl IntoFuture for UseEval {
-    type Output = Result<serde_json::Value, EvalError>;
-    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
-
-    fn into_future(self) -> Self::IntoFuture {
-        Box::pin(self.join())
-    }
-}
-
-/// Represents an error when evaluating JavaScript
-#[derive(Debug)]
-#[non_exhaustive]
-pub enum EvalError {
-    /// The platform does not support evaluating JavaScript.
-    Unsupported,
-
-    /// The provided JavaScript has already been ran.
-    Finished,
-
-    /// The provided JavaScript is not valid and can't be ran.
-    InvalidJs(String),
-
-    /// Represents an error communicating between JavaScript and Rust.
-    Communication(String),
-}
-
-impl Display for EvalError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            EvalError::Unsupported => write!(f, "EvalError::Unsupported - eval is not supported on the current platform"),
-            EvalError::Finished => write!(f, "EvalError::Finished - eval has already ran"),
-            EvalError::InvalidJs(_) => write!(f, "EvalError::InvalidJs - the provided javascript is invalid"),
-            EvalError::Communication(_) => write!(f, "EvalError::Communication - there was an error trying to communicate with between javascript and rust"),
-        }
-    }
-}
-
-impl Error for EvalError {}

+ 0 - 562
packages/html/src/document/head.rs

@@ -1,562 +0,0 @@
-#![doc = include_str!("../../docs/head.md")]
-
-use std::{cell::RefCell, collections::HashSet, rc::Rc};
-
-use crate as dioxus_elements;
-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<T: PartialEq + Clone + 'static>(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 ");
-        }
-    }
-}
-
-/// An error that can occur when extracting a single text node from a component
-pub enum ExtractSingleTextNodeError<'a> {
-    /// The node contained an render error, so we can't extract the text node
-    RenderError(&'a RenderError),
-    /// There was only one child, but it wasn't a text node
-    NonTextNode,
-    /// There is multiple child nodes
-    NonTemplate,
-}
-
-impl ExtractSingleTextNodeError<'_> {
-    /// Log a warning depending on the error
-    pub fn log(&self, component: &str) {
-        match self {
-            ExtractSingleTextNodeError::RenderError(err) => {
-                tracing::error!("Error while rendering {component}: {err}");
-            }
-            ExtractSingleTextNodeError::NonTextNode => {
-                tracing::error!(
-                    "Error while rendering {component}: The children of {component} must be a single text node"
-                );
-            }
-            ExtractSingleTextNodeError::NonTemplate => {
-                tracing::error!(
-                    "Error while rendering {component}: The children of {component} must be a single text node"
-                );
-            }
-        }
-    }
-}
-
-fn extract_single_text_node(children: &Element) -> Result<String, ExtractSingleTextNodeError<'_>> {
-    let vnode = match children {
-        Element::Ok(vnode) => vnode,
-        Element::Err(err) => {
-            return Err(ExtractSingleTextNodeError::RenderError(err));
-        }
-    };
-    // The title's children must be in one of two forms:
-    // 1. rsx! { "static text" }
-    // 2. rsx! { "title: {dynamic_text}" }
-    match vnode.template {
-        // rsx! { "static text" }
-        Template {
-            roots: &[TemplateNode::Text { text }],
-            node_paths: &[],
-            attr_paths: &[],
-            ..
-        } => Ok(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) => Ok(text.value.clone()),
-                _ => Err(ExtractSingleTextNodeError::NonTextNode),
-            }
-        }
-        _ => Err(ExtractSingleTextNodeError::NonTemplate),
-    }
-}
-
-#[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 text = match extract_single_text_node(&children) {
-        Ok(text) => text,
-        Err(err) => {
-            err.log("Title");
-            return VNode::empty();
-        }
-    };
-
-    // 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;
-    }
-
-    VNode::empty()
-}
-
-#[non_exhaustive]
-/// Props for the [`Meta`] component
-#[derive(Clone, Props, PartialEq)]
-pub struct MetaProps {
-    pub property: Option<String>,
-    pub name: Option<String>,
-    pub charset: Option<String>,
-    pub http_equiv: Option<String>,
-    pub content: Option<String>,
-    #[props(extends = meta, extends = GlobalAttributes)]
-    pub additional_attributes: Vec<Attribute>,
-}
-
-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",
-///         }
-///     }
-/// }
-/// ```
-///
-/// <div class="warning">
-///
-/// Any updates to the props after the first render will not be reflected in the head.
-///
-/// </div>
-#[component]
-pub fn Meta(props: MetaProps) -> Element {
-    use_update_warning(&props, "Meta {}");
-
-    use_hook(|| {
-        let document = document();
-        document.create_meta(props);
-    });
-
-    VNode::empty()
-}
-
-#[non_exhaustive]
-#[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<String>,
-    pub defer: Option<bool>,
-    pub crossorigin: Option<String>,
-    pub fetchpriority: Option<String>,
-    pub integrity: Option<String>,
-    pub nomodule: Option<bool>,
-    pub nonce: Option<String>,
-    pub referrerpolicy: Option<String>,
-    pub r#type: Option<String>,
-    #[props(extends = script, extends = GlobalAttributes)]
-    pub additional_attributes: Vec<Attribute>,
-}
-
-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()));
-        }
-        if let Some(src) = &self.src {
-            attributes.push(("src", src.clone()));
-        }
-        attributes
-    }
-
-    pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
-        extract_single_text_node(&self.children)
-    }
-}
-
-/// 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: asset!("./assets/script.js"),
-///         }
-///     }
-/// }
-/// ```
-///
-/// <div class="warning">
-///
-/// Any updates to the props after the first render will not be reflected in the head.
-///
-/// </div>
-#[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);
-    });
-
-    VNode::empty()
-}
-
-#[non_exhaustive]
-#[derive(Clone, Props, PartialEq)]
-pub struct StyleProps {
-    /// Styles are deduplicated by their href attribute
-    pub href: Option<String>,
-    pub media: Option<String>,
-    pub nonce: Option<String>,
-    pub title: Option<String>,
-    /// The contents of the style tag. If present, the children must be a single text node.
-    pub children: Element,
-    #[props(extends = style, extends = GlobalAttributes)]
-    pub additional_attributes: Vec<Attribute>,
-}
-
-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) -> Result<String, ExtractSingleTextNodeError<'_>> {
-        extract_single_text_node(&self.children)
-    }
-}
-
-/// 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;
-///                 }}
-///             "#
-///         }
-///     }
-/// }
-/// ```
-///
-/// <div class="warning">
-///
-/// Any updates to the props after the first render will not be reflected in the head.
-///
-/// </div>
-#[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);
-    });
-
-    VNode::empty()
-}
-
-use super::*;
-
-#[non_exhaustive]
-#[derive(Clone, Props, PartialEq)]
-pub struct LinkProps {
-    pub rel: Option<String>,
-    pub media: Option<String>,
-    pub title: Option<String>,
-    pub disabled: Option<bool>,
-    pub r#as: Option<String>,
-    pub sizes: Option<String>,
-    /// Links are deduplicated by their href attribute
-    pub href: Option<String>,
-    pub crossorigin: Option<String>,
-    pub referrerpolicy: Option<String>,
-    pub fetchpriority: Option<String>,
-    pub hreflang: Option<String>,
-    pub integrity: Option<String>,
-    pub r#type: Option<String>,
-    pub blocking: Option<String>,
-    #[props(extends = link, extends = GlobalAttributes)]
-    pub additional_attributes: Vec<Attribute>,
-}
-
-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: asset!("./assets/style.css"),
-///             rel: "stylesheet",
-///         }
-///     }
-/// }
-/// ```
-///
-/// <div class="warning">
-///
-/// Any updates to the props after the first render will not be reflected in the head.
-///
-/// </div>
-#[doc(alias = "<link>")]
-#[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);
-    });
-
-    VNode::empty()
-}
-
-fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
-    match ScopeId::ROOT.has_context::<T>() {
-        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::<LinkContext>()
-        .0
-        .should_insert(href)
-}
-
-#[derive(Default, Clone)]
-struct ScriptContext(DeduplicationContext);
-
-fn should_insert_script(src: &str) -> bool {
-    get_or_insert_root_context::<ScriptContext>()
-        .0
-        .should_insert(src)
-}
-
-#[derive(Default, Clone)]
-struct StyleContext(DeduplicationContext);
-
-fn should_insert_style(href: &str) -> bool {
-    get_or_insert_root_context::<StyleContext>()
-        .0
-        .should_insert(href)
-}
-
-#[derive(Default, Clone)]
-struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
-
-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
-        }
-    }
-}

+ 0 - 162
packages/html/src/document/mod.rs

@@ -1,162 +0,0 @@
-// 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};
-
-#[allow(unused)]
-mod eval;
-pub use eval::*;
-
-pub mod head;
-pub use head::{Meta, MetaProps, Script, ScriptProps, Style, StyleProps, Title, TitleProps};
-
-fn format_string_for_js(s: &str) -> String {
-    let escaped = s
-        .replace('\\', "\\\\")
-        .replace('\n', "\\n")
-        .replace('\r', "\\r")
-        .replace('"', "\\\"");
-    format!("\"{escaped}\"")
-}
-
-fn format_attributes(attributes: &[(&str, String)]) -> String {
-    let mut formatted = String::from("[");
-    for (key, value) in attributes {
-        formatted.push_str(&format!(
-            "[{}, {}],",
-            format_string_for_js(key),
-            format_string_for_js(value)
-        ));
-    }
-    if formatted.ends_with(',') {
-        formatted.pop();
-    }
-    formatted.push(']');
-    formatted
-}
-
-fn create_element_in_head(
-    tag: &str,
-    attributes: &[(&str, String)],
-    children: Option<String>,
-) -> String {
-    let helpers = include_str!("../js/head.js");
-    let attributes = format_attributes(attributes);
-    let children = children
-        .as_deref()
-        .map(format_string_for_js)
-        .unwrap_or("null".to_string());
-    let tag = format_string_for_js(tag);
-    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<Box<dyn Evaluator>>;
-
-    /// Set the title of the document
-    fn set_title(&self, title: String) {
-        self.new_evaluator(format!("document.title = {title:?};"));
-    }
-
-    /// Create a new meta tag
-    fn create_meta(&self, props: MetaProps) {
-        let attributes = props.attributes();
-        let js = create_element_in_head("meta", &attributes, None);
-        self.new_evaluator(js);
-    }
-
-    /// Create a new script tag
-    fn create_script(&self, props: ScriptProps) {
-        let attributes = props.attributes();
-        let js = match (&props.src, props.script_contents()) {
-            // The script has inline contents, render it as a script tag
-            (_, Ok(contents)) => create_element_in_head("script", &attributes, Some(contents)),
-            // The script has a src, render it as a script tag without a body
-            (Some(_), _) => create_element_in_head("script", &attributes, None),
-            // The script has neither contents nor src, log an error
-            (None, Err(err)) => {
-                err.log("Script");
-                return;
-            }
-        };
-        self.new_evaluator(js);
-    }
-
-    /// Create a new style tag
-    fn create_style(&self, props: StyleProps) {
-        let mut attributes = props.attributes();
-        let js = match (&props.href, props.style_contents()) {
-            // The style has inline contents, render it as a style tag
-            (_, Ok(contents)) => create_element_in_head("style", &attributes, Some(contents)),
-            // The style has a src, render it as a link tag
-            (Some(_), _) => {
-                attributes.push(("type", "text/css".into()));
-                create_element_in_head("link", &attributes, None)
-            }
-            // The style has neither contents nor src, log an error
-            (None, Err(err)) => {
-                err.log("Style");
-                return;
-            }
-        };
-        self.new_evaluator(js);
-    }
-
-    /// Create a new link tag
-    fn create_link(&self, props: head::LinkProps) {
-        let attributes = props.attributes();
-        let js = create_element_in_head("link", &attributes, None);
-        self.new_evaluator(js);
-    }
-
-    /// Get a reference to the document as `dyn Any`
-    fn as_any(&self) -> &dyn std::any::Any;
-}
-
-/// The default No-Op document
-pub struct NoOpDocument;
-
-impl Document for NoOpDocument {
-    fn new_evaluator(&self, _js: String) -> GenerationalBox<Box<dyn Evaluator>> {
-        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))
-    }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
-    }
-}
-
-/// The default No-Op evaluator
-pub 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<Result<serde_json::Value, EvalError>> {
-        Poll::Ready(Err(EvalError::Unsupported))
-    }
-    fn poll_join(
-        &mut self,
-        _context: &mut Context<'_>,
-    ) -> Poll<Result<serde_json::Value, EvalError>> {
-        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<dyn Document> {
-    dioxus_core::prelude::try_consume_context::<Rc<dyn Document>>()
-        // 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<dyn Document>)
-}

+ 0 - 8
packages/html/src/lib.rs

@@ -41,9 +41,6 @@ pub use elements::*;
 pub use events::*;
 pub use render_template::*;
 
-#[cfg(feature = "document")]
-pub mod document;
-
 pub mod extensions {
     pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
     pub use crate::elements::extensions::*;
@@ -51,11 +48,6 @@ 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::*;
     pub use crate::events::*;
     pub use crate::point_interaction::*;

+ 2 - 1
packages/liveview/Cargo.toml

@@ -22,7 +22,8 @@ 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", "document", "mounted"] }
+dioxus-html = { workspace = true, features = ["serialize", "mounted"] }
+dioxus-document = { workspace = true }
 rustc-hash = { workspace = true }
 dioxus-core = { workspace = true, features = ["serialize"] }
 dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }

+ 3 - 7
packages/liveview/src/eval.rs

@@ -1,5 +1,5 @@
 use dioxus_core::ScopeId;
-use dioxus_html::document::{Document, EvalError, Evaluator};
+use dioxus_document::{Document, Eval, EvalError, Evaluator};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use std::rc::Rc;
 
@@ -18,12 +18,8 @@ pub struct LiveviewDocument {
 }
 
 impl Document for LiveviewDocument {
-    fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
-        LiveviewEvaluator::create(self.query.clone(), js)
-    }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
+    fn eval(&self, js: String) -> Eval {
+        Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
     }
 }
 

+ 1 - 1
packages/playwright-tests/fullstack/src/main.rs

@@ -17,7 +17,7 @@ fn app() -> Element {
 
     rsx! {
         h1 { "hello axum! {count}" }
-        Title { "hello axum! {count}" }
+        document::Title { "hello axum! {count}" }
         button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
         button {
             class: "server-button",

+ 1 - 1
packages/playwright-tests/nested-suspense/src/main.rs

@@ -44,7 +44,7 @@ fn LoadTitle() -> Element {
         .unwrap();
 
     rsx! {
-        Title { "{title.title}" }
+        document::Title { "{title.title}" }
     }
 }
 

+ 5 - 5
packages/playwright-tests/web/src/main.rs

@@ -9,7 +9,7 @@ fn app() -> Element {
     rsx! {
         div {
             "hello axum! {num}"
-            Title { "hello axum! {num}" }
+            document::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" } }
@@ -24,7 +24,7 @@ fn app() -> Element {
         button {
             class: "eval-button",
             onclick: move |_| async move {
-                let mut eval = eval(
+                let mut eval = document::eval(
                     r#"
                         window.document.title = 'Hello from Dioxus Eval!';
                         // Receive and multiply 10 numbers
@@ -38,9 +38,9 @@ fn app() -> Element {
 
                 // Send 10 numbers
                 for i in 0..10 {
-                    eval.send(serde_json::Value::from(i)).unwrap();
-                    let value = eval.recv().await.unwrap();
-                    assert_eq!(value, serde_json::Value::from(i * 2));
+                    eval.send(i).unwrap();
+                    let value: i32 = eval.recv().await.unwrap();
+                    assert_eq!(value, i * 2);
                 }
 
                 let result = eval.recv().await;

+ 4 - 5
packages/router/src/history/liveview.rs

@@ -1,7 +1,7 @@
 use super::HistoryProvider;
 use crate::routable::Routable;
+use dioxus_lib::document::Eval;
 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};
@@ -168,11 +168,10 @@ where
         let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
             Arc::new(RwLock::new(Arc::new(|| {})));
 
-        let eval_provider = document();
+        let eval_provider = dioxus_lib::document::document();
 
-        let create_eval = Rc::new(move |script: &str| {
-            UseEval::new(eval_provider.new_evaluator(script.to_string()))
-        }) as Rc<dyn Fn(&str) -> UseEval>;
+        let create_eval = Rc::new(move |script: &str| eval_provider.eval(script.to_string()))
+            as Rc<dyn Fn(&str) -> Eval>;
 
         // Listen to server actions
         spawn({

+ 1 - 0
packages/static-generation/src/ssg.rs

@@ -1,4 +1,5 @@
 use dioxus_isrg::*;
+use dioxus_lib::document::Document;
 use dioxus_lib::prelude::*;
 use dioxus_router::prelude::*;
 use dioxus_ssr::renderer;

+ 2 - 1
packages/web/Cargo.toml

@@ -13,6 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 dioxus-core = { workspace = true }
 dioxus-core-types = { workspace = true }
 dioxus-html = { workspace = true }
+dioxus-document = { workspace = true }
 dioxus-devtools = { workspace = true }
 dioxus-signals = { workspace = true }
 dioxus-interpreter-js = { workspace = true, features = [
@@ -100,7 +101,7 @@ file_engine = [
     "web-sys/FileReader"
 ]
 devtools = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location", "dep:serde_json", "dep:serde", "dioxus-core/serialize"]
-document = ["dioxus-html/document", "dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"]
+document = ["dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"]
 
 [dev-dependencies]
 dioxus = { workspace = true, default-features = true }

+ 1 - 1
packages/web/src/devtools.rs

@@ -8,7 +8,7 @@ use std::time::Duration;
 
 use dioxus_core::ScopeId;
 use dioxus_devtools::{DevserverMsg, HotReloadMsg};
-use dioxus_html::prelude::eval;
+use dioxus_document::eval;
 use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use js_sys::JsString;
 use wasm_bindgen::JsCast;

+ 6 - 14
packages/web/src/document.rs

@@ -1,5 +1,5 @@
 use dioxus_core::ScopeId;
-use dioxus_html::document::{Document, EvalError, Evaluator, NoOpEvaluator};
+use dioxus_document::{Document, Eval, EvalError, Evaluator};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use js_sys::Function;
 use serde::Serialize;
@@ -64,12 +64,8 @@ pub fn init_document() {
 /// The web-target's document provider.
 pub struct WebDocument;
 impl Document for WebDocument {
-    fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
-        WebEvaluator::create(js)
-    }
-
-    fn as_any(&self) -> &dyn std::any::Any {
-        self
+    fn eval(&self, js: String) -> Eval {
+        Eval::new(WebEvaluator::create(js))
     }
 }
 
@@ -95,10 +91,8 @@ impl WebEvaluator {
     fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
         let owner = UnsyncStorage::owner();
 
-        let generational_box = owner.insert(Box::new(NoOpEvaluator) as Box<dyn Evaluator>);
-
         // add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
-        let channels = WebDioxusChannel::new(JSOwner::new(owner));
+        let channels = WebDioxusChannel::new(JSOwner::new(owner.clone()));
 
         // The Rust side of the channel is a weak reference to the DioxusChannel
         let weak_channels = channels.weak();
@@ -131,13 +125,11 @@ impl WebEvaluator {
             )),
         };
 
-        generational_box.set(Box::new(Self {
+        owner.insert(Box::new(Self {
             channels: weak_channels,
             result: Some(result),
             next_future: None,
-        }) as Box<dyn Evaluator>);
-
-        generational_box
+        }) as Box<dyn Evaluator>)
     }
 }
 

+ 1 - 1
packages/web/src/js/hash.txt

@@ -1 +1 @@
-[3479327739946104450]
+[1614426347475783279]

+ 1 - 1
packages/web/src/ts/eval.ts

@@ -2,7 +2,7 @@ import {
   DioxusChannel,
   Channel,
   WeakDioxusChannel,
-} from "../../../html/src/ts/eval";
+} from "../../../document/src/ts/eval";
 
 export class WebDioxusChannel extends DioxusChannel {
   js_to_rust: Channel;