Skip to content

Commit cb651a8

Browse files
authored
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
1 parent 7d5bd44 commit cb651a8

File tree

11 files changed

+242
-11
lines changed

11 files changed

+242
-11
lines changed

packages/cli/src/build/builder.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -657,8 +657,6 @@ impl AppBuilder {
657657
None => self.build.asset_dir(),
658658
};
659659

660-
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
661-
662660
// Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
663661
let changed_file = dunce::canonicalize(changed_file)
664662
.inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}"))
@@ -668,6 +666,8 @@ impl AppBuilder {
668666
let resource = artifacts.assets.assets.get(&changed_file)?;
669667
let output_path = asset_dir.join(resource.bundled_path());
670668

669+
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
670+
671671
// Remove the old asset if it exists
672672
_ = std::fs::remove_file(&output_path);
673673

@@ -1310,6 +1310,6 @@ We checked the folder: {}
13101310

13111311
/// Check if the queued build is blocking hotreloads
13121312
pub(crate) fn can_receive_hotreloads(&self) -> bool {
1313-
matches!(&self.stage, BuildStage::Success)
1313+
matches!(&self.stage, BuildStage::Success | BuildStage::Failed)
13141314
}
13151315
}

packages/cli/src/build/request.rs

+10
Original file line numberDiff line numberDiff line change
@@ -3758,4 +3758,14 @@ r#" <script>
37583758
trimmed_path
37593759
}
37603760
}
3761+
3762+
/// Get the path to the package manifest directory
3763+
pub(crate) fn package_manifest_dir(&self) -> PathBuf {
3764+
self.workspace.krates[self.crate_package]
3765+
.manifest_path
3766+
.parent()
3767+
.unwrap()
3768+
.to_path_buf()
3769+
.into()
3770+
}
37613771
}

packages/cli/src/config/app.rs

+6
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ pub(crate) struct ApplicationConfig {
1010

1111
#[serde(default)]
1212
pub(crate) out_dir: Option<PathBuf>,
13+
14+
#[serde(default)]
15+
pub(crate) tailwind_input: Option<PathBuf>,
16+
17+
#[serde(default)]
18+
pub(crate) tailwind_output: Option<PathBuf>,
1319
}

packages/cli/src/config/dioxus_config.rs

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ impl Default for DioxusConfig {
2222
asset_dir: None,
2323
sub_package: None,
2424
out_dir: None,
25+
tailwind_input: None,
26+
tailwind_output: None,
2527
},
2628
web: WebConfig {
2729
app: WebAppConfig {

packages/cli/src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod platform;
1717
mod rustcwrapper;
1818
mod serve;
1919
mod settings;
20+
mod tailwind;
2021
mod wasm_bindgen;
2122
mod wasm_opt;
2223
mod workspace;
@@ -31,6 +32,7 @@ pub(crate) use logging::*;
3132
pub(crate) use platform::*;
3233
pub(crate) use rustcwrapper::*;
3334
pub(crate) use settings::*;
35+
pub(crate) use tailwind::*;
3436
pub(crate) use wasm_bindgen::*;
3537
pub(crate) use workspace::*;
3638

packages/cli/src/serve/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
7272
continue;
7373
}
7474

75-
tracing::debug!("Starting hotpatching: {:?}", files);
7675
builder.handle_file_change(&files, &mut devserver).await;
7776
}
7877

packages/cli/src/serve/runner.rs

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::{AppBuilder, ServeUpdate, WebServer};
22
use crate::{
33
BuildArtifacts, BuildId, BuildMode, BuildTargets, Error, HotpatchModuleCache, Platform, Result,
4-
ServeArgs, TraceSrc, Workspace,
4+
ServeArgs, TailwindCli, TraceSrc, Workspace,
55
};
66
use anyhow::Context;
77
use dioxus_core::internal::{
@@ -72,6 +72,9 @@ pub(crate) struct AppServer {
7272
pub(crate) devserver_bind_ip: IpAddr,
7373
pub(crate) proxied_port: Option<u16>,
7474
pub(crate) cross_origin_policy: bool,
75+
76+
// Additional plugin-type tools
77+
pub(crate) _tw_watcher: tokio::task::JoinHandle<Result<()>>,
7578
}
7679

7780
pub(crate) struct CachedFile {
@@ -148,6 +151,13 @@ impl AppServer {
148151
.map(|server| AppBuilder::start(&server, build_mode))
149152
.transpose()?;
150153

154+
let tw_watcher = TailwindCli::serve(
155+
client.build.package_manifest_dir(),
156+
client.build.config.application.tailwind_input.clone(),
157+
client.build.config.application.tailwind_output.clone(),
158+
)
159+
.await?;
160+
151161
tracing::debug!("Proxied port: {:?}", proxied_port);
152162

153163
// Create the runner
@@ -174,6 +184,7 @@ impl AppServer {
174184
_force_sequential: force_sequential,
175185
cross_origin_policy,
176186
fullstack,
187+
_tw_watcher: tw_watcher,
177188
};
178189

179190
// Only register the hot-reload stuff if we're watching the filesystem

packages/cli/src/settings.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ impl CliSettings {
124124

125125
/// Check if we should prefer to use the no-downloads feature
126126
pub(crate) fn prefer_no_downloads() -> bool {
127-
if cfg!(feature = "no-downloads") {
127+
if cfg!(feature = "no-downloads") && !cfg!(debug_assertions) {
128128
return true;
129129
}
130130

packages/cli/src/tailwind.rs

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
use crate::{CliSettings, Result, Workspace};
2+
use anyhow::{anyhow, Context};
3+
use std::{
4+
path::{Path, PathBuf},
5+
process::Stdio,
6+
};
7+
use tokio::process::Command;
8+
9+
#[derive(Debug)]
10+
pub(crate) struct TailwindCli {
11+
version: String,
12+
}
13+
14+
impl TailwindCli {
15+
const V3_TAG: &'static str = "v3.4.15";
16+
const V4_TAG: &'static str = "v4.1.5";
17+
18+
pub(crate) fn new(version: String) -> Self {
19+
Self { version }
20+
}
21+
22+
pub(crate) async fn serve(
23+
manifest_dir: PathBuf,
24+
input_path: Option<PathBuf>,
25+
output_path: Option<PathBuf>,
26+
) -> Result<tokio::task::JoinHandle<Result<()>>> {
27+
Ok(tokio::spawn(async move {
28+
let Some(tailwind) = Self::autodetect(&manifest_dir) else {
29+
return Ok(());
30+
};
31+
32+
if !tailwind.get_binary_path()?.exists() {
33+
tracing::info!("Installing tailwindcss@{}", tailwind.version);
34+
tailwind.install_github().await?;
35+
}
36+
37+
let proc = tailwind.watch(&manifest_dir, input_path, output_path)?;
38+
proc.wait_with_output().await?;
39+
40+
Ok(())
41+
}))
42+
}
43+
44+
/// Use the correct tailwind version based on the manifest directory.
45+
/// - If `tailwind.config.js` or `tailwind.config.ts` exists, use v3.
46+
/// - If `tailwind.css` exists, use v4.
47+
pub(crate) fn autodetect(manifest_dir: &Path) -> Option<Self> {
48+
if manifest_dir.join("tailwind.config.js").exists() {
49+
return Some(Self::v3());
50+
}
51+
52+
if manifest_dir.join("tailwind.config.ts").exists() {
53+
return Some(Self::v3());
54+
}
55+
56+
if manifest_dir.join("tailwind.css").exists() {
57+
return Some(Self::v4());
58+
}
59+
60+
None
61+
}
62+
63+
pub(crate) fn v4() -> Self {
64+
Self::new(Self::V4_TAG.to_string())
65+
}
66+
67+
pub(crate) fn v3() -> Self {
68+
Self::new(Self::V3_TAG.to_string())
69+
}
70+
71+
pub(crate) fn watch(
72+
&self,
73+
manifest_dir: &Path,
74+
input_path: Option<PathBuf>,
75+
output_path: Option<PathBuf>,
76+
) -> Result<tokio::process::Child> {
77+
let binary_path = self.get_binary_path()?;
78+
79+
let input_path = input_path.unwrap_or_else(|| manifest_dir.join("tailwind.css"));
80+
let output_path =
81+
output_path.unwrap_or_else(|| manifest_dir.join("assets").join("tailwind.css"));
82+
83+
if !output_path.exists() {
84+
std::fs::create_dir_all(output_path.parent().unwrap())
85+
.context("failed to create tailwindcss output directory")?;
86+
}
87+
88+
let mut cmd = Command::new(binary_path);
89+
let proc = cmd
90+
.arg("--input")
91+
.arg(input_path)
92+
.arg("--output")
93+
.arg(output_path)
94+
.arg("--watch")
95+
.stdout(Stdio::piped())
96+
.stderr(Stdio::piped())
97+
.spawn()?;
98+
99+
Ok(proc)
100+
}
101+
102+
fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
103+
if CliSettings::prefer_no_downloads() {
104+
which::which("tailwindcss").map_err(|_| anyhow!("Missing tailwindcss@{}", self.version))
105+
} else {
106+
let installed_name = self.installed_bin_name();
107+
let install_dir = self.install_dir()?;
108+
Ok(install_dir.join(installed_name))
109+
}
110+
}
111+
112+
fn installed_bin_name(&self) -> String {
113+
let mut name = format!("tailwindcss-{}", self.version);
114+
if cfg!(windows) {
115+
name = format!("{name}.exe");
116+
}
117+
name
118+
}
119+
120+
async fn install_github(&self) -> anyhow::Result<()> {
121+
tracing::debug!(
122+
"Attempting to install tailwindcss@{} from GitHub",
123+
self.version
124+
);
125+
126+
let url = self.git_install_url().ok_or_else(|| {
127+
anyhow!(
128+
"no available GitHub binary for tailwindcss@{}",
129+
self.version
130+
)
131+
})?;
132+
133+
// Get the final binary location.
134+
let binary_path = self.get_binary_path()?;
135+
136+
// Download then extract tailwindcss.
137+
let bytes = reqwest::get(url).await?.bytes().await?;
138+
139+
std::fs::create_dir_all(binary_path.parent().unwrap())
140+
.context("failed to create tailwindcss directory")?;
141+
142+
std::fs::write(&binary_path, &bytes).context("failed to write tailwindcss binary")?;
143+
144+
// Make the binary executable.
145+
#[cfg(unix)]
146+
{
147+
use std::os::unix::fs::PermissionsExt;
148+
let mut perms = binary_path.metadata()?.permissions();
149+
perms.set_mode(0o755);
150+
std::fs::set_permissions(&binary_path, perms)?;
151+
}
152+
153+
Ok(())
154+
}
155+
156+
fn downloaded_bin_name(&self) -> Option<String> {
157+
let platform = match target_lexicon::HOST.operating_system {
158+
target_lexicon::OperatingSystem::Linux => "linux",
159+
target_lexicon::OperatingSystem::Darwin(_) => "macos",
160+
target_lexicon::OperatingSystem::Windows => "windows",
161+
_ => return None,
162+
};
163+
164+
let arch = match target_lexicon::HOST.architecture {
165+
target_lexicon::Architecture::X86_64 if platform == "windows" => "x64.exe",
166+
target_lexicon::Architecture::X86_64 => "x64",
167+
target_lexicon::Architecture::Aarch64(_) => "arm64",
168+
_ => return None,
169+
};
170+
171+
Some(format!("tailwindcss-{}-{}", platform, arch))
172+
}
173+
174+
fn install_dir(&self) -> Result<PathBuf> {
175+
let bindgen_dir = Workspace::dioxus_home_dir().join("tailwind/");
176+
Ok(bindgen_dir)
177+
}
178+
179+
fn git_install_url(&self) -> Option<String> {
180+
// eg:
181+
//
182+
// https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.5/tailwindcss-linux-arm64
183+
//
184+
// tailwindcss-linux-arm64
185+
// tailwindcss-linux-x64
186+
// tailwindcss-macos-arm64
187+
// tailwindcss-macos-x64
188+
// tailwindcss-windows-x64.exe
189+
// tailwindcss-linux-arm64-musl
190+
// tailwindcss-linux-x64-musl
191+
Some(format!(
192+
"https://github.com/tailwindlabs/tailwindcss/releases/download/{}/{}",
193+
self.version,
194+
self.downloaded_bin_name()?
195+
))
196+
}
197+
}

packages/cli/src/wasm_bindgen.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{CliSettings, Result};
1+
use crate::{CliSettings, Result, Workspace};
22
use anyhow::{anyhow, Context};
33
use flate2::read::GzDecoder;
44
use std::path::{Path, PathBuf};
@@ -402,10 +402,7 @@ impl WasmBindgen {
402402
}
403403

404404
async fn install_dir(&self) -> anyhow::Result<PathBuf> {
405-
let bindgen_dir = dirs::data_local_dir()
406-
.expect("user should be running on a compatible operating system")
407-
.join("dioxus/wasm-bindgen/");
408-
405+
let bindgen_dir = Workspace::dioxus_home_dir().join("wasm-bindgen/");
409406
fs::create_dir_all(&bindgen_dir).await?;
410407
Ok(bindgen_dir)
411408
}

packages/cli/src/workspace.rs

+7
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,13 @@ impl Workspace {
369369
.context("Failed to find dx")?,
370370
)
371371
}
372+
373+
/// Returns the path to the dioxus home directory, used to install tools and other things
374+
pub(crate) fn dioxus_home_dir() -> PathBuf {
375+
dirs::data_local_dir()
376+
.map(|f| f.join("dioxus/"))
377+
.unwrap_or_else(|| dirs::home_dir().unwrap().join(".dioxus"))
378+
}
372379
}
373380

374381
impl std::fmt::Debug for Workspace {

0 commit comments

Comments
 (0)