tovuk 0.1.58

Deploy Rust backends, static frontends, and fullstack apps to Tovuk.
use super::{
    constants::{FRONTEND_JAVASCRIPT_EXTENSIONS, FRONTEND_SOURCE_ROOTS},
    project::walk_project_files,
};
use std::path::Path;

#[derive(Default)]
pub(crate) struct FrontendSourceReport {
    pub(crate) typescript: Vec<String>,
    pub(crate) javascript: Vec<String>,
    pub(crate) server_routes: Vec<String>,
}

pub(crate) fn frontend_source_report(project_dir: &Path) -> FrontendSourceReport {
    let mut report = FrontendSourceReport::default();
    walk_project_files(project_dir, |_file, relative| {
        if is_frontend_server_route(relative) {
            report.server_routes.push(relative.to_owned());
        }
        if !is_frontend_source_path(relative) {
            return;
        }
        if is_frontend_typescript_source(relative) {
            report.typescript.push(relative.to_owned());
        } else if is_frontend_javascript_source(relative) {
            report.javascript.push(relative.to_owned());
        }
    });
    report
}

pub(crate) fn backend_javascript_or_typescript_sources(
    project_dir: &Path,
    label: &str,
) -> Vec<String> {
    let mut matches = Vec::new();
    walk_project_files(project_dir, |_file, relative| {
        if is_backend_javascript_or_typescript_source(relative) {
            matches.push(if label.is_empty() {
                relative.to_owned()
            } else {
                format!("{label}/{relative}")
            });
        }
    });
    matches
}

fn is_frontend_source_path(relative: &str) -> bool {
    relative
        .split('/')
        .next()
        .is_some_and(|root| FRONTEND_SOURCE_ROOTS.contains(&root))
}

fn is_frontend_typescript_source(relative: &str) -> bool {
    !relative.ends_with(".d.ts")
        && Path::new(relative)
            .extension()
            .is_some_and(|extension| matches_ignore_ascii_case(extension, &["ts", "tsx"]))
}

fn matches_ignore_ascii_case(value: &std::ffi::OsStr, candidates: &[&str]) -> bool {
    candidates
        .iter()
        .any(|candidate| value.eq_ignore_ascii_case(candidate))
}

fn is_frontend_javascript_source(relative: &str) -> bool {
    FRONTEND_JAVASCRIPT_EXTENSIONS
        .iter()
        .any(|extension| relative.ends_with(extension))
}

fn is_frontend_server_route(relative: &str) -> bool {
    if !is_frontend_typescript_source(relative) && !is_frontend_javascript_source(relative) {
        return false;
    }
    let parts = relative
        .to_ascii_lowercase()
        .split('/')
        .map(str::to_owned)
        .collect::<Vec<_>>();
    let file = parts.last().map_or("", String::as_str);
    file.starts_with("+server.")
        || file.starts_with("middleware.")
        || path_starts_with(&parts, &["pages", "api"])
        || path_starts_with(&parts, &["src", "pages", "api"])
        || (file.starts_with("route.")
            && (path_starts_with(&parts, &["app", "api"])
                || path_starts_with(&parts, &["src", "app", "api"])))
}

fn path_starts_with(path_parts: &[String], prefix: &[&str]) -> bool {
    path_parts.len() >= prefix.len()
        && prefix
            .iter()
            .enumerate()
            .all(|(index, part)| path_parts[index] == *part)
}

fn is_backend_javascript_or_typescript_source(relative: &str) -> bool {
    if relative.ends_with(".d.ts") || !is_javascript_or_typescript_path(relative) {
        return false;
    }
    relative
        .split('/')
        .any(|component| ["api", "app", "pages", "routes", "server", "src"].contains(&component))
}

fn is_javascript_or_typescript_path(relative: &str) -> bool {
    [".cjs", ".js", ".jsx", ".mjs", ".ts", ".tsx"]
        .iter()
        .any(|extension| relative.ends_with(extension))
}