use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use anyhow::{Context, Result};
use prek_consts::env_vars::EnvVars;
use prek_consts::prepend_paths;
use tracing::debug;
use crate::cli::reporter::{HookInstallReporter, HookRunReporter};
use crate::hook::InstalledHook;
use crate::hook::{Hook, InstallInfo};
use crate::languages::LanguageImpl;
use crate::languages::node::NodeRequest;
use crate::languages::node::installer::{NodeInstaller, NodeResult, bin_dir, lib_dir};
use crate::languages::node::version::EXTRA_KEY_LTS;
use crate::languages::version::LanguageRequest;
use crate::process::Cmd;
use crate::run::run_by_batch;
use crate::store::{Store, ToolBucket};
#[derive(Debug, Copy, Clone)]
pub(crate) struct Node;
impl LanguageImpl for Node {
async fn install(
&self,
hook: Arc<Hook>,
store: &Store,
reporter: &HookInstallReporter,
) -> Result<InstalledHook> {
let progress = reporter.on_install_start(&hook);
let node_dir = store.tools_path(ToolBucket::Node);
let installer = NodeInstaller::new(node_dir);
let (node_request, allows_download) = match &hook.language_request {
LanguageRequest::Any { system_only } => (&NodeRequest::Any, !system_only),
LanguageRequest::Node(node_request) => (node_request, true),
_ => unreachable!(),
};
let node = installer
.install(store, node_request, allows_download)
.await
.context("Failed to install node")?;
let mut info = InstallInfo::new(
hook.language,
hook.env_key_dependencies().clone(),
&store.hooks_dir(),
)?;
let lts = serde_json::to_string(&node.version().lts).context("Failed to serialize LTS")?;
info.with_toolchain(node.node().to_path_buf());
info.with_language_version(node.version().version.clone());
info.with_extra(EXTRA_KEY_LTS, <s);
let bin_dir = bin_dir(&info.env_path);
let lib_dir = lib_dir(&info.env_path);
fs_err::tokio::create_dir_all(&bin_dir).await?;
fs_err::tokio::create_dir_all(&lib_dir).await?;
let deps = hook.install_dependencies();
if deps.is_empty() {
debug!("No dependencies to install");
} else {
let node_bin = node.node().parent().expect("Node binary must have parent");
let new_path = prepend_paths(&[&bin_dir, node_bin]).context("Failed to join PATH")?;
Cmd::new(node.npm(), "npm install")
.arg("install")
.arg("-g")
.arg("--no-progress")
.arg("--no-save")
.arg("--no-fund")
.arg("--no-audit")
.arg("--install-links")
.args(&*deps)
.env(EnvVars::PATH, new_path)
.env(EnvVars::NPM_CONFIG_PREFIX, &info.env_path)
.env_remove(EnvVars::NPM_CONFIG_USERCONFIG)
.env(EnvVars::NODE_PATH, &lib_dir)
.check(true)
.output()
.await?;
}
info.persist_env_path();
reporter.on_install_complete(progress);
Ok(InstalledHook::Installed {
hook,
info: Arc::new(info),
})
}
async fn check_health(&self, info: &InstallInfo) -> Result<()> {
let node = NodeResult::from_executables(info.toolchain.clone(), PathBuf::new())
.fill_version()
.await
.context("Failed to query node version")?;
if node.version().version != info.language_version {
anyhow::bail!(
"Node version mismatch: expected {}, found {}",
info.language_version,
node.version().version
);
}
Ok(())
}
async fn run(
&self,
hook: &InstalledHook,
filenames: &[&Path],
store: &Store,
reporter: &HookRunReporter,
) -> Result<(i32, Vec<u8>)> {
let progress = reporter.on_run_start(hook, filenames.len());
let env_dir = hook.env_path().expect("Node must have env path");
let node_bin = hook.toolchain_dir().expect("Node binary must have parent");
let new_path =
prepend_paths(&[&bin_dir(env_dir), node_bin]).context("Failed to join PATH")?;
let entry = hook.entry.resolve(Some(&new_path), store)?;
let run = async |batch: &[&Path]| {
let mut output = Cmd::new(&entry[0], "node hook")
.current_dir(hook.work_dir())
.args(&entry[1..])
.env(EnvVars::PATH, &new_path)
.env(EnvVars::NPM_CONFIG_PREFIX, env_dir)
.env_remove(EnvVars::NPM_CONFIG_USERCONFIG)
.env(EnvVars::NODE_PATH, lib_dir(env_dir))
.envs(&hook.env)
.args(&hook.args)
.args(batch)
.check(false)
.stdin(Stdio::null())
.pty_output()
.await?;
reporter.on_run_progress(progress, batch.len() as u64);
output.stdout.extend(output.stderr);
let code = output.status.code().unwrap_or(1);
anyhow::Ok((code, output.stdout))
};
let results = run_by_batch(hook, filenames, entry.argv(), run).await?;
reporter.on_run_complete(progress);
let mut combined_status = 0;
let mut combined_output = Vec::new();
for (code, output) in results {
combined_status |= code;
combined_output.extend(output);
}
Ok((combined_status, combined_output))
}
}