hauchiwa 0.9.1

Flexible static website generator library with incremental rebuilds and cached image optimization
Documentation
use std::process::{Command, Stdio};

use camino::{Utf8Path, Utf8PathBuf};
use thiserror::Error;

use crate::{Blueprint, error::HauchiwaError, graph::Handle, loader::GlobAssetsTask};

/// Errors that can occur when compiling JavaScript files.
#[derive(Debug, Error)]
pub enum ScriptError {
    /// An I/O error occurred during process execution.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// The Esbuild process returned a non-zero exit code.
    #[error("Esbuild execution failed: {0}")]
    Esbuild(String),

    /// Failed to parse Esbuild output as UTF-8.
    #[error("UTF-8 conversion error: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),

    /// An internal build error (e.g., failed to store the artifact).
    #[error("Build error: {0}")]
    Build(#[from] crate::error::BuildError),
}

/// Represents a compiled JavaScript module.
#[derive(Clone)]
pub struct Script {
    /// The path to the compiled JavaScript file (e.g., hashed path).
    pub path: Utf8PathBuf,
}

impl<G> Blueprint<G>
where
    G: Send + Sync + 'static,
{
    /// Compiles JavaScript files using Esbuild.
    ///
    /// This loader finds files matching `glob_entry`, bundles and minifies them
    /// using the `esbuild` command-line tool, and stores the resulting artifacts.
    ///
    /// **Note:** This loader requires the `esbuild` binary to be available in the system PATH.
    ///
    /// # Arguments
    ///
    /// * `glob_entry`: Glob pattern for the entry points (e.g., "src/main.ts").
    /// * `glob_watch`: Glob pattern for files to watch for changes (often broader, e.g., "src/**/*.ts").
    ///
    /// # Returns
    ///
    /// A [`Handle`] to a [`crate::loader::Assets<Script>`], mapping original file paths to [`Script`] objects.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # let mut config = hauchiwa::Blueprint::<()>::new();
    /// // Compile main.ts using esbuild, watching all ts files in the scripts directory.
    /// let scripts = config.load_js("scripts/main.ts", "scripts/**/*.ts");
    /// ```
    pub fn load_js(
        &mut self,
        glob_entry: &'static str,
        glob_watch: &'static str,
    ) -> Result<Handle<super::Assets<Script>>, HauchiwaError> {
        Ok(self.add_task_opaque(GlobAssetsTask::new(
            vec![glob_entry],
            vec![glob_watch],
            move |_, store, input| {
                let data = compile_esbuild(&input.path)?;
                let path = store.save(&data, "js").map_err(ScriptError::Build)?;

                Ok((input.path, Script { path }))
            },
        )?))
    }
}

fn compile_esbuild(file: &Utf8Path) -> Result<Vec<u8>, ScriptError> {
    let output = Command::new("esbuild")
        .arg(file.as_str())
        .arg("--format=esm")
        .arg("--bundle")
        .arg("--minify")
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit())
        .output()?;

    if !output.status.success() {
        return Err(ScriptError::Esbuild(String::from_utf8(output.stdout)?));
    }

    Ok(output.stdout)
}