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;
use crate::hook::{Hook, InstallInfo, InstalledHook};
use crate::languages::deno::DenoRequest;
use crate::languages::deno::installer::{DenoInstaller, DenoResult, bin_dir};
use crate::languages::{ExecutionEnvironment, LanguageBackend};
use crate::process::Cmd;
use crate::store::{CacheBucket, Store, ToolBucket};
fn is_valid_install_name(name: &str) -> bool {
let mut chars = name.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphanumeric())
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn parse_install_dependency(spec: &str) -> (&str, Option<&str>) {
let Some((dep, name)) = spec.rsplit_once(':') else {
return (spec, None);
};
let looks_like_path = dep.starts_with('.') || dep.starts_with('/') || dep.contains(['/', '\\']);
if is_valid_install_name(name) && (looks_like_path || dep.contains(':')) {
(dep, Some(name))
} else {
(spec, None)
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct Deno;
#[async_trait::async_trait(?Send)]
impl LanguageBackend for Deno {
async fn install(
&self,
store: &Store,
hook: Arc<Hook>,
reporter: &HookInstallReporter,
) -> Result<InstalledHook> {
let progress = reporter.on_install_start(&hook);
let deno_dir = store.tools_path(ToolBucket::Deno);
let installer = DenoInstaller::new(deno_dir);
let deno_request: &DenoRequest = hook.language_request.version();
let deno = installer
.install(store, deno_request, hook.language_request.allows_download())
.await
.context("Failed to install deno")?;
let mut info = InstallInfo::new(&hook, &store.hooks_dir())?;
info.with_toolchain(deno.deno().to_path_buf());
info.with_language_version((**deno.version()).clone());
let env_bin_dir = bin_dir(&info.env_path);
fs_err::tokio::create_dir_all(&env_bin_dir).await?;
let install_dir = hook.repo_path().unwrap_or(hook.work_dir());
let deno_cache_dir = store.cache_path(CacheBucket::Deno);
fs_err::tokio::create_dir_all(&deno_cache_dir).await?;
if !hook.additional_dependencies.is_empty() {
debug!(deps = ?hook.additional_dependencies, "Installing deno dependencies");
}
for spec in &hook.additional_dependencies {
let (dep, name) = parse_install_dependency(spec);
let mut install_cmd = Cmd::new(deno.deno());
install_cmd
.current_dir(install_dir)
.env(EnvVars::DENO_DIR, &deno_cache_dir)
.env(EnvVars::DENO_NO_UPDATE_CHECK, "1")
.arg("install")
.arg("--allow-all")
.arg("--global")
.arg("--force")
.arg("--root")
.arg(&info.env_path);
if let Some(name) = name {
install_cmd.arg("--name").arg(name);
}
install_cmd
.arg(dep)
.check(true)
.output()
.await
.with_context(|| format!("Failed to install deno dependency `{spec}`"))?;
}
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 deno = DenoResult::from_executable(info.toolchain.clone())
.await
.context("Failed to query deno version")?;
if **deno.version() != info.language_version {
anyhow::bail!(
"Deno version mismatch: expected {}, found {}",
info.language_version,
deno.version()
);
}
Ok(())
}
fn execution_environment(
&self,
store: &Store,
hook: &InstalledHook,
) -> Result<ExecutionEnvironment> {
let deno_cache_dir = store.cache_path(CacheBucket::Deno);
let info = hook.install_info().expect("Deno must be installed");
let deno_bin_dir = hook.toolchain_dir().expect("Deno must have toolchain dir");
let new_path = prepend_paths(&[&bin_dir(&info.env_path), deno_bin_dir])
.context("Failed to join PATH")?;
let mut environment = ExecutionEnvironment::new();
environment
.set_path(&new_path)
.env(EnvVars::DENO_DIR, &deno_cache_dir)
.env(EnvVars::DENO_NO_UPDATE_CHECK, "1");
Ok(environment)
}
}
#[cfg(test)]
mod tests {
use super::parse_install_dependency;
#[test]
fn parse_install_dependency_without_name() {
assert_eq!(
parse_install_dependency("npm:prettier@3"),
("npm:prettier@3", None)
);
}
#[test]
fn parse_install_dependency_with_name() {
assert_eq!(
parse_install_dependency("npm:prettier@3:fmt-tool"),
("npm:prettier@3", Some("fmt-tool"))
);
}
#[test]
fn parse_install_dependency_with_local_path_name() {
assert_eq!(
parse_install_dependency("./tools/echo.ts:echo-tool"),
("./tools/echo.ts", Some("echo-tool"))
);
}
#[test]
fn parse_install_dependency_with_invalid_name_keeps_original() {
assert_eq!(
parse_install_dependency("./tools/echo.ts:not valid"),
("./tools/echo.ts:not valid", None)
);
}
}