mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 12:19:12 +00:00

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
242 lines
6.8 KiB
Rust
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()
|
|
}
|