Skip to main content

include_tailwind_build/
lib.rs

1use std::{env, path::{Path, PathBuf}, process::Command};
2
3pub use serde_json::json;
4
5impl Default for BuildConfig { fn default() -> Self { Self::new() } }
6/// config for building tailwind
7///
8/// ```rust
9/// // example
10/// BuildConfig::new().with_cdn_src("https://my.cdn.com").build()?;
11/// ```
12#[derive(Debug, Clone)]
13pub struct BuildConfig {
14    css_path: Option<PathBuf>,
15    always: bool,
16    cdn_src: String,
17    tailwind_version: String,
18}
19
20
21impl BuildConfig {
22    /// creates a new instance of the tailwind config with default values
23    pub fn new() -> Self {
24        Self {
25            css_path: None, // style.css
26            cdn_src: format!("https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"),
27            tailwind_version: format!("@tailwindcss/cli@4.3.0"),
28            always: false,
29        }
30    }
31
32    /// specify the tailwind cli package to use
33    ///
34    /// default: `@tailwindcss/cli@4.3.0`
35    pub fn with_tailwind_package(mut self, version: impl Into<String>) -> Self {
36        self.tailwind_version = version.into(); self
37    }
38
39    /// changes the path from which the css file is loaded
40    /// specifying a file makes it required
41    /// specifying `None` looks for a `style.css` file
42    /// at the root of of the project
43    /// if the file does not exist it uses the the default:
44    /// ```css
45    /// @import "tailwindcss";
46    /// @source "src/**/*.{rs,html,js}";
47    /// ```
48    pub fn with_path(mut self, p: Option<impl AsRef<Path>>) -> Self {
49        self.css_path = p.map(|v| v.as_ref().to_path_buf()); self
50    }
51
52    /// specifies the cdn used as a source for the jit builds
53    pub fn with_cdn_src(mut self, s: impl Into<String>) -> Self {
54        self.cdn_src = s.into(); self
55    }
56
57    /// always rebuilds tailwind, never uses jit
58    /// (corosponds to the `include_tailwind!(always)` macro)
59    pub fn always(mut self) -> Self { self.always = true; self }
60
61    fn is_release() -> bool {
62        println!("cargo:rerun-if-env-changed=PROFILE");
63
64        match env::var("PROFILE").as_ref().map(|v| v.as_str()) {
65            Ok("release") => true,
66            Ok("debug") => false,
67            Ok(v) => {
68                println!("cargo:warning='PROFILE' was neither release nor debug ('{v}')");
69                false
70            },
71            Err(_) => {
72                println!("cargo:warning='PROFILE' was not defined, defaulting to debug");
73                false
74            },
75        }
76    }
77
78    const DEFAULT_STYLE_CSS: &'static str = r#"
79@import "tailwindcss";
80@source "{src_dir}/**/*.{rs,html,js}"
81"#;
82
83    fn css_path(&self) -> PathBuf {
84        let p = if let Some(css_path) = &self.css_path {
85            css_path.clone()
86        } else {
87            let default_p = PathBuf::from("style.css");
88            if default_p.exists() { default_p }
89            else {
90                let temp_p = PathBuf::from(std::env::var("OUT_DIR").unwrap())
91                    .join("style.css");
92
93                if !temp_p.exists() {
94                    std::fs::write(&temp_p, Self::DEFAULT_STYLE_CSS)
95                        .expect("could not write temp style.css file");
96                }
97
98                temp_p
99            }
100        };
101        println!("cargo:rerun-if-changed={}", p.to_str().unwrap());
102        p
103    }
104
105    fn write_css_string(&self, out_dir: &Path, src_dir: &Path) -> Result<(), Error> {
106        let css_path = self.css_path();
107        let css_string = std::fs::read_to_string(&css_path)
108            .map_err(|err| Error::StyleCssNotFound(css_path, err))?
109            .replace("{src_dir}", src_dir.to_str().ok_or(Error::InvalidSrcPath)?);
110        std::fs::write(out_dir.join("style.in.css"), css_string)?;
111        Ok(())
112    }
113
114    fn compile_tailwind(&self, out_dir: &Path) -> Result<(), Error> {
115        let tw_in_path = out_dir.join("style.in.css");
116        let tw_out_path = out_dir.join("style.css");
117
118        if !Command::new("npx")
119            .args([&self.tailwind_version])
120            .arg("-i").arg(&tw_in_path)
121            .arg("-o").arg(&tw_out_path)
122            .args(["--minify"])
123            .current_dir(out_dir)
124            .status().unwrap()
125        .success() {
126            panic!("could not build styles");
127        }
128
129        println!("cargo:rustc-env=INCLUDE_TAILWIND_PATH={}", tw_out_path.to_str().unwrap());
130
131        Ok(())
132    }
133
134    // https://tailwindcss.com/docs/installation/play-cdn
135    fn setup_jit(&self, out_dir: &Path) -> Result<(), Error> {
136        let jit_config_path = out_dir.join("style.in.css");
137
138        println!("cargo:rustc-env=INCLUDE_TAILWIND_JIT_CONFIG_PATH={}",
139            jit_config_path.to_str().unwrap());
140
141        println!("cargo:rustc-env=INCLUDE_TAILWIND_JIT_URL={}", self.cdn_src);
142
143        Ok(())
144    }
145
146    /// builds tailwind using the specified config
147    pub fn build(&self) -> Result<(), Error> {
148        let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not provided"));
149        let src_dir = std::fs::canonicalize("./src").expect("could not canonicalize");
150        let release = Self::is_release();
151
152        self.write_css_string(&out_dir, &src_dir)?;
153
154        if release || self.always {
155            self.compile_tailwind(&out_dir)?;
156        } else {
157            self.setup_jit(&out_dir)?;
158        }
159
160        Ok(())
161    }
162}
163
164#[derive(Debug, thiserror::Error)]
165pub enum Error {
166    #[error(transparent)]
167    Io(#[from] std::io::Error),
168    #[error("the source dir contained invalid unicode")]
169    InvalidSrcPath,
170    #[error("could not read style.css at '{0}' -> {1}")]
171    StyleCssNotFound(PathBuf, std::io::Error),
172    #[error("tailwind could not be installed")]
173    TailwindInstallError,
174}
175
176/// builds tailwind with the default config
177pub fn build_tailwind() -> Result<(), Error> { BuildConfig::default().build() }
178