diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-04-06 18:46:44 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-06 18:46:44 -0400 |
commit | d07aa4a0723b04583b7cb1e09152457d866d13d3 (patch) | |
tree | f329a30becca95583fb71b4158c939c68228ce06 /cli | |
parent | 1586c52b5b5ad511ec0bf896e94de8585f743cf8 (diff) |
refactor(npm): use deno_npm and deno_semver (#18602)
Diffstat (limited to 'cli')
35 files changed, 645 insertions, 5771 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 873a088d9..5161c7ae6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -42,12 +42,14 @@ winres.workspace = true [dependencies] deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } -deno_doc = "0.59.0" -deno_emit = "0.17.0" -deno_graph = "=0.45.0" +deno_doc = "0.60.0" +deno_emit = "0.18.0" +deno_graph = "=0.46.0" deno_lint = { version = "0.43.0", features = ["docs"] } deno_lockfile.workspace = true +deno_npm = "0.1.0" deno_runtime = { workspace = true, features = ["dont_create_runtime_snapshot", "include_js_files_for_snapshotting"] } +deno_semver = "0.2.0" deno_task_shell = "0.11.0" napi_sym.workspace = true @@ -68,7 +70,7 @@ dprint-plugin-markdown = "=0.15.2" dprint-plugin-typescript = "=0.84.0" encoding_rs.workspace = true env_logger = "=0.9.0" -eszip = "=0.38.0" +eszip = "=0.39.0" fancy-regex = "=0.10.0" flate2.workspace = true fs3.workspace = true diff --git a/cli/args/lockfile.rs b/cli/args/lockfile.rs index 1a3233c5a..31519aee3 100644 --- a/cli/args/lockfile.rs +++ b/cli/args/lockfile.rs @@ -1,18 +1,30 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; + +use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; + +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::futures::stream::FuturesOrdered; +use deno_core::futures::StreamExt; +use deno_core::parking_lot::Mutex; +use deno_npm::registry::NpmRegistryApi; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::resolution::NpmResolutionSnapshotCreateOptions; +use deno_npm::resolution::NpmResolutionSnapshotCreateOptionsPackage; +use deno_npm::NpmPackageId; +use deno_semver::npm::NpmPackageReq; use crate::args::config_file::LockConfig; use crate::args::ConfigFile; -use crate::npm::NpmResolutionPackage; use crate::Flags; use super::DenoSubcommand; pub use deno_lockfile::Lockfile; pub use deno_lockfile::LockfileError; -use deno_lockfile::NpmPackageDependencyLockfileInfo; -use deno_lockfile::NpmPackageLockfileInfo; pub fn discover( flags: &Flags, @@ -61,24 +73,68 @@ pub fn discover( Ok(Some(lockfile)) } -// NOTE(bartlomieju): we don't want a reverse mapping to be possible. -#[allow(clippy::from_over_into)] -impl Into<NpmPackageLockfileInfo> for NpmResolutionPackage { - fn into(self) -> NpmPackageLockfileInfo { - let dependencies = self - .dependencies - .into_iter() - .map(|(name, id)| NpmPackageDependencyLockfileInfo { - name, - id: id.as_serialized(), - }) - .collect(); - - NpmPackageLockfileInfo { - display_id: self.pkg_id.nv.to_string(), - serialized_id: self.pkg_id.as_serialized(), - integrity: self.dist.integrity().to_string(), - dependencies, +pub async fn snapshot_from_lockfile( + lockfile: Arc<Mutex<Lockfile>>, + api: &dyn NpmRegistryApi, +) -> Result<NpmResolutionSnapshot, AnyError> { + let (root_packages, mut packages) = { + let lockfile = lockfile.lock(); + + let mut root_packages = + HashMap::<NpmPackageReq, NpmPackageId>::with_capacity( + lockfile.content.npm.specifiers.len(), + ); + // collect the specifiers to version mappings + for (key, value) in &lockfile.content.npm.specifiers { + let package_req = NpmPackageReq::from_str(key) + .with_context(|| format!("Unable to parse npm specifier: {key}"))?; + let package_id = NpmPackageId::from_serialized(value)?; + root_packages.insert(package_req, package_id.clone()); + } + + // now fill the packages except for the dist information + let mut packages = Vec::with_capacity(lockfile.content.npm.packages.len()); + for (key, package) in &lockfile.content.npm.packages { + let pkg_id = NpmPackageId::from_serialized(key)?; + + // collect the dependencies + let mut dependencies = HashMap::with_capacity(package.dependencies.len()); + for (name, specifier) in &package.dependencies { + let dep_id = NpmPackageId::from_serialized(specifier)?; + dependencies.insert(name.clone(), dep_id); + } + + packages.push(NpmResolutionSnapshotCreateOptionsPackage { + pkg_id, + dist: Default::default(), // temporarily empty + dependencies, + }); } + (root_packages, packages) + }; + + // now that the lockfile is dropped, fetch the package version information + let mut version_infos = + FuturesOrdered::from_iter(packages.iter().map(|p| p.pkg_id.nv.clone()).map( + |nv| async move { + match api.package_version_info(&nv).await? { + Some(version_info) => Ok(version_info), + None => { + bail!("could not find '{}' specified in the lockfile. Maybe try again with --reload", nv); + } + } + }, + )); + + let mut i = 0; + while let Some(version_info) = version_infos.next().await { + packages[i].dist = version_info?.dist; + i += 1; } + + NpmResolutionSnapshot::from_packages(NpmResolutionSnapshotCreateOptions { + packages, + root_packages, + }) + .context("The lockfile is corrupt. You can recreate it with --lock-write") } diff --git a/cli/args/mod.rs b/cli/args/mod.rs index bbf3f7efb..20c382622 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -8,14 +8,14 @@ mod lockfile; pub mod package_json; pub use self::import_map::resolve_import_map_from_specifier; +use self::lockfile::snapshot_from_lockfile; use self::package_json::PackageJsonDeps; use ::import_map::ImportMap; use deno_core::resolve_url_or_path; -use deno_graph::npm::NpmPackageReqReference; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_semver::npm::NpmPackageReqReference; use indexmap::IndexMap; -use crate::npm::NpmRegistryApi; -use crate::npm::NpmResolutionSnapshot; pub use config_file::BenchConfig; pub use config_file::CompilerOptions; pub use config_file::ConfigFile; @@ -65,6 +65,7 @@ use std::sync::Arc; use crate::cache::DenoDir; use crate::file_fetcher::FileFetcher; use crate::npm::NpmProcessState; +use crate::npm::NpmRegistry; use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::version; @@ -745,7 +746,7 @@ impl CliOptions { pub async fn resolve_npm_resolution_snapshot( &self, - api: &NpmRegistryApi, + api: &NpmRegistry, ) -> Result<Option<NpmResolutionSnapshot>, AnyError> { if let Some(state) = &*NPM_PROCESS_STATE { // TODO(bartlomieju): remove this clone @@ -755,7 +756,7 @@ impl CliOptions { if let Some(lockfile) = self.maybe_lock_file() { if !lockfile.lock().overwrite { return Ok(Some( - NpmResolutionSnapshot::from_lockfile(lockfile.clone(), api) + snapshot_from_lockfile(lockfile.clone(), api) .await .with_context(|| { format!( diff --git a/cli/args/package_json.rs b/cli/args/package_json.rs index 5975395bb..c4d4ce956 100644 --- a/cli/args/package_json.rs +++ b/cli/args/package_json.rs @@ -7,41 +7,18 @@ use std::path::PathBuf; use deno_core::anyhow::bail; use deno_core::error::AnyError; -use deno_graph::npm::NpmPackageReq; -use deno_graph::semver::NpmVersionReqSpecifierParseError; -use deno_graph::semver::VersionReq; +use deno_npm::registry::parse_dep_entry_name_and_raw_version; +use deno_npm::registry::PackageDepNpmSchemeValueParseError; use deno_runtime::deno_node::PackageJson; +use deno_semver::npm::NpmPackageReq; +use deno_semver::npm::NpmVersionReqSpecifierParseError; +use deno_semver::VersionReq; use thiserror::Error; -#[derive(Debug, Clone, Error, PartialEq, Eq, Hash)] -#[error("Could not find @ symbol in npm url '{value}'")] -pub struct PackageJsonDepNpmSchemeValueParseError { - pub value: String, -} - -/// Gets the name and raw version constraint taking into account npm -/// package aliases. -pub fn parse_dep_entry_name_and_raw_version<'a>( - key: &'a str, - value: &'a str, -) -> Result<(&'a str, &'a str), PackageJsonDepNpmSchemeValueParseError> { - if let Some(package_and_version) = value.strip_prefix("npm:") { - if let Some((name, version)) = package_and_version.rsplit_once('@') { - Ok((name, version)) - } else { - Err(PackageJsonDepNpmSchemeValueParseError { - value: value.to_string(), - }) - } - } else { - Ok((key, value)) - } -} - -#[derive(Debug, Error, Clone, Hash)] +#[derive(Debug, Error, Clone)] pub enum PackageJsonDepValueParseError { #[error(transparent)] - SchemeValue(#[from] PackageJsonDepNpmSchemeValueParseError), + SchemeValue(#[from] PackageDepNpmSchemeValueParseError), #[error(transparent)] Specifier(#[from] NpmVersionReqSpecifierParseError), #[error("Not implemented scheme '{scheme}'")] diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 539868eca..8c2126561 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -27,12 +27,12 @@ use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::ModuleSpecifier; -use deno_graph::npm::NpmPackageReqReference; use deno_graph::Resolution; use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_lint::rules::LintRule; use deno_runtime::tokio_util::create_basic_runtime; +use deno_semver::npm::NpmPackageReqReference; use log::error; use std::collections::HashMap; use std::sync::Arc; diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 311979a53..abd91d7fd 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -21,7 +21,7 @@ use crate::node; use crate::node::node_resolve_npm_reference; use crate::node::NodeResolution; use crate::npm::NpmPackageResolver; -use crate::npm::NpmRegistryApi; +use crate::npm::NpmRegistry; use crate::npm::NpmResolution; use crate::npm::PackageJsonDepsInstaller; use crate::resolver::CliGraphResolver; @@ -37,16 +37,17 @@ use deno_core::futures::future; use deno_core::parking_lot::Mutex; use deno_core::url; use deno_core::ModuleSpecifier; -use deno_graph::npm::NpmPackageReq; -use deno_graph::npm::NpmPackageReqReference; use deno_graph::GraphImport; use deno_graph::Resolution; use deno_runtime::deno_node::NodeResolutionMode; use deno_runtime::deno_node::PackageJson; use deno_runtime::permissions::PermissionsContainer; +use deno_semver::npm::NpmPackageReq; +use deno_semver::npm::NpmPackageReqReference; use indexmap::IndexMap; use lsp::Url; use once_cell::sync::Lazy; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; @@ -1165,7 +1166,7 @@ impl Documents { maybe_import_map: Option<Arc<import_map::ImportMap>>, maybe_config_file: Option<&ConfigFile>, maybe_package_json: Option<&PackageJson>, - npm_registry_api: NpmRegistryApi, + npm_registry_api: NpmRegistry, npm_resolution: NpmResolution, ) { fn calculate_resolver_config_hash( @@ -1186,7 +1187,23 @@ impl Documents { hasher.write_str(import_map.base_url().as_str()); } hasher.write_hashable(&maybe_jsx_config); - hasher.write_hashable(&maybe_package_json_deps); + if let Some(package_json_deps) = &maybe_package_json_deps { + // We need to ensure the hashing is deterministic so explicitly type + // this in order to catch if the type of package_json_deps ever changes + // from a sorted/deterministic BTreeMap to something else. + let package_json_deps: &BTreeMap<_, _> = *package_json_deps; + for (key, value) in package_json_deps { + hasher.write_hashable(key); + match value { + Ok(value) => { + hasher.write_hashable(value); + } + Err(err) => { + hasher.write_str(&err.to_string()); + } + } + } + } hasher.finish() } @@ -1847,7 +1864,7 @@ console.log(b, "hello deno"); #[test] fn test_documents_refresh_dependencies_config_change() { - let npm_registry_api = NpmRegistryApi::new_uninitialized(); + let npm_registry_api = NpmRegistry::new_uninitialized(); let npm_resolution = NpmResolution::new(npm_registry_api.clone(), None, None); diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index ce74c3747..c672f76f0 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -81,7 +81,7 @@ use crate::lsp::urls::LspUrlKind; use crate::npm::create_npm_fs_resolver; use crate::npm::NpmCache; use crate::npm::NpmPackageResolver; -use crate::npm::NpmRegistryApi; +use crate::npm::NpmRegistry; use crate::npm::NpmResolution; use crate::proc_state::ProcState; use crate::tools::fmt::format_file; @@ -145,7 +145,7 @@ pub struct Inner { /// A lazily create "server" for handling test run requests. maybe_testing_server: Option<testing::TestServer>, /// Npm's registry api. - npm_api: NpmRegistryApi, + npm_api: NpmRegistry, /// Npm cache npm_cache: NpmCache, /// Npm resolution that is stored in memory. @@ -417,8 +417,8 @@ impl LanguageServer { fn create_lsp_structs( dir: &DenoDir, http_client: HttpClient, -) -> (NpmRegistryApi, NpmCache, NpmPackageResolver, NpmResolution) { - let registry_url = NpmRegistryApi::default_url(); +) -> (NpmRegistry, NpmCache, NpmPackageResolver, NpmResolution) { + let registry_url = NpmRegistry::default_url(); let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly); let npm_cache = NpmCache::from_deno_dir( dir, @@ -430,7 +430,7 @@ fn create_lsp_structs( http_client.clone(), progress_bar.clone(), ); - let api = NpmRegistryApi::new( + let api = NpmRegistry::new( registry_url.clone(), npm_cache.clone(), http_client, diff --git a/cli/node/mod.rs b/cli/node/mod.rs index 0906deed0..28fd180da 100644 --- a/cli/node/mod.rs +++ b/cli/node/mod.rs @@ -15,8 +15,6 @@ use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::serde_json::Value; use deno_core::url::Url; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageNvReference; use deno_runtime::deno_node; use deno_runtime::deno_node::errors; use deno_runtime::deno_node::find_builtin_node_module; @@ -35,6 +33,8 @@ use deno_runtime::deno_node::RealFs; use deno_runtime::deno_node::RequireNpmResolver; use deno_runtime::deno_node::DEFAULT_CONDITIONS; use deno_runtime::permissions::PermissionsContainer; +use deno_semver::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageNvReference; use once_cell::sync::Lazy; use regex::Regex; diff --git a/cli/npm/cache.rs b/cli/npm/cache.rs index 81fb76772..3c37aebe9 100644 --- a/cli/npm/cache.rs +++ b/cli/npm/cache.rs @@ -13,8 +13,10 @@ use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::url::Url; -use deno_graph::npm::NpmPackageNv; -use deno_graph::semver::Version; +use deno_npm::registry::NpmPackageVersionDistInfo; +use deno_npm::NpmPackageCacheFolderId; +use deno_semver::npm::NpmPackageNv; +use deno_semver::Version; use once_cell::sync::Lazy; use crate::args::CacheSetting; @@ -25,7 +27,6 @@ use crate::util::fs::hard_link_dir_recursive; use crate::util::path::root_url_to_safe_local_dirname; use crate::util::progress_bar::ProgressBar; -use super::registry::NpmPackageVersionDistInfo; use super::tarball::verify_and_extract_tarball; static SHOULD_SYNC_DOWNLOAD: Lazy<bool> = @@ -112,32 +113,6 @@ pub fn with_folder_sync_lock( } } -pub struct NpmPackageCacheFolderId { - pub nv: NpmPackageNv, - /// Peer dependency resolution may require us to have duplicate copies - /// of the same package. - pub copy_index: usize, -} - -impl NpmPackageCacheFolderId { - pub fn with_no_count(&self) -> Self { - Self { - nv: self.nv.clone(), - copy_index: 0, - } - } -} - -impl std::fmt::Display for NpmPackageCacheFolderId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.nv)?; - if self.copy_index > 0 { - write!(f, "_{}", self.copy_index)?; - } - Ok(()) - } -} - #[derive(Clone, Debug)] pub struct ReadonlyNpmCache { root_dir: PathBuf, @@ -515,8 +490,8 @@ pub fn mixed_case_package_name_decode(name: &str) -> Option<String> { #[cfg(test)] mod test { use deno_core::url::Url; - use deno_graph::npm::NpmPackageNv; - use deno_graph::semver::Version; + use deno_semver::npm::NpmPackageNv; + use deno_semver::Version; use super::ReadonlyNpmCache; use crate::npm::cache::NpmPackageCacheFolderId; diff --git a/cli/npm/installer.rs b/cli/npm/installer.rs index 72a58fb53..6d048f7ca 100644 --- a/cli/npm/installer.rs +++ b/cli/npm/installer.rs @@ -4,16 +4,19 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use deno_core::error::AnyError; +use deno_core::futures::stream::FuturesOrdered; +use deno_core::futures::StreamExt; +use deno_npm::registry::NpmRegistryApi; use crate::args::package_json::PackageJsonDeps; -use super::NpmRegistryApi; +use super::NpmRegistry; use super::NpmResolution; #[derive(Debug)] struct PackageJsonDepsInstallerInner { has_installed: AtomicBool, - npm_registry_api: NpmRegistryApi, + npm_registry_api: NpmRegistry, npm_resolution: NpmResolution, package_deps: PackageJsonDeps, } @@ -24,7 +27,7 @@ pub struct PackageJsonDepsInstaller(Option<Arc<PackageJsonDepsInstallerInner>>); impl PackageJsonDepsInstaller { pub fn new( - npm_registry_api: NpmRegistryApi, + npm_registry_api: NpmRegistry, npm_resolution: NpmResolution, deps: Option<PackageJsonDeps>, ) -> Self { @@ -76,17 +79,20 @@ impl PackageJsonDepsInstaller { .collect::<Vec<_>>(); package_reqs.sort(); // deterministic resolution - inner - .npm_registry_api - .cache_in_parallel( - package_reqs.iter().map(|req| req.name.clone()).collect(), - ) - .await?; + let mut req_with_infos = + FuturesOrdered::from_iter(package_reqs.into_iter().map(|req| { + let api = inner.npm_registry_api.clone(); + async move { + let info = api.package_info(&req.name).await?; + Ok::<_, AnyError>((req, info)) + } + })); - for package_req in package_reqs { + while let Some(result) = req_with_infos.next().await { + let (req, info) = result?; inner .npm_resolution - .resolve_package_req_as_pending(package_req)?; + .resolve_package_req_as_pending_with_info(req, &info)?; } Ok(()) diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index b1ce6fda4..95a0a3017 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -10,15 +10,8 @@ mod tarball; pub use cache::should_sync_download; pub use cache::NpmCache; pub use installer::PackageJsonDepsInstaller; -#[cfg(test)] -pub use registry::NpmPackageVersionDistInfo; -pub use registry::NpmRegistryApi; -#[cfg(test)] -pub use registry::TestNpmRegistryApiInner; -pub use resolution::NpmPackageId; +pub use registry::NpmRegistry; pub use resolution::NpmResolution; -pub use resolution::NpmResolutionPackage; -pub use resolution::NpmResolutionSnapshot; pub use resolvers::create_npm_fs_resolver; pub use resolvers::NpmPackageResolver; pub use resolvers::NpmProcessState; diff --git a/cli/npm/registry.rs b/cli/npm/registry.rs index 75760c171..0dcdb720a 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -1,7 +1,5 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use std::borrow::Cow; -use std::cmp::Ordering; use std::collections::HashMap; use std::collections::HashSet; use std::fs; @@ -10,21 +8,21 @@ use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; -use deno_core::anyhow::bail; +use deno_core::anyhow::anyhow; use deno_core::anyhow::Context; use deno_core::error::custom_error; use deno_core::error::AnyError; -use deno_core::futures; +use deno_core::futures::future::BoxFuture; +use deno_core::futures::future::Shared; +use deno_core::futures::FutureExt; use deno_core::parking_lot::Mutex; -use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::url::Url; -use deno_graph::npm::NpmPackageNv; -use deno_graph::semver::VersionReq; +use deno_core::TaskQueue; +use deno_npm::registry::NpmPackageInfo; +use deno_npm::registry::NpmRegistryApi; use once_cell::sync::Lazy; -use serde::Serialize; -use crate::args::package_json::parse_dep_entry_name_and_raw_version; use crate::args::CacheSetting; use crate::cache::CACHE_PERM; use crate::http_util::HttpClient; @@ -34,162 +32,6 @@ use crate::util::progress_bar::ProgressBar; use super::cache::should_sync_download; use super::cache::NpmCache; -// npm registry docs: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md - -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -pub struct NpmPackageInfo { - pub name: String, - pub versions: HashMap<String, NpmPackageVersionInfo>, - #[serde(rename = "dist-tags")] - pub dist_tags: HashMap<String, String>, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum NpmDependencyEntryKind { - Dep, - Peer, - OptionalPeer, -} - -impl NpmDependencyEntryKind { - pub fn is_optional(&self) -> bool { - matches!(self, NpmDependencyEntryKind::OptionalPeer) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NpmDependencyEntry { - pub kind: NpmDependencyEntryKind, - pub bare_specifier: String, - pub name: String, - pub version_req: VersionReq, - /// When the dependency is also marked as a peer dependency, - /// use this entry to resolve the dependency when it can't - /// be resolved as a peer dependency. - pub peer_dep_version_req: Option<VersionReq>, -} - -impl PartialOrd for NpmDependencyEntry { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) - } -} - -impl Ord for NpmDependencyEntry { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // sort the dependencies alphabetically by name then by version descending - match self.name.cmp(&other.name) { - // sort by newest to oldest - Ordering::Equal => other - .version_req - .version_text() - .cmp(self.version_req.version_text()), - ordering => ordering, - } - } -} - -#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct NpmPeerDependencyMeta { - #[serde(default)] - optional: bool, -} - -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -#[serde(untagged)] -pub enum NpmPackageVersionBinEntry { - String(String), - Map(HashMap<String, String>), -} - -#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct NpmPackageVersionInfo { - pub version: String, - pub dist: NpmPackageVersionDistInfo, - pub bin: Option<NpmPackageVersionBinEntry>, - // Bare specifier to version (ex. `"typescript": "^3.0.1") or possibly - // package and version (ex. `"typescript-3.0.1": "npm:typescript@3.0.1"`). - #[serde(default)] - pub dependencies: HashMap<String, String>, - #[serde(default)] - pub peer_dependencies: HashMap<String, String>, - #[serde(default)] - pub peer_dependencies_meta: HashMap<String, NpmPeerDependencyMeta>, -} - -impl NpmPackageVersionInfo { - pub fn dependencies_as_entries( - &self, - ) -> Result<Vec<NpmDependencyEntry>, AnyError> { - fn parse_dep_entry( - (key, value): (&String, &String), - kind: NpmDependencyEntryKind, - ) -> Result<NpmDependencyEntry, AnyError> { - let (name, version_req) = - parse_dep_entry_name_and_raw_version(key, value)?; - let version_req = - VersionReq::parse_from_npm(version_req).with_context(|| { - format!("error parsing version requirement for dependency: {key}@{version_req}") - })?; - Ok(NpmDependencyEntry { - kind, - bare_specifier: key.to_string(), - name: name.to_string(), - version_req, - peer_dep_version_req: None, - }) - } - - let mut result = HashMap::with_capacity( - self.dependencies.len() + self.peer_dependencies.len(), - ); - for entry in &self.peer_dependencies { - let is_optional = self - .peer_dependencies_meta - .get(entry.0) - .map(|d| d.optional) - .unwrap_or(false); - let kind = match is_optional { - true => NpmDependencyEntryKind::OptionalPeer, - false => NpmDependencyEntryKind::Peer, - }; - let entry = parse_dep_entry(entry, kind)?; - result.insert(entry.bare_specifier.clone(), entry); - } - for entry in &self.dependencies { - let entry = parse_dep_entry(entry, NpmDependencyEntryKind::Dep)?; - // people may define a dependency as a peer dependency as well, - // so in those cases, attempt to resolve as a peer dependency, - // but then use this dependency version requirement otherwise - if let Some(peer_dep_entry) = result.get_mut(&entry.bare_specifier) { - peer_dep_entry.peer_dep_version_req = Some(entry.version_req); - } else { - result.insert(entry.bare_specifier.clone(), entry); - } - } - Ok(result.into_values().collect()) - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct NpmPackageVersionDistInfo { - /// URL to the tarball. - pub tarball: String, - shasum: String, - integrity: Option<String>, -} - -impl NpmPackageVersionDistInfo { - pub fn integrity(&self) -> Cow<String> { - self - .integrity - .as_ref() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(format!("sha1-{}", self.shasum))) - } -} - static NPM_REGISTRY_DEFAULT_URL: Lazy<Url> = Lazy::new(|| { let env_var_name = "NPM_CONFIG_REGISTRY"; if let Ok(registry_url) = std::env::var(env_var_name) { @@ -209,9 +51,9 @@ static NPM_REGISTRY_DEFAULT_URL: Lazy<Url> = Lazy::new(|| { }); #[derive(Clone, Debug)] -pub struct NpmRegistryApi(Arc<dyn NpmRegistryApiInner>); +pub struct NpmRegistry(Option<Arc<NpmRegistryApiInner>>); -impl NpmRegistryApi { +impl NpmRegistry { pub fn default_url() -> &'static Url { &NPM_REGISTRY_DEFAULT_URL } @@ -222,188 +64,142 @@ impl NpmRegistryApi { http_client: HttpClient, progress_bar: ProgressBar, ) -> Self { - Self(Arc::new(RealNpmRegistryApiInner { + Self(Some(Arc::new(NpmRegistryApiInner { base_url, cache, mem_cache: Default::default(), previously_reloaded_packages: Default::default(), http_client, progress_bar, - })) + }))) } - /// Creates an npm registry API that will be uninitialized - /// and error for every request. This is useful for tests - /// or for initializing the LSP. + /// Creates an npm registry API that will be uninitialized. This is + /// useful for tests or for initializing the LSP. pub fn new_uninitialized() -> Self { - Self(Arc::new(NullNpmRegistryApiInner)) - } - - #[cfg(test)] - pub fn new_for_test(api: TestNpmRegistryApiInner) -> NpmRegistryApi { - Self(Arc::new(api)) - } - - pub async fn package_info( - &self, - name: &str, - ) -> Result<Arc<NpmPackageInfo>, AnyError> { - let maybe_package_info = self.0.maybe_package_info(name).await?; - match maybe_package_info { - Some(package_info) => Ok(package_info), - None => bail!("npm package '{}' does not exist", name), - } - } - - pub async fn package_version_info( - &self, - nv: &NpmPackageNv, - ) -> Result<Option<NpmPackageVersionInfo>, AnyError> { - let package_info = self.package_info(&nv.name).await?; - Ok(package_info.versions.get(&nv.version.to_string()).cloned()) - } - - /// Caches all the package information in memory in parallel. - pub async fn cache_in_parallel( - &self, - package_names: Vec<String>, - ) -> Result<(), AnyError> { - let mut unresolved_tasks = Vec::with_capacity(package_names.len()); - - // cache the package info up front in parallel - if should_sync_download() { - // for deterministic test output - let mut ordered_names = package_names; - ordered_names.sort(); - for name in ordered_names { - self.package_info(&name).await?; - } - } else { - for name in package_names { - let api = self.clone(); - unresolved_tasks.push(tokio::task::spawn(async move { - // This is ok to call because api will internally cache - // the package information in memory. - api.package_info(&name).await - })); - } - }; - - for result in futures::future::join_all(unresolved_tasks).await { - result??; // surface the first error - } - - Ok(()) + Self(None) } /// Clears the internal memory cache. pub fn clear_memory_cache(&self) { - self.0.clear_memory_cache(); + self.inner().clear_memory_cache(); } pub fn get_cached_package_info( &self, name: &str, ) -> Option<Arc<NpmPackageInfo>> { - self.0.get_cached_package_info(name) + self.inner().get_cached_package_info(name) } pub fn base_url(&self) -> &Url { - self.0.base_url() + &self.inner().base_url } -} - -#[async_trait] -trait NpmRegistryApiInner: std::fmt::Debug + Sync + Send + 'static { - async fn maybe_package_info( - &self, - name: &str, - ) -> Result<Option<Arc<NpmPackageInfo>>, AnyError>; - fn clear_memory_cache(&self); - - fn get_cached_package_info(&self, name: &str) -> Option<Arc<NpmPackageInfo>>; - - fn base_url(&self) -> &Url; + fn inner(&self) -> &Arc<NpmRegistryApiInner> { + // this panicking indicates a bug in the code where this + // wasn't initialized + self.0.as_ref().unwrap() + } } -#[async_trait] -impl NpmRegistryApiInner for RealNpmRegistryApiInner { - fn base_url(&self) -> &Url { - &self.base_url - } +static SYNC_DOWNLOAD_TASK_QUEUE: Lazy<TaskQueue> = + Lazy::new(TaskQueue::default); +#[async_trait] +impl NpmRegistryApi for NpmRegistry { async fn maybe_package_info( &self, name: &str, ) -> Result<Option<Arc<NpmPackageInfo>>, AnyError> { - self.maybe_package_info(name).await - } - - fn clear_memory_cache(&self) { - self.mem_cache.lock().clear(); + if should_sync_download() { + let inner = self.inner().clone(); + SYNC_DOWNLOAD_TASK_QUEUE + .queue(async move { inner.maybe_package_info(name).await }) + .await + } else { + self.inner().maybe_package_info(name).await + } } +} - fn get_cached_package_info(&self, name: &str) -> Option<Arc<NpmPackageInfo>> { - self.mem_cache.lock().get(name).cloned().flatten() - } +#[derive(Debug)] +enum CacheItem { + Pending( + Shared<BoxFuture<'static, Result<Option<Arc<NpmPackageInfo>>, String>>>, + ), + Resolved(Option<Arc<NpmPackageInfo>>), } #[derive(Debug)] -struct RealNpmRegistryApiInner { +struct NpmRegistryApiInner { base_url: Url, cache: NpmCache, - mem_cache: Mutex<HashMap<String, Option<Arc<NpmPackageInfo>>>>, + mem_cache: Mutex<HashMap<String, CacheItem>>, previously_reloaded_packages: Mutex<HashSet<String>>, http_client: HttpClient, progress_bar: ProgressBar, } -impl RealNpmRegistryApiInner { +impl NpmRegistryApiInner { pub async fn maybe_package_info( - &self, + self: &Arc<Self>, name: &str, ) -> Result<Option<Arc<NpmPackageInfo>>, AnyError> { - let maybe_maybe_info = self.mem_cache.lock().get(name).cloned(); - if let Some(maybe_info) = maybe_maybe_info { - Ok(maybe_info) - } else { - let mut maybe_package_info = None; - if self.cache.cache_setting().should_use_for_npm_package(name) + let (created, future) = { + let mut mem_cache = self.mem_cache.lock(); + match mem_cache.get(name) { + Some(CacheItem::Resolved(maybe_info)) => { + return Ok(maybe_info.clone()); + } + Some(CacheItem::Pending(future)) => (false, future.clone()), + None => { + if self.cache.cache_setting().should_use_for_npm_package(name) // if this has been previously reloaded, then try loading from the // file system cache || !self.previously_reloaded_packages.lock().insert(name.to_string()) - { - // attempt to load from the file cache - maybe_package_info = self.load_file_cached_package_info(name); - } - - if maybe_package_info.is_none() { - maybe_package_info = self - .load_package_info_from_registry(name) - .await - .with_context(|| { - format!( - "Error getting response at {} for package \"{}\"", - self.get_package_url(name), - name - ) - })?; + { + // attempt to load from the file cache + if let Some(info) = self.load_file_cached_package_info(name) { + let result = Some(Arc::new(info)); + mem_cache + .insert(name.to_string(), CacheItem::Resolved(result.clone())); + return Ok(result); + } + } + + let future = { + let api = self.clone(); + let name = name.to_string(); + async move { api.load_package_info_from_registry(&name).await } + .boxed() + .shared() + }; + mem_cache + .insert(name.to_string(), CacheItem::Pending(future.clone())); + (true, future) + } } - let maybe_package_info = maybe_package_info.map(Arc::new); + }; - // Not worth the complexity to ensure multiple in-flight requests - // for the same package only request once because with how this is - // used that should never happen. - let mut mem_cache = self.mem_cache.lock(); - Ok(match mem_cache.get(name) { - // another thread raced here, so use its result instead - Some(info) => info.clone(), - None => { - mem_cache.insert(name.to_string(), maybe_package_info.clone()); - maybe_package_info + if created { + match future.await { + Ok(maybe_info) => { + // replace the cache item to say it's resolved now + self + .mem_cache + .lock() + .insert(name.to_string(), CacheItem::Resolved(maybe_info.clone())); + Ok(maybe_info) } - }) + Err(err) => { + // purge the item from the cache so it loads next time + self.mem_cache.lock().remove(name); + Err(anyhow!("{}", err)) + } + } + } else { + Ok(future.await.map_err(|err| anyhow!("{}", err))?) } } @@ -478,6 +274,25 @@ impl RealNpmRegistryApiInner { async fn load_package_info_from_registry( &self, name: &str, + ) -> Result<Option<Arc<NpmPackageInfo>>, String> { + self + .load_package_info_from_registry_inner(name) + .await + .with_context(|| { + format!( + "Error getting response at {} for package \"{}\"", + self.get_package_url(name), + name + ) + }) + .map(|info| info.map(Arc::new)) + // make cloneable + .map_err(|err| format!("{err:#}")) + } + + async fn load_package_info_from_registry_inner( + &self, + name: &str, ) -> Result<Option<NpmPackageInfo>, AnyError> { if *self.cache.cache_setting() == CacheSetting::Only { return Err(custom_error( @@ -513,206 +328,20 @@ impl RealNpmRegistryApiInner { let name_folder_path = self.cache.package_name_folder(name, &self.base_url); name_folder_path.join("registry.json") } -} - -#[derive(Debug)] -struct NullNpmRegistryApiInner; - -#[async_trait] -impl NpmRegistryApiInner for NullNpmRegistryApiInner { - async fn maybe_package_info( - &self, - _name: &str, - ) -> Result<Option<Arc<NpmPackageInfo>>, AnyError> { - Err(deno_core::anyhow::anyhow!( - "Deno bug. Please report. Registry API was not initialized." - )) - } - - fn clear_memory_cache(&self) {} - - fn get_cached_package_info( - &self, - _name: &str, - ) -> Option<Arc<NpmPackageInfo>> { - None - } - - fn base_url(&self) -> &Url { - NpmRegistryApi::default_url() - } -} - -/// Note: This test struct is not thread safe for setup -/// purposes. Construct everything on the same thread. -#[cfg(test)] -#[derive(Clone, Default, Debug)] -pub struct TestNpmRegistryApiInner { - package_infos: Arc<Mutex<HashMap<String, NpmPackageInfo>>>, -} - -#[cfg(test)] -impl TestNpmRegistryApiInner { - pub fn add_package_info(&self, name: &str, info: NpmPackageInfo) { - let previous = self.package_infos.lock().insert(name.to_string(), info); - assert!(previous.is_none()); - } - - pub fn ensure_package(&self, name: &str) { - if !self.package_infos.lock().contains_key(name) { - self.add_package_info( - name, - NpmPackageInfo { - name: name.to_string(), - ..Default::default() - }, - ); - } - } - pub fn ensure_package_version(&self, name: &str, version: &str) { - self.ensure_package(name); - let mut infos = self.package_infos.lock(); - let info = infos.get_mut(name).unwrap(); - if !info.versions.contains_key(version) { - info.versions.insert( - version.to_string(), - NpmPackageVersionInfo { - version: version.to_string(), - ..Default::default() - }, - ); - } - } - - pub fn add_dependency( - &self, - package_from: (&str, &str), - package_to: (&str, &str), - ) { - let mut infos = self.package_infos.lock(); - let info = infos.get_mut(package_from.0).unwrap(); - let version = info.versions.get_mut(package_from.1).unwrap(); - version - .dependencies - .insert(package_to.0.to_string(), package_to.1.to_string()); - } - - pub fn add_dist_tag(&self, package_name: &str, tag: &str, version: &str) { - let mut infos = self.package_infos.lock(); - let info = infos.get_mut(package_name).unwrap(); - info.dist_tags.insert(tag.to_string(), version.to_string()); - } - - pub fn add_peer_dependency( - &self, - package_from: (&str, &str), - package_to: (&str, &str), - ) { - let mut infos = self.package_infos.lock(); - let info = infos.get_mut(package_from.0).unwrap(); - let version = info.versions.get_mut(package_from.1).unwrap(); - version - .peer_dependencies - .insert(package_to.0.to_string(), package_to.1.to_string()); - } - - pub fn add_optional_peer_dependency( - &self, - package_from: (&str, &str), - package_to: (&str, &str), - ) { - let mut infos = self.package_infos.lock(); - let info = infos.get_mut(package_from.0).unwrap(); - let version = info.versions.get_mut(package_from.1).unwrap(); - version - .peer_dependencies - .insert(package_to.0.to_string(), package_to.1.to_string()); - version.peer_dependencies_meta.insert( - package_to.0.to_string(), - NpmPeerDependencyMeta { optional: true }, - ); + pub fn clear_memory_cache(&self) { + self.mem_cache.lock().clear(); } -} -#[cfg(test)] -#[async_trait] -impl NpmRegistryApiInner for TestNpmRegistryApiInner { - async fn maybe_package_info( + pub fn get_cached_package_info( &self, name: &str, - ) -> Result<Option<Arc<NpmPackageInfo>>, AnyError> { - let result = self.package_infos.lock().get(name).cloned(); - Ok(result.map(Arc::new)) - } - - fn clear_memory_cache(&self) { - // do nothing for the test api - } - - fn get_cached_package_info( - &self, - _name: &str, ) -> Option<Arc<NpmPackageInfo>> { - None - } - - fn base_url(&self) -> &Url { - NpmRegistryApi::default_url() - } -} - -#[cfg(test)] -mod test { - use std::collections::HashMap; - - use deno_core::serde_json; - - use crate::npm::registry::NpmPackageVersionBinEntry; - use crate::npm::NpmPackageVersionDistInfo; - - use super::NpmPackageVersionInfo; - - #[test] - fn deserializes_minimal_pkg_info() { - let text = r#"{ "version": "1.0.0", "dist": { "tarball": "value", "shasum": "test" } }"#; - let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); - assert_eq!( - info, - NpmPackageVersionInfo { - version: "1.0.0".to_string(), - dist: NpmPackageVersionDistInfo { - tarball: "value".to_string(), - shasum: "test".to_string(), - integrity: None, - }, - bin: None, - dependencies: Default::default(), - peer_dependencies: Default::default(), - peer_dependencies_meta: Default::default() - } - ); - } - - #[test] - fn deserializes_bin_entry() { - // string - let text = r#"{ "version": "1.0.0", "bin": "bin-value", "dist": { "tarball": "value", "shasum": "test" } }"#; - let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); - assert_eq!( - info.bin, - Some(NpmPackageVersionBinEntry::String("bin-value".to_string())) - ); - - // map - let text = r#"{ "version": "1.0.0", "bin": { "a": "a-value", "b": "b-value" }, "dist": { "tarball": "value", "shasum": "test" } }"#; - let info: NpmPackageVersionInfo = serde_json::from_str(text).unwrap(); - assert_eq!( - info.bin, - Some(NpmPackageVersionBinEntry::Map(HashMap::from([ - ("a".to_string(), "a-value".to_string()), - ("b".to_string(), "b-value".to_string()), - ]))) - ); + let mem_cache = self.mem_cache.lock(); + if let Some(CacheItem::Resolved(maybe_info)) = mem_cache.get(name) { + maybe_info.clone() + } else { + None + } } } diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs new file mode 100644 index 000000000..291acf4bc --- /dev/null +++ b/cli/npm/resolution.rs @@ -0,0 +1,312 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashSet; +use std::sync::Arc; + +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::parking_lot::RwLock; +use deno_core::TaskQueue; +use deno_lockfile::NpmPackageDependencyLockfileInfo; +use deno_lockfile::NpmPackageLockfileInfo; +use deno_npm::registry::NpmPackageInfo; +use deno_npm::resolution::NpmPackagesPartitioned; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::NpmPackageCacheFolderId; +use deno_npm::NpmPackageId; +use deno_npm::NpmResolutionPackage; +use deno_semver::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageNvReference; +use deno_semver::npm::NpmPackageReq; +use deno_semver::npm::NpmPackageReqReference; + +use crate::args::Lockfile; + +use super::registry::NpmRegistry; + +/// Handles updating and storing npm resolution in memory where the underlying +/// snapshot can be updated concurrently. Additionally handles updating the lockfile +/// based on changes to the resolution. +/// +/// This does not interact with the file system. +#[derive(Clone)] +pub struct NpmResolution(Arc<NpmResolutionInner>); + +struct NpmResolutionInner { + api: NpmRegistry, + snapshot: RwLock<NpmResolutionSnapshot>, + update_queue: TaskQueue, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, +} + +impl std::fmt::Debug for NpmResolution { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let snapshot = self.0.snapshot.read(); + f.debug_struct("NpmResolution") + .field("snapshot", &snapshot) + .finish() + } +} + +impl NpmResolution { + pub fn new( + api: NpmRegistry, + initial_snapshot: Option<NpmResolutionSnapshot>, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, + ) -> Self { + Self(Arc::new(NpmResolutionInner { + api, + snapshot: RwLock::new(initial_snapshot.unwrap_or_default()), + update_queue: Default::default(), + maybe_lockfile, + })) + } + + pub async fn add_package_reqs( + &self, + package_reqs: Vec<NpmPackageReq>, + ) -> Result<(), AnyError> { + let inner = &self.0; + + // only allow one thread in here at a time + let _permit = inner.update_queue.acquire().await; + let snapshot = inner.snapshot.read().clone(); + + let snapshot = add_package_reqs_to_snapshot( + &inner.api, + package_reqs, + snapshot, + self.0.maybe_lockfile.clone(), + ) + .await?; + + *inner.snapshot.write() = snapshot; + Ok(()) + } + + pub async fn set_package_reqs( + &self, + package_reqs: Vec<NpmPackageReq>, + ) -> Result<(), AnyError> { + let inner = &self.0; + // only allow one thread in here at a time + let _permit = inner.update_queue.acquire().await; + let snapshot = inner.snapshot.read().clone(); + + let reqs_set = package_reqs.iter().collect::<HashSet<_>>(); + let has_removed_package = !snapshot + .package_reqs() + .keys() + .all(|req| reqs_set.contains(req)); + // if any packages were removed, we need to completely recreate the npm resolution snapshot + let snapshot = if has_removed_package { + NpmResolutionSnapshot::default() + } else { + snapshot + }; + let snapshot = add_package_reqs_to_snapshot( + &inner.api, + package_reqs, + snapshot, + self.0.maybe_lockfile.clone(), + ) + .await?; + + *inner.snapshot.write() = snapshot; + + Ok(()) + } + + pub async fn resolve_pending(&self) -> Result<(), AnyError> { + let inner = &self.0; + // only allow one thread in here at a time + let _permit = inner.update_queue.acquire().await; + let snapshot = inner.snapshot.read().clone(); + + let snapshot = add_package_reqs_to_snapshot( + &inner.api, + Vec::new(), + snapshot, + self.0.maybe_lockfile.clone(), + ) + .await?; + + *inner.snapshot.write() = snapshot; + + Ok(()) + } + + pub fn pkg_req_ref_to_nv_ref( + &self, + req_ref: NpmPackageReqReference, + ) -> Result<NpmPackageNvReference, AnyError> { + let node_id = self.resolve_pkg_id_from_pkg_req(&req_ref.req)?; + Ok(NpmPackageNvReference { + nv: node_id.nv, + sub_path: req_ref.sub_path, + }) + } + + pub fn resolve_package_cache_folder_id_from_id( + &self, + id: &NpmPackageId, + ) -> Option<NpmPackageCacheFolderId> { + self + .0 + .snapshot + .read() + .package_from_id(id) + .map(|p| p.get_package_cache_folder_id()) + } + + pub fn resolve_package_from_package( + &self, + name: &str, + referrer: &NpmPackageCacheFolderId, + ) -> Result<NpmResolutionPackage, AnyError> { + self + .0 + .snapshot + .read() + .resolve_package_from_package(name, referrer) + .cloned() + } + + /// Resolve a node package from a deno module. + pub fn resolve_pkg_id_from_pkg_req( + &self, + req: &NpmPackageReq, + ) -> Result<NpmPackageId, AnyError> { + self + .0 + .snapshot + .read() + .resolve_pkg_from_pkg_req(req) + .map(|pkg| pkg.pkg_id.clone()) + } + + pub fn resolve_pkg_id_from_deno_module( + &self, + id: &NpmPackageNv, + ) -> Result<NpmPackageId, AnyError> { + self + .0 + .snapshot + .read() + .resolve_package_from_deno_module(id) + .map(|pkg| pkg.pkg_id.clone()) + } + + /// Resolves a package requirement for deno graph. This should only be + /// called by deno_graph's NpmResolver or for resolving packages in + /// a package.json + pub fn resolve_package_req_as_pending( + &self, + pkg_req: &NpmPackageReq, + ) -> Result<NpmPackageNv, AnyError> { + // we should always have this because it should have been cached before here + let package_info = + self.0.api.get_cached_package_info(&pkg_req.name).unwrap(); + self.resolve_package_req_as_pending_with_info(pkg_req, &package_info) + } + + /// Resolves a package requirement for deno graph. This should only be + /// called by deno_graph's NpmResolver or for resolving packages in + /// a package.json + pub fn resolve_package_req_as_pending_with_info( + &self, + pkg_req: &NpmPackageReq, + package_info: &NpmPackageInfo, + ) -> Result<NpmPackageNv, AnyError> { + debug_assert_eq!(pkg_req.name, package_info.name); + let inner = &self.0; + let mut snapshot = inner.snapshot.write(); + let nv = snapshot.resolve_package_req_as_pending(pkg_req, package_info)?; + Ok(nv) + } + + pub fn all_packages_partitioned(&self) -> NpmPackagesPartitioned { + self.0.snapshot.read().all_packages_partitioned() + } + + pub fn has_packages(&self) -> bool { + !self.0.snapshot.read().is_empty() + } + + pub fn snapshot(&self) -> NpmResolutionSnapshot { + self.0.snapshot.read().clone() + } + + pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { + let snapshot = self.0.snapshot.read(); + populate_lockfile_from_snapshot(lockfile, &snapshot) + } +} + +async fn add_package_reqs_to_snapshot( + api: &NpmRegistry, + package_reqs: Vec<NpmPackageReq>, + snapshot: NpmResolutionSnapshot, + maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, +) -> Result<NpmResolutionSnapshot, AnyError> { + if !snapshot.has_pending() + && package_reqs + .iter() + .all(|req| snapshot.package_reqs().contains_key(req)) + { + return Ok(snapshot); // already up to date + } + + let result = snapshot.resolve_pending(package_reqs, api).await; + api.clear_memory_cache(); + let snapshot = result?; // propagate the error after clearing the memory cache + + if let Some(lockfile_mutex) = maybe_lockfile { + let mut lockfile = lockfile_mutex.lock(); + populate_lockfile_from_snapshot(&mut lockfile, &snapshot)?; + Ok(snapshot) + } else { + Ok(snapshot) + } +} + +fn populate_lockfile_from_snapshot( + lockfile: &mut Lockfile, + snapshot: &NpmResolutionSnapshot, +) -> Result<(), AnyError> { + for (package_req, nv) in snapshot.package_reqs() { + lockfile.insert_npm_specifier( + package_req.to_string(), + snapshot + .resolve_package_from_deno_module(nv) + .unwrap() + .pkg_id + .as_serialized(), + ); + } + for package in snapshot.all_packages() { + lockfile + .check_or_insert_npm_package(npm_package_to_lockfile_info(package))?; + } + Ok(()) +} + +fn npm_package_to_lockfile_info( + pkg: NpmResolutionPackage, +) -> NpmPackageLockfileInfo { + let dependencies = pkg + .dependencies + .into_iter() + .map(|(name, id)| NpmPackageDependencyLockfileInfo { + name, + id: id.as_serialized(), + }) + .collect(); + + NpmPackageLockfileInfo { + display_id: pkg.pkg_id.nv.to_string(), + serialized_id: pkg.pkg_id.as_serialized(), + integrity: pkg.dist.integrity().to_string(), + dependencies, + } +} diff --git a/cli/npm/resolution/common.rs b/cli/npm/resolution/common.rs deleted file mode 100644 index eb3e51f7d..000000000 --- a/cli/npm/resolution/common.rs +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use deno_core::anyhow::bail; -use deno_core::error::AnyError; -use deno_graph::semver::Version; -use deno_graph::semver::VersionReq; -use once_cell::sync::Lazy; - -use super::NpmPackageId; -use crate::npm::registry::NpmPackageInfo; -use crate::npm::registry::NpmPackageVersionInfo; - -pub static LATEST_VERSION_REQ: Lazy<VersionReq> = - Lazy::new(|| VersionReq::parse_from_specifier("latest").unwrap()); - -pub fn resolve_best_package_version_and_info<'info, 'version>( - version_req: &VersionReq, - package_info: &'info NpmPackageInfo, - existing_versions: impl Iterator<Item = &'version Version>, -) -> Result<VersionAndInfo<'info>, AnyError> { - if let Some(version) = resolve_best_from_existing_versions( - version_req, - package_info, - existing_versions, - )? { - match package_info.versions.get(&version.to_string()) { - Some(version_info) => Ok(VersionAndInfo { - version, - info: version_info, - }), - None => { - bail!( - "could not find version '{}' for '{}'", - version, - &package_info.name - ) - } - } - } else { - // get the information - get_resolved_package_version_and_info(version_req, package_info, None) - } -} - -#[derive(Clone)] -pub struct VersionAndInfo<'a> { - pub version: Version, - pub info: &'a NpmPackageVersionInfo, -} - -fn get_resolved_package_version_and_info<'a>( - version_req: &VersionReq, - info: &'a NpmPackageInfo, - parent: Option<&NpmPackageId>, -) -> Result<VersionAndInfo<'a>, AnyError> { - if let Some(tag) = version_req.tag() { - tag_to_version_info(info, tag, parent) - } else { - let mut maybe_best_version: Option<VersionAndInfo> = None; - for version_info in info.versions.values() { - let version = Version::parse_from_npm(&version_info.version)?; - if version_req.matches(&version) { - let is_best_version = maybe_best_version - .as_ref() - .map(|best_version| best_version.version.cmp(&version).is_lt()) - .unwrap_or(true); - if is_best_version { - maybe_best_version = Some(VersionAndInfo { - version, - info: version_info, - }); - } - } - } - - match maybe_best_version { - Some(v) => Ok(v), - // If the package isn't found, it likely means that the user needs to use - // `--reload` to get the latest npm package information. Although it seems - // like we could make this smart by fetching the latest information for - // this package here, we really need a full restart. There could be very - // interesting bugs that occur if this package's version was resolved by - // something previous using the old information, then now being smart here - // causes a new fetch of the package information, meaning this time the - // previous resolution of this package's version resolved to an older - // version, but next time to a different version because it has new information. - None => bail!( - concat!( - "Could not find npm package '{}' matching {}{}. ", - "Try retrieving the latest npm package information by running with --reload", - ), - info.name, - version_req.version_text(), - match parent { - Some(resolved_id) => format!(" as specified in {}", resolved_id.nv), - None => String::new(), - } - ), - } - } -} - -pub fn version_req_satisfies( - version_req: &VersionReq, - version: &Version, - package_info: &NpmPackageInfo, - parent: Option<&NpmPackageId>, -) -> Result<bool, AnyError> { - match version_req.tag() { - Some(tag) => { - let tag_version = tag_to_version_info(package_info, tag, parent)?.version; - Ok(tag_version == *version) - } - None => Ok(version_req.matches(version)), - } -} - -fn resolve_best_from_existing_versions<'a>( - version_req: &VersionReq, - package_info: &NpmPackageInfo, - existing_versions: impl Iterator<Item = &'a Version>, -) -> Result<Option<Version>, AnyError> { - let mut maybe_best_version: Option<&Version> = None; - for version in existing_versions { - if version_req_satisfies(version_req, version, package_info, None)? { - let is_best_version = maybe_best_version - .as_ref() - .map(|best_version| (*best_version).cmp(version).is_lt()) - .unwrap_or(true); - if is_best_version { - maybe_best_version = Some(version); - } - } - } - Ok(maybe_best_version.cloned()) -} - -fn tag_to_version_info<'a>( - info: &'a NpmPackageInfo, - tag: &str, - parent: Option<&NpmPackageId>, -) -> Result<VersionAndInfo<'a>, AnyError> { - // For when someone just specifies @types/node, we want to pull in a - // "known good" version of @types/node that works well with Deno and - // not necessarily the latest version. For example, we might only be - // compatible with Node vX, but then Node vY is published so we wouldn't - // want to pull that in. - // Note: If the user doesn't want this behavior, then they can specify an - // explicit version. - if tag == "latest" && info.name == "@types/node" { - return get_resolved_package_version_and_info( - // WARNING: When bumping this version, check if anything needs to be - // updated in the `setNodeOnlyGlobalNames` call in 99_main_compiler.js - &VersionReq::parse_from_npm("18.0.0 - 18.11.18").unwrap(), - info, - parent, - ); - } - - if let Some(version) = info.dist_tags.get(tag) { - match info.versions.get(version) { - Some(info) => Ok(VersionAndInfo { - version: Version::parse_from_npm(version)?, - info, - }), - None => { - bail!( - "Could not find version '{}' referenced in dist-tag '{}'.", - version, - tag, - ) - } - } - } else { - bail!("Could not find dist-tag '{}'.", tag) - } -} - -#[cfg(test)] -mod test { - use std::collections::HashMap; - - use deno_graph::npm::NpmPackageReqReference; - - use super::*; - - #[test] - fn test_get_resolved_package_version_and_info() { - // dist tag where version doesn't exist - let package_ref = NpmPackageReqReference::from_str("npm:test").unwrap(); - let package_info = NpmPackageInfo { - name: "test".to_string(), - versions: HashMap::new(), - dist_tags: HashMap::from([( - "latest".to_string(), - "1.0.0-alpha".to_string(), - )]), - }; - let result = get_resolved_package_version_and_info( - package_ref - .req - .version_req - .as_ref() - .unwrap_or(&*LATEST_VERSION_REQ), - &package_info, - None, - ); - assert_eq!( - result.err().unwrap().to_string(), - "Could not find version '1.0.0-alpha' referenced in dist-tag 'latest'." - ); - - // dist tag where version is a pre-release - let package_ref = NpmPackageReqReference::from_str("npm:test").unwrap(); - let package_info = NpmPackageInfo { - name: "test".to_string(), - versions: HashMap::from([ - ("0.1.0".to_string(), NpmPackageVersionInfo::default()), - ( - "1.0.0-alpha".to_string(), - NpmPackageVersionInfo { - version: "0.1.0-alpha".to_string(), - ..Default::default() - }, - ), - ]), - dist_tags: HashMap::from([( - "latest".to_string(), - "1.0.0-alpha".to_string(), - )]), - }; - let result = get_resolved_package_version_and_info( - package_ref - .req - .version_req - .as_ref() - .unwrap_or(&*LATEST_VERSION_REQ), - &package_info, - None, - ); - assert_eq!(result.unwrap().version.to_string(), "1.0.0-alpha"); - } -} diff --git a/cli/npm/resolution/graph.rs b/cli/npm/resolution/graph.rs deleted file mode 100644 index 65754f3bf..000000000 --- a/cli/npm/resolution/graph.rs +++ /dev/null @@ -1,3726 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::collections::hash_map::DefaultHasher; -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::collections::HashSet; -use std::collections::VecDeque; -use std::hash::Hash; -use std::hash::Hasher; -use std::sync::Arc; - -use deno_core::anyhow::bail; -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageReq; -use deno_graph::semver::Version; -use deno_graph::semver::VersionReq; -use log::debug; - -use crate::npm::registry::NpmDependencyEntry; -use crate::npm::registry::NpmDependencyEntryKind; -use crate::npm::registry::NpmPackageInfo; -use crate::npm::registry::NpmPackageVersionInfo; -use crate::npm::resolution::common::resolve_best_package_version_and_info; -use crate::npm::resolution::snapshot::SnapshotPackageCopyIndexResolver; -use crate::npm::NpmRegistryApi; - -use super::common::version_req_satisfies; -use super::common::LATEST_VERSION_REQ; -use super::snapshot::NpmResolutionSnapshot; -use super::NpmPackageId; -use super::NpmResolutionPackage; - -// todo(dsherret): for perf we should use an arena/bump allocator for -// creating the nodes and paths since this is done in a phase - -/// A unique identifier to a node in the graph. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -struct NodeId(u32); - -/// A resolved package in the resolution graph. -#[derive(Debug)] -struct Node { - /// The specifier to child relationship in the graph. The specifier is - /// the key in an npm package's dependencies map (ex. "express"). We - /// use a BTreeMap for some determinism when creating the snapshot. - /// - /// Note: We don't want to store the children as a `NodeRef` because - /// multiple paths might visit through the children and we don't want - /// to share those references with those paths. - pub children: BTreeMap<String, NodeId>, - /// Whether the node has demonstrated to have no peer dependencies in its - /// descendants. If this is true then we can skip analyzing this node - /// again when we encounter it another time in the dependency tree, which - /// is much faster. - pub no_peers: bool, -} - -#[derive(Clone)] -enum ResolvedIdPeerDep { - /// This is a reference to the parent instead of the child because we only have a - /// node reference to the parent, since we've traversed it, but the child node may - /// change from under it. - ParentReference { - parent: GraphPathNodeOrRoot, - child_pkg_nv: Arc<NpmPackageNv>, - }, - /// A node that was created during snapshotting and is not being used in any path. - SnapshotNodeId(NodeId), -} - -impl ResolvedIdPeerDep { - pub fn current_state_hash(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.current_state_hash_with_hasher(&mut hasher); - hasher.finish() - } - - pub fn current_state_hash_with_hasher(&self, hasher: &mut DefaultHasher) { - match self { - ResolvedIdPeerDep::ParentReference { - parent, - child_pkg_nv, - } => { - match parent { - GraphPathNodeOrRoot::Root(root) => root.hash(hasher), - GraphPathNodeOrRoot::Node(node) => node.node_id().hash(hasher), - } - child_pkg_nv.hash(hasher); - } - ResolvedIdPeerDep::SnapshotNodeId(node_id) => { - node_id.hash(hasher); - } - } - } -} - -/// A pending resolved identifier used in the graph. At the end of resolution, these -/// will become fully resolved to an `NpmPackageId`. -#[derive(Clone)] -struct ResolvedId { - nv: Arc<NpmPackageNv>, - peer_dependencies: Vec<ResolvedIdPeerDep>, -} - -impl ResolvedId { - /// Gets a hash of the resolved identifier at this current moment in time. - /// - /// WARNING: A resolved identifier references a value that could change in - /// the future, so this should be used with that in mind. - pub fn current_state_hash(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.nv.hash(&mut hasher); - for dep in &self.peer_dependencies { - dep.current_state_hash_with_hasher(&mut hasher); - } - hasher.finish() - } - - pub fn push_peer_dep(&mut self, peer_dep: ResolvedIdPeerDep) -> bool { - let new_hash = peer_dep.current_state_hash(); - for dep in &self.peer_dependencies { - if new_hash == dep.current_state_hash() { - return false; // peer dep already set - } - } - self.peer_dependencies.push(peer_dep); - true - } -} - -/// Mappings of node identifiers to resolved identifiers. Each node has exactly -/// one resolved identifier. -/// -/// The mapping from resolved to node_ids is imprecise and will do a best attempt -/// at sharing nodes. -#[derive(Default)] -struct ResolvedNodeIds { - node_to_resolved_id: HashMap<NodeId, (ResolvedId, u64)>, - resolved_to_node_id: HashMap<u64, NodeId>, -} - -impl ResolvedNodeIds { - pub fn set(&mut self, node_id: NodeId, resolved_id: ResolvedId) { - let resolved_id_hash = resolved_id.current_state_hash(); - if let Some((_, old_resolved_id_key)) = self - .node_to_resolved_id - .insert(node_id, (resolved_id, resolved_id_hash)) - { - // ensure the old resolved id key is removed as it might be stale - self.resolved_to_node_id.remove(&old_resolved_id_key); - } - self.resolved_to_node_id.insert(resolved_id_hash, node_id); - } - - pub fn get(&self, node_id: NodeId) -> Option<&ResolvedId> { - self.node_to_resolved_id.get(&node_id).map(|(id, _)| id) - } - - pub fn get_node_id(&self, resolved_id: &ResolvedId) -> Option<NodeId> { - self - .resolved_to_node_id - .get(&resolved_id.current_state_hash()) - .copied() - } -} - -// todo(dsherret): for some reason the lsp errors when using an Rc<RefCell<NodeId>> here -// instead of an Arc<Mutex<NodeId>>. We should investigate and fix. - -/// A pointer to a specific node in a graph path. The underlying node id -/// may change as peer dependencies are created. -#[derive(Clone, Debug)] -struct NodeIdRef(Arc<Mutex<NodeId>>); - -impl NodeIdRef { - pub fn new(node_id: NodeId) -> Self { - NodeIdRef(Arc::new(Mutex::new(node_id))) - } - - pub fn change(&self, node_id: NodeId) { - *self.0.lock() = node_id; - } - - pub fn get(&self) -> NodeId { - *self.0.lock() - } -} - -#[derive(Clone)] -enum GraphPathNodeOrRoot { - Node(Arc<GraphPath>), - Root(Arc<NpmPackageNv>), -} - -/// Path through the graph that represents a traversal through the graph doing -/// the dependency resolution. The graph tries to share duplicate package -/// information and we try to avoid traversing parts of the graph that we know -/// are resolved. -struct GraphPath { - previous_node: Option<GraphPathNodeOrRoot>, - node_id_ref: NodeIdRef, - specifier: String, - // we could consider not storing this here and instead reference the resolved - // nodes, but we should performance profile this code first - nv: Arc<NpmPackageNv>, - /// Descendants in the path that circularly link to an ancestor in a child.These - /// descendants should be kept up to date and always point to this node. - linked_circular_descendants: Mutex<Vec<Arc<GraphPath>>>, -} - -impl GraphPath { - pub fn for_root(node_id: NodeId, nv: Arc<NpmPackageNv>) -> Arc<Self> { - Arc::new(Self { - previous_node: Some(GraphPathNodeOrRoot::Root(nv.clone())), - node_id_ref: NodeIdRef::new(node_id), - // use an empty specifier - specifier: "".to_string(), - nv, - linked_circular_descendants: Default::default(), - }) - } - - pub fn node_id(&self) -> NodeId { - self.node_id_ref.get() - } - - pub fn specifier(&self) -> &str { - &self.specifier - } - - pub fn change_id(&self, node_id: NodeId) { - self.node_id_ref.change(node_id) - } - - pub fn with_id( - self: &Arc<GraphPath>, - node_id: NodeId, - specifier: String, - nv: Arc<NpmPackageNv>, - ) -> Arc<Self> { - Arc::new(Self { - previous_node: Some(GraphPathNodeOrRoot::Node(self.clone())), - node_id_ref: NodeIdRef::new(node_id), - specifier, - nv, - linked_circular_descendants: Default::default(), - }) - } - - /// Gets if there is an ancestor with the same name & version along this path. - pub fn find_ancestor(&self, nv: &NpmPackageNv) -> Option<Arc<GraphPath>> { - let mut maybe_next_node = self.previous_node.as_ref(); - while let Some(GraphPathNodeOrRoot::Node(next_node)) = maybe_next_node { - // we've visited this before, so stop - if *next_node.nv == *nv { - return Some(next_node.clone()); - } - maybe_next_node = next_node.previous_node.as_ref(); - } - None - } - - /// Gets the bottom-up path to the ancestor not including the current or ancestor node. - pub fn get_path_to_ancestor_exclusive( - &self, - ancestor_node_id: NodeId, - ) -> Vec<&Arc<GraphPath>> { - let mut path = Vec::new(); - let mut maybe_next_node = self.previous_node.as_ref(); - while let Some(GraphPathNodeOrRoot::Node(next_node)) = maybe_next_node { - if next_node.node_id() == ancestor_node_id { - break; - } - path.push(next_node); - maybe_next_node = next_node.previous_node.as_ref(); - } - debug_assert!(maybe_next_node.is_some()); - path - } - - pub fn ancestors(&self) -> GraphPathAncestorIterator { - GraphPathAncestorIterator { - next: self.previous_node.as_ref(), - } - } -} - -struct GraphPathAncestorIterator<'a> { - next: Option<&'a GraphPathNodeOrRoot>, -} - -impl<'a> Iterator for GraphPathAncestorIterator<'a> { - type Item = &'a GraphPathNodeOrRoot; - fn next(&mut self) -> Option<Self::Item> { - if let Some(next) = self.next.take() { - if let GraphPathNodeOrRoot::Node(node) = next { - self.next = node.previous_node.as_ref(); - } - Some(next) - } else { - None - } - } -} - -#[derive(Default)] -pub struct Graph { - /// Each requirement is mapped to a specific name and version. - package_reqs: HashMap<NpmPackageReq, Arc<NpmPackageNv>>, - /// Then each name and version is mapped to an exact node id. - /// Note: Uses a BTreeMap in order to create some determinism - /// when creating the snapshot. - root_packages: BTreeMap<Arc<NpmPackageNv>, NodeId>, - package_name_versions: HashMap<String, HashSet<Version>>, - nodes: HashMap<NodeId, Node>, - resolved_node_ids: ResolvedNodeIds, - // This will be set when creating from a snapshot, then - // inform the final snapshot creation. - packages_to_copy_index: HashMap<NpmPackageId, usize>, - /// Packages that the resolver should resolve first. - pending_unresolved_packages: Vec<Arc<NpmPackageNv>>, -} - -impl Graph { - pub fn from_snapshot( - snapshot: NpmResolutionSnapshot, - ) -> Result<Self, AnyError> { - fn get_or_create_graph_node( - graph: &mut Graph, - pkg_id: &NpmPackageId, - packages: &HashMap<NpmPackageId, NpmResolutionPackage>, - created_package_ids: &mut HashMap<NpmPackageId, NodeId>, - ) -> Result<NodeId, AnyError> { - if let Some(id) = created_package_ids.get(pkg_id) { - return Ok(*id); - } - - let node_id = graph.create_node(&pkg_id.nv); - created_package_ids.insert(pkg_id.clone(), node_id); - - let peer_dep_ids = pkg_id - .peer_dependencies - .iter() - .map(|peer_dep| { - Ok(ResolvedIdPeerDep::SnapshotNodeId(get_or_create_graph_node( - graph, - peer_dep, - packages, - created_package_ids, - )?)) - }) - .collect::<Result<Vec<_>, AnyError>>()?; - let graph_resolved_id = ResolvedId { - nv: Arc::new(pkg_id.nv.clone()), - peer_dependencies: peer_dep_ids, - }; - graph.resolved_node_ids.set(node_id, graph_resolved_id); - let resolution = match packages.get(pkg_id) { - Some(resolved_id) => resolved_id, - // maybe the user messed around with the lockfile - None => bail!("not found package: {}", pkg_id.as_serialized()), - }; - for (name, child_id) in &resolution.dependencies { - let child_node_id = get_or_create_graph_node( - graph, - child_id, - packages, - created_package_ids, - )?; - graph.set_child_of_parent_node(node_id, name, child_node_id); - } - Ok(node_id) - } - - let mut graph = Self { - // Note: It might be more correct to store the copy index - // from past resolutions with the node somehow, but maybe not. - packages_to_copy_index: snapshot - .packages - .iter() - .map(|(id, p)| (id.clone(), p.copy_index)) - .collect(), - package_reqs: snapshot - .package_reqs - .into_iter() - .map(|(k, v)| (k, Arc::new(v))) - .collect(), - pending_unresolved_packages: snapshot - .pending_unresolved_packages - .into_iter() - .map(Arc::new) - .collect(), - ..Default::default() - }; - let mut created_package_ids = - HashMap::with_capacity(snapshot.packages.len()); - for (id, resolved_id) in snapshot.root_packages { - let node_id = get_or_create_graph_node( - &mut graph, - &resolved_id, - &snapshot.packages, - &mut created_package_ids, - )?; - graph.root_packages.insert(Arc::new(id), node_id); - } - Ok(graph) - } - - pub fn take_pending_unresolved(&mut self) -> Vec<Arc<NpmPackageNv>> { - std::mem::take(&mut self.pending_unresolved_packages) - } - - pub fn has_package_req(&self, req: &NpmPackageReq) -> bool { - self.package_reqs.contains_key(req) - } - - pub fn has_root_package(&self, id: &NpmPackageNv) -> bool { - self.root_packages.contains_key(id) - } - - fn get_npm_pkg_id(&self, node_id: NodeId) -> NpmPackageId { - let resolved_id = self.resolved_node_ids.get(node_id).unwrap(); - self.get_npm_pkg_id_from_resolved_id(resolved_id, HashSet::new()) - } - - fn get_npm_pkg_id_from_resolved_id( - &self, - resolved_id: &ResolvedId, - seen: HashSet<NodeId>, - ) -> NpmPackageId { - if resolved_id.peer_dependencies.is_empty() { - NpmPackageId { - nv: (*resolved_id.nv).clone(), - peer_dependencies: Vec::new(), - } - } else { - let mut npm_pkg_id = NpmPackageId { - nv: (*resolved_id.nv).clone(), - peer_dependencies: Vec::with_capacity( - resolved_id.peer_dependencies.len(), - ), - }; - let mut seen_children_resolved_ids = - HashSet::with_capacity(resolved_id.peer_dependencies.len()); - for peer_dep in &resolved_id.peer_dependencies { - let maybe_node_and_resolved_id = match peer_dep { - ResolvedIdPeerDep::SnapshotNodeId(node_id) => self - .resolved_node_ids - .get(*node_id) - .map(|resolved_id| (*node_id, resolved_id)), - ResolvedIdPeerDep::ParentReference { - parent, - child_pkg_nv: child_nv, - } => match &parent { - GraphPathNodeOrRoot::Root(_) => { - self.root_packages.get(child_nv).and_then(|node_id| { - self - .resolved_node_ids - .get(*node_id) - .map(|resolved_id| (*node_id, resolved_id)) - }) - } - GraphPathNodeOrRoot::Node(parent_path) => { - self.nodes.get(&parent_path.node_id()).and_then(|parent| { - parent - .children - .values() - .filter_map(|child_id| { - let child_id = *child_id; - self - .resolved_node_ids - .get(child_id) - .map(|resolved_id| (child_id, resolved_id)) - }) - .find(|(_, resolved_id)| resolved_id.nv == *child_nv) - }) - } - }, - }; - // this should always be set - debug_assert!(maybe_node_and_resolved_id.is_some()); - if let Some((child_id, child_resolved_id)) = maybe_node_and_resolved_id - { - let mut new_seen = seen.clone(); - if new_seen.insert(child_id) { - let child_peer = self.get_npm_pkg_id_from_resolved_id( - child_resolved_id, - new_seen.clone(), - ); - - if seen_children_resolved_ids.insert(child_peer.clone()) { - npm_pkg_id.peer_dependencies.push(child_peer); - } - } - } - } - npm_pkg_id - } - } - - fn get_or_create_for_id( - &mut self, - resolved_id: &ResolvedId, - ) -> (bool, NodeId) { - if let Some(node_id) = self.resolved_node_ids.get_node_id(resolved_id) { - return (false, node_id); - } - - let node_id = self.create_node(&resolved_id.nv); - self.resolved_node_ids.set(node_id, resolved_id.clone()); - (true, node_id) - } - - fn create_node(&mut self, pkg_nv: &NpmPackageNv) -> NodeId { - let node_id = NodeId(self.nodes.len() as u32); - let node = Node { - children: Default::default(), - no_peers: false, - }; - - self - .package_name_versions - .entry(pkg_nv.name.clone()) - .or_default() - .insert(pkg_nv.version.clone()); - self.nodes.insert(node_id, node); - - node_id - } - - fn borrow_node_mut(&mut self, node_id: NodeId) -> &mut Node { - self.nodes.get_mut(&node_id).unwrap() - } - - fn set_child_of_parent_node( - &mut self, - parent_id: NodeId, - specifier: &str, - child_id: NodeId, - ) { - assert_ne!(child_id, parent_id); - let parent = self.borrow_node_mut(parent_id); - parent.children.insert(specifier.to_string(), child_id); - } - - pub async fn into_snapshot( - self, - api: &NpmRegistryApi, - ) -> Result<NpmResolutionSnapshot, AnyError> { - let packages_to_pkg_ids = self - .nodes - .keys() - .map(|node_id| (*node_id, self.get_npm_pkg_id(*node_id))) - .collect::<HashMap<_, _>>(); - let mut copy_index_resolver = - SnapshotPackageCopyIndexResolver::from_map_with_capacity( - self.packages_to_copy_index, - self.nodes.len(), - ); - let mut packages = HashMap::with_capacity(self.nodes.len()); - let mut packages_by_name: HashMap<String, Vec<_>> = - HashMap::with_capacity(self.nodes.len()); - - // todo(dsherret): there is a lurking bug within the peer dependencies code. - // You can see it by using `NodeIds` instead of `NpmPackageIds` on this travered_ids - // hashset, which will cause the bottom of the "tree" nodes to be populated in - // the result instead of the top of the "tree". I think there's maybe one small - // thing that's not being updated properly. - let mut traversed_ids = HashSet::with_capacity(self.nodes.len()); - let mut pending = VecDeque::new(); - - for root_id in self.root_packages.values().copied() { - let pkg_id = packages_to_pkg_ids.get(&root_id).unwrap(); - if traversed_ids.insert(pkg_id.clone()) { - pending.push_back((root_id, pkg_id)); - } - } - - while let Some((node_id, pkg_id)) = pending.pop_front() { - let node = self.nodes.get(&node_id).unwrap(); - - packages_by_name - .entry(pkg_id.nv.name.clone()) - .or_default() - .push(pkg_id.clone()); - - // todo(dsherret): grab this from the dep entry cache, which should have it - let dist = api - .package_version_info(&pkg_id.nv) - .await? - .unwrap_or_else(|| panic!("missing: {:?}", pkg_id.nv)) - .dist; - - let mut dependencies = HashMap::with_capacity(node.children.len()); - for (specifier, child_id) in &node.children { - let child_id = *child_id; - let child_pkg_id = packages_to_pkg_ids.get(&child_id).unwrap(); - if traversed_ids.insert(child_pkg_id.clone()) { - pending.push_back((child_id, child_pkg_id)); - } - dependencies.insert(specifier.clone(), (*child_pkg_id).clone()); - } - - packages.insert( - (*pkg_id).clone(), - NpmResolutionPackage { - copy_index: copy_index_resolver.resolve(pkg_id), - pkg_id: (*pkg_id).clone(), - dist, - dependencies, - }, - ); - } - - Ok(NpmResolutionSnapshot { - root_packages: self - .root_packages - .into_iter() - .map(|(nv, node_id)| { - ( - (*nv).clone(), - packages_to_pkg_ids.get(&node_id).unwrap().clone(), - ) - }) - .collect(), - packages_by_name: packages_by_name - .into_iter() - .map(|(name, mut ids)| { - ids.sort(); - ids.dedup(); - (name, ids) - }) - .collect(), - packages, - package_reqs: self - .package_reqs - .into_iter() - .map(|(req, nv)| (req, (*nv).clone())) - .collect(), - pending_unresolved_packages: self - .pending_unresolved_packages - .into_iter() - .map(|nv| (*nv).clone()) - .collect(), - }) - } - - // Debugging methods - - #[cfg(debug_assertions)] - #[allow(unused)] - fn output_path(&self, path: &Arc<GraphPath>) { - eprintln!("-----------"); - self.output_node(path.node_id(), false); - for path in path.ancestors() { - match path { - GraphPathNodeOrRoot::Node(node) => { - self.output_node(node.node_id(), false) - } - GraphPathNodeOrRoot::Root(pkg_id) => { - let node_id = self.root_packages.get(pkg_id).unwrap(); - eprintln!( - "Root: {} ({}: {})", - pkg_id, - node_id.0, - self.get_npm_pkg_id(*node_id).as_serialized() - ) - } - } - } - eprintln!("-----------"); - } - - #[cfg(debug_assertions)] - #[allow(unused)] - fn output_node(&self, node_id: NodeId, show_children: bool) { - eprintln!( - "{:>4}: {}", - node_id.0, - self.get_npm_pkg_id(node_id).as_serialized() - ); - - if show_children { - let node = self.nodes.get(&node_id).unwrap(); - eprintln!(" Children:"); - for (specifier, child_id) in &node.children { - eprintln!(" {}: {}", specifier, child_id.0); - } - } - } - - #[cfg(debug_assertions)] - #[allow(unused)] - pub fn output_nodes(&self) { - eprintln!("~~~"); - let mut node_ids = self - .resolved_node_ids - .node_to_resolved_id - .keys() - .copied() - .collect::<Vec<_>>(); - node_ids.sort_by(|a, b| a.0.cmp(&b.0)); - for node_id in node_ids { - self.output_node(node_id, true); - } - eprintln!("~~~"); - } -} - -#[derive(Default)] -struct DepEntryCache(HashMap<Arc<NpmPackageNv>, Arc<Vec<NpmDependencyEntry>>>); - -impl DepEntryCache { - pub fn store( - &mut self, - nv: Arc<NpmPackageNv>, - version_info: &NpmPackageVersionInfo, - ) -> Result<Arc<Vec<NpmDependencyEntry>>, AnyError> { - debug_assert!(!self.0.contains_key(&nv)); // we should not be re-inserting - let mut deps = version_info - .dependencies_as_entries() - .with_context(|| format!("npm package: {nv}"))?; - // Ensure name alphabetical and then version descending - // so these are resolved in that order - deps.sort(); - let deps = Arc::new(deps); - self.0.insert(nv, deps.clone()); - Ok(deps) - } - - pub fn get( - &self, - id: &NpmPackageNv, - ) -> Option<&Arc<Vec<NpmDependencyEntry>>> { - self.0.get(id) - } -} - -struct UnresolvedOptionalPeer { - specifier: String, - graph_path: Arc<GraphPath>, -} - -pub struct GraphDependencyResolver<'a> { - graph: &'a mut Graph, - api: &'a NpmRegistryApi, - pending_unresolved_nodes: VecDeque<Arc<GraphPath>>, - unresolved_optional_peers: - HashMap<Arc<NpmPackageNv>, Vec<UnresolvedOptionalPeer>>, - dep_entry_cache: DepEntryCache, -} - -impl<'a> GraphDependencyResolver<'a> { - pub fn new(graph: &'a mut Graph, api: &'a NpmRegistryApi) -> Self { - Self { - graph, - api, - pending_unresolved_nodes: Default::default(), - unresolved_optional_peers: Default::default(), - dep_entry_cache: Default::default(), - } - } - - pub fn add_root_package( - &mut self, - package_nv: &NpmPackageNv, - package_info: &NpmPackageInfo, - ) -> Result<(), AnyError> { - if self.graph.root_packages.contains_key(package_nv) { - return Ok(()); // already added - } - - // todo(dsherret): using a version requirement here is a temporary hack - // to reuse code in a large refactor. We should resolve the node directly - // from the package name and version - let version_req = - VersionReq::parse_from_specifier(&format!("{}", package_nv.version)) - .unwrap(); - let (pkg_nv, node_id) = self.resolve_node_from_info( - &package_nv.name, - &version_req, - package_info, - None, - )?; - self.graph.root_packages.insert(pkg_nv.clone(), node_id); - self - .pending_unresolved_nodes - .push_back(GraphPath::for_root(node_id, pkg_nv)); - Ok(()) - } - - pub fn add_package_req( - &mut self, - package_req: &NpmPackageReq, - package_info: &NpmPackageInfo, - ) -> Result<(), AnyError> { - if self.graph.package_reqs.contains_key(package_req) { - return Ok(()); // already added - } - - let (pkg_id, node_id) = self.resolve_node_from_info( - &package_req.name, - package_req - .version_req - .as_ref() - .unwrap_or(&*LATEST_VERSION_REQ), - package_info, - None, - )?; - self - .graph - .package_reqs - .insert(package_req.clone(), pkg_id.clone()); - self.graph.root_packages.insert(pkg_id.clone(), node_id); - self - .pending_unresolved_nodes - .push_back(GraphPath::for_root(node_id, pkg_id)); - Ok(()) - } - - fn analyze_dependency( - &mut self, - entry: &NpmDependencyEntry, - package_info: &NpmPackageInfo, - parent_path: &Arc<GraphPath>, - ) -> Result<NodeId, AnyError> { - debug_assert_eq!(entry.kind, NpmDependencyEntryKind::Dep); - let parent_id = parent_path.node_id(); - let (child_nv, mut child_id) = self.resolve_node_from_info( - &entry.name, - &entry.version_req, - package_info, - Some(parent_id), - )?; - // Some packages may resolves to themselves as a dependency. If this occurs, - // just ignore adding these as dependencies because this is likely a mistake - // in the package. - if child_id != parent_id { - let maybe_ancestor = parent_path.find_ancestor(&child_nv); - if let Some(ancestor) = &maybe_ancestor { - child_id = ancestor.node_id(); - } - - let new_path = parent_path.with_id( - child_id, - entry.bare_specifier.to_string(), - child_nv, - ); - if let Some(ancestor) = maybe_ancestor { - // this node is circular, so we link it to the ancestor - self.add_linked_circular_descendant(&ancestor, new_path); - } else { - self.graph.set_child_of_parent_node( - parent_id, - &entry.bare_specifier, - child_id, - ); - self.pending_unresolved_nodes.push_back(new_path); - } - } - Ok(child_id) - } - - fn resolve_node_from_info( - &mut self, - pkg_req_name: &str, - version_req: &VersionReq, - package_info: &NpmPackageInfo, - parent_id: Option<NodeId>, - ) -> Result<(Arc<NpmPackageNv>, NodeId), AnyError> { - let version_and_info = resolve_best_package_version_and_info( - version_req, - package_info, - self - .graph - .package_name_versions - .entry(package_info.name.clone()) - .or_default() - .iter(), - )?; - let resolved_id = ResolvedId { - nv: Arc::new(NpmPackageNv { - name: package_info.name.to_string(), - version: version_and_info.version.clone(), - }), - peer_dependencies: Vec::new(), - }; - let (_, node_id) = self.graph.get_or_create_for_id(&resolved_id); - let pkg_nv = resolved_id.nv; - - let has_deps = if let Some(deps) = self.dep_entry_cache.get(&pkg_nv) { - !deps.is_empty() - } else { - let deps = self - .dep_entry_cache - .store(pkg_nv.clone(), version_and_info.info)?; - !deps.is_empty() - }; - - if !has_deps { - // ensure this is set if not, as it's an optimization - let mut node = self.graph.borrow_node_mut(node_id); - node.no_peers = true; - } - - debug!( - "{} - Resolved {}@{} to {}", - match parent_id { - Some(parent_id) => self.graph.get_npm_pkg_id(parent_id).as_serialized(), - None => "<package-req>".to_string(), - }, - pkg_req_name, - version_req.version_text(), - pkg_nv.to_string(), - ); - - Ok((pkg_nv, node_id)) - } - - pub async fn resolve_pending(&mut self) -> Result<(), AnyError> { - // go down through the dependencies by tree depth - while let Some(parent_path) = self.pending_unresolved_nodes.pop_front() { - let (parent_nv, child_deps) = { - let node_id = parent_path.node_id(); - if self.graph.nodes.get(&node_id).unwrap().no_peers { - // We can skip as there's no reason to analyze this graph segment further. - continue; - } - - let pkg_nv = self - .graph - .resolved_node_ids - .get(node_id) - .unwrap() - .nv - .clone(); - let deps = if let Some(deps) = self.dep_entry_cache.get(&pkg_nv) { - deps.clone() - } else { - // the api should have this in the cache at this point, so no need to parallelize - match self.api.package_version_info(&pkg_nv).await? { - Some(version_info) => { - self.dep_entry_cache.store(pkg_nv.clone(), &version_info)? - } - None => { - bail!("Could not find version information for {}", pkg_nv) - } - } - }; - - (pkg_nv, deps) - }; - - // cache all the dependencies' registry infos in parallel if should - self - .api - .cache_in_parallel({ - child_deps.iter().map(|dep| dep.name.clone()).collect() - }) - .await?; - - // resolve the dependencies - let mut found_peer = false; - - for dep in child_deps.iter() { - let package_info = self.api.package_info(&dep.name).await?; - - match dep.kind { - NpmDependencyEntryKind::Dep => { - let parent_id = parent_path.node_id(); - let node = self.graph.nodes.get(&parent_id).unwrap(); - let child_id = match node.children.get(&dep.bare_specifier) { - Some(child_id) => { - // this dependency was previously analyzed by another path - // so we don't attempt to resolve the version again - let child_id = *child_id; - let child_nv = self - .graph - .resolved_node_ids - .get(child_id) - .unwrap() - .nv - .clone(); - let maybe_ancestor = parent_path.find_ancestor(&child_nv); - let child_path = parent_path.with_id( - child_id, - dep.bare_specifier.clone(), - child_nv, - ); - if let Some(ancestor) = maybe_ancestor { - // when the nv appears as an ancestor, use that node - // and mark this as circular - self.add_linked_circular_descendant(&ancestor, child_path); - } else { - // mark the child as pending - self.pending_unresolved_nodes.push_back(child_path); - } - child_id - } - None => { - self.analyze_dependency(dep, &package_info, &parent_path)? - } - }; - - if !found_peer { - found_peer = !self.graph.borrow_node_mut(child_id).no_peers; - } - } - NpmDependencyEntryKind::Peer - | NpmDependencyEntryKind::OptionalPeer => { - found_peer = true; - // we need to re-evaluate peer dependencies every time and can't - // skip over them because they might be evaluated differently based - // on the current path - let maybe_new_id = self.resolve_peer_dep( - &dep.bare_specifier, - dep, - &package_info, - &parent_path, - )?; - - // For optional dependencies, we want to resolve them if any future - // same parent version resolves them. So when not resolved, store them to be - // potentially resolved later. - // - // Note: This is not a good solution, but will probably work ok in most - // scenarios. We can work on improving this in the future. We probably - // want to resolve future optional peers to the same dependency for example. - if dep.kind == NpmDependencyEntryKind::OptionalPeer { - match maybe_new_id { - Some(new_id) => { - if let Some(unresolved_optional_peers) = - self.unresolved_optional_peers.remove(&parent_nv) - { - for optional_peer in unresolved_optional_peers { - let peer_parent = GraphPathNodeOrRoot::Node( - optional_peer.graph_path.clone(), - ); - self.set_new_peer_dep( - &[&optional_peer.graph_path], - peer_parent, - &optional_peer.specifier, - new_id, - ); - } - } - } - None => { - // store this for later if it's resolved for this version - self - .unresolved_optional_peers - .entry(parent_nv.clone()) - .or_default() - .push(UnresolvedOptionalPeer { - specifier: dep.bare_specifier.clone(), - graph_path: parent_path.clone(), - }); - } - } - } - } - } - } - - if !found_peer { - self.graph.borrow_node_mut(parent_path.node_id()).no_peers = true; - } - } - Ok(()) - } - - fn resolve_peer_dep( - &mut self, - specifier: &str, - peer_dep: &NpmDependencyEntry, - peer_package_info: &NpmPackageInfo, - ancestor_path: &Arc<GraphPath>, - ) -> Result<Option<NodeId>, AnyError> { - debug_assert!(matches!( - peer_dep.kind, - NpmDependencyEntryKind::Peer | NpmDependencyEntryKind::OptionalPeer - )); - - let mut path = vec![ancestor_path]; - - // the current dependency might have had the peer dependency - // in another bare specifier slot... if so resolve it to that - { - let maybe_peer_dep = self.find_peer_dep_in_node( - ancestor_path, - peer_dep, - peer_package_info, - )?; - - if let Some((peer_parent, peer_dep_id)) = maybe_peer_dep { - // this will always have an ancestor because we're not at the root - self.set_new_peer_dep(&path, peer_parent, specifier, peer_dep_id); - return Ok(Some(peer_dep_id)); - } - } - - // Peer dependencies are resolved based on its ancestors' siblings. - // If not found, then it resolves based on the version requirement if non-optional. - for ancestor_node in ancestor_path.ancestors() { - match ancestor_node { - GraphPathNodeOrRoot::Node(ancestor_graph_path_node) => { - path.push(ancestor_graph_path_node); - let maybe_peer_dep = self.find_peer_dep_in_node( - ancestor_graph_path_node, - peer_dep, - peer_package_info, - )?; - if let Some((parent, peer_dep_id)) = maybe_peer_dep { - // this will always have an ancestor because we're not at the root - self.set_new_peer_dep(&path, parent, specifier, peer_dep_id); - return Ok(Some(peer_dep_id)); - } - } - GraphPathNodeOrRoot::Root(root_pkg_id) => { - // in this case, the parent is the root so the children are all the package requirements - if let Some(child_id) = find_matching_child( - peer_dep, - peer_package_info, - self.graph.root_packages.iter().map(|(nv, id)| (*id, nv)), - )? { - let peer_parent = GraphPathNodeOrRoot::Root(root_pkg_id.clone()); - self.set_new_peer_dep(&path, peer_parent, specifier, child_id); - return Ok(Some(child_id)); - } - } - } - } - - // We didn't find anything by searching the ancestor siblings, so we need - // to resolve based on the package info - if !peer_dep.kind.is_optional() { - let parent_id = ancestor_path.node_id(); - let (_, node_id) = self.resolve_node_from_info( - &peer_dep.name, - peer_dep - .peer_dep_version_req - .as_ref() - .unwrap_or(&peer_dep.version_req), - peer_package_info, - Some(parent_id), - )?; - let peer_parent = GraphPathNodeOrRoot::Node(ancestor_path.clone()); - self.set_new_peer_dep(&[ancestor_path], peer_parent, specifier, node_id); - Ok(Some(node_id)) - } else { - Ok(None) - } - } - - fn find_peer_dep_in_node( - &self, - path: &Arc<GraphPath>, - peer_dep: &NpmDependencyEntry, - peer_package_info: &NpmPackageInfo, - ) -> Result<Option<(GraphPathNodeOrRoot, NodeId)>, AnyError> { - let node_id = path.node_id(); - let resolved_node_id = self.graph.resolved_node_ids.get(node_id).unwrap(); - // check if this node itself is a match for - // the peer dependency and if so use that - if resolved_node_id.nv.name == peer_dep.name - && version_req_satisfies( - &peer_dep.version_req, - &resolved_node_id.nv.version, - peer_package_info, - None, - )? - { - let parent = path.previous_node.as_ref().unwrap().clone(); - Ok(Some((parent, node_id))) - } else { - let node = self.graph.nodes.get(&node_id).unwrap(); - let children = node.children.values().map(|child_node_id| { - let child_node_id = *child_node_id; - ( - child_node_id, - &self.graph.resolved_node_ids.get(child_node_id).unwrap().nv, - ) - }); - find_matching_child(peer_dep, peer_package_info, children).map( - |maybe_child_id| { - maybe_child_id.map(|child_id| { - let parent = GraphPathNodeOrRoot::Node(path.clone()); - (parent, child_id) - }) - }, - ) - } - } - - fn add_peer_deps_to_path( - &mut self, - // path from the node above the resolved dep to just above the peer dep - path: &[&Arc<GraphPath>], - peer_deps: &[(&ResolvedIdPeerDep, Arc<NpmPackageNv>)], - ) { - debug_assert!(!path.is_empty()); - - for graph_path_node in path.iter().rev() { - let old_node_id = graph_path_node.node_id(); - let old_resolved_id = self - .graph - .resolved_node_ids - .get(old_node_id) - .unwrap() - .clone(); - - let mut new_resolved_id = old_resolved_id; - let mut has_changed = false; - for (peer_dep, nv) in peer_deps { - if *nv == new_resolved_id.nv { - continue; - } - if new_resolved_id.push_peer_dep((*peer_dep).clone()) { - has_changed = true; - } - } - - if !has_changed { - continue; // nothing to change - } - - let (created, new_node_id) = - self.graph.get_or_create_for_id(&new_resolved_id); - - if created { - let old_children = - self.graph.borrow_node_mut(old_node_id).children.clone(); - // copy over the old children to this new one - for (specifier, child_id) in &old_children { - self.graph.set_child_of_parent_node( - new_node_id, - specifier, - *child_id, - ); - } - } - - graph_path_node.change_id(new_node_id); - - let circular_descendants = - graph_path_node.linked_circular_descendants.lock().clone(); - for descendant in circular_descendants { - let path = descendant.get_path_to_ancestor_exclusive(new_node_id); - self.add_peer_deps_to_path(&path, peer_deps); - descendant.change_id(new_node_id); - - // update the bottom node to point to this new node id - let bottom_node_id = path[0].node_id(); - self.graph.set_child_of_parent_node( - bottom_node_id, - descendant.specifier(), - descendant.node_id(), - ); - } - - // update the previous parent to have this as its child - match graph_path_node.previous_node.as_ref().unwrap() { - GraphPathNodeOrRoot::Root(pkg_id) => { - self.graph.root_packages.insert(pkg_id.clone(), new_node_id); - } - GraphPathNodeOrRoot::Node(parent_node_path) => { - let parent_node_id = parent_node_path.node_id(); - let parent_node = self.graph.borrow_node_mut(parent_node_id); - parent_node - .children - .insert(graph_path_node.specifier().to_string(), new_node_id); - } - } - } - } - - fn set_new_peer_dep( - &mut self, - // path from the node above the resolved dep to just above the peer dep - path: &[&Arc<GraphPath>], - peer_dep_parent: GraphPathNodeOrRoot, - peer_dep_specifier: &str, - peer_dep_id: NodeId, - ) { - debug_assert!(!path.is_empty()); - let peer_dep_nv = self - .graph - .resolved_node_ids - .get(peer_dep_id) - .unwrap() - .nv - .clone(); - - let peer_dep = ResolvedIdPeerDep::ParentReference { - parent: peer_dep_parent, - child_pkg_nv: peer_dep_nv.clone(), - }; - - let top_node = path.last().unwrap(); - let (maybe_circular_ancestor, path) = if top_node.nv == peer_dep_nv { - // it's circular, so exclude the top node - (Some(top_node), &path[0..path.len() - 1]) - } else { - (None, path) - }; - self.add_peer_deps_to_path(path, &[(&peer_dep, peer_dep_nv.clone())]); - - // now set the peer dependency - let bottom_node = path.first().unwrap(); - self.graph.set_child_of_parent_node( - bottom_node.node_id(), - peer_dep_specifier, - peer_dep_id, - ); - - // queue next step - let new_path = bottom_node.with_id( - peer_dep_id, - peer_dep_specifier.to_string(), - peer_dep_nv, - ); - if let Some(ancestor_node) = maybe_circular_ancestor { - // it's circular, so link this in step with the ancestor node - ancestor_node - .linked_circular_descendants - .lock() - .push(new_path); - } else { - // mark the peer dep as needing to be analyzed - self.pending_unresolved_nodes.push_back(new_path); - } - - debug!( - "Resolved peer dependency for {} in {} to {}", - peer_dep_specifier, - &self - .graph - .get_npm_pkg_id(bottom_node.node_id()) - .as_serialized(), - &self.graph.get_npm_pkg_id(peer_dep_id).as_serialized(), - ); - } - - fn add_linked_circular_descendant( - &mut self, - ancestor: &Arc<GraphPath>, - descendant: Arc<GraphPath>, - ) { - let ancestor_node_id = ancestor.node_id(); - let path = descendant.get_path_to_ancestor_exclusive(ancestor_node_id); - - let ancestor_resolved_id = self - .graph - .resolved_node_ids - .get(ancestor_node_id) - .unwrap() - .clone(); - - let peer_deps = ancestor_resolved_id - .peer_dependencies - .iter() - .map(|peer_dep| { - ( - peer_dep, - match &peer_dep { - ResolvedIdPeerDep::ParentReference { child_pkg_nv, .. } => { - child_pkg_nv.clone() - } - ResolvedIdPeerDep::SnapshotNodeId(node_id) => self - .graph - .resolved_node_ids - .get(*node_id) - .unwrap() - .nv - .clone(), - }, - ) - }) - .collect::<Vec<_>>(); - if !peer_deps.is_empty() { - self.add_peer_deps_to_path(&path, &peer_deps); - } - - let bottom_node_id = path[0].node_id(); - self.graph.set_child_of_parent_node( - bottom_node_id, - descendant.specifier(), - descendant.node_id(), - ); - - ancestor.linked_circular_descendants.lock().push(descendant); - } -} - -fn find_matching_child<'a>( - peer_dep: &NpmDependencyEntry, - peer_package_info: &NpmPackageInfo, - children: impl Iterator<Item = (NodeId, &'a Arc<NpmPackageNv>)>, -) -> Result<Option<NodeId>, AnyError> { - for (child_id, pkg_id) in children { - if pkg_id.name == peer_dep.name - && version_req_satisfies( - &peer_dep.version_req, - &pkg_id.version, - peer_package_info, - None, - )? - { - return Ok(Some(child_id)); - } - } - Ok(None) -} - -#[cfg(test)] -mod test { - use deno_graph::npm::NpmPackageReqReference; - use pretty_assertions::assert_eq; - - use crate::npm::registry::TestNpmRegistryApiInner; - - use super::*; - - #[test] - fn resolved_id_tests() { - let mut ids = ResolvedNodeIds::default(); - let node_id = NodeId(0); - let resolved_id = ResolvedId { - nv: Arc::new(NpmPackageNv::from_str("package@1.1.1").unwrap()), - peer_dependencies: Vec::new(), - }; - ids.set(node_id, resolved_id.clone()); - assert!(ids.get(node_id).is_some()); - assert!(ids.get(NodeId(1)).is_none()); - assert_eq!(ids.get_node_id(&resolved_id), Some(node_id)); - - let resolved_id_new = ResolvedId { - nv: Arc::new(NpmPackageNv::from_str("package@1.1.2").unwrap()), - peer_dependencies: Vec::new(), - }; - ids.set(node_id, resolved_id_new.clone()); - assert_eq!(ids.get_node_id(&resolved_id), None); // stale entry should have been removed - assert!(ids.get(node_id).is_some()); - assert_eq!(ids.get_node_id(&resolved_id_new), Some(node_id)); - } - - #[tokio::test] - async fn resolve_deps_no_peer() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "0.1.0"); - api.ensure_package_version("package-c", "0.0.10"); - api.ensure_package_version("package-d", "3.2.1"); - api.ensure_package_version("package-d", "3.2.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^0.1")); - api.add_dependency(("package-c", "0.1.0"), ("package-d", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ("package-b".to_string(), "package-b@2.0.0".to_string(),), - ("package-c".to_string(), "package-c@0.1.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@0.1.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-d".to_string(), - "package-d@3.2.1".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-d@3.2.1".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_deps_circular() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "*")); - api.add_dependency(("package-b", "2.0.0"), ("package-a", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@2.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn peer_deps_simple_top_tree() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1.0", "npm:package-peer@1.0"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - } - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1.0".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string() - ), - ( - "package-peer@1.0".to_string(), - "package-peer@1.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn peer_deps_simple_root_pkg_children() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_dependency(("package-0", "1.0.0"), ("package-a", "1")); - api.add_dependency(("package-0", "1.0.0"), ("package-peer", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-a".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@1.0.0".to_string(),) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - } - ] - ); - assert_eq!( - package_reqs, - vec![( - "package-0@1.0".to_string(), - "package-0@1.0.0_package-peer@1.0.0".to_string() - ),] - ); - } - - #[tokio::test] - async fn peer_deps_simple_deeper() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-1", "1.0.0"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_dependency(("package-0", "1.0.0"), ("package-1", "1")); - api.add_dependency(("package-1", "1.0.0"), ("package-a", "1")); - api.add_dependency(("package-1", "1.0.0"), ("package-peer", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-1".to_string(), - "package-1@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-1@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-a".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@1.0.0".to_string(),) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - } - ] - ); - assert_eq!( - package_reqs, - vec![("package-0@1.0".to_string(), "package-0@1.0.0".to_string()),] - ); - } - - #[tokio::test] - async fn resolve_with_peer_deps_top_tree() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "3.0.0"); - api.ensure_package_version("package-peer", "4.0.0"); - api.ensure_package_version("package-peer", "4.1.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer", "4")); - api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - // the peer dependency is specified here at the top of the tree - // so it should resolve to 4.0.0 instead of 4.1.0 - vec!["npm:package-a@1", "npm:package-peer@4.0.0"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@2.0.0_package-peer@4.0.0".to_string(), - ), - ( - "package-c".to_string(), - "package-c@3.0.0_package-peer@4.0.0".to_string(), - ), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-c@3.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@4.0.0".to_string() - ), - ( - "package-peer@4.0.0".to_string(), - "package-peer@4.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_with_peer_deps_ancestor_sibling_not_top_tree() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.1.1"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "3.0.0"); - api.ensure_package_version("package-peer", "4.0.0"); - api.ensure_package_version("package-peer", "4.1.0"); - api.add_dependency(("package-0", "1.1.1"), ("package-a", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3")); - // the peer dependency is specified here as a sibling of "a" and "b" - // so it should resolve to 4.0.0 instead of 4.1.0 - api.add_dependency(("package-a", "1.0.0"), ("package-peer", "4.0.0")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer", "4")); - api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-0@1.1.1"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.1.1".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0_package-peer@4.0.0".to_string(), - ),]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@2.0.0_package-peer@4.0.0".to_string(), - ), - ( - "package-c".to_string(), - "package-c@3.0.0_package-peer@4.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@4.0.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-c@3.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-0@1.1.1".to_string(), "package-0@1.1.1".to_string())] - ); - } - - #[tokio::test] - async fn resolve_with_peer_deps_auto_resolved() { - // in this case, the peer dependency is not found in the tree - // so it's auto-resolved based on the registry - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "3.0.0"); - api.ensure_package_version("package-peer", "4.0.0"); - api.ensure_package_version("package-peer", "4.1.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer", "4")); - api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@2.0.0_package-peer@4.1.0".to_string(), - ), - ( - "package-c".to_string(), - "package-c@3.0.0_package-peer@4.1.0".to_string(), - ), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-peer@4.1.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.1.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-c@3.0.0_package-peer@4.1.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.1.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@4.1.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_with_optional_peer_dep_not_resolved() { - // in this case, the peer dependency is not found in the tree - // so it's auto-resolved based on the registry - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "3.0.0"); - api.ensure_package_version("package-peer", "4.0.0"); - api.ensure_package_version("package-peer", "4.1.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3")); - api.add_optional_peer_dependency( - ("package-b", "2.0.0"), - ("package-peer", "4"), - ); - api.add_optional_peer_dependency( - ("package-c", "3.0.0"), - ("package-peer", "*"), - ); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ("package-b".to_string(), "package-b@2.0.0".to_string(),), - ("package-c".to_string(), "package-c@3.0.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@3.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_with_optional_peer_found() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "3.0.0"); - api.ensure_package_version("package-peer", "4.0.0"); - api.ensure_package_version("package-peer", "4.1.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3")); - api.add_optional_peer_dependency( - ("package-b", "2.0.0"), - ("package-peer", "4"), - ); - api.add_optional_peer_dependency( - ("package-c", "3.0.0"), - ("package-peer", "*"), - ); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1", "npm:package-peer@4.0.0"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@2.0.0_package-peer@4.0.0".to_string(), - ), - ( - "package-c".to_string(), - "package-c@3.0.0_package-peer@4.0.0".to_string(), - ), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-c@3.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@4.0.0".to_string() - ), - ( - "package-peer@4.0.0".to_string(), - "package-peer@4.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_optional_peer_first_not_resolved_second_resolved_scenario1() - { - // When resolving a dependency a second time and it has an optional - // peer dependency that wasn't previously resolved, it should resolve all the - // previous versions to the new one - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^1")); - api.add_dependency(("package-a", "1.0.0"), ("package-peer", "^1")); - api.add_optional_peer_dependency( - ("package-b", "1.0.0"), - ("package-peer", "*"), - ); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1", "npm:package-b@1"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@1.0.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string() - ), - ( - "package-b@1".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_optional_peer_first_not_resolved_second_resolved_scenario2() - { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "2.0.0"); - api.add_optional_peer_dependency( - ("package-a", "1.0.0"), - ("package-peer", "*"), - ); - api.add_dependency(("package-b", "1.0.0"), ("package-a", "1.0.0")); - api.add_dependency(("package-b", "1.0.0"), ("package-peer", "2.0.0")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1", "npm:package-b@1"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@2.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-a".to_string(), - "package-a@1.0.0_package-peer@2.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@2.0.0".to_string(),) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@2.0.0".to_string() - ), - ( - "package-b@1".to_string(), - "package-b@1.0.0_package-peer@2.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_optional_dep_npm_req_top() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_optional_peer_dependency( - ("package-a", "1.0.0"), - ("package-peer", "*"), - ); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1", "npm:package-peer@1"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string() - ), - ( - "package-peer@1".to_string(), - "package-peer@1.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_optional_dep_different_resolution_second_time() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.ensure_package_version("package-peer", "2.0.0"); - api.add_optional_peer_dependency( - ("package-a", "1.0.0"), - ("package-peer", "*"), - ); - api.add_dependency(("package-b", "1.0.0"), ("package-a", "1.0.0")); - api.add_dependency(("package-b", "1.0.0"), ("package-peer", "2.0.0")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec![ - "npm:package-a@1", - "npm:package-b@1", - "npm:package-peer@1.0.0", - ], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@2.0.0".to_string(), - copy_index: 1, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@2.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ("package-peer".to_string(), "package-peer@2.0.0".to_string(),), - ( - "package-a".to_string(), - "package-a@1.0.0_package-peer@2.0.0".to_string(), - ), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string() - ), - ( - "package-b@1".to_string(), - "package-b@1.0.0_package-peer@2.0.0".to_string() - ), - ( - "package-peer@1.0.0".to_string(), - "package-peer@1.0.0".to_string() - ) - ] - ); - } - #[tokio::test] - async fn resolve_peer_dep_other_specifier_slot() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-peer", "2.0.0"); - // bit of an edge case... probably nobody has ever done this - api.add_dependency( - ("package-a", "1.0.0"), - ("package-peer2", "npm:package-peer@2"), - ); - api.add_peer_dependency(("package-a", "1.0.0"), ("package-peer", "2")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ("package-peer".to_string(), "package-peer@2.0.0".to_string(),), - ( - "package-peer2".to_string(), - "package-peer@2.0.0".to_string(), - ), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@2.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@2.0.0".to_string() - ),] - ); - } - - #[tokio::test] - async fn resolve_nested_peer_deps_auto_resolved() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-peer-a", "2.0.0"); - api.ensure_package_version("package-peer-b", "3.0.0"); - api.add_peer_dependency(("package-0", "1.0.0"), ("package-peer-a", "2")); - api.add_peer_dependency( - ("package-peer-a", "2.0.0"), - ("package-peer-b", "3"), - ); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0_package-peer-a@2.0.0__package-peer-b@3.0.0" - .to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer-a".to_string(), - "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer-b".to_string(), - "package-peer-b@3.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-b@3.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![( - "package-0@1.0".to_string(), - "package-0@1.0.0_package-peer-a@2.0.0__package-peer-b@3.0.0" - .to_string() - )] - ); - } - - #[tokio::test] - async fn resolve_nested_peer_deps_ancestor_sibling_deps() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-peer-a", "2.0.0"); - api.ensure_package_version("package-peer-b", "3.0.0"); - api.add_dependency(("package-0", "1.0.0"), ("package-peer-b", "*")); - api.add_peer_dependency(("package-0", "1.0.0"), ("package-peer-a", "2")); - api.add_peer_dependency( - ("package-peer-a", "2.0.0"), - ("package-peer-b", "3"), - ); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec![ - "npm:package-0@1.0", - "npm:package-peer-a@2", - "npm:package-peer-b@3", - ], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0_package-peer-a@2.0.0__package-peer-b@3.0.0_package-peer-b@3.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-peer-a".to_string(), - "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(), - ), - ( - "package-peer-b".to_string(), - "package-peer-b@3.0.0".to_string(), - ) - ]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer-b".to_string(), - "package-peer-b@3.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-b@3.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-0@1.0".to_string(), - "package-0@1.0.0_package-peer-a@2.0.0__package-peer-b@3.0.0_package-peer-b@3.0.0" - .to_string() - ), - ( - "package-peer-a@2".to_string(), - "package-peer-a@2.0.0_package-peer-b@3.0.0".to_string() - ), - ( - "package-peer-b@3".to_string(), - "package-peer-b@3.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_with_peer_deps_multiple() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.1.1"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-c", "3.0.0"); - api.ensure_package_version("package-d", "3.5.0"); - api.ensure_package_version("package-e", "3.6.0"); - api.ensure_package_version("package-peer-a", "4.0.0"); - api.ensure_package_version("package-peer-a", "4.1.0"); - api.ensure_package_version("package-peer-b", "5.3.0"); - api.ensure_package_version("package-peer-b", "5.4.1"); - api.ensure_package_version("package-peer-c", "6.2.0"); - api.add_dependency(("package-0", "1.1.1"), ("package-a", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "^2")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "^3")); - api.add_dependency(("package-a", "1.0.0"), ("package-d", "^3")); - api.add_dependency(("package-a", "1.0.0"), ("package-peer-a", "4.0.0")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-peer-a", "4")); - api.add_peer_dependency( - ("package-b", "2.0.0"), - ("package-peer-c", "=6.2.0"), // will be auto-resolved - ); - api.add_peer_dependency(("package-c", "3.0.0"), ("package-peer-a", "*")); - api.add_peer_dependency( - ("package-peer-a", "4.0.0"), - ("package-peer-b", "^5.4"), // will be auto-resolved - ); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-0@1.1.1", "npm:package-e@3"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.1.1".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(), - ),]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@2.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1_package-peer-c@6.2.0".to_string(), - ), - ( - "package-c".to_string(), - "package-c@3.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(), - ), - ( - "package-d".to_string(), - "package-d@3.5.0".to_string(), - ), - ( - "package-peer-a".to_string(), - "package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(), - ), - ]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1_package-peer-c@6.2.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-peer-a".to_string(), - "package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(), - ), - ( - "package-peer-c".to_string(), - "package-peer-c@6.2.0".to_string(), - ) - ]) - }, - TestNpmResolutionPackage { - pkg_id: "package-c@3.0.0_package-peer-a@4.0.0__package-peer-b@5.4.1".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer-a".to_string(), - "package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-d@3.5.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-e@3.6.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-a@4.0.0_package-peer-b@5.4.1".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer-b".to_string(), - "package-peer-b@5.4.1".to_string(), - )]) - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-b@5.4.1".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer-c@6.2.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ("package-0@1.1.1".to_string(), "package-0@1.1.1".to_string()), - ("package-e@3".to_string(), "package-e@3.6.0".to_string()), - ] - ); - } - - #[tokio::test] - async fn resolve_peer_deps_circular() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "*")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-a", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@2.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_peer_deps_multiple_copies() { - // repeat this a few times to have a higher probability of surfacing indeterminism - for _ in 0..3 { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-dep", "3.0.0"); - api.ensure_package_version("package-peer", "4.0.0"); - api.ensure_package_version("package-peer", "5.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-dep", "*")); - api.add_dependency(("package-a", "1.0.0"), ("package-peer", "4")); - api.add_dependency(("package-b", "2.0.0"), ("package-dep", "*")); - api.add_dependency(("package-b", "2.0.0"), ("package-peer", "5")); - api.add_peer_dependency(("package-dep", "3.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1", "npm:package-b@2"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-dep".to_string(), - "package-dep@3.0.0_package-peer@4.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@4.0.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-peer@5.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-dep".to_string(), - "package-dep@3.0.0_package-peer@5.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@5.0.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-dep@3.0.0_package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@4.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-dep@3.0.0_package-peer@5.0.0".to_string(), - copy_index: 1, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@5.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@4.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@5.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1".to_string(), - "package-a@1.0.0_package-peer@4.0.0".to_string() - ), - ( - "package-b@2".to_string(), - "package-b@2.0.0_package-peer@5.0.0".to_string() - ) - ] - ); - } - } - - #[tokio::test] - async fn resolve_dep_with_peer_deps_dep_then_peer() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-peer", "1")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1.0", "npm:package-b@1.0"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-b@1.0.0__package-peer@1.0.0" - .to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-c".to_string(), - "package-c@1.0.0_package-b@1.0.0__package-peer@1.0.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@1.0.0".to_string(),) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-b@1.0.0__package-peer@1.0.0" - .to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1.0".to_string(), - "package-a@1.0.0_package-b@1.0.0__package-peer@1.0.0".to_string() - ), - ( - "package-b@1.0".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_dep_with_peer_deps_then_other_dep_with_different_peer() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-peer", "1.1.0"); - api.ensure_package_version("package-peer", "1.2.0"); - api.add_peer_dependency(("package-a", "1.0.0"), ("package-peer", "*")); // should select 1.2.0, then 1.1.0 - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-peer", "=1.1.0")); - api.add_dependency(("package-c", "1.0.0"), ("package-a", "1")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-a@1.0", "npm:package-b@1.0"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.1.0".to_string(), - copy_index: 1, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.1.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.2.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.2.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.1.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-c".to_string(), - "package-c@1.0.0_package-peer@1.1.0".to_string(), - ), - ("package-peer".to_string(), "package-peer@1.1.0".to_string(),) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-peer@1.1.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0_package-peer@1.1.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.1.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.2.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-a@1.0".to_string(), - "package-a@1.0.0_package-peer@1.2.0".to_string() - ), - ( - "package-b@1.0".to_string(), - "package-b@1.0.0_package-peer@1.1.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_dep_and_peer_dist_tag() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.ensure_package_version("package-b", "3.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-d", "1.0.0"); - api.ensure_package_version("package-e", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "some-tag")); - api.add_dependency(("package-a", "1.0.0"), ("package-d", "1.0.0")); - api.add_dependency(("package-a", "1.0.0"), ("package-c", "1.0.0")); - api.add_dependency(("package-a", "1.0.0"), ("package-e", "1.0.0")); - api.add_dependency(("package-e", "1.0.0"), ("package-b", "some-tag")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-d", "other-tag")); - api.add_dist_tag("package-b", "some-tag", "2.0.0"); - api.add_dist_tag("package-d", "other-tag", "1.0.0"); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-d@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ("package-b".to_string(), "package-b@2.0.0".to_string(),), - ( - "package-c".to_string(), - "package-c@1.0.0_package-d@1.0.0".to_string(), - ), - ("package-d".to_string(), "package-d@1.0.0".to_string(),), - ("package-e".to_string(), "package-e@1.0.0".to_string(),), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-d@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-d".to_string(), - "package-d@1.0.0".to_string(), - ),]), - }, - TestNpmResolutionPackage { - pkg_id: "package-d@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-e@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@2.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![( - "package-a@1.0".to_string(), - "package-a@1.0.0_package-d@1.0.0".to_string() - ),] - ); - } - - #[tokio::test] - async fn package_has_self_as_dependency() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-a", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await; - assert_eq!( - packages, - vec![TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - // in this case, we just ignore that the package did this - dependencies: Default::default(), - }] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn package_has_self_but_different_version_as_dependency() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-a", "0.5.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-a", "^0.5")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@0.5.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@0.5.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn grand_child_package_has_self_as_peer_dependency_root() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "2")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-a", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@2.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - )]), - } - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn grand_child_package_has_self_as_peer_dependency_under_root() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "2.0.0"); - api.add_dependency(("package-0", "1.0.0"), ("package-a", "*")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "2")); - api.add_peer_dependency(("package-b", "2.0.0"), ("package-a", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@2.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@2.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - )]), - } - ] - ); - assert_eq!( - package_reqs, - vec![("package-0@1.0".to_string(), "package-0@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_peer_deps_in_ancestor_root() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-a", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_peer_deps_in_ancestor_non_root() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0".to_string(), - ),]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-b@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-b@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn nested_deps_same_peer_dep_ancestor() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-1", "1.0.0"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-d", "1.0.0"); - api.add_dependency(("package-0", "1.0.0"), ("package-a", "1")); - api.add_dependency(("package-0", "1.0.0"), ("package-1", "1")); - api.add_dependency(("package-1", "1.0.0"), ("package-a", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-c", "1.0.0"), ("package-d", "1")); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-a", "*")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-a", "*")); - api.add_peer_dependency(("package-d", "1.0.0"), ("package-a", "*")); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-0", "*")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-0", "*")); - api.add_peer_dependency(("package-d", "1.0.0"), ("package-0", "*")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-0@1.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0_package-0@1.0.0".to_string(), - ), ( - "package-1".to_string(), - "package-1@1.0.0_package-0@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-1@1.0.0_package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0_package-0@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-0".to_string(), - "package-0@1.0.0".to_string(), - ), - ( - "package-a".to_string(), - "package-a@1.0.0_package-0@1.0.0".to_string(), - ), - ( - "package-c".to_string(), - "package-c@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(), - ) - ]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-0".to_string(), - "package-0@1.0.0".to_string(), - ), - ( - "package-a".to_string(), - "package-a@1.0.0_package-0@1.0.0".to_string(), - ), - ( - "package-d".to_string(), - "package-d@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(), - ) - ]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-d@1.0.0_package-0@1.0.0_package-a@1.0.0__package-0@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-0".to_string(), - "package-0@1.0.0".to_string(), - ), - ( - "package-a".to_string(), - "package-a@1.0.0_package-0@1.0.0".to_string(), - ) - ]), - - } - ] - ); - assert_eq!( - package_reqs, - vec![("package-0@1.0".to_string(), "package-0@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn peer_dep_resolved_then_resolved_deeper() { - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-0", "1.0.0"); - api.ensure_package_version("package-1", "1.0.0"); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-peer", "1.0.0"); - api.add_dependency(("package-0", "1.0.0"), ("package-a", "1")); - api.add_dependency(("package-0", "1.0.0"), ("package-1", "1")); - api.add_dependency(("package-1", "1.0.0"), ("package-a", "1")); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_peer_dependency(("package-b", "1.0.0"), ("package-peer", "*")); - - let (packages, package_reqs) = run_resolver_and_get_output( - api, - vec!["npm:package-0@1.0", "npm:package-peer@1.0"], - ) - .await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-0@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-1".to_string(), - "package-1@1.0.0_package-peer@1.0.0".to_string(), - ), - ( - "package-a".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string(), - ) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-1@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-peer".to_string(), - "package-peer@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-peer@1.0.0".to_string(), - copy_index: 0, - dependencies: Default::default(), - } - ] - ); - assert_eq!( - package_reqs, - vec![ - ( - "package-0@1.0".to_string(), - "package-0@1.0.0_package-peer@1.0.0".to_string() - ), - ( - "package-peer@1.0".to_string(), - "package-peer@1.0.0".to_string() - ) - ] - ); - } - - #[tokio::test] - async fn resolve_dep_with_peer_deps_circular_1() { - // a -> b -> c -> d -> c where c has a peer dependency on b - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-d", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-c", "1.0.0"), ("package-d", "1")); - api.add_dependency(("package-d", "1.0.0"), ("package-c", "1")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0".to_string(), - ),]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-b@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-b@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ("package-b".to_string(), "package-b@1.0.0".to_string(),), - ( - "package-d".to_string(), - "package-d@1.0.0_package-b@1.0.0".to_string(), - ) - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-d@1.0.0_package-b@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-b@1.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_dep_with_peer_deps_circular_2() { - // a -> b -> c -> d -> c where c has a peer dependency on b - // -> e -> f -> d -> c where f has a peer dep on a - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-d", "1.0.0"); - api.ensure_package_version("package-e", "1.0.0"); - api.ensure_package_version("package-f", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-c", "1.0.0"), ("package-d", "1")); - api.add_dependency(("package-c", "1.0.0"), ("package-e", "1")); - api.add_dependency(("package-d", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-e", "1.0.0"), ("package-f", "1")); - api.add_dependency(("package-f", "1.0.0"), ("package-d", "1")); - api.add_peer_dependency(("package-f", "1.0.0"), ("package-a", "1")); - api.add_peer_dependency(("package-c", "1.0.0"), ("package-b", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-b".to_string(), - "package-b@1.0.0_package-a@1.0.0".to_string(), - ), - ( - "package-d".to_string(), - "package-d@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(), - ), - ( - "package-e".to_string(), - "package-e@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string() - ) - ]), - - }, - TestNpmResolutionPackage { - pkg_id: "package-d@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-e@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-f".to_string(), - "package-f@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-f@1.0.0_package-a@1.0.0_package-b@1.0.0__package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string(), - ), ( - "package-d".to_string(), - "package-d@1.0.0_package-b@1.0.0__package-a@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[tokio::test] - async fn resolve_dep_with_peer_deps_circular_3() { - // a -> b -> c -> d -> c (peer) - // -> e -> a (peer) - let api = TestNpmRegistryApiInner::default(); - api.ensure_package_version("package-a", "1.0.0"); - api.ensure_package_version("package-b", "1.0.0"); - api.ensure_package_version("package-c", "1.0.0"); - api.ensure_package_version("package-d", "1.0.0"); - api.ensure_package_version("package-e", "1.0.0"); - api.add_dependency(("package-a", "1.0.0"), ("package-b", "1")); - api.add_dependency(("package-b", "1.0.0"), ("package-c", "1")); - api.add_dependency(("package-c", "1.0.0"), ("package-d", "1")); - api.add_dependency(("package-d", "1.0.0"), ("package-e", "1")); - api.add_peer_dependency(("package-d", "1.0.0"), ("package-c", "1")); - api.add_peer_dependency(("package-e", "1.0.0"), ("package-a", "1")); - - let (packages, package_reqs) = - run_resolver_and_get_output(api, vec!["npm:package-a@1.0.0"]).await; - assert_eq!( - packages, - vec![ - TestNpmResolutionPackage { - pkg_id: "package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-b".to_string(), - "package-b@1.0.0_package-a@1.0.0".to_string(), - ),]), - }, - TestNpmResolutionPackage { - pkg_id: "package-b@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-c".to_string(), - "package-c@1.0.0_package-a@1.0.0".to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: "package-c@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-d".to_string(), - "package-d@1.0.0_package-c@1.0.0__package-a@1.0.0_package-a@1.0.0" - .to_string(), - )]), - }, - TestNpmResolutionPackage { - pkg_id: - "package-d@1.0.0_package-c@1.0.0__package-a@1.0.0_package-a@1.0.0" - .to_string(), - copy_index: 0, - dependencies: BTreeMap::from([ - ( - "package-c".to_string(), - "package-c@1.0.0_package-a@1.0.0".to_string(), - ), - ( - "package-e".to_string(), - "package-e@1.0.0_package-a@1.0.0".to_string() - ), - ]), - }, - TestNpmResolutionPackage { - pkg_id: "package-e@1.0.0_package-a@1.0.0".to_string(), - copy_index: 0, - dependencies: BTreeMap::from([( - "package-a".to_string(), - "package-a@1.0.0".to_string() - ),]), - }, - ] - ); - assert_eq!( - package_reqs, - vec![("package-a@1.0.0".to_string(), "package-a@1.0.0".to_string())] - ); - } - - #[derive(Debug, PartialEq, Eq)] - struct TestNpmResolutionPackage { - pub pkg_id: String, - pub copy_index: usize, - pub dependencies: BTreeMap<String, String>, - } - - async fn run_resolver_and_get_output( - api: TestNpmRegistryApiInner, - reqs: Vec<&str>, - ) -> (Vec<TestNpmResolutionPackage>, Vec<(String, String)>) { - let mut graph = Graph::default(); - let api = NpmRegistryApi::new_for_test(api); - let mut resolver = GraphDependencyResolver::new(&mut graph, &api); - - for req in reqs { - let req = NpmPackageReqReference::from_str(req).unwrap().req; - resolver - .add_package_req(&req, &api.package_info(&req.name).await.unwrap()) - .unwrap(); - } - - resolver.resolve_pending().await.unwrap(); - let snapshot = graph.into_snapshot(&api).await.unwrap(); - - { - let new_snapshot = Graph::from_snapshot(snapshot.clone()) - .unwrap() - .into_snapshot(&api) - .await - .unwrap(); - assert_eq!( - snapshot, new_snapshot, - "recreated snapshot should be the same" - ); - // create one again from the new snapshot - let new_snapshot2 = Graph::from_snapshot(new_snapshot.clone()) - .unwrap() - .into_snapshot(&api) - .await - .unwrap(); - assert_eq!( - snapshot, new_snapshot2, - "second recreated snapshot should be the same" - ); - } - - let mut packages = snapshot.all_packages(); - packages.sort_by(|a, b| a.pkg_id.cmp(&b.pkg_id)); - let mut package_reqs = snapshot - .package_reqs - .into_iter() - .map(|(a, b)| { - ( - a.to_string(), - snapshot.root_packages.get(&b).unwrap().as_serialized(), - ) - }) - .collect::<Vec<_>>(); - package_reqs.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string())); - let packages = packages - .into_iter() - .map(|pkg| TestNpmResolutionPackage { - pkg_id: pkg.pkg_id.as_serialized(), - copy_index: pkg.copy_index, - dependencies: pkg - .dependencies - .into_iter() - .map(|(key, value)| (key, value.as_serialized())) - .collect(), - }) - .collect(); - - (packages, package_reqs) - } -} diff --git a/cli/npm/resolution/mod.rs b/cli/npm/resolution/mod.rs deleted file mode 100644 index 82dc1c62c..000000000 --- a/cli/npm/resolution/mod.rs +++ /dev/null @@ -1,636 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::collections::HashSet; -use std::sync::Arc; - -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; -use deno_core::parking_lot::RwLock; -use deno_core::TaskQueue; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageNvReference; -use deno_graph::npm::NpmPackageReq; -use deno_graph::npm::NpmPackageReqReference; -use deno_graph::semver::Version; -use log::debug; -use serde::Deserialize; -use serde::Serialize; -use thiserror::Error; - -use crate::args::Lockfile; -use crate::npm::resolution::common::LATEST_VERSION_REQ; - -use self::common::resolve_best_package_version_and_info; -use self::graph::GraphDependencyResolver; -use self::snapshot::NpmPackagesPartitioned; - -use super::cache::NpmPackageCacheFolderId; -use super::registry::NpmPackageVersionDistInfo; -use super::registry::NpmRegistryApi; - -mod common; -mod graph; -mod snapshot; - -use graph::Graph; -pub use snapshot::NpmResolutionSnapshot; - -#[derive(Debug, Error)] -#[error("Invalid npm package id '{text}'. {message}")] -pub struct NpmPackageNodeIdDeserializationError { - message: String, - text: String, -} - -/// A resolved unique identifier for an npm package. This contains -/// the resolved name, version, and peer dependency resolution identifiers. -#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct NpmPackageId { - pub nv: NpmPackageNv, - pub peer_dependencies: Vec<NpmPackageId>, -} - -// Custom debug implementation for more concise test output -impl std::fmt::Debug for NpmPackageId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_serialized()) - } -} - -impl NpmPackageId { - pub fn as_serialized(&self) -> String { - self.as_serialized_with_level(0) - } - - fn as_serialized_with_level(&self, level: usize) -> String { - // WARNING: This should not change because it's used in the lockfile - let mut result = format!( - "{}@{}", - if level == 0 { - self.nv.name.to_string() - } else { - self.nv.name.replace('/', "+") - }, - self.nv.version - ); - for peer in &self.peer_dependencies { - // unfortunately we can't do something like `_3` when - // this gets deep because npm package names can start - // with a number - result.push_str(&"_".repeat(level + 1)); - result.push_str(&peer.as_serialized_with_level(level + 1)); - } - result - } - - pub fn from_serialized( - id: &str, - ) -> Result<Self, NpmPackageNodeIdDeserializationError> { - use monch::*; - - fn parse_name(input: &str) -> ParseResult<&str> { - if_not_empty(substring(move |input| { - for (pos, c) in input.char_indices() { - // first character might be a scope, so skip it - if pos > 0 && c == '@' { - return Ok((&input[pos..], ())); - } - } - ParseError::backtrace() - }))(input) - } - - fn parse_version(input: &str) -> ParseResult<&str> { - if_not_empty(substring(skip_while(|c| c != '_')))(input) - } - - fn parse_name_and_version(input: &str) -> ParseResult<(String, Version)> { - let (input, name) = parse_name(input)?; - let (input, _) = ch('@')(input)?; - let at_version_input = input; - let (input, version) = parse_version(input)?; - match Version::parse_from_npm(version) { - Ok(version) => Ok((input, (name.to_string(), version))), - Err(err) => ParseError::fail(at_version_input, format!("{err:#}")), - } - } - - fn parse_level_at_level<'a>( - level: usize, - ) -> impl Fn(&'a str) -> ParseResult<'a, ()> { - fn parse_level(input: &str) -> ParseResult<usize> { - let level = input.chars().take_while(|c| *c == '_').count(); - Ok((&input[level..], level)) - } - - move |input| { - let (input, parsed_level) = parse_level(input)?; - if parsed_level == level { - Ok((input, ())) - } else { - ParseError::backtrace() - } - } - } - - fn parse_peers_at_level<'a>( - level: usize, - ) -> impl Fn(&'a str) -> ParseResult<'a, Vec<NpmPackageId>> { - move |mut input| { - let mut peers = Vec::new(); - while let Ok((level_input, _)) = parse_level_at_level(level)(input) { - input = level_input; - let peer_result = parse_id_at_level(level)(input)?; - input = peer_result.0; - peers.push(peer_result.1); - } - Ok((input, peers)) - } - } - - fn parse_id_at_level<'a>( - level: usize, - ) -> impl Fn(&'a str) -> ParseResult<'a, NpmPackageId> { - move |input| { - let (input, (name, version)) = parse_name_and_version(input)?; - let name = if level > 0 { - name.replace('+', "/") - } else { - name - }; - let (input, peer_dependencies) = - parse_peers_at_level(level + 1)(input)?; - Ok(( - input, - NpmPackageId { - nv: NpmPackageNv { name, version }, - peer_dependencies, - }, - )) - } - } - - with_failure_handling(parse_id_at_level(0))(id).map_err(|err| { - NpmPackageNodeIdDeserializationError { - message: format!("{err:#}"), - text: id.to_string(), - } - }) - } -} - -impl Ord for NpmPackageId { - fn cmp(&self, other: &Self) -> Ordering { - match self.nv.cmp(&other.nv) { - Ordering::Equal => self.peer_dependencies.cmp(&other.peer_dependencies), - ordering => ordering, - } - } -} - -impl PartialOrd for NpmPackageId { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) - } -} - -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct NpmResolutionPackage { - pub pkg_id: NpmPackageId, - /// The peer dependency resolution can differ for the same - /// package (name and version) depending on where it is in - /// the resolution tree. This copy index indicates which - /// copy of the package this is. - pub copy_index: usize, - pub dist: NpmPackageVersionDistInfo, - /// Key is what the package refers to the other package as, - /// which could be different from the package name. - pub dependencies: HashMap<String, NpmPackageId>, -} - -impl std::fmt::Debug for NpmResolutionPackage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // custom debug implementation for deterministic output in the tests - f.debug_struct("NpmResolutionPackage") - .field("pkg_id", &self.pkg_id) - .field("copy_index", &self.copy_index) - .field("dist", &self.dist) - .field( - "dependencies", - &self.dependencies.iter().collect::<BTreeMap<_, _>>(), - ) - .finish() - } -} - -impl NpmResolutionPackage { - pub fn get_package_cache_folder_id(&self) -> NpmPackageCacheFolderId { - NpmPackageCacheFolderId { - nv: self.pkg_id.nv.clone(), - copy_index: self.copy_index, - } - } -} - -/// Handles updating and storing npm resolution in memory. -/// -/// This does not interact with the file system. -#[derive(Clone)] -pub struct NpmResolution(Arc<NpmResolutionInner>); - -struct NpmResolutionInner { - api: NpmRegistryApi, - snapshot: RwLock<NpmResolutionSnapshot>, - update_queue: TaskQueue, - maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, -} - -impl std::fmt::Debug for NpmResolution { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let snapshot = self.0.snapshot.read(); - f.debug_struct("NpmResolution") - .field("snapshot", &snapshot) - .finish() - } -} - -impl NpmResolution { - pub fn new( - api: NpmRegistryApi, - initial_snapshot: Option<NpmResolutionSnapshot>, - maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, - ) -> Self { - Self(Arc::new(NpmResolutionInner { - api, - snapshot: RwLock::new(initial_snapshot.unwrap_or_default()), - update_queue: Default::default(), - maybe_lockfile, - })) - } - - pub async fn add_package_reqs( - &self, - package_reqs: Vec<NpmPackageReq>, - ) -> Result<(), AnyError> { - let inner = &self.0; - - // only allow one thread in here at a time - let _permit = inner.update_queue.acquire().await; - let snapshot = inner.snapshot.read().clone(); - - let snapshot = add_package_reqs_to_snapshot( - &inner.api, - package_reqs, - snapshot, - self.0.maybe_lockfile.clone(), - ) - .await?; - - *inner.snapshot.write() = snapshot; - Ok(()) - } - - pub async fn set_package_reqs( - &self, - package_reqs: Vec<NpmPackageReq>, - ) -> Result<(), AnyError> { - let inner = &self.0; - // only allow one thread in here at a time - let _permit = inner.update_queue.acquire().await; - let snapshot = inner.snapshot.read().clone(); - - let reqs_set = package_reqs.iter().collect::<HashSet<_>>(); - let has_removed_package = !snapshot - .package_reqs - .keys() - .all(|req| reqs_set.contains(req)); - // if any packages were removed, we need to completely recreate the npm resolution snapshot - let snapshot = if has_removed_package { - NpmResolutionSnapshot::default() - } else { - snapshot - }; - let snapshot = add_package_reqs_to_snapshot( - &inner.api, - package_reqs, - snapshot, - self.0.maybe_lockfile.clone(), - ) - .await?; - - *inner.snapshot.write() = snapshot; - - Ok(()) - } - - pub async fn resolve_pending(&self) -> Result<(), AnyError> { - let inner = &self.0; - // only allow one thread in here at a time - let _permit = inner.update_queue.acquire().await; - let snapshot = inner.snapshot.read().clone(); - - let snapshot = add_package_reqs_to_snapshot( - &inner.api, - Vec::new(), - snapshot, - self.0.maybe_lockfile.clone(), - ) - .await?; - - *inner.snapshot.write() = snapshot; - - Ok(()) - } - - pub fn pkg_req_ref_to_nv_ref( - &self, - req_ref: NpmPackageReqReference, - ) -> Result<NpmPackageNvReference, AnyError> { - let node_id = self.resolve_pkg_id_from_pkg_req(&req_ref.req)?; - Ok(NpmPackageNvReference { - nv: node_id.nv, - sub_path: req_ref.sub_path, - }) - } - - pub fn resolve_package_cache_folder_id_from_id( - &self, - id: &NpmPackageId, - ) -> Option<NpmPackageCacheFolderId> { - self - .0 - .snapshot - .read() - .package_from_id(id) - .map(|p| p.get_package_cache_folder_id()) - } - - pub fn resolve_package_from_package( - &self, - name: &str, - referrer: &NpmPackageCacheFolderId, - ) -> Result<NpmResolutionPackage, AnyError> { - self - .0 - .snapshot - .read() - .resolve_package_from_package(name, referrer) - .cloned() - } - - /// Resolve a node package from a deno module. - pub fn resolve_pkg_id_from_pkg_req( - &self, - req: &NpmPackageReq, - ) -> Result<NpmPackageId, AnyError> { - self - .0 - .snapshot - .read() - .resolve_pkg_from_pkg_req(req) - .map(|pkg| pkg.pkg_id.clone()) - } - - pub fn resolve_pkg_id_from_deno_module( - &self, - id: &NpmPackageNv, - ) -> Result<NpmPackageId, AnyError> { - self - .0 - .snapshot - .read() - .resolve_package_from_deno_module(id) - .map(|pkg| pkg.pkg_id.clone()) - } - - /// Resolves a package requirement for deno graph. This should only be - /// called by deno_graph's NpmResolver or for resolving packages in - /// a package.json - pub fn resolve_package_req_as_pending( - &self, - pkg_req: &NpmPackageReq, - ) -> Result<NpmPackageNv, AnyError> { - let inner = &self.0; - // we should always have this because it should have been cached before here - let package_info = - inner.api.get_cached_package_info(&pkg_req.name).unwrap(); - - let mut snapshot = inner.snapshot.write(); - let version_req = - pkg_req.version_req.as_ref().unwrap_or(&*LATEST_VERSION_REQ); - let version_and_info = - match snapshot.packages_by_name.get(&package_info.name) { - Some(existing_versions) => resolve_best_package_version_and_info( - version_req, - &package_info, - existing_versions.iter().map(|p| &p.nv.version), - )?, - None => resolve_best_package_version_and_info( - version_req, - &package_info, - Vec::new().iter(), - )?, - }; - let id = NpmPackageNv { - name: package_info.name.to_string(), - version: version_and_info.version, - }; - debug!( - "Resolved {}@{} to {}", - pkg_req.name, - version_req.version_text(), - id.to_string(), - ); - snapshot.package_reqs.insert(pkg_req.clone(), id.clone()); - let packages_with_name = snapshot - .packages_by_name - .entry(package_info.name.clone()) - .or_default(); - if !packages_with_name.iter().any(|p| p.nv == id) { - packages_with_name.push(NpmPackageId { - nv: id.clone(), - peer_dependencies: Vec::new(), - }); - } - snapshot.pending_unresolved_packages.push(id.clone()); - Ok(id) - } - - pub fn all_packages_partitioned(&self) -> NpmPackagesPartitioned { - self.0.snapshot.read().all_packages_partitioned() - } - - pub fn has_packages(&self) -> bool { - !self.0.snapshot.read().packages.is_empty() - } - - pub fn snapshot(&self) -> NpmResolutionSnapshot { - self.0.snapshot.read().clone() - } - - pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { - let snapshot = self.0.snapshot.read(); - for (package_req, nv) in snapshot.package_reqs.iter() { - lockfile.insert_npm_specifier( - package_req.to_string(), - snapshot.root_packages.get(nv).unwrap().as_serialized(), - ); - } - for package in snapshot.all_packages() { - lockfile.check_or_insert_npm_package(package.into())?; - } - Ok(()) - } -} - -async fn add_package_reqs_to_snapshot( - api: &NpmRegistryApi, - package_reqs: Vec<NpmPackageReq>, - snapshot: NpmResolutionSnapshot, - maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, -) -> Result<NpmResolutionSnapshot, AnyError> { - if snapshot.pending_unresolved_packages.is_empty() - && package_reqs - .iter() - .all(|req| snapshot.package_reqs.contains_key(req)) - { - return Ok(snapshot); // already up to date - } - - // convert the snapshot to a traversable graph - let mut graph = Graph::from_snapshot(snapshot).with_context(|| { - deno_core::anyhow::anyhow!( - "Failed creating npm state. Try recreating your lockfile." - ) - })?; - let pending_unresolved = graph.take_pending_unresolved(); - - // avoid loading the info if this is already in the graph - let package_reqs = package_reqs - .into_iter() - .filter(|r| !graph.has_package_req(r)) - .collect::<Vec<_>>(); - let pending_unresolved = pending_unresolved - .into_iter() - .filter(|p| !graph.has_root_package(p)) - .collect::<Vec<_>>(); - - // cache the packages in parallel - api - .cache_in_parallel( - package_reqs - .iter() - .map(|req| req.name.clone()) - .chain(pending_unresolved.iter().map(|id| id.name.clone())) - .collect::<HashSet<_>>() - .into_iter() - .collect::<Vec<_>>(), - ) - .await?; - - // go over the top level package names first (npm package reqs and pending unresolved), - // then down the tree one level at a time through all the branches - let mut resolver = GraphDependencyResolver::new(&mut graph, api); - - // The package reqs and ids should already be sorted - // in the order they should be resolved in. - for package_req in package_reqs { - let info = api.package_info(&package_req.name).await?; - resolver.add_package_req(&package_req, &info)?; - } - - for pkg_id in pending_unresolved { - let info = api.package_info(&pkg_id.name).await?; - resolver.add_root_package(&pkg_id, &info)?; - } - - resolver.resolve_pending().await?; - - let result = graph.into_snapshot(api).await; - api.clear_memory_cache(); - - if let Some(lockfile_mutex) = maybe_lockfile { - let mut lockfile = lockfile_mutex.lock(); - match result { - Ok(snapshot) => { - for (package_req, nv) in snapshot.package_reqs.iter() { - lockfile.insert_npm_specifier( - package_req.to_string(), - snapshot.root_packages.get(nv).unwrap().as_serialized(), - ); - } - for package in snapshot.all_packages() { - lockfile.check_or_insert_npm_package(package.into())?; - } - Ok(snapshot) - } - Err(err) => Err(err), - } - } else { - result - } -} - -#[cfg(test)] -mod test { - use deno_graph::npm::NpmPackageNv; - use deno_graph::semver::Version; - - use super::NpmPackageId; - - #[test] - fn serialize_npm_package_id() { - let id = NpmPackageId { - nv: NpmPackageNv { - name: "pkg-a".to_string(), - version: Version::parse_from_npm("1.2.3").unwrap(), - }, - peer_dependencies: vec![ - NpmPackageId { - nv: NpmPackageNv { - name: "pkg-b".to_string(), - version: Version::parse_from_npm("3.2.1").unwrap(), - }, - peer_dependencies: vec![ - NpmPackageId { - nv: NpmPackageNv { - name: "pkg-c".to_string(), - version: Version::parse_from_npm("1.3.2").unwrap(), - }, - peer_dependencies: vec![], - }, - NpmPackageId { - nv: NpmPackageNv { - name: "pkg-d".to_string(), - version: Version::parse_from_npm("2.3.4").unwrap(), - }, - peer_dependencies: vec![], - }, - ], - }, - NpmPackageId { - nv: NpmPackageNv { - name: "pkg-e".to_string(), - version: Version::parse_from_npm("2.3.1").unwrap(), - }, - peer_dependencies: vec![NpmPackageId { - nv: NpmPackageNv { - name: "pkg-f".to_string(), - version: Version::parse_from_npm("2.3.1").unwrap(), - }, - peer_dependencies: vec![], - }], - }, - ], - }; - - // this shouldn't change because it's used in the lockfile - let serialized = id.as_serialized(); - assert_eq!(serialized, "pkg-a@1.2.3_pkg-b@3.2.1__pkg-c@1.3.2__pkg-d@2.3.4_pkg-e@2.3.1__pkg-f@2.3.1"); - assert_eq!(NpmPackageId::from_serialized(&serialized).unwrap(), id); - } -} diff --git a/cli/npm/resolution/snapshot.rs b/cli/npm/resolution/snapshot.rs deleted file mode 100644 index e8df8286e..000000000 --- a/cli/npm/resolution/snapshot.rs +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::collections::HashSet; -use std::sync::Arc; - -use deno_core::anyhow::anyhow; -use deno_core::anyhow::bail; -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageReq; -use deno_graph::semver::VersionReq; -use serde::Deserialize; -use serde::Serialize; - -use crate::args::Lockfile; -use crate::npm::cache::NpmPackageCacheFolderId; -use crate::npm::registry::NpmPackageVersionDistInfo; -use crate::npm::registry::NpmRegistryApi; - -use super::NpmPackageId; -use super::NpmResolutionPackage; - -/// Packages partitioned by if they are "copy" packages or not. -pub struct NpmPackagesPartitioned { - pub packages: Vec<NpmResolutionPackage>, - /// Since peer dependency resolution occurs based on ancestors and ancestor - /// siblings, this may sometimes cause the same package (name and version) - /// to have different dependencies based on where it appears in the tree. - /// For these packages, we create a "copy package" or duplicate of the package - /// whose dependencies are that of where in the tree they've resolved to. - pub copy_packages: Vec<NpmResolutionPackage>, -} - -impl NpmPackagesPartitioned { - pub fn into_all(self) -> Vec<NpmResolutionPackage> { - let mut packages = self.packages; - packages.extend(self.copy_packages); - packages - } -} - -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct NpmResolutionSnapshot { - /// The unique package requirements map to a single npm package name and version. - #[serde(with = "map_to_vec")] - pub(super) package_reqs: HashMap<NpmPackageReq, NpmPackageNv>, - // Each root level npm package name and version maps to an exact npm package node id. - #[serde(with = "map_to_vec")] - pub(super) root_packages: HashMap<NpmPackageNv, NpmPackageId>, - pub(super) packages_by_name: HashMap<String, Vec<NpmPackageId>>, - #[serde(with = "map_to_vec")] - pub(super) packages: HashMap<NpmPackageId, NpmResolutionPackage>, - /// Ordered list based on resolution of packages whose dependencies - /// have not yet been resolved - pub(super) pending_unresolved_packages: Vec<NpmPackageNv>, -} - -impl std::fmt::Debug for NpmResolutionSnapshot { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // do a custom debug implementation that creates deterministic output for the tests - f.debug_struct("NpmResolutionSnapshot") - .field( - "package_reqs", - &self.package_reqs.iter().collect::<BTreeMap<_, _>>(), - ) - .field( - "root_packages", - &self.root_packages.iter().collect::<BTreeMap<_, _>>(), - ) - .field( - "packages_by_name", - &self.packages_by_name.iter().collect::<BTreeMap<_, _>>(), - ) - .field( - "packages", - &self.packages.iter().collect::<BTreeMap<_, _>>(), - ) - .field( - "pending_unresolved_packages", - &self.pending_unresolved_packages, - ) - .finish() - } -} - -// This is done so the maps with non-string keys get serialized and deserialized as vectors. -// Adapted from: https://github.com/serde-rs/serde/issues/936#issuecomment-302281792 -mod map_to_vec { - use std::collections::HashMap; - - use serde::de::Deserialize; - use serde::de::Deserializer; - use serde::ser::Serializer; - use serde::Serialize; - - pub fn serialize<S, K: Serialize, V: Serialize>( - map: &HashMap<K, V>, - serializer: S, - ) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.collect_seq(map.iter()) - } - - pub fn deserialize< - 'de, - D, - K: Deserialize<'de> + Eq + std::hash::Hash, - V: Deserialize<'de>, - >( - deserializer: D, - ) -> Result<HashMap<K, V>, D::Error> - where - D: Deserializer<'de>, - { - let mut map = HashMap::new(); - for (key, value) in Vec::<(K, V)>::deserialize(deserializer)? { - map.insert(key, value); - } - Ok(map) - } -} - -impl NpmResolutionSnapshot { - /// Gets if this snapshot is empty. - pub fn is_empty(&self) -> bool { - self.packages.is_empty() && self.pending_unresolved_packages.is_empty() - } - - /// Resolve a package from a package requirement. - pub fn resolve_pkg_from_pkg_req( - &self, - req: &NpmPackageReq, - ) -> Result<&NpmResolutionPackage, AnyError> { - match self.package_reqs.get(req) { - Some(id) => self.resolve_package_from_deno_module(id), - None => bail!("could not find npm package directory for '{}'", req), - } - } - - /// Resolve a package from a deno module. - pub fn resolve_package_from_deno_module( - &self, - id: &NpmPackageNv, - ) -> Result<&NpmResolutionPackage, AnyError> { - match self.root_packages.get(id) { - Some(id) => Ok(self.packages.get(id).unwrap()), - None => bail!("could not find npm package directory for '{}'", id), - } - } - - pub fn top_level_packages(&self) -> Vec<NpmPackageId> { - self.root_packages.values().cloned().collect::<Vec<_>>() - } - - pub fn package_from_id( - &self, - id: &NpmPackageId, - ) -> Option<&NpmResolutionPackage> { - self.packages.get(id) - } - - pub fn resolve_package_from_package( - &self, - name: &str, - referrer: &NpmPackageCacheFolderId, - ) -> Result<&NpmResolutionPackage, AnyError> { - // todo(dsherret): do we need an additional hashmap to get this quickly? - let referrer_package = self - .packages_by_name - .get(&referrer.nv.name) - .and_then(|packages| { - packages - .iter() - .filter(|p| p.nv.version == referrer.nv.version) - .filter_map(|node_id| { - let package = self.packages.get(node_id)?; - if package.copy_index == referrer.copy_index { - Some(package) - } else { - None - } - }) - .next() - }) - .ok_or_else(|| { - anyhow!("could not find referrer npm package '{}'", referrer) - })?; - - let name = name_without_path(name); - if let Some(id) = referrer_package.dependencies.get(name) { - return Ok(self.packages.get(id).unwrap()); - } - - if referrer_package.pkg_id.nv.name == name { - return Ok(referrer_package); - } - - // TODO(bartlomieju): this should use a reverse lookup table in the - // snapshot instead of resolving best version again. - let any_version_req = VersionReq::parse_from_npm("*").unwrap(); - if let Some(id) = self.resolve_best_package_id(name, &any_version_req) { - if let Some(pkg) = self.packages.get(&id) { - return Ok(pkg); - } - } - - bail!( - "could not find npm package '{}' referenced by '{}'", - name, - referrer - ) - } - - pub fn all_packages(&self) -> Vec<NpmResolutionPackage> { - self.packages.values().cloned().collect() - } - - pub fn all_packages_partitioned(&self) -> NpmPackagesPartitioned { - let mut packages = self.all_packages(); - let mut copy_packages = Vec::with_capacity(packages.len() / 2); // at most 1 copy for every package - - // partition out any packages that are "copy" packages - for i in (0..packages.len()).rev() { - if packages[i].copy_index > 0 { - copy_packages.push(packages.swap_remove(i)); - } - } - - NpmPackagesPartitioned { - packages, - copy_packages, - } - } - - pub fn resolve_best_package_id( - &self, - name: &str, - version_req: &VersionReq, - ) -> Option<NpmPackageId> { - // todo(dsherret): this is not exactly correct because some ids - // will be better than others due to peer dependencies - let mut maybe_best_id: Option<&NpmPackageId> = None; - if let Some(node_ids) = self.packages_by_name.get(name) { - for node_id in node_ids.iter() { - if version_req.matches(&node_id.nv.version) { - let is_best_version = maybe_best_id - .as_ref() - .map(|best_id| best_id.nv.version.cmp(&node_id.nv.version).is_lt()) - .unwrap_or(true); - if is_best_version { - maybe_best_id = Some(node_id); - } - } - } - } - maybe_best_id.cloned() - } - - pub async fn from_lockfile( - lockfile: Arc<Mutex<Lockfile>>, - api: &NpmRegistryApi, - ) -> Result<Self, AnyError> { - let mut package_reqs: HashMap<NpmPackageReq, NpmPackageNv>; - let mut root_packages: HashMap<NpmPackageNv, NpmPackageId>; - let mut packages_by_name: HashMap<String, Vec<NpmPackageId>>; - let mut packages: HashMap<NpmPackageId, NpmResolutionPackage>; - let mut copy_index_resolver: SnapshotPackageCopyIndexResolver; - - { - let lockfile = lockfile.lock(); - - // pre-allocate collections - package_reqs = - HashMap::with_capacity(lockfile.content.npm.specifiers.len()); - root_packages = - HashMap::with_capacity(lockfile.content.npm.specifiers.len()); - let packages_len = lockfile.content.npm.packages.len(); - packages = HashMap::with_capacity(packages_len); - packages_by_name = HashMap::with_capacity(packages_len); // close enough - copy_index_resolver = - SnapshotPackageCopyIndexResolver::with_capacity(packages_len); - let mut verify_ids = HashSet::with_capacity(packages_len); - - // collect the specifiers to version mappings - for (key, value) in &lockfile.content.npm.specifiers { - let package_req = NpmPackageReq::from_str(key) - .with_context(|| format!("Unable to parse npm specifier: {key}"))?; - let package_id = NpmPackageId::from_serialized(value)?; - package_reqs.insert(package_req, package_id.nv.clone()); - root_packages.insert(package_id.nv.clone(), package_id.clone()); - verify_ids.insert(package_id.clone()); - } - - // then the packages - for (key, value) in &lockfile.content.npm.packages { - let package_id = NpmPackageId::from_serialized(key)?; - - // collect the dependencies - let mut dependencies = HashMap::default(); - - packages_by_name - .entry(package_id.nv.name.to_string()) - .or_default() - .push(package_id.clone()); - - for (name, specifier) in &value.dependencies { - let dep_id = NpmPackageId::from_serialized(specifier)?; - dependencies.insert(name.to_string(), dep_id.clone()); - verify_ids.insert(dep_id); - } - - let package = NpmResolutionPackage { - pkg_id: package_id.clone(), - copy_index: copy_index_resolver.resolve(&package_id), - // temporary dummy value - dist: NpmPackageVersionDistInfo::default(), - dependencies, - }; - - packages.insert(package_id, package); - } - - // verify that all these ids exist in packages - for id in &verify_ids { - if !packages.contains_key(id) { - bail!( - "the lockfile is corrupt. You can recreate it with --lock-write" - ); - } - } - } - - api - .cache_in_parallel(packages_by_name.keys().cloned().collect()) - .await?; - - // ensure the dist is set for each package - for package in packages.values_mut() { - // this will read from the memory cache now - let version_info = match api - .package_version_info(&package.pkg_id.nv) - .await? - { - Some(version_info) => version_info, - None => { - bail!("could not find '{}' specified in the lockfile. Maybe try again with --reload", package.pkg_id.nv); - } - }; - package.dist = version_info.dist; - } - - Ok(Self { - package_reqs, - root_packages, - packages_by_name, - packages, - pending_unresolved_packages: Default::default(), - }) - } -} - -pub struct SnapshotPackageCopyIndexResolver { - packages_to_copy_index: HashMap<NpmPackageId, usize>, - package_name_version_to_copy_count: HashMap<NpmPackageNv, usize>, -} - -impl SnapshotPackageCopyIndexResolver { - pub fn with_capacity(capacity: usize) -> Self { - Self { - packages_to_copy_index: HashMap::with_capacity(capacity), - package_name_version_to_copy_count: HashMap::with_capacity(capacity), // close enough - } - } - - pub fn from_map_with_capacity( - mut packages_to_copy_index: HashMap<NpmPackageId, usize>, - capacity: usize, - ) -> Self { - let mut package_name_version_to_copy_count = - HashMap::with_capacity(capacity); // close enough - if capacity > packages_to_copy_index.len() { - packages_to_copy_index.reserve(capacity - packages_to_copy_index.len()); - } - - for (node_id, index) in &packages_to_copy_index { - let entry = package_name_version_to_copy_count - .entry(node_id.nv.clone()) - .or_insert(0); - if *entry < *index { - *entry = *index; - } - } - Self { - packages_to_copy_index, - package_name_version_to_copy_count, - } - } - - pub fn resolve(&mut self, node_id: &NpmPackageId) -> usize { - if let Some(index) = self.packages_to_copy_index.get(node_id) { - *index - } else { - let index = *self - .package_name_version_to_copy_count - .entry(node_id.nv.clone()) - .and_modify(|count| { - *count += 1; - }) - .or_insert(0); - self.packages_to_copy_index.insert(node_id.clone(), index); - index - } - } -} - -fn name_without_path(name: &str) -> &str { - let mut search_start_index = 0; - if name.starts_with('@') { - if let Some(slash_index) = name.find('/') { - search_start_index = slash_index + 1; - } - } - if let Some(slash_index) = &name[search_start_index..].find('/') { - // get the name up until the path slash - &name[0..search_start_index + slash_index] - } else { - name - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_name_without_path() { - assert_eq!(name_without_path("foo"), "foo"); - assert_eq!(name_without_path("@foo/bar"), "@foo/bar"); - assert_eq!(name_without_path("@foo/bar/baz"), "@foo/bar"); - assert_eq!(name_without_path("@hello"), "@hello"); - } - - #[test] - fn test_copy_index_resolver() { - let mut copy_index_resolver = - SnapshotPackageCopyIndexResolver::with_capacity(10); - assert_eq!( - copy_index_resolver - .resolve(&NpmPackageId::from_serialized("package@1.0.0").unwrap()), - 0 - ); - assert_eq!( - copy_index_resolver - .resolve(&NpmPackageId::from_serialized("package@1.0.0").unwrap()), - 0 - ); - assert_eq!( - copy_index_resolver.resolve( - &NpmPackageId::from_serialized("package@1.0.0_package-b@1.0.0") - .unwrap() - ), - 1 - ); - assert_eq!( - copy_index_resolver.resolve( - &NpmPackageId::from_serialized( - "package@1.0.0_package-b@1.0.0__package-c@2.0.0" - ) - .unwrap() - ), - 2 - ); - assert_eq!( - copy_index_resolver.resolve( - &NpmPackageId::from_serialized("package@1.0.0_package-b@1.0.0") - .unwrap() - ), - 1 - ); - assert_eq!( - copy_index_resolver - .resolve(&NpmPackageId::from_serialized("package-b@1.0.0").unwrap()), - 0 - ); - } -} diff --git a/cli/npm/resolvers/common.rs b/cli/npm/resolvers/common.rs index a8e822bb9..8b8be5ce2 100644 --- a/cli/npm/resolvers/common.rs +++ b/cli/npm/resolvers/common.rs @@ -9,13 +9,13 @@ use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::futures; use deno_core::url::Url; +use deno_npm::NpmPackageId; +use deno_npm::NpmResolutionPackage; use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NodeResolutionMode; use crate::npm::cache::should_sync_download; use crate::npm::NpmCache; -use crate::npm::NpmPackageId; -use crate::npm::NpmResolutionPackage; /// Part of the resolution that interacts with the file system. #[async_trait] diff --git a/cli/npm/resolvers/global.rs b/cli/npm/resolvers/global.rs index 5d5334299..518a9110a 100644 --- a/cli/npm/resolvers/global.rs +++ b/cli/npm/resolvers/global.rs @@ -9,15 +9,15 @@ use async_trait::async_trait; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::url::Url; +use deno_npm::NpmPackageCacheFolderId; +use deno_npm::NpmPackageId; +use deno_npm::NpmResolutionPackage; use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NodeResolutionMode; -use crate::npm::cache::NpmPackageCacheFolderId; use crate::npm::resolution::NpmResolution; use crate::npm::resolvers::common::cache_packages; use crate::npm::NpmCache; -use crate::npm::NpmPackageId; -use crate::npm::NpmResolutionPackage; use super::common::ensure_registry_read_permission; use super::common::types_package_name; diff --git a/cli/npm/resolvers/local.rs b/cli/npm/resolvers/local.rs index f9cc9b09b..59d8b0829 100644 --- a/cli/npm/resolvers/local.rs +++ b/cli/npm/resolvers/local.rs @@ -19,6 +19,9 @@ use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::url::Url; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::NpmPackageCacheFolderId; +use deno_npm::NpmPackageId; use deno_runtime::deno_core::futures; use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NodeResolutionMode; @@ -27,11 +30,8 @@ use tokio::task::JoinHandle; use crate::npm::cache::mixed_case_package_name_encode; use crate::npm::cache::should_sync_download; -use crate::npm::cache::NpmPackageCacheFolderId; use crate::npm::resolution::NpmResolution; -use crate::npm::resolution::NpmResolutionSnapshot; use crate::npm::NpmCache; -use crate::npm::NpmPackageId; use crate::util::fs::copy_dir_recursive; use crate::util::fs::hard_link_dir_recursive; @@ -386,12 +386,7 @@ async fn sync_resolution_with_fs( // node_modules/.deno/<package_id>/node_modules/<package_name> let mut found_names = HashSet::new(); let mut pending_packages = VecDeque::new(); - pending_packages.extend( - snapshot - .top_level_packages() - .into_iter() - .map(|id| (id, true)), - ); + pending_packages.extend(snapshot.top_level_packages().map(|id| (id, true))); while let Some((id, is_top_level)) = pending_packages.pop_front() { let root_folder_name = if found_names.insert(id.nv.name.clone()) { id.nv.name.clone() @@ -400,7 +395,7 @@ async fn sync_resolution_with_fs( } else { continue; // skip, already handled }; - let package = snapshot.package_from_id(&id).unwrap(); + let package = snapshot.package_from_id(id).unwrap(); let local_registry_package_path = join_package_name( &deno_local_registry_dir .join(get_package_folder_id_folder_name( @@ -415,7 +410,7 @@ async fn sync_resolution_with_fs( &join_package_name(root_node_modules_dir_path, &root_folder_name), )?; for id in package.dependencies.values() { - pending_packages.push_back((id.clone(), false)); + pending_packages.push_back((id, false)); } } diff --git a/cli/npm/resolvers/mod.rs b/cli/npm/resolvers/mod.rs index c8ac6f44a..c958743dc 100644 --- a/cli/npm/resolvers/mod.rs +++ b/cli/npm/resolvers/mod.rs @@ -4,26 +4,29 @@ mod common; mod global; mod local; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; + use deno_ast::ModuleSpecifier; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_core::url::Url; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageNvReference; -use deno_graph::npm::NpmPackageReq; -use deno_graph::npm::NpmPackageReqReference; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::NpmPackageId; use deno_runtime::deno_node::NodePermissions; use deno_runtime::deno_node::NodeResolutionMode; use deno_runtime::deno_node::PathClean; use deno_runtime::deno_node::RequireNpmResolver; +use deno_semver::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageNvReference; +use deno_semver::npm::NpmPackageReq; +use deno_semver::npm::NpmPackageReqReference; use global::GlobalNpmPackageResolver; use serde::Deserialize; use serde::Serialize; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; use crate::args::Lockfile; use crate::util::fs::canonicalize_path_maybe_not_exists; @@ -33,8 +36,6 @@ use self::common::NpmPackageFsResolver; use self::local::LocalNpmPackageResolver; use super::resolution::NpmResolution; use super::NpmCache; -use super::NpmPackageId; -use super::NpmResolutionSnapshot; /// State provided to the process via an environment variable. #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/cli/npm/tarball.rs b/cli/npm/tarball.rs index 1f804a9aa..ce1ac3339 100644 --- a/cli/npm/tarball.rs +++ b/cli/npm/tarball.rs @@ -7,13 +7,13 @@ use std::path::PathBuf; use deno_core::anyhow::bail; use deno_core::error::AnyError; -use deno_graph::npm::NpmPackageNv; +use deno_npm::registry::NpmPackageVersionDistInfo; +use deno_semver::npm::NpmPackageNv; use flate2::read::GzDecoder; use tar::Archive; use tar::EntryType; use super::cache::with_folder_sync_lock; -use super::registry::NpmPackageVersionDistInfo; pub fn verify_and_extract_tarball( package: &NpmPackageNv, @@ -116,7 +116,7 @@ fn extract_tarball(data: &[u8], output_folder: &Path) -> Result<(), AnyError> { #[cfg(test)] mod test { - use deno_graph::semver::Version; + use deno_semver::Version; use super::*; diff --git a/cli/proc_state.rs b/cli/proc_state.rs index ab3d0bc4d..188f57289 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -28,7 +28,7 @@ use crate::node::NodeResolution; use crate::npm::create_npm_fs_resolver; use crate::npm::NpmCache; use crate::npm::NpmPackageResolver; -use crate::npm::NpmRegistryApi; +use crate::npm::NpmRegistry; use crate::npm::NpmResolution; use crate::npm::PackageJsonDepsInstaller; use crate::resolver::CliGraphResolver; @@ -47,7 +47,6 @@ use deno_core::resolve_url_or_path; use deno_core::CompiledWasmModuleStore; use deno_core::ModuleSpecifier; use deno_core::SharedArrayBufferStore; -use deno_graph::npm::NpmPackageReqReference; use deno_graph::source::Loader; use deno_graph::source::Resolver; use deno_graph::Module; @@ -59,6 +58,7 @@ use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_web::BlobStore; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::permissions::PermissionsContainer; +use deno_semver::npm::NpmPackageReqReference; use import_map::ImportMap; use log::warn; use std::borrow::Cow; @@ -95,7 +95,7 @@ pub struct Inner { pub resolver: Arc<CliGraphResolver>, maybe_file_watcher_reporter: Option<FileWatcherReporter>, pub node_analysis_cache: NodeAnalysisCache, - pub npm_api: NpmRegistryApi, + pub npm_api: NpmRegistry, pub npm_cache: NpmCache, pub npm_resolver: NpmPackageResolver, pub npm_resolution: NpmResolution, @@ -233,14 +233,14 @@ impl ProcState { let lockfile = cli_options.maybe_lock_file(); - let npm_registry_url = NpmRegistryApi::default_url().to_owned(); + let npm_registry_url = NpmRegistry::default_url().to_owned(); let npm_cache = NpmCache::from_deno_dir( &dir, cli_options.cache_setting(), http_client.clone(), progress_bar.clone(), ); - let npm_api = NpmRegistryApi::new( + let npm_api = NpmRegistry::new( npm_registry_url.clone(), npm_cache.clone(), http_client.clone(), diff --git a/cli/resolver.rs b/cli/resolver.rs index b113fc470..5861a758f 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -8,19 +8,20 @@ use deno_core::futures::future::LocalBoxFuture; use deno_core::futures::FutureExt; use deno_core::ModuleSpecifier; use deno_core::TaskQueue; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageReq; use deno_graph::source::NpmResolver; use deno_graph::source::Resolver; use deno_graph::source::UnknownBuiltInNodeModuleError; use deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE; +use deno_npm::registry::NpmRegistryApi; use deno_runtime::deno_node::is_builtin_node_module; +use deno_semver::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageReq; use import_map::ImportMap; use std::sync::Arc; use crate::args::package_json::PackageJsonDeps; use crate::args::JsxImportSourceConfig; -use crate::npm::NpmRegistryApi; +use crate::npm::NpmRegistry; use crate::npm::NpmResolution; use crate::npm::PackageJsonDepsInstaller; @@ -32,7 +33,7 @@ pub struct CliGraphResolver { maybe_default_jsx_import_source: Option<String>, maybe_jsx_import_source_module: Option<String>, no_npm: bool, - npm_registry_api: NpmRegistryApi, + npm_registry_api: NpmRegistry, npm_resolution: NpmResolution, package_json_deps_installer: PackageJsonDepsInstaller, sync_download_queue: Option<Arc<TaskQueue>>, @@ -42,7 +43,7 @@ impl Default for CliGraphResolver { fn default() -> Self { // This is not ideal, but necessary for the LSP. In the future, we should // refactor the LSP and force this to be initialized. - let npm_registry_api = NpmRegistryApi::new_uninitialized(); + let npm_registry_api = NpmRegistry::new_uninitialized(); let npm_resolution = NpmResolution::new(npm_registry_api.clone(), None, None); Self { @@ -63,7 +64,7 @@ impl CliGraphResolver { maybe_jsx_import_source_config: Option<JsxImportSourceConfig>, maybe_import_map: Option<Arc<ImportMap>>, no_npm: bool, - npm_registry_api: NpmRegistryApi, + npm_registry_api: NpmRegistry, npm_resolution: NpmResolution, package_json_deps_installer: PackageJsonDepsInstaller, ) -> Self { diff --git a/cli/standalone.rs b/cli/standalone.rs index 08caacda6..cef8e5afd 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -187,7 +187,7 @@ impl ModuleLoader for EmbeddedModuleLoader { } let module = module?; - let code = module.source().await; + let code = module.source().await.unwrap_or_default(); let code = std::str::from_utf8(&code) .map_err(|_| type_error("Module source is not utf-8"))? .to_owned() diff --git a/cli/tests/integration/npm_tests.rs b/cli/tests/integration/npm_tests.rs index ea93d8879..606e63224 100644 --- a/cli/tests/integration/npm_tests.rs +++ b/cli/tests/integration/npm_tests.rs @@ -1119,7 +1119,13 @@ fn lock_file_missing_top_level_package() { let stderr = String::from_utf8(output.stderr).unwrap(); assert_eq!( stderr, - "error: failed reading lockfile 'deno.lock'\n\nCaused by:\n the lockfile is corrupt. You can recreate it with --lock-write\n" + concat!( + "error: failed reading lockfile 'deno.lock'\n", + "\n", + "Caused by:\n", + " 0: The lockfile is corrupt. You can recreate it with --lock-write\n", + " 1: Could not find referenced package 'cowsay@1.5.0' in the list of packages.\n" + ) ); } diff --git a/cli/tests/testdata/npm/deno_run_non_existent.out b/cli/tests/testdata/npm/deno_run_non_existent.out index f0e8a0791..3bb6d146c 100644 --- a/cli/tests/testdata/npm/deno_run_non_existent.out +++ b/cli/tests/testdata/npm/deno_run_non_existent.out @@ -1,2 +1,2 @@ Download http://localhost:4545/npm/registry/mkdirp -error: Could not find npm package 'mkdirp' matching 0.5.125. Try retrieving the latest npm package information by running with --reload +error: Could not find npm package 'mkdirp' matching '0.5.125'. Try retrieving the latest npm package information by running with --reload diff --git a/cli/tools/info.rs b/cli/tools/info.rs index 5d4bc7bad..566eb1387 100644 --- a/cli/tools/info.rs +++ b/cli/tools/info.rs @@ -10,25 +10,25 @@ use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::serde_json; use deno_core::serde_json::json; -use deno_graph::npm::NpmPackageNv; -use deno_graph::npm::NpmPackageNvReference; -use deno_graph::npm::NpmPackageReqReference; use deno_graph::Dependency; use deno_graph::Module; use deno_graph::ModuleError; use deno_graph::ModuleGraph; use deno_graph::ModuleGraphError; use deno_graph::Resolution; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::NpmPackageId; +use deno_npm::NpmResolutionPackage; use deno_runtime::colors; +use deno_semver::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageNvReference; +use deno_semver::npm::NpmPackageReqReference; use crate::args::Flags; use crate::args::InfoFlags; use crate::display; use crate::graph_util::graph_lock_or_exit; -use crate::npm::NpmPackageId; use crate::npm::NpmPackageResolver; -use crate::npm::NpmResolutionPackage; -use crate::npm::NpmResolutionSnapshot; use crate::proc_state::ProcState; use crate::util::checksum; diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs index 86291b2c9..2578c3845 100644 --- a/cli/tools/installer.rs +++ b/cli/tools/installer.rs @@ -15,7 +15,7 @@ use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::url::Url; -use deno_graph::npm::NpmPackageReqReference; +use deno_semver::npm::NpmPackageReqReference; use log::Level; use once_cell::sync::Lazy; use regex::Regex; diff --git a/cli/tools/repl/session.rs b/cli/tools/repl/session.rs index 95233de05..7fc251362 100644 --- a/cli/tools/repl/session.rs +++ b/cli/tools/repl/session.rs @@ -18,10 +18,10 @@ use deno_core::futures::StreamExt; use deno_core::serde_json; use deno_core::serde_json::Value; use deno_core::LocalInspectorSession; -use deno_graph::npm::NpmPackageReqReference; use deno_graph::source::Resolver; use deno_runtime::deno_node; use deno_runtime::worker::MainWorker; +use deno_semver::npm::NpmPackageReqReference; use once_cell::sync::Lazy; use super::cdp; diff --git a/cli/tools/task.rs b/cli/tools/task.rs index 65601aa69..33595ad2e 100644 --- a/cli/tools/task.rs +++ b/cli/tools/task.rs @@ -10,7 +10,7 @@ use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures; use deno_core::futures::future::LocalBoxFuture; -use deno_graph::npm::NpmPackageNv; +use deno_semver::npm::NpmPackageNv; use deno_task_shell::ExecuteResult; use deno_task_shell::ShellCommand; use deno_task_shell::ShellCommandContext; diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index 933dad095..e1f86dccf 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -17,7 +17,7 @@ use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures::future::BoxFuture; use deno_core::futures::FutureExt; -use deno_graph::semver::Version; +use deno_semver::Version; use once_cell::sync::Lazy; use std::borrow::Cow; use std::env; diff --git a/cli/tools/vendor/test.rs b/cli/tools/vendor/test.rs index 177a80b8a..5b1f792c5 100644 --- a/cli/tools/vendor/test.rs +++ b/cli/tools/vendor/test.rs @@ -20,7 +20,7 @@ use deno_graph::ModuleGraph; use import_map::ImportMap; use crate::cache::ParsedSourceCache; -use crate::npm::NpmRegistryApi; +use crate::npm::NpmRegistry; use crate::npm::NpmResolution; use crate::npm::PackageJsonDepsInstaller; use crate::resolver::CliGraphResolver; @@ -264,7 +264,7 @@ async fn build_test_graph( analyzer: &dyn deno_graph::ModuleAnalyzer, ) -> ModuleGraph { let resolver = original_import_map.map(|m| { - let npm_registry_api = NpmRegistryApi::new_uninitialized(); + let npm_registry_api = NpmRegistry::new_uninitialized(); let npm_resolution = NpmResolution::new(npm_registry_api.clone(), None, None); let deps_installer = PackageJsonDepsInstaller::new( diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index 3bd8efefa..919bad0b1 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -31,13 +31,13 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_core::RuntimeOptions; use deno_core::Snapshot; -use deno_graph::npm::NpmPackageNvReference; -use deno_graph::npm::NpmPackageReqReference; use deno_graph::Module; use deno_graph::ModuleGraph; use deno_graph::ResolutionResolved; use deno_runtime::deno_node::NodeResolutionMode; use deno_runtime::permissions::PermissionsContainer; +use deno_semver::npm::NpmPackageNvReference; +use deno_semver::npm::NpmPackageReqReference; use lsp_types::Url; use once_cell::sync::Lazy; use std::borrow::Cow; diff --git a/cli/worker.rs b/cli/worker.rs index edd604519..26b70d9a5 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -15,7 +15,6 @@ use deno_core::serde_v8; use deno_core::v8; use deno_core::Extension; use deno_core::ModuleId; -use deno_graph::npm::NpmPackageReqReference; use deno_runtime::colors; use deno_runtime::deno_node; use deno_runtime::fmt_errors::format_js_error; @@ -27,6 +26,7 @@ use deno_runtime::web_worker::WebWorkerOptions; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; use deno_runtime::BootstrapOptions; +use deno_semver::npm::NpmPackageReqReference; use crate::args::DenoSubcommand; use crate::errors; |