include_tailwind_build/
lib.rs1use std::{env, path::{Path, PathBuf}, process::Command};
2
3pub use serde_json::json;
4
5impl Default for BuildConfig { fn default() -> Self { Self::new() } }
6#[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 pub fn new() -> Self {
24 Self {
25 css_path: None, 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 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 pub fn with_cdn_src(mut self, s: impl Into<String>) -> Self {
52 self.cdn_src = s.into(); self
53 }
54
55 pub fn with_tw_config(mut self, config: serde_json::Value) -> Self {
68 self.tailwind_config = config; self
69 }
70
71 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 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 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
223pub fn build_tailwind() -> Result<(), Error> { BuildConfig::default().build() }
225