summaryrefslogtreecommitdiff
path: root/cli/npm/resolution.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/npm/resolution.rs')
-rw-r--r--cli/npm/resolution.rs466
1 files changed, 466 insertions, 0 deletions
diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs
new file mode 100644
index 000000000..4caa27330
--- /dev/null
+++ b/cli/npm/resolution.rs
@@ -0,0 +1,466 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::cmp::Ordering;
+use std::collections::HashMap;
+use std::collections::VecDeque;
+
+use deno_ast::ModuleSpecifier;
+use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::RwLock;
+
+use super::registry::NpmPackageInfo;
+use super::registry::NpmPackageVersionDistInfo;
+use super::registry::NpmPackageVersionInfo;
+use super::registry::NpmRegistryApi;
+
+/// The version matcher used for npm schemed urls is more strict than
+/// the one used by npm packages.
+pub trait NpmVersionMatcher {
+ fn matches(&self, version: &semver::Version) -> bool;
+ fn version_text(&self) -> String;
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct NpmPackageReference {
+ pub req: NpmPackageReq,
+ pub sub_path: Option<String>,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
+pub struct NpmPackageReq {
+ pub name: String,
+ pub version_req: Option<semver::VersionReq>,
+}
+
+impl std::fmt::Display for NpmPackageReq {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match &self.version_req {
+ Some(req) => write!(f, "{}@{}", self.name, req),
+ None => write!(f, "{}", self.name),
+ }
+ }
+}
+
+impl NpmVersionMatcher for NpmPackageReq {
+ fn matches(&self, version: &semver::Version) -> bool {
+ match &self.version_req {
+ Some(req) => req.matches(version),
+ None => version.pre.is_empty(),
+ }
+ }
+
+ fn version_text(&self) -> String {
+ self
+ .version_req
+ .as_ref()
+ .map(|v| format!("{}", v))
+ .unwrap_or_else(|| "non-prerelease".to_string())
+ }
+}
+
+impl NpmPackageReference {
+ pub fn from_specifier(
+ specifier: &ModuleSpecifier,
+ ) -> Result<NpmPackageReference, AnyError> {
+ Self::from_str(specifier.as_str())
+ }
+
+ pub fn from_str(specifier: &str) -> Result<NpmPackageReference, AnyError> {
+ let specifier = match specifier.strip_prefix("npm:") {
+ Some(s) => s,
+ None => {
+ bail!("Not an npm specifier: '{}'", specifier);
+ }
+ };
+ let (name, version_req) = match specifier.rsplit_once('@') {
+ Some((name, version_req)) => (
+ name,
+ match semver::VersionReq::parse(version_req) {
+ Ok(v) => Some(v),
+ Err(_) => None, // not a version requirement
+ },
+ ),
+ None => (specifier, None),
+ };
+ Ok(NpmPackageReference {
+ req: NpmPackageReq {
+ name: name.to_string(),
+ version_req,
+ },
+ // todo: implement and support this
+ sub_path: None,
+ })
+ }
+}
+
+impl std::fmt::Display for NpmPackageReference {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if let Some(sub_path) = &self.sub_path {
+ write!(f, "{}/{}", self.req, sub_path)
+ } else {
+ write!(f, "{}", self.req)
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct NpmPackageId {
+ pub name: String,
+ pub version: semver::Version,
+}
+
+impl NpmPackageId {
+ pub fn scope(&self) -> Option<&str> {
+ if self.name.starts_with('@') && self.name.contains('/') {
+ self.name.split('/').next()
+ } else {
+ None
+ }
+ }
+}
+
+impl std::fmt::Display for NpmPackageId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}@{}", self.name, self.version)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct NpmResolutionPackage {
+ pub id: NpmPackageId,
+ 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>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct NpmResolutionSnapshot {
+ package_reqs: HashMap<NpmPackageReq, semver::Version>,
+ packages_by_name: HashMap<String, Vec<semver::Version>>,
+ packages: HashMap<NpmPackageId, NpmResolutionPackage>,
+}
+
+impl NpmResolutionSnapshot {
+ /// Resolve a node package from a deno module.
+ pub fn resolve_package_from_deno_module(
+ &self,
+ req: &NpmPackageReq,
+ ) -> Result<&NpmResolutionPackage, AnyError> {
+ match self.package_reqs.get(req) {
+ Some(version) => Ok(
+ self
+ .packages
+ .get(&NpmPackageId {
+ name: req.name.clone(),
+ version: version.clone(),
+ })
+ .unwrap(),
+ ),
+ None => bail!("could not find npm package directory for '{}'", req),
+ }
+ }
+
+ pub fn resolve_package_from_package(
+ &self,
+ name: &str,
+ referrer: &NpmPackageId,
+ ) -> Result<&NpmResolutionPackage, AnyError> {
+ match self.packages.get(referrer) {
+ Some(referrer_package) => match referrer_package.dependencies.get(name) {
+ Some(id) => Ok(self.packages.get(id).unwrap()),
+ None => {
+ bail!(
+ "could not find package '{}' referenced by '{}'",
+ name,
+ referrer
+ )
+ }
+ },
+ None => bail!("could not find referrer package '{}'", referrer),
+ }
+ }
+
+ pub fn all_packages(&self) -> Vec<NpmResolutionPackage> {
+ self.packages.values().cloned().collect()
+ }
+
+ pub fn resolve_best_package_version(
+ &self,
+ name: &str,
+ version_matcher: &impl NpmVersionMatcher,
+ ) -> Option<semver::Version> {
+ let mut maybe_best_version: Option<&semver::Version> = None;
+ if let Some(versions) = self.packages_by_name.get(name) {
+ for version in versions {
+ if version_matcher.matches(version) {
+ 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);
+ }
+ }
+ }
+ }
+ maybe_best_version.cloned()
+ }
+}
+
+pub struct NpmResolution {
+ api: NpmRegistryApi,
+ snapshot: RwLock<NpmResolutionSnapshot>,
+ update_sempahore: tokio::sync::Semaphore,
+}
+
+impl std::fmt::Debug for NpmResolution {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let snapshot = self.snapshot.read();
+ f.debug_struct("NpmResolution")
+ .field("snapshot", &snapshot)
+ .finish()
+ }
+}
+
+impl NpmResolution {
+ pub fn new(api: NpmRegistryApi) -> Self {
+ Self {
+ api,
+ snapshot: Default::default(),
+ update_sempahore: tokio::sync::Semaphore::new(1),
+ }
+ }
+
+ pub async fn add_package_reqs(
+ &self,
+ mut packages: Vec<NpmPackageReq>,
+ ) -> Result<(), AnyError> {
+ // multiple packages are resolved in alphabetical order
+ packages.sort_by(|a, b| a.name.cmp(&b.name));
+
+ // only allow one thread in here at a time
+ let _permit = self.update_sempahore.acquire().await.unwrap();
+ let mut snapshot = self.snapshot.read().clone();
+ let mut pending_dependencies = VecDeque::new();
+
+ // go over the top level packages first, then down the
+ // tree one level at a time through all the branches
+ for package_ref in packages {
+ if snapshot.package_reqs.contains_key(&package_ref) {
+ // skip analyzing this package, as there's already a matching top level package
+ continue;
+ }
+ // inspect the list of current packages
+ if let Some(version) =
+ snapshot.resolve_best_package_version(&package_ref.name, &package_ref)
+ {
+ snapshot.package_reqs.insert(package_ref, version);
+ continue; // done, no need to continue
+ }
+
+ // no existing best version, so resolve the current packages
+ let info = self.api.package_info(&package_ref.name).await?;
+ let version_and_info = get_resolved_package_version_and_info(
+ &package_ref.name,
+ &package_ref,
+ info,
+ None,
+ )?;
+ let id = NpmPackageId {
+ name: package_ref.name.clone(),
+ version: version_and_info.version.clone(),
+ };
+ let dependencies = version_and_info
+ .info
+ .dependencies_as_entries()
+ .with_context(|| format!("Package: {}", id))?;
+
+ pending_dependencies.push_back((id.clone(), dependencies));
+ snapshot.packages.insert(
+ id.clone(),
+ NpmResolutionPackage {
+ id,
+ dist: version_and_info.info.dist,
+ dependencies: Default::default(),
+ },
+ );
+ snapshot
+ .packages_by_name
+ .entry(package_ref.name.clone())
+ .or_default()
+ .push(version_and_info.version.clone());
+ snapshot
+ .package_reqs
+ .insert(package_ref, version_and_info.version);
+ }
+
+ // now go down through the dependencies by tree depth
+ while let Some((parent_package_id, mut deps)) =
+ pending_dependencies.pop_front()
+ {
+ // sort the dependencies alphabetically by name then by version descending
+ deps.sort_by(|a, b| match a.name.cmp(&b.name) {
+ // sort by newest to oldest
+ Ordering::Equal => b
+ .version_req
+ .version_text()
+ .cmp(&a.version_req.version_text()),
+ ordering => ordering,
+ });
+
+ // now resolve them
+ for dep in deps {
+ // check if an existing dependency matches this
+ let id = if let Some(version) =
+ snapshot.resolve_best_package_version(&dep.name, &dep.version_req)
+ {
+ NpmPackageId {
+ name: dep.name.clone(),
+ version,
+ }
+ } else {
+ // get the information
+ let info = self.api.package_info(&dep.name).await?;
+ let version_and_info = get_resolved_package_version_and_info(
+ &dep.name,
+ &dep.version_req,
+ info,
+ None,
+ )?;
+ let dependencies = version_and_info
+ .info
+ .dependencies_as_entries()
+ .with_context(|| {
+ format!("Package: {}@{}", dep.name, version_and_info.version)
+ })?;
+
+ let id = NpmPackageId {
+ name: dep.name.clone(),
+ version: version_and_info.version.clone(),
+ };
+ pending_dependencies.push_back((id.clone(), dependencies));
+ snapshot.packages.insert(
+ id.clone(),
+ NpmResolutionPackage {
+ id: id.clone(),
+ dist: version_and_info.info.dist,
+ dependencies: Default::default(),
+ },
+ );
+ snapshot
+ .packages_by_name
+ .entry(dep.name.clone())
+ .or_default()
+ .push(id.version.clone());
+
+ id
+ };
+
+ // add this version as a dependency of the package
+ snapshot
+ .packages
+ .get_mut(&parent_package_id)
+ .unwrap()
+ .dependencies
+ .insert(dep.bare_specifier.clone(), id);
+ }
+ }
+
+ *self.snapshot.write() = snapshot;
+ Ok(())
+ }
+
+ pub fn resolve_package_from_package(
+ &self,
+ name: &str,
+ referrer: &NpmPackageId,
+ ) -> Result<NpmResolutionPackage, AnyError> {
+ self
+ .snapshot
+ .read()
+ .resolve_package_from_package(name, referrer)
+ .cloned()
+ }
+
+ /// Resolve a node package from a deno module.
+ pub fn resolve_package_from_deno_module(
+ &self,
+ package: &NpmPackageReq,
+ ) -> Result<NpmResolutionPackage, AnyError> {
+ self
+ .snapshot
+ .read()
+ .resolve_package_from_deno_module(package)
+ .cloned()
+ }
+
+ pub fn all_packages(&self) -> Vec<NpmResolutionPackage> {
+ self.snapshot.read().all_packages()
+ }
+
+ pub fn has_packages(&self) -> bool {
+ !self.snapshot.read().packages.is_empty()
+ }
+
+ pub fn snapshot(&self) -> NpmResolutionSnapshot {
+ self.snapshot.read().clone()
+ }
+}
+
+#[derive(Clone)]
+struct VersionAndInfo {
+ version: semver::Version,
+ info: NpmPackageVersionInfo,
+}
+
+fn get_resolved_package_version_and_info(
+ pkg_name: &str,
+ version_matcher: &impl NpmVersionMatcher,
+ info: NpmPackageInfo,
+ parent: Option<&NpmPackageId>,
+) -> Result<VersionAndInfo, AnyError> {
+ let mut maybe_best_version: Option<VersionAndInfo> = None;
+ for (_, version_info) in info.versions.into_iter() {
+ let version = semver::Version::parse(&version_info.version)?;
+ if version_matcher.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 package '{}' matching {}{}. ",
+ "Try retreiving the latest npm package information by running with --reload",
+ ),
+ pkg_name,
+ version_matcher.version_text(),
+ match parent {
+ Some(id) => format!(" as specified in {}", id),
+ None => String::new(),
+ }
+ ),
+ }
+}