2023-02-20 13:14:06 -05:00
|
|
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
|
2023-02-23 12:33:23 -05:00
|
|
|
use std::collections::BTreeMap;
|
2023-02-20 13:14:06 -05:00
|
|
|
use std::collections::HashMap;
|
2023-02-23 10:58:10 -05:00
|
|
|
use std::path::Path;
|
|
|
|
use std::path::PathBuf;
|
2023-02-20 13:14:06 -05:00
|
|
|
|
|
|
|
use deno_core::anyhow::bail;
|
|
|
|
use deno_core::error::AnyError;
|
|
|
|
use deno_graph::npm::NpmPackageReq;
|
2023-03-03 17:27:05 -05:00
|
|
|
use deno_graph::semver::NpmVersionReqSpecifierParseError;
|
2023-02-20 13:14:06 -05:00
|
|
|
use deno_graph::semver::VersionReq;
|
|
|
|
use deno_runtime::deno_node::PackageJson;
|
2023-03-03 17:27:05 -05:00
|
|
|
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,
|
|
|
|
}
|
2023-02-20 13:14:06 -05:00
|
|
|
|
|
|
|
/// 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,
|
2023-03-03 17:27:05 -05:00
|
|
|
) -> Result<(&'a str, &'a str), PackageJsonDepNpmSchemeValueParseError> {
|
2023-02-20 13:14:06 -05:00
|
|
|
if let Some(package_and_version) = value.strip_prefix("npm:") {
|
|
|
|
if let Some((name, version)) = package_and_version.rsplit_once('@') {
|
|
|
|
Ok((name, version))
|
|
|
|
} else {
|
2023-03-03 17:27:05 -05:00
|
|
|
Err(PackageJsonDepNpmSchemeValueParseError {
|
|
|
|
value: value.to_string(),
|
|
|
|
})
|
2023-02-20 13:14:06 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Ok((key, value))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 17:27:05 -05:00
|
|
|
#[derive(Debug, Error, Clone, Hash)]
|
|
|
|
pub enum PackageJsonDepValueParseError {
|
|
|
|
#[error(transparent)]
|
|
|
|
SchemeValue(#[from] PackageJsonDepNpmSchemeValueParseError),
|
|
|
|
#[error(transparent)]
|
|
|
|
Specifier(#[from] NpmVersionReqSpecifierParseError),
|
2023-03-15 15:23:30 -04:00
|
|
|
#[error("Not implemented scheme '{scheme}'")]
|
2023-03-03 17:27:05 -05:00
|
|
|
Unsupported { scheme: String },
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type PackageJsonDeps =
|
|
|
|
BTreeMap<String, Result<NpmPackageReq, PackageJsonDepValueParseError>>;
|
|
|
|
|
2023-02-20 13:14:06 -05:00
|
|
|
/// Gets an application level package.json's npm package requirements.
|
|
|
|
///
|
|
|
|
/// Note that this function is not general purpose. It is specifically for
|
|
|
|
/// parsing the application level package.json that the user has control
|
|
|
|
/// over. This is a design limitation to allow mapping these dependency
|
|
|
|
/// entries to npm specifiers which can then be used in the resolver.
|
|
|
|
pub fn get_local_package_json_version_reqs(
|
|
|
|
package_json: &PackageJson,
|
2023-03-03 17:27:05 -05:00
|
|
|
) -> PackageJsonDeps {
|
|
|
|
fn parse_entry(
|
|
|
|
key: &str,
|
|
|
|
value: &str,
|
|
|
|
) -> Result<NpmPackageReq, PackageJsonDepValueParseError> {
|
|
|
|
if value.starts_with("workspace:")
|
|
|
|
|| value.starts_with("file:")
|
|
|
|
|| value.starts_with("git:")
|
|
|
|
|| value.starts_with("http:")
|
|
|
|
|| value.starts_with("https:")
|
|
|
|
{
|
|
|
|
return Err(PackageJsonDepValueParseError::Unsupported {
|
2023-03-15 15:23:30 -04:00
|
|
|
scheme: value.split(':').next().unwrap().to_string(),
|
2023-03-03 17:27:05 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
let (name, version_req) = parse_dep_entry_name_and_raw_version(key, value)
|
|
|
|
.map_err(PackageJsonDepValueParseError::SchemeValue)?;
|
|
|
|
|
|
|
|
let result = VersionReq::parse_from_specifier(version_req);
|
|
|
|
match result {
|
|
|
|
Ok(version_req) => Ok(NpmPackageReq {
|
|
|
|
name: name.to_string(),
|
|
|
|
version_req: Some(version_req),
|
|
|
|
}),
|
|
|
|
Err(err) => Err(PackageJsonDepValueParseError::Specifier(err)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-20 13:14:06 -05:00
|
|
|
fn insert_deps(
|
|
|
|
deps: Option<&HashMap<String, String>>,
|
2023-03-03 17:27:05 -05:00
|
|
|
result: &mut PackageJsonDeps,
|
|
|
|
) {
|
2023-02-20 13:14:06 -05:00
|
|
|
if let Some(deps) = deps {
|
|
|
|
for (key, value) in deps {
|
2023-03-03 17:27:05 -05:00
|
|
|
result.insert(key.to_string(), parse_entry(key, value));
|
2023-02-20 13:14:06 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let deps = package_json.dependencies.as_ref();
|
|
|
|
let dev_deps = package_json.dev_dependencies.as_ref();
|
2023-02-23 12:33:23 -05:00
|
|
|
let mut result = BTreeMap::new();
|
2023-02-20 13:14:06 -05:00
|
|
|
|
|
|
|
// insert the dev dependencies first so the dependencies will
|
|
|
|
// take priority and overwrite any collisions
|
2023-03-03 17:27:05 -05:00
|
|
|
insert_deps(dev_deps, &mut result);
|
|
|
|
insert_deps(deps, &mut result);
|
2023-02-20 13:14:06 -05:00
|
|
|
|
2023-03-03 17:27:05 -05:00
|
|
|
result
|
2023-02-20 13:14:06 -05:00
|
|
|
}
|
|
|
|
|
2023-02-23 10:58:10 -05:00
|
|
|
/// Attempts to discover the package.json file, maybe stopping when it
|
|
|
|
/// reaches the specified `maybe_stop_at` directory.
|
|
|
|
pub fn discover_from(
|
|
|
|
start: &Path,
|
|
|
|
maybe_stop_at: Option<PathBuf>,
|
|
|
|
) -> Result<Option<PackageJson>, AnyError> {
|
|
|
|
const PACKAGE_JSON_NAME: &str = "package.json";
|
|
|
|
|
|
|
|
// note: ancestors() includes the `start` path
|
|
|
|
for ancestor in start.ancestors() {
|
|
|
|
let path = ancestor.join(PACKAGE_JSON_NAME);
|
|
|
|
|
|
|
|
let source = match std::fs::read_to_string(&path) {
|
|
|
|
Ok(source) => source,
|
|
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
|
|
if let Some(stop_at) = maybe_stop_at.as_ref() {
|
|
|
|
if ancestor == stop_at {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Err(err) => bail!(
|
|
|
|
"Error loading package.json at {}. {:#}",
|
|
|
|
path.display(),
|
|
|
|
err
|
|
|
|
),
|
|
|
|
};
|
|
|
|
|
|
|
|
let package_json = PackageJson::load_from_string(path.clone(), source)?;
|
|
|
|
log::debug!("package.json file found at '{}'", path.display());
|
|
|
|
return Ok(Some(package_json));
|
|
|
|
}
|
|
|
|
|
|
|
|
log::debug!("No package.json file found");
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
2023-02-20 13:14:06 -05:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_dep_entry_name_and_raw_version() {
|
|
|
|
let cases = [
|
|
|
|
("test", "^1.2", Ok(("test", "^1.2"))),
|
|
|
|
("test", "1.x - 2.6", Ok(("test", "1.x - 2.6"))),
|
|
|
|
("test", "npm:package@^1.2", Ok(("package", "^1.2"))),
|
|
|
|
(
|
|
|
|
"test",
|
|
|
|
"npm:package",
|
2023-03-03 17:27:05 -05:00
|
|
|
Err("Could not find @ symbol in npm url 'npm:package'"),
|
2023-02-20 13:14:06 -05:00
|
|
|
),
|
|
|
|
];
|
|
|
|
for (key, value, expected_result) in cases {
|
|
|
|
let result = parse_dep_entry_name_and_raw_version(key, value);
|
|
|
|
match result {
|
|
|
|
Ok(result) => assert_eq!(result, expected_result.unwrap()),
|
|
|
|
Err(err) => assert_eq!(err.to_string(), expected_result.err().unwrap()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 17:27:05 -05:00
|
|
|
fn get_local_package_json_version_reqs_for_tests(
|
|
|
|
package_json: &PackageJson,
|
|
|
|
) -> BTreeMap<String, Result<NpmPackageReq, String>> {
|
|
|
|
get_local_package_json_version_reqs(package_json)
|
|
|
|
.into_iter()
|
|
|
|
.map(|(k, v)| {
|
|
|
|
(
|
|
|
|
k,
|
|
|
|
match v {
|
|
|
|
Ok(v) => Ok(v),
|
|
|
|
Err(err) => Err(err.to_string()),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect::<BTreeMap<_, _>>()
|
|
|
|
}
|
|
|
|
|
2023-02-20 13:14:06 -05:00
|
|
|
#[test]
|
|
|
|
fn test_get_local_package_json_version_reqs() {
|
|
|
|
let mut package_json = PackageJson::empty(PathBuf::from("/package.json"));
|
|
|
|
package_json.dependencies = Some(HashMap::from([
|
|
|
|
("test".to_string(), "^1.2".to_string()),
|
|
|
|
("other".to_string(), "npm:package@~1.3".to_string()),
|
|
|
|
]));
|
|
|
|
package_json.dev_dependencies = Some(HashMap::from([
|
|
|
|
("package_b".to_string(), "~2.2".to_string()),
|
|
|
|
// should be ignored
|
|
|
|
("other".to_string(), "^3.2".to_string()),
|
|
|
|
]));
|
2023-03-03 17:27:05 -05:00
|
|
|
let deps = get_local_package_json_version_reqs_for_tests(&package_json);
|
2023-02-20 13:14:06 -05:00
|
|
|
assert_eq!(
|
2023-03-03 17:27:05 -05:00
|
|
|
deps,
|
2023-02-23 12:33:23 -05:00
|
|
|
BTreeMap::from([
|
2023-02-20 13:14:06 -05:00
|
|
|
(
|
|
|
|
"test".to_string(),
|
2023-03-03 17:27:05 -05:00
|
|
|
Ok(NpmPackageReq::from_str("test@^1.2").unwrap())
|
2023-02-20 13:14:06 -05:00
|
|
|
),
|
|
|
|
(
|
|
|
|
"other".to_string(),
|
2023-03-03 17:27:05 -05:00
|
|
|
Ok(NpmPackageReq::from_str("package@~1.3").unwrap())
|
2023-02-20 13:14:06 -05:00
|
|
|
),
|
|
|
|
(
|
|
|
|
"package_b".to_string(),
|
2023-03-03 17:27:05 -05:00
|
|
|
Ok(NpmPackageReq::from_str("package_b@~2.2").unwrap())
|
2023-02-20 13:14:06 -05:00
|
|
|
)
|
|
|
|
])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_get_local_package_json_version_reqs_errors_non_npm_specifier() {
|
|
|
|
let mut package_json = PackageJson::empty(PathBuf::from("/package.json"));
|
|
|
|
package_json.dependencies = Some(HashMap::from([(
|
|
|
|
"test".to_string(),
|
|
|
|
"1.x - 1.3".to_string(),
|
|
|
|
)]));
|
2023-03-03 17:27:05 -05:00
|
|
|
let map = get_local_package_json_version_reqs_for_tests(&package_json);
|
2023-02-20 13:14:06 -05:00
|
|
|
assert_eq!(
|
2023-03-03 17:27:05 -05:00
|
|
|
map,
|
|
|
|
BTreeMap::from([(
|
|
|
|
"test".to_string(),
|
|
|
|
Err(
|
|
|
|
concat!(
|
|
|
|
"Invalid npm specifier version requirement. Unexpected character.\n",
|
|
|
|
" - 1.3\n",
|
|
|
|
" ~"
|
|
|
|
)
|
|
|
|
.to_string()
|
|
|
|
)
|
|
|
|
)])
|
2023-02-20 13:14:06 -05:00
|
|
|
);
|
|
|
|
}
|
2023-02-24 19:27:50 -05:00
|
|
|
|
|
|
|
#[test]
|
2023-02-24 21:16:26 -05:00
|
|
|
fn test_get_local_package_json_version_reqs_skips_certain_specifiers() {
|
2023-02-24 19:27:50 -05:00
|
|
|
let mut package_json = PackageJson::empty(PathBuf::from("/package.json"));
|
|
|
|
package_json.dependencies = Some(HashMap::from([
|
|
|
|
("test".to_string(), "1".to_string()),
|
2023-03-15 15:23:30 -04:00
|
|
|
("work-test".to_string(), "workspace:1.1.1".to_string()),
|
|
|
|
("file-test".to_string(), "file:something".to_string()),
|
|
|
|
("git-test".to_string(), "git:something".to_string()),
|
|
|
|
("http-test".to_string(), "http://something".to_string()),
|
|
|
|
("https-test".to_string(), "https://something".to_string()),
|
2023-02-24 19:27:50 -05:00
|
|
|
]));
|
2023-03-03 17:27:05 -05:00
|
|
|
let result = get_local_package_json_version_reqs_for_tests(&package_json);
|
2023-02-24 19:27:50 -05:00
|
|
|
assert_eq!(
|
|
|
|
result,
|
2023-03-03 17:27:05 -05:00
|
|
|
BTreeMap::from([
|
|
|
|
(
|
2023-03-15 15:23:30 -04:00
|
|
|
"file-test".to_string(),
|
|
|
|
Err("Not implemented scheme 'file'".to_string()),
|
2023-03-03 17:27:05 -05:00
|
|
|
),
|
|
|
|
(
|
2023-03-15 15:23:30 -04:00
|
|
|
"git-test".to_string(),
|
|
|
|
Err("Not implemented scheme 'git'".to_string()),
|
2023-03-03 17:27:05 -05:00
|
|
|
),
|
|
|
|
(
|
2023-03-15 15:23:30 -04:00
|
|
|
"http-test".to_string(),
|
|
|
|
Err("Not implemented scheme 'http'".to_string()),
|
2023-03-03 17:27:05 -05:00
|
|
|
),
|
|
|
|
(
|
2023-03-15 15:23:30 -04:00
|
|
|
"https-test".to_string(),
|
|
|
|
Err("Not implemented scheme 'https'".to_string()),
|
2023-03-03 17:27:05 -05:00
|
|
|
),
|
|
|
|
(
|
|
|
|
"test".to_string(),
|
|
|
|
Ok(NpmPackageReq::from_str("test@1").unwrap())
|
|
|
|
),
|
|
|
|
(
|
2023-03-15 15:23:30 -04:00
|
|
|
"work-test".to_string(),
|
|
|
|
Err("Not implemented scheme 'workspace'".to_string()),
|
2023-03-03 17:27:05 -05:00
|
|
|
)
|
|
|
|
])
|
2023-02-24 19:27:50 -05:00
|
|
|
);
|
|
|
|
}
|
2023-02-20 13:14:06 -05:00
|
|
|
}
|