switch to a Document trait and introduce Script/Head/Style/Meta components (#2635)

* switch to a Document trait and introduce Script/Head/Style/Meta components

* Fix desktop title

* Insert special elements into the head during the inital SSR render

* Make all head component attributes optional

* hydrate head elements

* improve the server streaming head warning

* Document fullstack head hydration approach

* deduplicate head elements by href

* move Link into head::Link

* document head components

* add meta and title examples

* Fix a few doc examples

* fix formatting

* Add title to playwright tests

* serde is optional on web, but it is enabled by hydrate

* remove leftover console log
This commit is contained in:
Evan Almloff 2024-07-18 03:54:03 +02:00 committed by GitHub
parent a490a94ae5
commit 176e67e5b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 1703 additions and 495 deletions

484
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,12 +30,17 @@ members = [
"packages/fullstack",
"packages/server-macro",
"packages/static-generation",
"packages/lazy-js-bundle",
# Fullstack examples
"packages/fullstack/examples/axum-hello-world",
"packages/fullstack/examples/axum-router",
"packages/fullstack/examples/axum-streaming",
"packages/fullstack/examples/axum-desktop",
"packages/fullstack/examples/axum-auth",
"packages/fullstack/examples/hackernews",
# Static generation examples
"packages/static-generation/examples/simple",
"packages/static-generation/examples/router",
"packages/static-generation/examples/github-pages",
@ -85,6 +90,7 @@ dioxus-hot-reload = { path = "packages/hot-reload", version = "0.5.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.5.0" }
dioxus-static-site-generation = { path = "packages/static-generation", version = "0.5.0" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0", default-features = false }
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.5.0" }
tracing = "0.1.37"
tracing-futures = "0.2.5"
toml = "0.8"
@ -173,6 +179,7 @@ reqwest = { version = "0.11.9", features = ["json"], optional = true }
http-range = { version = "0.1.5", optional = true }
ciborium = { version = "0.2.1", optional = true }
base64 = { version = "0.21.0", optional = true }
tracing-subscriber = "0.3.17"
[dev-dependencies]
dioxus = { workspace = true, features = ["router"] }

36
examples/meta.rs Normal file
View file

@ -0,0 +1,36 @@
//! This example shows how to add metadata to the page with the Meta component
use dioxus::prelude::*;
fn main() {
tracing_subscriber::fmt::init();
launch(app);
}
fn app() -> Element {
rsx! {
// You can use the Meta component to render a meta tag into the head of the page
// Meta tags are useful to provide information about the page to search engines and social media sites
// This example sets up meta tags for the open graph protocol for social media previews
Meta {
property: "og:title",
content: "My Site",
}
Meta {
property: "og:type",
content: "website",
}
Meta {
property: "og:url",
content: "https://www.example.com",
}
Meta {
property: "og:image",
content: "https://example.com/image.jpg",
}
Meta {
name: "description",
content: "My Site is a site",
}
}
}

22
examples/title.rs Normal file
View file

@ -0,0 +1,22 @@
//! This example shows how to set the title of the page or window with the Title component
use dioxus::prelude::*;
fn main() {
tracing_subscriber::fmt::init();
launch(app);
}
fn app() -> Element {
let mut count = use_signal(|| 0);
rsx! {
div {
// You can set the title of the page with the Title component
// In web applications, this sets the title in the head. On desktop, it sets the window title
Title { "My Application (Count {count})" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
}
}

View file

@ -918,7 +918,6 @@ impl VirtualDom {
"Calling {} listeners",
listeners.len()
);
tracing::info!("Listeners: {:?}", listeners);
for listener in listeners.into_iter().rev() {
if let AttributeValue::Listener(listener) = listener {
self.runtime.rendering.set(false);

View file

@ -15,9 +15,9 @@ dioxus-html = { workspace = true, features = [
"serialize",
"native-bind",
"mounted",
"eval",
"document",
] }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "eval"] }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
dioxus-cli-config = { workspace = true, features = ["read-config"] }
generational-box = { workspace = true }

View file

@ -211,8 +211,8 @@ impl Config {
/// Sets the menu the window will use. This will override the default menu bar.
///
/// > Note: Menu will be hidden if
/// [`with_decorations`](tao::window::WindowBuilder::with_decorations)
/// is set to false and passed into [`with_window`](Config::with_window)
/// > [`with_decorations`](tao::window::WindowBuilder::with_decorations)
/// > is set to false and passed into [`with_window`](Config::with_window)
#[allow(unused)]
pub fn with_menu(mut self, menu: impl Into<Option<DioxusMenu>>) -> Self {
#[cfg(not(any(target_os = "ios", target_os = "android")))]

View file

@ -1,23 +1,27 @@
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use dioxus_html::document::{Document, EvalError, Evaluator};
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use crate::{query::Query, DesktopContext};
/// Represents the desktop-target's provider of evaluators.
pub struct DesktopEvalProvider {
pub struct DesktopDocument {
pub(crate) desktop_ctx: DesktopContext,
}
impl DesktopEvalProvider {
impl DesktopDocument {
pub fn new(desktop_ctx: DesktopContext) -> Self {
Self { desktop_ctx }
}
}
impl EvalProvider for DesktopEvalProvider {
impl Document for DesktopDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
DesktopEvaluator::create(self.desktop_ctx.clone(), js)
}
fn set_title(&self, title: String) {
self.desktop_ctx.window.set_title(&title);
}
}
/// Represents a desktop-target's JavaScript evaluator.

View file

@ -7,9 +7,9 @@ mod app;
mod assets;
mod config;
mod desktop_context;
mod document;
mod edits;
mod element;
mod eval;
mod event_handlers;
mod events;
mod file_upload;

View file

@ -1,5 +1,5 @@
use crate::{assets::*, edits::EditQueue};
use dioxus_interpreter_js::eval::NATIVE_EVAL_JS;
use dioxus_html::document::NATIVE_EVAL_JS;
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
use dioxus_interpreter_js::NATIVE_JS;
use std::path::{Component, Path, PathBuf};

View file

@ -1,11 +1,11 @@
use crate::menubar::DioxusMenu;
use crate::{
app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider,
app::SharedContext, assets::AssetHandlerRegistry, document::DesktopDocument, edits::EditQueue,
file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
DesktopContext, DesktopService,
};
use dioxus_core::{ScopeId, VirtualDom};
use dioxus_html::prelude::EvalProvider;
use dioxus_html::document::Document;
use futures_util::{pin_mut, FutureExt};
use std::{rc::Rc, task::Waker};
use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
@ -214,8 +214,7 @@ impl WebviewInstance {
file_hover,
));
let provider: Rc<dyn EvalProvider> =
Rc::new(DesktopEvalProvider::new(desktop_context.clone()));
let provider: Rc<dyn Document> = Rc::new(DesktopDocument::new(desktop_context.clone()));
dom.in_runtime(|| {
ScopeId::ROOT.provide_context(desktop_context.clone());

View file

@ -33,7 +33,7 @@ axum = { workspace = true, optional = true }
dioxus-hot-reload = { workspace = true, optional = true }
[features]
default = ["macro", "html", "hot-reload", "signals", "hooks", "launch", "mounted", "file_engine", "eval"]
default = ["macro", "html", "hot-reload", "signals", "hooks", "launch", "mounted", "file_engine", "document"]
minimal = ["macro", "html", "signals", "hooks", "launch"]
signals = ["dep:dioxus-signals"]
macro = ["dep:dioxus-core-macro"]
@ -42,7 +42,7 @@ hooks = ["dep:dioxus-hooks"]
hot-reload = ["dep:dioxus-hot-reload", "dioxus-web?/hot_reload", "dioxus-fullstack?/hot-reload"]
mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
file_engine = ["dioxus-web?/file_engine"]
eval = ["dioxus-web?/eval", "dioxus-html?/eval"]
document = ["dioxus-web?/document", "dioxus-html?/document"]
launch = ["dep:dioxus-config-macro"]
router = ["dep:dioxus-router"]

View file

@ -20,6 +20,7 @@ axum = { workspace = true, features = ["ws", "macros"], optional = true }
tower-http = { workspace = true, optional = true, features = ["fs"] }
dioxus-lib = { workspace = true }
generational-box = { workspace = true }
# Dioxus + SSR
dioxus-ssr = { workspace = true, optional = true }
@ -71,19 +72,18 @@ tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], option
dioxus = { workspace = true, features = ["fullstack"] }
[features]
default = ["hot-reload", "panic_hook"]
default = ["hot-reload", "panic_hook", "document", "file_engine", "mounted"]
panic_hook = ["dioxus-web?/panic_hook"]
hot-reload = ["dioxus-web?/hot_reload", "dioxus-hot-reload/serve"]
mounted = ["dioxus-web?/mounted"]
file_engine = ["dioxus-web?/file_engine"]
eval = ["dioxus-web?/eval"]
document = ["dioxus-web?/document"]
web = ["dep:dioxus-web", "dep:web-sys"]
desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
mobile = ["dep:dioxus-mobile"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
static-site-generation = []
server = [
"server_fn/ssr",
"dioxus_server_macro/server",

View file

@ -0,0 +1,4 @@
#[cfg(feature = "server")]
pub(crate) mod server;
#[cfg(feature = "web")]
pub(crate) mod web;

View file

@ -0,0 +1,140 @@
//! On the server, we collect any elements that should be rendered into the head in the first frame of SSR.
//! After the first frame, we have already sent down the head, so we can't modify it in place. The web client
//! will hydrate the head with the correct contents once it loads.
use std::cell::RefCell;
use dioxus_lib::{html::document::*, prelude::*};
use dioxus_ssr::Renderer;
use generational_box::GenerationalBox;
#[derive(Default)]
struct ServerDocumentInner {
streaming: bool,
title: Option<String>,
meta: Vec<Element>,
link: Vec<Element>,
script: Vec<Element>,
}
/// A Document provider that collects all contents injected into the head for SSR rendering.
#[derive(Default)]
pub(crate) struct ServerDocument(RefCell<ServerDocumentInner>);
impl ServerDocument {
pub(crate) fn render(
&self,
to: &mut impl std::fmt::Write,
renderer: &mut Renderer,
) -> std::fmt::Result {
fn lazy_app(props: Element) -> Element {
props
}
let myself = self.0.borrow();
let element = rsx! {
if let Some(title) = myself.title.as_ref() {
title { title: "{title}" }
}
{myself.meta.iter().map(|m| rsx! { {m} })}
{myself.link.iter().map(|l| rsx! { {l} })}
{myself.script.iter().map(|s| rsx! { {s} })}
};
let mut dom = VirtualDom::new_with_props(lazy_app, element);
dom.rebuild_in_place();
// We don't hydrate the head, so we can set the pre_render flag to false to save a few bytes
let was_pre_rendering = renderer.pre_render;
renderer.pre_render = false;
renderer.render_to(to, &dom)?;
renderer.pre_render = was_pre_rendering;
Ok(())
}
pub(crate) fn start_streaming(&self) {
self.0.borrow_mut().streaming = true;
}
pub(crate) fn warn_if_streaming(&self) {
if self.0.borrow().streaming {
tracing::warn!("Attempted to insert content into the head after the initial streaming frame. Inserting content into the head only works during the initial render of SSR outside before resolving any suspense boundaries.");
}
}
/// Write the head element into the serialized context for hydration
/// We write true if the head element was written to the DOM during server side rendering
pub(crate) fn serialize_for_hydration(&self) {
let serialize = crate::html_storage::serialize_context();
serialize.push(&!self.0.borrow().streaming);
}
}
impl Document for ServerDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
NoOpDocument.new_evaluator(js)
}
fn set_title(&self, title: String) {
self.warn_if_streaming();
self.serialize_for_hydration();
self.0.borrow_mut().title = Some(title);
}
fn create_meta(&self, props: MetaProps) {
self.warn_if_streaming();
self.serialize_for_hydration();
self.0.borrow_mut().meta.push(rsx! {
meta {
name: props.name,
charset: props.charset,
http_equiv: props.http_equiv,
content: props.content,
property: props.property,
}
});
}
fn create_script(&self, props: ScriptProps) {
self.warn_if_streaming();
self.serialize_for_hydration();
let children = props.script_contents();
self.0.borrow_mut().script.push(rsx! {
script {
src: props.src,
defer: props.defer,
crossorigin: props.crossorigin,
fetchpriority: props.fetchpriority,
integrity: props.integrity,
nomodule: props.nomodule,
nonce: props.nonce,
referrerpolicy: props.referrerpolicy,
r#type: props.r#type,
{children}
}
});
}
fn create_link(&self, props: head::LinkProps) {
self.warn_if_streaming();
self.serialize_for_hydration();
self.0.borrow_mut().link.push(rsx! {
link {
rel: props.rel,
media: props.media,
title: props.title,
disabled: props.disabled,
r#as: props.r#as,
sizes: props.sizes,
href: props.href,
crossorigin: props.crossorigin,
referrerpolicy: props.referrerpolicy,
fetchpriority: props.fetchpriority,
hreflang: props.hreflang,
integrity: props.integrity,
r#type: props.r#type,
blocking: props.blocking,
}
})
}
}

View file

@ -0,0 +1,58 @@
#![allow(unused)]
//! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
use dioxus_lib::events::Document;
use dioxus_web::WebDocument;
fn head_element_written_on_server() -> bool {
dioxus_web::take_server_data()
.ok()
.flatten()
.unwrap_or_default()
}
pub(crate) struct FullstackWebDocument;
impl Document for FullstackWebDocument {
fn new_evaluator(
&self,
js: String,
) -> generational_box::GenerationalBox<Box<dyn dioxus_lib::prelude::document::Evaluator>> {
WebDocument.new_evaluator(js)
}
fn set_title(&self, title: String) {
if head_element_written_on_server() {
return;
}
WebDocument.set_title(title);
}
fn create_meta(&self, props: dioxus_lib::prelude::MetaProps) {
if head_element_written_on_server() {
return;
}
WebDocument.create_meta(props);
}
fn create_script(&self, props: dioxus_lib::prelude::ScriptProps) {
if head_element_written_on_server() {
return;
}
WebDocument.create_script(props);
}
fn create_style(&self, props: dioxus_lib::prelude::StyleProps) {
if head_element_written_on_server() {
return;
}
WebDocument.create_style(props);
}
fn create_link(&self, props: dioxus_lib::prelude::head::LinkProps) {
if head_element_written_on_server() {
return;
}
WebDocument.create_link(props);
}
}

View file

@ -22,27 +22,29 @@ use serde::{de::DeserializeOwned, Serialize};
/// ```
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
server_fn: impl Fn() -> O,
) -> O {
use_hook(|| server_cached(server_fn))
}
pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
value: impl FnOnce() -> O,
) -> O {
#[cfg(feature = "server")]
{
let serialize = crate::html_storage::use_serialize_context();
use_hook(|| {
let data = server_fn();
serialize.push(&data);
data
})
let serialize = crate::html_storage::serialize_context();
let data = value();
serialize.push(&data);
data
}
#[cfg(all(not(feature = "server"), feature = "web"))]
{
use_hook(|| {
dioxus_web::take_server_data()
.ok()
.flatten()
.unwrap_or_else(server_fn)
})
dioxus_web::take_server_data()
.ok()
.flatten()
.unwrap_or_else(value)
}
#[cfg(not(any(feature = "server", feature = "web")))]
{
use_hook(server_fn)
value()
}
}

View file

@ -32,7 +32,11 @@ impl SerializeContext {
}
pub(crate) fn use_serialize_context() -> SerializeContext {
use_hook(|| has_context().unwrap_or_else(|| provide_context(SerializeContext::default())))
use_hook(serialize_context)
}
pub(crate) fn serialize_context() -> SerializeContext {
has_context().unwrap_or_else(|| provide_context(SerializeContext::default()))
}
#[derive(serde::Serialize, serde::Deserialize, Default)]

View file

@ -50,12 +50,23 @@ pub fn launch(
#[allow(unused)]
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
#[allow(unused_mut)] mut contexts: Vec<
Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>,
>,
platform_config: Config,
) {
let contexts = Arc::new(contexts);
let factory = virtual_dom_factory(root, contexts);
let mut factory = virtual_dom_factory(root, contexts);
let cfg = platform_config.web_cfg.hydrate(true);
#[cfg(feature = "document")]
let factory = move || {
let mut vdom = factory();
vdom.provide_root_context(std::rc::Rc::new(crate::document::web::FullstackWebDocument)
as std::rc::Rc<dyn dioxus_lib::prelude::document::Document>);
vdom
};
dioxus_web::launch::launch_virtual_dom(factory(), cfg)
}

View file

@ -18,6 +18,8 @@ pub mod launch;
pub use config::*;
#[cfg(feature = "document")]
mod document;
#[cfg(feature = "server")]
mod render;
#[cfg(feature = "server")]

View file

@ -160,8 +160,34 @@ impl SsrRendererPool {
let join_handle = spawn_platform(move || async move {
let mut virtual_dom = virtual_dom_factory();
#[cfg(feature = "document")]
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
#[cfg(feature = "document")]
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
let mut pre_body = String::new();
if let Err(err) = wrapper.render_head(&mut pre_body) {
_ = into.start_send(Err(err));
return;
}
#[cfg(feature = "document")]
{
// Collect any head content from the document provider and inject that into the head
if let Err(err) = document.render(&mut pre_body, &mut renderer) {
_ = into.start_send(Err(err.into()));
return;
}
// Enable a warning when inserting contents into the head during streaming
document.start_streaming();
}
if let Err(err) = wrapper.render_before_body(&mut pre_body) {
_ = into.start_send(Err(err));
return;
@ -206,10 +232,6 @@ impl SsrRendererPool {
};
}
// poll the future, which may call server_context()
tracing::info!("Rebuilding vdom");
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
// Render the initial frame with loading placeholders
let mut initial_frame = renderer.render(&virtual_dom);
@ -364,6 +386,18 @@ impl FullstackHTMLTemplate {
}
impl FullstackHTMLTemplate {
/// Render any content before the head of the page.
pub fn render_head<R: std::fmt::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
to.write_str(&index.head)?;
Ok(())
}
/// Render any content before the body of the page.
pub fn render_before_body<R: std::fmt::Write>(
&self,
@ -371,7 +405,7 @@ impl FullstackHTMLTemplate {
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
to.write_str(&index.pre_main)?;
to.write_str(&index.close_head)?;
Ok(())
}

View file

@ -107,13 +107,19 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
post_main.1.to_string(),
);
let (head, close_head) = pre_main.split_once("</head>").unwrap_or_else(|| {
panic!("Failed to find closing </head> tag after id=\"{root_id}\" in index.html.")
});
let (head, close_head) = (head.to_string(), "</head>".to_string() + close_head);
let (post_main, after_closing_body_tag) =
post_main.split_once("</body>").unwrap_or_else(|| {
panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
});
IndexHtml {
pre_main,
head,
close_head,
post_main: post_main.to_string(),
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
}
@ -121,7 +127,8 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
#[derive(Clone)]
pub(crate) struct IndexHtml {
pub(crate) pre_main: String,
pub(crate) head: String,
pub(crate) close_head: String,
pub(crate) post_main: String,
pub(crate) after_closing_body_tag: String,
}

View file

@ -11,12 +11,15 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { workspace = true }
dioxus-core-macro = { workspace = true }
dioxus-rsx = { workspace = true, optional = true }
dioxus-html-internal-macro = { workspace = true }
dioxus-hooks = { workspace = true }
generational-box = { workspace = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1", optional = true }
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
js-sys = { version = "0.3.56", optional = true }
euclid = "0.22.7"
enumset = "1.1.2"
@ -51,14 +54,18 @@ features = [
"CompositionEvent",
]
[build-dependencies]
lazy-js-bundle = { workspace = true }
[dev-dependencies]
serde_json = "1"
dioxus = { workspace = true }
dioxus-web = { workspace = true }
tokio = { workspace = true, features = ["time"] }
manganis = { workspace = true }
[features]
default = ["serialize", "mounted", "eval", "file-engine"]
default = ["serialize", "mounted", "document", "file-engine"]
serialize = [
"dep:serde",
"dep:serde_json",
@ -76,7 +83,7 @@ mounted = [
"web-sys?/ScrollBehavior",
"web-sys?/HtmlElement",
]
eval = [
document = [
"dep:serde",
"dep:serde_json"
]
@ -87,7 +94,7 @@ file-engine = [
"web-sys?/FileList",
"web-sys?/FileReader"
]
wasm-bind = ["dep:web-sys", "dep:wasm-bindgen"]
wasm-bind = ["dep:web-sys", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"]
native-bind = ["dep:tokio", "file-engine"]
hot-reload-context = ["dep:dioxus-rsx", "dioxus-rsx/hot_reload_traits"]
html-to-rsx = []

View file

View file

9
packages/html/build.rs Normal file
View file

@ -0,0 +1,9 @@
fn main() {
// If any TS files change, re-run the build script
lazy_js_bundle::LazyTypeScriptBindings::new()
.with_watching("./src/ts")
.with_binding("./src/ts/eval.ts", "./src/js/eval.js")
.with_binding("./src/ts/native_eval.ts", "./src/js/native_eval.js")
.with_binding("./src/ts/head.ts", "./src/js/head.js")
.run();
}

View file

@ -0,0 +1,59 @@
# Modifying the Head
Dioxus includes a series of components that render into the head of the page:
- [Title](crate::Title)
- [Meta](crate::Meta)
- [head::Link](crate::head::Link)
- [Script](crate::Script)
- [Style](crate::Style)
Each of these components can be used to add extra information to the head of the page. For example, you can use the `Title` component to set the title of the page, or the `Meta` component to add extra metadata to the page.
## Limitations
Components that render into the head of the page do have a few key limitations:
- With the exception of the `Title` component, all components that render into the head cannot be modified after the first time they are rendered.
- Components that render into the head will not be removed even after the component is removed from the tree.
## Example
```rust, no_run
# use dioxus::prelude::*;
fn RedirectToDioxusHomepageWithoutJS() -> Element {
rsx! {
// You can use the meta component to render a meta tag into the head of the page
// This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
Meta {
http_equiv: "refresh",
content: "10;url=https://dioxuslabs.com",
}
}
}
```
## Fullstack Rendering
Head components are compatible with fullstack rendering, but only head components that are rendered in the initial render (before suspense boundaries resolve) will be rendered into the head.
If you have any important metadata that you want to render into the head, make sure to render it outside of any pending suspense boundaries.
```rust, no_run
# use dioxus::prelude::*;
# #[component]
# fn LoadData(children: Element) -> Element { unimplemented!() }
fn App() -> Element {
rsx! {
// This will render in SSR
Title { "My Page" }
SuspenseBoundary {
fallback: |_| rsx! { "Loading..." },
LoadData {
// This will only be rendered on the client after hydration so it may not be visible to search engines
Meta { name: "description", content: "My Page" }
}
}
}
}
```

View file

@ -1,13 +1,14 @@
/// Code for the Dioxus channel used to communicate between the dioxus and javascript code
pub const NATIVE_EVAL_JS: &str = include_str!("./js/native_eval.js");
#[cfg(feature = "native-bind")]
pub const NATIVE_EVAL_JS: &str = include_str!("../js/native_eval.js");
#[cfg(feature = "webonly")]
#[cfg(feature = "wasm-bind")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub struct JSOwner {
_owner: Box<dyn std::any::Any>,
}
#[cfg(feature = "webonly")]
#[cfg(feature = "wasm-bind")]
impl JSOwner {
pub fn new(owner: impl std::any::Any) -> Self {
Self {
@ -16,7 +17,7 @@ impl JSOwner {
}
}
#[cfg(feature = "webonly")]
#[cfg(feature = "wasm-bind")]
#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
extern "C" {
pub type WebDioxusChannel;

View file

@ -1,18 +1,14 @@
#![allow(clippy::await_holding_refcell_ref)]
#![doc = include_str!("../docs/eval.md")]
#![doc = include_str!("../../docs/eval.md")]
use dioxus_core::prelude::*;
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use generational_box::GenerationalBox;
use std::future::{poll_fn, Future, IntoFuture};
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
/// A struct that implements EvalProvider is sent through [`ScopeState`]'s provide_context function
/// so that [`eval`] can provide a platform agnostic interface for evaluating JavaScript code.
pub trait EvalProvider {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>>;
}
use super::document;
/// The platform's evaluator.
pub trait Evaluator {
@ -42,55 +38,22 @@ type EvalCreator = Rc<dyn Fn(&str) -> UseEval>;
/// has access to most, if not all of your application data.**
#[must_use]
pub fn eval_provider() -> EvalCreator {
let eval_provider = consume_context::<Rc<dyn EvalProvider>>();
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 = include_str!("../../docs/eval.md")]
#[doc(alias = "javascript")]
pub fn eval(script: &str) -> UseEval {
let eval_provider = dioxus_core::prelude::try_consume_context::<Rc<dyn EvalProvider>>()
// Create a dummy provider that always hiccups when trying to evaluate
// That way, we can still compile and run the code without a real provider
.unwrap_or_else(|| {
struct DummyProvider;
impl EvalProvider for DummyProvider {
fn new_evaluator(&self, _js: String) -> GenerationalBox<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(DummyEvaluator))
}
}
struct DummyEvaluator;
impl Evaluator for DummyEvaluator {
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
Err(EvalError::Unsupported)
}
fn poll_recv(
&mut self,
_context: &mut Context<'_>,
) -> Poll<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))
}
}
Rc::new(DummyProvider) as Rc<dyn EvalProvider>
});
UseEval::new(eval_provider.new_evaluator(script.to_string()))
let document = use_hook(document);
UseEval::new(document.new_evaluator(script.to_string()))
}
/// A wrapper around the target platform's evaluator that lets you send and receive data from JavaScript spawned by [`eval`].
///
#[doc = include_str!("../docs/eval.md")]
#[doc = include_str!("../../docs/eval.md")]
#[derive(Clone, Copy)]
pub struct UseEval {
evaluator: GenerationalBox<Box<dyn Evaluator>>,

View file

@ -0,0 +1,520 @@
#![doc = include_str!("../../docs/head.md")]
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use dioxus_core::{prelude::*, DynamicNode};
use dioxus_core_macro::*;
/// Warn the user if they try to change props on a element that is injected into the head
#[allow(unused)]
fn use_update_warning<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 ");
}
}
}
fn extract_single_text_node(children: &Element, component: &str) -> Option<String> {
let vnode = match children {
Element::Ok(vnode) => vnode,
Element::Err(err) => {
tracing::error!("Error while rendering {component}: {err}");
return None;
}
};
// The title's children must be in one of two forms:
// 1. rsx! { "static text" }
// 2. rsx! { "title: {dynamic_text}" }
match vnode.template.get() {
// rsx! { "static text" }
Template {
roots: &[TemplateNode::Text { text }],
node_paths: &[],
attr_paths: &[],
..
} => Some(text.to_string()),
// rsx! { "title: {dynamic_text}" }
Template {
roots: &[TemplateNode::Dynamic { id }],
node_paths: &[&[0]],
attr_paths: &[],
..
} => {
let node = &vnode.dynamic_nodes[id];
match node {
DynamicNode::Text(text) => Some(text.value.clone()),
_ => {
tracing::error!("Error while rendering {component}: The children of {component} must be a single text node. It cannot be a component, if statement, loop, or a fragment");
None
}
}
}
_ => {
tracing::error!(
"Error while rendering title: The children of title must be a single text node"
);
None
}
}
}
#[derive(Clone, Props, PartialEq)]
pub struct TitleProps {
/// The contents of the title tag. The children must be a single text node.
children: Element,
}
/// Render the title of the page. On web renderers, this will set the [title](crate::elements::title) in the head. On desktop, it will set the window title.
///
/// Unlike most head components, the Title can be modified after the first render. Only the latest update to the title will be reflected if multiple title components are rendered.
///
///
/// The children of the title component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the title will not be updated.
///
/// # Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn App() -> Element {
/// rsx! {
/// // You can use the Title component to render a title tag into the head of the page or window
/// Title { "My Page" }
/// }
/// }
/// ```
#[component]
pub fn Title(props: TitleProps) -> Element {
let children = props.children;
let Some(text) = extract_single_text_node(&children, "Title") else {
return rsx! {};
};
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
let document = use_hook(document);
let last_text = use_hook(|| {
// Set the title initially
document.set_title(text.clone());
Rc::new(RefCell::new(text.clone()))
});
// If the text changes, update the title
let mut last_text = last_text.borrow_mut();
if text != *last_text {
document.set_title(text.clone());
*last_text = text;
}
rsx! {}
}
/// Props for the [`Meta`] component
#[derive(Clone, Props, PartialEq)]
pub struct MetaProps {
pub property: Option<String>,
pub name: Option<String>,
pub charset: Option<String>,
pub http_equiv: Option<String>,
pub content: Option<String>,
}
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);
});
rsx! {}
}
#[derive(Clone, Props, PartialEq)]
pub struct ScriptProps {
/// The contents of the script tag. If present, the children must be a single text node.
pub children: Element,
/// Scripts are deduplicated by their src attribute
pub src: Option<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>,
}
impl ScriptProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(defer) = &self.defer {
attributes.push(("defer", defer.to_string()));
}
if let Some(crossorigin) = &self.crossorigin {
attributes.push(("crossorigin", crossorigin.clone()));
}
if let Some(fetchpriority) = &self.fetchpriority {
attributes.push(("fetchpriority", fetchpriority.clone()));
}
if let Some(integrity) = &self.integrity {
attributes.push(("integrity", integrity.clone()));
}
if let Some(nomodule) = &self.nomodule {
attributes.push(("nomodule", nomodule.to_string()));
}
if let Some(nonce) = &self.nonce {
attributes.push(("nonce", nonce.clone()));
}
if let Some(referrerpolicy) = &self.referrerpolicy {
attributes.push(("referrerpolicy", referrerpolicy.clone()));
}
if let Some(r#type) = &self.r#type {
attributes.push(("type", r#type.clone()));
}
attributes
}
pub fn script_contents(&self) -> Option<String> {
extract_single_text_node(&self.children, "Script")
}
}
/// Render a [`script`](crate::elements::script) tag into the head of the page.
///
///
/// If present, the children of the script component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the script will not be added.
///
///
/// Any scripts you add will be deduplicated by their `src` attribute (if present).
///
/// # Example
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn LoadScript() -> Element {
/// rsx! {
/// // You can use the Script component to render a script tag into the head of the page
/// Script {
/// src: manganis::mg!(file("./assets/script.js")),
/// }
/// }
/// }
/// ```
///
/// <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);
});
rsx! {}
}
#[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,
}
impl StyleProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(href) = &self.href {
attributes.push(("href", href.clone()));
}
if let Some(media) = &self.media {
attributes.push(("media", media.clone()));
}
if let Some(nonce) = &self.nonce {
attributes.push(("nonce", nonce.clone()));
}
if let Some(title) = &self.title {
attributes.push(("title", title.clone()));
}
attributes
}
pub fn style_contents(&self) -> Option<String> {
extract_single_text_node(&self.children, "Title")
}
}
/// Render a [`style`](crate::elements::style) tag into the head of the page.
///
/// If present, the children of the style component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the style will not be added.
///
/// # Example
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn RedBackground() -> Element {
/// rsx! {
/// // You can use the style component to render a style tag into the head of the page
/// // This style tag will set the background color of the page to red
/// Style {
/// r#"
/// body {{
/// background-color: red;
/// }}
/// "#,
/// }
/// }
/// }
/// ```
///
/// <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);
});
rsx! {}
}
use super::*;
#[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>,
}
impl LinkProps {
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
let mut attributes = Vec::new();
if let Some(rel) = &self.rel {
attributes.push(("rel", rel.clone()));
}
if let Some(media) = &self.media {
attributes.push(("media", media.clone()));
}
if let Some(title) = &self.title {
attributes.push(("title", title.clone()));
}
if let Some(disabled) = &self.disabled {
attributes.push(("disabled", disabled.to_string()));
}
if let Some(r#as) = &self.r#as {
attributes.push(("as", r#as.clone()));
}
if let Some(sizes) = &self.sizes {
attributes.push(("sizes", sizes.clone()));
}
if let Some(href) = &self.href {
attributes.push(("href", href.clone()));
}
if let Some(crossorigin) = &self.crossorigin {
attributes.push(("crossOrigin", crossorigin.clone()));
}
if let Some(referrerpolicy) = &self.referrerpolicy {
attributes.push(("referrerPolicy", referrerpolicy.clone()));
}
if let Some(fetchpriority) = &self.fetchpriority {
attributes.push(("fetchPriority", fetchpriority.clone()));
}
if let Some(hreflang) = &self.hreflang {
attributes.push(("hrefLang", hreflang.clone()));
}
if let Some(integrity) = &self.integrity {
attributes.push(("integrity", integrity.clone()));
}
if let Some(r#type) = &self.r#type {
attributes.push(("type", r#type.clone()));
}
if let Some(blocking) = &self.blocking {
attributes.push(("blocking", blocking.clone()));
}
attributes
}
}
/// Render a [`link`](crate::elements::link) tag into the head of the page.
///
/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
///
/// # Example
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn RedBackground() -> Element {
/// rsx! {
/// // You can use the meta component to render a meta tag into the head of the page
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
/// head::Link {
/// href: manganis::mg!(file("./assets/style.css")),
/// rel: "stylesheet",
/// }
/// }
/// }
/// ```
///
/// <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);
});
rsx! {}
}
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
}
}
}

View file

@ -0,0 +1,114 @@
// API inspired by Reacts implementation of head only elements. We use components here instead of elements to simplify internals.
use std::{
rc::Rc,
task::{Context, Poll},
};
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
mod bindings;
#[allow(unused)]
pub use bindings::*;
mod eval;
pub use eval::*;
pub mod head;
pub use head::{Meta, MetaProps, Script, ScriptProps, Style, StyleProps, Title, TitleProps};
fn format_attributes(attributes: &[(&str, String)]) -> String {
let mut formatted = String::from("[");
for (key, value) in attributes {
formatted.push_str(&format!("[{key:?}, {value:?}],"));
}
if formatted.ends_with(',') {
formatted.pop();
}
formatted.push(']');
formatted
}
fn create_element_in_head(
tag: &str,
attributes: &[(&str, String)],
children: Option<String>,
) -> String {
let helpers = include_str!("../js/head.js");
let attributes = format_attributes(attributes);
let children = children
.map(|c| format!("\"{c}\""))
.unwrap_or("null".to_string());
format!(r#"{helpers};window.createElementInHead("{tag}", {attributes}, {children});"#)
}
/// A provider for document-related functionality. By default most methods are driven through [`eval`].
pub trait Document {
/// Create a new evaluator for the document that evaluates JavaScript and facilitates communication between JavaScript and Rust.
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>>;
/// Set the title of the document
fn set_title(&self, title: String) {
self.new_evaluator(format!("document.title = {title:?};"));
}
fn create_meta(&self, props: MetaProps) {
let attributes = props.attributes();
let js = create_element_in_head("meta", &attributes, None);
self.new_evaluator(js);
}
fn create_script(&self, props: ScriptProps) {
let attributes = props.attributes();
let js = create_element_in_head("script", &attributes, props.script_contents());
self.new_evaluator(js);
}
fn create_style(&self, props: StyleProps) {
let attributes = props.attributes();
let js = create_element_in_head("style", &attributes, props.style_contents());
self.new_evaluator(js);
}
fn create_link(&self, props: head::LinkProps) {
let attributes = props.attributes();
let js = create_element_in_head("link", &attributes, None);
self.new_evaluator(js);
}
}
/// The default No-Op document
pub struct NoOpDocument;
impl Document for NoOpDocument {
fn new_evaluator(&self, _js: String) -> GenerationalBox<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))
}
}
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>)
}

View file

@ -655,6 +655,11 @@ builder_constructors! {
title: String DEFAULT, // FIXME
r#type: Mime "type",
integrity: String DEFAULT,
disabled: Bool DEFAULT,
referrerpolicy: ReferrerPolicy DEFAULT,
fetchpriority: FetchPriority DEFAULT,
blocking: Blocking DEFAULT,
r#as: As "as",
};
/// Build a
@ -665,6 +670,7 @@ builder_constructors! {
content: String DEFAULT,
http_equiv: String "http-equiv",
name: Metadata DEFAULT,
property: Metadata DEFAULT,
};
/// Build a
@ -1261,6 +1267,8 @@ builder_constructors! {
nonce: Nonce DEFAULT,
src: Uri DEFAULT,
text: String DEFAULT,
fetchpriority: String DEFAULT,
referrerpolicy: String DEFAULT,
r#async: Bool "async",
r#type: String "type", // TODO could be an enum

View file

@ -0,0 +1 @@
[10372071913661173523, 8375185156499858125, 4813754958077120784]

View file

@ -0,0 +1 @@
var createElementInHead=function(tag,attributes,children){const element=document.createElement(tag);for(let[key,value]of attributes)element.setAttribute(key,value);if(children)element.appendChild(document.createTextNode(children));document.head.appendChild(element)};window.createElementInHead=createElementInHead;

View file

@ -47,8 +47,8 @@ pub use elements::*;
pub use events::*;
pub use render_template::*;
#[cfg(feature = "eval")]
pub mod eval;
#[cfg(feature = "document")]
pub mod document;
pub mod extensions {
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
@ -57,9 +57,12 @@ pub mod extensions {
pub mod prelude {
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
#[cfg(feature = "document")]
pub use crate::document::{
self, document, eval, head, Document, Meta, MetaProps, Script, ScriptProps, Style,
StyleProps, Title, TitleProps, UseEval,
};
pub use crate::elements::extensions::*;
#[cfg(feature = "eval")]
pub use crate::eval::*;
pub use crate::events::*;
pub use crate::point_interaction::*;
pub use keyboard_types::{self, Code, Key, Location, Modifiers};

3
packages/html/src/ts/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# please dont accidentally run tsc and commit your js in this dir.
*.js

View file

@ -0,0 +1,19 @@
// Helper functions for working with the document head
function createElementInHead(
tag: string,
attributes: [string, string][],
children: string | null
): void {
const element = document.createElement(tag);
for (const [key, value] of attributes) {
element.setAttribute(key, value);
}
if (children) {
element.appendChild(document.createTextNode(children));
}
document.head.appendChild(element);
}
// @ts-ignore
window.createElementInHead = createElementInHead;

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "CommonJS",
"lib": [
"ES2015",
"DOM",
"dom",
"dom.iterable",
"ESNext"
],
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
},
"exclude": [
"**/*.spec.ts"
]
}

View file

@ -26,7 +26,7 @@ dioxus-core = { workspace = true, optional = true }
dioxus-html = { workspace = true, optional = true }
[build-dependencies]
md5 = "0.7.0"
lazy-js-bundle = { workspace = true }
[features]
default = []
@ -42,4 +42,3 @@ webonly = [
]
binary-protocol = ["sledgehammer", "dep:dioxus-core", "dep:dioxus-html"]
minimal_bindings = []
eval = []

View file

@ -1,84 +1,14 @@
use std::collections::hash_map::DefaultHasher;
use std::path::PathBuf;
use std::{hash::Hasher, process::Command};
fn main() {
// If any TS changes, re-run the build script
let watching = std::fs::read_dir("./src/ts").unwrap();
let ts_paths: Vec<_> = watching
.into_iter()
.flatten()
.map(|entry| entry.path())
.filter(|path| path.extension().map(|ext| ext == "ts").unwrap_or(false))
.collect();
for path in &ts_paths {
println!("cargo:rerun-if-changed={}", path.display());
}
// Compute the hash of the ts files
let hash = hash_ts_files(ts_paths);
// If the hash matches the one on disk, we're good and don't need to update bindings
let fs_hash_string = std::fs::read_to_string("src/js/hash.txt");
let expected = fs_hash_string
.as_ref()
.map(|s| s.trim())
.unwrap_or_default();
if expected == hash.to_string() {
return;
}
// Otherwise, generate the bindings and write the new hash to disk
// Generate the bindings for both native and web
gen_bindings("common", "common");
gen_bindings("native", "native");
gen_bindings("core", "core");
gen_bindings("eval", "eval");
gen_bindings("native_eval", "native_eval");
gen_bindings("hydrate", "hydrate");
gen_bindings("initialize_streaming", "initialize_streaming");
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
}
/// Hashes the contents of a directory
fn hash_ts_files(mut files: Vec<PathBuf>) -> u64 {
// Different systems will read the files in different orders, so we sort them to make sure the hash is consistent
files.sort();
let mut hash = DefaultHasher::new();
for file in files {
let contents = std::fs::read_to_string(file).unwrap();
// windows + git does a weird thing with line endings, so we need to normalize them
for line in contents.lines() {
hash.write(line.as_bytes());
}
}
hash.finish()
}
// okay...... so tsc might fail if the user doesn't have it installed
// we don't really want to fail if that's the case
// but if you started *editing* the .ts files, you're gonna have a bad time
// so.....
// we need to hash each of the .ts files and add that hash to the JS files
// if the hashes don't match, we need to fail the build
// that way we also don't need
fn gen_bindings(input_name: &str, output_name: &str) {
// If the file is generated, and the hash is different, we need to generate it
let status = Command::new("bun")
.arg("build")
.arg(format!("src/ts/{input_name}.ts"))
.arg("--outfile")
.arg(format!("src/js/{output_name}.js"))
.arg("--minify-whitespace")
.arg("--minify-syntax")
.status()
.unwrap();
if !status.success() {
panic!(
"Failed to generate bindings for {}. Make sure you have tsc installed",
input_name
);
}
// If any TS files change, re-run the build script
lazy_js_bundle::LazyTypeScriptBindings::new()
.with_watching("./src/ts")
.with_binding("./src/ts/common.ts", "./src/js/common.js")
.with_binding("./src/ts/native.ts", "./src/js/native.js")
.with_binding("./src/ts/core.ts", "./src/js/core.js")
.with_binding("./src/ts/hydrate.ts", "./src/js/hydrate.js")
.with_binding(
"./src/ts/initialize_streaming.ts",
"./src/js/initialize_streaming.js",
)
.run();
}

View file

@ -1 +0,0 @@
this files are generated - do not edit them!

View file

@ -1 +1 @@
6628058622237003340
[6449103750905854967, 12029349297046688094, 14626980229647476238, 8716623267269178440, 5336385715226370016, 14456089431355876478, 3589298972260118311, 2745859031945642653, 5638004933879392817]

View file

@ -24,9 +24,6 @@ pub mod unified_bindings;
#[cfg(feature = "sledgehammer")]
pub use unified_bindings::*;
#[cfg(feature = "eval")]
pub mod eval;
// Common bindings for minimal usage.
#[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
pub mod minimal_bindings {

View file

@ -0,0 +1,11 @@
[package]
name = "lazy-js-bundle"
version = { workspace = true }
edition = "2021"
authors = ["Jonathan Kelley"]
description = "A codegen library to bundle TypeScript into JavaScript without requiring a bundler to be installed"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://docs.rs/dioxus"
keywords = ["ts", "codegen", "typescript", "javascript", "wasm"]

View file

@ -0,0 +1,202 @@
use std::collections::hash_map::DefaultHasher;
use std::path::{Path, PathBuf};
use std::{hash::Hasher, process::Command};
struct Binding {
input_path: PathBuf,
output_path: PathBuf,
}
/// A builder for generating TypeScript bindings lazily
#[derive(Default)]
pub struct LazyTypeScriptBindings {
binding: Vec<Binding>,
minify_level: MinifyLevel,
watching: Vec<PathBuf>,
}
impl LazyTypeScriptBindings {
/// Create a new builder for generating TypeScript bindings that inputs from the given path and outputs javascript to the given path
pub fn new() -> Self {
Self::default()
}
/// Add a binding to generate
pub fn with_binding(
mut self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Self {
let input_path = input_path.as_ref();
let output_path = output_path.as_ref();
self.binding.push(Binding {
input_path: input_path.to_path_buf(),
output_path: output_path.to_path_buf(),
});
self
}
/// Set the minify level for the bindings
pub fn with_minify_level(mut self, minify_level: MinifyLevel) -> Self {
self.minify_level = minify_level;
self
}
/// Watch any .js or .ts files in a directory and re-generate the bindings when they change
// TODO: we should watch any files that get bundled by bun by reading the source map
pub fn with_watching(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
self.watching.push(path.to_path_buf());
self
}
/// Run the bindings
pub fn run(&self) {
// If any TS changes, re-run the build script
let mut watching_paths = Vec::new();
for path in &self.watching {
if let Ok(dir) = std::fs::read_dir(path) {
for entry in dir.flatten() {
let path = entry.path();
if path
.extension()
.map(|ext| ext == "ts" || ext == "js")
.unwrap_or(false)
{
watching_paths.push(path);
}
}
} else {
watching_paths.push(path.to_path_buf());
}
}
for path in &watching_paths {
println!("cargo:rerun-if-changed={}", path.display());
}
// Compute the hash of the input files
let hashes = hash_files(watching_paths);
// Try to find a common prefix for the output files and put the hash in there otherwise, write it to src/binding_hash.txt
let mut hash_location: Option<PathBuf> = None;
for path in &self.binding {
match hash_location {
Some(current_hash_location) => {
let mut common_path = PathBuf::new();
for component in path
.output_path
.components()
.zip(current_hash_location.components())
{
if component.0 != component.1 {
break;
}
common_path.push(component.0);
}
hash_location =
(common_path.components().next().is_some()).then_some(common_path);
}
None => {
hash_location = Some(path.output_path.clone());
}
};
}
let hash_location = hash_location.unwrap_or_else(|| PathBuf::from("./src/js"));
std::fs::create_dir_all(&hash_location).unwrap();
let hash_location = hash_location.join("hash.txt");
// If the hash matches the one on disk, we're good and don't need to update bindings
let fs_hash_string = std::fs::read_to_string(&hash_location);
let expected = fs_hash_string
.as_ref()
.map(|s| s.trim())
.unwrap_or_default();
let hashes_string = format!("{hashes:?}");
if expected == hashes_string {
return;
}
// Otherwise, generate the bindings and write the new hash to disk
for path in &self.binding {
gen_bindings(&path.input_path, &path.output_path, self.minify_level);
}
std::fs::write(hash_location, hashes_string).unwrap();
}
}
/// The level of minification to apply to the bindings
#[derive(Copy, Clone, Debug, Default)]
pub enum MinifyLevel {
/// Don't minify the bindings
None,
/// Minify whitespace
Whitespace,
/// Minify whitespace and syntax
#[default]
Syntax,
/// Minify whitespace, syntax, and identifiers
Identifiers,
}
impl MinifyLevel {
fn as_args(&self) -> &'static [&'static str] {
match self {
MinifyLevel::None => &[],
MinifyLevel::Whitespace => &["--minify-whitespace"],
MinifyLevel::Syntax => &["--minify-whitespace", "--minify-syntax"],
MinifyLevel::Identifiers => &[
"--minify-whitespace",
"--minify-syntax",
"--minify-identifiers",
],
}
}
}
/// Hashes the contents of a directory
fn hash_files(mut files: Vec<PathBuf>) -> Vec<u64> {
// Different systems will read the files in different orders, so we sort them to make sure the hash is consistent
files.sort();
let mut hashes = Vec::new();
for file in files {
let mut hash = DefaultHasher::new();
let Ok(contents) = std::fs::read_to_string(file) else {
continue;
};
// windows + git does a weird thing with line endings, so we need to normalize them
for line in contents.lines() {
hash.write(line.as_bytes());
}
hashes.push(hash.finish());
}
hashes
}
// okay...... so bun might fail if the user doesn't have it installed
// we don't really want to fail if that's the case
// but if you started *editing* the .ts files, you're gonna have a bad time
// so.....
// we need to hash each of the .ts files and add that hash to the JS files
// if the hashes don't match, we need to fail the build
// that way we also don't need
fn gen_bindings(input_path: &Path, output_path: &Path, minify_level: MinifyLevel) {
// If the file is generated, and the hash is different, we need to generate it
let status = Command::new("bun")
.arg("build")
.arg(input_path)
.arg("--outfile")
.arg(output_path)
.args(minify_level.as_args())
.status()
.unwrap();
if !status.success() {
panic!(
"Failed to generate bindings for {:?}. Make sure you have bun installed",
input_path
);
}
}

View file

@ -22,7 +22,7 @@ tokio-stream = { version = "0.1.11", features = ["net"] }
tokio-util = { version = "0.7.4", features = ["rt"] }
serde = { version = "1.0.151", features = ["derive"] }
serde_json = "1.0.91"
dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] }
dioxus-html = { workspace = true, features = ["serialize", "document", "mounted"] }
rustc-hash = { workspace = true }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }

View file

@ -1,48 +1,48 @@
use dioxus_core::ScopeId;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use dioxus_html::document::{Document, EvalError, Evaluator};
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use std::rc::Rc;
use crate::query::{Query, QueryEngine};
/// Provides the DesktopEvalProvider through [`cx.provide_context`].
/// Provides the LiveviewDocument through [`ScopeId::provide_context`].
pub fn init_eval() {
let query = ScopeId::ROOT.consume_context::<QueryEngine>().unwrap();
let provider: Rc<dyn EvalProvider> = Rc::new(DesktopEvalProvider { query });
let provider: Rc<dyn Document> = Rc::new(LiveviewDocument { query });
ScopeId::ROOT.provide_context(provider);
}
/// Reprints the desktop-target's provider of evaluators.
pub struct DesktopEvalProvider {
/// Reprints the liveview-target's provider of evaluators.
pub struct LiveviewDocument {
query: QueryEngine,
}
impl EvalProvider for DesktopEvalProvider {
impl Document for LiveviewDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
DesktopEvaluator::create(self.query.clone(), js)
LiveviewEvaluator::create(self.query.clone(), js)
}
}
/// Reprents a desktop-target's JavaScript evaluator.
pub(crate) struct DesktopEvaluator {
/// Reprents a liveview-target's JavaScript evaluator.
pub(crate) struct LiveviewEvaluator {
query: Query<serde_json::Value>,
}
impl DesktopEvaluator {
/// Creates a new evaluator for desktop-based targets.
impl LiveviewEvaluator {
/// Creates a new evaluator for liveview-based targets.
pub fn create(query_engine: QueryEngine, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
let query = query_engine.new_query(&js);
// We create a generational box that is owned by the query slot so that when we drop the query slot, the generational box is also dropped.
let owner = UnsyncStorage::owner();
let query_id = query.id;
let query = owner.insert(Box::new(DesktopEvaluator { query }) as Box<dyn Evaluator>);
let query = owner.insert(Box::new(LiveviewEvaluator { query }) as Box<dyn Evaluator>);
query_engine.active_requests.slab.borrow_mut()[query_id].owner = Some(owner);
query
}
}
impl Evaluator for DesktopEvaluator {
impl Evaluator for LiveviewEvaluator {
/// # Panics
/// This will panic if the query is currently being awaited.
fn poll_join(

View file

@ -85,7 +85,7 @@ fn handle_edits_code() -> String {
.replace("export", "");
while let Some(import_start) = interpreter.find("import") {
let import_end = interpreter[import_start..]
.find(|c| c == ';' || c == '\n')
.find([';', '\n'])
.map(|i| i + import_start)
.unwrap_or_else(|| interpreter.len());
interpreter.replace_range(import_start..import_end, "");

View file

@ -1,34 +1,38 @@
// @ts-check
const { test, expect } = require('@playwright/test');
const { test, expect } = require("@playwright/test");
test('button click', async ({ page }) => {
await page.goto('http://localhost:3333');
test("button click", async ({ page }) => {
await page.goto("http://localhost:3333");
await page.waitForTimeout(1000);
// Expect the page to contain the counter text.
const main = page.locator('#main');
await expect(main).toContainText('hello axum! 12345');
const main = page.locator("#main");
await expect(main).toContainText("hello axum! 12345");
// Expect the title to contain the counter text.
await expect(page).toHaveTitle("hello axum! 12345");
// Click the increment button.
let button = page.locator('button.increment-button');
let button = page.locator("button.increment-button");
await button.click();
// Expect the page to contain the updated counter text.
await expect(main).toContainText('hello axum! 12346');
await expect(main).toContainText("hello axum! 12346");
// Expect the title to contain the updated counter text.
await expect(page).toHaveTitle("hello axum! 12346");
});
test('fullstack communication', async ({ page }) => {
await page.goto('http://localhost:3333');
test("fullstack communication", async ({ page }) => {
await page.goto("http://localhost:3333");
await page.waitForTimeout(1000);
// Expect the page to contain the counter text.
const main = page.locator('#main');
await expect(main).toContainText('Server said: ...');
const main = page.locator("#main");
await expect(main).toContainText("Server said: ...");
// Click the increment button.
let button = page.locator('button.server-button');
let button = page.locator("button.server-button");
await button.click();
// Expect the page to contain the updated counter text.
await expect(main).toContainText('Server said: Hello from the server!');
await expect(main).toContainText("Server said: Hello from the server!");
});

View file

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

View file

@ -17,6 +17,9 @@ test("nested suspense resolves", async ({ page }) => {
"The robot becomes sentient and says hello world"
);
// And expect the title to have resolved on the client
await expect(page).toHaveTitle("The robot says hello world");
// And more loading text for the nested suspense
await expect(mainMessageDiv).toContainText("Loading 1...");
await expect(mainMessageDiv).toContainText("Loading 2...");

View file

@ -17,6 +17,10 @@ fn main() {
fn app() -> Element {
rsx! {
SuspenseBoundary {
fallback: move |_| rsx! {},
LoadTitle {}
}
MessageWithLoader { id: 0 }
}
}
@ -33,6 +37,17 @@ fn MessageWithLoader(id: usize) -> Element {
}
}
#[component]
fn LoadTitle() -> Element {
let title = use_server_future(move || server_content(0))?()
.unwrap()
.unwrap();
rsx! {
Title { "{title.title}" }
}
}
#[component]
fn Message(id: usize) -> Element {
let message = use_server_future(move || server_content(id))?()

View file

@ -7,6 +7,8 @@ test("button click", async ({ page }) => {
// Expect the page to contain the counter text.
const main = page.locator("#main");
await expect(main).toContainText("hello axum! 0");
// Expect the title to contain the counter text.
await expect(page).toHaveTitle("hello axum! 0");
// Click the increment button.
let button = page.locator("button.increment-button");
@ -14,6 +16,8 @@ test("button click", async ({ page }) => {
// Expect the page to contain the updated counter text.
await expect(main).toContainText("hello axum! 1");
// Expect the title to contain the updated counter text.
await expect(page).toHaveTitle("hello axum! 1");
});
test("svg", async ({ page }) => {

View file

@ -9,6 +9,7 @@ fn app() -> Element {
rsx! {
div {
"hello axum! {num}"
Title { "hello axum! {num}" }
button { class: "increment-button", onclick: move |_| num += 1, "Increment" }
}
svg { circle { cx: 50, cy: 50, r: 40, stroke: "green", fill: "yellow" } }

View file

@ -198,6 +198,7 @@ impl Debug for LinkProps {
/// # r#"<a href="/" dioxus-prevent-default="" class="link_class active" rel="link_rel" target="_blank" aria-current="page" id="link_id">A fully configured link</a>"#
/// # );
/// ```
#[doc(alias = "<a>")]
#[allow(non_snake_case)]
pub fn Link(props: LinkProps) -> Element {
let LinkProps {

View file

@ -1,6 +1,7 @@
use super::HistoryProvider;
use crate::routable::Routable;
use dioxus_lib::prelude::*;
use document::UseEval;
use serde::{Deserialize, Serialize};
use std::sync::{Mutex, RwLock};
use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
@ -38,11 +39,6 @@ where
last_visited: usize,
}
#[derive(Serialize, Deserialize)]
struct SessionStorage {
liveview: Option<String>,
}
enum Action<R: Routable> {
GoBack,
GoForward,
@ -172,7 +168,7 @@ where
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
Arc::new(RwLock::new(Arc::new(|| {})));
let eval_provider = consume_context::<Rc<dyn EvalProvider>>();
let eval_provider = document();
let create_eval = Rc::new(move |script: &str| {
UseEval::new(eval_provider.new_evaluator(script.to_string()))

View file

@ -10,7 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"]
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["eval"]}
dioxus-html = { workspace = true, features = ["document"]}
dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true }
dioxus-interpreter-js = { workspace = true }
generational-box = { workspace = true }

View file

@ -54,15 +54,15 @@ features = [
]
[features]
default = ["panic_hook", "mounted", "file_engine", "hot_reload", "eval"]
default = ["panic_hook", "mounted", "file_engine", "hot_reload", "document"]
panic_hook = ["dep:console_error_panic_hook"]
hydrate = ["web-sys/Comment", "ciborium"]
hydrate = ["web-sys/Comment", "ciborium", "dep:serde"]
mounted = ["web-sys/Element", "dioxus-html/mounted"]
file_engine = [
"dioxus-html/file-engine",
]
hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location", "dep:serde_json", "dep:serde", "dioxus-core/serialize"]
eval = ["dioxus-html/eval", "dioxus-interpreter-js/eval", "dep:serde-wasm-bindgen", "dep:serde_json"]
document = ["dioxus-html/document", "dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"]
[dev-dependencies]
dioxus = { workspace = true, default-features = true }

View file

@ -1,5 +1,7 @@
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use dioxus_interpreter_js::eval::{JSOwner, WeakDioxusChannel, WebDioxusChannel};
use dioxus_core::ScopeId;
use dioxus_html::document::{
Document, EvalError, Evaluator, JSOwner, WeakDioxusChannel, WebDioxusChannel,
};
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use js_sys::Function;
use serde_json::Value;
@ -8,15 +10,17 @@ use std::pin::Pin;
use std::{rc::Rc, str::FromStr};
use wasm_bindgen::prelude::*;
/// Provides the WebEvalProvider through [`cx.provide_context`].
pub fn init_eval() {
let provider: Rc<dyn EvalProvider> = Rc::new(WebEvalProvider);
dioxus_core::ScopeId::ROOT.provide_context(provider);
/// Provides the WebEvalProvider through [`ScopeId::provide_context`].
pub fn init_document() {
let provider: Rc<dyn Document> = Rc::new(WebDocument);
if ScopeId::ROOT.has_context::<Rc<dyn Document>>().is_none() {
ScopeId::ROOT.provide_context(provider);
}
}
/// Represents the web-target's provider of evaluators.
pub struct WebEvalProvider;
impl EvalProvider for WebEvalProvider {
/// The web-target's document provider.
pub struct WebDocument;
impl Document for WebDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
WebEvaluator::create(js)
}

View file

@ -80,9 +80,21 @@ impl HTMLDataCursor {
}
/// An error that can occur when trying to take data from the server
#[derive(Debug)]
pub enum TakeDataError {
/// Deserializing the data failed
DeserializationError(ciborium::de::Error<std::io::Error>),
/// No data was available
DataNotAvailable,
}
impl std::fmt::Display for TakeDataError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
Self::DataNotAvailable => write!(f, "DataNotAvailable"),
}
}
}
impl std::error::Error for TakeDataError {}

View file

@ -35,8 +35,10 @@ pub mod launch;
mod mutations;
pub use event::*;
#[cfg(feature = "eval")]
mod eval;
#[cfg(feature = "document")]
mod document;
#[cfg(feature = "document")]
pub use document::WebDocument;
#[cfg(all(feature = "hot_reload", debug_assertions))]
mod hot_reload;
@ -60,8 +62,8 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! {
let mut dom = virtual_dom;
#[cfg(feature = "eval")]
dom.in_runtime(eval::init_eval);
#[cfg(feature = "document")]
dom.in_runtime(document::init_document);
#[cfg(feature = "panic_hook")]
if web_config.default_panic_hook {