deno/libs/npm_installer/lifecycle_scripts.rs
Nathan Whitaker b88c621f22
fix(install): print install report on add, cache, and all install variants, move scripts and deprecation warnings after the report (#30549)
We were only doing the new output on the top level install (`deno
install` with no args or flags), we now do it on `deno install
npm:chalk`, `deno add npm:chalk`, `deno cache ./foo.ts`, "deno install
--entrypoint ./foo.ts"`.

Additionally the scripts and deprecation warnings were printing above
the output, now they're deferred and displayed below
2025-08-28 09:48:14 +02:00

242 lines
6.8 KiB
Rust

// 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<SmallStackString, String>,
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<dyn FnOnce(&dyn sys_traits::boxed::FsOpenBoxed) + Send + Sync>;
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<PackageWithScript<'a>>,
packages_with_scripts_not_run: Vec<(&'a NpmResolutionPackage, PathBuf)>,
config: &'a LifecycleScriptsConfig,
strategy: Box<dyn LifecycleScriptsStrategy + 'a>,
}
impl<'a, TSys: FsMetadata> LifecycleScripts<'a, TSys> {
pub fn new<TLifecycleScriptsStrategy: LifecycleScriptsStrategy + 'a>(
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()
}