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
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-07 17:17:39 -07:00
1 parent 7d5bd44791
commit cb651a8ca2
11 files changed
+242 -11

No files matched your search

+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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::*;
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {