Prechádzať zdrojové kódy

add `--offline`, `--locked`, remove `dioxus` branding if dioxus is not being used explicitly (#4155)

* wip: add some spin logging

* wipe dioxus branding if not being used by dioxus

* add --locked, --offline, --frozen

* properly git init with `dx new` and `dx init`

* dont panic in verbosity

* Move build-assets to "tools"

* change ui for dx new offline

* pin tauri-bundle

* pin tauri

* run tailwind once
Jonathan Kelley 2 dní pred
rodič
commit
dd53ce72a7

+ 2 - 2
Cargo.toml

@@ -227,8 +227,8 @@ thiserror = "2.0.12"
 prettyplease = { version = "0.2.30", features = ["verbatim"] }
 const_format = "0.2.34"
 cargo_toml = { version = "0.21.0" }
-tauri-utils = { version = "2.2.0" }
-tauri-bundler = { version = "2.2.4" }
+tauri-utils = { version = "=2.4.0" }
+tauri-bundler = { version = "=2.4.0" }
 lru = "0.13.0"
 async-trait = "0.1.87"
 axum = { version = "0.8.1", default-features = false }

+ 22 - 0
packages/cli/src/build/request.rs

@@ -390,6 +390,7 @@ pub(crate) struct BuildRequest {
     pub(crate) link_err_file: Arc<NamedTempFile>,
     pub(crate) rustc_wrapper_args_file: Arc<NamedTempFile>,
     pub(crate) base_path: Option<String>,
+    pub(crate) using_dioxus_explicitly: bool,
 }
 
 /// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin
@@ -757,6 +758,7 @@ impl BuildRequest {
             package,
             main_target,
             rustflags,
+            using_dioxus_explicitly,
             skip_assets: args.skip_assets,
             base_path: args.base_path.clone(),
             wasm_split: args.wasm_split,
@@ -1132,6 +1134,14 @@ impl BuildRequest {
             return Ok(());
         }
 
+        // Run the tailwind build before bundling anything else
+        crate::TailwindCli::run_once(
+            self.package_manifest_dir(),
+            self.config.application.tailwind_input.clone(),
+            self.config.application.tailwind_output.clone(),
+        )
+        .await?;
+
         let asset_dir = self.asset_dir();
 
         // First, clear the asset dir of any files that don't exist in the new manifest
@@ -2147,6 +2157,18 @@ impl BuildRequest {
         };
         cargo_args.push(self.executable_name().to_string());
 
+        // Set offline/locked/frozen
+        let lock_opts = crate::VERBOSITY.get().cloned().unwrap_or_default();
+        if lock_opts.frozen {
+            cargo_args.push("--frozen".to_string());
+        }
+        if lock_opts.locked {
+            cargo_args.push("--locked".to_string());
+        }
+        if lock_opts.offline {
+            cargo_args.push("--offline".to_string());
+        }
+
         // Merge in extra args. Order shouldn't really matter.
         cargo_args.extend(self.extra_cargo_args.clone());
         cargo_args.push("--".to_string());

+ 58 - 6
packages/cli/src/cli/create.rs

@@ -1,6 +1,6 @@
 use super::*;
 use crate::TraceSrc;
-use cargo_generate::{GenerateArgs, TemplatePath};
+use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
 use std::path::Path;
 
 pub(crate) static DEFAULT_TEMPLATE: &str = "gh:dioxuslabs/dioxus-template";
@@ -46,15 +46,25 @@ pub struct Create {
     /// Default values can be overridden with `--option`
     #[clap(short, long)]
     yes: bool,
+
+    /// Specify the VCS used to initialize the generated template.
+    /// Options: `git`, `none`.
+    #[arg(long, value_parser)]
+    vcs: Option<Vcs>,
 }
 
 impl Create {
-    pub fn create(mut self) -> Result<StructuredOutput> {
+    pub async fn create(mut self) -> Result<StructuredOutput> {
         // Project name defaults to directory name.
         if self.name.is_none() {
             self.name = Some(create::name_from_path(&self.path)?);
         }
 
+        // Perform a connectivity check so we just don't it around doing nothing if there's a network error
+        if self.template.is_none() {
+            connectivity_check().await?;
+        }
+
         // If no template is specified, use the default one and set the branch to the latest release.
         resolve_template_and_branch(&mut self.template, &mut self.branch);
 
@@ -72,14 +82,13 @@ impl Create {
             init: true,
             name: self.name,
             silent: self.yes,
-            vcs: Some(cargo_generate::Vcs::Git),
+            vcs: self.vcs,
             template_path: TemplatePath {
                 auto_path: self.template,
                 branch: self.branch,
                 revision: self.revision,
                 subfolder: self.subtemplate,
                 tag: self.tag,
-
                 ..Default::default()
             },
             verbose: crate::logging::VERBOSITY
@@ -93,7 +102,7 @@ impl Create {
         tracing::debug!(dx_src = ?TraceSrc::Dev, "Creating new project with args: {args:#?}");
         let path = cargo_generate::generate(args)?;
 
-        _ = post_create(&path);
+        _ = post_create(&path, &self.vcs.unwrap_or(Vcs::Git));
 
         Ok(StructuredOutput::Success)
     }
@@ -145,7 +154,7 @@ pub(crate) fn name_from_path(path: &Path) -> Result<String> {
 }
 
 /// Post-creation actions for newly setup crates.
-pub(crate) fn post_create(path: &Path) -> Result<()> {
+pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> {
     let parent_dir = path.parent();
     let metadata = if parent_dir.is_none() {
         None
@@ -168,6 +177,7 @@ pub(crate) fn post_create(path: &Path) -> Result<()> {
 
     // 1. Add the new project to the workspace, if it exists.
     //    This must be executed first in order to run `cargo fmt` on the new project.
+    let is_workspace = metadata.is_some();
     metadata.and_then(|metadata| {
         let cargo_toml_path = &metadata.workspace_root.join("Cargo.toml");
         let cargo_toml_str = std::fs::read_to_string(cargo_toml_path).ok()?;
@@ -223,6 +233,11 @@ pub(crate) fn post_create(path: &Path) -> Result<()> {
     let mut file = std::fs::File::create(readme_path)?;
     file.write_all(new_readme.as_bytes())?;
 
+    // 5. Run git init
+    if !is_workspace {
+        vcs.initialize(path, Some("main"), true)?;
+    }
+
     tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}\n\n`cd` to your project and run `dx serve` to start developing.\nMore information is available in the generated `README.md`.\n\nBuild cool things! ✌️", path.display());
 
     Ok(())
@@ -239,6 +254,43 @@ fn remove_triple_newlines(string: &str) -> String {
     new_string
 }
 
+/// Perform a health check against github itself before we attempt to download any templates hosted
+/// on github.
+pub(crate) async fn connectivity_check() -> Result<()> {
+    if crate::VERBOSITY
+        .get()
+        .map(|f| f.offline)
+        .unwrap_or_default()
+    {
+        return Ok(());
+    }
+
+    use crate::styles::{GLOW_STYLE, LINK_STYLE};
+    let client = reqwest::Client::new();
+    for x in 0..=5 {
+        tokio::select! {
+            res = client.head("https://github.com/DioxusLabs/").header("User-Agent", "dioxus-cli").send() => {
+                if res.is_ok() {
+                    return Ok(());
+                }
+                tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
+            },
+            _ = tokio::time::sleep(std::time::Duration::from_millis(if x == 1 { 500 } else { 2000 })) => {}
+        }
+        if x == 0 {
+            println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for {LINK_STYLE}https://github.com/dioxuslabs{LINK_STYLE:#}...")
+        } else {
+            println!(
+                "{GLOW_STYLE}warning{GLOW_STYLE:#}: ({x}/5) Taking a while, maybe your internet is down?"
+            );
+        }
+    }
+
+    Err(Error::Network(
+        "Error connecting to template repository. Try cloning the template manually or add `dioxus` to a `cargo new` project.".to_string(),
+    ))
+}
+
 // todo: re-enable these tests with better parallelization
 //
 // #[cfg(test)]

+ 14 - 4
packages/cli/src/cli/init.rs

@@ -1,5 +1,5 @@
 use super::*;
-use cargo_generate::{GenerateArgs, TemplatePath};
+use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
 
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 #[clap(name = "init")]
@@ -43,15 +43,25 @@ pub struct Init {
     /// Default values can be overridden with `--option`
     #[clap(short, long)]
     yes: bool,
+
+    /// Specify the VCS used to initialize the generated template.
+    /// Options: `git`, `none`.
+    #[arg(long, value_parser)]
+    vcs: Option<Vcs>,
 }
 
 impl Init {
-    pub fn init(mut self) -> Result<StructuredOutput> {
+    pub async fn init(mut self) -> Result<StructuredOutput> {
         // Project name defaults to directory name.
         if self.name.is_none() {
             self.name = Some(create::name_from_path(&self.path)?);
         }
 
+        // Perform a connectivity check so we just don't it around doing nothing if there's a network error
+        if self.template.is_none() {
+            create::connectivity_check().await?;
+        }
+
         // If no template is specified, use the default one and set the branch to the latest release.
         create::resolve_template_and_branch(&mut self.template, &mut self.branch);
 
@@ -64,7 +74,7 @@ impl Init {
             init: true,
             name: self.name,
             silent: self.yes,
-            vcs: Some(cargo_generate::Vcs::Git),
+            vcs: self.vcs,
             template_path: TemplatePath {
                 auto_path: self.template,
                 branch: self.branch,
@@ -77,7 +87,7 @@ impl Init {
         };
         create::restore_cursor_on_sigint();
         let path = cargo_generate::generate(args)?;
-        _ = create::post_create(&path);
+        _ = create::post_create(&path, &self.vcs.unwrap_or(Vcs::Git));
         Ok(StructuredOutput::Success)
     }
 }

+ 15 - 6
packages/cli/src/cli/mod.rs

@@ -70,10 +70,6 @@ pub(crate) enum Commands {
     #[clap(name = "run")]
     Run(run::RunArgs),
 
-    /// Build the assets for a specific target.
-    #[clap(name = "assets")]
-    BuildAssets(build_assets::BuildAssets),
-
     /// Init a new project for Dioxus in the current directory (by default).
     /// Will attempt to keep your project in a good state.
     #[clap(name = "init")]
@@ -103,6 +99,18 @@ pub(crate) enum Commands {
     /// Update the Dioxus CLI to the latest version.
     #[clap(name = "self-update")]
     SelfUpdate(update::SelfUpdate),
+
+    /// Run a dioxus build tool. IE `build-assets`, etc
+    #[clap(name = "tools")]
+    #[clap(subcommand)]
+    Tools(BuildTools),
+}
+
+#[derive(Subcommand)]
+pub enum BuildTools {
+    /// Build the assets for a specific target.
+    #[clap(name = "assets")]
+    BuildAssets(build_assets::BuildAssets),
 }
 
 impl Display for Commands {
@@ -119,8 +127,8 @@ impl Display for Commands {
             Commands::Check(_) => write!(f, "check"),
             Commands::Bundle(_) => write!(f, "bundle"),
             Commands::Run(_) => write!(f, "run"),
-            Commands::BuildAssets(_) => write!(f, "assets"),
             Commands::SelfUpdate(_) => write!(f, "self-update"),
+            Commands::Tools(_) => write!(f, "tools"),
         }
     }
 }
@@ -156,8 +164,9 @@ pub mod styles {
     pub(crate) const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
 
     // extra styles for styling logs
-    // we can style stuff using the ansi sequences like: "hotpatched in {GLOW_STYLE}{}{GLOW_STYLE:X}ms"
+    // we can style stuff using the ansi sequences like: "hotpatched in {GLOW_STYLE}{}{GLOW_STYLE:#}ms"
     pub(crate) const GLOW_STYLE: Style = AnsiColor::Yellow.on_default();
     pub(crate) const NOTE_STYLE: Style = AnsiColor::Green.on_default();
     pub(crate) const LINK_STYLE: Style = AnsiColor::Blue.on_default();
+    pub(crate) const ERROR_STYLE: Style = AnsiColor::Red.on_default();
 }

+ 2 - 7
packages/cli/src/cli/platform_override.rs

@@ -86,12 +86,8 @@ where
         let mut subcommand = matches.subcommand();
         while let Some((name, sub_matches)) = subcommand {
             match name {
-                "@client" => {
-                    client = Some(sub_matches);
-                }
-                "@server" => {
-                    server = Some(sub_matches);
-                }
+                "@client" => client = Some(sub_matches),
+                "@server" => server = Some(sub_matches),
                 _ => {}
             }
             subcommand = sub_matches.subcommand();
@@ -117,7 +113,6 @@ where
     }
 }
 
-/// Chain together multiple target commands
 #[derive(Debug, Subcommand, Clone)]
 #[command(subcommand_precedence_over_arg = true)]
 pub(crate) enum PlatformOverrides<T: Args> {

+ 20 - 18
packages/cli/src/cli/target.rs

@@ -2,48 +2,50 @@ use crate::cli::*;
 use crate::Platform;
 use target_lexicon::Triple;
 
+const HELP_HEADING: &str = "Target Options";
+
 /// A single target to build for
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 pub(crate) struct TargetArgs {
     /// Build platform: support Web & Desktop [default: "default_platform"]
-    #[clap(long, value_enum)]
+    #[clap(long, value_enum, help_heading = HELP_HEADING)]
     pub(crate) platform: Option<Platform>,
 
     /// Build in release mode [default: false]
-    #[clap(long, short)]
+    #[clap(long, short, help_heading = HELP_HEADING)]
     #[serde(default)]
     pub(crate) release: bool,
 
     /// The package to build
-    #[clap(short, long)]
+    #[clap(short, long, help_heading = HELP_HEADING)]
     pub(crate) package: Option<String>,
 
     /// Build a specific binary [default: ""]
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) bin: Option<String>,
 
     /// Build a specific example [default: ""]
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) example: Option<String>,
 
     /// Build the app with custom a profile
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) profile: Option<String>,
 
     /// Space separated list of features to activate
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) features: Vec<String>,
 
     /// Don't include the default features in the build
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) no_default_features: bool,
 
     /// Include all features in the build
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) all_features: bool,
 
     /// Rustc platform triple
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) target: Option<Triple>,
 
     /// Extra arguments passed to `cargo`
@@ -52,7 +54,7 @@ pub(crate) struct TargetArgs {
     ///
     /// This can include stuff like, "--locked", "--frozen", etc. Note that `dx` sets many of these
     /// args directly from other args in this command.
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) cargo_args: Option<String>,
 
     /// Extra arguments passed to `rustc`. This can be used to customize the linker, or other flags.
@@ -62,36 +64,36 @@ pub(crate) struct TargetArgs {
     ///
     /// cargo rustc -- -Clink-arg=-Wl,-blah
     ///
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) rustc_args: Option<String>,
 
     /// Skip collecting assets from dependencies [default: false]
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     #[serde(default)]
     pub(crate) skip_assets: bool,
 
     /// Inject scripts to load the wasm and js files for your dioxus app if they are not already present [default: true]
-    #[clap(long, default_value_t = true)]
+    #[clap(long, default_value_t = true, help_heading = HELP_HEADING)]
     pub(crate) inject_loading_scripts: bool,
 
     /// Experimental: Bundle split the wasm binary into multiple chunks based on `#[wasm_split]` annotations [default: false]
-    #[clap(long, default_value_t = false)]
+    #[clap(long, default_value_t = false, help_heading = HELP_HEADING)]
     pub(crate) wasm_split: bool,
 
     /// Generate debug symbols for the wasm binary [default: true]
     ///
     /// This will make the binary larger and take longer to compile, but will allow you to debug the
     /// wasm binary
-    #[clap(long, default_value_t = true)]
+    #[clap(long, default_value_t = true, help_heading = HELP_HEADING)]
     pub(crate) debug_symbols: bool,
 
     /// Are we building for a device or just the simulator.
     /// If device is false, then we'll build for the simulator
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) device: Option<bool>,
 
     /// The base path the build will fetch assets relative to. This will override the
     /// base path set in the `dioxus` config.
-    #[clap(long)]
+    #[clap(long, help_heading = HELP_HEADING)]
     pub(crate) base_path: Option<String>,
 }

+ 13 - 1
packages/cli/src/cli/verbosity.rs

@@ -1,6 +1,6 @@
 use clap::Parser;
 
-#[derive(Parser, Clone, Copy, Debug)]
+#[derive(Parser, Clone, Copy, Debug, Default)]
 pub struct Verbosity {
     /// Use verbose output [default: false]
     #[clap(long, global = true)]
@@ -13,4 +13,16 @@ pub struct Verbosity {
     /// Output logs in JSON format
     #[clap(long, global = true)]
     pub(crate) json_output: bool,
+
+    /// Assert that `Cargo.lock` will remain unchanged
+    #[clap(long, global = true, help_heading = "Manifest Options")]
+    pub(crate) locked: bool,
+
+    /// Run without accessing the network
+    #[clap(long, global = true, help_heading = "Manifest Options")]
+    pub(crate) offline: bool,
+
+    /// Equivalent to specifying both --locked and --offline
+    #[clap(long, global = true, help_heading = "Manifest Options")]
+    pub(crate) frozen: bool,
 }

+ 8 - 5
packages/cli/src/error.rs

@@ -27,24 +27,27 @@ pub(crate) enum Error {
     #[error("Invalid proxy URL: {0}")]
     InvalidProxy(#[from] hyper::http::uri::InvalidUri),
 
-    #[error("Failed to establish proxy: {0}")]
+    #[error("Establishing proxy: {0}")]
     ProxySetup(String),
 
-    #[error("Failed to bundle project: {0}")]
+    #[error("Bundling project: {0}")]
     BundleFailed(#[from] tauri_bundler::Error),
 
-    #[error("Failed to perform hotpatch: {0}")]
+    #[error("Performing hotpatch: {0}")]
     PatchingFailed(#[from] crate::build::PatchError),
 
-    #[error("Failed to read object file: {0}")]
+    #[error("Reading object file: {0}")]
     ObjectReadFailed(#[from] object::Error),
 
     #[error("{0}")]
     CapturedPanic(String),
 
-    #[error("Failed to render template: {0}")]
+    #[error("Rendering template error: {0}")]
     TemplateParse(#[from] handlebars::RenderError),
 
+    #[error("Network connectivity error: {0}")]
+    Network(String),
+
     #[error(transparent)]
     Other(#[from] anyhow::Error),
 }

+ 7 - 4
packages/cli/src/main.rs

@@ -51,8 +51,8 @@ async fn main() {
     let args = TraceController::initialize();
     let result = match args.action {
         Commands::Translate(opts) => opts.translate(),
-        Commands::New(opts) => opts.create(),
-        Commands::Init(opts) => opts.init(),
+        Commands::New(opts) => opts.create().await,
+        Commands::Init(opts) => opts.init().await,
         Commands::Config(opts) => opts.config().await,
         Commands::Autoformat(opts) => opts.autoformat().await,
         Commands::Check(opts) => opts.check().await,
@@ -61,8 +61,8 @@ async fn main() {
         Commands::Serve(opts) => opts.serve().await,
         Commands::Bundle(opts) => opts.bundle().await,
         Commands::Run(opts) => opts.run().await,
-        Commands::BuildAssets(opts) => opts.run().await,
         Commands::SelfUpdate(opts) => opts.self_update().await,
+        Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
     };
 
     // Provide a structured output for third party tools that can consume the output of the CLI
@@ -71,7 +71,10 @@ async fn main() {
             tracing::debug!(json = ?output);
         }
         Err(err) => {
-            eprintln!("{err}");
+            eprintln!(
+                "{ERROR_STYLE}failed:{ERROR_STYLE:#} {err}",
+                ERROR_STYLE = styles::ERROR_STYLE
+            );
 
             tracing::error!(
                 json = ?StructuredOutput::Error {

+ 15 - 8
packages/cli/src/serve/mod.rs

@@ -37,25 +37,32 @@ pub(crate) use update::*;
 /// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
 ///   to a dynamic one on the fly.
 pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
+    // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
+    let mut builder = AppServer::start(args).await?;
+    let mut devserver = WebServer::start(&builder)?;
+    let mut screen = Output::start(builder.interactive).await?;
+
     // This is our default splash screen. We might want to make this a fancier splash screen in the future
     // Also, these commands might not be the most important, but it's all we've got enabled right now
     tracing::info!(
         r#"-----------------------------------------------------------------
-                Serving your Dioxus app! 🚀
+                Serving your app: {binname}! 🚀
                 • Press {GLOW_STYLE}`ctrl+c`{GLOW_STYLE:#} to exit the server
                 • Press {GLOW_STYLE}`r`{GLOW_STYLE:#} to rebuild the app
                 • Press {GLOW_STYLE}`p`{GLOW_STYLE:#} to toggle automatic rebuilds
                 • Press {GLOW_STYLE}`v`{GLOW_STYLE:#} to toggle verbose logging
-                • Press {GLOW_STYLE}`/`{GLOW_STYLE:#} for more commands and shortcuts
-                Learn more at {LINK_STYLE}https://dioxuslabs.com/learn/0.7/getting_started{LINK_STYLE:#}
+                • Press {GLOW_STYLE}`/`{GLOW_STYLE:#} for more commands and shortcuts{extra}
                ----------------------------------------------------------------"#,
+        binname = builder.client.build.executable_name(),
+        extra = if builder.client.build.using_dioxus_explicitly {
+            format!(
+                "\n                Learn more at {LINK_STYLE}https://dioxuslabs.com/learn/0.7/getting_started{LINK_STYLE:#}"
+            )
+        } else {
+            String::new()
+        }
     );
 
-    // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
-    let mut builder = AppServer::start(args).await?;
-    let mut devserver = WebServer::start(&builder)?;
-    let mut screen = Output::start(builder.interactive).await?;
-
     loop {
         // Draw the state of the server to the screen
         screen.render(&builder, &devserver);

+ 17 - 15
packages/cli/src/serve/output.rs

@@ -165,7 +165,7 @@ impl Output {
     }
 
     pub(crate) fn remote_shutdown(interactive: bool) -> io::Result<()> {
-        if interactive {
+        if interactive && crossterm::terminal::is_raw_mode_enabled().unwrap_or(true) {
             stdout()
                 .execute(Show)?
                 .execute(DisableFocusChange)?
@@ -766,21 +766,23 @@ impl Output {
         let links_list: [_; 2] =
             Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom);
 
-        frame.render_widget(
-            Paragraph::new(Line::from(vec![
-                "Read the docs: ".gray(),
-                "https://dioxuslabs.com/0.6/docs".blue(),
-            ])),
-            links_list[0],
-        );
+        if state.runner.client.build.using_dioxus_explicitly {
+            frame.render_widget(
+                Paragraph::new(Line::from(vec![
+                    "Read the docs: ".gray(),
+                    "https://dioxuslabs.com/0.6/docs".blue(),
+                ])),
+                links_list[0],
+            );
 
-        frame.render_widget(
-            Paragraph::new(Line::from(vec![
-                "Video tutorials: ".gray(),
-                "https://youtube.com/@DioxusLabs".blue(),
-            ])),
-            links_list[1],
-        );
+            frame.render_widget(
+                Paragraph::new(Line::from(vec![
+                    "Video tutorials: ".gray(),
+                    "https://youtube.com/@DioxusLabs".blue(),
+                ])),
+                links_list[1],
+            );
+        }
 
         let cmds = [
             "",

+ 33 - 3
packages/cli/src/tailwind.rs

@@ -19,6 +19,35 @@ impl TailwindCli {
         Self { version }
     }
 
+    pub(crate) async fn run_once(
+        manifest_dir: PathBuf,
+        input_path: Option<PathBuf>,
+        output_path: Option<PathBuf>,
+    ) -> Result<()> {
+        let Some(tailwind) = Self::autodetect(&manifest_dir) else {
+            return Ok(());
+        };
+
+        if !tailwind.get_binary_path()?.exists() {
+            tracing::info!("Installing tailwindcss@{}", tailwind.version);
+            tailwind.install_github().await?;
+        }
+
+        let output = tailwind
+            .run(&manifest_dir, input_path, output_path, false)?
+            .wait_with_output()
+            .await?;
+
+        if !output.stderr.is_empty() {
+            tracing::warn!(
+                "Warnings while running tailwind: {}",
+                String::from_utf8_lossy(&output.stdout)
+            );
+        }
+
+        Ok(())
+    }
+
     pub(crate) fn serve(
         manifest_dir: PathBuf,
         input_path: Option<PathBuf>,
@@ -36,7 +65,7 @@ impl TailwindCli {
 
             // the tw watcher blocks on stdin, and `.wait()` will drop stdin
             // unfortunately the tw watcher just deadlocks in this case, so we take the stdin manually
-            let mut proc = tailwind.watch(&manifest_dir, input_path, output_path)?;
+            let mut proc = tailwind.run(&manifest_dir, input_path, output_path, true)?;
             let stdin = proc.stdin.take();
             proc.wait().await?;
             drop(stdin);
@@ -76,11 +105,12 @@ impl TailwindCli {
         Self::new(Self::V3_TAG.to_string())
     }
 
-    pub(crate) fn watch(
+    pub(crate) fn run(
         &self,
         manifest_dir: &Path,
         input_path: Option<PathBuf>,
         output_path: Option<PathBuf>,
+        watch: bool,
     ) -> Result<tokio::process::Child> {
         let binary_path = self.get_binary_path()?;
 
@@ -110,7 +140,7 @@ impl TailwindCli {
             .arg(input_path)
             .arg("--output")
             .arg(output_path)
-            .arg("--watch")
+            .args(watch.then_some("--watch"))
             .kill_on_drop(true)
             .stdin(Stdio::piped())
             .stdout(Stdio::null())

+ 47 - 8
packages/cli/src/workspace.rs

@@ -1,13 +1,14 @@
+use crate::styles::GLOW_STYLE;
 use crate::CliSettings;
 use crate::Result;
 use crate::{config::DioxusConfig, AndroidTools};
 use anyhow::Context;
 use ignore::gitignore::Gitignore;
-use krates::{semver::Version, KrateDetails};
+use krates::{semver::Version, KrateDetails, LockOptions};
 use krates::{Cmd, Krates, NodeId};
-use std::path::PathBuf;
 use std::sync::Arc;
 use std::{collections::HashSet, path::Path};
+use std::{path::PathBuf, time::Duration};
 use target_lexicon::Triple;
 use tokio::process::Command;
 
@@ -34,12 +35,50 @@ impl Workspace {
             return Ok(ws.clone());
         }
 
-        let cmd = Cmd::new();
-        let mut builder = krates::Builder::new();
-        builder.workspace(true);
-        let krates = builder
-            .build(cmd, |_| {})
-            .context("Failed to run cargo metadata")?;
+        let krates_future = tokio::task::spawn_blocking(|| {
+            let manifest_options = crate::logging::VERBOSITY.get().unwrap();
+            let lock_options = LockOptions {
+                frozen: manifest_options.frozen,
+                locked: manifest_options.locked,
+                offline: manifest_options.offline,
+            };
+
+            let mut cmd = Cmd::new();
+            cmd.lock_opts(lock_options);
+
+            let mut builder = krates::Builder::new();
+            builder.workspace(true);
+            let res = builder
+                .build(cmd, |_| {})
+                .context("Failed to run cargo metadata");
+
+            if !lock_options.offline {
+                if let Ok(res) = std::env::var("SIMULATE_SLOW_NETWORK") {
+                    std::thread::sleep(Duration::from_secs(res.parse().unwrap_or(5)));
+                }
+            }
+
+            res
+        });
+
+        let spin_future = async move {
+            tokio::time::sleep(Duration::from_millis(500)).await;
+            println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for cargo-metadata...");
+            tokio::time::sleep(Duration::from_millis(2000)).await;
+            for x in 1..=5 {
+                tokio::time::sleep(Duration::from_millis(2000)).await;
+                println!(
+                    "{GLOW_STYLE}warning{GLOW_STYLE:#}: ({x}/5) Taking a while, maybe your internet is down?"
+                );
+            }
+        };
+
+        let krates = tokio::select! {
+            f = krates_future => f.context("failed to run cargo metadata")??,
+            _ = spin_future => return Err(crate::Error::Network(
+                "cargo metadata took too long to respond, try again with --offline".to_string(),
+            )),
+        };
 
         let settings = CliSettings::global_or_default();
         let sysroot = Command::new("rustc")