1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00
denoland-deno/ext/node/resolution.rs

819 lines
21 KiB
Rust
Raw Permalink Normal View History

// Copyright 2018-2022 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::generic_error;
use deno_core::error::AnyError;
use deno_core::serde_json::Map;
use deno_core::serde_json::Value;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
use regex::Regex;
use crate::errors;
use crate::package_json::PackageJson;
use crate::path::PathClean;
use crate::RequireNpmResolver;
pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"];
pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeModuleKind {
Esm,
Cjs,
}
fn to_specifier_display_string(url: &ModuleSpecifier) -> String {
if let Ok(path) = url.to_file_path() {
path.display().to_string()
} else {
url.to_string()
}
}
fn throw_import_not_defined(
specifier: &str,
package_json_path: Option<&Path>,
base: &ModuleSpecifier,
) -> AnyError {
errors::err_package_import_not_defined(
specifier,
package_json_path.map(|p| p.parent().unwrap().display().to_string()),
&to_specifier_display_string(base),
)
}
fn pattern_key_compare(a: &str, b: &str) -> i32 {
let a_pattern_index = a.find('*');
let b_pattern_index = b.find('*');
let base_len_a = if let Some(index) = a_pattern_index {
index + 1
} else {
a.len()
};
let base_len_b = if let Some(index) = b_pattern_index {
index + 1
} else {
b.len()
};
if base_len_a > base_len_b {
return -1;
}
if base_len_b > base_len_a {
return 1;
}
if a_pattern_index.is_none() {
return 1;
}
if b_pattern_index.is_none() {
return -1;
}
if a.len() > b.len() {
return -1;
}
if b.len() > a.len() {
return 1;
}
0
}
pub fn package_imports_resolve(
name: &str,
referrer: &ModuleSpecifier,
referrer_kind: NodeModuleKind,
conditions: &[&str],
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PathBuf, AnyError> {
if name == "#" || name.starts_with("#/") || name.ends_with('/') {
let reason = "is not a valid internal imports specifier name";
return Err(errors::err_invalid_module_specifier(
name,
reason,
Some(to_specifier_display_string(referrer)),
));
}
let package_config = get_package_scope_config(referrer, npm_resolver)?;
let mut package_json_path = None;
if package_config.exists {
package_json_path = Some(package_config.path.clone());
if let Some(imports) = &package_config.imports {
if imports.contains_key(name) && !name.contains('*') {
let maybe_resolved = resolve_package_target(
package_json_path.as_ref().unwrap(),
imports.get(name).unwrap().to_owned(),
"".to_string(),
name.to_string(),
referrer,
referrer_kind,
false,
true,
conditions,
npm_resolver,
)?;
if let Some(resolved) = maybe_resolved {
return Ok(resolved);
}
} else {
let mut best_match = "";
let mut best_match_subpath = None;
for key in imports.keys() {
let pattern_index = key.find('*');
if let Some(pattern_index) = pattern_index {
let key_sub = &key[0..=pattern_index];
if name.starts_with(key_sub) {
let pattern_trailer = &key[pattern_index + 1..];
if name.len() > key.len()
&& name.ends_with(&pattern_trailer)
&& pattern_key_compare(best_match, key) == 1
&& key.rfind('*') == Some(pattern_index)
{
best_match = key;
best_match_subpath = Some(
name[pattern_index..=(name.len() - pattern_trailer.len())]
.to_string(),
);
}
}
}
}
if !best_match.is_empty() {
let target = imports.get(best_match).unwrap().to_owned();
let maybe_resolved = resolve_package_target(
package_json_path.as_ref().unwrap(),
target,
best_match_subpath.unwrap(),
best_match.to_string(),
referrer,
referrer_kind,
true,
true,
conditions,
npm_resolver,
)?;
if let Some(resolved) = maybe_resolved {
return Ok(resolved);
}
}
}
}
}
Err(throw_import_not_defined(
name,
package_json_path.as_deref(),
referrer,
))
}
fn throw_invalid_package_target(
subpath: String,
target: String,
package_json_path: &Path,
internal: bool,
referrer: &ModuleSpecifier,
) -> AnyError {
errors::err_invalid_package_target(
package_json_path.parent().unwrap().display().to_string(),
subpath,
target,
internal,
Some(referrer.as_str().to_string()),
)
}
fn throw_invalid_subpath(
subpath: String,
package_json_path: &Path,
internal: bool,
referrer: &ModuleSpecifier,
) -> AnyError {
let ie = if internal { "imports" } else { "exports" };
let reason = format!(
"request is not a valid subpath for the \"{}\" resolution of {}",
ie,
package_json_path.display(),
);
errors::err_invalid_module_specifier(
&subpath,
&reason,
Some(to_specifier_display_string(referrer)),
)
}
#[allow(clippy::too_many_arguments)]
fn resolve_package_target_string(
target: String,
subpath: String,
match_: String,
package_json_path: &Path,
referrer: &ModuleSpecifier,
referrer_kind: NodeModuleKind,
pattern: bool,
internal: bool,
conditions: &[&str],
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PathBuf, AnyError> {
if !subpath.is_empty() && !pattern && !target.ends_with('/') {
return Err(throw_invalid_package_target(
match_,
target,
package_json_path,
internal,
referrer,
));
}
let invalid_segment_re =
Regex::new(r"(^|\|/)(..?|node_modules)(\|/|$)").expect("bad regex");
let pattern_re = Regex::new(r"\*").expect("bad regex");
if !target.starts_with("./") {
if internal && !target.starts_with("../") && !target.starts_with('/') {
let is_url = Url::parse(&target).is_ok();
if !is_url {
let export_target = if pattern {
pattern_re
.replace(&target, |_caps: &regex::Captures| subpath.clone())
.to_string()
} else {
format!("{}{}", target, subpath)
};
let package_json_url =
ModuleSpecifier::from_file_path(package_json_path).unwrap();
return package_resolve(
&export_target,
&package_json_url,
referrer_kind,
conditions,
npm_resolver,
);
}
}
return Err(throw_invalid_package_target(
match_,
target,
package_json_path,
internal,
referrer,
));
}
if invalid_segment_re.is_match(&target[2..]) {
return Err(throw_invalid_package_target(
match_,
target,
package_json_path,
internal,
referrer,
));
}
let package_path = package_json_path.parent().unwrap();
let resolved_path = package_path.join(&target).clean();
if !resolved_path.starts_with(package_path) {
return Err(throw_invalid_package_target(
match_,
target,
package_json_path,
internal,
referrer,
));
}
if subpath.is_empty() {
return Ok(resolved_path);
}
if invalid_segment_re.is_match(&subpath) {
let request = if pattern {
match_.replace('*', &subpath)
} else {
format!("{}{}", match_, subpath)
};
return Err(throw_invalid_subpath(
request,
package_json_path,
internal,
referrer,
));
}
if pattern {
let resolved_path_str = resolved_path.to_string_lossy();
let replaced = pattern_re
.replace(&resolved_path_str, |_caps: &regex::Captures| {
subpath.clone()
});
return Ok(PathBuf::from(replaced.to_string()));
}
Ok(resolved_path.join(&subpath).clean())
}
#[allow(clippy::too_many_arguments)]
fn resolve_package_target(
package_json_path: &Path,
target: Value,
subpath: String,
package_subpath: String,
referrer: &ModuleSpecifier,
referrer_kind: NodeModuleKind,
pattern: bool,
internal: bool,
conditions: &[&str],
npm_resolver: &dyn RequireNpmResolver,
) -> Result<Option<PathBuf>, AnyError> {
if let Some(target) = target.as_str() {
return Ok(Some(resolve_package_target_string(
target.to_string(),
subpath,
package_subpath,
package_json_path,
referrer,
referrer_kind,
pattern,
internal,
conditions,
npm_resolver,
)?));
} else if let Some(target_arr) = target.as_array() {
if target_arr.is_empty() {
return Ok(None);
}
let mut last_error = None;
for target_item in target_arr {
let resolved_result = resolve_package_target(
package_json_path,
target_item.to_owned(),
subpath.clone(),
package_subpath.clone(),
referrer,
referrer_kind,
pattern,
internal,
conditions,
npm_resolver,
);
if let Err(e) = resolved_result {
let err_string = e.to_string();
last_error = Some(e);
if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") {
continue;
}
return Err(last_error.unwrap());
}
let resolved = resolved_result.unwrap();
if resolved.is_none() {
last_error = None;
continue;
}
return Ok(resolved);
}
if last_error.is_none() {
return Ok(None);
}
return Err(last_error.unwrap());
} else if let Some(target_obj) = target.as_object() {
for key in target_obj.keys() {
// TODO(bartlomieju): verify that keys are not numeric
// return Err(errors::err_invalid_package_config(
// to_file_path_string(package_json_url),
// Some(base.as_str().to_string()),
// Some("\"exports\" cannot contain numeric property keys.".to_string()),
// ));
if key == "default" || conditions.contains(&key.as_str()) {
let condition_target = target_obj.get(key).unwrap().to_owned();
let resolved = resolve_package_target(
package_json_path,
condition_target,
subpath.clone(),
package_subpath.clone(),
referrer,
referrer_kind,
pattern,
internal,
conditions,
npm_resolver,
)?;
if resolved.is_none() {
continue;
}
return Ok(resolved);
}
}
} else if target.is_null() {
return Ok(None);
}
Err(throw_invalid_package_target(
package_subpath,
target.to_string(),
package_json_path,
internal,
referrer,
))
}
fn throw_exports_not_found(
subpath: String,
package_json_path: &Path,
referrer: &ModuleSpecifier,
) -> AnyError {
errors::err_package_path_not_exported(
package_json_path.parent().unwrap().display().to_string(),
subpath,
Some(to_specifier_display_string(referrer)),
)
}
pub fn package_exports_resolve(
package_json_path: &Path,
package_subpath: String,
package_exports: &Map<String, Value>,
referrer: &ModuleSpecifier,
referrer_kind: NodeModuleKind,
conditions: &[&str],
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PathBuf, AnyError> {
if package_exports.contains_key(&package_subpath)
&& package_subpath.find('*').is_none()
&& !package_subpath.ends_with('/')
{
let target = package_exports.get(&package_subpath).unwrap().to_owned();
let resolved = resolve_package_target(
package_json_path,
target,
"".to_string(),
package_subpath.to_string(),
referrer,
referrer_kind,
false,
false,
conditions,
npm_resolver,
)?;
if resolved.is_none() {
return Err(throw_exports_not_found(
package_subpath,
package_json_path,
referrer,
));
}
return Ok(resolved.unwrap());
}
let mut best_match = "";
let mut best_match_subpath = None;
for key in package_exports.keys() {
let pattern_index = key.find('*');
if let Some(pattern_index) = pattern_index {
let key_sub = &key[0..pattern_index];
if package_subpath.starts_with(key_sub) {
// When this reaches EOL, this can throw at the top of the whole function:
//
// if (StringPrototypeEndsWith(packageSubpath, '/'))
// throwInvalidSubpath(packageSubpath)
//
// To match "imports" and the spec.
if package_subpath.ends_with('/') {
// TODO(bartlomieju):
// emitTrailingSlashPatternDeprecation();
}
let pattern_trailer = &key[pattern_index + 1..];
if package_subpath.len() > key.len()
&& package_subpath.ends_with(&pattern_trailer)
&& pattern_key_compare(best_match, key) == 1
&& key.rfind('*') == Some(pattern_index)
{
best_match = key;
best_match_subpath = Some(
package_subpath
[pattern_index..(package_subpath.len() - pattern_trailer.len())]
.to_string(),
);
}
}
}
}
if !best_match.is_empty() {
let target = package_exports.get(best_match).unwrap().to_owned();
let maybe_resolved = resolve_package_target(
package_json_path,
target,
best_match_subpath.unwrap(),
best_match.to_string(),
referrer,
referrer_kind,
true,
false,
conditions,
npm_resolver,
)?;
if let Some(resolved) = maybe_resolved {
return Ok(resolved);
} else {
return Err(throw_exports_not_found(
package_subpath,
package_json_path,
referrer,
));
}
}
Err(throw_exports_not_found(
package_subpath,
package_json_path,
referrer,
))
}
fn parse_package_name(
specifier: &str,
referrer: &ModuleSpecifier,
) -> Result<(String, String, bool), AnyError> {
let mut separator_index = specifier.find('/');
let mut valid_package_name = true;
let mut is_scoped = false;
if specifier.is_empty() {
valid_package_name = false;
} else if specifier.starts_with('@') {
is_scoped = true;
if let Some(index) = separator_index {
separator_index = specifier[index + 1..]
.find('/')
.map(|new_index| index + 1 + new_index);
} else {
valid_package_name = false;
}
}
let package_name = if let Some(index) = separator_index {
specifier[0..index].to_string()
} else {
specifier.to_string()
};
// Package name cannot have leading . and cannot have percent-encoding or separators.
for ch in package_name.chars() {
if ch == '%' || ch == '\\' {
valid_package_name = false;
break;
}
}
if !valid_package_name {
return Err(errors::err_invalid_module_specifier(
specifier,
"is not a valid package name",
Some(to_specifier_display_string(referrer)),
));
}
let package_subpath = if let Some(index) = separator_index {
format!(".{}", specifier.chars().skip(index).collect::<String>())
} else {
".".to_string()
};
Ok((package_name, package_subpath, is_scoped))
}
pub fn package_resolve(
specifier: &str,
referrer: &ModuleSpecifier,
referrer_kind: NodeModuleKind,
conditions: &[&str],
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PathBuf, AnyError> {
let (package_name, package_subpath, _is_scoped) =
parse_package_name(specifier, referrer)?;
// ResolveSelf
let package_config = get_package_scope_config(referrer, npm_resolver)?;
if package_config.exists
&& package_config.name.as_ref() == Some(&package_name)
{
if let Some(exports) = &package_config.exports {
return package_exports_resolve(
&package_config.path,
package_subpath,
exports,
referrer,
referrer_kind,
conditions,
npm_resolver,
);
}
}
let package_dir_path = npm_resolver.resolve_package_folder_from_package(
&package_name,
&referrer.to_file_path().unwrap(),
)?;
let package_json_path = package_dir_path.join("package.json");
// todo: error with this instead when can't find package
// Err(errors::err_module_not_found(
// &package_json_url
// .join(".")
// .unwrap()
// .to_file_path()
// .unwrap()
// .display()
// .to_string(),
// &to_file_path_string(referrer),
// "package",
// ))
// Package match.
let package_json = PackageJson::load(npm_resolver, package_json_path)?;
if let Some(exports) = &package_json.exports {
return package_exports_resolve(
&package_json.path,
package_subpath,
exports,
referrer,
referrer_kind,
conditions,
npm_resolver,
);
}
if package_subpath == "." {
return legacy_main_resolve(&package_json, referrer_kind);
}
Ok(package_json.path.parent().unwrap().join(&package_subpath))
}
pub fn get_package_scope_config(
referrer: &ModuleSpecifier,
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PackageJson, AnyError> {
let root_folder = npm_resolver
.resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?;
let package_json_path = root_folder.join("package.json");
PackageJson::load(npm_resolver, package_json_path)
}
pub fn get_closest_package_json(
url: &ModuleSpecifier,
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PackageJson, AnyError> {
let package_json_path = get_closest_package_json_path(url, npm_resolver)?;
PackageJson::load(npm_resolver, package_json_path)
}
fn get_closest_package_json_path(
url: &ModuleSpecifier,
npm_resolver: &dyn RequireNpmResolver,
) -> Result<PathBuf, AnyError> {
let file_path = url.to_file_path().unwrap();
let mut current_dir = file_path.parent().unwrap();
let package_json_path = current_dir.join("package.json");
if package_json_path.exists() {
return Ok(package_json_path);
}
let root_pkg_folder = npm_resolver
.resolve_package_folder_from_path(&url.to_file_path().unwrap())?;
while current_dir.starts_with(&root_pkg_folder) {
current_dir = current_dir.parent().unwrap();
let package_json_path = current_dir.join("package.json");
if package_json_path.exists() {
return Ok(package_json_path);
}
}
bail!("did not find package.json in {}", root_pkg_folder.display())
}
fn file_exists(path: &Path) -> bool {
if let Ok(stats) = std::fs::metadata(path) {
stats.is_file()
} else {
false
}
}
pub fn legacy_main_resolve(
package_json: &PackageJson,
referrer_kind: NodeModuleKind,
) -> Result<PathBuf, AnyError> {
let maybe_main = package_json.main(referrer_kind);
let mut guess;
if let Some(main) = maybe_main {
guess = package_json.path.parent().unwrap().join(main).clean();
if file_exists(&guess) {
return Ok(guess);
}
let mut found = false;
// todo(dsherret): investigate exactly how node handles this
let endings = match referrer_kind {
NodeModuleKind::Cjs => vec![
".js",
".cjs",
".json",
".node",
"/index.js",
"/index.cjs",
"/index.json",
"/index.node",
],
NodeModuleKind::Esm => vec![
".js",
".mjs",
".json",
".node",
"/index.js",
"/index.mjs",
".cjs",
"/index.cjs",
"/index.json",
"/index.node",
],
};
for ending in endings {
guess = package_json
.path
.parent()
.unwrap()
.join(&format!("{}{}", main, ending))
.clean();
if file_exists(&guess) {
found = true;
break;
}
}
if found {
// TODO(bartlomieju): emitLegacyIndexDeprecation()
return Ok(guess);
}
}
let index_file_names = match referrer_kind {
NodeModuleKind::Cjs => {
vec!["index.js", "index.cjs", "index.json", "index.node"]
}
NodeModuleKind::Esm => vec![
"index.js",
"index.mjs",
"index.cjs",
"index.json",
"index.node",
],
};
for index_file_name in index_file_names {
guess = package_json
.path
.parent()
.unwrap()
.join(index_file_name)
.clean();
if file_exists(&guess) {
// TODO(bartlomieju): emitLegacyIndexDeprecation()
return Ok(guess);
}
}
Err(generic_error("not found"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_package_name() {
let dummy_referrer = Url::parse("http://example.com").unwrap();
assert_eq!(
parse_package_name("fetch-blob", &dummy_referrer).unwrap(),
("fetch-blob".to_string(), ".".to_string(), false)
);
assert_eq!(
parse_package_name("@vue/plugin-vue", &dummy_referrer).unwrap(),
("@vue/plugin-vue".to_string(), ".".to_string(), true)
);
assert_eq!(
parse_package_name("@astrojs/prism/dist/highlighter", &dummy_referrer)
.unwrap(),
(
"@astrojs/prism".to_string(),
"./dist/highlighter".to_string(),
true
)
);
}
}