Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tailwind v4 support #428

Merged
merged 13 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions examples/project/style/tailwind.css
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
11 changes: 0 additions & 11 deletions examples/project/tailwind.config.js

This file was deleted.

1 change: 1 addition & 0 deletions src/compile/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub async fn style(
build(&proj).await
})
}

fn build_sass(proj: &Arc<Project>) -> JoinHandle<Result<Outcome<String>>> {
let proj = proj.clone();
tokio::spawn(async move {
Expand Down
28 changes: 16 additions & 12 deletions src/compile/tailwind.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use camino::Utf8Path;
use tokio::process::Command;

use crate::{
Expand All @@ -14,8 +15,10 @@ use crate::{
};

pub async fn compile_tailwind(proj: &Project, tw_conf: &TailwindConfig) -> Result<Outcome<String>> {
if !tw_conf.config_file.exists() {
create_default_tailwind_config(tw_conf).await?;
if let Some(config_file) = tw_conf.config_file.as_ref() {
if !config_file.exists() {
create_default_tailwind_config(config_file).await?
}
}

let (line, process) = tailwind_process(proj, "tailwindcss", tw_conf).await?;
Expand Down Expand Up @@ -56,7 +59,7 @@ pub async fn compile_tailwind(proj: &Project, tw_conf: &TailwindConfig) -> Resul
}
}

async fn create_default_tailwind_config(tw_conf: &TailwindConfig) -> Result<()> {
async fn create_default_tailwind_config(config_file: &Utf8Path) -> Result<()> {
let contents = r#"/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
Expand All @@ -69,7 +72,7 @@ async fn create_default_tailwind_config(tw_conf: &TailwindConfig) -> Result<()>
plugins: [],
}
"#;
fs::write(&tw_conf.config_file, contents).await
fs::write(config_file, contents).await
}

pub async fn tailwind_process(
Expand All @@ -79,14 +82,15 @@ pub async fn tailwind_process(
) -> Result<(String, Command)> {
let tailwind = Exe::Tailwind.get().await.dot()?;

let mut args: Vec<&str> = vec![
"--input",
tw_conf.input_file.as_str(),
"--config",
tw_conf.config_file.as_str(),
"--output",
tw_conf.tmp_file.as_str(),
];
let mut args = vec!["--input", tw_conf.input_file.as_str()];

if let Some(config_file) = tw_conf.config_file.as_ref() {
args.push("--config");
args.push(config_file.as_str());
}

args.push("--output");
args.push(tw_conf.tmp_file.as_str());

if proj.release {
// minify & optimize
Expand Down
7 changes: 3 additions & 4 deletions src/config/dotenvs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::ProjectConfig;
use super::{ProjectConfig, ENV_VAR_LEPTOS_SASS_VERSION, ENV_VAR_LEPTOS_TAILWIND_VERSION};
use crate::ext::anyhow::Result;
use crate::ext::exe;
use camino::{Utf8Path, Utf8PathBuf};
use std::{env, fs};

Expand Down Expand Up @@ -59,8 +58,8 @@ fn overlay(conf: &mut ProjectConfig, envs: impl Iterator<Item = (String, String)
"DISABLE_SERVER_FN_HASH" => conf.disable_server_fn_hash = true,
// put these here to suppress the warning, but there's no
// good way at the moment to pull the ProjectConfig all the way to Exe
exe::ENV_VAR_LEPTOS_TAILWIND_VERSION => {}
exe::ENV_VAR_LEPTOS_SASS_VERSION => {}
ENV_VAR_LEPTOS_TAILWIND_VERSION => {}
ENV_VAR_LEPTOS_SASS_VERSION => {}
_ if key.starts_with("LEPTOS_") => {
log::warn!("Env {key} is not used by cargo-leptos")
}
Expand Down
2 changes: 2 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod profile;
mod project;
mod style;
mod tailwind;
mod version;

use std::{fmt::Debug, sync::Arc};

Expand All @@ -27,6 +28,7 @@ pub use profile::Profile;
pub use project::{Project, ProjectConfig};
pub use style::StyleConfig;
pub use tailwind::TailwindConfig;
pub use version::*;

pub struct Config {
/// absolute path to the working dir
Expand Down
28 changes: 21 additions & 7 deletions src/config/tailwind.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use camino::Utf8PathBuf;

use super::ProjectConfig;
use super::{ProjectConfig, VersionConfig};
use anyhow::{bail, Result};

#[derive(Clone, Debug)]
pub struct TailwindConfig {
pub input_file: Utf8PathBuf,
pub config_file: Utf8PathBuf,
pub config_file: Option<Utf8PathBuf>,
pub tmp_file: Utf8PathBuf,
}

Expand All @@ -21,11 +21,25 @@ impl TailwindConfig {
return Ok(None);
};

let config_file = conf.config_dir.join(
conf.tailwind_config_file
.clone()
.unwrap_or_else(|| Utf8PathBuf::from("tailwind.config.js")),
);
let is_v_4 = VersionConfig::Tailwind.version().starts_with("v4");

let config_file = if is_v_4 {
if conf.tailwind_config_file.is_some()
|| conf.config_dir.join("tailwind.config.js").exists()
{
log::info!("JavaScript config files are no longer required in Tailwind CSS v4. If you still need to use a JS config file, refer to the docs here: https://tailwindcss.com/docs/upgrade-guide#using-a-javascript-config-file.");
}

conf.tailwind_config_file.clone()
} else {
Some(
conf.config_dir.join(
conf.tailwind_config_file
.clone()
.unwrap_or_else(|| Utf8PathBuf::from("tailwind.config.js")),
),
)
};

let tmp_file = conf.tmp_dir.join("tailwind.css");

Expand Down
31 changes: 31 additions & 0 deletions src/config/version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::{borrow::Cow, env};

pub const ENV_VAR_LEPTOS_TAILWIND_VERSION: &str = "LEPTOS_TAILWIND_VERSION";
pub const ENV_VAR_LEPTOS_SASS_VERSION: &str = "LEPTOS_SASS_VERSION";

pub enum VersionConfig {
Tailwind,
Sass,
}

impl VersionConfig {
pub fn version<'a>(&self) -> Cow<'a, str> {
env::var(self.env_var_version_name())
.map(Cow::Owned)
.unwrap_or_else(|_| self.default_version().into())
}

pub fn default_version(&self) -> &'static str {
match self {
Self::Tailwind => "v4.0.6",
Self::Sass => "1.83.4",
}
}

pub fn env_var_version_name(&self) -> &'static str {
match self {
Self::Tailwind => ENV_VAR_LEPTOS_TAILWIND_VERSION,
Self::Sass => ENV_VAR_LEPTOS_SASS_VERSION,
}
}
}
69 changes: 41 additions & 28 deletions src/ext/exe.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
config::VersionConfig,
ext::{
anyhow::{bail, Context, Result},
Paint,
Expand All @@ -7,6 +8,7 @@ use crate::{
};
use bytes::Bytes;
use std::{
borrow::Cow,
fs::{self, File},
io::{Cursor, Write},
path::{Path, PathBuf},
Expand Down Expand Up @@ -40,9 +42,6 @@ lazy_static::lazy_static! {
static ref ON_STARTUP_DEBUG_ONCE: Once = Once::new();
}

pub const ENV_VAR_LEPTOS_TAILWIND_VERSION: &str = "LEPTOS_TAILWIND_VERSION";
pub const ENV_VAR_LEPTOS_SASS_VERSION: &str = "LEPTOS_SASS_VERSION";

impl ExeMeta {
#[allow(clippy::wrong_self_convention)]
fn from_global_path(&self) -> Option<PathBuf> {
Expand Down Expand Up @@ -260,7 +259,7 @@ impl Exe {
/// as it carries classifiers, etc, but strip non-ascii
/// digits from the prefix.
#[inline]
fn sanitize_version_prefix<'a>(ver_string: &'a str) -> Result<&'a str> {
fn sanitize_version_prefix(ver_string: &str) -> Result<&str> {
if let [b'v', rest @ ..] = ver_string.as_bytes() {
str::from_utf8(rest).dot()
} else {
Expand Down Expand Up @@ -304,11 +303,14 @@ impl Command for CommandTailwind {
fn name(&self) -> &'static str {
"tailwindcss"
}
fn version(&self) -> Cow<'_, str> {
VersionConfig::Tailwind.version()
}
fn default_version(&self) -> &'static str {
"v3.4.0"
VersionConfig::Tailwind.default_version()
}
fn env_var_version_name(&self) -> &'static str {
ENV_VAR_LEPTOS_TAILWIND_VERSION
VersionConfig::Tailwind.env_var_version_name()
}
fn github_owner(&self) -> &'static str {
"tailwindlabs"
Expand All @@ -319,6 +321,8 @@ impl Command for CommandTailwind {

/// Tool binary download url for the given OS and platform arch
fn download_url(&self, target_os: &str, target_arch: &str, version: &str) -> Result<String> {
let use_musl = is_linux_musl_env() && version.starts_with("v4");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I should put in the effort to make the Tailwind version check more robust. I doubt they'll be coming out with a new major version any time soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might consider making it error if the version is greater than 4 and provide a useful error message


match (target_os, target_arch) {
("windows", "x86_64") => Ok(format!(
"https://github.com/{}/{}/releases/download/{}/{}-windows-x64.exe",
Expand All @@ -341,13 +345,27 @@ impl Command for CommandTailwind {
version,
self.name()
)),
("linux", "x86_64") if use_musl => Ok(format!(
"https://github.com/{}/{}/releases/download/{}/{}-linux-x64-musl",
self.github_owner(),
self.github_repo(),
version,
self.name()
)),
("linux", "x86_64") => Ok(format!(
"https://github.com/{}/{}/releases/download/{}/{}-linux-x64",
self.github_owner(),
self.github_repo(),
version,
self.name()
)),
("linux", "aarch64") if use_musl => Ok(format!(
"https://github.com/{}/{}/releases/download/{}/{}-linux-arm64-musl",
self.github_owner(),
self.github_repo(),
version,
self.name()
)),
("linux", "aarch64") => Ok(format!(
"https://github.com/{}/{}/releases/download/{}/{}-linux-arm64",
self.github_owner(),
Expand All @@ -364,17 +382,16 @@ impl Command for CommandTailwind {
}
}

fn executable_name(
&self,
target_os: &str,
target_arch: &str,
_version: Option<&str>,
) -> Result<String> {
fn executable_name(&self, target_os: &str, target_arch: &str, version: &str) -> Result<String> {
let use_musl = is_linux_musl_env() && version.starts_with("v4");

Ok(match (target_os, target_arch) {
("windows", _) => format!("{}-windows-x64.exe", self.name()),
("macos", "x86_64") => format!("{}-macos-x64", self.name()),
("macos", "aarch64") => format!("{}-macos-arm64", self.name()),
("linux", "x86_64") if use_musl => format!("{}-linux-x64-musl", self.name()),
("linux", "x86_64") => format!("{}-linux-x64", self.name()),
(_, _) if use_musl => format!("{}-linux-arm64-musl", self.name()),
(_, _) => format!("{}-linux-arm64", self.name()),
})
}
Expand All @@ -383,17 +400,19 @@ impl Command for CommandTailwind {
"Try manually installing tailwindcss: https://tailwindcss.com/docs/installation".to_string()
}
}

#[async_trait]
impl Command for CommandSass {
fn name(&self) -> &'static str {
"sass"
}
fn version(&self) -> Cow<'_, str> {
VersionConfig::Sass.version()
}
fn default_version(&self) -> &'static str {
"1.58.3"
VersionConfig::Sass.default_version()
}
fn env_var_version_name(&self) -> &'static str {
ENV_VAR_LEPTOS_SASS_VERSION
VersionConfig::Sass.env_var_version_name()
}
fn github_owner(&self) -> &'static str {
"dart-musl"
Expand Down Expand Up @@ -450,7 +469,7 @@ impl Command for CommandSass {
&self,
target_os: &str,
_target_arch: &str,
_version: Option<&str>,
_version: &str,
) -> Result<String> {
Ok(match target_os {
"windows" => "dart-sass/sass.bat".to_string(),
Expand All @@ -462,25 +481,21 @@ impl Command for CommandSass {
"Try manually installing sass: https://sass-lang.com/install".to_string()
}
}

#[async_trait]
/// Template trait, implementors should only fill in
/// the command-specific logic. Handles caching, latest
/// version checking against the GitHub API and env var
/// version override for a given command.
#[async_trait]
trait Command {
fn name(&self) -> &'static str;
fn version(&self) -> Cow<'_, str>;
fn default_version(&self) -> &str;
fn env_var_version_name(&self) -> &str;
fn github_owner(&self) -> &str;
fn github_repo(&self) -> &str;
fn download_url(&self, target_os: &str, target_arch: &str, version: &str) -> Result<String>;
fn executable_name(
&self,
target_os: &str,
target_arch: &str,
version: Option<&str>,
) -> Result<String>;
fn executable_name(&self, target_os: &str, target_arch: &str, version: &str) -> Result<String>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made version a &str instead of an Option<&str> since the only place the function is called, the version is guarunteed to be known. It also doesn't seem like this function will be called in a different place.

#[allow(unused)]
fn manual_install_instructions(&self) -> String {
// default placeholder text, individual commands can override and customize
Expand All @@ -504,7 +519,7 @@ trait Command {
async fn exe_meta(&self, target_os: &str, target_arch: &str) -> Result<ExeMeta> {
let version = self.resolve_version().await;
let url = self.download_url(target_os, target_arch, version.as_str())?;
let exe = self.executable_name(target_os, target_arch, Some(version.as_str()))?;
let exe = self.executable_name(target_os, target_arch, version.as_str())?;
Ok(ExeMeta {
name: self.name(),
version,
Expand Down Expand Up @@ -652,9 +667,7 @@ trait Command {
return self.default_version().into();
}

let version = env::var(self.env_var_version_name())
.unwrap_or_else(|_| self.default_version().into())
.to_owned();
let version = self.version();

let latest = self.check_for_latest_version().await;

Expand Down Expand Up @@ -685,7 +698,7 @@ trait Command {
),
}

version
version.to_string()
}
}

Expand Down
Loading
Loading