tovuk 0.1.68

Deploy Rust workers, static frontends, and worker-static apps to Tovuk.
use super::{
    layout::frontend_package_manager,
    predicates::{
        referenced_package_scripts, uses_javascript_linter, uses_native_dead_code_checker,
        uses_native_duplicate_checker, uses_native_frontend_linter, uses_native_health_checker,
        uses_strict_frontend_typechecker,
    },
};
use crate::cli::{
    doctor::{DoctorCheck, doctor_check, first_output_line},
    project::read_package_json,
};
use serde_json::Value;
use std::{
    collections::BTreeSet,
    path::Path,
    process::{Command, Stdio},
};

pub(super) fn frontend_script_checks(project_dir: &Path, run_scripts: bool) -> Vec<DoctorCheck> {
    let manifest = read_package_json(project_dir);
    let typecheck = package_script_value(manifest.as_ref(), "typecheck");
    let lint = package_script_value(manifest.as_ref(), "lint");
    let mut checks = vec![
        package_script_exists_check("typecheck", &typecheck),
        package_script_exists_check("lint", &lint),
        strict_typecheck_check(&typecheck),
        native_lint_check(manifest.as_ref()),
        native_quality_gate_check(manifest.as_ref()),
    ];
    if run_scripts && checks.iter().all(|check| check.ok) {
        checks.push(package_script_check(project_dir, "typecheck"));
        checks.push(package_script_check(project_dir, "lint"));
    }
    checks
}

fn package_script_exists_check(script: &str, command: &str) -> DoctorCheck {
    doctor_check(
        &format!("package script {script}"),
        !command.is_empty(),
        "found",
        "missing",
        &format!("Add a non-empty \"{script}\" script to package.json, then retry."),
    )
}

fn strict_typecheck_check(command: &str) -> DoctorCheck {
    doctor_check(
        "strict frontend typecheck",
        uses_strict_frontend_typechecker(command),
        "accepted",
        "native typecheck missing",
        "Set package.json `typecheck` to `oxlint src vite.config.ts --deny-warnings --type-aware --type-check --tsconfig tsconfig.json`, then retry.",
    )
}

fn native_lint_check(manifest: Option<&Value>) -> DoctorCheck {
    let ok = !package_script_tree_uses(
        manifest,
        "lint",
        uses_javascript_linter,
        &mut BTreeSet::new(),
    ) && package_script_tree_uses(
        manifest,
        "lint",
        uses_native_frontend_linter,
        &mut BTreeSet::new(),
    );
    doctor_check(
        "native frontend lint",
        ok,
        "accepted",
        "native linter missing",
        "Replace the lint script with native tooling such as `oxlint src vite.config.ts --deny-warnings`, `biome check .`, or `deno lint`, then retry.",
    )
}

fn native_quality_gate_check(manifest: Option<&Value>) -> DoctorCheck {
    let ok = package_script_tree_uses(
        manifest,
        "lint",
        uses_native_dead_code_checker,
        &mut BTreeSet::new(),
    ) && package_script_tree_uses(
        manifest,
        "lint",
        uses_native_duplicate_checker,
        &mut BTreeSet::new(),
    ) && package_script_tree_uses(
        manifest,
        "lint",
        uses_native_health_checker,
        &mut BTreeSet::new(),
    );
    doctor_check(
        "native frontend quality gates",
        ok,
        "accepted",
        "dead-code, duplicate-code, or health gate missing",
        "Add Fallow checks for `dead-code`, semantic `dupes`, and `health` to package.json `lint`, then retry.",
    )
}

fn package_script_value(manifest: Option<&Value>, script: &str) -> String {
    manifest
        .and_then(|manifest| manifest.get("scripts"))
        .and_then(|scripts| scripts.get(script))
        .and_then(Value::as_str)
        .unwrap_or_default()
        .trim()
        .to_owned()
}

fn package_script_tree_uses(
    manifest: Option<&Value>,
    script: &str,
    predicate: fn(&str) -> bool,
    seen: &mut BTreeSet<String>,
) -> bool {
    if !seen.insert(script.to_owned()) {
        return false;
    }
    let command = package_script_value(manifest, script);
    if command.is_empty() {
        return false;
    }
    if predicate(&command) {
        return true;
    }
    referenced_package_scripts(&command)
        .iter()
        .any(|referenced| package_script_tree_uses(manifest, referenced, predicate, seen))
}

fn package_script_check(project_dir: &Path, script: &str) -> DoctorCheck {
    let manager = frontend_package_manager(project_dir);
    let args = if manager == "bun" {
        vec!["run", script]
    } else {
        vec!["run", "--silent", script]
    };
    let result = Command::new(manager)
        .args(args)
        .current_dir(project_dir)
        .stdin(Stdio::null())
        .output();
    let output = match result {
        Ok(output) => output,
        Err(error) => {
            return DoctorCheck {
                name: format!("{manager} run {script}"),
                ok: false,
                message: error.to_string(),
                agent_instruction: Some(format!(
                    "Install {}, then run `{manager} run {script}` before deploying.",
                    if manager == "bun" {
                        "Bun"
                    } else {
                        "Node.js and npm"
                    }
                )),
            };
        }
    };
    DoctorCheck {
        name: format!("{manager} run {script}"),
        ok: output.status.success(),
        message: if output.status.success() {
            "passed".to_owned()
        } else {
            first_output_line(
                &output.stderr,
                &output.stdout,
                &format!("{manager} run {script}"),
            )
        },
        agent_instruction: if output.status.success() {
            None
        } else {
            Some(format!(
                "Run `{manager} run {script}`, fix every error, then redeploy."
            ))
        },
    }
}