summaryrefslogtreecommitdiff
path: root/resolvers/node/resolution.rs
diff options
context:
space:
mode:
Diffstat (limited to 'resolvers/node/resolution.rs')
-rw-r--r--resolvers/node/resolution.rs2023
1 files changed, 2023 insertions, 0 deletions
diff --git a/resolvers/node/resolution.rs b/resolvers/node/resolution.rs
new file mode 100644
index 000000000..ad9dbb710
--- /dev/null
+++ b/resolvers/node/resolution.rs
@@ -0,0 +1,2023 @@
+// 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: &regex::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: &regex::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"
+ );
+ }
+}