Skip to main content

jhol_core/
run.rs

1//! Native script runner: run package.json scripts without npm or Bun.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6/// Read script command from package.json. Returns the script string or error.
7pub fn get_script_command(script_name: &str, package_json_path: &Path) -> Result<String, String> {
8    let s = std::fs::read_to_string(package_json_path)
9        .map_err(|e| format!("Could not read package.json: {}", e))?;
10    let v: serde_json::Value =
11        serde_json::from_str(&s).map_err(|e| format!("Invalid package.json: {}", e))?;
12    let scripts = v
13        .get("scripts")
14        .and_then(|s| s.as_object())
15        .ok_or_else(|| "package.json has no \"scripts\" object.".to_string())?;
16    let cmd = scripts
17        .get(script_name)
18        .and_then(|c| c.as_str())
19        .map(String::from)
20        .ok_or_else(|| format!("Missing script \"{}\" in package.json.", script_name))?;
21    if cmd.trim().is_empty() {
22        return Err(format!("Script \"{}\" is empty.", script_name));
23    }
24    Ok(cmd)
25}
26
27/// Run a package.json script: set PATH to include node_modules/.bin, optional npm_* env, then shell.
28pub fn run_script(
29    script_name: &str,
30    cwd: &Path,
31    extra_env: Option<HashMap<String, String>>,
32) -> Result<std::process::ExitStatus, String> {
33    let package_json_path = cwd.join("package.json");
34    if !package_json_path.exists() {
35        return Err("No package.json found in current directory.".to_string());
36    }
37    let script_cmd = get_script_command(script_name, &package_json_path)?;
38
39    let bin_dir = cwd.join("node_modules").join(".bin");
40    let path_env = std::env::var("PATH").unwrap_or_default();
41    let new_path = if bin_dir.exists() {
42        let bin_str = bin_dir.to_string_lossy();
43        #[cfg(unix)]
44        let sep = ":";
45        #[cfg(windows)]
46        let sep = ";";
47        format!("{}{}{}", bin_str, sep, path_env)
48    } else {
49        path_env
50    };
51
52    let mut env: HashMap<String, String> = extra_env.unwrap_or_default();
53    env.insert("PATH".to_string(), new_path);
54
55    if let Ok(s) = std::fs::read_to_string(&package_json_path) {
56        if let Ok(pkg_json) = serde_json::from_str::<serde_json::Value>(&s) {
57        if let Some(name) = pkg_json.get("name").and_then(|n| n.as_str()) {
58            env.insert("npm_package_name".to_string(), name.to_string());
59        }
60        if let Some(ver) = pkg_json.get("version").and_then(|v| v.as_str()) {
61            env.insert("npm_package_version".to_string(), ver.to_string());
62        }
63        }
64    }
65
66    #[cfg(unix)]
67    let (shell, shell_arg, script_arg) = {
68        let sh = std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string());
69        (sh, "-c", script_cmd)
70    };
71
72    #[cfg(windows)]
73    let (shell, shell_arg, script_arg) = {
74        ("cmd".to_string(), "/c", script_cmd)
75    };
76
77    let status = std::process::Command::new(&shell)
78        .arg(shell_arg)
79        .arg(script_arg)
80        .current_dir(cwd)
81        .envs(env)
82        .status()
83        .map_err(|e| format!("Failed to run script: {}", e))?;
84
85    Ok(status)
86}