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}