use std::collections::HashMap;
use std::path::Path;
use std::process::Stdio;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::oneshot;
pub const SIDECAR_JS: &str = include_str!("assets/tailwind-sidecar.mjs");
pub fn is_tailwind_css(source: &str) -> bool {
source.contains("@import \"tailwindcss\"")
|| source.contains("@import 'tailwindcss'")
|| source.contains("@tailwind ")
|| source.contains("@theme")
}
pub struct Sidecar {
stdin: tokio::sync::Mutex<tokio::process::ChildStdin>,
pending: Mutex<HashMap<u64, oneshot::Sender<Result<String, String>>>>,
counter: AtomicU64,
base: String,
_child: tokio::process::Child,
}
impl Sidecar {
pub async fn spawn(root: &Path) -> anyhow::Result<std::sync::Arc<Sidecar>> {
let script = root.join(".oj-cache").join("tailwind-sidecar.mjs");
if let Some(parent) = script.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&script, SIDECAR_JS)?;
let mut child = tokio::process::Command::new("node")
.arg(&script)
.current_dir(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| anyhow::anyhow!("cannot spawn node for tailwind sidecar: {e}"))?;
let stdin = child.stdin.take().expect("piped stdin");
let stdout = child.stdout.take().expect("piped stdout");
let sidecar = std::sync::Arc::new(Sidecar {
stdin: tokio::sync::Mutex::new(stdin),
pending: Mutex::new(HashMap::new()),
counter: AtomicU64::new(1),
base: root.display().to_string(),
_child: child,
});
let reader_ref = std::sync::Arc::clone(&sidecar);
tokio::spawn(async move {
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(line)) = lines.next_line().await {
let Ok(msg) = serde_json::from_str::<serde_json::Value>(&line) else { continue };
let Some(id) = msg["id"].as_u64() else { continue };
let result = match msg["css"].as_str() {
Some(css) => Ok(css.to_string()),
None => Err(msg["error"].as_str().unwrap_or("sidecar error").to_string()),
};
if let Some(tx) = reader_ref.pending.lock().unwrap().remove(&id) {
let _ = tx.send(result);
}
}
});
Ok(sidecar)
}
pub async fn compile(&self, css: &str, from: &str) -> Result<String, String> {
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
self.pending.lock().unwrap().insert(id, tx);
let request = serde_json::json!({ "id": id, "base": self.base, "css": css, "from": from });
{
let mut stdin = self.stdin.lock().await;
if stdin.write_all(format!("{request}\n").as_bytes()).await.is_err() {
return Err("tailwind sidecar died (is tailwindcss installed?)".into());
}
}
match tokio::time::timeout(std::time::Duration::from_secs(20), rx).await {
Ok(Ok(result)) => result,
_ => Err("tailwind sidecar timed out".into()),
}
}
}