cargo-leptos 0.1.2

Build tool for Leptos.
use std::sync::Arc;

use super::ChangeSet;
use crate::{
    config::{Project, StyleConfig},
    ext::exe::Exe,
    ext::{
        anyhow::{anyhow, bail, Context, Result},
        PathBufExt,
    },
    fs,
    logger::GRAY,
    service::site::SourcedSiteFile,
    signal::{Outcome, Product},
};
use lightningcss::{
    stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet},
    targets::Browsers,
};
use tokio::process::Command;
use tokio::task::JoinHandle;

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

    tokio::spawn(async move {
        if !changes.need_style_build(true, false) {
            log::debug!("Style no build needed {changes:?}");
            return Ok(Outcome::Success(Product::None));
        }
        Ok(Outcome::Success(build(&proj).await?))
    })
}

async fn build(proj: &Project) -> Result<Product> {
    let Some(style) = &proj.style else {
        log::trace!("Style not configured");
        return Ok(Product::None);
    };
    fs::create_dir_all(style.file.dest.clone().without_last())
        .await
        .dot()?;

    log::debug!("Style found: {}", &style.file);

    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") => {
            fs::copy(&style.file.source, &style.file.dest).await.dot()?;
        }
        _ => bail!("Not a css/sass/scss style file: {}", &style.file),
    };

    process_css(proj, &style)
        .await
        .context(format!("process css {}", &style.file))
}

async fn compile_sass(style_file: &SourcedSiteFile, optimise: bool) -> Result<()> {
    let mut args = vec![style_file.source.as_str(), style_file.dest.as_str()];
    optimise.then(|| args.push("--no-source-map"));

    let exe = Exe::Sass.get().await.dot()?;

    let mut cmd = Command::new(exe).args(&args).spawn()?;

    log::trace!(
        "Style running {}",
        GRAY.paint(format!("sass {}", args.join(" ")))
    );

    cmd.wait()
        .await
        .context(format!("sass {}", args.join(" ")))?;

    log::trace!(
        "Style compiled sass {}",
        GRAY.paint(style_file.dest.to_string())
    );
    Ok(())
}

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, style: &StyleConfig) -> Result<Product> {
    let browsers = browser_lists(&style.browserquery).context("leptos.style.browserquery")?;

    let css = fs::read_to_string(&style.file.dest).await?;

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

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

    let options = PrinterOptions::<'_> {
        targets: 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(&style.file.as_site_file(), bytes)
        .await?
    {
        true => {
            log::trace!(
                "Style finished with changes {}",
                GRAY.paint(&style.file.to_string())
            );
            Product::Style
        }
        false => {
            log::trace!("Style finished without changes");
            Product::None
        }
    };
    Ok(prod)
}