Browse Source

Merge branch 'fullstack-serve' into intigrate-collect-assets

Evan Almloff 1 year ago
parent
commit
afb8578605

+ 54 - 0
.github/workflows/cli_release.yml

@@ -0,0 +1,54 @@
+name: Build CLI for Release
+
+# Will run automatically on every new release
+on:
+  release:
+    types: [published]
+
+jobs:
+  build-and-upload:
+    permissions:
+      contents: write
+    runs-on: ${{ matrix.platform.os }}
+    strategy:
+      matrix:
+        platform:
+          - {
+              target: x86_64-pc-windows-msvc,
+              os: windows-latest,
+              toolchain: "1.70.0",
+            }
+          - {
+              target: x86_64-apple-darwin,
+              os: macos-latest,
+              toolchain: "1.70.0",
+            }
+          - {
+              target: x86_64-unknown-linux-gnu,
+              os: ubuntu-latest,
+              toolchain: "1.70.0",
+            }
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install stable
+        uses: dtolnay/rust-toolchain@master
+        with:
+          toolchain: ${{ matrix.platform.toolchain }}
+          targets: ${{ matrix.platform.target }}
+
+      # Setup the Github Actions Cache for the CLI package
+      - name: Setup cache
+        uses: Swatinem/rust-cache@v2
+        with:
+          workspaces: packages/cli -> ../../target
+
+      # This neat action can build and upload the binary in one go!
+      - name: Build and upload binary
+        uses: taiki-e/upload-rust-binary-action@v1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          target: ${{ matrix.platform.target }}
+          bin: dx
+          archive: dx-${{ matrix.platform.target }}
+          checksum: sha256
+          manifest_path: packages/cli/Cargo.toml

+ 1 - 1
README.md

@@ -40,7 +40,7 @@
     <span> | </span>
     <a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
     <span> | </span>
-    <a href="https://dioxuslabs.com/docs/0.3/guide/en/"> Guide </a>
+    <a href="https://dioxuslabs.com/learn/0.4/guide"> Guide </a>
     <span> | </span>
     <a href="https://github.com/DioxusLabs/dioxus/blob/master/notes/README/ZH_CN.md"> 中文 </a>
     <span> | </span>

+ 2 - 1
docs/guide/examples/readme_expanded.rs

@@ -89,7 +89,8 @@ fn app(cx: Scope) -> Element {
                     key: None,
                     // The static template this node will use. The template is stored in a Cell so it can be replaced with a new template when hot rsx reloading is enabled
                     template: std::cell::Cell::new(TEMPLATE),
-                    root_ids: Default::default(),
+                    root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(__cx.bump())
+                        .into(),
                     dynamic_nodes: __cx.bump().alloc([
                         // The dynamic count text node (dynamic node id 0)
                         __cx.text_node(format_args!("High-Five counter: {0}", count)),

+ 2 - 0
docs/guide/src/en/interactivity/event_handlers.md

@@ -51,6 +51,8 @@ Any event handlers will still be called.
 
 > Normally, in React or JavaScript, you'd call "preventDefault" on the event in the callback. Dioxus does _not_ currently support this behavior. Note: this means you cannot conditionally prevent default behavior based on the data in the event.
 
+> Note about forms: if an event handler is attached to the `onsubmit` event of a form, default behavior is to **not submit it**, meaning having `prevent_default: "onsubmit"` will submit it in this case.
+
 ## Handler Props
 
 Sometimes, you might want to make a component that accepts an event handler. A simple example would be a `FancyButton` component, which accepts an `on_click` handler:

+ 2 - 0
docs/guide/src/pt-br/interactivity/event_handlers.md

@@ -64,3 +64,5 @@ Então, você pode usá-lo como qualquer outro manipulador:
 > Nota: assim como qualquer outro atributo, você pode nomear os manipuladores como quiser! Embora eles devam começar com `on`, para que o prop seja automaticamente transformado em um `EventHandler` no local da chamada.
 >
 > Você também pode colocar dados personalizados no evento, em vez de, por exemplo, `MouseData`
+
+> Nota sobre formulários: se um manipulador de evento está anexado ao evento `onsubmit` em um formulário, o comportamento padrão é de **não submetê-lo**. Portanto, especificar `prevent_default: "onsubmit"` irá submetê-lo.

+ 6 - 0
packages/cli/Cargo.toml

@@ -102,3 +102,9 @@ name = "dx"
 
 [dev-dependencies]
 tempfile = "3.3"
+
+[package.metadata.binstall]
+pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
+
+[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
+pkg-fmt = "zip"

+ 26 - 0
packages/cli/src/cli/build.rs

@@ -47,6 +47,32 @@ impl Build {
             Platform::Desktop => {
                 crate::builder::build_desktop(&crate_config, false)?;
             }
+            Platform::Fullstack => {
+                {
+                    let mut web_config = crate_config.clone();
+                    let web_feature = self.build.client_feature;
+                    let features = &mut web_config.features;
+                    match features {
+                        Some(features) => {
+                            features.push(web_feature);
+                        }
+                        None => web_config.features = Some(vec![web_feature]),
+                    };
+                    crate::builder::build(&crate_config, false)?;
+                }
+                {
+                    let mut desktop_config = crate_config.clone();
+                    let desktop_feature = self.build.server_feature;
+                    let features = &mut desktop_config.features;
+                    match features {
+                        Some(features) => {
+                            features.push(desktop_feature);
+                        }
+                        None => desktop_config.features = Some(vec![desktop_feature]),
+                    };
+                    crate::builder::build_desktop(&desktop_config, false)?;
+                }
+            }
         }
 
         let temp = gen_page(&crate_config, false);

+ 35 - 0
packages/cli/src/cli/cfg.rs

@@ -35,6 +35,30 @@ pub struct ConfigOptsBuild {
     /// Space separated list of features to activate
     #[clap(long)]
     pub features: Option<Vec<String>>,
+
+    /// The feature to use for the client in a fullstack app [default: "web"]
+    #[clap(long, default_value_t = { "web".to_string() })]
+    pub client_feature: String,
+
+    /// The feature to use for the server in a fullstack app [default: "ssr"]
+    #[clap(long, default_value_t = { "ssr".to_string() })]
+    pub server_feature: String,
+}
+
+impl From<ConfigOptsServe> for ConfigOptsBuild {
+    fn from(serve: ConfigOptsServe) -> Self {
+        Self {
+            target: serve.target,
+            release: serve.release,
+            verbose: serve.verbose,
+            example: serve.example,
+            profile: serve.profile,
+            platform: serve.platform,
+            features: serve.features,
+            client_feature: serve.client_feature,
+            server_feature: serve.server_feature,
+        }
+    }
 }
 
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
@@ -89,6 +113,14 @@ pub struct ConfigOptsServe {
     /// Space separated list of features to activate
     #[clap(long)]
     pub features: Option<Vec<String>>,
+
+    /// The feature to use for the client in a fullstack app [default: "web"]
+    #[clap(long, default_value_t = { "web".to_string() })]
+    pub client_feature: String,
+
+    /// The feature to use for the server in a fullstack app [default: "ssr"]
+    #[clap(long, default_value_t = { "ssr".to_string() })]
+    pub server_feature: String,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -99,6 +131,9 @@ pub enum Platform {
     #[clap(name = "desktop")]
     #[serde(rename = "desktop")]
     Desktop,
+    #[clap(name = "fullstack")]
+    #[serde(rename = "fullstack")]
+    Fullstack,
 }
 
 /// Config options for the bundling system.

+ 5 - 1
packages/cli/src/cli/serve.rs

@@ -12,6 +12,7 @@ pub struct Serve {
 impl Serve {
     pub async fn serve(self, bin: Option<PathBuf>) -> Result<()> {
         let mut crate_config = crate::CrateConfig::new(bin)?;
+        let serve_cfg = self.serve.clone();
 
         // change the relase state.
         crate_config.with_hot_reload(self.serve.hot_reload);
@@ -49,7 +50,10 @@ impl Serve {
                     .await?;
             }
             cfg::Platform::Desktop => {
-                server::desktop::startup(crate_config.clone()).await?;
+                server::desktop::startup(crate_config.clone(), &serve_cfg).await?;
+            }
+            cfg::Platform::Fullstack => {
+                server::fullstack::startup(crate_config.clone(), &serve_cfg).await?;
             }
         }
         Ok(())

+ 60 - 45
packages/cli/src/server/desktop/mod.rs

@@ -1,4 +1,5 @@
 use crate::{
+    cfg::ConfigOptsServe,
     server::{
         output::{print_console_info, PrettierOptions},
         setup_file_watcher, setup_file_watcher_hot_reload,
@@ -19,7 +20,16 @@ use tokio::sync::broadcast::{self};
 #[cfg(feature = "plugin")]
 use plugin::PluginManager;
 
-pub async fn startup(config: CrateConfig) -> Result<()> {
+use super::Platform;
+
+pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> {
+    startup_with_platform::<DesktopPlatform>(config, serve).await
+}
+
+pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
+    config: CrateConfig,
+    serve: &ConfigOptsServe,
+) -> Result<()> {
     // ctrl-c shutdown checker
     let _crate_config = config.clone();
     let _ = ctrlc::set_handler(move || {
@@ -29,17 +39,19 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
     });
 
     match config.hot_reload {
-        true => serve_hot_reload(config).await?,
-        false => serve_default(config).await?,
+        true => serve_hot_reload::<P>(config, serve).await?,
+        false => serve_default::<P>(config, serve).await?,
     }
 
     Ok(())
 }
 
 /// Start the server without hot reload
-pub async fn serve_default(config: CrateConfig) -> Result<()> {
-    let (child, first_build_result) = start_desktop(&config)?;
-    let currently_running_child: RwLock<Child> = RwLock::new(child);
+async fn serve_default<P: Platform + Send + 'static>(
+    config: CrateConfig,
+    serve: &ConfigOptsServe,
+) -> Result<()> {
+    let platform = RwLock::new(P::start(&config, serve)?);
 
     log::info!("🚀 Starting development server...");
 
@@ -49,49 +61,29 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
         {
             let config = config.clone();
 
-            move || {
-                let mut current_child = currently_running_child.write().unwrap();
-                current_child.kill()?;
-                let (child, result) = start_desktop(&config)?;
-                *current_child = child;
-                Ok(result)
-            }
+            move || platform.write().unwrap().rebuild(&config)
         },
         &config,
         None,
     )
     .await?;
 
-    // Print serve info
-    print_console_info(
-        &config,
-        PrettierOptions {
-            changed: vec![],
-            warnings: first_build_result.warnings,
-            elapsed_time: first_build_result.elapsed_time,
-        },
-        None,
-    );
-
     std::future::pending::<()>().await;
 
     Ok(())
 }
 
-/// Start the server without hot reload
-
 /// Start dx serve with hot reload
-pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
-    let (_, first_build_result) = start_desktop(&config)?;
-
-    println!("🚀 Starting development server...");
+async fn serve_hot_reload<P: Platform + Send + 'static>(
+    config: CrateConfig,
+    serve: &ConfigOptsServe,
+) -> Result<()> {
+    let platform = RwLock::new(P::start(&config, serve)?);
 
     // Setup hot reload
     let FileMapBuildResult { map, errors } =
         FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
 
-    println!("🚀 Starting development server...");
-
     for err in errors {
         log::error!("{}", err);
     }
@@ -119,24 +111,13 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
                 for channel in &mut *channels.lock().unwrap() {
                     send_msg(HotReloadMsg::Shutdown, channel);
                 }
-                Ok(start_desktop(&config)?.1)
+                platform.write().unwrap().rebuild(&config)
             }
         },
         None,
     )
     .await?;
 
-    // Print serve info
-    print_console_info(
-        &config,
-        PrettierOptions {
-            changed: vec![],
-            warnings: first_build_result.warnings,
-            elapsed_time: first_build_result.elapsed_time,
-        },
-        None,
-    );
-
     clear_paths();
 
     match LocalSocketListener::bind("@dioxusin") {
@@ -228,7 +209,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
     }
 }
 
-pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
+fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
     // Run the desktop application
     let result = crate::builder::build_desktop(config, true)?;
 
@@ -249,3 +230,37 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
         }
     }
 }
+
+pub(crate) struct DesktopPlatform {
+    currently_running_child: Child,
+}
+
+impl Platform for DesktopPlatform {
+    fn start(config: &CrateConfig, _serve: &ConfigOptsServe) -> Result<Self> {
+        let (child, first_build_result) = start_desktop(config)?;
+
+        log::info!("🚀 Starting development server...");
+
+        // Print serve info
+        print_console_info(
+            config,
+            PrettierOptions {
+                changed: vec![],
+                warnings: first_build_result.warnings,
+                elapsed_time: first_build_result.elapsed_time,
+            },
+            None,
+        );
+
+        Ok(Self {
+            currently_running_child: child,
+        })
+    }
+
+    fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult> {
+        self.currently_running_child.kill()?;
+        let (child, result) = start_desktop(config)?;
+        self.currently_running_child = child;
+        Ok(result)
+    }
+}

+ 72 - 0
packages/cli/src/server/fullstack/mod.rs

@@ -0,0 +1,72 @@
+use crate::{
+    cfg::{ConfigOptsBuild, ConfigOptsServe},
+    CrateConfig, Result,
+};
+
+use super::{desktop, Platform};
+
+pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> {
+    desktop::startup_with_platform::<FullstackPlatform>(config, serve).await
+}
+
+struct FullstackPlatform {
+    serve: ConfigOptsServe,
+    desktop: desktop::DesktopPlatform,
+}
+
+impl Platform for FullstackPlatform {
+    fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        {
+            build_web(serve.clone())?;
+        }
+
+        let mut desktop_config = config.clone();
+        let desktop_feature = serve.server_feature.clone();
+        let features = &mut desktop_config.features;
+        match features {
+            Some(features) => {
+                features.push(desktop_feature);
+            }
+            None => desktop_config.features = Some(vec![desktop_feature]),
+        };
+        let desktop = desktop::DesktopPlatform::start(&desktop_config, serve)?;
+
+        Ok(Self {
+            desktop,
+            serve: serve.clone(),
+        })
+    }
+
+    fn rebuild(&mut self, crate_config: &CrateConfig) -> Result<crate::BuildResult> {
+        build_web(self.serve.clone())?;
+        {
+            let mut desktop_config = crate_config.clone();
+            let desktop_feature = self.serve.server_feature.clone();
+            let features = &mut desktop_config.features;
+            match features {
+                Some(features) => {
+                    features.push(desktop_feature);
+                }
+                None => desktop_config.features = Some(vec![desktop_feature]),
+            };
+            self.desktop.rebuild(&desktop_config)
+        }
+    }
+}
+
+fn build_web(serve: ConfigOptsServe) -> Result<()> {
+    let mut web_config: ConfigOptsBuild = serve.into();
+    let web_feature = web_config.client_feature.clone();
+    let features = &mut web_config.features;
+    match features {
+        Some(features) => {
+            features.push(web_feature);
+        }
+        None => web_config.features = Some(vec![web_feature]),
+    };
+    web_config.platform = Some(crate::cfg::Platform::Web);
+    crate::cli::build::Build { build: web_config }.build(None)
+}

+ 9 - 1
packages/cli/src/server/mod.rs

@@ -1,4 +1,4 @@
-use crate::{BuildResult, CrateConfig, Result};
+use crate::{cfg::ConfigOptsServe, BuildResult, CrateConfig, Result};
 
 use cargo_metadata::diagnostic::Diagnostic;
 use dioxus_core::Template;
@@ -14,6 +14,7 @@ use tokio::sync::broadcast::Sender;
 mod output;
 use output::*;
 pub mod desktop;
+pub mod fullstack;
 pub mod web;
 
 /// Sets up a file watcher
@@ -180,3 +181,10 @@ async fn setup_file_watcher_hot_reload<F: Fn() -> Result<BuildResult> + Send + '
 
     Ok(watcher)
 }
+
+pub(crate) trait Platform {
+    fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
+    where
+        Self: Sized;
+    fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
+}

+ 64 - 63
packages/cli/src/server/web/mod.rs

@@ -5,7 +5,7 @@ use crate::{
         output::{print_console_info, PrettierOptions, WebServerInfo},
         setup_file_watcher, setup_file_watcher_hot_reload,
     },
-    BuildResult, CrateConfig, Result,
+    BuildResult, CrateConfig, Result, WebHttpsConfig,
 };
 use axum::{
     body::{Full, HttpBody},
@@ -67,7 +67,7 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res
 }
 
 /// Start the server without hot reload
-pub async fn serve_default(
+async fn serve_default(
     ip: String,
     port: u16,
     config: CrateConfig,
@@ -128,7 +128,7 @@ pub async fn serve_default(
 }
 
 /// Start dx serve with hot reload
-pub async fn serve_hot_reload(
+async fn serve_hot_reload(
     ip: String,
     port: u16,
     config: CrateConfig,
@@ -218,67 +218,12 @@ async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
         return Ok(None);
     }
 
-    let (cert_path, key_path) = match web_config.mkcert {
+    let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
         // mkcert, use it
-        Some(true) => {
-            // Get paths to store certs, otherwise use ssl/item.pem
-            let key_path = web_config
-                .key_path
-                .clone()
-                .unwrap_or(DEFAULT_KEY_PATH.to_string());
-
-            let cert_path = web_config
-                .cert_path
-                .clone()
-                .unwrap_or(DEFAULT_CERT_PATH.to_string());
-
-            // Create ssl directory if using defaults
-            if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
-                _ = fs::create_dir("ssl");
-            }
-
-            let cmd = Command::new("mkcert")
-                .args([
-                    "-install",
-                    "-key-file",
-                    &key_path,
-                    "-cert-file",
-                    &cert_path,
-                    "localhost",
-                    "::1",
-                    "127.0.0.1",
-                ])
-                .spawn();
-
-            match cmd {
-                Err(e) => {
-                    match e.kind() {
-                        io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
-                        e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
-                    };
-                    return Err("failed to generate mkcert certificates".into());
-                }
-                Ok(mut cmd) => {
-                    cmd.wait()?;
-                }
-            }
-
-            (cert_path, key_path)
-        }
-        // not mkcert
-        Some(false) => {
-            // get paths to cert & key
-            if let (Some(key), Some(cert)) =
-                (web_config.key_path.clone(), web_config.cert_path.clone())
-            {
-                (cert, key)
-            } else {
-                // missing cert or key
-                return Err("https is enabled but cert or key path is missing".into());
-            }
-        }
-        // other
-        _ => return Ok(None),
+        get_rustls_with_mkcert(web_config)?
+    } else {
+        // if mkcert not specified or false, don't use it
+        get_rustls_without_mkcert(web_config)?
     };
 
     Ok(Some(
@@ -286,6 +231,62 @@ async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
     ))
 }
 
+fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
+    // Get paths to store certs, otherwise use ssl/item.pem
+    let key_path = web_config
+        .key_path
+        .clone()
+        .unwrap_or(DEFAULT_KEY_PATH.to_string());
+
+    let cert_path = web_config
+        .cert_path
+        .clone()
+        .unwrap_or(DEFAULT_CERT_PATH.to_string());
+
+    // Create ssl directory if using defaults
+    if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
+        _ = fs::create_dir("ssl");
+    }
+
+    let cmd = Command::new("mkcert")
+        .args([
+            "-install",
+            "-key-file",
+            &key_path,
+            "-cert-file",
+            &cert_path,
+            "localhost",
+            "::1",
+            "127.0.0.1",
+        ])
+        .spawn();
+
+    match cmd {
+        Err(e) => {
+            match e.kind() {
+                io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
+                e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
+            };
+            return Err("failed to generate mkcert certificates".into());
+        }
+        Ok(mut cmd) => {
+            cmd.wait()?;
+        }
+    }
+
+    Ok((cert_path, key_path))
+}
+
+fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
+    // get paths to cert & key
+    if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
+        Ok((cert, key))
+    } else {
+        // missing cert or key
+        Err("https is enabled but cert or key path is missing".into())
+    }
+}
+
 /// Sets up and returns a router
 async fn setup_router(
     config: CrateConfig,

+ 6 - 2
packages/core/src/create.rs

@@ -87,8 +87,12 @@ impl<'b> VirtualDom {
             }
         }
 
-        // Intialize the root nodes slice
-        *node.root_ids.borrow_mut() = vec![ElementId(0); node.template.get().roots.len()];
+        // Initialize the root nodes slice
+        {
+            let mut nodes_mut = node.root_ids.borrow_mut();
+            let len = node.template.get().roots.len();
+            nodes_mut.resize(len, ElementId::default());
+        };
 
         // The best renderers will have templates prehydrated and registered
         // Just in case, let's create the template using instructions anyways

+ 9 - 1
packages/core/src/diff.rs

@@ -15,6 +15,7 @@ use DynamicNode::*;
 
 impl<'b> VirtualDom {
     pub(super) fn diff_scope(&mut self, scope: ScopeId) {
+        self.runtime.scope_stack.borrow_mut().push(scope);
         let scope_state = &mut self.get_scope(scope).unwrap();
         unsafe {
             // Load the old and new bump arenas
@@ -45,6 +46,7 @@ impl<'b> VirtualDom {
                 (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
             };
         }
+        self.runtime.scope_stack.borrow_mut().pop();
     }
 
     fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
@@ -126,7 +128,13 @@ impl<'b> VirtualDom {
             });
 
         // Make sure the roots get transferred over while we're here
-        *right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone();
+        {
+            let mut right = right_template.root_ids.borrow_mut();
+            right.clear();
+            for &element in left_template.root_ids.borrow().iter() {
+                right.push(element);
+            }
+        }
 
         let root_ids = right_template.root_ids.borrow();
 

+ 7 - 7
packages/core/src/nodes.rs

@@ -54,7 +54,7 @@ pub struct VNode<'a> {
 
     /// The IDs for the roots of this template - to be used when moving the template around and removing it from
     /// the actual Dom
-    pub root_ids: RefCell<Vec<ElementId>>,
+    pub root_ids: RefCell<bumpalo::collections::Vec<'a, ElementId>>,
 
     /// The dynamic parts of the template
     pub dynamic_nodes: &'a [DynamicNode<'a>],
@@ -65,11 +65,11 @@ pub struct VNode<'a> {
 
 impl<'a> VNode<'a> {
     /// Create a template with no nodes that will be skipped over during diffing
-    pub fn empty() -> Element<'a> {
+    pub fn empty(cx: &'a ScopeState) -> Element<'a> {
         Some(VNode {
             key: None,
             parent: None,
-            root_ids: Default::default(),
+            root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
             dynamic_nodes: &[],
             dynamic_attrs: &[],
             template: Cell::new(Template {
@@ -698,7 +698,7 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
 impl<'a, 'b> IntoDynNode<'b> for &'a str {
     fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
         DynamicNode::Text(VText {
-            value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(),
+            value: cx.bump().alloc_str(self),
             id: Default::default(),
         })
     }
@@ -741,10 +741,10 @@ impl<'a> IntoTemplate<'a> for VNode<'a> {
     }
 }
 impl<'a> IntoTemplate<'a> for Element<'a> {
-    fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
+    fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
         match self {
-            Some(val) => val.into_template(_cx),
-            _ => VNode::empty().unwrap(),
+            Some(val) => val.into_template(cx),
+            _ => VNode::empty(cx).unwrap(),
         }
     }
 }

+ 2 - 2
packages/core/tests/fuzzing.rs

@@ -179,7 +179,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
                 node_paths: &[&[0]],
                 attr_paths: &[],
             }),
-            root_ids: Default::default(),
+            root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
             dynamic_nodes: cx.bump().alloc([cx.component(
                 create_random_element,
                 DepthProps { depth, root: false },
@@ -276,7 +276,7 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                 key: None,
                 parent: None,
                 template: Cell::new(template),
-                root_ids: Default::default(),
+                root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
                 dynamic_nodes: {
                     let dynamic_nodes: Vec<_> = dynamic_node_types
                         .iter()

+ 14 - 0
packages/hooks/src/use_shared_state.rs

@@ -282,6 +282,20 @@ impl<T> UseSharedState<T> {
             ),
         }
     }
+
+    /// Take a reference to the inner value temporarily and produce a new value
+    #[cfg_attr(debug_assertions, track_caller)]
+    #[cfg_attr(debug_assertions, inline(never))]
+    pub fn with<O>(&self, immutable_callback: impl FnOnce(&T) -> O) -> O {
+        immutable_callback(&*self.read())
+    }
+
+    /// Take a mutable reference to the inner value temporarily and produce a new value
+    #[cfg_attr(debug_assertions, track_caller)]
+    #[cfg_attr(debug_assertions, inline(never))]
+    pub fn with_mut<O>(&self, mutable_callback: impl FnOnce(&mut T) -> O) -> O {
+        mutable_callback(&mut *self.write())
+    }
 }
 
 impl<T> Clone for UseSharedState<T> {

+ 10 - 1
packages/html/src/events/form.rs

@@ -1,4 +1,4 @@
-use std::{collections::HashMap, fmt::Debug};
+use std::{any::Any, collections::HashMap, fmt::Debug};
 
 use dioxus_core::Event;
 
@@ -45,6 +45,12 @@ impl FileEngine for SerializedFileEngine {
             .await
             .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
     }
+
+    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
+        self.read_file(file)
+            .await
+            .map(|val| Box::new(val) as Box<dyn Any>)
+    }
 }
 
 #[cfg(feature = "serialize")]
@@ -89,6 +95,9 @@ pub trait FileEngine {
 
     // read a file to string
     async fn read_file_to_string(&self, file: &str) -> Option<String>;
+
+    // returns a file in platform's native representation
+    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>>;
 }
 
 impl_event! {

+ 6 - 0
packages/html/src/native_bind/native_file_engine.rs

@@ -1,3 +1,4 @@
+use std::any::Any;
 use std::path::PathBuf;
 
 use crate::FileEngine;
@@ -40,4 +41,9 @@ impl FileEngine for NativeFileEngine {
 
         Some(contents)
     }
+
+    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
+        let file = File::open(file).await.ok()?;
+        Some(Box::new(file))
+    }
 }

+ 3 - 2
packages/native-core/tests/fuzzing.rs

@@ -187,7 +187,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
                 node_paths: &[&[0]],
                 attr_paths: &[],
             }),
-            root_ids: Default::default(),
+            root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
             dynamic_nodes: cx.bump().alloc([cx.component(
                 create_random_element,
                 DepthProps { depth, root: false },
@@ -257,7 +257,8 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                 key: None,
                 parent: None,
                 template: Cell::new(template),
-                root_ids: Default::default(),
+                root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump())
+                    .into(),
                 dynamic_nodes: {
                     let dynamic_nodes: Vec<_> = dynamic_node_types
                         .iter()

+ 2 - 1
packages/rsx/src/lib.rs

@@ -231,6 +231,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
 
         // Render and release the mutable borrow on context
         let roots = quote! { #( #root_printer ),* };
+        let root_count = self.roots.len();
         let node_printer = &context.dynamic_nodes;
         let dyn_attr_printer = &context.dynamic_attributes;
         let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*]));
@@ -247,7 +248,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
                 parent: None,
                 key: #key_tokens,
                 template: std::cell::Cell::new(TEMPLATE),
-                root_ids: Default::default(),
+                root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(),
                 dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
                 dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
             }

+ 23 - 0
packages/web/src/file_engine.rs

@@ -1,3 +1,5 @@
+use std::any::Any;
+
 use dioxus_html::FileEngine;
 use futures_channel::oneshot;
 use js_sys::Uint8Array;
@@ -100,4 +102,25 @@ impl FileEngine for WebFileEngine {
             None
         }
     }
+
+    async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
+        let file = self.find(file)?;
+        Some(Box::new(file))
+    }
+}
+
+/// Helper trait for WebFileEngine
+#[async_trait::async_trait(?Send)]
+pub trait WebFileEngineExt {
+    /// returns web_sys::File
+    async fn get_web_file(&self, file: &str) -> Option<web_sys::File>;
+}
+
+#[async_trait::async_trait(?Send)]
+impl WebFileEngineExt for std::sync::Arc<dyn FileEngine> {
+    async fn get_web_file(&self, file: &str) -> Option<web_sys::File> {
+        let native_file = self.get_native_file(file).await?;
+        let ret = native_file.downcast::<web_sys::File>().ok()?;
+        Some(*ret)
+    }
 }

+ 1 - 0
packages/web/src/lib.rs

@@ -54,6 +54,7 @@
 //     - Do DOM work in the next requestAnimationFrame callback
 
 pub use crate::cfg::Config;
+pub use crate::file_engine::WebFileEngineExt;
 use dioxus_core::{Element, Scope, VirtualDom};
 use futures_util::{
     future::{select, Either},