// Copyright 2018-2025 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use anyhow::Error as AnyError; use deno_error::JsErrorBox; use deno_npm::NpmPackageExtraInfo; use deno_npm::NpmResolutionPackage; use deno_npm::resolution::NpmResolutionSnapshot; use deno_semver::SmallStackString; use deno_semver::Version; use deno_semver::package::PackageNv; use sys_traits::FsMetadata; use crate::CachedNpmPackageExtraInfoProvider; use crate::LifecycleScriptsConfig; use crate::PackagesAllowedScripts; pub struct PackageWithScript<'a> { pub package: &'a NpmResolutionPackage, pub scripts: HashMap, pub package_folder: PathBuf, } pub struct LifecycleScriptsExecutorOptions<'a> { pub init_cwd: &'a Path, pub process_state: &'a str, pub root_node_modules_dir_path: &'a Path, pub on_ran_pkg_scripts: &'a dyn Fn(&NpmResolutionPackage) -> Result<(), JsErrorBox>, pub snapshot: &'a NpmResolutionSnapshot, pub system_packages: &'a [NpmResolutionPackage], pub packages_with_scripts: &'a [PackageWithScript<'a>], pub extra_info_provider: &'a CachedNpmPackageExtraInfoProvider, } pub struct LifecycleScriptsWarning { message: String, did_warn_fn: DidWarnFn, } impl std::fmt::Debug for LifecycleScriptsWarning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LifecycleScriptsWarning") .field("message", &self.message) .finish() } } type DidWarnFn = Box; impl LifecycleScriptsWarning { pub(crate) fn new(message: String, did_warn_fn: DidWarnFn) -> Self { Self { message, did_warn_fn, } } pub fn into_message( self, sys: &dyn sys_traits::boxed::FsOpenBoxed, ) -> String { (self.did_warn_fn)(sys); self.message } } #[derive(Debug)] pub struct NullLifecycleScriptsExecutor; #[async_trait::async_trait(?Send)] impl LifecycleScriptsExecutor for NullLifecycleScriptsExecutor { async fn execute( &self, _options: LifecycleScriptsExecutorOptions<'_>, ) -> Result<(), AnyError> { Ok(()) } } #[async_trait::async_trait(?Send)] pub trait LifecycleScriptsExecutor: Sync + Send { async fn execute( &self, options: LifecycleScriptsExecutorOptions<'_>, ) -> Result<(), AnyError>; } pub trait LifecycleScriptsStrategy { fn can_run_scripts(&self) -> bool { true } fn warn_on_scripts_not_run( &self, packages: &[(&NpmResolutionPackage, PathBuf)], ) -> Result<(), std::io::Error>; fn has_warned(&self, package: &NpmResolutionPackage) -> bool; fn has_run(&self, package: &NpmResolutionPackage) -> bool; } pub fn has_lifecycle_scripts( sys: &impl FsMetadata, extra: &NpmPackageExtraInfo, package_path: &Path, ) -> bool { if let Some(install) = extra.scripts.get("install") { { // default script if !is_broken_default_install_script(sys, install, package_path) { return true; } } } extra.scripts.contains_key("preinstall") || extra.scripts.contains_key("postinstall") } // npm defaults to running `node-gyp rebuild` if there is a `binding.gyp` file // but it always fails if the package excludes the `binding.gyp` file when they publish. // (for example, `fsevents` hits this) pub fn is_broken_default_install_script( sys: &impl FsMetadata, script: &str, package_path: &Path, ) -> bool { script == "node-gyp rebuild" && !sys.fs_exists_no_err(package_path.join("binding.gyp")) } pub struct LifecycleScripts<'a, TSys: FsMetadata> { sys: &'a TSys, packages_with_scripts: Vec>, packages_with_scripts_not_run: Vec<(&'a NpmResolutionPackage, PathBuf)>, config: &'a LifecycleScriptsConfig, strategy: Box, } impl<'a, TSys: FsMetadata> LifecycleScripts<'a, TSys> { pub fn new( sys: &'a TSys, config: &'a LifecycleScriptsConfig, strategy: TLifecycleScriptsStrategy, ) -> Self { Self { sys, config, packages_with_scripts: Vec::new(), packages_with_scripts_not_run: Vec::new(), strategy: Box::new(strategy), } } pub fn can_run_scripts(&self, package_nv: &PackageNv) -> bool { if !self.strategy.can_run_scripts() { return false; } match &self.config.allowed { PackagesAllowedScripts::All => true, // TODO: make this more correct PackagesAllowedScripts::Some(allow_list) => allow_list.iter().any(|s| { let s = s.strip_prefix("npm:").unwrap_or(s); s == package_nv.name || s == package_nv.to_string() }), PackagesAllowedScripts::None => false, } } pub fn has_run_scripts(&self, package: &NpmResolutionPackage) -> bool { self.strategy.has_run(package) } /// Register a package for running lifecycle scripts, if applicable. /// /// `package_path` is the path containing the package's code (its root dir). /// `package_meta_path` is the path to serve as the base directory for lifecycle /// script-related metadata (e.g. to store whether the scripts have been run already) pub fn add( &mut self, package: &'a NpmResolutionPackage, extra: &NpmPackageExtraInfo, package_path: Cow<'_, Path>, ) { if has_lifecycle_scripts(self.sys, extra, &package_path) { if self.can_run_scripts(&package.id.nv) { if !self.has_run_scripts(package) { self.packages_with_scripts.push(PackageWithScript { package, scripts: extra.scripts.clone(), package_folder: package_path.into_owned(), }); } } else if !self.has_run_scripts(package) && (self.config.explicit_install || !self.strategy.has_warned(package)) { // Skip adding `esbuild` as it is known that it can work properly without lifecycle script // being run, and it's also very popular - any project using Vite would raise warnings. { let nv = &package.id.nv; if nv.name == "esbuild" && nv.version >= Version::parse_standard("0.18.0").unwrap() { return; } } self .packages_with_scripts_not_run .push((package, package_path.into_owned())); } } } pub fn warn_not_run_scripts(&self) -> Result<(), std::io::Error> { if !self.packages_with_scripts_not_run.is_empty() { self .strategy .warn_on_scripts_not_run(&self.packages_with_scripts_not_run)?; } Ok(()) } pub fn packages_with_scripts(&self) -> &[PackageWithScript<'a>] { &self.packages_with_scripts } } pub static LIFECYCLE_SCRIPTS_RUNNING_ENV_VAR: &str = "DENO_INTERNAL_IS_LIFECYCLE_SCRIPT"; pub fn is_running_lifecycle_script(sys: &impl sys_traits::EnvVar) -> bool { sys.env_var(LIFECYCLE_SCRIPTS_RUNNING_ENV_VAR).is_ok() }