mirror of
https://github.com/denoland/deno.git
synced 2024-12-01 16:51:13 -05:00
3479bc7661
This PR fixes peer dependency resolution to only resolve peers based on the current graph traversal path. Previously, it would resolve a peers by looking at a graph node's ancestors, which is not correct because graph nodes are shared by different resolutions. It also stores more information about peer dependency resolution in the lockfile.
241 lines
7.3 KiB
Rust
241 lines
7.3 KiB
Rust
// 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(
|
|
&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");
|
|
}
|
|
}
|