mirror of
https://github.com/denoland/deno
synced 2024-08-27 20:09:17 +00:00
273 lines
7.7 KiB
Rust
273 lines
7.7 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use deno_core::anyhow::bail;
|
|
use deno_core::error::AnyError;
|
|
use deno_npm::registry::parse_dep_entry_name_and_raw_version;
|
|
use deno_runtime::deno_node::PackageJson;
|
|
use deno_semver::package::PackageReq;
|
|
use deno_semver::VersionReq;
|
|
use deno_semver::VersionReqSpecifierParseError;
|
|
use indexmap::IndexMap;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error, Clone)]
|
|
pub enum PackageJsonDepValueParseError {
|
|
#[error(transparent)]
|
|
Specifier(#[from] VersionReqSpecifierParseError),
|
|
#[error("Not implemented scheme '{scheme}'")]
|
|
Unsupported { scheme: String },
|
|
}
|
|
|
|
pub type PackageJsonDeps =
|
|
IndexMap<String, Result<PackageReq, PackageJsonDepValueParseError>>;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct PackageJsonDepsProvider(Option<PackageJsonDeps>);
|
|
|
|
impl PackageJsonDepsProvider {
|
|
pub fn new(deps: Option<PackageJsonDeps>) -> Self {
|
|
Self(deps)
|
|
}
|
|
|
|
pub fn deps(&self) -> Option<&PackageJsonDeps> {
|
|
self.0.as_ref()
|
|
}
|
|
|
|
pub fn reqs(&self) -> Option<Vec<&PackageReq>> {
|
|
match &self.0 {
|
|
Some(deps) => {
|
|
let mut package_reqs = deps
|
|
.values()
|
|
.filter_map(|r| r.as_ref().ok())
|
|
.collect::<Vec<_>>();
|
|
package_reqs.sort(); // deterministic resolution
|
|
Some(package_reqs)
|
|
}
|
|
None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
) -> PackageJsonDeps {
|
|
fn parse_entry(
|
|
key: &str,
|
|
value: &str,
|
|
) -> Result<PackageReq, 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 {
|
|
scheme: value.split(':').next().unwrap().to_string(),
|
|
});
|
|
}
|
|
let (name, version_req) = parse_dep_entry_name_and_raw_version(key, value);
|
|
let result = VersionReq::parse_from_specifier(version_req);
|
|
match result {
|
|
Ok(version_req) => Ok(PackageReq {
|
|
name: name.to_string(),
|
|
version_req,
|
|
}),
|
|
Err(err) => Err(PackageJsonDepValueParseError::Specifier(err)),
|
|
}
|
|
}
|
|
|
|
fn insert_deps(
|
|
deps: Option<&IndexMap<String, String>>,
|
|
result: &mut PackageJsonDeps,
|
|
) {
|
|
if let Some(deps) = deps {
|
|
for (key, value) in deps {
|
|
result
|
|
.entry(key.to_string())
|
|
.or_insert_with(|| parse_entry(key, value));
|
|
}
|
|
}
|
|
}
|
|
|
|
let deps = package_json.dependencies.as_ref();
|
|
let dev_deps = package_json.dev_dependencies.as_ref();
|
|
let mut result = IndexMap::new();
|
|
|
|
// favors the deps over dev_deps
|
|
insert_deps(deps, &mut result);
|
|
insert_deps(dev_deps, &mut result);
|
|
|
|
result
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::PathBuf;
|
|
|
|
use super::*;
|
|
|
|
fn get_local_package_json_version_reqs_for_tests(
|
|
package_json: &PackageJson,
|
|
) -> IndexMap<String, Result<PackageReq, 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::<IndexMap<_, _>>()
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_local_package_json_version_reqs() {
|
|
let mut package_json = PackageJson::empty(PathBuf::from("/package.json"));
|
|
package_json.dependencies = Some(IndexMap::from([
|
|
("test".to_string(), "^1.2".to_string()),
|
|
("other".to_string(), "npm:package@~1.3".to_string()),
|
|
]));
|
|
package_json.dev_dependencies = Some(IndexMap::from([
|
|
("package_b".to_string(), "~2.2".to_string()),
|
|
// should be ignored
|
|
("other".to_string(), "^3.2".to_string()),
|
|
]));
|
|
let deps = get_local_package_json_version_reqs_for_tests(&package_json);
|
|
assert_eq!(
|
|
deps,
|
|
IndexMap::from([
|
|
(
|
|
"test".to_string(),
|
|
Ok(PackageReq::from_str("test@^1.2").unwrap())
|
|
),
|
|
(
|
|
"other".to_string(),
|
|
Ok(PackageReq::from_str("package@~1.3").unwrap())
|
|
),
|
|
(
|
|
"package_b".to_string(),
|
|
Ok(PackageReq::from_str("package_b@~2.2").unwrap())
|
|
)
|
|
])
|
|
);
|
|
}
|
|
|
|
#[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(IndexMap::from([(
|
|
"test".to_string(),
|
|
"1.x - 1.3".to_string(),
|
|
)]));
|
|
let map = get_local_package_json_version_reqs_for_tests(&package_json);
|
|
assert_eq!(
|
|
map,
|
|
IndexMap::from([(
|
|
"test".to_string(),
|
|
Err(
|
|
concat!(
|
|
"Invalid specifier version requirement. Unexpected character.\n",
|
|
" - 1.3\n",
|
|
" ~"
|
|
)
|
|
.to_string()
|
|
)
|
|
)])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_local_package_json_version_reqs_skips_certain_specifiers() {
|
|
let mut package_json = PackageJson::empty(PathBuf::from("/package.json"));
|
|
package_json.dependencies = Some(IndexMap::from([
|
|
("test".to_string(), "1".to_string()),
|
|
("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()),
|
|
]));
|
|
let result = get_local_package_json_version_reqs_for_tests(&package_json);
|
|
assert_eq!(
|
|
result,
|
|
IndexMap::from([
|
|
(
|
|
"file-test".to_string(),
|
|
Err("Not implemented scheme 'file'".to_string()),
|
|
),
|
|
(
|
|
"git-test".to_string(),
|
|
Err("Not implemented scheme 'git'".to_string()),
|
|
),
|
|
(
|
|
"http-test".to_string(),
|
|
Err("Not implemented scheme 'http'".to_string()),
|
|
),
|
|
(
|
|
"https-test".to_string(),
|
|
Err("Not implemented scheme 'https'".to_string()),
|
|
),
|
|
(
|
|
"test".to_string(),
|
|
Ok(PackageReq::from_str("test@1").unwrap())
|
|
),
|
|
(
|
|
"work-test".to_string(),
|
|
Err("Not implemented scheme 'workspace'".to_string()),
|
|
)
|
|
])
|
|
);
|
|
}
|
|
}
|