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