mirror of
https://github.com/denoland/deno.git
synced 2024-12-27 01:29:14 -05:00
b8ed6f8221
This is for when not using a node_modules directory or when using auto install for a node_modules dir.
2023 lines
56 KiB
Rust
2023 lines
56 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::bail;
|
|
use anyhow::Error as AnyError;
|
|
use deno_media_type::MediaType;
|
|
use deno_package_json::PackageJsonRc;
|
|
use serde_json::Map;
|
|
use serde_json::Value;
|
|
use url::Url;
|
|
|
|
use crate::env::NodeResolverEnv;
|
|
use crate::errors;
|
|
use crate::errors::CanonicalizingPkgJsonDirError;
|
|
use crate::errors::ClosestPkgJsonError;
|
|
use crate::errors::DataUrlReferrerError;
|
|
use crate::errors::FinalizeResolutionError;
|
|
use crate::errors::InvalidModuleSpecifierError;
|
|
use crate::errors::InvalidPackageTargetError;
|
|
use crate::errors::LegacyResolveError;
|
|
use crate::errors::ModuleNotFoundError;
|
|
use crate::errors::NodeJsErrorCode;
|
|
use crate::errors::NodeJsErrorCoded;
|
|
use crate::errors::NodeResolveError;
|
|
use crate::errors::NodeResolveRelativeJoinError;
|
|
use crate::errors::PackageExportsResolveError;
|
|
use crate::errors::PackageImportNotDefinedError;
|
|
use crate::errors::PackageImportsResolveError;
|
|
use crate::errors::PackageImportsResolveErrorKind;
|
|
use crate::errors::PackageJsonLoadError;
|
|
use crate::errors::PackagePathNotExportedError;
|
|
use crate::errors::PackageResolveError;
|
|
use crate::errors::PackageSubpathResolveError;
|
|
use crate::errors::PackageSubpathResolveErrorKind;
|
|
use crate::errors::PackageTargetNotFoundError;
|
|
use crate::errors::PackageTargetResolveError;
|
|
use crate::errors::PackageTargetResolveErrorKind;
|
|
use crate::errors::ResolveBinaryCommandsError;
|
|
use crate::errors::ResolvePkgJsonBinExportError;
|
|
use crate::errors::ResolvePkgSubpathFromDenoModuleError;
|
|
use crate::errors::TypeScriptNotSupportedInNpmError;
|
|
use crate::errors::TypesNotFoundError;
|
|
use crate::errors::TypesNotFoundErrorData;
|
|
use crate::errors::UnsupportedDirImportError;
|
|
use crate::errors::UnsupportedEsmUrlSchemeError;
|
|
use crate::errors::UrlToNodeResolutionError;
|
|
use crate::path::strip_unc_prefix;
|
|
use crate::path::to_file_specifier;
|
|
use crate::NpmResolverRc;
|
|
use crate::PathClean;
|
|
use deno_package_json::PackageJson;
|
|
|
|
pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"];
|
|
pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"];
|
|
static TYPES_ONLY_CONDITIONS: &[&str] = &["types"];
|
|
|
|
pub type NodeModuleKind = deno_package_json::NodeModuleKind;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum NodeResolutionMode {
|
|
Execution,
|
|
Types,
|
|
}
|
|
|
|
impl NodeResolutionMode {
|
|
pub fn is_types(&self) -> bool {
|
|
matches!(self, NodeResolutionMode::Types)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum NodeResolution {
|
|
Esm(Url),
|
|
CommonJs(Url),
|
|
BuiltIn(String),
|
|
}
|
|
|
|
impl NodeResolution {
|
|
pub fn into_url(self) -> Url {
|
|
match self {
|
|
Self::Esm(u) => u,
|
|
Self::CommonJs(u) => u,
|
|
Self::BuiltIn(specifier) => {
|
|
if specifier.starts_with("node:") {
|
|
Url::parse(&specifier).unwrap()
|
|
} else {
|
|
Url::parse(&format!("node:{specifier}")).unwrap()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn into_specifier_and_media_type(
|
|
resolution: Option<Self>,
|
|
) -> (Url, MediaType) {
|
|
match resolution {
|
|
Some(NodeResolution::CommonJs(specifier)) => {
|
|
let media_type = MediaType::from_specifier(&specifier);
|
|
(
|
|
specifier,
|
|
match media_type {
|
|
MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs,
|
|
MediaType::TypeScript | MediaType::Tsx => MediaType::Cts,
|
|
MediaType::Dts => MediaType::Dcts,
|
|
_ => media_type,
|
|
},
|
|
)
|
|
}
|
|
Some(NodeResolution::Esm(specifier)) => {
|
|
let media_type = MediaType::from_specifier(&specifier);
|
|
(
|
|
specifier,
|
|
match media_type {
|
|
MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs,
|
|
MediaType::TypeScript | MediaType::Tsx => MediaType::Mts,
|
|
MediaType::Dts => MediaType::Dmts,
|
|
_ => media_type,
|
|
},
|
|
)
|
|
}
|
|
Some(resolution) => (resolution.into_url(), MediaType::Dts),
|
|
None => (
|
|
Url::parse("internal:///missing_dependency.d.ts").unwrap(),
|
|
MediaType::Dts,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::disallowed_types)]
|
|
pub type NodeResolverRc<TEnv> = crate::sync::MaybeArc<NodeResolver<TEnv>>;
|
|
|
|
#[derive(Debug)]
|
|
pub struct NodeResolver<TEnv: NodeResolverEnv> {
|
|
env: TEnv,
|
|
npm_resolver: NpmResolverRc,
|
|
}
|
|
|
|
impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> {
|
|
pub fn new(env: TEnv, npm_resolver: NpmResolverRc) -> Self {
|
|
Self { env, npm_resolver }
|
|
}
|
|
|
|
pub fn in_npm_package(&self, specifier: &Url) -> bool {
|
|
self.npm_resolver.in_npm_package(specifier)
|
|
}
|
|
|
|
/// This function is an implementation of `defaultResolve` in
|
|
/// `lib/internal/modules/esm/resolve.js` from Node.
|
|
pub fn resolve(
|
|
&self,
|
|
specifier: &str,
|
|
referrer: &Url,
|
|
referrer_kind: NodeModuleKind,
|
|
mode: NodeResolutionMode,
|
|
) -> Result<NodeResolution, NodeResolveError> {
|
|
// Note: if we are here, then the referrer is an esm module
|
|
// TODO(bartlomieju): skipped "policy" part as we don't plan to support it
|
|
|
|
if self.env.is_builtin_node_module(specifier) {
|
|
return Ok(NodeResolution::BuiltIn(specifier.to_string()));
|
|
}
|
|
|
|
if let Ok(url) = Url::parse(specifier) {
|
|
if url.scheme() == "data" {
|
|
return Ok(NodeResolution::Esm(url));
|
|
}
|
|
|
|
if let Some(module_name) =
|
|
get_module_name_from_builtin_node_module_specifier(&url)
|
|
{
|
|
return Ok(NodeResolution::BuiltIn(module_name.to_string()));
|
|
}
|
|
|
|
let protocol = url.scheme();
|
|
|
|
if protocol != "file" && protocol != "data" {
|
|
return Err(
|
|
UnsupportedEsmUrlSchemeError {
|
|
url_scheme: protocol.to_string(),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
// todo(dsherret): this seems wrong
|
|
if referrer.scheme() == "data" {
|
|
let url = referrer
|
|
.join(specifier)
|
|
.map_err(|source| DataUrlReferrerError { source })?;
|
|
return Ok(NodeResolution::Esm(url));
|
|
}
|
|
}
|
|
|
|
let url = self.module_resolve(
|
|
specifier,
|
|
referrer,
|
|
referrer_kind,
|
|
// even though the referrer may be CJS, if we're here that means we're doing ESM resolution
|
|
DEFAULT_CONDITIONS,
|
|
mode,
|
|
)?;
|
|
|
|
let url = if mode.is_types() {
|
|
let file_path = to_file_path(&url);
|
|
self.path_to_declaration_url(&file_path, Some(referrer), referrer_kind)?
|
|
} else {
|
|
url
|
|
};
|
|
|
|
let url = self.finalize_resolution(url, Some(referrer))?;
|
|
let resolve_response = self.url_to_node_resolution(url)?;
|
|
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
|
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
|
Ok(resolve_response)
|
|
}
|
|
|
|
fn module_resolve(
|
|
&self,
|
|
specifier: &str,
|
|
referrer: &Url,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, NodeResolveError> {
|
|
if should_be_treated_as_relative_or_absolute_path(specifier) {
|
|
Ok(referrer.join(specifier).map_err(|err| {
|
|
NodeResolveRelativeJoinError {
|
|
path: specifier.to_string(),
|
|
base: referrer.clone(),
|
|
source: err,
|
|
}
|
|
})?)
|
|
} else if specifier.starts_with('#') {
|
|
let pkg_config = self
|
|
.get_closest_package_json(referrer)
|
|
.map_err(PackageImportsResolveErrorKind::ClosestPkgJson)
|
|
.map_err(|err| PackageImportsResolveError(Box::new(err)))?;
|
|
Ok(self.package_imports_resolve(
|
|
specifier,
|
|
Some(referrer),
|
|
referrer_kind,
|
|
pkg_config.as_deref(),
|
|
conditions,
|
|
mode,
|
|
)?)
|
|
} else if let Ok(resolved) = Url::parse(specifier) {
|
|
Ok(resolved)
|
|
} else {
|
|
Ok(self.package_resolve(
|
|
specifier,
|
|
referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
)?)
|
|
}
|
|
}
|
|
|
|
fn finalize_resolution(
|
|
&self,
|
|
resolved: Url,
|
|
maybe_referrer: Option<&Url>,
|
|
) -> Result<Url, FinalizeResolutionError> {
|
|
let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C");
|
|
|
|
if encoded_sep_re.is_match(resolved.path()) {
|
|
return Err(
|
|
errors::InvalidModuleSpecifierError {
|
|
request: resolved.to_string(),
|
|
reason: Cow::Borrowed(
|
|
"must not include encoded \"/\" or \"\\\\\" characters",
|
|
),
|
|
maybe_referrer: maybe_referrer.map(to_file_path_string),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
if resolved.scheme() == "node" {
|
|
return Ok(resolved);
|
|
}
|
|
|
|
let path = to_file_path(&resolved);
|
|
|
|
// TODO(bartlomieju): currently not supported
|
|
// if (getOptionValue('--experimental-specifier-resolution') === 'node') {
|
|
// ...
|
|
// }
|
|
|
|
let p_str = path.to_str().unwrap();
|
|
let p = if p_str.ends_with('/') {
|
|
p_str[p_str.len() - 1..].to_string()
|
|
} else {
|
|
p_str.to_string()
|
|
};
|
|
|
|
let (is_dir, is_file) = if let Ok(stats) = self.env.stat_sync(Path::new(&p))
|
|
{
|
|
(stats.is_dir, stats.is_file)
|
|
} else {
|
|
(false, false)
|
|
};
|
|
if is_dir {
|
|
return Err(
|
|
UnsupportedDirImportError {
|
|
dir_url: resolved.clone(),
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
);
|
|
} else if !is_file {
|
|
return Err(
|
|
ModuleNotFoundError {
|
|
specifier: resolved,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
typ: "module",
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
Ok(resolved)
|
|
}
|
|
|
|
pub fn resolve_package_subpath_from_deno_module(
|
|
&self,
|
|
package_dir: &Path,
|
|
package_subpath: Option<&str>,
|
|
maybe_referrer: Option<&Url>,
|
|
mode: NodeResolutionMode,
|
|
) -> Result<NodeResolution, ResolvePkgSubpathFromDenoModuleError> {
|
|
let node_module_kind = NodeModuleKind::Esm;
|
|
let package_subpath = package_subpath
|
|
.map(|s| format!("./{s}"))
|
|
.unwrap_or_else(|| ".".to_string());
|
|
let resolved_url = self.resolve_package_dir_subpath(
|
|
package_dir,
|
|
&package_subpath,
|
|
maybe_referrer,
|
|
node_module_kind,
|
|
DEFAULT_CONDITIONS,
|
|
mode,
|
|
)?;
|
|
let resolve_response = self.url_to_node_resolution(resolved_url)?;
|
|
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
|
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
|
Ok(resolve_response)
|
|
}
|
|
|
|
pub fn resolve_binary_commands(
|
|
&self,
|
|
package_folder: &Path,
|
|
) -> Result<Vec<String>, ResolveBinaryCommandsError> {
|
|
let pkg_json_path = package_folder.join("package.json");
|
|
let Some(package_json) = self.load_package_json(&pkg_json_path)? else {
|
|
return Ok(Vec::new());
|
|
};
|
|
|
|
Ok(match &package_json.bin {
|
|
Some(Value::String(_)) => {
|
|
let Some(name) = &package_json.name else {
|
|
return Err(ResolveBinaryCommandsError::MissingPkgJsonName {
|
|
pkg_json_path,
|
|
});
|
|
};
|
|
let name = name.split("/").last().unwrap();
|
|
vec![name.to_string()]
|
|
}
|
|
Some(Value::Object(o)) => {
|
|
o.iter().map(|(key, _)| key.clone()).collect::<Vec<_>>()
|
|
}
|
|
_ => Vec::new(),
|
|
})
|
|
}
|
|
|
|
pub fn resolve_binary_export(
|
|
&self,
|
|
package_folder: &Path,
|
|
sub_path: Option<&str>,
|
|
) -> Result<NodeResolution, ResolvePkgJsonBinExportError> {
|
|
let pkg_json_path = package_folder.join("package.json");
|
|
let Some(package_json) = self.load_package_json(&pkg_json_path)? else {
|
|
return Err(ResolvePkgJsonBinExportError::MissingPkgJson {
|
|
pkg_json_path,
|
|
});
|
|
};
|
|
let bin_entry =
|
|
resolve_bin_entry_value(&package_json, sub_path).map_err(|err| {
|
|
ResolvePkgJsonBinExportError::InvalidBinProperty {
|
|
message: err.to_string(),
|
|
}
|
|
})?;
|
|
let url = to_file_specifier(&package_folder.join(bin_entry));
|
|
|
|
let resolve_response = self.url_to_node_resolution(url)?;
|
|
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
|
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
|
Ok(resolve_response)
|
|
}
|
|
|
|
pub fn url_to_node_resolution(
|
|
&self,
|
|
url: Url,
|
|
) -> Result<NodeResolution, UrlToNodeResolutionError> {
|
|
let url_str = url.as_str().to_lowercase();
|
|
if url_str.starts_with("http") || url_str.ends_with(".json") {
|
|
Ok(NodeResolution::Esm(url))
|
|
} else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") {
|
|
let maybe_package_config = self.get_closest_package_json(&url)?;
|
|
match maybe_package_config {
|
|
Some(c) if c.typ == "module" => Ok(NodeResolution::Esm(url)),
|
|
Some(_) => Ok(NodeResolution::CommonJs(url)),
|
|
None => Ok(NodeResolution::Esm(url)),
|
|
}
|
|
} else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") {
|
|
Ok(NodeResolution::Esm(url))
|
|
} else if url_str.ends_with(".ts") || url_str.ends_with(".mts") {
|
|
if self.in_npm_package(&url) {
|
|
Err(TypeScriptNotSupportedInNpmError { specifier: url }.into())
|
|
} else {
|
|
Ok(NodeResolution::Esm(url))
|
|
}
|
|
} else {
|
|
Ok(NodeResolution::CommonJs(url))
|
|
}
|
|
}
|
|
|
|
/// Checks if the resolved file has a corresponding declaration file.
|
|
fn path_to_declaration_url(
|
|
&self,
|
|
path: &Path,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
) -> Result<Url, TypesNotFoundError> {
|
|
fn probe_extensions<TEnv: NodeResolverEnv>(
|
|
fs: &TEnv,
|
|
path: &Path,
|
|
lowercase_path: &str,
|
|
referrer_kind: NodeModuleKind,
|
|
) -> Option<PathBuf> {
|
|
let mut searched_for_d_mts = false;
|
|
let mut searched_for_d_cts = false;
|
|
if lowercase_path.ends_with(".mjs") {
|
|
let d_mts_path = with_known_extension(path, "d.mts");
|
|
if fs.exists_sync(&d_mts_path) {
|
|
return Some(d_mts_path);
|
|
}
|
|
searched_for_d_mts = true;
|
|
} else if lowercase_path.ends_with(".cjs") {
|
|
let d_cts_path = with_known_extension(path, "d.cts");
|
|
if fs.exists_sync(&d_cts_path) {
|
|
return Some(d_cts_path);
|
|
}
|
|
searched_for_d_cts = true;
|
|
}
|
|
|
|
let dts_path = with_known_extension(path, "d.ts");
|
|
if fs.exists_sync(&dts_path) {
|
|
return Some(dts_path);
|
|
}
|
|
|
|
let specific_dts_path = match referrer_kind {
|
|
NodeModuleKind::Cjs if !searched_for_d_cts => {
|
|
Some(with_known_extension(path, "d.cts"))
|
|
}
|
|
NodeModuleKind::Esm if !searched_for_d_mts => {
|
|
Some(with_known_extension(path, "d.mts"))
|
|
}
|
|
_ => None, // already searched above
|
|
};
|
|
if let Some(specific_dts_path) = specific_dts_path {
|
|
if fs.exists_sync(&specific_dts_path) {
|
|
return Some(specific_dts_path);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
let lowercase_path = path.to_string_lossy().to_lowercase();
|
|
if lowercase_path.ends_with(".d.ts")
|
|
|| lowercase_path.ends_with(".d.cts")
|
|
|| lowercase_path.ends_with(".d.mts")
|
|
{
|
|
return Ok(to_file_specifier(path));
|
|
}
|
|
if let Some(path) =
|
|
probe_extensions(&self.env, path, &lowercase_path, referrer_kind)
|
|
{
|
|
return Ok(to_file_specifier(&path));
|
|
}
|
|
if self.env.is_dir_sync(path) {
|
|
let resolution_result = self.resolve_package_dir_subpath(
|
|
path,
|
|
/* sub path */ ".",
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
match referrer_kind {
|
|
NodeModuleKind::Esm => DEFAULT_CONDITIONS,
|
|
NodeModuleKind::Cjs => REQUIRE_CONDITIONS,
|
|
},
|
|
NodeResolutionMode::Types,
|
|
);
|
|
if let Ok(resolution) = resolution_result {
|
|
return Ok(resolution);
|
|
}
|
|
let index_path = path.join("index.js");
|
|
if let Some(path) = probe_extensions(
|
|
&self.env,
|
|
&index_path,
|
|
&index_path.to_string_lossy().to_lowercase(),
|
|
referrer_kind,
|
|
) {
|
|
return Ok(to_file_specifier(&path));
|
|
}
|
|
}
|
|
// allow resolving .css files for types resolution
|
|
if lowercase_path.ends_with(".css") {
|
|
return Ok(to_file_specifier(path));
|
|
}
|
|
Err(TypesNotFoundError(Box::new(TypesNotFoundErrorData {
|
|
code_specifier: to_file_specifier(path),
|
|
maybe_referrer: maybe_referrer.cloned(),
|
|
})))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn package_imports_resolve(
|
|
&self,
|
|
name: &str,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
referrer_pkg_json: Option<&PackageJson>,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageImportsResolveError> {
|
|
if name == "#" || name.starts_with("#/") || name.ends_with('/') {
|
|
let reason = "is not a valid internal imports specifier name";
|
|
return Err(
|
|
errors::InvalidModuleSpecifierError {
|
|
request: name.to_string(),
|
|
reason: Cow::Borrowed(reason),
|
|
maybe_referrer: maybe_referrer.map(to_specifier_display_string),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
let mut package_json_path = None;
|
|
if let Some(pkg_json) = &referrer_pkg_json {
|
|
package_json_path = Some(pkg_json.path.clone());
|
|
if let Some(imports) = &pkg_json.imports {
|
|
if imports.contains_key(name) && !name.contains('*') {
|
|
let target = imports.get(name).unwrap();
|
|
let maybe_resolved = self.resolve_package_target(
|
|
package_json_path.as_ref().unwrap(),
|
|
target,
|
|
"",
|
|
name,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
false,
|
|
true,
|
|
conditions,
|
|
mode,
|
|
)?;
|
|
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())],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !best_match.is_empty() {
|
|
let target = imports.get(best_match).unwrap();
|
|
let maybe_resolved = self.resolve_package_target(
|
|
package_json_path.as_ref().unwrap(),
|
|
target,
|
|
best_match_subpath.unwrap(),
|
|
best_match,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
true,
|
|
true,
|
|
conditions,
|
|
mode,
|
|
)?;
|
|
if let Some(resolved) = maybe_resolved {
|
|
return Ok(resolved);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(
|
|
PackageImportNotDefinedError {
|
|
name: name.to_string(),
|
|
package_json_path,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_target_string(
|
|
&self,
|
|
target: &str,
|
|
subpath: &str,
|
|
match_: &str,
|
|
package_json_path: &Path,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
pattern: bool,
|
|
internal: bool,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageTargetResolveError> {
|
|
if !subpath.is_empty() && !pattern && !target.ends_with('/') {
|
|
return Err(
|
|
InvalidPackageTargetError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
sub_path: match_.to_string(),
|
|
target: target.to_string(),
|
|
is_import: internal,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
let invalid_segment_re =
|
|
lazy_regex::regex!(r"(^|\\|/)(\.\.?|node_modules)(\\|/|$)");
|
|
let pattern_re = lazy_regex::regex!(r"\*");
|
|
if !target.starts_with("./") {
|
|
if internal && !target.starts_with("../") && !target.starts_with('/') {
|
|
let target_url = Url::parse(target);
|
|
match target_url {
|
|
Ok(url) => {
|
|
if get_module_name_from_builtin_node_module_specifier(&url)
|
|
.is_some()
|
|
{
|
|
return Ok(url);
|
|
}
|
|
}
|
|
Err(_) => {
|
|
let export_target = if pattern {
|
|
pattern_re
|
|
.replace(target, |_caps: ®ex::Captures| subpath)
|
|
.to_string()
|
|
} else {
|
|
format!("{target}{subpath}")
|
|
};
|
|
let package_json_url = to_file_specifier(package_json_path);
|
|
let result = match self.package_resolve(
|
|
&export_target,
|
|
&package_json_url,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
) {
|
|
Ok(url) => Ok(url),
|
|
Err(err) => match err.code() {
|
|
NodeJsErrorCode::ERR_INVALID_MODULE_SPECIFIER
|
|
| NodeJsErrorCode::ERR_INVALID_PACKAGE_CONFIG
|
|
| NodeJsErrorCode::ERR_INVALID_PACKAGE_TARGET
|
|
| NodeJsErrorCode::ERR_PACKAGE_IMPORT_NOT_DEFINED
|
|
| NodeJsErrorCode::ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
| NodeJsErrorCode::ERR_UNKNOWN_FILE_EXTENSION
|
|
| NodeJsErrorCode::ERR_UNSUPPORTED_DIR_IMPORT
|
|
| NodeJsErrorCode::ERR_UNSUPPORTED_ESM_URL_SCHEME
|
|
| NodeJsErrorCode::ERR_TYPES_NOT_FOUND => {
|
|
Err(PackageTargetResolveErrorKind::PackageResolve(err).into())
|
|
}
|
|
NodeJsErrorCode::ERR_MODULE_NOT_FOUND => Err(
|
|
PackageTargetResolveErrorKind::NotFound(
|
|
PackageTargetNotFoundError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
target: export_target.to_string(),
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
referrer_kind,
|
|
mode,
|
|
},
|
|
)
|
|
.into(),
|
|
),
|
|
},
|
|
};
|
|
|
|
return match result {
|
|
Ok(url) => Ok(url),
|
|
Err(err) => {
|
|
if self.env.is_builtin_node_module(target) {
|
|
Ok(Url::parse(&format!("node:{}", target)).unwrap())
|
|
} else {
|
|
Err(err)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return Err(
|
|
InvalidPackageTargetError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
sub_path: match_.to_string(),
|
|
target: target.to_string(),
|
|
is_import: internal,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
if invalid_segment_re.is_match(&target[2..]) {
|
|
return Err(
|
|
InvalidPackageTargetError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
sub_path: match_.to_string(),
|
|
target: target.to_string(),
|
|
is_import: internal,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
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(
|
|
InvalidPackageTargetError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
sub_path: match_.to_string(),
|
|
target: target.to_string(),
|
|
is_import: internal,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
if subpath.is_empty() {
|
|
return Ok(to_file_specifier(&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,
|
|
maybe_referrer,
|
|
)
|
|
.into(),
|
|
);
|
|
}
|
|
if pattern {
|
|
let resolved_path_str = resolved_path.to_string_lossy();
|
|
let replaced = pattern_re
|
|
.replace(&resolved_path_str, |_caps: ®ex::Captures| subpath);
|
|
return Ok(to_file_specifier(&PathBuf::from(replaced.to_string())));
|
|
}
|
|
Ok(to_file_specifier(&resolved_path.join(subpath).clean()))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_target(
|
|
&self,
|
|
package_json_path: &Path,
|
|
target: &Value,
|
|
subpath: &str,
|
|
package_subpath: &str,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
pattern: bool,
|
|
internal: bool,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Option<Url>, PackageTargetResolveError> {
|
|
let result = self.resolve_package_target_inner(
|
|
package_json_path,
|
|
target,
|
|
subpath,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
pattern,
|
|
internal,
|
|
conditions,
|
|
mode,
|
|
);
|
|
match result {
|
|
Ok(maybe_resolved) => Ok(maybe_resolved),
|
|
Err(err) => {
|
|
if mode.is_types()
|
|
&& err.code() == NodeJsErrorCode::ERR_TYPES_NOT_FOUND
|
|
&& conditions != TYPES_ONLY_CONDITIONS
|
|
{
|
|
// try resolving with just "types" conditions for when someone misconfigures
|
|
// and puts the "types" condition in the wrong place
|
|
if let Ok(Some(resolved)) = self.resolve_package_target_inner(
|
|
package_json_path,
|
|
target,
|
|
subpath,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
pattern,
|
|
internal,
|
|
TYPES_ONLY_CONDITIONS,
|
|
mode,
|
|
) {
|
|
return Ok(Some(resolved));
|
|
}
|
|
}
|
|
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_target_inner(
|
|
&self,
|
|
package_json_path: &Path,
|
|
target: &Value,
|
|
subpath: &str,
|
|
package_subpath: &str,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
pattern: bool,
|
|
internal: bool,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Option<Url>, PackageTargetResolveError> {
|
|
if let Some(target) = target.as_str() {
|
|
let url = self.resolve_package_target_string(
|
|
target,
|
|
subpath,
|
|
package_subpath,
|
|
package_json_path,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
pattern,
|
|
internal,
|
|
conditions,
|
|
mode,
|
|
)?;
|
|
if mode.is_types() && url.scheme() == "file" {
|
|
let path = url.to_file_path().unwrap();
|
|
return Ok(Some(self.path_to_declaration_url(
|
|
&path,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
)?));
|
|
} else {
|
|
return Ok(Some(url));
|
|
}
|
|
} 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 = self.resolve_package_target(
|
|
package_json_path,
|
|
target_item,
|
|
subpath,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
pattern,
|
|
internal,
|
|
conditions,
|
|
mode,
|
|
);
|
|
|
|
match resolved_result {
|
|
Ok(Some(resolved)) => return Ok(Some(resolved)),
|
|
Ok(None) => {
|
|
last_error = None;
|
|
continue;
|
|
}
|
|
Err(e) => {
|
|
if e.code() == NodeJsErrorCode::ERR_INVALID_PACKAGE_TARGET {
|
|
last_error = Some(e);
|
|
continue;
|
|
} else {
|
|
return Err(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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())
|
|
|| mode.is_types() && key.as_str() == "types"
|
|
{
|
|
let condition_target = target_obj.get(key).unwrap();
|
|
|
|
let resolved = self.resolve_package_target(
|
|
package_json_path,
|
|
condition_target,
|
|
subpath,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
pattern,
|
|
internal,
|
|
conditions,
|
|
mode,
|
|
)?;
|
|
match resolved {
|
|
Some(resolved) => return Ok(Some(resolved)),
|
|
None => {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if target.is_null() {
|
|
return Ok(None);
|
|
}
|
|
|
|
Err(
|
|
InvalidPackageTargetError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
sub_path: package_subpath.to_string(),
|
|
target: target.to_string(),
|
|
is_import: internal,
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
}
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn package_exports_resolve(
|
|
&self,
|
|
package_json_path: &Path,
|
|
package_subpath: &str,
|
|
package_exports: &Map<String, Value>,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageExportsResolveError> {
|
|
if package_exports.contains_key(package_subpath)
|
|
&& package_subpath.find('*').is_none()
|
|
&& !package_subpath.ends_with('/')
|
|
{
|
|
let target = package_exports.get(package_subpath).unwrap();
|
|
let resolved = self.resolve_package_target(
|
|
package_json_path,
|
|
target,
|
|
"",
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
false,
|
|
false,
|
|
conditions,
|
|
mode,
|
|
)?;
|
|
return match resolved {
|
|
Some(resolved) => Ok(resolved),
|
|
None => Err(
|
|
PackagePathNotExportedError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
subpath: package_subpath.to_string(),
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
mode,
|
|
}
|
|
.into(),
|
|
),
|
|
};
|
|
}
|
|
|
|
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();
|
|
let maybe_resolved = self.resolve_package_target(
|
|
package_json_path,
|
|
target,
|
|
&best_match_subpath.unwrap(),
|
|
best_match,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
true,
|
|
false,
|
|
conditions,
|
|
mode,
|
|
)?;
|
|
if let Some(resolved) = maybe_resolved {
|
|
return Ok(resolved);
|
|
} else {
|
|
return Err(
|
|
PackagePathNotExportedError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
subpath: package_subpath.to_string(),
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
mode,
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
}
|
|
|
|
Err(
|
|
PackagePathNotExportedError {
|
|
pkg_json_path: package_json_path.to_path_buf(),
|
|
subpath: package_subpath.to_string(),
|
|
maybe_referrer: maybe_referrer.map(ToOwned::to_owned),
|
|
mode,
|
|
}
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
pub(super) fn package_resolve(
|
|
&self,
|
|
specifier: &str,
|
|
referrer: &Url,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageResolveError> {
|
|
let (package_name, package_subpath, _is_scoped) =
|
|
parse_npm_pkg_name(specifier, referrer)?;
|
|
|
|
if let Some(package_config) = self.get_closest_package_json(referrer)? {
|
|
// ResolveSelf
|
|
if package_config.name.as_ref() == Some(&package_name) {
|
|
if let Some(exports) = &package_config.exports {
|
|
return self
|
|
.package_exports_resolve(
|
|
&package_config.path,
|
|
&package_subpath,
|
|
exports,
|
|
Some(referrer),
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
)
|
|
.map_err(|err| err.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
self.resolve_package_subpath_for_package(
|
|
&package_name,
|
|
&package_subpath,
|
|
referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_subpath_for_package(
|
|
&self,
|
|
package_name: &str,
|
|
package_subpath: &str,
|
|
referrer: &Url,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageResolveError> {
|
|
let result = self.resolve_package_subpath_for_package_inner(
|
|
package_name,
|
|
package_subpath,
|
|
referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
);
|
|
if mode.is_types() && !matches!(result, Ok(Url { .. })) {
|
|
// try to resolve with the @types package
|
|
let package_name = types_package_name(package_name);
|
|
if let Ok(result) = self.resolve_package_subpath_for_package_inner(
|
|
&package_name,
|
|
package_subpath,
|
|
referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
) {
|
|
return Ok(result);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_subpath_for_package_inner(
|
|
&self,
|
|
package_name: &str,
|
|
package_subpath: &str,
|
|
referrer: &Url,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageResolveError> {
|
|
let package_dir_path = self
|
|
.npm_resolver
|
|
.resolve_package_folder_from_package(package_name, referrer)?;
|
|
|
|
// 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.
|
|
self
|
|
.resolve_package_dir_subpath(
|
|
&package_dir_path,
|
|
package_subpath,
|
|
Some(referrer),
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
)
|
|
.map_err(|err| err.into())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_dir_subpath(
|
|
&self,
|
|
package_dir_path: &Path,
|
|
package_subpath: &str,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageSubpathResolveError> {
|
|
let package_json_path = package_dir_path.join("package.json");
|
|
match self.load_package_json(&package_json_path)? {
|
|
Some(pkg_json) => self.resolve_package_subpath(
|
|
&pkg_json,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
),
|
|
None => self
|
|
.resolve_package_subpath_no_pkg_json(
|
|
package_dir_path,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
mode,
|
|
)
|
|
.map_err(|err| {
|
|
PackageSubpathResolveErrorKind::LegacyResolve(err).into()
|
|
}),
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn resolve_package_subpath(
|
|
&self,
|
|
package_json: &PackageJson,
|
|
package_subpath: &str,
|
|
referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, PackageSubpathResolveError> {
|
|
if let Some(exports) = &package_json.exports {
|
|
let result = self.package_exports_resolve(
|
|
&package_json.path,
|
|
package_subpath,
|
|
exports,
|
|
referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
mode,
|
|
);
|
|
match result {
|
|
Ok(found) => return Ok(found),
|
|
Err(exports_err) => {
|
|
if mode.is_types() && package_subpath == "." {
|
|
return self
|
|
.legacy_main_resolve(package_json, referrer, referrer_kind, mode)
|
|
.map_err(|err| {
|
|
PackageSubpathResolveErrorKind::LegacyResolve(err).into()
|
|
});
|
|
}
|
|
return Err(
|
|
PackageSubpathResolveErrorKind::Exports(exports_err).into(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if package_subpath == "." {
|
|
return self
|
|
.legacy_main_resolve(package_json, referrer, referrer_kind, mode)
|
|
.map_err(|err| {
|
|
PackageSubpathResolveErrorKind::LegacyResolve(err).into()
|
|
});
|
|
}
|
|
|
|
self
|
|
.resolve_subpath_exact(
|
|
package_json.path.parent().unwrap(),
|
|
package_subpath,
|
|
referrer,
|
|
referrer_kind,
|
|
mode,
|
|
)
|
|
.map_err(|err| {
|
|
PackageSubpathResolveErrorKind::LegacyResolve(err.into()).into()
|
|
})
|
|
}
|
|
|
|
fn resolve_subpath_exact(
|
|
&self,
|
|
directory: &Path,
|
|
package_subpath: &str,
|
|
referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, TypesNotFoundError> {
|
|
assert_ne!(package_subpath, ".");
|
|
let file_path = directory.join(package_subpath);
|
|
if mode.is_types() {
|
|
Ok(self.path_to_declaration_url(&file_path, referrer, referrer_kind)?)
|
|
} else {
|
|
Ok(to_file_specifier(&file_path))
|
|
}
|
|
}
|
|
|
|
fn resolve_package_subpath_no_pkg_json(
|
|
&self,
|
|
directory: &Path,
|
|
package_subpath: &str,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, LegacyResolveError> {
|
|
if package_subpath == "." {
|
|
self.legacy_index_resolve(directory, maybe_referrer, referrer_kind, mode)
|
|
} else {
|
|
self
|
|
.resolve_subpath_exact(
|
|
directory,
|
|
package_subpath,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
mode,
|
|
)
|
|
.map_err(|err| err.into())
|
|
}
|
|
}
|
|
|
|
pub fn get_closest_package_json(
|
|
&self,
|
|
url: &Url,
|
|
) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> {
|
|
let Ok(file_path) = url.to_file_path() else {
|
|
return Ok(None);
|
|
};
|
|
self.get_closest_package_json_from_path(&file_path)
|
|
}
|
|
|
|
pub fn get_closest_package_json_from_path(
|
|
&self,
|
|
file_path: &Path,
|
|
) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> {
|
|
// we use this for deno compile using byonm because the script paths
|
|
// won't be in virtual file system, but the package.json paths will be
|
|
fn canonicalize_first_ancestor_exists(
|
|
dir_path: &Path,
|
|
env: &dyn NodeResolverEnv,
|
|
) -> Result<Option<PathBuf>, std::io::Error> {
|
|
for ancestor in dir_path.ancestors() {
|
|
match env.realpath_sync(ancestor) {
|
|
Ok(dir_path) => return Ok(Some(dir_path)),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
// keep searching
|
|
}
|
|
Err(err) => return Err(err),
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
let parent_dir = file_path.parent().unwrap();
|
|
let Some(start_dir) = canonicalize_first_ancestor_exists(
|
|
parent_dir, &self.env,
|
|
)
|
|
.map_err(|source| CanonicalizingPkgJsonDirError {
|
|
dir_path: parent_dir.to_path_buf(),
|
|
source,
|
|
})?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let start_dir = strip_unc_prefix(start_dir);
|
|
for current_dir in start_dir.ancestors() {
|
|
let package_json_path = current_dir.join("package.json");
|
|
if let Some(pkg_json) = self.load_package_json(&package_json_path)? {
|
|
return Ok(Some(pkg_json));
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub fn load_package_json(
|
|
&self,
|
|
package_json_path: &Path,
|
|
) -> Result<Option<PackageJsonRc>, PackageJsonLoadError> {
|
|
crate::package_json::load_pkg_json(
|
|
self.env.pkg_json_fs(),
|
|
package_json_path,
|
|
)
|
|
}
|
|
|
|
pub(super) fn legacy_main_resolve(
|
|
&self,
|
|
package_json: &PackageJson,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, LegacyResolveError> {
|
|
let maybe_main = if mode.is_types() {
|
|
match package_json.types.as_ref() {
|
|
Some(types) => Some(types.as_str()),
|
|
None => {
|
|
// fallback to checking the main entrypoint for
|
|
// a corresponding declaration file
|
|
if let Some(main) = package_json.main(referrer_kind) {
|
|
let main = package_json.path.parent().unwrap().join(main).clean();
|
|
let decl_url_result = self.path_to_declaration_url(
|
|
&main,
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
);
|
|
// don't surface errors, fallback to checking the index now
|
|
if let Ok(url) = decl_url_result {
|
|
return Ok(url);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
package_json.main(referrer_kind)
|
|
};
|
|
|
|
if let Some(main) = maybe_main {
|
|
let guess = package_json.path.parent().unwrap().join(main).clean();
|
|
if self.env.is_file_sync(&guess) {
|
|
return Ok(to_file_specifier(&guess));
|
|
}
|
|
|
|
// todo(dsherret): investigate exactly how node and typescript handles this
|
|
let endings = if mode.is_types() {
|
|
match referrer_kind {
|
|
NodeModuleKind::Cjs => {
|
|
vec![".d.ts", ".d.cts", "/index.d.ts", "/index.d.cts"]
|
|
}
|
|
NodeModuleKind::Esm => vec![
|
|
".d.ts",
|
|
".d.mts",
|
|
"/index.d.ts",
|
|
"/index.d.mts",
|
|
".d.cts",
|
|
"/index.d.cts",
|
|
],
|
|
}
|
|
} else {
|
|
vec![".js", "/index.js"]
|
|
};
|
|
for ending in endings {
|
|
let guess = package_json
|
|
.path
|
|
.parent()
|
|
.unwrap()
|
|
.join(format!("{main}{ending}"))
|
|
.clean();
|
|
if self.env.is_file_sync(&guess) {
|
|
// TODO(bartlomieju): emitLegacyIndexDeprecation()
|
|
return Ok(to_file_specifier(&guess));
|
|
}
|
|
}
|
|
}
|
|
|
|
self.legacy_index_resolve(
|
|
package_json.path.parent().unwrap(),
|
|
maybe_referrer,
|
|
referrer_kind,
|
|
mode,
|
|
)
|
|
}
|
|
|
|
fn legacy_index_resolve(
|
|
&self,
|
|
directory: &Path,
|
|
maybe_referrer: Option<&Url>,
|
|
referrer_kind: NodeModuleKind,
|
|
mode: NodeResolutionMode,
|
|
) -> Result<Url, LegacyResolveError> {
|
|
let index_file_names = if mode.is_types() {
|
|
// todo(dsherret): investigate exactly how typescript does this
|
|
match referrer_kind {
|
|
NodeModuleKind::Cjs => vec!["index.d.ts", "index.d.cts"],
|
|
NodeModuleKind::Esm => vec!["index.d.ts", "index.d.mts", "index.d.cts"],
|
|
}
|
|
} else {
|
|
vec!["index.js"]
|
|
};
|
|
for index_file_name in index_file_names {
|
|
let guess = directory.join(index_file_name).clean();
|
|
if self.env.is_file_sync(&guess) {
|
|
// TODO(bartlomieju): emitLegacyIndexDeprecation()
|
|
return Ok(to_file_specifier(&guess));
|
|
}
|
|
}
|
|
|
|
if mode.is_types() {
|
|
Err(
|
|
TypesNotFoundError(Box::new(TypesNotFoundErrorData {
|
|
code_specifier: to_file_specifier(&directory.join("index.js")),
|
|
maybe_referrer: maybe_referrer.cloned(),
|
|
}))
|
|
.into(),
|
|
)
|
|
} else {
|
|
Err(
|
|
ModuleNotFoundError {
|
|
specifier: to_file_specifier(&directory.join("index.js")),
|
|
typ: "module",
|
|
maybe_referrer: maybe_referrer.cloned(),
|
|
}
|
|
.into(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resolve_bin_entry_value<'a>(
|
|
package_json: &'a PackageJson,
|
|
bin_name: Option<&str>,
|
|
) -> Result<&'a str, AnyError> {
|
|
let bin = match &package_json.bin {
|
|
Some(bin) => bin,
|
|
None => bail!(
|
|
"'{}' did not have a bin property",
|
|
package_json.path.display(),
|
|
),
|
|
};
|
|
let bin_entry = match bin {
|
|
Value::String(_) => {
|
|
if bin_name.is_some()
|
|
&& bin_name
|
|
!= package_json
|
|
.name
|
|
.as_deref()
|
|
.map(|name| name.rsplit_once('/').map_or(name, |(_, name)| name))
|
|
{
|
|
None
|
|
} else {
|
|
Some(bin)
|
|
}
|
|
}
|
|
Value::Object(o) => {
|
|
if let Some(bin_name) = bin_name {
|
|
o.get(bin_name)
|
|
} else if o.len() == 1
|
|
|| o.len() > 1 && o.values().all(|v| v == o.values().next().unwrap())
|
|
{
|
|
o.values().next()
|
|
} else {
|
|
package_json.name.as_ref().and_then(|n| o.get(n))
|
|
}
|
|
}
|
|
_ => bail!(
|
|
"'{}' did not have a bin property with a string or object value",
|
|
package_json.path.display()
|
|
),
|
|
};
|
|
let bin_entry = match bin_entry {
|
|
Some(e) => e,
|
|
None => {
|
|
let prefix = package_json
|
|
.name
|
|
.as_ref()
|
|
.map(|n| {
|
|
let mut prefix = format!("npm:{}", n);
|
|
if let Some(version) = &package_json.version {
|
|
prefix.push('@');
|
|
prefix.push_str(version);
|
|
}
|
|
prefix.push('/');
|
|
prefix
|
|
})
|
|
.unwrap_or_default();
|
|
let keys = bin
|
|
.as_object()
|
|
.map(|o| {
|
|
o.keys()
|
|
.map(|k| format!(" * {prefix}{k}"))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
bail!(
|
|
"'{}' did not have a bin entry{}{}",
|
|
package_json.path.display(),
|
|
bin_name
|
|
.or(package_json.name.as_deref())
|
|
.map(|name| format!(" for '{}'", name))
|
|
.unwrap_or_default(),
|
|
if keys.is_empty() {
|
|
"".to_string()
|
|
} else {
|
|
format!("\n\nPossibilities:\n{}", keys.join("\n"))
|
|
}
|
|
)
|
|
}
|
|
};
|
|
match bin_entry {
|
|
Value::String(s) => Ok(s),
|
|
_ => bail!(
|
|
"'{}' had a non-string sub property of bin",
|
|
package_json.path.display(),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn to_file_path(url: &Url) -> PathBuf {
|
|
url
|
|
.to_file_path()
|
|
.unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}"))
|
|
}
|
|
|
|
fn to_file_path_string(url: &Url) -> String {
|
|
to_file_path(url).display().to_string()
|
|
}
|
|
|
|
fn should_be_treated_as_relative_or_absolute_path(specifier: &str) -> bool {
|
|
if specifier.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
if specifier.starts_with('/') {
|
|
return true;
|
|
}
|
|
|
|
is_relative_specifier(specifier)
|
|
}
|
|
|
|
// TODO(ry) We very likely have this utility function elsewhere in Deno.
|
|
fn is_relative_specifier(specifier: &str) -> bool {
|
|
let specifier_len = specifier.len();
|
|
let specifier_chars: Vec<_> = specifier.chars().take(3).collect();
|
|
|
|
if !specifier_chars.is_empty() && specifier_chars[0] == '.' {
|
|
if specifier_len == 1 || specifier_chars[1] == '/' {
|
|
return true;
|
|
}
|
|
if specifier_chars[1] == '.'
|
|
&& (specifier_len == 2 || specifier_chars[2] == '/')
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Alternate `PathBuf::with_extension` that will handle known extensions
|
|
/// more intelligently.
|
|
fn with_known_extension(path: &Path, ext: &str) -> PathBuf {
|
|
const NON_DECL_EXTS: &[&str] = &[
|
|
"cjs", "js", "json", "jsx", "mjs", "tsx", /* ex. types.d */ "d",
|
|
];
|
|
const DECL_EXTS: &[&str] = &["cts", "mts", "ts"];
|
|
|
|
let file_name = match path.file_name() {
|
|
Some(value) => value.to_string_lossy(),
|
|
None => return path.to_path_buf(),
|
|
};
|
|
let lowercase_file_name = file_name.to_lowercase();
|
|
let period_index = lowercase_file_name.rfind('.').and_then(|period_index| {
|
|
let ext = &lowercase_file_name[period_index + 1..];
|
|
if DECL_EXTS.contains(&ext) {
|
|
if let Some(next_period_index) =
|
|
lowercase_file_name[..period_index].rfind('.')
|
|
{
|
|
if &lowercase_file_name[next_period_index + 1..period_index] == "d" {
|
|
Some(next_period_index)
|
|
} else {
|
|
Some(period_index)
|
|
}
|
|
} else {
|
|
Some(period_index)
|
|
}
|
|
} else if NON_DECL_EXTS.contains(&ext) {
|
|
Some(period_index)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
let file_name = match period_index {
|
|
Some(period_index) => &file_name[..period_index],
|
|
None => &file_name,
|
|
};
|
|
path.with_file_name(format!("{file_name}.{ext}"))
|
|
}
|
|
|
|
fn to_specifier_display_string(url: &Url) -> String {
|
|
if let Ok(path) = url.to_file_path() {
|
|
path.display().to_string()
|
|
} else {
|
|
url.to_string()
|
|
}
|
|
}
|
|
|
|
fn throw_invalid_subpath(
|
|
subpath: String,
|
|
package_json_path: &Path,
|
|
internal: bool,
|
|
maybe_referrer: Option<&Url>,
|
|
) -> InvalidModuleSpecifierError {
|
|
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(),
|
|
);
|
|
InvalidModuleSpecifierError {
|
|
request: subpath,
|
|
reason: Cow::Owned(reason),
|
|
maybe_referrer: maybe_referrer.map(to_specifier_display_string),
|
|
}
|
|
}
|
|
|
|
pub fn parse_npm_pkg_name(
|
|
specifier: &str,
|
|
referrer: &Url,
|
|
) -> Result<(String, String, bool), InvalidModuleSpecifierError> {
|
|
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::InvalidModuleSpecifierError {
|
|
request: specifier.to_string(),
|
|
reason: Cow::Borrowed("is not a valid package name"),
|
|
maybe_referrer: 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))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// Gets the corresponding @types package for the provided package name.
|
|
fn types_package_name(package_name: &str) -> String {
|
|
debug_assert!(!package_name.starts_with("@types/"));
|
|
// Scoped packages will get two underscores for each slash
|
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/tree/15f1ece08f7b498f4b9a2147c2a46e94416ca777#what-about-scoped-packages
|
|
format!("@types/{}", package_name.replace('/', "__"))
|
|
}
|
|
|
|
/// Ex. returns `fs` for `node:fs`
|
|
fn get_module_name_from_builtin_node_module_specifier(
|
|
specifier: &Url,
|
|
) -> Option<&str> {
|
|
if specifier.scheme() != "node" {
|
|
return None;
|
|
}
|
|
|
|
let (_, specifier) = specifier.as_str().split_once(':')?;
|
|
Some(specifier)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
|
|
fn build_package_json(json: Value) -> PackageJson {
|
|
PackageJson::load_from_value(PathBuf::from("/package.json"), json)
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_bin_entry_value() {
|
|
// should resolve the specified value
|
|
let pkg_json = build_package_json(json!({
|
|
"name": "pkg",
|
|
"version": "1.1.1",
|
|
"bin": {
|
|
"bin1": "./value1",
|
|
"bin2": "./value2",
|
|
"pkg": "./value3",
|
|
}
|
|
}));
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, Some("bin1")).unwrap(),
|
|
"./value1"
|
|
);
|
|
|
|
// should resolve the value with the same name when not specified
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, None).unwrap(),
|
|
"./value3"
|
|
);
|
|
|
|
// should not resolve when specified value does not exist
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, Some("other"),)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
concat!(
|
|
"'/package.json' did not have a bin entry for 'other'\n",
|
|
"\n",
|
|
"Possibilities:\n",
|
|
" * npm:pkg@1.1.1/bin1\n",
|
|
" * npm:pkg@1.1.1/bin2\n",
|
|
" * npm:pkg@1.1.1/pkg"
|
|
)
|
|
);
|
|
|
|
// should not resolve when default value can't be determined
|
|
let pkg_json = build_package_json(json!({
|
|
"name": "pkg",
|
|
"version": "1.1.1",
|
|
"bin": {
|
|
"bin": "./value1",
|
|
"bin2": "./value2",
|
|
}
|
|
}));
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, None)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
concat!(
|
|
"'/package.json' did not have a bin entry for 'pkg'\n",
|
|
"\n",
|
|
"Possibilities:\n",
|
|
" * npm:pkg@1.1.1/bin\n",
|
|
" * npm:pkg@1.1.1/bin2",
|
|
)
|
|
);
|
|
|
|
// should resolve since all the values are the same
|
|
let pkg_json = build_package_json(json!({
|
|
"name": "pkg",
|
|
"version": "1.2.3",
|
|
"bin": {
|
|
"bin1": "./value",
|
|
"bin2": "./value",
|
|
}
|
|
}));
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, None,).unwrap(),
|
|
"./value"
|
|
);
|
|
|
|
// should not resolve when specified and is a string
|
|
let pkg_json = build_package_json(json!({
|
|
"name": "pkg",
|
|
"version": "1.2.3",
|
|
"bin": "./value",
|
|
}));
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, Some("path"),)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
"'/package.json' did not have a bin entry for 'path'"
|
|
);
|
|
|
|
// no version in the package.json
|
|
let pkg_json = build_package_json(json!({
|
|
"name": "pkg",
|
|
"bin": {
|
|
"bin1": "./value1",
|
|
"bin2": "./value2",
|
|
}
|
|
}));
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, None)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
concat!(
|
|
"'/package.json' did not have a bin entry for 'pkg'\n",
|
|
"\n",
|
|
"Possibilities:\n",
|
|
" * npm:pkg/bin1\n",
|
|
" * npm:pkg/bin2",
|
|
)
|
|
);
|
|
|
|
// no name or version in the package.json
|
|
let pkg_json = build_package_json(json!({
|
|
"bin": {
|
|
"bin1": "./value1",
|
|
"bin2": "./value2",
|
|
}
|
|
}));
|
|
assert_eq!(
|
|
resolve_bin_entry_value(&pkg_json, None)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
concat!(
|
|
"'/package.json' did not have a bin entry\n",
|
|
"\n",
|
|
"Possibilities:\n",
|
|
" * bin1\n",
|
|
" * bin2",
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_package_name() {
|
|
let dummy_referrer = Url::parse("http://example.com").unwrap();
|
|
|
|
assert_eq!(
|
|
parse_npm_pkg_name("fetch-blob", &dummy_referrer).unwrap(),
|
|
("fetch-blob".to_string(), ".".to_string(), false)
|
|
);
|
|
assert_eq!(
|
|
parse_npm_pkg_name("@vue/plugin-vue", &dummy_referrer).unwrap(),
|
|
("@vue/plugin-vue".to_string(), ".".to_string(), true)
|
|
);
|
|
assert_eq!(
|
|
parse_npm_pkg_name("@astrojs/prism/dist/highlighter", &dummy_referrer)
|
|
.unwrap(),
|
|
(
|
|
"@astrojs/prism".to_string(),
|
|
"./dist/highlighter".to_string(),
|
|
true
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_known_extension() {
|
|
let cases = &[
|
|
("test", "d.ts", "test.d.ts"),
|
|
("test.d.ts", "ts", "test.ts"),
|
|
("test.worker", "d.ts", "test.worker.d.ts"),
|
|
("test.d.mts", "js", "test.js"),
|
|
];
|
|
for (path, ext, expected) in cases {
|
|
let actual = with_known_extension(&PathBuf::from(path), ext);
|
|
assert_eq!(actual.to_string_lossy(), *expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_types_package_name() {
|
|
assert_eq!(types_package_name("name"), "@types/name");
|
|
assert_eq!(
|
|
types_package_name("@scoped/package"),
|
|
"@types/@scoped__package"
|
|
);
|
|
}
|
|
}
|