diff options
Diffstat (limited to 'cli/npm')
-rw-r--r-- | cli/npm/managed/mod.rs | 12 | ||||
-rw-r--r-- | cli/npm/managed/resolvers/local.rs | 220 | ||||
-rw-r--r-- | cli/npm/managed/resolvers/local/bin_entries.rs | 76 | ||||
-rw-r--r-- | cli/npm/managed/resolvers/mod.rs | 3 |
4 files changed, 286 insertions, 25 deletions
diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs index 6022396d6..76645d1d6 100644 --- a/cli/npm/managed/mod.rs +++ b/cli/npm/managed/mod.rs @@ -29,6 +29,7 @@ use deno_semver::package::PackageReq; use resolution::AddPkgReqsResult; use crate::args::CliLockfile; +use crate::args::LifecycleScriptsConfig; use crate::args::NpmProcessState; use crate::args::NpmProcessStateKind; use crate::args::PackageJsonInstallDepsProvider; @@ -70,6 +71,7 @@ pub struct CliNpmResolverManagedCreateOptions { pub npm_system_info: NpmSystemInfo, pub package_json_deps_provider: Arc<PackageJsonInstallDepsProvider>, pub npmrc: Arc<ResolvedNpmRc>, + pub lifecycle_scripts: LifecycleScriptsConfig, } pub async fn create_managed_npm_resolver_for_lsp( @@ -98,6 +100,7 @@ pub async fn create_managed_npm_resolver_for_lsp( options.maybe_node_modules_path, options.npm_system_info, snapshot, + options.lifecycle_scripts, ) }) .await @@ -122,6 +125,7 @@ pub async fn create_managed_npm_resolver( options.maybe_node_modules_path, options.npm_system_info, snapshot, + options.lifecycle_scripts, )) } @@ -138,6 +142,7 @@ fn create_inner( node_modules_dir_path: Option<PathBuf>, npm_system_info: NpmSystemInfo, snapshot: Option<ValidSerializedNpmResolutionSnapshot>, + lifecycle_scripts: LifecycleScriptsConfig, ) -> Arc<dyn CliNpmResolver> { let resolution = Arc::new(NpmResolution::from_serialized( npm_api.clone(), @@ -160,6 +165,7 @@ fn create_inner( tarball_cache.clone(), node_modules_dir_path, npm_system_info.clone(), + lifecycle_scripts.clone(), ); Arc::new(ManagedCliNpmResolver::new( fs, @@ -172,6 +178,7 @@ fn create_inner( tarball_cache, text_only_progress_bar, npm_system_info, + lifecycle_scripts, )) } @@ -258,6 +265,7 @@ pub struct ManagedCliNpmResolver { text_only_progress_bar: ProgressBar, npm_system_info: NpmSystemInfo, top_level_install_flag: AtomicFlag, + lifecycle_scripts: LifecycleScriptsConfig, } impl std::fmt::Debug for ManagedCliNpmResolver { @@ -281,6 +289,7 @@ impl ManagedCliNpmResolver { tarball_cache: Arc<TarballCache>, text_only_progress_bar: ProgressBar, npm_system_info: NpmSystemInfo, + lifecycle_scripts: LifecycleScriptsConfig, ) -> Self { Self { fs, @@ -294,6 +303,7 @@ impl ManagedCliNpmResolver { tarball_cache, npm_system_info, top_level_install_flag: Default::default(), + lifecycle_scripts, } } @@ -578,6 +588,7 @@ impl CliNpmResolver for ManagedCliNpmResolver { self.tarball_cache.clone(), self.root_node_modules_path().map(ToOwned::to_owned), self.npm_system_info.clone(), + self.lifecycle_scripts.clone(), ), self.maybe_lockfile.clone(), self.npm_api.clone(), @@ -587,6 +598,7 @@ impl CliNpmResolver for ManagedCliNpmResolver { self.tarball_cache.clone(), self.text_only_progress_bar.clone(), self.npm_system_info.clone(), + self.lifecycle_scripts.clone(), )) } diff --git a/cli/npm/managed/resolvers/local.rs b/cli/npm/managed/resolvers/local.rs index 1f8e82d54..913cf986d 100644 --- a/cli/npm/managed/resolvers/local.rs +++ b/cli/npm/managed/resolvers/local.rs @@ -16,8 +16,11 @@ use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; +use crate::args::LifecycleScriptsConfig; +use crate::args::PackagesAllowedScripts; use async_trait::async_trait; use deno_ast::ModuleSpecifier; +use deno_core::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures::stream::FuturesUnordered; @@ -68,6 +71,7 @@ pub struct LocalNpmPackageResolver { root_node_modules_url: Url, system_info: NpmSystemInfo, registry_read_permission_checker: RegistryReadPermissionChecker, + lifecycle_scripts: LifecycleScriptsConfig, } impl LocalNpmPackageResolver { @@ -81,6 +85,7 @@ impl LocalNpmPackageResolver { tarball_cache: Arc<TarballCache>, node_modules_folder: PathBuf, system_info: NpmSystemInfo, + lifecycle_scripts: LifecycleScriptsConfig, ) -> Self { Self { cache, @@ -97,6 +102,7 @@ impl LocalNpmPackageResolver { .unwrap(), root_node_modules_path: node_modules_folder, system_info, + lifecycle_scripts, } } @@ -245,6 +251,7 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { &self.tarball_cache, &self.root_node_modules_path, &self.system_info, + &self.lifecycle_scripts, ) .await } @@ -260,7 +267,131 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { } } +// take in all (non copy) packages from snapshot, +// and resolve the set of available binaries to create +// custom commands available to the task runner +fn resolve_baseline_custom_commands( + snapshot: &NpmResolutionSnapshot, + packages: &[NpmResolutionPackage], + local_registry_dir: &Path, +) -> Result<crate::task_runner::TaskCustomCommands, AnyError> { + let mut custom_commands = crate::task_runner::TaskCustomCommands::new(); + custom_commands + .insert("npx".to_string(), Rc::new(crate::task_runner::NpxCommand)); + + custom_commands + .insert("npm".to_string(), Rc::new(crate::task_runner::NpmCommand)); + + custom_commands + .insert("node".to_string(), Rc::new(crate::task_runner::NodeCommand)); + + custom_commands.insert( + "node-gyp".to_string(), + Rc::new(crate::task_runner::NodeGypCommand), + ); + + // TODO: this recreates the bin entries which could be redoing some work, but the ones + // we compute earlier in `sync_resolution_with_fs` may not be exhaustive (because we skip + // doing it for packages that are set up already. + // realistically, scripts won't be run very often so it probably isn't too big of an issue. + resolve_custom_commands_from_packages( + custom_commands, + snapshot, + packages, + local_registry_dir, + ) +} + +// resolves the custom commands from an iterator of packages +// and adds them to the existing custom commands. +// note that this will overwrite any existing custom commands +fn resolve_custom_commands_from_packages< + 'a, + P: IntoIterator<Item = &'a NpmResolutionPackage>, +>( + mut commands: crate::task_runner::TaskCustomCommands, + snapshot: &'a NpmResolutionSnapshot, + packages: P, + local_registry_dir: &Path, +) -> Result<crate::task_runner::TaskCustomCommands, AnyError> { + let mut bin_entries = bin_entries::BinEntries::new(); + for package in packages { + let package_path = + local_node_modules_package_path(local_registry_dir, package); + + if package.bin.is_some() { + bin_entries.add(package.clone(), package_path); + } + } + let bins = bin_entries.into_bin_files(snapshot); + for (bin_name, script_path) in bins { + commands.insert( + bin_name.clone(), + Rc::new(crate::task_runner::NodeModulesFileRunCommand { + command_name: bin_name, + path: script_path, + }), + ); + } + + Ok(commands) +} + +fn local_node_modules_package_path( + local_registry_dir: &Path, + package: &NpmResolutionPackage, +) -> PathBuf { + local_registry_dir + .join(get_package_folder_id_folder_name( + &package.get_package_cache_folder_id(), + )) + .join("node_modules") + .join(&package.id.nv.name) +} + +// resolves the custom commands from the dependencies of a package +// and adds them to the existing custom commands. +// note that this will overwrite any existing custom commands. +fn resolve_custom_commands_from_deps( + baseline: crate::task_runner::TaskCustomCommands, + package: &NpmResolutionPackage, + snapshot: &NpmResolutionSnapshot, + local_registry_dir: &Path, +) -> Result<crate::task_runner::TaskCustomCommands, AnyError> { + resolve_custom_commands_from_packages( + baseline, + snapshot, + package + .dependencies + .values() + .map(|id| snapshot.package_from_id(id).unwrap()), + local_registry_dir, + ) +} + +fn can_run_scripts( + allow_scripts: &PackagesAllowedScripts, + package_nv: &PackageNv, +) -> bool { + match allow_scripts { + 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, + } +} + +fn has_lifecycle_scripts(package: &NpmResolutionPackage) -> bool { + package.scripts.contains_key("preinstall") + || package.scripts.contains_key("install") + || package.scripts.contains_key("postinstall") +} + /// Creates a pnpm style folder structure. +#[allow(clippy::too_many_arguments)] async fn sync_resolution_with_fs( snapshot: &NpmResolutionSnapshot, cache: &Arc<NpmCache>, @@ -269,6 +400,7 @@ async fn sync_resolution_with_fs( tarball_cache: &Arc<TarballCache>, root_node_modules_dir_path: &Path, system_info: &NpmSystemInfo, + lifecycle_scripts: &LifecycleScriptsConfig, ) -> Result<(), AnyError> { if snapshot.is_empty() && pkg_json_deps_provider.workspace_pkgs().is_empty() { return Ok(()); // don't create the directory @@ -307,6 +439,8 @@ async fn sync_resolution_with_fs( let mut newest_packages_by_name: HashMap<&String, &NpmResolutionPackage> = HashMap::with_capacity(package_partitions.packages.len()); let bin_entries = Rc::new(RefCell::new(bin_entries::BinEntries::new())); + let mut packages_with_scripts = Vec::with_capacity(2); + let mut packages_with_scripts_not_run = Vec::new(); for package in &package_partitions.packages { if let Some(current_pkg) = newest_packages_by_name.get_mut(&package.id.nv.name) @@ -331,6 +465,7 @@ async fn sync_resolution_with_fs( // are forced to be recreated setup_cache.remove_dep(&package_folder_name); + let folder_path = folder_path.clone(); let bin_entries_to_setup = bin_entries.clone(); cache_futures.push(async move { tarball_cache @@ -368,6 +503,24 @@ async fn sync_resolution_with_fs( Ok::<_, AnyError>(()) }); } + + if has_lifecycle_scripts(package) { + let scripts_run = folder_path.join(".scripts-run"); + if can_run_scripts(&lifecycle_scripts.allowed, &package.id.nv) { + if !scripts_run.exists() { + let sub_node_modules = folder_path.join("node_modules"); + let package_path = + join_package_name(&sub_node_modules, &package.id.nv.name); + packages_with_scripts.push(( + package.clone(), + package_path, + scripts_run, + )); + } + } else if !scripts_run.exists() { + packages_with_scripts_not_run.push(package.id.nv.clone()); + } + } } while let Some(result) = cache_futures.next().await { @@ -509,6 +662,73 @@ async fn sync_resolution_with_fs( } } + if !packages_with_scripts.is_empty() { + // get custom commands for each bin available in the node_modules dir (essentially + // the scripts that are in `node_modules/.bin`) + let base = resolve_baseline_custom_commands( + snapshot, + &package_partitions.packages, + &deno_local_registry_dir, + )?; + let init_cwd = lifecycle_scripts.initial_cwd.as_deref().unwrap(); + + for (package, package_path, scripts_run_path) in packages_with_scripts { + // add custom commands for binaries from the package's dependencies. this will take precedence over the + // baseline commands, so if the package relies on a bin that conflicts with one higher in the dependency tree, the + // correct bin will be used. + let custom_commands = resolve_custom_commands_from_deps( + base.clone(), + &package, + snapshot, + &deno_local_registry_dir, + )?; + for script_name in ["preinstall", "install", "postinstall"] { + if let Some(script) = package.scripts.get(script_name) { + let exit_code = + crate::task_runner::run_task(crate::task_runner::RunTaskOptions { + task_name: script_name, + script, + cwd: &package_path, + env_vars: crate::task_runner::real_env_vars(), + custom_commands: custom_commands.clone(), + init_cwd, + argv: &[], + root_node_modules_dir: Some(root_node_modules_dir_path), + }) + .await?; + if exit_code != 0 { + anyhow::bail!( + "script '{}' in '{}' failed with exit code {}", + script_name, + package.id.nv, + exit_code, + ); + } + } + } + fs::write(scripts_run_path, "")?; + } + } + + if !packages_with_scripts_not_run.is_empty() { + let (maybe_install, maybe_install_example) = if *crate::args::DENO_FUTURE { + ( + " or `deno install`", + " or `deno install --allow-scripts=pkg1,pkg2`", + ) + } else { + ("", "") + }; + let packages = packages_with_scripts_not_run + .iter() + .map(|p| p.to_string()) + .collect::<Vec<_>>() + .join(", "); + log::warn!("{}: Packages contained npm lifecycle scripts (preinstall/install/postinstall) that were not executed. + This may cause the packages to not work correctly. To run them, use the `--allow-scripts` flag with `deno cache`{maybe_install} + (e.g. `deno cache --allow-scripts=pkg1,pkg2 <entrypoint>`{maybe_install_example}):\n {packages}", crate::colors::yellow("warning")); + } + setup_cache.save(); drop(single_process_lock); drop(pb_clear_guard); diff --git a/cli/npm/managed/resolvers/local/bin_entries.rs b/cli/npm/managed/resolvers/local/bin_entries.rs index 4a3c5ce4f..980a2653b 100644 --- a/cli/npm/managed/resolvers/local/bin_entries.rs +++ b/cli/npm/managed/resolvers/local/bin_entries.rs @@ -71,19 +71,16 @@ impl BinEntries { self.entries.push((package, package_path)); } - /// Finish setting up the bin entries, writing the necessary files - /// to disk. - pub(super) fn finish( - mut self, + fn for_each_entry( + &mut self, snapshot: &NpmResolutionSnapshot, - bin_node_modules_dir_path: &Path, + mut f: impl FnMut( + &NpmResolutionPackage, + &Path, + &str, // bin name + &str, // bin script + ) -> Result<(), AnyError>, ) -> Result<(), AnyError> { - if !self.entries.is_empty() && !bin_node_modules_dir_path.exists() { - std::fs::create_dir_all(bin_node_modules_dir_path).with_context( - || format!("Creating '{}'", bin_node_modules_dir_path.display()), - )?; - } - if !self.collisions.is_empty() { // walking the dependency tree to find out the depth of each package // is sort of expensive, so we only do it if there's a collision @@ -101,13 +98,7 @@ impl BinEntries { // we already set up a bin entry with this name continue; } - set_up_bin_entry( - package, - name, - script, - package_path, - bin_node_modules_dir_path, - )?; + f(package, package_path, name, script)?; } deno_npm::registry::NpmPackageVersionBinEntry::Map(entries) => { for (name, script) in entries { @@ -115,13 +106,7 @@ impl BinEntries { // we already set up a bin entry with this name continue; } - set_up_bin_entry( - package, - name, - script, - package_path, - bin_node_modules_dir_path, - )?; + f(package, package_path, name, script)?; } } } @@ -130,6 +115,47 @@ impl BinEntries { Ok(()) } + + /// Collect the bin entries into a vec of (name, script path) + pub(super) fn into_bin_files( + mut self, + snapshot: &NpmResolutionSnapshot, + ) -> Vec<(String, PathBuf)> { + let mut bins = Vec::new(); + self + .for_each_entry(snapshot, |_, package_path, name, script| { + bins.push((name.to_string(), package_path.join(script))); + Ok(()) + }) + .unwrap(); + bins + } + + /// Finish setting up the bin entries, writing the necessary files + /// to disk. + pub(super) fn finish( + mut self, + snapshot: &NpmResolutionSnapshot, + bin_node_modules_dir_path: &Path, + ) -> Result<(), AnyError> { + if !self.entries.is_empty() && !bin_node_modules_dir_path.exists() { + std::fs::create_dir_all(bin_node_modules_dir_path).with_context( + || format!("Creating '{}'", bin_node_modules_dir_path.display()), + )?; + } + + self.for_each_entry(snapshot, |package, package_path, name, script| { + set_up_bin_entry( + package, + name, + script, + package_path, + bin_node_modules_dir_path, + ) + })?; + + Ok(()) + } } // walk the dependency tree to find out the depth of each package diff --git a/cli/npm/managed/resolvers/mod.rs b/cli/npm/managed/resolvers/mod.rs index a7f545916..2dfc323e9 100644 --- a/cli/npm/managed/resolvers/mod.rs +++ b/cli/npm/managed/resolvers/mod.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use deno_npm::NpmSystemInfo; use deno_runtime::deno_fs::FileSystem; +use crate::args::LifecycleScriptsConfig; use crate::args::PackageJsonInstallDepsProvider; use crate::util::progress_bar::ProgressBar; @@ -32,6 +33,7 @@ pub fn create_npm_fs_resolver( tarball_cache: Arc<TarballCache>, maybe_node_modules_path: Option<PathBuf>, system_info: NpmSystemInfo, + lifecycle_scripts: LifecycleScriptsConfig, ) -> Arc<dyn NpmPackageFsResolver> { match maybe_node_modules_path { Some(node_modules_folder) => Arc::new(LocalNpmPackageResolver::new( @@ -43,6 +45,7 @@ pub fn create_npm_fs_resolver( tarball_cache, node_modules_folder, system_info, + lifecycle_scripts, )), None => Arc::new(GlobalNpmPackageResolver::new( npm_cache, |