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 run_once(
manifest_dir: PathBuf,
input_path: Option<PathBuf>,
output_path: Option<PathBuf>,
) -> Result<()> {
let Some(tailwind) = Self::autodetect(&manifest_dir, &input_path) 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>,
output_path: Option<PathBuf>,
) -> tokio::task::JoinHandle<Result<()>> {
tokio::spawn(async move {
let Some(tailwind) = Self::autodetect(&manifest_dir, &input_path) else {
return Ok(());
};
if !tailwind.get_binary_path()?.exists() {
tracing::info!("Installing tailwindcss@{}", tailwind.version);
tailwind.install_github().await?;
}
let mut proc = tailwind.run(&manifest_dir, input_path, output_path, true)?;
let stdin = proc.stdin.take();
proc.wait().await?;
drop(stdin);
Ok(())
})
}
pub(crate) fn autodetect(manifest_dir: &Path, input_path: &Option<PathBuf>) -> Option<Self> {
let dir = input_path
.as_ref()
.map(|p| manifest_dir.join(p))
.and_then(|p| p.parent().map(|parent| parent.to_path_buf()))
.unwrap_or(manifest_dir.to_path_buf());
if dir.join("tailwind.config.js").exists() || dir.join("tailwind.config.ts").exists() {
return Some(Self::v3());
}
if input_path
.as_ref()
.map(|p| manifest_dir.join(p).exists())
.unwrap_or_else(|| 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 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()?;
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")?;
}
tracing::debug!("Spawning tailwindcss@{} with args: {:?}", self.version, {
[
binary_path.to_string_lossy().to_string(),
"--input".to_string(),
input_path.to_string_lossy().to_string(),
"--output".to_string(),
output_path.to_string_lossy().to_string(),
"--watch".to_string(),
]
});
let mut cmd = Command::new(binary_path);
let proc = cmd
.arg("--input")
.arg(input_path)
.arg("--output")
.arg(output_path)
.args(watch.then_some("--watch"))
.current_dir(manifest_dir)
.kill_on_drop(true)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Ok(proc)
}
pub 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) -> &'static str {
if cfg!(windows) {
"tailwindcss.exe"
} else {
"tailwindcss"
}
}
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
)
})?;
let binary_path = self.get_binary_path()?;
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")?;
#[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(_) if platform == "windows" => "x64.exe",
target_lexicon::Architecture::Aarch64(_) => "arm64",
_ => return None,
};
Some(format!("tailwindcss-{platform}-{arch}"))
}
fn install_dir(&self) -> Result<PathBuf> {
Ok(Workspace::tools_dir().join(format!("tailwindcss-{}", self.version)))
}
fn git_install_url(&self) -> Option<String> {
Some(format!(
"https://github.com/tailwindlabs/tailwindcss/releases/download/{}/{}",
self.version,
self.downloaded_bin_name()?
))
}
}