Skip to main content

xtask_wasm/
sass.rs

1use crate::anyhow::{Context, Result};
2use crate::dist::Transformer;
3use std::{fs, path::Path};
4
5/// A [`Transformer`] that compiles SASS/SCSS files to CSS.
6///
7/// Files whose names begin with `_` are treated as partials and skipped (not emitted
8/// to the dist directory). All other `.sass` and `.scss` files are compiled to `.css`.
9/// Non-SASS files are not claimed and fall through to the default plain-copy behaviour.
10///
11/// Add this transformer to [`Dist`] to enable SASS/SCSS compilation:
12///
13/// ```rust,no_run
14/// use xtask_wasm::{anyhow::Result, clap, SassTransformer};
15///
16/// #[derive(clap::Parser)]
17/// enum Opt {
18///     Dist(xtask_wasm::Dist),
19/// }
20///
21/// fn main() -> Result<()> {
22///     let opt: Opt = clap::Parser::parse();
23///
24///     match opt {
25///         Opt::Dist(dist) => {
26///             dist.transformer(SassTransformer::default())
27///                 .build("my-project")?;
28///         }
29///     }
30///
31///     Ok(())
32/// }
33/// ```
34///
35/// To customise the compilation options, construct `SassTransformer` directly:
36///
37/// ```rust,no_run
38/// use xtask_wasm::{anyhow::Result, clap, SassTransformer};
39///
40/// #[derive(clap::Parser)]
41/// enum Opt {
42///     Dist(xtask_wasm::Dist),
43/// }
44///
45/// fn main() -> Result<()> {
46///     let opt: Opt = clap::Parser::parse();
47///
48///     match opt {
49///         Opt::Dist(dist) => {
50///             dist.transformer(SassTransformer {
51///                     options: sass_rs::Options {
52///                         output_style: sass_rs::OutputStyle::Compressed,
53///                         ..Default::default()
54///                     },
55///                 })
56///                 .build("my-project")?;
57///         }
58///     }
59///
60///     Ok(())
61/// }
62/// ```
63///
64/// [`Dist`]: crate::Dist
65#[derive(Debug, Default)]
66pub struct SassTransformer {
67    /// Options forwarded to [`sass_rs::compile_file`].
68    pub options: sass_rs::Options,
69}
70
71impl Transformer for SassTransformer {
72    fn transform(&self, source: &Path, dest: &Path) -> Result<bool> {
73        fn is_sass(path: &Path) -> bool {
74            matches!(
75                path.extension()
76                    .and_then(|x| x.to_str().map(|x| x.to_lowercase()))
77                    .as_deref(),
78                Some("sass") | Some("scss")
79            )
80        }
81
82        fn is_partial(path: &Path) -> bool {
83            path.file_name()
84                .expect("WalkDir does not yield paths ending with `..` or `.`")
85                .to_str()
86                .map(|x| x.starts_with('_'))
87                .unwrap_or(false)
88        }
89
90        if !is_sass(source) {
91            return Ok(false);
92        }
93
94        // Partials are silently skipped — claiming the file prevents the plain-copy
95        // fallback from copying the raw .scss into dist.
96        if is_partial(source) {
97            return Ok(true);
98        }
99
100        let dest = dest.with_extension("css");
101        let css = sass_rs::compile_file(source, self.options.clone()).map_err(|e| {
102            crate::anyhow::anyhow!("could not compile SASS file `{}`: {}", source.display(), e)
103        })?;
104        fs::write(&dest, css)
105            .with_context(|| format!("could not write CSS to `{}`", dest.display()))?;
106
107        Ok(true)
108    }
109}