esbuild_client 0.7.1

A Rust implementation of a client for communicating with esbuild's service API over stdio
Documentation
use std::path::{Path, PathBuf};
use std::sync::Once;
use std::{fs, io};
use sys_traits::{FsFileLock, OpenOptions};

use directories::ProjectDirs;
use esbuild_client::{EsbuildService, EsbuildServiceOptions};
use sys_traits::FsOpen;
use sys_traits::impls::RealSys;

fn base_dir() -> PathBuf {
    let project_dirs = ProjectDirs::from(
        "esbuild_client_test",
        "esbuild_client_test",
        "esbuild_client_test",
    )
    .unwrap();
    project_dirs.cache_dir().to_path_buf()
}

fn npm_package_name() -> String {
    let platform = match (std::env::consts::ARCH, std::env::consts::OS) {
        ("x86_64", "linux") => "linux-x64".to_string(),
        ("aarch64", "linux") => "linux-arm64".to_string(),
        ("x86_64", "macos" | "apple") => "darwin-x64".to_string(),
        ("aarch64", "macos" | "apple") => "darwin-arm64".to_string(),
        ("x86_64", "windows") => "win32-x64".to_string(),
        ("aarch64", "windows") => "win32-arm64".to_string(),
        _ => panic!(
            "Unsupported platform: {} {}",
            std::env::consts::ARCH,
            std::env::consts::OS
        ),
    };

    format!("@esbuild/{}", platform)
}

pub const ESBUILD_VERSION: &str = "0.25.5";

fn npm_package_url() -> String {
    let package_name = npm_package_name();
    let Some((_, platform)) = package_name.split_once('/') else {
        panic!("Invalid package name: {}", package_name);
    };

    format!(
        "https://registry.npmjs.org/{}/-/{}-{}.tgz",
        package_name, platform, ESBUILD_VERSION
    )
}

struct EsbuildFileLock {
    file: sys_traits::impls::RealFsFile,
}

impl Drop for EsbuildFileLock {
    fn drop(&mut self) {
        let _ = self.file.fs_file_unlock();
    }
}

impl EsbuildFileLock {
    fn new(access_path: &Path) -> Self {
        let path = access_path.parent().unwrap().join(".esbuild.lock");
        let mut options = OpenOptions::new_write();
        options.create = true;
        options.read = true;
        let mut file = RealSys.fs_open(&path, &options).unwrap();
        file.fs_file_lock(sys_traits::FsFileLockMode::Exclusive)
            .unwrap();
        Self { file }
    }
}

pub fn fetch_esbuild() -> PathBuf {
    static ONCE: Once = Once::new();
    ONCE.call_once(|| {
        pretty_env_logger::init();
    });

    let esbuild_bin_dir = base_dir().join("bin");
    eprintln!("esbuild_bin_dir: {:?}", esbuild_bin_dir);

    let esbuild_bin_path = esbuild_bin_dir.join("esbuild");
    eprintln!("esbuild_bin_path: {:?}", esbuild_bin_path);
    if esbuild_bin_path.exists() {
        eprintln!("esbuild_bin_path exists");
        return esbuild_bin_path;
    }

    if !esbuild_bin_dir.exists() {
        std::fs::create_dir_all(&esbuild_bin_dir).unwrap();
    }
    let _lock = EsbuildFileLock::new(&esbuild_bin_path);
    if esbuild_bin_path.exists() {
        eprintln!("esbuild_bin_path exists");
        return esbuild_bin_path;
    }

    let esbuild_bin_url = npm_package_url();
    eprintln!("fetching esbuild from: {}", esbuild_bin_url);
    let response = ureq::get(esbuild_bin_url).call().unwrap();

    let reader = response.into_body().into_reader();
    let decoder = flate2::read::GzDecoder::new(reader);
    let mut archive = tar::Archive::new(decoder);

    let want_path = if cfg!(target_os = "windows") {
        "package/esbuild.exe"
    } else {
        "package/bin/esbuild"
    };

    for entry in archive.entries().unwrap() {
        let mut entry = entry.unwrap();
        let path = entry.path().unwrap();

        eprintln!("on entry: {:?}", path);
        if path == std::path::Path::new(want_path) {
            eprintln!("extracting esbuild to: {}", esbuild_bin_path.display());
            std::io::copy(
                &mut entry,
                &mut std::fs::File::create(&esbuild_bin_path).unwrap(),
            )
            .unwrap();

            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                std::fs::set_permissions(&esbuild_bin_path, std::fs::Permissions::from_mode(0o755))
                    .unwrap();
            }
            eprintln!("esbuild_bin_path created");

            break;
        }
    }

    esbuild_bin_path
}

pub struct TestDir {
    pub path: PathBuf,
}

impl TestDir {
    pub fn new(name: &str) -> io::Result<Self> {
        let path = std::env::temp_dir().join(name);
        fs::create_dir_all(&path)?;
        Ok(TestDir { path })
    }

    pub fn create_file(&self, name: &str, content: &str) -> io::Result<PathBuf> {
        let file_path = self.path.join(name);
        fs::write(&file_path, content)?;
        Ok(file_path)
    }
}

impl Drop for TestDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}

#[allow(dead_code)]
pub async fn create_esbuild_service(
    test_dir: &TestDir,
) -> Result<EsbuildService, Box<dyn std::error::Error>> {
    create_esbuild_service_with_plugin(test_dir, None).await
}

#[allow(dead_code)]
pub async fn create_esbuild_service_with_plugin(
    test_dir: &TestDir,
    plugin_handler: impl esbuild_client::MakePluginHandler,
) -> Result<EsbuildService, Box<dyn std::error::Error>> {
    let esbuild_path = if std::env::var("ESBUILD_PATH").is_ok() {
        PathBuf::from(std::env::var("ESBUILD_PATH").unwrap())
    } else {
        fetch_esbuild()
    };
    eprintln!("fetched esbuild: {:?}", esbuild_path);
    Ok(EsbuildService::new(
        esbuild_path,
        ESBUILD_VERSION,
        plugin_handler,
        EsbuildServiceOptions {
            cwd: Some(&test_dir.path),
        },
    )
    .await?)
}