1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use super::ChangeSet;
use crate::{
    compile::{sass::compile_sass, tailwind::compile_tailwind},
    config::Project,
    ext::{
        anyhow::{anyhow, bail, Context, Result},
        PathBufExt,
    },
    fs,
    logger::GRAY,
    signal::{Outcome, Product},
};
use lightningcss::{
    stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet},
    targets::Browsers,
    targets::Targets,
};
use std::sync::Arc;
use tokio::task::JoinHandle;

pub async fn style(proj: &Arc<Project>, changes: &ChangeSet) -> JoinHandle<Result<Outcome<Product>>> {
    let changes = changes.clone();
    let proj = proj.clone();

    tokio::spawn(async move {
        let css_in_source = proj.style.tailwind.is_some();
        if !changes.need_style_build(true, css_in_source) {
            log::debug!("Style no build needed {changes:?}");
            return Ok(Outcome::Success(Product::None));
        }
        build(&proj).await
    })
}
fn build_sass(proj: &Arc<Project>) -> JoinHandle<Result<Outcome<String>>> {
    let proj = proj.clone();
    tokio::spawn(async move {
        let Some(style_file) = &proj.style.file else {
            log::trace!("Style not configured");
            return Ok(Outcome::Success("".to_string()));
        };

        log::trace!("Style found: {}", &style_file);
        fs::create_dir_all(style_file.dest.clone().without_last()).await.dot()?;
        match style_file.source.extension() {
            Some("sass") | Some("scss") => compile_sass(style_file, proj.release)
                .await
                .context(format!("compile sass/scss: {}", &style_file)),
            Some("css") => Ok(Outcome::Success(fs::read_to_string(&style_file.source).await.dot()?)),
            _ => bail!("Not a css/sass/scss style file: {}", &style_file),
        }
    })
}

fn build_tailwind(proj: &Arc<Project>) -> JoinHandle<Result<Outcome<String>>> {
    let proj = proj.clone();
    tokio::spawn(async move {
        let Some(tw_conf) = proj.style.tailwind.as_ref() else {
            log::trace!("Tailwind not configured");
            return Ok(Outcome::Success("".to_string()));
        };
        log::trace!("Tailwind config: {:?}", &tw_conf);
        compile_tailwind(&proj, tw_conf).await
    })
}

async fn build(proj: &Arc<Project>) -> Result<Outcome<Product>> {
    let css_handle = build_sass(proj);
    let tw_handle = build_tailwind(proj);
    let css = css_handle.await??;
    let tw = tw_handle.await??;

    use Outcome::*;
    let css = match (css, tw) {
        (Stopped, _) | (_, Stopped) => return Ok(Stopped),
        (Failed, _) | (_, Failed) => return Ok(Failed),
        (Success(css), Success(tw)) => format!("{css}\n{tw}"),
    };
    Ok(Success(process_css(proj, css).await?))
}

fn browser_lists(query: &str) -> Result<Option<Browsers>> {
    Browsers::from_browserslist([query]).context(format!("Error in browserlist query: {query}"))
}

async fn process_css(proj: &Project, css: String) -> Result<Product> {
    let browsers = browser_lists(&proj.style.browser_query).context("glory.style.browser_query")?;

    let mut stylesheet = StyleSheet::parse(&css, ParserOptions::default()).map_err(|e| anyhow!("{e}"))?;

    if proj.release {
        stylesheet.minify(MinifyOptions::default())?;
    }

    let options = PrinterOptions::<'_> {
        targets: Targets::from(browsers),
        minify: proj.release,
        ..Default::default()
    };

    let style_output = stylesheet.to_css(options)?;

    let bytes = style_output.code.as_bytes();

    let prod = match proj.site.updated_with(&proj.style.site_file, bytes).await? {
        true => {
            log::trace!("Style finished with changes {}", GRAY.paint(&proj.style.site_file.to_string()));
            Product::Style("".to_string()) //TODO
        }
        false => {
            log::trace!("Style finished without changes");
            Product::None
        }
    };
    Ok(prod)
}