diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index 4cd33f47e..544c375a2 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -36,6 +36,8 @@ jobs: toolchain: ${{ matrix.platform.toolchain }} targets: ${{ matrix.platform.target }} + - uses: ilammy/setup-nasm@v1 + # Setup the Github Actions Cache for the CLI package - name: Setup cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b1f1ed7b..8001af650 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,6 +39,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: actions/checkout@v4 @@ -51,6 +52,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: davidB/rust-cargo-make@v1 @@ -66,6 +68,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: rustup component add rustfmt - uses: actions/checkout@v4 - run: cargo fmt --all -- --check @@ -77,6 +80,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy @@ -124,6 +128,8 @@ jobs: } steps: + - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - name: install stable uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index f9ad3855f..6c8a260b3 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -26,8 +26,8 @@ env: RUST_BACKTRACE: 1 # Change to specific Rust release to pin rust_stable: stable - rust_nightly: nightly-2022-11-03 - rust_clippy: 1.65.0 + rust_nightly: nightly-2023-11-16 + rust_clippy: 1.70.0 # When updating this, also update: # - README.md # - tokio/README.md @@ -70,6 +70,7 @@ jobs: run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - name: Install Rust ${{ env.rust_nightly }} uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7b23c6111..6b1b5acf0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,6 +20,7 @@ jobs: steps: # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - uses: actions/setup-node@v4 with: node-version: 16 diff --git a/examples/compose.rs b/examples/compose.rs index 133192281..823e47a57 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -61,7 +61,7 @@ fn compose(cx: Scope) -> Element { }, "Click to send" } - + input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" } } }) diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 1253efba2..7eb9db375 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element { p { "This should show an image:" } - img { src: "examples/assets/logo.png" } + img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() } } }) } diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 88b37789d..f0534cbf8 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -18,13 +18,14 @@ fn main() { ); } +const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css")); + fn app(cx: Scope) -> Element { let files = use_ref(cx, Files::new); cx.render(rsx! { div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } - style { include_str!("./assets/fileexplorer.css") } header { i { class: "material-icons icon-menu", "menu" } h1 { "Files: ", files.read().current() } diff --git a/examples/tailwind/Dioxus.toml b/examples/tailwind/Dioxus.toml index b440d6a69..b7e78ff7f 100644 --- a/examples/tailwind/Dioxus.toml +++ b/examples/tailwind/Dioxus.toml @@ -30,7 +30,7 @@ watch_path = ["src", "public"] [web.resource] # CSS style file -style = ["/tailwind.css"] +style = [] # Javascript code file script = [] diff --git a/examples/tailwind/dist/tailwind3531548035813279582.css b/examples/tailwind/dist/tailwind3531548035813279582.css new file mode 100644 index 000000000..2196a9412 --- /dev/null +++ b/examples/tailwind/dist/tailwind3531548035813279582.css @@ -0,0 +1 @@ +*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-16{margin-bottom:4rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-5{margin-right:1.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-5\/6{width:83.3333%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-2{padding:.5rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.leading-relaxed{line-height:1.625}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}@media (width>=640px){.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (width>=768px){.md\:mb-0{margin-bottom:0}.md\:ml-auto{margin-left:auto}.md\:mt-0{margin-top:0}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:pr-16{padding-right:4rem}.md\:text-left{text-align:left}}@media (width>=1024px){.lg\:inline-block{display:inline-block}.lg\:w-full{width:100%}.lg\:max-w-lg{max-width:32rem}.lg\:flex-grow{flex-grow:1}.lg\:pr-24{padding-right:6rem}} \ No newline at end of file diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs index 8a49071db..10124e47c 100644 --- a/examples/tailwind/src/main.rs +++ b/examples/tailwind/src/main.rs @@ -2,13 +2,11 @@ use dioxus::prelude::*; +const _STYLE: &str = mg!(file("./public/tailwind.css")); + fn main() { #[cfg(not(target_arch = "wasm32"))] - dioxus_desktop::launch_cfg( - app, - dioxus_desktop::Config::new() - .with_custom_head(r#""#.to_string()), - ); + dioxus_desktop::launch(app); #[cfg(target_arch = "wasm32")] dioxus_web::launch(app); } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 34e96c285..cdf41aaa0 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -7,6 +7,8 @@ fn main() { dioxus_desktop::launch(app); } +const _STYLE: &str = mg!(file("./examples/assets/todomvc.css")); + #[derive(PartialEq, Eq, Clone, Copy)] pub enum FilterState { All, @@ -47,7 +49,6 @@ pub fn app(cx: Scope<()>) -> Element { cx.render(rsx! { section { class: "todoapp", - style { include_str!("./assets/todomvc.css") } TodoHeader { todos: todos } section { class: "main", if !todos.is_empty() { diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 4c115184e..4335faa22 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -30,7 +30,7 @@ cargo_metadata = "0.15.0" tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] } atty = "0.2.14" chrono = "0.4.19" -anyhow = "1.0.53" +anyhow = "1" hyper = "0.14.17" hyper-rustls = "0.23.2" indicatif = "0.17.5" @@ -75,6 +75,8 @@ toml_edit = "0.19.11" tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } tauri-utils = "=1.4.*" +manganis-cli-support= { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } + dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } rsx-rosetta = { workspace = true } diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index a5721d7a6..6d1ccea05 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -2,31 +2,35 @@ use crate::{ config::{CrateConfig, ExecutableType}, error::{Error, Result}, tools::Tool, - DioxusConfig, }; use cargo_metadata::{diagnostic::Diagnostic, Message}; use indicatif::{ProgressBar, ProgressStyle}; +use lazy_static::lazy_static; +use manganis_cli_support::AssetManifestExt; use serde::Serialize; use std::{ fs::{copy, create_dir_all, File}, - io::Read, + io::{Read, Write}, panic, path::PathBuf, time::Duration, }; use wasm_bindgen_cli_support::Bindgen; +lazy_static! { + static ref PROGRESS_BARS: indicatif::MultiProgress = indicatif::MultiProgress::new(); +} + #[derive(Serialize, Debug, Clone)] pub struct BuildResult { pub warnings: Vec, pub elapsed_time: u128, } -#[allow(unused)] -pub fn build(config: &CrateConfig, quiet: bool) -> Result { +pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result { // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) // [2] Generate the appropriate build folders - // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm + // [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm // [4] Wasm-opt the .wasm file with whatever optimizations need to be done // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary // [6] Link up the html page to the wasm module @@ -41,6 +45,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .. } = config; + let _gaurd = WebAssetConfigDropGuard::new(); + // start to build the assets let ignore_files = build_assets(config)?; @@ -60,8 +66,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .output()?; } - let cmd = subprocess::Exec::cmd("cargo"); - let cmd = cmd + let cmd = subprocess::Exec::cmd("cargo") + .env("CARGO_TARGET_DIR", target_dir) .cwd(crate_dir) .arg("build") .arg("--target") @@ -252,19 +258,28 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } } + if !skip_assets { + process_assets(config)?; + } + Ok(BuildResult { warnings: warning_messages, elapsed_time: t_start.elapsed().as_millis(), }) } -pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { +pub fn build_desktop( + config: &CrateConfig, + _is_serve: bool, + skip_assets: bool, +) -> Result { log::info!("🚅 Running build [Desktop] command..."); let t_start = std::time::Instant::now(); let ignore_files = build_assets(config)?; let mut cmd = subprocess::Exec::cmd("cargo") + .env("CARGO_TARGET_DIR", &config.target_dir) .cwd(&config.crate_dir) .arg("build") .arg("--message-format=json"); @@ -375,6 +390,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result Result<()> { + let manifest = config.asset_manifest(); + let mut file = File::create(config.out_dir.join("__assets_head.html"))?; + file.write_all(manifest.head().as_bytes())?; + Ok(()) +} + fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { let mut warning_messages: Vec = vec![]; - let pb = ProgressBar::new_spinner(); + let mut pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(200)); + pb = PROGRESS_BARS.add(pb); pb.set_style( ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}") .unwrap() @@ -406,14 +436,6 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { ); pb.set_message("💼 Waiting to start build the project..."); - struct StopSpinOnDrop(ProgressBar); - - impl Drop for StopSpinOnDrop { - fn drop(&mut self) { - self.0.finish_and_clear(); - } - } - let stdout = cmd.detached().stream_stdout()?; let reader = std::io::BufReader::new(stdout); @@ -449,13 +471,17 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { std::process::exit(1); } } - _ => (), // Unknown message + _ => { + // Unknown message + } } } Ok(warning_messages) } -pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { +pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String { + let _gaurd = WebAssetConfigDropGuard::new(); + let crate_root = crate::cargo::crate_root().unwrap(); let custom_html_file = crate_root.join("index.html"); let mut html = if custom_html_file.is_file() { @@ -470,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { String::from(include_str!("./assets/index.html")) }; - let resources = config.web.resource.clone(); + let resources = config.dioxus_config.web.resource.clone(); let mut style_list = resources.style.unwrap_or_default(); let mut script_list = resources.script.unwrap_or_default(); @@ -490,6 +516,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { )) } if config + .dioxus_config .application .tools .clone() @@ -498,6 +525,10 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { { style_str.push_str("\n"); } + if !skip_assets { + let manifest = config.asset_manifest(); + style_str.push_str(&manifest.head()); + } replace_or_insert_before("{style_include}", &style_str, " String { ); } - let base_path = match &config.web.app.base_path { + let base_path = match &config.dioxus_config.web.app.base_path { Some(path) => path, None => ".", }; - let app_name = &config.application.name; + let app_name = &config.dioxus_config.application.name; // Check if a script already exists if html.contains("{app_name}") && html.contains("{base_path}") { html = html.replace("{app_name}", app_name); @@ -547,6 +578,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { } let title = config + .dioxus_config .web .app .title @@ -716,3 +748,42 @@ fn build_assets(config: &CrateConfig) -> Result> { Ok(result) } + +/// Process any assets collected from the binary +fn process_assets(config: &CrateConfig) -> anyhow::Result<()> { + let manifest = config.asset_manifest(); + + let static_asset_output_dir = PathBuf::from( + config + .dioxus_config + .web + .app + .base_path + .clone() + .unwrap_or_default(), + ); + let static_asset_output_dir = config.out_dir.join(static_asset_output_dir); + + manifest.copy_static_assets_to(static_asset_output_dir)?; + + Ok(()) +} + +pub(crate) struct WebAssetConfigDropGuard; + +impl WebAssetConfigDropGuard { + pub fn new() -> Self { + // Set up the collect asset config + manganis_cli_support::Config::default() + .with_assets_serve_location("/") + .save(); + Self {} + } +} + +impl Drop for WebAssetConfigDropGuard { + fn drop(&mut self) { + // Reset the config + manganis_cli_support::Config::default().save(); + } +} diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 0deeb38ca..d8032f034 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,6 +1,8 @@ -use crate::cfg::Platform; #[cfg(feature = "plugin")] use crate::plugin::PluginManager; +use crate::server::fullstack::FullstackServerEnvGuard; +use crate::server::fullstack::FullstackWebEnvGuard; +use crate::{cfg::Platform, WebAssetConfigDropGuard}; use super::*; @@ -13,23 +15,26 @@ pub struct Build { } impl Build { - pub fn build(self, bin: Option) -> Result<()> { + pub fn build(self, bin: Option, target_dir: Option<&std::path::Path>) -> Result<()> { let mut crate_config = crate::CrateConfig::new(bin)?; + if let Some(target_dir) = target_dir { + crate_config.target_dir = target_dir.to_path_buf(); + } // change the release state. crate_config.with_release(self.build.release); crate_config.with_verbose(self.build.verbose); if self.build.example.is_some() { - crate_config.as_example(self.build.example.unwrap()); + crate_config.as_example(self.build.example.clone().unwrap()); } if self.build.profile.is_some() { - crate_config.set_profile(self.build.profile.unwrap()); + crate_config.set_profile(self.build.profile.clone().unwrap()); } if self.build.features.is_some() { - crate_config.set_features(self.build.features.unwrap()); + crate_config.set_features(self.build.features.clone().unwrap()); } let platform = self @@ -37,25 +42,56 @@ impl Build { .platform .unwrap_or(crate_config.dioxus_config.application.default_platform); - if let Some(target) = self.build.target { + if let Some(target) = self.build.target.clone() { crate_config.set_target(target); } - crate_config.set_cargo_args(self.build.cargo_args); + crate_config.set_cargo_args(self.build.cargo_args.clone()); // #[cfg(feature = "plugin")] // let _ = PluginManager::on_build_start(&crate_config, &platform); match platform { Platform::Web => { - crate::builder::build(&crate_config, true)?; + crate::builder::build(&crate_config, false, self.build.skip_assets)?; } Platform::Desktop => { - crate::builder::build_desktop(&crate_config, false)?; + crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?; + } + Platform::Fullstack => { + // Fullstack mode must be built with web configs on the desktop (server) binary as well as the web binary + let _config = WebAssetConfigDropGuard::new(); + { + let mut web_config = crate_config.clone(); + let _gaurd = FullstackWebEnvGuard::new(&self.build); + 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, self.build.skip_assets)?; + } + { + 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]), + }; + let _gaurd = + FullstackServerEnvGuard::new(self.build.force_debug, self.build.release); + crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?; + } } } - let temp = gen_page(&crate_config.dioxus_config, false); + let temp = gen_page(&crate_config, false, self.build.skip_assets); let mut file = std::fs::File::create( crate_config diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index fd97930ed..aaa823373 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -83,7 +83,7 @@ impl Bundle { crate_config.set_cargo_args(self.build.cargo_args); // build the desktop app - build_desktop(&crate_config, false)?; + build_desktop(&crate_config, false, false)?; // copy the binary to the out dir let package = crate_config.manifest.package.unwrap(); @@ -134,6 +134,19 @@ impl Bundle { } } + // Add all assets from collect assets to the bundle + { + let config = manganis_cli_support::Config::current(); + let location = config.assets_serve_location().to_string(); + let location = format!("./{}", location); + println!("Adding assets from {} to bundle", location); + if let Some(resources) = &mut bundle_settings.resources { + resources.push(location); + } else { + bundle_settings.resources = Some(vec![location]); + } + } + let mut settings = SettingsBuilder::new() .project_out_directory(crate_config.out_dir) .package_settings(PackageSettings { diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 0d2a5e212..62212e574 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -11,6 +11,11 @@ pub struct ConfigOptsBuild { #[serde(default)] pub release: bool, + /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] + #[clap(long)] + #[serde(default)] + pub force_debug: bool, + // Use verbose output [default: false] #[clap(long)] #[serde(default)] @@ -28,10 +33,23 @@ pub struct ConfigOptsBuild { #[clap(long, value_enum)] pub platform: Option, + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub skip_assets: bool, + /// Space separated list of features to activate #[clap(long)] pub features: Option>, + /// 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, + /// Rustc platform triple #[clap(long)] pub target: Option, @@ -41,6 +59,25 @@ pub struct ConfigOptsBuild { pub cargo_args: Vec, } +impl From 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, + skip_assets: serve.skip_assets, + force_debug: serve.force_debug, + cargo_args: serve.cargo_args, + } + } +} + #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsServe { /// Port of dev server @@ -62,6 +99,11 @@ pub struct ConfigOptsServe { #[serde(default)] pub release: bool, + /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] + #[clap(long)] + #[serde(default)] + pub force_debug: bool, + // Use verbose output [default: false] #[clap(long)] #[serde(default)] @@ -71,7 +113,7 @@ pub struct ConfigOptsServe { #[clap(long)] pub profile: Option, - /// Build platform: support Web & Desktop [default: "default_platform"] + /// Build platform: support Web, Desktop, and Fullstack [default: "default_platform"] #[clap(long, value_enum)] pub platform: Option, @@ -90,6 +132,19 @@ pub struct ConfigOptsServe { #[clap(long)] pub features: Option>, + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub skip_assets: bool, + + /// 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, + /// Rustc platform triple #[clap(long)] pub target: Option, @@ -107,6 +162,9 @@ pub enum Platform { #[clap(name = "desktop")] #[serde(rename = "desktop")] Desktop, + #[clap(name = "fullstack")] + #[serde(rename = "fullstack")] + Fullstack, } /// Config options for the bundling system. diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 5fed1e1a6..9a0f0d4fb 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -28,6 +28,12 @@ impl Clean { remove_dir_all(crate_config.crate_dir.join(&out_dir))?; } + let fullstack_out_dir = crate_config.crate_dir.join(".dioxus"); + + if fullstack_out_dir.is_dir() { + remove_dir_all(fullstack_out_dir)?; + } + Ok(()) } } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index e08b02339..1bb24ecdb 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -12,6 +12,7 @@ pub struct Serve { impl Serve { pub async fn serve(self, bin: Option) -> 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); @@ -48,21 +49,29 @@ impl Serve { match platform { cfg::Platform::Web => { // generate dev-index page - Serve::regen_dev_page(&crate_config)?; + Serve::regen_dev_page(&crate_config, self.serve.skip_assets)?; // start the develop server - server::web::startup(self.serve.port, crate_config.clone(), self.serve.open) - .await?; + server::web::startup( + self.serve.port, + crate_config.clone(), + self.serve.open, + self.serve.skip_assets, + ) + .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(()) } - pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> { - let serve_html = gen_page(&crate_config.dioxus_config, true); + pub fn regen_dev_page(crate_config: &CrateConfig, skip_assets: bool) -> Result<()> { + let serve_html = gen_page(crate_config, true, skip_assets); let dist_path = crate_config.crate_dir.join( crate_config diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 727504e6a..5b4f23524 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -1,4 +1,6 @@ use crate::{cfg::Platform, error::Result}; +use manganis_cli_support::AssetManifest; +use manganis_cli_support::AssetManifestExt; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -303,6 +305,13 @@ impl CrateConfig { }) } + pub fn asset_manifest(&self) -> AssetManifest { + AssetManifest::load_from_path( + self.crate_dir.join("Cargo.toml"), + self.workspace_dir.join("Cargo.lock"), + ) + } + pub fn as_example(&mut self, example_name: String) -> &mut Self { self.executable = ExecutableType::Example(example_name); self diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 2358551f2..0695d7ac8 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -63,7 +63,7 @@ async fn main() -> anyhow::Result<()> { .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), Build(opts) if bin.is_ok() => opts - .build(Some(bin.unwrap().clone())) + .build(Some(bin.unwrap().clone()), None) .map_err(|e| anyhow!("🚫 Building project failed: {}", e)), Clean(opts) if bin.is_ok() => opts diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index ec21fa4c0..34f85a312 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -1,11 +1,12 @@ +use crate::server::Platform; use crate::{ + cfg::ConfigOptsServe, server::{ output::{print_console_info, PrettierOptions}, setup_file_watcher, }, BuildResult, CrateConfig, Result, }; - use dioxus_hot_reload::HotReloadMsg; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; @@ -21,7 +22,14 @@ use plugin::PluginManager; use super::HotReloadState; -pub async fn startup(config: CrateConfig) -> Result<()> { +pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> { + startup_with_platform::(config, serve).await +} + +pub(crate) async fn startup_with_platform( + config: CrateConfig, + serve_cfg: &ConfigOptsServe, +) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); let _ = ctrlc::set_handler(move || { @@ -51,15 +59,18 @@ pub async fn startup(config: CrateConfig) -> Result<()> { false => None, }; - serve(config, hot_reload_state).await?; + serve::

(config, serve_cfg, hot_reload_state).await?; Ok(()) } /// Start the server without hot reload -pub async fn serve(config: CrateConfig, hot_reload_state: Option) -> Result<()> { - let (child, first_build_result) = start_desktop(&config)?; - let currently_running_child: RwLock = RwLock::new(child); +async fn serve( + config: CrateConfig, + serve: &ConfigOptsServe, + hot_reload_state: Option, +) -> Result<()> { + let platform = RwLock::new(P::start(&config, serve)?); log::info!("🚀 Starting development server..."); @@ -68,15 +79,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option let _watcher = setup_file_watcher( { let config = config.clone(); - - move || { - let mut current_child = currently_running_child.write().unwrap(); - log::trace!("Killing old process"); - current_child.kill()?; - let (child, result) = start_desktop(&config)?; - *current_child = child; - Ok(result) - } + move || platform.write().unwrap().rebuild(&config) }, &config, None, @@ -84,19 +87,9 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option ) .await?; - // Print serve info - print_console_info( - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - None, - ); - match hot_reload_state { Some(hot_reload_state) => { + // The open interprocess sockets start_desktop_hot_reload(hot_reload_state).await?; } None => { @@ -192,7 +185,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<() } fn clear_paths(file_socket_path: &std::path::Path) { - if cfg!(target_os = "macos") { + if cfg!(unix) { // On unix, if you force quit the application, it can leave the file socket open // This will cause the local socket listener to fail to open // We check if the file socket is already open from an old session and then delete it @@ -217,10 +210,9 @@ 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, skip_assets: bool) -> Result<(RAIIChild, BuildResult)> { // Run the desktop application - log::trace!("Building application"); - let result = crate::builder::build_desktop(config, true)?; + let result = crate::builder::build_desktop(config, true, skip_assets)?; match &config.executable { crate::ExecutableType::Binary(name) @@ -230,10 +222,58 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { if cfg!(windows) { file.set_extension("exe"); } - log::trace!("Running application from {:?}", file); - let child = Command::new(file.to_str().unwrap()).spawn()?; + let active = "DIOXUS_ACTIVE"; + let child = RAIIChild( + Command::new(file.to_str().unwrap()) + .env(active, "true") + .spawn()?, + ); Ok((child, result)) } } } + +pub(crate) struct DesktopPlatform { + currently_running_child: RAIIChild, + skip_assets: bool, +} + +impl Platform for DesktopPlatform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result { + let (child, first_build_result) = start_desktop(config, serve.skip_assets)?; + + 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, + skip_assets: serve.skip_assets, + }) + } + + fn rebuild(&mut self, config: &CrateConfig) -> Result { + self.currently_running_child.0.kill()?; + let (child, result) = start_desktop(config, self.skip_assets)?; + self.currently_running_child = child; + Ok(result) + } +} + +struct RAIIChild(Child); + +impl Drop for RAIIChild { + fn drop(&mut self) { + let _ = self.0.kill(); + } +} diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs new file mode 100644 index 000000000..fa9c77459 --- /dev/null +++ b/packages/cli/src/server/fullstack/mod.rs @@ -0,0 +1,161 @@ +use crate::{ + cfg::{ConfigOptsBuild, ConfigOptsServe}, + CrateConfig, Result, WebAssetConfigDropGuard, +}; + +use super::{desktop, Platform}; + +pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> { + desktop::startup_with_platform::(config, serve).await +} + +fn start_web_build_thread( + config: &CrateConfig, + serve: &ConfigOptsServe, +) -> std::thread::JoinHandle> { + let serve = serve.clone(); + let target_directory = config.crate_dir.join(".dioxus").join("web"); + std::fs::create_dir_all(&target_directory).unwrap(); + std::thread::spawn(move || build_web(serve, &target_directory)) +} + +struct FullstackPlatform { + serve: ConfigOptsServe, + desktop: desktop::DesktopPlatform, + _config: WebAssetConfigDropGuard, +} + +impl Platform for FullstackPlatform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + where + Self: Sized, + { + let thread_handle = start_web_build_thread(config, serve); + + 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 config = WebAssetConfigDropGuard::new(); + let desktop = desktop::DesktopPlatform::start(&desktop_config, serve)?; + thread_handle + .join() + .map_err(|_| anyhow::anyhow!("Failed to join thread"))??; + + Ok(Self { + desktop, + serve: serve.clone(), + _config: config, + }) + } + + fn rebuild(&mut self, crate_config: &CrateConfig) -> Result { + let thread_handle = start_web_build_thread(crate_config, &self.serve); + let result = { + 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]), + }; + let _gaurd = FullstackServerEnvGuard::new(self.serve.force_debug, self.serve.release); + self.desktop.rebuild(&desktop_config) + }; + thread_handle + .join() + .map_err(|_| anyhow::anyhow!("Failed to join thread"))??; + result + } +} + +fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> 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); + + let _gaurd = FullstackWebEnvGuard::new(&web_config); + crate::cli::build::Build { build: web_config }.build(None, Some(target_directory)) +} + +// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default +// This reduces a hello world from ~40MB to ~2MB +pub(crate) struct FullstackWebEnvGuard { + old_rustflags: Option, +} + +impl FullstackWebEnvGuard { + pub fn new(serve: &ConfigOptsBuild) -> Self { + Self { + old_rustflags: (!serve.force_debug).then(|| { + let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); + let debug_assertions = if serve.release { + "" + } else { + " -C debug-assertions" + }; + + std::env::set_var( + "RUSTFLAGS", + format!( + "{old_rustflags} -C debuginfo=none -C strip=debuginfo{debug_assertions}" + ), + ); + old_rustflags + }), + } + } +} + +impl Drop for FullstackWebEnvGuard { + fn drop(&mut self) { + if let Some(old_rustflags) = self.old_rustflags.take() { + std::env::set_var("RUSTFLAGS", old_rustflags); + } + } +} + +// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default +// This reduces a hello world from ~40MB to ~2MB +pub(crate) struct FullstackServerEnvGuard { + old_rustflags: Option, +} + +impl FullstackServerEnvGuard { + pub fn new(debug: bool, release: bool) -> Self { + Self { + old_rustflags: (!debug).then(|| { + let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); + let debug_assertions = if release { "" } else { " -C debug-assertions" }; + + std::env::set_var( + "RUSTFLAGS", + format!("{old_rustflags} -C opt-level=2 {debug_assertions}"), + ); + old_rustflags + }), + } + } +} + +impl Drop for FullstackServerEnvGuard { + fn drop(&mut self) { + if let Some(old_rustflags) = self.old_rustflags.take() { + std::env::set_var("RUSTFLAGS", old_rustflags); + } + } +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index eb4021184..895d9e51e 100644 --- a/packages/cli/src/server/mod.rs +++ b/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::{self}; mod output; use output::*; pub mod desktop; +pub mod fullstack; pub mod web; /// Sets up a file watcher @@ -141,6 +142,13 @@ async fn setup_file_watcher Result + Send + 'static>( Ok(watcher) } +pub(crate) trait Platform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + where + Self: Sized; + fn rebuild(&mut self, config: &CrateConfig) -> Result; +} + #[derive(Clone)] pub struct HotReloadState { pub messages: broadcast::Sender>, diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 98ff1e078..896d08643 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -48,7 +48,12 @@ struct WsReloadState { update: broadcast::Sender<()>, } -pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { +pub async fn startup( + port: u16, + config: CrateConfig, + start_browser: bool, + skip_assets: bool, +) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); let _ = ctrlc::set_handler(move || { @@ -80,7 +85,15 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res false => None, }; - serve(ip, port, config, start_browser, hot_reload_state).await?; + serve( + ip, + port, + config, + start_browser, + skip_assets, + hot_reload_state, + ) + .await?; Ok(()) } @@ -91,9 +104,10 @@ pub async fn serve( port: u16, config: CrateConfig, start_browser: bool, + skip_assets: bool, hot_reload_state: Option, ) -> Result<()> { - let first_build_result = crate::builder::build(&config, true)?; + let first_build_result = crate::builder::build(&config, false, skip_assets)?; log::info!("🚀 Starting development server..."); @@ -106,7 +120,7 @@ pub async fn serve( { let config = config.clone(); let reload_tx = reload_tx.clone(); - move || build(&config, &reload_tx) + move || build(&config, &reload_tx, skip_assets) }, &config, Some(WebServerInfo { @@ -420,8 +434,8 @@ async fn ws_handler( }) } -fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { - let result = builder::build(config, true)?; +fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result { + let result = builder::build(config, true, skip_assets)?; // change the websocket reload state to true; // the page will auto-reload. if config @@ -431,7 +445,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { .reload_html .unwrap_or(false) { - let _ = Serve::regen_dev_page(config); + let _ = Serve::regen_dev_page(config, skip_assets); } let _ = reload_tx.send(()); Ok(result) diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 566e9ba7c..6b5e5408a 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -98,7 +98,7 @@ impl Runtime { } } -/// A gaurd for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. +/// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. /// /// ```rust /// use dioxus::prelude::*; diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 1d190ccaf..e1ff55c85 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -1,13 +1,33 @@ //! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely -use dioxus::prelude::*; -use std::{sync::atomic::AtomicUsize, time::Duration}; - -static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); - #[cfg(not(miri))] #[tokio::test] async fn it_works() { + use dioxus::prelude::*; + use std::{sync::atomic::AtomicUsize, time::Duration}; + + static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); + + fn app(cx: Scope) -> Element { + cx.use_hook(|| { + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(50)).await; + POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); + } + }); + + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(25)).await; + POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); + } + }); + }); + + cx.render(rsx!(())) + } + let mut dom = VirtualDom::new(app); let _ = dom.rebuild(); @@ -24,23 +44,3 @@ async fn it_works() { 135 ); } - -fn app(cx: Scope) -> Element { - cx.use_hook(|| { - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_micros(50)).await; - POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); - } - }); - - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_micros(25)).await; - POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); - } - }); - }); - - cx.render(rsx!(())) -} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 47bd64934..357cf63e2 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -18,7 +18,7 @@ dioxus-hot-reload = { workspace = true, optional = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } -tracing = { workspace = true } +tracing.workspace = true wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ @@ -40,8 +40,9 @@ urlencoding = "2.1.2" async-trait = "0.1.68" crossbeam-channel = "0.5.8" - [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +# This is only for debug mode, and it appears mobile does not support some packages this uses +manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } rfd = "0.12" global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" } diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index f957dd321..061c040ed 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -17,7 +17,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(false)), + Config::new().with_window(WindowBuilder::new().with_visible(true)), ); // Stop deadman's switch diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 0907121d4..e12d8644d 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -16,7 +16,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(false)), + Config::new().with_window(WindowBuilder::new().with_visible(true)), ); should_panic.store(false, std::sync::atomic::Ordering::SeqCst); diff --git a/packages/desktop/src/collect_assets.rs b/packages/desktop/src/collect_assets.rs new file mode 100644 index 000000000..aa9050a75 --- /dev/null +++ b/packages/desktop/src/collect_assets.rs @@ -0,0 +1,60 @@ +pub fn copy_assets() { + #[cfg(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + ))] + { + // The CLI will copy assets to the current working directory + if std::env::var_os("DIOXUS_ACTIVE").is_some() { + return; + } + use manganis_cli_support::AssetManifest; + use manganis_cli_support::AssetManifestExt; + use manganis_cli_support::Config; + use std::path::PathBuf; + let config = Config::current(); + let asset_location = config.assets_serve_location(); + let asset_location = PathBuf::from(asset_location); + let _ = std::fs::remove_dir_all(&asset_location); + + println!("Finding assets... (Note: if you run a dioxus desktop application with the CLI. This process will be significantly faster.)"); + let manifest = AssetManifest::load(); + let has_assets = manifest + .packages() + .iter() + .any(|package| !package.assets().is_empty()); + + if has_assets { + println!("Copying and optimizing assets..."); + manifest.copy_static_assets_to(&asset_location).unwrap(); + println!("Copied assets to {}", asset_location.display()); + } else { + println!("No assets found"); + } + } + #[cfg(not(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + )))] + { + println!( + "Skipping assets in release mode. You compile assets with the dioxus-cli in release mode" + ); + } +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 519c198a9..9d66ddfb6 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -4,6 +4,7 @@ #![deny(missing_docs)] mod cfg; +mod collect_assets; mod desktop_context; mod element; mod escape; @@ -146,6 +147,9 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } }); + // Copy over any assets we find + crate::collect_assets::copy_assets(); + // Set the event converter dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter)); diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 61c6af661..19bb6e55f 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -216,6 +216,7 @@ pub(super) async fn desktop_handler( request: Request>, custom_head: Option, custom_index: Option, + #[allow(unused_variables)] assets_head: Option, root_name: &str, asset_handlers: &AssetHandlerRegistry, edit_queue: &EditQueue, @@ -240,9 +241,46 @@ pub(super) async fn desktop_handler( // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. let mut template = include_str!("./index.html").to_string(); - if let Some(custom_head) = custom_head { - template = template.replace("", &custom_head); + #[allow(unused_mut)] + let mut head = custom_head.unwrap_or_default(); + #[cfg(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + ))] + { + use manganis_cli_support::AssetManifestExt; + let manifest = manganis_cli_support::AssetManifest::load(); + head += &manifest.head(); } + #[cfg(not(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + )))] + { + if let Some(assets_head) = assets_head { + head += &assets_head; + } else { + tracing::warn!("No assets head found. You can compile assets with the dioxus-cli in release mode"); + } + } + + template = template.replace("", &head); template .replace( @@ -279,12 +317,10 @@ pub(super) async fn desktop_handler( // Else, try to serve a file from the filesystem. // If the path is relative, we'll try to serve it from the assets directory. - let mut asset = get_asset_root() - .unwrap_or_else(|| Path::new(".").to_path_buf()) - .join(&request.path); + let mut asset = get_asset_root_or_default().join(&request.path); if !asset.exists() { - asset = PathBuf::from("/").join(request.path); + asset = PathBuf::from("/").join(&request.path); } if asset.exists() { @@ -295,7 +331,7 @@ pub(super) async fn desktop_handler( return; } }; - let asset = match std::fs::read(asset) { + let asset = match std::fs::read(&asset) { Ok(asset) => asset, Err(err) => { tracing::error!("error reading asset: {}", err); @@ -314,6 +350,12 @@ pub(super) async fn desktop_handler( } } + tracing::error!( + "Failed to find {} (as path {})", + request.uri().path(), + asset.display() + ); + match Response::builder() .status(StatusCode::NOT_FOUND) .body(Cow::from(String::from("Not Found").into_bytes())) @@ -325,6 +367,11 @@ pub(super) async fn desktop_handler( } } +#[allow(unreachable_code)] +pub(crate) fn get_asset_root_or_default() -> PathBuf { + get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()) +} + #[allow(unreachable_code)] fn get_asset_root() -> Option { /* @@ -339,7 +386,7 @@ fn get_asset_root() -> Option { */ - if std::env::var_os("CARGO").is_some() { + if std::env::var_os("CARGO").is_some() || std::env::var_os("DIOXUS_ACTIVE").is_some() { return None; } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index f4bc4c4ed..23d1b8396 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -18,6 +18,46 @@ pub(crate) fn build( let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); let root_name = cfg.root_name.clone(); + let assets_head = { + #[cfg(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + ))] + { + None + } + #[cfg(not(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + )))] + { + let head = crate::protocol::get_asset_root_or_default(); + let head = head.join("dist/__assets_head.html"); + match std::fs::read_to_string(&head) { + Ok(s) => Some(s), + Err(err) => { + tracing::error!("Failed to read {head:?}: {err}"); + None + } + } + } + }; // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs // if cfg.enable_default_menu_bar { @@ -58,6 +98,7 @@ pub(crate) fn build( move |request, responder| { let custom_head = custom_head.clone(); let index_file = index_file.clone(); + let assets_head = assets_head.clone(); let root_name = root_name.clone(); let asset_handlers_ref = asset_handlers_ref.clone(); let edit_queue = edit_queue.clone(); @@ -66,6 +107,7 @@ pub(crate) fn build( request, custom_head.clone(), index_file.clone(), + assets_head.clone(), &root_name, &asset_handlers_ref, &edit_queue, diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 2c284bf1a..170f0321e 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -16,6 +16,7 @@ dioxus-html = { workspace = true, optional = true } dioxus-core-macro = { workspace = true, optional = true } dioxus-hooks = { workspace = true, optional = true } dioxus-rsx = { workspace = true, optional = true } +manganis = { git = "https://github.com/DioxusLabs/collect-assets" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { workspace = true, optional = true } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 6256e96d7..1e64715b0 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -22,6 +22,9 @@ pub use dioxus_rsx as rsx; pub use dioxus_core_macro as core_macro; pub mod prelude { + pub use manganis; + pub use manganis::mg; + #[cfg(feature = "hooks")] pub use crate::hooks::*; diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 2f3eca420..0215fcaad 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -70,6 +70,10 @@ dioxus-hot-reload = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } +[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +# This is only for debug mode, and it appears mobile does not support some packages this uses +manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } + [features] default = ["hot-reload", "default-tls"] router = ["dioxus-router"] diff --git a/packages/fullstack/examples/axum-auth/.gitignore b/packages/fullstack/examples/axum-auth/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-auth/.gitignore +++ b/packages/fullstack/examples/axum-auth/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-desktop/.gitignore b/packages/fullstack/examples/axum-desktop/.gitignore index 6047329c6..21fff11dd 100644 --- a/packages/fullstack/examples/axum-desktop/.gitignore +++ b/packages/fullstack/examples/axum-desktop/.gitignore @@ -1,2 +1,4 @@ dist -target \ No newline at end of file +target +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-hello-world/.gitignore b/packages/fullstack/examples/axum-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-hello-world/.gitignore +++ b/packages/fullstack/examples/axum-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-router/.gitignore b/packages/fullstack/examples/axum-router/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-router/.gitignore +++ b/packages/fullstack/examples/axum-router/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/salvo-hello-world/.gitignore b/packages/fullstack/examples/salvo-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/salvo-hello-world/.gitignore +++ b/packages/fullstack/examples/salvo-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/static-hydrated/.gitignore b/packages/fullstack/examples/static-hydrated/.gitignore index c7f237de8..b98a2a695 100644 --- a/packages/fullstack/examples/static-hydrated/.gitignore +++ b/packages/fullstack/examples/static-hydrated/.gitignore @@ -1,3 +1,4 @@ docs target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/warp-hello-world/.gitignore b/packages/fullstack/examples/warp-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/warp-hello-world/.gitignore +++ b/packages/fullstack/examples/warp-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index 09e6e1aa1..a14c51c0a 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -276,6 +276,9 @@ where fn serve_static_assets(mut self, assets_path: impl Into) -> Self { use tower_http::services::{ServeDir, ServeFile}; + // Copy over any assets we find + crate::collect_assets::copy_assets(); + let assets_path = assets_path.into(); // Serve all files in dist folder except index.html diff --git a/packages/fullstack/src/adapters/salvo_adapter.rs b/packages/fullstack/src/adapters/salvo_adapter.rs index 674722665..84894bbda 100644 --- a/packages/fullstack/src/adapters/salvo_adapter.rs +++ b/packages/fullstack/src/adapters/salvo_adapter.rs @@ -241,6 +241,9 @@ impl DioxusRouterExt for Router { } fn serve_static_assets(mut self, assets_path: impl Into) -> Self { + // Copy over any assets we find + crate::collect_assets::copy_assets(); + let assets_path = assets_path.into(); // Serve all files in dist folder except index.html diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index 316d70e56..682063084 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -187,6 +187,9 @@ pub fn serve_dioxus_application(cfg: Config) { let target_dir = crate_dir.join("target"); let hot_reload_socket_path = target_dir.join("dioxusin"); - #[cfg(target_os = "macos")] + #[cfg(unix)] { // On unix, if you force quit the application, it can leave the file socket open // This will cause the local socket listener to fail to open diff --git a/playwright-tests/playwright.config.js b/playwright-tests/playwright.config.js index b4b0c4041..dcee8f0e4 100644 --- a/playwright-tests/playwright.config.js +++ b/playwright-tests/playwright.config.js @@ -76,23 +76,23 @@ module.exports = defineConfig({ command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test", port: 3030, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { cwd: path.join(process.cwd(), "web"), - command: "cargo run --package dioxus-cli -- serve", + command: "cargo run --package dioxus-cli --release -- serve --skip-assets", port: 8080, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { cwd: path.join(process.cwd(), 'fullstack'), - command: 'cargo run --package dioxus-cli -- build --features web --release && cargo run --release --features ssr', + command: 'cargo run --package dioxus-cli --release -- build --features web --release --skip-assets\ncargo run --release --features ssr', port: 3333, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", },