tailwindcss_native_rust_macro/
lib.rs1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use proc_macro_error::{abort, abort_call_site, proc_macro_error};
4use quote::quote;
5use std::path::PathBuf;
6use syn::{parse::Parse, parse_macro_input, punctuated::Punctuated, LitStr, Token};
7
8mod kw {
10 syn::custom_keyword!(config);
11 syn::custom_keyword!(config_env);
12 syn::custom_keyword!(input);
13 syn::custom_keyword!(input_env);
14 syn::custom_keyword!(tailwindcss_bin);
15 syn::custom_keyword!(tailwindcss_bin_env);
16}
17
18enum Argument {
19 Config {
20 _kw_token: kw::config,
21 _colon_token: Token![:],
22 value: LitStr,
23 },
24 ConfigEnv {
25 _kw_token: kw::config_env,
26 _colon_token: Token![:],
27 value: LitStr,
28 },
29 Input {
30 _kw_token: kw::input,
31 _colon_token: Token![:],
32 value: LitStr,
33 },
34 InputEnv {
35 _kw_token: kw::input_env,
36 _colon_token: Token![:],
37 value: LitStr,
38 },
39 TailwindCssBin {
40 _kw_token: kw::tailwindcss_bin,
41 _colon_token: Token![:],
42 value: LitStr,
43 },
44 TailwindCssBinEnv {
45 _kw_token: kw::tailwindcss_bin_env,
46 _colon_token: Token![:],
47 value: LitStr,
48 },
49}
50impl Parse for Argument {
51 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
52 let lookahead1 = input.lookahead1();
53 if lookahead1.peek(kw::config) {
54 Ok(Argument::Config {
55 _kw_token: input.parse()?,
56 _colon_token: input.parse()?,
57 value: input.parse()?,
58 })
59 } else if lookahead1.peek(kw::config_env) {
60 Ok(Argument::ConfigEnv {
61 _kw_token: input.parse()?,
62 _colon_token: input.parse()?,
63 value: input.parse()?,
64 })
65 } else if lookahead1.peek(kw::input) {
66 Ok(Argument::Input {
67 _kw_token: input.parse()?,
68 _colon_token: input.parse()?,
69 value: input.parse()?,
70 })
71 } else if lookahead1.peek(kw::input_env) {
72 Ok(Argument::InputEnv {
73 _kw_token: input.parse()?,
74 _colon_token: input.parse()?,
75 value: input.parse()?,
76 })
77 } else if lookahead1.peek(kw::tailwindcss_bin) {
78 Ok(Argument::TailwindCssBin {
79 _kw_token: input.parse()?,
80 _colon_token: input.parse()?,
81 value: input.parse()?,
82 })
83 } else if lookahead1.peek(kw::tailwindcss_bin_env) {
84 Ok(Argument::TailwindCssBinEnv {
85 _kw_token: input.parse()?,
86 _colon_token: input.parse()?,
87 value: input.parse()?,
88 })
89 } else {
90 Err(lookahead1.error())
91 }
92 }
93}
94impl Argument {
95 fn as_config(&self) -> Option<&LitStr> {
96 match self {
97 Argument::Config { value, .. } => Some(value),
98 _ => None,
99 }
100 }
101 fn as_config_env(&self) -> Option<&LitStr> {
102 match self {
103 Argument::ConfigEnv { value, .. } => Some(value),
104 _ => None,
105 }
106 }
107 fn as_input(&self) -> Option<&LitStr> {
108 match self {
109 Argument::Input { value, .. } => Some(value),
110 _ => None,
111 }
112 }
113 fn as_input_env(&self) -> Option<&LitStr> {
114 match self {
115 Argument::InputEnv { value, .. } => Some(value),
116 _ => None,
117 }
118 }
119 fn as_tailwindcss_bin(&self) -> Option<&LitStr> {
120 match self {
121 Argument::TailwindCssBin { value, .. } => Some(value),
122 _ => None,
123 }
124 }
125 fn as_tailwindcss_bin_env(&self) -> Option<&LitStr> {
126 match self {
127 Argument::TailwindCssBinEnv { value, .. } => Some(value),
128 _ => None,
129 }
130 }
131}
132
133struct MacroArgs {
134 args: Punctuated<Argument, Token![,]>,
135}
136impl Parse for MacroArgs {
137 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
138 Ok(MacroArgs {
139 args: Punctuated::parse_terminated(input)?,
140 })
141 }
142}
143
144#[proc_macro_error]
180#[proc_macro]
181pub fn include_tailwind(item: TokenStream) -> TokenStream {
182 let MacroArgs { args } = parse_macro_input!(item as MacroArgs);
183 let manifest_path = std::env::var("CARGO_MANIFEST_DIR")
185 .unwrap()
186 .parse::<PathBuf>()
187 .unwrap();
188 let args = args.iter().collect::<Vec<_>>();
189 let config = args.iter().copied().filter_map(Argument::as_config).next();
190 let config_env = args
191 .iter()
192 .copied()
193 .filter_map(Argument::as_config_env)
194 .next();
195 let input = args.iter().copied().filter_map(Argument::as_input).next();
196 let input_env = args
197 .iter()
198 .copied()
199 .filter_map(Argument::as_input_env)
200 .next();
201 let tailwindcss_bin = args
202 .iter()
203 .copied()
204 .filter_map(Argument::as_tailwindcss_bin)
205 .next();
206 let tailwindcss_bin_env = args
207 .iter()
208 .copied()
209 .filter_map(Argument::as_tailwindcss_bin_env)
210 .next();
211
212 let config_env = config_env.map(LitStr::value);
213 let config_env = config_env.as_deref().unwrap_or("TAILWINDCSS_CONFIG");
214 let input_env = input_env.map(LitStr::value);
215 let input_env = input_env.as_deref().unwrap_or("TAILWINDCSS_INPUT");
216 let tailwindcss_bin_env = tailwindcss_bin_env.map(LitStr::value);
217 let tailwindcss_bin_env = tailwindcss_bin_env.as_deref().unwrap_or("TAILWINDCSS_BIN");
218
219 let config = match config {
220 Some(config) => config.value(),
221 None => match std::env::var(config_env) {
222 Ok(config) => config,
223 Err(e) => abort_call_site!(format!(
224 "Required `config` arg or `TAILWINDCSS_CONFIG` env var: {}",
225 e
226 )),
227 },
228 };
229
230 let input = match input {
231 Some(input) => input.value(),
232 None => match std::env::var(input_env) {
233 Ok(input) => input,
234 Err(e) => abort_call_site!(format!(
235 "Required `input` arg or `TAILWINDCSS_INPUT` env var: {}",
236 e
237 )),
238 },
239 };
240
241 let tailwindcss_bin = match tailwindcss_bin {
242 Some(tailwindcss_bin) => tailwindcss_bin.value(),
243 None => match std::env::var(tailwindcss_bin_env) {
244 Ok(bin) => bin,
245 Err(e) => abort_call_site!(format!(
246 "Required `tailwindcss_bin` arg or `TAILWINDCSS_BIN` env var: {}",
247 e
248 )),
249 },
250 };
251
252 let mut config_path = match config.parse::<PathBuf>() {
253 Ok(path) => path,
254 Err(e) => abort_call_site!(format!("Provided config is not a path: {}", e)),
255 };
256 if config_path.is_relative() {
257 config_path = manifest_path.join(config_path);
258 }
259 if !config_path.exists() {
260 abort!(
261 config,
262 format!(
263 "Config path does not exist: {}",
264 config_path.to_string_lossy()
265 )
266 )
267 }
268
269 let mut input_path = match input.parse::<PathBuf>() {
270 Ok(path) => path,
271 Err(e) => abort_call_site!(format!("Provided input is not a path: {}", e)),
272 };
273 if input_path.is_relative() {
274 input_path = manifest_path.join(input_path);
275 }
276 if !input_path.exists() {
277 abort!(
278 input,
279 format!(
280 "Input path does not exist: {}",
281 input_path.to_string_lossy()
282 )
283 )
284 }
285
286 let mut tailwindcss_bin_path = match tailwindcss_bin.parse::<PathBuf>() {
287 Ok(path) => path,
288 Err(e) => abort!(
289 tailwindcss_bin,
290 format!("Provided tailwindcss_bin is not a path: {}", e)
291 ),
292 };
293 if tailwindcss_bin_path.is_relative() {
294 tailwindcss_bin_path = manifest_path.join(tailwindcss_bin_path);
295 }
296 if !tailwindcss_bin_path.exists() {
297 abort!(
298 tailwindcss_bin,
299 format!(
300 "The tailwindcss_bin path does not exist: {}",
301 tailwindcss_bin_path.to_string_lossy()
302 )
303 )
304 }
305
306 let tw_proc_output = std::process::Command::new(tailwindcss_bin_path)
307 .arg("-c")
308 .arg(config_path)
309 .arg("-i")
310 .arg(input_path)
311 .arg("--minify")
312 .output();
313
314 let tw_proc_output = match tw_proc_output {
315 Ok(tw_proc) => tw_proc,
316 Err(e) => abort_call_site!(format!("Tailwind proc did not run correctly: {e}")),
317 };
318
319 if !tw_proc_output.status.success() {
320 abort_call_site!(format!("Tailwind proc did not run correctly."))
321 }
322
323 let tw_content_str = match std::str::from_utf8(&tw_proc_output.stdout) {
324 Ok(content) => content,
325 Err(e) => abort_call_site!(format!("Generated file is not utf8: {}", e)),
326 };
327
328 let tw_content_lit = LitStr::new(tw_content_str, Span::call_site());
329
330 quote! {
331 #tw_content_lit
332 }
333 .into()
334}