tailwindcss_native_rust_macro/
lib.rs

1use 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
8/// Keywords used internally by the application.
9mod 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/// The `include_tailwind!` macro expects to be passed a set of arguments that
145/// will govern how it behaves. The expected format is as follows:
146///
147/// ```rust
148/// include_tailwind! {
149///     config: "path/to/tailwind.config.js",
150///     input: "path/to/tailwind.input.js",
151///     tailwindcss_bin: "/path/to/tailwindcss/bin/tailwindcss"
152/// }
153/// ```
154///
155/// If a relative path is given, it will be taken relative to the
156/// `CARGO_MANIFEST_DIR`.
157///
158/// The macro will then compile to an inline string representing the output from
159/// tailwindcss. This can then be embeded in and returned by (with necessary CSS
160/// headers) your web server framework of choice.
161///
162/// If any of the arguments are not present, they will be read from a
163/// corresponding environment variable:
164///
165///  - TAILWINDCSS_CONFIG
166///  - TAILWINDCSS_INPUT
167///  - TAILWINDCSS_BIN
168///
169/// If you would like to override the environment variable being read from,
170/// you may do that with the `_env` parameters.
171///
172/// ```rust
173/// include_tailwind! {
174///     config_env: "MY_TAILWINDCSS_CONFIG_ENV_VAR",
175///     input_env: "MY_TAILWINDCSS_INPUT_ENV_VAR",
176///     tailwindcss_bin: "MY_TAILWINDCSS_BIN_ENV_VAR"
177/// }
178/// ```
179#[proc_macro_error]
180#[proc_macro]
181pub fn include_tailwind(item: TokenStream) -> TokenStream {
182    let MacroArgs { args } = parse_macro_input!(item as MacroArgs);
183    // Unwrap is okay as long as we are building from inside CARGO... Who isn't?
184    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}