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 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
25impl BuildConfig {
28 pub fn new() -> Self {
30 Self {
31 css_path: None, cdn_src: format!("https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"),
33 always: false,
34 }
35 }
36
37 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 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 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 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
205pub fn build_tailwind() -> Result<(), Error> { BuildConfig::default().build() }
207