Переглянути джерело

Inline the TailwindCLI into dx and run it during serve (#4086)

* wip: download the tailwind cli and then run it during serve
* allow no-downloads again
Jonathan Kelley 1 місяць тому
батько
коміт
cb651a8ca2

+ 3 - 3
packages/cli/src/build/builder.rs

@@ -657,8 +657,6 @@ impl AppBuilder {
             None => self.build.asset_dir(),
         };
 
-        tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
-
         // Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
         let changed_file = dunce::canonicalize(changed_file)
             .inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}"))
@@ -668,6 +666,8 @@ impl AppBuilder {
         let resource = artifacts.assets.assets.get(&changed_file)?;
         let output_path = asset_dir.join(resource.bundled_path());
 
+        tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
+
         // Remove the old asset if it exists
         _ = std::fs::remove_file(&output_path);
 
@@ -1310,6 +1310,6 @@ We checked the folder: {}
 
     /// Check if the queued build is blocking hotreloads
     pub(crate) fn can_receive_hotreloads(&self) -> bool {
-        matches!(&self.stage, BuildStage::Success)
+        matches!(&self.stage, BuildStage::Success | BuildStage::Failed)
     }
 }

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

@@ -3758,4 +3758,14 @@ r#" <script>
             trimmed_path
         }
     }
+
+    /// Get the path to the package manifest directory
+    pub(crate) fn package_manifest_dir(&self) -> PathBuf {
+        self.workspace.krates[self.crate_package]
+            .manifest_path
+            .parent()
+            .unwrap()
+            .to_path_buf()
+            .into()
+    }
 }

+ 6 - 0
packages/cli/src/config/app.rs

@@ -10,4 +10,10 @@ pub(crate) struct ApplicationConfig {
 
     #[serde(default)]
     pub(crate) out_dir: Option<PathBuf>,
+
+    #[serde(default)]
+    pub(crate) tailwind_input: Option<PathBuf>,
+
+    #[serde(default)]
+    pub(crate) tailwind_output: Option<PathBuf>,
 }

+ 2 - 0
packages/cli/src/config/dioxus_config.rs

@@ -22,6 +22,8 @@ impl Default for DioxusConfig {
                 asset_dir: None,
                 sub_package: None,
                 out_dir: None,
+                tailwind_input: None,
+                tailwind_output: None,
             },
             web: WebConfig {
                 app: WebAppConfig {

+ 2 - 0
packages/cli/src/main.rs

@@ -17,6 +17,7 @@ mod platform;
 mod rustcwrapper;
 mod serve;
 mod settings;
+mod tailwind;
 mod wasm_bindgen;
 mod wasm_opt;
 mod workspace;
@@ -31,6 +32,7 @@ pub(crate) use logging::*;
 pub(crate) use platform::*;
 pub(crate) use rustcwrapper::*;
 pub(crate) use settings::*;
+pub(crate) use tailwind::*;
 pub(crate) use wasm_bindgen::*;
 pub(crate) use workspace::*;
 

+ 0 - 1
packages/cli/src/serve/mod.rs

@@ -72,7 +72,6 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
                     continue;
                 }
 
-                tracing::debug!("Starting hotpatching: {:?}", files);
                 builder.handle_file_change(&files, &mut devserver).await;
             }
 

+ 12 - 1
packages/cli/src/serve/runner.rs

@@ -1,7 +1,7 @@
 use super::{AppBuilder, ServeUpdate, WebServer};
 use crate::{
     BuildArtifacts, BuildId, BuildMode, BuildTargets, Error, HotpatchModuleCache, Platform, Result,
-    ServeArgs, TraceSrc, Workspace,
+    ServeArgs, TailwindCli, TraceSrc, Workspace,
 };
 use anyhow::Context;
 use dioxus_core::internal::{
@@ -72,6 +72,9 @@ pub(crate) struct AppServer {
     pub(crate) devserver_bind_ip: IpAddr,
     pub(crate) proxied_port: Option<u16>,
     pub(crate) cross_origin_policy: bool,
+
+    // Additional plugin-type tools
+    pub(crate) _tw_watcher: tokio::task::JoinHandle<Result<()>>,
 }
 
 pub(crate) struct CachedFile {
@@ -148,6 +151,13 @@ impl AppServer {
             .map(|server| AppBuilder::start(&server, build_mode))
             .transpose()?;
 
+        let tw_watcher = TailwindCli::serve(
+            client.build.package_manifest_dir(),
+            client.build.config.application.tailwind_input.clone(),
+            client.build.config.application.tailwind_output.clone(),
+        )
+        .await?;
+
         tracing::debug!("Proxied port: {:?}", proxied_port);
 
         // Create the runner
@@ -174,6 +184,7 @@ impl AppServer {
             _force_sequential: force_sequential,
             cross_origin_policy,
             fullstack,
+            _tw_watcher: tw_watcher,
         };
 
         // Only register the hot-reload stuff if we're watching the filesystem

+ 1 - 1
packages/cli/src/settings.rs

@@ -124,7 +124,7 @@ impl CliSettings {
 
     /// Check if we should prefer to use the no-downloads feature
     pub(crate) fn prefer_no_downloads() -> bool {
-        if cfg!(feature = "no-downloads") {
+        if cfg!(feature = "no-downloads") && !cfg!(debug_assertions) {
             return true;
         }
 

+ 197 - 0
packages/cli/src/tailwind.rs

@@ -0,0 +1,197 @@
+use crate::{CliSettings, Result, Workspace};
+use anyhow::{anyhow, Context};
+use std::{
+    path::{Path, PathBuf},
+    process::Stdio,
+};
+use tokio::process::Command;
+
+#[derive(Debug)]
+pub(crate) struct TailwindCli {
+    version: String,
+}
+
+impl TailwindCli {
+    const V3_TAG: &'static str = "v3.4.15";
+    const V4_TAG: &'static str = "v4.1.5";
+
+    pub(crate) fn new(version: String) -> Self {
+        Self { version }
+    }
+
+    pub(crate) async fn serve(
+        manifest_dir: PathBuf,
+        input_path: Option<PathBuf>,
+        output_path: Option<PathBuf>,
+    ) -> Result<tokio::task::JoinHandle<Result<()>>> {
+        Ok(tokio::spawn(async move {
+            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 proc = tailwind.watch(&manifest_dir, input_path, output_path)?;
+            proc.wait_with_output().await?;
+
+            Ok(())
+        }))
+    }
+
+    /// Use the correct tailwind version based on the manifest directory.
+    /// - If `tailwind.config.js` or `tailwind.config.ts` exists, use v3.
+    /// - If `tailwind.css` exists, use v4.
+    pub(crate) fn autodetect(manifest_dir: &Path) -> Option<Self> {
+        if manifest_dir.join("tailwind.config.js").exists() {
+            return Some(Self::v3());
+        }
+
+        if manifest_dir.join("tailwind.config.ts").exists() {
+            return Some(Self::v3());
+        }
+
+        if manifest_dir.join("tailwind.css").exists() {
+            return Some(Self::v4());
+        }
+
+        None
+    }
+
+    pub(crate) fn v4() -> Self {
+        Self::new(Self::V4_TAG.to_string())
+    }
+
+    pub(crate) fn v3() -> Self {
+        Self::new(Self::V3_TAG.to_string())
+    }
+
+    pub(crate) fn watch(
+        &self,
+        manifest_dir: &Path,
+        input_path: Option<PathBuf>,
+        output_path: Option<PathBuf>,
+    ) -> Result<tokio::process::Child> {
+        let binary_path = self.get_binary_path()?;
+
+        let input_path = input_path.unwrap_or_else(|| manifest_dir.join("tailwind.css"));
+        let output_path =
+            output_path.unwrap_or_else(|| manifest_dir.join("assets").join("tailwind.css"));
+
+        if !output_path.exists() {
+            std::fs::create_dir_all(output_path.parent().unwrap())
+                .context("failed to create tailwindcss output directory")?;
+        }
+
+        let mut cmd = Command::new(binary_path);
+        let proc = cmd
+            .arg("--input")
+            .arg(input_path)
+            .arg("--output")
+            .arg(output_path)
+            .arg("--watch")
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .spawn()?;
+
+        Ok(proc)
+    }
+
+    fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
+        if CliSettings::prefer_no_downloads() {
+            which::which("tailwindcss").map_err(|_| anyhow!("Missing tailwindcss@{}", self.version))
+        } else {
+            let installed_name = self.installed_bin_name();
+            let install_dir = self.install_dir()?;
+            Ok(install_dir.join(installed_name))
+        }
+    }
+
+    fn installed_bin_name(&self) -> String {
+        let mut name = format!("tailwindcss-{}", self.version);
+        if cfg!(windows) {
+            name = format!("{name}.exe");
+        }
+        name
+    }
+
+    async fn install_github(&self) -> anyhow::Result<()> {
+        tracing::debug!(
+            "Attempting to install tailwindcss@{} from GitHub",
+            self.version
+        );
+
+        let url = self.git_install_url().ok_or_else(|| {
+            anyhow!(
+                "no available GitHub binary for tailwindcss@{}",
+                self.version
+            )
+        })?;
+
+        // Get the final binary location.
+        let binary_path = self.get_binary_path()?;
+
+        // Download then extract tailwindcss.
+        let bytes = reqwest::get(url).await?.bytes().await?;
+
+        std::fs::create_dir_all(binary_path.parent().unwrap())
+            .context("failed to create tailwindcss directory")?;
+
+        std::fs::write(&binary_path, &bytes).context("failed to write tailwindcss binary")?;
+
+        // Make the binary executable.
+        #[cfg(unix)]
+        {
+            use std::os::unix::fs::PermissionsExt;
+            let mut perms = binary_path.metadata()?.permissions();
+            perms.set_mode(0o755);
+            std::fs::set_permissions(&binary_path, perms)?;
+        }
+
+        Ok(())
+    }
+
+    fn downloaded_bin_name(&self) -> Option<String> {
+        let platform = match target_lexicon::HOST.operating_system {
+            target_lexicon::OperatingSystem::Linux => "linux",
+            target_lexicon::OperatingSystem::Darwin(_) => "macos",
+            target_lexicon::OperatingSystem::Windows => "windows",
+            _ => return None,
+        };
+
+        let arch = match target_lexicon::HOST.architecture {
+            target_lexicon::Architecture::X86_64 if platform == "windows" => "x64.exe",
+            target_lexicon::Architecture::X86_64 => "x64",
+            target_lexicon::Architecture::Aarch64(_) => "arm64",
+            _ => return None,
+        };
+
+        Some(format!("tailwindcss-{}-{}", platform, arch))
+    }
+
+    fn install_dir(&self) -> Result<PathBuf> {
+        let bindgen_dir = Workspace::dioxus_home_dir().join("tailwind/");
+        Ok(bindgen_dir)
+    }
+
+    fn git_install_url(&self) -> Option<String> {
+        // eg:
+        //
+        // https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.5/tailwindcss-linux-arm64
+        //
+        // tailwindcss-linux-arm64
+        // tailwindcss-linux-x64
+        // tailwindcss-macos-arm64
+        // tailwindcss-macos-x64
+        // tailwindcss-windows-x64.exe
+        // tailwindcss-linux-arm64-musl
+        // tailwindcss-linux-x64-musl
+        Some(format!(
+            "https://github.com/tailwindlabs/tailwindcss/releases/download/{}/{}",
+            self.version,
+            self.downloaded_bin_name()?
+        ))
+    }
+}

+ 2 - 5
packages/cli/src/wasm_bindgen.rs

@@ -1,4 +1,4 @@
-use crate::{CliSettings, Result};
+use crate::{CliSettings, Result, Workspace};
 use anyhow::{anyhow, Context};
 use flate2::read::GzDecoder;
 use std::path::{Path, PathBuf};
@@ -402,10 +402,7 @@ impl WasmBindgen {
     }
 
     async fn install_dir(&self) -> anyhow::Result<PathBuf> {
-        let bindgen_dir = dirs::data_local_dir()
-            .expect("user should be running on a compatible operating system")
-            .join("dioxus/wasm-bindgen/");
-
+        let bindgen_dir = Workspace::dioxus_home_dir().join("wasm-bindgen/");
         fs::create_dir_all(&bindgen_dir).await?;
         Ok(bindgen_dir)
     }

+ 7 - 0
packages/cli/src/workspace.rs

@@ -369,6 +369,13 @@ impl Workspace {
                 .context("Failed to find dx")?,
         )
     }
+
+    /// Returns the path to the dioxus home directory, used to install tools and other things
+    pub(crate) fn dioxus_home_dir() -> PathBuf {
+        dirs::data_local_dir()
+            .map(|f| f.join("dioxus/"))
+            .unwrap_or_else(|| dirs::home_dir().unwrap().join(".dioxus"))
+    }
 }
 
 impl std::fmt::Debug for Workspace {