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    tailwind_config: serde_json::Value,
17    cdn_src: 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            tailwind_config: serde_json::json!({
27                "content": ["{src_dir}/**/*.{html,js,rs}"],
28                "theme": { "extend": {} },
29                "plugins": [],
30            }),
31            cdn_src: format!("https://cdn.tailwindcss.com"),
32            always: false,
33        }
34    }
35
36    /// changes the path from which the css file is loaded
37    /// specifying a file makes it required
38    /// specifying `None` looks for a `style.css` file
39    /// at the root of of the project
40    /// if the file does not exist it uses the the default:
41    /// ```css
42    /// @tailwind base;
43    /// @tailwind components;
44    /// @tailwind utilities;
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    /// specifies the config used by tailwind, the config needs to be specified as json
56    /// as it is used by both the jit and the normal config
57    /// (`{src_dir}` expands to the actual `/src` of the project)
58    ///
59    /// ```rust
60    /// // default config:
61    /// json!({
62    ///     "content": ["{src_dir}/**/*.{html,js,rs}"],
63    ///     "theme": { "extend": {} },
64    ///     "plugins": [],
65    /// })
66    /// ```
67    pub fn with_tw_config(mut self, config: serde_json::Value) -> Self {
68        self.tailwind_config = config; self
69    }
70
71    /// always rebuilds tailwind, never uses jit
72    /// (corosponds to the `include_tailwind!(always)` macro)
73    pub fn always(mut self) -> Self { self.always = true; self }
74
75    fn is_release() -> bool {
76        println!("cargo:rerun-if-env-changed=PROFILE");
77
78        match env::var("PROFILE").as_ref().map(|v| v.as_str()) {
79            Ok("release") => true,
80            Ok("debug") => false,
81            Ok(v) => {
82                println!("cargo:warning='PROFILE' was neither release nor debug ('{v}')");
83                false
84            },
85            Err(_) => {
86                println!("cargo:warning='PROFILE' was not defined, defaulting to debug");
87                false
88            },
89        }
90    }
91
92    const DEFAULT_PACKAGE_JSON: &'static str = r#"{
93    "name": "include-tailwind",
94    "version": "1.0.0",
95    "description": "the autogenerated package.json for include-tailwind",
96    "devDependencies": {
97        "tailwindcss": "^3.4.4"
98    }
99}
100"#;
101
102    const DEFAULT_STYLE_CSS: &'static str = r#"
103@tailwind base;
104@tailwind components;
105@tailwind utilities;
106"#;
107
108    fn config_string(&self, src_dir: &Path) -> Result<String, Error> {
109        let config_string = serde_json::to_string_pretty(&self.tailwind_config)
110            .expect("could not serialize tailwind config")
111            .replace("{src_dir}", src_dir.to_str().ok_or(Error::InvalidSrcPath)?);
112
113        Ok(config_string)
114    }
115
116    fn install_tailwind(&self, out_dir: &Path, src_dir: &Path) -> Result<(), Error> {
117        let package_json_path = out_dir.join("package.json");
118        let node_modules_path = out_dir.join("node_modules");
119        let tw_config_path = out_dir.join("tailwind.config.js");
120
121        if !package_json_path.exists() {
122            println!("creating package.json ({package_json_path:?})");
123            std::fs::write(&package_json_path, Self::DEFAULT_PACKAGE_JSON)?;
124        } else { println!("package.json already exists, not creating another one") }
125
126        if !node_modules_path.exists() {
127            println!("installing tailwind");
128            if !Command::new("npm").args(["install"])
129                .current_dir(out_dir)
130                .status()
131            .unwrap().success() { panic!("could not install tailwind") }
132        } else { println!("node_modules already exists, not installing") }
133
134        println!("writing tailwind config ({tw_config_path:?})");
135        let config_string = self.config_string(src_dir)?;
136        let config = format!("
137            module.exports = {config_string}
138        ");
139        std::fs::write(&tw_config_path, config)?;
140
141        Ok(())
142    }
143
144    fn compile_tailwind(&self, out_dir: &Path) -> Result<(), Error> {
145        let tw_in_path = out_dir.join("style.in.css");
146        let tw_out_path = out_dir.join("style.css");
147
148        if let Some(p) = &self.css_path {
149            println!("cargo:rerun-if-changed={}", p.to_string_lossy());
150            if p.exists() {
151                println!("copying {p:?} to build css");
152                std::fs::copy(p, &tw_in_path)?;
153            } else { panic!("specified a css path but it does not exists") }
154        } else {
155            let default_style_path = PathBuf::from("style.css");
156            if default_style_path.exists() {
157                println!("copying style.css (default path)");
158                std::fs::copy(&default_style_path, &tw_in_path)?;
159            } else {
160                println!("creating default style.css");
161                std::fs::write(&tw_in_path, Self::DEFAULT_STYLE_CSS)?;
162            }
163        }
164
165        if !Command::new("npx")
166            .args(["tailwindcss"])
167            .arg("-i").arg(&tw_in_path)
168            .arg("-o").arg(&tw_out_path)
169            .args(["--minify"])
170            .current_dir(out_dir)
171            .status().unwrap()
172        .success() {
173            panic!("could not build styles");
174        }
175
176        println!("cargo:rustc-env=INCLUDE_TAILWIND_PATH={}", tw_out_path.to_str().unwrap());
177
178        Ok(())
179    }
180
181    // https://tailwindcss.com/docs/installation/play-cdn
182    fn setup_jit(&self, out_dir: &Path, src_dir: &Path) -> Result<(), Error> {
183        let config_string = self.config_string(src_dir)?;
184        let jit_config_path = out_dir.join("jit_config.js");
185        let config = format!("tailwind.config = {config_string}");
186        std::fs::write(&jit_config_path, config)?;
187        println!("cargo:rustc-env=INCLUDE_TAILWIND_JIT_CONFIG_PATH={}",
188            jit_config_path.to_str().unwrap());
189
190        println!("cargo:rustc-env=INCLUDE_TAILWIND_JIT_URL={}", self.cdn_src);
191
192        Ok(())
193    }
194
195    /// builds tailwind using the specified config
196    pub fn build(&self) -> Result<(), Error> {
197        let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not provided"));
198        let src_dir = std::fs::canonicalize("./src").expect("could not canonicalize");
199        let release = Self::is_release();
200
201
202        if release || self.always {
203            self.install_tailwind(&out_dir, &src_dir)?;
204            self.compile_tailwind(&out_dir)?;
205        } else {
206            self.setup_jit(&out_dir, &src_dir)?;
207        }
208
209        Ok(())
210    }
211}
212
213#[derive(Debug, thiserror::Error)]
214pub enum Error {
215    #[error(transparent)]
216    Io(#[from] std::io::Error),
217    #[error("the source dir contained invalid unicode")]
218    InvalidSrcPath,
219    #[error("tailwind could not be installed")]
220    TailwindInstallError,
221}
222
223/// builds tailwind with the default config
224pub fn build_tailwind() -> Result<(), Error> { BuildConfig::default().build() }
225