summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/node/analyze.rs110
-rw-r--r--ext/node/lib.rs112
-rw-r--r--ext/node/ops/require.rs138
-rw-r--r--ext/node/package_json.rs10
-rw-r--r--ext/node/resolution.rs2099
-rw-r--r--ext/node/resolver.rs686
6 files changed, 1541 insertions, 1614 deletions
diff --git a/ext/node/analyze.rs b/ext/node/analyze.rs
index a206f4425..f1af2f611 100644
--- a/ext/node/analyze.rs
+++ b/ext/node/analyze.rs
@@ -5,6 +5,7 @@ use std::collections::VecDeque;
use std::fmt::Write;
use std::path::Path;
use std::path::PathBuf;
+use std::sync::Arc;
use deno_core::anyhow::Context;
use deno_core::ModuleSpecifier;
@@ -12,11 +13,11 @@ use once_cell::sync::Lazy;
use deno_core::error::AnyError;
-use crate::package_exports_resolve;
use crate::NodeFs;
use crate::NodeModuleKind;
use crate::NodePermissions;
use crate::NodeResolutionMode;
+use crate::NodeResolver;
use crate::NpmResolver;
use crate::PackageJson;
use crate::PathClean;
@@ -64,23 +65,26 @@ pub trait CjsEsmCodeAnalyzer {
) -> Result<HashSet<String>, AnyError>;
}
-pub struct NodeCodeTranslator<
- TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer,
- TNpmResolver: NpmResolver,
-> {
+pub struct NodeCodeTranslator<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer> {
cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer,
- npm_resolver: TNpmResolver,
+ fs: Arc<dyn NodeFs>,
+ node_resolver: Arc<NodeResolver>,
+ npm_resolver: Arc<dyn NpmResolver>,
}
-impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
- NodeCodeTranslator<TCjsEsmCodeAnalyzer, TNpmResolver>
+impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer>
+ NodeCodeTranslator<TCjsEsmCodeAnalyzer>
{
pub fn new(
cjs_esm_code_analyzer: TCjsEsmCodeAnalyzer,
- npm_resolver: TNpmResolver,
+ fs: Arc<dyn NodeFs>,
+ node_resolver: Arc<NodeResolver>,
+ npm_resolver: Arc<dyn NpmResolver>,
) -> Self {
Self {
cjs_esm_code_analyzer,
+ fs,
+ node_resolver,
npm_resolver,
}
}
@@ -105,7 +109,7 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
/// For all discovered reexports the analysis will be performed recursively.
///
/// If successful a source code for equivalent ES module is returned.
- pub fn translate_cjs_to_esm<Fs: NodeFs>(
+ pub fn translate_cjs_to_esm(
&self,
specifier: &ModuleSpecifier,
source: &str,
@@ -142,7 +146,7 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
handled_reexports.insert(reexport.to_string());
// First, resolve relate reexport specifier
- let resolved_reexport = self.resolve::<Fs>(
+ let resolved_reexport = self.resolve(
&reexport,
&referrer,
// FIXME(bartlomieju): check if these conditions are okay, probably
@@ -154,7 +158,9 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
// Second, read the source code from disk
let reexport_specifier =
ModuleSpecifier::from_file_path(&resolved_reexport).unwrap();
- let reexport_file_text = Fs::read_to_string(&resolved_reexport)
+ let reexport_file_text = self
+ .fs
+ .read_to_string(&resolved_reexport)
.with_context(|| {
format!(
"Could not find '{}' ({}) referenced from {}",
@@ -208,7 +214,7 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
Ok(translated_source)
}
- fn resolve<Fs: NodeFs>(
+ fn resolve(
&self,
specifier: &str,
referrer: &ModuleSpecifier,
@@ -223,10 +229,8 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
let referrer_path = referrer.to_file_path().unwrap();
if specifier.starts_with("./") || specifier.starts_with("../") {
if let Some(parent) = referrer_path.parent() {
- return file_extension_probe::<Fs>(
- parent.join(specifier),
- &referrer_path,
- );
+ return self
+ .file_extension_probe(parent.join(specifier), &referrer_path);
} else {
todo!();
}
@@ -245,15 +249,16 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
)?;
let package_json_path = module_dir.join("package.json");
- if Fs::exists(&package_json_path) {
- let package_json = PackageJson::load::<Fs>(
- &self.npm_resolver,
+ if self.fs.exists(&package_json_path) {
+ let package_json = PackageJson::load(
+ &*self.fs,
+ &*self.npm_resolver,
permissions,
package_json_path.clone(),
)?;
if let Some(exports) = &package_json.exports {
- return package_exports_resolve::<Fs>(
+ return self.node_resolver.package_exports_resolve(
&package_json_path,
package_subpath,
exports,
@@ -261,7 +266,6 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
NodeModuleKind::Esm,
conditions,
mode,
- &self.npm_resolver,
permissions,
);
}
@@ -269,12 +273,13 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
// old school
if package_subpath != "." {
let d = module_dir.join(package_subpath);
- if Fs::is_dir(&d) {
+ if self.fs.is_dir(&d) {
// subdir might have a package.json that specifies the entrypoint
let package_json_path = d.join("package.json");
- if Fs::exists(&package_json_path) {
- let package_json = PackageJson::load::<Fs>(
- &self.npm_resolver,
+ if self.fs.exists(&package_json_path) {
+ let package_json = PackageJson::load(
+ &*self.fs,
+ &*self.npm_resolver,
permissions,
package_json_path,
)?;
@@ -285,7 +290,7 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
return Ok(d.join("index.js").clean());
}
- return file_extension_probe::<Fs>(d, &referrer_path);
+ return self.file_extension_probe(d, &referrer_path);
} else if let Some(main) = package_json.main(NodeModuleKind::Cjs) {
return Ok(module_dir.join(main).clean());
} else {
@@ -294,6 +299,33 @@ impl<TCjsEsmCodeAnalyzer: CjsEsmCodeAnalyzer, TNpmResolver: NpmResolver>
}
Err(not_found(specifier, &referrer_path))
}
+
+ fn file_extension_probe(
+ &self,
+ p: PathBuf,
+ referrer: &Path,
+ ) -> Result<PathBuf, AnyError> {
+ let p = p.clean();
+ if self.fs.exists(&p) {
+ let file_name = p.file_name().unwrap();
+ let p_js =
+ p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
+ if self.fs.is_file(&p_js) {
+ return Ok(p_js);
+ } else if self.fs.is_dir(&p) {
+ return Ok(p.join("index.js"));
+ } else {
+ return Ok(p);
+ }
+ } else if let Some(file_name) = p.file_name() {
+ let p_js =
+ p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
+ if self.fs.is_file(&p_js) {
+ return Ok(p_js);
+ }
+ }
+ Err(not_found(&p.to_string_lossy(), referrer))
+ }
}
fn esm_code_from_top_level_decls(
@@ -455,30 +487,6 @@ fn parse_specifier(specifier: &str) -> Option<(String, String)> {
Some((package_name, package_subpath))
}
-fn file_extension_probe<Fs: NodeFs>(
- p: PathBuf,
- referrer: &Path,
-) -> Result<PathBuf, AnyError> {
- let p = p.clean();
- if Fs::exists(&p) {
- let file_name = p.file_name().unwrap();
- let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
- if Fs::is_file(&p_js) {
- return Ok(p_js);
- } else if Fs::is_dir(&p) {
- return Ok(p.join("index.js"));
- } else {
- return Ok(p);
- }
- } else if let Some(file_name) = p.file_name() {
- let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
- if Fs::is_file(&p_js) {
- return Ok(p_js);
- }
- }
- Err(not_found(&p.to_string_lossy(), referrer))
-}
-
fn not_found(path: &str, referrer: &Path) -> AnyError {
let msg = format!(
"[ERR_MODULE_NOT_FOUND] Cannot find module \"{}\" imported from \"{}\"",
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 2b2ced89c..e63c73537 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -27,7 +27,6 @@ mod package_json;
mod path;
mod polyfill;
mod resolution;
-mod resolver;
pub use package_json::PackageJson;
pub use path::PathClean;
@@ -35,22 +34,13 @@ pub use polyfill::is_builtin_node_module;
pub use polyfill::resolve_builtin_node_module;
pub use polyfill::NodeModulePolyfill;
pub use polyfill::SUPPORTED_BUILTIN_NODE_MODULES;
-pub use resolution::get_closest_package_json;
-pub use resolution::get_package_scope_config;
-pub use resolution::legacy_main_resolve;
-pub use resolution::package_exports_resolve;
-pub use resolution::package_imports_resolve;
-pub use resolution::package_resolve;
-pub use resolution::path_to_declaration_path;
pub use resolution::NodeModuleKind;
+pub use resolution::NodeResolution;
pub use resolution::NodeResolutionMode;
-pub use resolution::DEFAULT_CONDITIONS;
-pub use resolver::NodeResolution;
-pub use resolver::NodeResolver;
+pub use resolution::NodeResolver;
pub trait NodeEnv {
type P: NodePermissions;
- type Fs: NodeFs;
}
pub trait NodePermissions {
@@ -71,24 +61,26 @@ pub struct NodeFsMetadata {
pub is_dir: bool,
}
-pub trait NodeFs {
- fn current_dir() -> io::Result<PathBuf>;
- fn metadata<P: AsRef<Path>>(path: P) -> io::Result<NodeFsMetadata>;
- fn is_file<P: AsRef<Path>>(path: P) -> bool;
- fn is_dir<P: AsRef<Path>>(path: P) -> bool;
- fn exists<P: AsRef<Path>>(path: P) -> bool;
- fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>;
- fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf>;
+pub trait NodeFs: std::fmt::Debug + Send + Sync {
+ fn current_dir(&self) -> io::Result<PathBuf>;
+ fn metadata(&self, path: &Path) -> io::Result<NodeFsMetadata>;
+ fn is_file(&self, path: &Path) -> bool;
+ fn is_dir(&self, path: &Path) -> bool;
+ fn exists(&self, path: &Path) -> bool;
+ fn read_to_string(&self, path: &Path) -> io::Result<String>;
+ fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
}
+#[derive(Debug)]
pub struct RealFs;
+
impl NodeFs for RealFs {
- fn current_dir() -> io::Result<PathBuf> {
+ fn current_dir(&self) -> io::Result<PathBuf> {
#[allow(clippy::disallowed_methods)]
std::env::current_dir()
}
- fn metadata<P: AsRef<Path>>(path: P) -> io::Result<NodeFsMetadata> {
+ fn metadata(&self, path: &Path) -> io::Result<NodeFsMetadata> {
#[allow(clippy::disallowed_methods)]
std::fs::metadata(path).map(|metadata| {
// on most systems, calling is_file() and is_dir() is cheap
@@ -100,35 +92,35 @@ impl NodeFs for RealFs {
})
}
- fn exists<P: AsRef<Path>>(path: P) -> bool {
+ fn exists(&self, path: &Path) -> bool {
#[allow(clippy::disallowed_methods)]
std::fs::metadata(path).is_ok()
}
- fn is_file<P: AsRef<Path>>(path: P) -> bool {
+ fn is_file(&self, path: &Path) -> bool {
#[allow(clippy::disallowed_methods)]
std::fs::metadata(path)
.map(|m| m.is_file())
.unwrap_or(false)
}
- fn is_dir<P: AsRef<Path>>(path: P) -> bool {
+ fn is_dir(&self, path: &Path) -> bool {
#[allow(clippy::disallowed_methods)]
std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
}
- fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
+ fn read_to_string(&self, path: &Path) -> io::Result<String> {
#[allow(clippy::disallowed_methods)]
std::fs::read_to_string(path)
}
- fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
+ fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
#[allow(clippy::disallowed_methods)]
- std::path::Path::canonicalize(path.as_ref())
+ std::path::Path::canonicalize(path)
}
}
-pub trait NpmResolver {
+pub trait NpmResolver: std::fmt::Debug + Send + Sync {
/// Resolves an npm package folder path from an npm package referrer.
fn resolve_package_folder_from_package(
&self,
@@ -177,57 +169,6 @@ pub trait NpmResolver {
) -> Result<(), AnyError>;
}
-impl<T: NpmResolver + ?Sized> NpmResolver for Arc<T> {
- fn resolve_package_folder_from_package(
- &self,
- specifier: &str,
- referrer: &ModuleSpecifier,
- mode: NodeResolutionMode,
- ) -> Result<PathBuf, AnyError> {
- (**self).resolve_package_folder_from_package(specifier, referrer, mode)
- }
-
- fn resolve_package_folder_from_path(
- &self,
- path: &Path,
- ) -> Result<PathBuf, AnyError> {
- (**self).resolve_package_folder_from_path(path)
- }
-
- fn resolve_package_folder_from_deno_module(
- &self,
- pkg_nv: &NpmPackageNv,
- ) -> Result<PathBuf, AnyError> {
- (**self).resolve_package_folder_from_deno_module(pkg_nv)
- }
-
- fn resolve_pkg_id_from_pkg_req(
- &self,
- req: &NpmPackageReq,
- ) -> Result<NpmPackageId, PackageReqNotFoundError> {
- (**self).resolve_pkg_id_from_pkg_req(req)
- }
-
- fn resolve_nv_ref_from_pkg_req_ref(
- &self,
- req_ref: &NpmPackageReqReference,
- ) -> Result<NpmPackageNvReference, PackageReqNotFoundError> {
- (**self).resolve_nv_ref_from_pkg_req_ref(req_ref)
- }
-
- fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool {
- (**self).in_npm_package(specifier)
- }
-
- fn ensure_read_permission(
- &self,
- permissions: &mut dyn NodePermissions,
- path: &Path,
- ) -> Result<(), AnyError> {
- (**self).ensure_read_permission(permissions, path)
- }
-}
-
pub static NODE_GLOBAL_THIS_NAME: Lazy<String> = Lazy::new(|| {
let now = std::time::SystemTime::now();
let seconds = now
@@ -582,11 +523,18 @@ deno_core::extension!(deno_node,
"zlib.ts",
],
options = {
- maybe_npm_resolver: Option<Rc<dyn NpmResolver>>,
+ maybe_npm_resolver: Option<Arc<dyn NpmResolver>>,
+ fs: Option<Arc<dyn NodeFs>>,
},
state = |state, options| {
+ let fs = options.fs.unwrap_or_else(|| Arc::new(RealFs));
+ state.put(fs.clone());
if let Some(npm_resolver) = options.maybe_npm_resolver {
- state.put(npm_resolver);
+ state.put(npm_resolver.clone());
+ state.put(Rc::new(NodeResolver::new(
+ fs,
+ npm_resolver,
+ )))
}
},
);
diff --git a/ext/node/ops/require.rs b/ext/node/ops/require.rs
index 82a043340..513b3f589 100644
--- a/ext/node/ops/require.rs
+++ b/ext/node/ops/require.rs
@@ -13,6 +13,7 @@ use std::cell::RefCell;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
+use std::sync::Arc;
use crate::resolution;
use crate::NodeEnv;
@@ -20,6 +21,7 @@ use crate::NodeFs;
use crate::NodeModuleKind;
use crate::NodePermissions;
use crate::NodeResolutionMode;
+use crate::NodeResolver;
use crate::NpmResolver;
use crate::PackageJson;
@@ -31,7 +33,7 @@ where
P: NodePermissions + 'static,
{
let resolver = {
- let resolver = state.borrow::<Rc<dyn NpmResolver>>();
+ let resolver = state.borrow::<Arc<dyn NpmResolver>>();
resolver.clone()
};
let permissions = state.borrow_mut::<P>();
@@ -96,10 +98,11 @@ pub fn op_require_node_module_paths<Env>(
where
Env: NodeEnv + 'static,
{
+ let fs = state.borrow::<Arc<dyn NodeFs>>().clone();
// Guarantee that "from" is absolute.
let from = deno_core::resolve_path(
&from,
- &(Env::Fs::current_dir()).context("Unable to get CWD")?,
+ &(fs.current_dir()).context("Unable to get CWD")?,
)
.unwrap()
.to_file_path()
@@ -191,7 +194,7 @@ fn op_require_resolve_deno_dir(
request: String,
parent_filename: String,
) -> Option<String> {
- let resolver = state.borrow::<Rc<dyn NpmResolver>>();
+ let resolver = state.borrow::<Arc<dyn NpmResolver>>();
resolver
.resolve_package_folder_from_package(
&request,
@@ -204,7 +207,7 @@ fn op_require_resolve_deno_dir(
#[op]
fn op_require_is_deno_dir_package(state: &mut OpState, path: String) -> bool {
- let resolver = state.borrow::<Rc<dyn NpmResolver>>();
+ let resolver = state.borrow::<Arc<dyn NpmResolver>>();
resolver.in_npm_package_at_path(&PathBuf::from(path))
}
@@ -264,7 +267,8 @@ where
{
let path = PathBuf::from(path);
ensure_read_permission::<Env::P>(state, &path)?;
- if let Ok(metadata) = Env::Fs::metadata(&path) {
+ let fs = state.borrow::<Arc<dyn NodeFs>>().clone();
+ if let Ok(metadata) = fs.metadata(&path) {
if metadata.is_file {
return Ok(0);
} else {
@@ -285,7 +289,8 @@ where
{
let path = PathBuf::from(request);
ensure_read_permission::<Env::P>(state, &path)?;
- let mut canonicalized_path = Env::Fs::canonicalize(&path)?;
+ let fs = state.borrow::<Arc<dyn NodeFs>>().clone();
+ let mut canonicalized_path = fs.canonicalize(&path)?;
if cfg!(windows) {
canonicalized_path = PathBuf::from(
canonicalized_path
@@ -353,7 +358,8 @@ where
if let Some(parent_id) = maybe_parent_id {
if parent_id == "<repl>" || parent_id == "internal/preload" {
- if let Ok(cwd) = Env::Fs::current_dir() {
+ let fs = state.borrow::<Arc<dyn NodeFs>>().clone();
+ if let Ok(cwd) = fs.current_dir() {
ensure_read_permission::<Env::P>(state, &cwd)?;
return Ok(Some(cwd.to_string_lossy().to_string()));
}
@@ -375,14 +381,14 @@ where
return Ok(None);
}
- let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
+ let node_resolver = state.borrow::<Rc<NodeResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
- let pkg = resolution::get_package_scope_config::<Env::Fs>(
- &Url::from_file_path(parent_path.unwrap()).unwrap(),
- &*resolver,
- permissions,
- )
- .ok();
+ let pkg = node_resolver
+ .get_package_scope_config(
+ &Url::from_file_path(parent_path.unwrap()).unwrap(),
+ permissions,
+ )
+ .ok();
if pkg.is_none() {
return Ok(None);
}
@@ -408,18 +414,18 @@ where
let referrer = deno_core::url::Url::from_file_path(&pkg.path).unwrap();
if let Some(exports) = &pkg.exports {
- resolution::package_exports_resolve::<Env::Fs>(
- &pkg.path,
- expansion,
- exports,
- &referrer,
- NodeModuleKind::Cjs,
- resolution::REQUIRE_CONDITIONS,
- NodeResolutionMode::Execution,
- &*resolver,
- permissions,
- )
- .map(|r| Some(r.to_string_lossy().to_string()))
+ node_resolver
+ .package_exports_resolve(
+ &pkg.path,
+ expansion,
+ exports,
+ &referrer,
+ NodeModuleKind::Cjs,
+ resolution::REQUIRE_CONDITIONS,
+ NodeResolutionMode::Execution,
+ permissions,
+ )
+ .map(|r| Some(r.to_string_lossy().to_string()))
} else {
Ok(None)
}
@@ -435,7 +441,8 @@ where
{
let file_path = PathBuf::from(file_path);
ensure_read_permission::<Env::P>(state, &file_path)?;
- Ok(Env::Fs::read_to_string(file_path)?)
+ let fs = state.borrow::<Arc<dyn NodeFs>>().clone();
+ Ok(fs.read_to_string(&file_path)?)
}
#[op]
@@ -462,10 +469,12 @@ fn op_require_resolve_exports<Env>(
where
Env: NodeEnv + 'static,
{
- let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
+ let fs = state.borrow::<Arc<dyn NodeFs>>().clone();
+ let npm_resolver = state.borrow::<Arc<dyn NpmResolver>>().clone();
+ let node_resolver = state.borrow::<Rc<NodeResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
- let pkg_path = if resolver
+ let pkg_path = if npm_resolver
.in_npm_package_at_path(&PathBuf::from(&modules_path))
&& !uses_local_node_modules_dir
{
@@ -473,32 +482,31 @@ where
} else {
let orignal = modules_path.clone();
let mod_dir = path_resolve(vec![modules_path, name]);
- if Env::Fs::is_dir(&mod_dir) {
+ if fs.is_dir(Path::new(&mod_dir)) {
mod_dir
} else {
orignal
}
};
- let pkg = PackageJson::load::<Env::Fs>(
- &*resolver,
+ let pkg = node_resolver.load_package_json(
permissions,
PathBuf::from(&pkg_path).join("package.json"),
)?;
if let Some(exports) = &pkg.exports {
let referrer = Url::from_file_path(parent_path).unwrap();
- resolution::package_exports_resolve::<Env::Fs>(
- &pkg.path,
- format!(".{expansion}"),
- exports,
- &referrer,
- NodeModuleKind::Cjs,
- resolution::REQUIRE_CONDITIONS,
- NodeResolutionMode::Execution,
- &*resolver,
- permissions,
- )
- .map(|r| Some(r.to_string_lossy().to_string()))
+ node_resolver
+ .package_exports_resolve(
+ &pkg.path,
+ format!(".{expansion}"),
+ exports,
+ &referrer,
+ NodeModuleKind::Cjs,
+ resolution::REQUIRE_CONDITIONS,
+ NodeResolutionMode::Execution,
+ permissions,
+ )
+ .map(|r| Some(r.to_string_lossy().to_string()))
} else {
Ok(None)
}
@@ -516,11 +524,10 @@ where
state,
PathBuf::from(&filename).parent().unwrap(),
)?;
- let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
+ let node_resolver = state.borrow::<Rc<NodeResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
- resolution::get_closest_package_json::<Env::Fs>(
+ node_resolver.get_closest_package_json(
&Url::from_file_path(filename).unwrap(),
- &*resolver,
permissions,
)
}
@@ -533,10 +540,12 @@ fn op_require_read_package_scope<Env>(
where
Env: NodeEnv + 'static,
{
- let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
+ let node_resolver = state.borrow::<Rc<NodeResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
let package_json_path = PathBuf::from(package_json_path);
- PackageJson::load::<Env::Fs>(&*resolver, permissions, package_json_path).ok()
+ node_resolver
+ .load_package_json(permissions, package_json_path)
+ .ok()
}
#[op]
@@ -550,29 +559,24 @@ where
{
let parent_path = PathBuf::from(&parent_filename);
ensure_read_permission::<Env::P>(state, &parent_path)?;
- let resolver = state.borrow::<Rc<dyn NpmResolver>>().clone();
+ let node_resolver = state.borrow::<Rc<NodeResolver>>().clone();
let permissions = state.borrow_mut::<Env::P>();
- let pkg = PackageJson::load::<Env::Fs>(
- &*resolver,
- permissions,
- parent_path.join("package.json"),
- )?;
+ let pkg = node_resolver
+ .load_package_json(permissions, parent_path.join("package.json"))?;
if pkg.imports.is_some() {
let referrer =
deno_core::url::Url::from_file_path(&parent_filename).unwrap();
- let r = resolution::package_imports_resolve::<Env::Fs>(
- &request,
- &referrer,
- NodeModuleKind::Cjs,
- resolution::REQUIRE_CONDITIONS,
- NodeResolutionMode::Execution,
- &*resolver,
- permissions,
- )
- .map(|r| Some(Url::from_file_path(r).unwrap().to_string()));
- state.put(resolver);
- r
+ node_resolver
+ .package_imports_resolve(
+ &request,
+ &referrer,
+ NodeModuleKind::Cjs,
+ resolution::REQUIRE_CONDITIONS,
+ NodeResolutionMode::Execution,
+ permissions,
+ )
+ .map(|r| Some(Url::from_file_path(r).unwrap().to_string()))
} else {
Ok(None)
}
diff --git a/ext/node/package_json.rs b/ext/node/package_json.rs
index 08f78681a..0e34897e3 100644
--- a/ext/node/package_json.rs
+++ b/ext/node/package_json.rs
@@ -62,16 +62,18 @@ impl PackageJson {
}
}
- pub fn load<Fs: NodeFs>(
+ pub fn load(
+ fs: &dyn NodeFs,
resolver: &dyn NpmResolver,
permissions: &mut dyn NodePermissions,
path: PathBuf,
) -> Result<PackageJson, AnyError> {
resolver.ensure_read_permission(permissions, &path)?;
- Self::load_skip_read_permission::<Fs>(path)
+ Self::load_skip_read_permission(fs, path)
}
- pub fn load_skip_read_permission<Fs: NodeFs>(
+ pub fn load_skip_read_permission(
+ fs: &dyn NodeFs,
path: PathBuf,
) -> Result<PackageJson, AnyError> {
assert!(path.is_absolute());
@@ -80,7 +82,7 @@ impl PackageJson {
return Ok(CACHE.with(|cache| cache.borrow()[&path].clone()));
}
- let source = match Fs::read_to_string(&path) {
+ let source = match fs.read_to_string(&path) {
Ok(source) => source,
Err(err) if err.kind() == ErrorKind::NotFound => {
return Ok(PackageJson::empty(path));
diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs
index d324f4b4b..e5db6b3ac 100644
--- a/ext/node/resolution.rs
+++ b/ext/node/resolution.rs
@@ -2,21 +2,28 @@
use std::path::Path;
use std::path::PathBuf;
+use std::sync::Arc;
use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::serde_json::Map;
use deno_core::serde_json::Value;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
+use deno_media_type::MediaType;
+use deno_semver::npm::NpmPackageNv;
+use deno_semver::npm::NpmPackageNvReference;
+use deno_semver::npm::NpmPackageReqReference;
use crate::errors;
-use crate::package_json::PackageJson;
-use crate::path::PathClean;
+use crate::AllowAllNodePermissions;
use crate::NodeFs;
use crate::NodePermissions;
use crate::NpmResolver;
+use crate::PackageJson;
+use crate::PathClean;
pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"];
pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"];
@@ -39,53 +46,1260 @@ impl NodeResolutionMode {
}
}
-/// Checks if the resolved file has a corresponding declaration file.
-pub fn path_to_declaration_path<Fs: NodeFs>(
- path: PathBuf,
- referrer_kind: NodeModuleKind,
-) -> Option<PathBuf> {
- fn probe_extensions<Fs: NodeFs>(
- path: &Path,
+#[derive(Debug)]
+pub enum NodeResolution {
+ Esm(ModuleSpecifier),
+ CommonJs(ModuleSpecifier),
+ BuiltIn(String),
+}
+
+impl NodeResolution {
+ pub fn into_url(self) -> ModuleSpecifier {
+ match self {
+ Self::Esm(u) => u,
+ Self::CommonJs(u) => u,
+ Self::BuiltIn(specifier) => {
+ if specifier.starts_with("node:") {
+ ModuleSpecifier::parse(&specifier).unwrap()
+ } else {
+ ModuleSpecifier::parse(&format!("node:{specifier}")).unwrap()
+ }
+ }
+ }
+ }
+
+ pub fn into_specifier_and_media_type(
+ resolution: Option<Self>,
+ ) -> (ModuleSpecifier, 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 => (
+ ModuleSpecifier::parse("internal:///missing_dependency.d.ts").unwrap(),
+ MediaType::Dts,
+ ),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct NodeResolver {
+ fs: Arc<dyn NodeFs>,
+ npm_resolver: Arc<dyn NpmResolver>,
+}
+
+impl NodeResolver {
+ pub fn new(fs: Arc<dyn NodeFs>, npm_resolver: Arc<dyn NpmResolver>) -> Self {
+ Self { fs, npm_resolver }
+ }
+
+ pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> 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: &ModuleSpecifier,
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<NodeResolution>, AnyError> {
+ // 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 crate::is_builtin_node_module(specifier) {
+ return Ok(Some(NodeResolution::BuiltIn(specifier.to_string())));
+ }
+
+ if let Ok(url) = Url::parse(specifier) {
+ if url.scheme() == "data" {
+ return Ok(Some(NodeResolution::Esm(url)));
+ }
+
+ let protocol = url.scheme();
+
+ if protocol == "node" {
+ let split_specifier = url.as_str().split(':');
+ let specifier = split_specifier.skip(1).collect::<String>();
+
+ if crate::is_builtin_node_module(&specifier) {
+ return Ok(Some(NodeResolution::BuiltIn(specifier)));
+ }
+ }
+
+ if protocol != "file" && protocol != "data" {
+ return Err(errors::err_unsupported_esm_url_scheme(&url));
+ }
+
+ // todo(dsherret): this seems wrong
+ if referrer.scheme() == "data" {
+ let url = referrer.join(specifier).map_err(AnyError::from)?;
+ return Ok(Some(NodeResolution::Esm(url)));
+ }
+ }
+
+ let url = self.module_resolve(
+ specifier,
+ referrer,
+ DEFAULT_CONDITIONS,
+ mode,
+ permissions,
+ )?;
+ let url = match url {
+ Some(url) => url,
+ None => return Ok(None),
+ };
+ let url = match mode {
+ NodeResolutionMode::Execution => url,
+ NodeResolutionMode::Types => {
+ let path = url.to_file_path().unwrap();
+ // todo(16370): the module kind is not correct here. I think we need
+ // typescript to tell us if the referrer is esm or cjs
+ let path =
+ match self.path_to_declaration_path(path, NodeModuleKind::Esm) {
+ Some(path) => path,
+ None => return Ok(None),
+ };
+ ModuleSpecifier::from_file_path(path).unwrap()
+ }
+ };
+
+ let resolve_response = self.url_to_node_resolution(url)?;
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(Some(resolve_response))
+ }
+
+ fn module_resolve(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<ModuleSpecifier>, AnyError> {
+ // note: if we're here, the referrer is an esm module
+ let url = if should_be_treated_as_relative_or_absolute_path(specifier) {
+ let resolved_specifier = referrer.join(specifier)?;
+ if mode.is_types() {
+ let file_path = to_file_path(&resolved_specifier);
+ // todo(dsherret): the node module kind is not correct and we
+ // should use the value provided by typescript instead
+ let declaration_path =
+ self.path_to_declaration_path(file_path, NodeModuleKind::Esm);
+ declaration_path.map(|declaration_path| {
+ ModuleSpecifier::from_file_path(declaration_path).unwrap()
+ })
+ } else {
+ Some(resolved_specifier)
+ }
+ } else if specifier.starts_with('#') {
+ Some(
+ self
+ .package_imports_resolve(
+ specifier,
+ referrer,
+ NodeModuleKind::Esm,
+ conditions,
+ mode,
+ permissions,
+ )
+ .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?,
+ )
+ } else if let Ok(resolved) = Url::parse(specifier) {
+ Some(resolved)
+ } else {
+ self
+ .package_resolve(
+ specifier,
+ referrer,
+ NodeModuleKind::Esm,
+ conditions,
+ mode,
+ permissions,
+ )?
+ .map(|p| ModuleSpecifier::from_file_path(p).unwrap())
+ };
+ Ok(match url {
+ Some(url) => Some(self.finalize_resolution(url, referrer)?),
+ None => None,
+ })
+ }
+
+ fn finalize_resolution(
+ &self,
+ resolved: ModuleSpecifier,
+ base: &ModuleSpecifier,
+ ) -> Result<ModuleSpecifier, AnyError> {
+ let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C");
+
+ if encoded_sep_re.is_match(resolved.path()) {
+ return Err(errors::err_invalid_module_specifier(
+ resolved.path(),
+ "must not include encoded \"/\" or \"\\\\\" characters",
+ Some(to_file_path_string(base)),
+ ));
+ }
+
+ 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.fs.metadata(Path::new(&p)) {
+ (stats.is_dir, stats.is_file)
+ } else {
+ (false, false)
+ };
+ if is_dir {
+ return Err(errors::err_unsupported_dir_import(
+ resolved.as_str(),
+ base.as_str(),
+ ));
+ } else if !is_file {
+ return Err(errors::err_module_not_found(
+ resolved.as_str(),
+ base.as_str(),
+ "module",
+ ));
+ }
+
+ Ok(resolved)
+ }
+
+ pub fn resolve_npm_req_reference(
+ &self,
+ reference: &NpmPackageReqReference,
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<NodeResolution>, AnyError> {
+ let reference = self
+ .npm_resolver
+ .resolve_nv_ref_from_pkg_req_ref(reference)?;
+ self.resolve_npm_reference(&reference, mode, permissions)
+ }
+
+ pub fn resolve_npm_reference(
+ &self,
+ reference: &NpmPackageNvReference,
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<NodeResolution>, AnyError> {
+ let package_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_deno_module(&reference.nv)?;
+ let node_module_kind = NodeModuleKind::Esm;
+ let maybe_resolved_path = self
+ .package_config_resolve(
+ &reference
+ .sub_path
+ .as_ref()
+ .map(|s| format!("./{s}"))
+ .unwrap_or_else(|| ".".to_string()),
+ &package_folder,
+ node_module_kind,
+ DEFAULT_CONDITIONS,
+ mode,
+ permissions,
+ )
+ .with_context(|| {
+ format!("Error resolving package config for '{reference}'")
+ })?;
+ let resolved_path = match maybe_resolved_path {
+ Some(resolved_path) => resolved_path,
+ None => return Ok(None),
+ };
+ let resolved_path = match mode {
+ NodeResolutionMode::Execution => resolved_path,
+ NodeResolutionMode::Types => {
+ match self.path_to_declaration_path(resolved_path, node_module_kind) {
+ Some(path) => path,
+ None => return Ok(None),
+ }
+ }
+ };
+ let url = ModuleSpecifier::from_file_path(resolved_path).unwrap();
+ let resolve_response = self.url_to_node_resolution(url)?;
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(Some(resolve_response))
+ }
+
+ pub fn resolve_binary_commands(
+ &self,
+ pkg_nv: &NpmPackageNv,
+ ) -> Result<Vec<String>, AnyError> {
+ let package_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_deno_module(pkg_nv)?;
+ let package_json_path = package_folder.join("package.json");
+ let package_json = self
+ .load_package_json(&mut AllowAllNodePermissions, package_json_path)?;
+
+ Ok(match package_json.bin {
+ Some(Value::String(_)) => vec![pkg_nv.name.to_string()],
+ Some(Value::Object(o)) => {
+ o.into_iter().map(|(key, _)| key).collect::<Vec<_>>()
+ }
+ _ => Vec::new(),
+ })
+ }
+
+ pub fn resolve_binary_export(
+ &self,
+ pkg_ref: &NpmPackageReqReference,
+ ) -> Result<NodeResolution, AnyError> {
+ let pkg_nv = self
+ .npm_resolver
+ .resolve_pkg_id_from_pkg_req(&pkg_ref.req)?
+ .nv;
+ let bin_name = pkg_ref.sub_path.as_deref();
+ let package_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_deno_module(&pkg_nv)?;
+ let package_json_path = package_folder.join("package.json");
+ let package_json = self
+ .load_package_json(&mut AllowAllNodePermissions, package_json_path)?;
+ let bin = match &package_json.bin {
+ Some(bin) => bin,
+ None => bail!(
+ "package '{}' did not have a bin property in its package.json",
+ &pkg_nv.name,
+ ),
+ };
+ let bin_entry = resolve_bin_entry_value(&pkg_nv, bin_name, bin)?;
+ let url =
+ ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap();
+
+ 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: ModuleSpecifier,
+ ) -> Result<NodeResolution, AnyError> {
+ let url_str = url.as_str().to_lowercase();
+ if url_str.starts_with("http") {
+ Ok(NodeResolution::Esm(url))
+ } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") {
+ let package_config =
+ self.get_closest_package_json(&url, &mut AllowAllNodePermissions)?;
+ if package_config.typ == "module" {
+ Ok(NodeResolution::Esm(url))
+ } else {
+ Ok(NodeResolution::CommonJs(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") {
+ Err(generic_error(format!(
+ "TypeScript files are not supported in npm packages: {url}"
+ )))
+ } else {
+ Ok(NodeResolution::CommonJs(url))
+ }
+ }
+
+ fn package_config_resolve(
+ &self,
+ package_subpath: &str,
+ package_dir: &Path,
+ referrer_kind: NodeModuleKind,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<PathBuf>, AnyError> {
+ let package_json_path = package_dir.join("package.json");
+ let referrer = ModuleSpecifier::from_directory_path(package_dir).unwrap();
+ let package_config =
+ self.load_package_json(permissions, package_json_path.clone())?;
+ if let Some(exports) = &package_config.exports {
+ let result = self.package_exports_resolve(
+ &package_json_path,
+ package_subpath.to_string(),
+ exports,
+ &referrer,
+ referrer_kind,
+ conditions,
+ mode,
+ permissions,
+ );
+ match result {
+ Ok(found) => return Ok(Some(found)),
+ Err(exports_err) => {
+ if mode.is_types() && package_subpath == "." {
+ if let Ok(Some(path)) =
+ self.legacy_main_resolve(&package_config, referrer_kind, mode)
+ {
+ return Ok(Some(path));
+ } else {
+ return Ok(None);
+ }
+ }
+ return Err(exports_err);
+ }
+ }
+ }
+ if package_subpath == "." {
+ return self.legacy_main_resolve(&package_config, referrer_kind, mode);
+ }
+
+ Ok(Some(package_dir.join(package_subpath)))
+ }
+
+ /// Checks if the resolved file has a corresponding declaration file.
+ pub(super) fn path_to_declaration_path(
+ &self,
+ path: PathBuf,
referrer_kind: NodeModuleKind,
) -> Option<PathBuf> {
- let specific_dts_path = match referrer_kind {
- NodeModuleKind::Cjs => with_known_extension(path, "d.cts"),
- NodeModuleKind::Esm => with_known_extension(path, "d.mts"),
+ fn probe_extensions(
+ fs: &dyn NodeFs,
+ path: &Path,
+ referrer_kind: NodeModuleKind,
+ ) -> Option<PathBuf> {
+ let specific_dts_path = match referrer_kind {
+ NodeModuleKind::Cjs => with_known_extension(path, "d.cts"),
+ NodeModuleKind::Esm => with_known_extension(path, "d.mts"),
+ };
+ if fs.exists(&specific_dts_path) {
+ return Some(specific_dts_path);
+ }
+ let dts_path = with_known_extension(path, "d.ts");
+ if fs.exists(&dts_path) {
+ Some(dts_path)
+ } else {
+ 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.ts")
+ {
+ return Some(path);
+ }
+ if let Some(path) = probe_extensions(&*self.fs, &path, referrer_kind) {
+ return Some(path);
+ }
+ if self.fs.is_dir(&path) {
+ if let Some(path) =
+ probe_extensions(&*self.fs, &path.join("index"), referrer_kind)
+ {
+ return Some(path);
+ }
+ }
+ None
+ }
+
+ pub(super) fn package_imports_resolve(
+ &self,
+ name: &str,
+ referrer: &ModuleSpecifier,
+ referrer_kind: NodeModuleKind,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<PathBuf, AnyError> {
+ if name == "#" || name.starts_with("#/") || name.ends_with('/') {
+ let reason = "is not a valid internal imports specifier name";
+ return Err(errors::err_invalid_module_specifier(
+ name,
+ reason,
+ Some(to_specifier_display_string(referrer)),
+ ));
+ }
+
+ let package_config =
+ self.get_package_scope_config(referrer, permissions)?;
+ let mut package_json_path = None;
+ if package_config.exists {
+ package_json_path = Some(package_config.path.clone());
+ if let Some(imports) = &package_config.imports {
+ if imports.contains_key(name) && !name.contains('*') {
+ let maybe_resolved = self.resolve_package_target(
+ package_json_path.as_ref().unwrap(),
+ imports.get(name).unwrap().to_owned(),
+ "".to_string(),
+ name.to_string(),
+ referrer,
+ referrer_kind,
+ false,
+ true,
+ conditions,
+ mode,
+ permissions,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ }
+ } else {
+ let mut best_match = "";
+ let mut best_match_subpath = None;
+ for key in imports.keys() {
+ let pattern_index = key.find('*');
+ if let Some(pattern_index) = pattern_index {
+ let key_sub = &key[0..=pattern_index];
+ if name.starts_with(key_sub) {
+ let pattern_trailer = &key[pattern_index + 1..];
+ if name.len() > key.len()
+ && name.ends_with(&pattern_trailer)
+ && pattern_key_compare(best_match, key) == 1
+ && key.rfind('*') == Some(pattern_index)
+ {
+ best_match = key;
+ best_match_subpath = Some(
+ name[pattern_index..=(name.len() - pattern_trailer.len())]
+ .to_string(),
+ );
+ }
+ }
+ }
+ }
+
+ if !best_match.is_empty() {
+ let target = imports.get(best_match).unwrap().to_owned();
+ let maybe_resolved = self.resolve_package_target(
+ package_json_path.as_ref().unwrap(),
+ target,
+ best_match_subpath.unwrap(),
+ best_match.to_string(),
+ referrer,
+ referrer_kind,
+ true,
+ true,
+ conditions,
+ mode,
+ permissions,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ }
+ }
+ }
+ }
+ }
+
+ Err(throw_import_not_defined(
+ name,
+ package_json_path.as_deref(),
+ referrer,
+ ))
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn resolve_package_target_string(
+ &self,
+ target: String,
+ subpath: String,
+ match_: String,
+ package_json_path: &Path,
+ referrer: &ModuleSpecifier,
+ referrer_kind: NodeModuleKind,
+ pattern: bool,
+ internal: bool,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<PathBuf, AnyError> {
+ if !subpath.is_empty() && !pattern && !target.ends_with('/') {
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ package_json_path,
+ internal,
+ referrer,
+ ));
+ }
+ let invalid_segment_re =
+ 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 is_url = Url::parse(&target).is_ok();
+ if !is_url {
+ let export_target = if pattern {
+ pattern_re
+ .replace(&target, |_caps: &regex::Captures| subpath.clone())
+ .to_string()
+ } else {
+ format!("{target}{subpath}")
+ };
+ let package_json_url =
+ ModuleSpecifier::from_file_path(package_json_path).unwrap();
+ return match self.package_resolve(
+ &export_target,
+ &package_json_url,
+ referrer_kind,
+ conditions,
+ mode,
+ permissions,
+ ) {
+ Ok(Some(path)) => Ok(path),
+ Ok(None) => Err(generic_error("not found")),
+ Err(err) => Err(err),
+ };
+ }
+ }
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ package_json_path,
+ internal,
+ referrer,
+ ));
+ }
+ if invalid_segment_re.is_match(&target[2..]) {
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ package_json_path,
+ internal,
+ referrer,
+ ));
+ }
+ let package_path = package_json_path.parent().unwrap();
+ let resolved_path = package_path.join(&target).clean();
+ if !resolved_path.starts_with(package_path) {
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ package_json_path,
+ internal,
+ referrer,
+ ));
+ }
+ if subpath.is_empty() {
+ return Ok(resolved_path);
+ }
+ if invalid_segment_re.is_match(&subpath) {
+ let request = if pattern {
+ match_.replace('*', &subpath)
+ } else {
+ format!("{match_}{subpath}")
+ };
+ return Err(throw_invalid_subpath(
+ request,
+ package_json_path,
+ internal,
+ referrer,
+ ));
+ }
+ if pattern {
+ let resolved_path_str = resolved_path.to_string_lossy();
+ let replaced = pattern_re
+ .replace(&resolved_path_str, |_caps: &regex::Captures| {
+ subpath.clone()
+ });
+ return Ok(PathBuf::from(replaced.to_string()));
+ }
+ Ok(resolved_path.join(&subpath).clean())
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn resolve_package_target(
+ &self,
+ package_json_path: &Path,
+ target: Value,
+ subpath: String,
+ package_subpath: String,
+ referrer: &ModuleSpecifier,
+ referrer_kind: NodeModuleKind,
+ pattern: bool,
+ internal: bool,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<PathBuf>, AnyError> {
+ if let Some(target) = target.as_str() {
+ return self
+ .resolve_package_target_string(
+ target.to_string(),
+ subpath,
+ package_subpath,
+ package_json_path,
+ referrer,
+ referrer_kind,
+ pattern,
+ internal,
+ conditions,
+ mode,
+ permissions,
+ )
+ .map(|path| {
+ if mode.is_types() {
+ self.path_to_declaration_path(path, referrer_kind)
+ } else {
+ Some(path)
+ }
+ });
+ } 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.to_owned(),
+ subpath.clone(),
+ package_subpath.clone(),
+ referrer,
+ referrer_kind,
+ pattern,
+ internal,
+ conditions,
+ mode,
+ permissions,
+ );
+
+ match resolved_result {
+ Ok(Some(resolved)) => return Ok(Some(resolved)),
+ Ok(None) => {
+ last_error = None;
+ continue;
+ }
+ Err(e) => {
+ let err_string = e.to_string();
+ last_error = Some(e);
+ if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") {
+ continue;
+ }
+ return Err(last_error.unwrap());
+ }
+ }
+ }
+ 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().to_owned();
+
+ let resolved = self.resolve_package_target(
+ package_json_path,
+ condition_target,
+ subpath.clone(),
+ package_subpath.clone(),
+ referrer,
+ referrer_kind,
+ pattern,
+ internal,
+ conditions,
+ mode,
+ permissions,
+ )?;
+ match resolved {
+ Some(resolved) => return Ok(Some(resolved)),
+ None => {
+ continue;
+ }
+ }
+ }
+ }
+ } else if target.is_null() {
+ return Ok(None);
+ }
+
+ Err(throw_invalid_package_target(
+ package_subpath,
+ target.to_string(),
+ package_json_path,
+ internal,
+ referrer,
+ ))
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn package_exports_resolve(
+ &self,
+ package_json_path: &Path,
+ package_subpath: String,
+ package_exports: &Map<String, Value>,
+ referrer: &ModuleSpecifier,
+ referrer_kind: NodeModuleKind,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<PathBuf, AnyError> {
+ if package_exports.contains_key(&package_subpath)
+ && package_subpath.find('*').is_none()
+ && !package_subpath.ends_with('/')
+ {
+ let target = package_exports.get(&package_subpath).unwrap().to_owned();
+ let resolved = self.resolve_package_target(
+ package_json_path,
+ target,
+ "".to_string(),
+ package_subpath.to_string(),
+ referrer,
+ referrer_kind,
+ false,
+ false,
+ conditions,
+ mode,
+ permissions,
+ )?;
+ if resolved.is_none() {
+ return Err(throw_exports_not_found(
+ package_subpath,
+ package_json_path,
+ referrer,
+ ));
+ }
+ return Ok(resolved.unwrap());
+ }
+
+ let mut best_match = "";
+ let mut best_match_subpath = None;
+ for key in package_exports.keys() {
+ let pattern_index = key.find('*');
+ if let Some(pattern_index) = pattern_index {
+ let key_sub = &key[0..pattern_index];
+ if package_subpath.starts_with(key_sub) {
+ // When this reaches EOL, this can throw at the top of the whole function:
+ //
+ // if (StringPrototypeEndsWith(packageSubpath, '/'))
+ // throwInvalidSubpath(packageSubpath)
+ //
+ // To match "imports" and the spec.
+ if package_subpath.ends_with('/') {
+ // TODO(bartlomieju):
+ // emitTrailingSlashPatternDeprecation();
+ }
+ let pattern_trailer = &key[pattern_index + 1..];
+ if package_subpath.len() > key.len()
+ && package_subpath.ends_with(&pattern_trailer)
+ && pattern_key_compare(best_match, key) == 1
+ && key.rfind('*') == Some(pattern_index)
+ {
+ best_match = key;
+ best_match_subpath = Some(
+ package_subpath[pattern_index
+ ..(package_subpath.len() - pattern_trailer.len())]
+ .to_string(),
+ );
+ }
+ }
+ }
+ }
+
+ if !best_match.is_empty() {
+ let target = package_exports.get(best_match).unwrap().to_owned();
+ let maybe_resolved = self.resolve_package_target(
+ package_json_path,
+ target,
+ best_match_subpath.unwrap(),
+ best_match.to_string(),
+ referrer,
+ referrer_kind,
+ true,
+ false,
+ conditions,
+ mode,
+ permissions,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ } else {
+ return Err(throw_exports_not_found(
+ package_subpath,
+ package_json_path,
+ referrer,
+ ));
+ }
+ }
+
+ Err(throw_exports_not_found(
+ package_subpath,
+ package_json_path,
+ referrer,
+ ))
+ }
+
+ pub(super) fn package_resolve(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ referrer_kind: NodeModuleKind,
+ conditions: &[&str],
+ mode: NodeResolutionMode,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<Option<PathBuf>, AnyError> {
+ let (package_name, package_subpath, _is_scoped) =
+ parse_package_name(specifier, referrer)?;
+
+ // ResolveSelf
+ let package_config =
+ self.get_package_scope_config(referrer, permissions)?;
+ if package_config.exists
+ && 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,
+ referrer,
+ referrer_kind,
+ conditions,
+ mode,
+ permissions,
+ )
+ .map(Some);
+ }
+ }
+
+ let package_dir_path = self
+ .npm_resolver
+ .resolve_package_folder_from_package(&package_name, referrer, mode)?;
+ let package_json_path = package_dir_path.join("package.json");
+
+ // todo: error with this instead when can't find package
+ // Err(errors::err_module_not_found(
+ // &package_json_url
+ // .join(".")
+ // .unwrap()
+ // .to_file_path()
+ // .unwrap()
+ // .display()
+ // .to_string(),
+ // &to_file_path_string(referrer),
+ // "package",
+ // ))
+
+ // Package match.
+ let package_json =
+ self.load_package_json(permissions, package_json_path)?;
+ if let Some(exports) = &package_json.exports {
+ return self
+ .package_exports_resolve(
+ &package_json.path,
+ package_subpath,
+ exports,
+ referrer,
+ referrer_kind,
+ conditions,
+ mode,
+ permissions,
+ )
+ .map(Some);
+ }
+ if package_subpath == "." {
+ return self.legacy_main_resolve(&package_json, referrer_kind, mode);
+ }
+
+ let file_path = package_json.path.parent().unwrap().join(&package_subpath);
+
+ if mode.is_types() {
+ let maybe_declaration_path =
+ self.path_to_declaration_path(file_path, referrer_kind);
+ Ok(maybe_declaration_path)
+ } else {
+ Ok(Some(file_path))
+ }
+ }
+
+ pub(super) fn get_package_scope_config(
+ &self,
+ referrer: &ModuleSpecifier,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<PackageJson, AnyError> {
+ let root_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?;
+ let package_json_path = root_folder.join("package.json");
+ self.load_package_json(permissions, package_json_path)
+ }
+
+ pub(super) fn get_closest_package_json(
+ &self,
+ url: &ModuleSpecifier,
+ permissions: &mut dyn NodePermissions,
+ ) -> Result<PackageJson, AnyError> {
+ let package_json_path = self.get_closest_package_json_path(url)?;
+ self.load_package_json(permissions, package_json_path)
+ }
+
+ fn get_closest_package_json_path(
+ &self,
+ url: &ModuleSpecifier,
+ ) -> Result<PathBuf, AnyError> {
+ let file_path = url.to_file_path().unwrap();
+ let mut current_dir = file_path.parent().unwrap();
+ let package_json_path = current_dir.join("package.json");
+ if self.fs.exists(&package_json_path) {
+ return Ok(package_json_path);
+ }
+ let root_pkg_folder = self
+ .npm_resolver
+ .resolve_package_folder_from_path(&url.to_file_path().unwrap())?;
+ while current_dir.starts_with(&root_pkg_folder) {
+ current_dir = current_dir.parent().unwrap();
+ let package_json_path = current_dir.join("package.json");
+ if self.fs.exists(&package_json_path) {
+ return Ok(package_json_path);
+ }
+ }
+
+ bail!("did not find package.json in {}", root_pkg_folder.display())
+ }
+
+ pub(super) fn load_package_json(
+ &self,
+ permissions: &mut dyn NodePermissions,
+ package_json_path: PathBuf,
+ ) -> Result<PackageJson, AnyError> {
+ PackageJson::load(
+ &*self.fs,
+ &*self.npm_resolver,
+ permissions,
+ package_json_path,
+ )
+ }
+
+ pub(super) fn legacy_main_resolve(
+ &self,
+ package_json: &PackageJson,
+ referrer_kind: NodeModuleKind,
+ mode: NodeResolutionMode,
+ ) -> Result<Option<PathBuf>, AnyError> {
+ let maybe_main = if mode.is_types() {
+ match package_json.types.as_ref() {
+ Some(types) => Some(types),
+ 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();
+ if let Some(path) =
+ self.path_to_declaration_path(main, referrer_kind)
+ {
+ return Ok(Some(path));
+ }
+ }
+ None
+ }
+ }
+ } else {
+ package_json.main(referrer_kind)
};
- if Fs::exists(&specific_dts_path) {
- return Some(specific_dts_path);
+
+ if let Some(main) = maybe_main {
+ let guess = package_json.path.parent().unwrap().join(main).clean();
+ if self.fs.is_file(&guess) {
+ return Ok(Some(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.fs.is_file(&guess) {
+ // TODO(bartlomieju): emitLegacyIndexDeprecation()
+ return Ok(Some(guess));
+ }
+ }
}
- let dts_path = with_known_extension(path, "d.ts");
- if Fs::exists(&dts_path) {
- Some(dts_path)
+
+ 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 {
- None
+ vec!["index.js"]
+ };
+ for index_file_name in index_file_names {
+ let guess = package_json
+ .path
+ .parent()
+ .unwrap()
+ .join(index_file_name)
+ .clean();
+ if self.fs.is_file(&guess) {
+ // TODO(bartlomieju): emitLegacyIndexDeprecation()
+ return Ok(Some(guess));
+ }
+ }
+
+ Ok(None)
+ }
+}
+
+fn resolve_bin_entry_value<'a>(
+ pkg_nv: &NpmPackageNv,
+ bin_name: Option<&str>,
+ bin: &'a Value,
+) -> Result<&'a str, AnyError> {
+ let bin_entry = match bin {
+ Value::String(_) => {
+ if bin_name.is_some() && bin_name.unwrap() != pkg_nv.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 {
+ o.get(&pkg_nv.name)
+ }
+ },
+ _ => bail!("package '{}' did not have a bin property with a string or object value in its package.json", pkg_nv),
+ };
+ let bin_entry = match bin_entry {
+ Some(e) => e,
+ None => {
+ let keys = bin
+ .as_object()
+ .map(|o| {
+ o.keys()
+ .map(|k| format!(" * npm:{pkg_nv}/{k}"))
+ .collect::<Vec<_>>()
+ })
+ .unwrap_or_default();
+ bail!(
+ "package '{}' did not have a bin entry for '{}' in its package.json{}",
+ pkg_nv,
+ bin_name.unwrap_or(&pkg_nv.name),
+ if keys.is_empty() {
+ "".to_string()
+ } else {
+ format!("\n\nPossibilities:\n{}", keys.join("\n"))
+ }
+ )
+ }
+ };
+ match bin_entry {
+ Value::String(s) => Ok(s),
+ _ => bail!(
+ "package '{}' had a non-string sub property of bin in its package.json",
+ pkg_nv,
+ ),
}
+}
- 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.ts")
- {
- return Some(path);
+fn to_file_path(url: &ModuleSpecifier) -> PathBuf {
+ url
+ .to_file_path()
+ .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}"))
+}
+
+fn to_file_path_string(url: &ModuleSpecifier) -> 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 let Some(path) = probe_extensions::<Fs>(&path, referrer_kind) {
- return Some(path);
+
+ if specifier.starts_with('/') {
+ return true;
}
- if Fs::is_dir(&path) {
- if let Some(path) =
- probe_extensions::<Fs>(&path.join("index"), referrer_kind)
+
+ 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().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 Some(path);
+ return true;
}
}
- None
+ false
}
/// Alternate `PathBuf::with_extension` that will handle known extensions
/// more intelligently.
-pub fn with_known_extension(path: &Path, ext: &str) -> PathBuf {
+fn with_known_extension(path: &Path, ext: &str) -> PathBuf {
const NON_DECL_EXTS: &[&str] = &["cjs", "js", "json", "jsx", "mjs", "tsx"];
const DECL_EXTS: &[&str] = &["cts", "mts", "ts"];
@@ -142,145 +1356,6 @@ fn throw_import_not_defined(
)
}
-fn pattern_key_compare(a: &str, b: &str) -> i32 {
- let a_pattern_index = a.find('*');
- let b_pattern_index = b.find('*');
-
- let base_len_a = if let Some(index) = a_pattern_index {
- index + 1
- } else {
- a.len()
- };
- let base_len_b = if let Some(index) = b_pattern_index {
- index + 1
- } else {
- b.len()
- };
-
- if base_len_a > base_len_b {
- return -1;
- }
-
- if base_len_b > base_len_a {
- return 1;
- }
-
- if a_pattern_index.is_none() {
- return 1;
- }
-
- if b_pattern_index.is_none() {
- return -1;
- }
-
- if a.len() > b.len() {
- return -1;
- }
-
- if b.len() > a.len() {
- return 1;
- }
-
- 0
-}
-
-pub fn package_imports_resolve<Fs: NodeFs>(
- name: &str,
- referrer: &ModuleSpecifier,
- referrer_kind: NodeModuleKind,
- conditions: &[&str],
- mode: NodeResolutionMode,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<PathBuf, AnyError> {
- if name == "#" || name.starts_with("#/") || name.ends_with('/') {
- let reason = "is not a valid internal imports specifier name";
- return Err(errors::err_invalid_module_specifier(
- name,
- reason,
- Some(to_specifier_display_string(referrer)),
- ));
- }
-
- let package_config =
- get_package_scope_config::<Fs>(referrer, npm_resolver, permissions)?;
- let mut package_json_path = None;
- if package_config.exists {
- package_json_path = Some(package_config.path.clone());
- if let Some(imports) = &package_config.imports {
- if imports.contains_key(name) && !name.contains('*') {
- let maybe_resolved = resolve_package_target::<Fs>(
- package_json_path.as_ref().unwrap(),
- imports.get(name).unwrap().to_owned(),
- "".to_string(),
- name.to_string(),
- referrer,
- referrer_kind,
- false,
- true,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )?;
- if let Some(resolved) = maybe_resolved {
- return Ok(resolved);
- }
- } else {
- let mut best_match = "";
- let mut best_match_subpath = None;
- for key in imports.keys() {
- let pattern_index = key.find('*');
- if let Some(pattern_index) = pattern_index {
- let key_sub = &key[0..=pattern_index];
- if name.starts_with(key_sub) {
- let pattern_trailer = &key[pattern_index + 1..];
- if name.len() > key.len()
- && name.ends_with(&pattern_trailer)
- && pattern_key_compare(best_match, key) == 1
- && key.rfind('*') == Some(pattern_index)
- {
- best_match = key;
- best_match_subpath = Some(
- name[pattern_index..=(name.len() - pattern_trailer.len())]
- .to_string(),
- );
- }
- }
- }
- }
-
- if !best_match.is_empty() {
- let target = imports.get(best_match).unwrap().to_owned();
- let maybe_resolved = resolve_package_target::<Fs>(
- package_json_path.as_ref().unwrap(),
- target,
- best_match_subpath.unwrap(),
- best_match.to_string(),
- referrer,
- referrer_kind,
- true,
- true,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )?;
- if let Some(resolved) = maybe_resolved {
- return Ok(resolved);
- }
- }
- }
- }
- }
-
- Err(throw_import_not_defined(
- name,
- package_json_path.as_deref(),
- referrer,
- ))
-}
-
fn throw_invalid_package_target(
subpath: String,
target: String,
@@ -316,245 +1391,6 @@ fn throw_invalid_subpath(
)
}
-#[allow(clippy::too_many_arguments)]
-fn resolve_package_target_string<Fs: NodeFs>(
- target: String,
- subpath: String,
- match_: String,
- package_json_path: &Path,
- referrer: &ModuleSpecifier,
- referrer_kind: NodeModuleKind,
- pattern: bool,
- internal: bool,
- conditions: &[&str],
- mode: NodeResolutionMode,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<PathBuf, AnyError> {
- if !subpath.is_empty() && !pattern && !target.ends_with('/') {
- return Err(throw_invalid_package_target(
- match_,
- target,
- package_json_path,
- internal,
- referrer,
- ));
- }
- let invalid_segment_re =
- 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 is_url = Url::parse(&target).is_ok();
- if !is_url {
- let export_target = if pattern {
- pattern_re
- .replace(&target, |_caps: &regex::Captures| subpath.clone())
- .to_string()
- } else {
- format!("{target}{subpath}")
- };
- let package_json_url =
- ModuleSpecifier::from_file_path(package_json_path).unwrap();
- return match package_resolve::<Fs>(
- &export_target,
- &package_json_url,
- referrer_kind,
- conditions,
- mode,
- npm_resolver,
- permissions,
- ) {
- Ok(Some(path)) => Ok(path),
- Ok(None) => Err(generic_error("not found")),
- Err(err) => Err(err),
- };
- }
- }
- return Err(throw_invalid_package_target(
- match_,
- target,
- package_json_path,
- internal,
- referrer,
- ));
- }
- if invalid_segment_re.is_match(&target[2..]) {
- return Err(throw_invalid_package_target(
- match_,
- target,
- package_json_path,
- internal,
- referrer,
- ));
- }
- let package_path = package_json_path.parent().unwrap();
- let resolved_path = package_path.join(&target).clean();
- if !resolved_path.starts_with(package_path) {
- return Err(throw_invalid_package_target(
- match_,
- target,
- package_json_path,
- internal,
- referrer,
- ));
- }
- if subpath.is_empty() {
- return Ok(resolved_path);
- }
- if invalid_segment_re.is_match(&subpath) {
- let request = if pattern {
- match_.replace('*', &subpath)
- } else {
- format!("{match_}{subpath}")
- };
- return Err(throw_invalid_subpath(
- request,
- package_json_path,
- internal,
- referrer,
- ));
- }
- if pattern {
- let resolved_path_str = resolved_path.to_string_lossy();
- let replaced = pattern_re
- .replace(&resolved_path_str, |_caps: &regex::Captures| {
- subpath.clone()
- });
- return Ok(PathBuf::from(replaced.to_string()));
- }
- Ok(resolved_path.join(&subpath).clean())
-}
-
-#[allow(clippy::too_many_arguments)]
-fn resolve_package_target<Fs: NodeFs>(
- package_json_path: &Path,
- target: Value,
- subpath: String,
- package_subpath: String,
- referrer: &ModuleSpecifier,
- referrer_kind: NodeModuleKind,
- pattern: bool,
- internal: bool,
- conditions: &[&str],
- mode: NodeResolutionMode,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<Option<PathBuf>, AnyError> {
- if let Some(target) = target.as_str() {
- return resolve_package_target_string::<Fs>(
- target.to_string(),
- subpath,
- package_subpath,
- package_json_path,
- referrer,
- referrer_kind,
- pattern,
- internal,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )
- .map(|path| {
- if mode.is_types() {
- path_to_declaration_path::<Fs>(path, referrer_kind)
- } else {
- Some(path)
- }
- });
- } else if let Some(target_arr) = target.as_array() {
- if target_arr.is_empty() {
- return Ok(None);
- }
-
- let mut last_error = None;
- for target_item in target_arr {
- let resolved_result = resolve_package_target::<Fs>(
- package_json_path,
- target_item.to_owned(),
- subpath.clone(),
- package_subpath.clone(),
- referrer,
- referrer_kind,
- pattern,
- internal,
- conditions,
- mode,
- npm_resolver,
- permissions,
- );
-
- match resolved_result {
- Ok(Some(resolved)) => return Ok(Some(resolved)),
- Ok(None) => {
- last_error = None;
- continue;
- }
- Err(e) => {
- let err_string = e.to_string();
- last_error = Some(e);
- if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") {
- continue;
- }
- return Err(last_error.unwrap());
- }
- }
- }
- 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().to_owned();
-
- let resolved = resolve_package_target::<Fs>(
- package_json_path,
- condition_target,
- subpath.clone(),
- package_subpath.clone(),
- referrer,
- referrer_kind,
- pattern,
- internal,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )?;
- match resolved {
- Some(resolved) => return Ok(Some(resolved)),
- None => {
- continue;
- }
- }
- }
- }
- } else if target.is_null() {
- return Ok(None);
- }
-
- Err(throw_invalid_package_target(
- package_subpath,
- target.to_string(),
- package_json_path,
- internal,
- referrer,
- ))
-}
-
fn throw_exports_not_found(
subpath: String,
package_json_path: &Path,
@@ -567,115 +1403,6 @@ fn throw_exports_not_found(
)
}
-#[allow(clippy::too_many_arguments)]
-pub fn package_exports_resolve<Fs: NodeFs>(
- package_json_path: &Path,
- package_subpath: String,
- package_exports: &Map<String, Value>,
- referrer: &ModuleSpecifier,
- referrer_kind: NodeModuleKind,
- conditions: &[&str],
- mode: NodeResolutionMode,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<PathBuf, AnyError> {
- if package_exports.contains_key(&package_subpath)
- && package_subpath.find('*').is_none()
- && !package_subpath.ends_with('/')
- {
- let target = package_exports.get(&package_subpath).unwrap().to_owned();
- let resolved = resolve_package_target::<Fs>(
- package_json_path,
- target,
- "".to_string(),
- package_subpath.to_string(),
- referrer,
- referrer_kind,
- false,
- false,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )?;
- if resolved.is_none() {
- return Err(throw_exports_not_found(
- package_subpath,
- package_json_path,
- referrer,
- ));
- }
- return Ok(resolved.unwrap());
- }
-
- let mut best_match = "";
- let mut best_match_subpath = None;
- for key in package_exports.keys() {
- let pattern_index = key.find('*');
- if let Some(pattern_index) = pattern_index {
- let key_sub = &key[0..pattern_index];
- if package_subpath.starts_with(key_sub) {
- // When this reaches EOL, this can throw at the top of the whole function:
- //
- // if (StringPrototypeEndsWith(packageSubpath, '/'))
- // throwInvalidSubpath(packageSubpath)
- //
- // To match "imports" and the spec.
- if package_subpath.ends_with('/') {
- // TODO(bartlomieju):
- // emitTrailingSlashPatternDeprecation();
- }
- let pattern_trailer = &key[pattern_index + 1..];
- if package_subpath.len() > key.len()
- && package_subpath.ends_with(&pattern_trailer)
- && pattern_key_compare(best_match, key) == 1
- && key.rfind('*') == Some(pattern_index)
- {
- best_match = key;
- best_match_subpath = Some(
- package_subpath
- [pattern_index..(package_subpath.len() - pattern_trailer.len())]
- .to_string(),
- );
- }
- }
- }
- }
-
- if !best_match.is_empty() {
- let target = package_exports.get(best_match).unwrap().to_owned();
- let maybe_resolved = resolve_package_target::<Fs>(
- package_json_path,
- target,
- best_match_subpath.unwrap(),
- best_match.to_string(),
- referrer,
- referrer_kind,
- true,
- false,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )?;
- if let Some(resolved) = maybe_resolved {
- return Ok(resolved);
- } else {
- return Err(throw_exports_not_found(
- package_subpath,
- package_json_path,
- referrer,
- ));
- }
- }
-
- Err(throw_exports_not_found(
- package_subpath,
- package_json_path,
- referrer,
- ))
-}
-
fn parse_package_name(
specifier: &str,
referrer: &ModuleSpecifier,
@@ -727,230 +1454,154 @@ fn parse_package_name(
Ok((package_name, package_subpath, is_scoped))
}
-pub fn package_resolve<Fs: NodeFs>(
- specifier: &str,
- referrer: &ModuleSpecifier,
- referrer_kind: NodeModuleKind,
- conditions: &[&str],
- mode: NodeResolutionMode,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<Option<PathBuf>, AnyError> {
- let (package_name, package_subpath, _is_scoped) =
- parse_package_name(specifier, referrer)?;
-
- // ResolveSelf
- let package_config =
- get_package_scope_config::<Fs>(referrer, npm_resolver, permissions)?;
- if package_config.exists
- && package_config.name.as_ref() == Some(&package_name)
- {
- if let Some(exports) = &package_config.exports {
- return package_exports_resolve::<Fs>(
- &package_config.path,
- package_subpath,
- exports,
- referrer,
- referrer_kind,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )
- .map(Some);
- }
- }
-
- let package_dir_path = npm_resolver.resolve_package_folder_from_package(
- &package_name,
- referrer,
- mode,
- )?;
- let package_json_path = package_dir_path.join("package.json");
-
- // todo: error with this instead when can't find package
- // Err(errors::err_module_not_found(
- // &package_json_url
- // .join(".")
- // .unwrap()
- // .to_file_path()
- // .unwrap()
- // .display()
- // .to_string(),
- // &to_file_path_string(referrer),
- // "package",
- // ))
-
- // Package match.
- let package_json =
- PackageJson::load::<Fs>(npm_resolver, permissions, package_json_path)?;
- if let Some(exports) = &package_json.exports {
- return package_exports_resolve::<Fs>(
- &package_json.path,
- package_subpath,
- exports,
- referrer,
- referrer_kind,
- conditions,
- mode,
- npm_resolver,
- permissions,
- )
- .map(Some);
- }
- if package_subpath == "." {
- return legacy_main_resolve::<Fs>(&package_json, referrer_kind, mode);
- }
-
- let file_path = package_json.path.parent().unwrap().join(&package_subpath);
+fn pattern_key_compare(a: &str, b: &str) -> i32 {
+ let a_pattern_index = a.find('*');
+ let b_pattern_index = b.find('*');
- if mode.is_types() {
- let maybe_declaration_path =
- path_to_declaration_path::<Fs>(file_path, referrer_kind);
- Ok(maybe_declaration_path)
+ let base_len_a = if let Some(index) = a_pattern_index {
+ index + 1
} else {
- Ok(Some(file_path))
- }
-}
-
-pub fn get_package_scope_config<Fs: NodeFs>(
- referrer: &ModuleSpecifier,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<PackageJson, AnyError> {
- let root_folder = npm_resolver
- .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?;
- let package_json_path = root_folder.join("package.json");
- PackageJson::load::<Fs>(npm_resolver, permissions, package_json_path)
-}
-
-pub fn get_closest_package_json<Fs: NodeFs>(
- url: &ModuleSpecifier,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<PackageJson, AnyError> {
- let package_json_path =
- get_closest_package_json_path::<Fs>(url, npm_resolver)?;
- PackageJson::load::<Fs>(npm_resolver, permissions, package_json_path)
-}
+ a.len()
+ };
+ let base_len_b = if let Some(index) = b_pattern_index {
+ index + 1
+ } else {
+ b.len()
+ };
-fn get_closest_package_json_path<Fs: NodeFs>(
- url: &ModuleSpecifier,
- npm_resolver: &dyn NpmResolver,
-) -> Result<PathBuf, AnyError> {
- let file_path = url.to_file_path().unwrap();
- let mut current_dir = file_path.parent().unwrap();
- let package_json_path = current_dir.join("package.json");
- if Fs::exists(&package_json_path) {
- return Ok(package_json_path);
- }
- let root_pkg_folder = npm_resolver
- .resolve_package_folder_from_path(&url.to_file_path().unwrap())?;
- while current_dir.starts_with(&root_pkg_folder) {
- current_dir = current_dir.parent().unwrap();
- let package_json_path = current_dir.join("package.json");
- if Fs::exists(&package_json_path) {
- return Ok(package_json_path);
- }
+ if base_len_a > base_len_b {
+ return -1;
}
- bail!("did not find package.json in {}", root_pkg_folder.display())
-}
+ if base_len_b > base_len_a {
+ return 1;
+ }
-pub fn legacy_main_resolve<Fs: NodeFs>(
- package_json: &PackageJson,
- referrer_kind: NodeModuleKind,
- mode: NodeResolutionMode,
-) -> Result<Option<PathBuf>, AnyError> {
- let maybe_main = if mode.is_types() {
- match package_json.types.as_ref() {
- Some(types) => Some(types),
- 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();
- if let Some(path) =
- path_to_declaration_path::<Fs>(main, referrer_kind)
- {
- return Ok(Some(path));
- }
- }
- None
- }
- }
- } else {
- package_json.main(referrer_kind)
- };
+ if a_pattern_index.is_none() {
+ return 1;
+ }
- if let Some(main) = maybe_main {
- let guess = package_json.path.parent().unwrap().join(main).clean();
- if Fs::is_file(&guess) {
- return Ok(Some(guess));
- }
+ if b_pattern_index.is_none() {
+ return -1;
+ }
- // 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 Fs::is_file(&guess) {
- // TODO(bartlomieju): emitLegacyIndexDeprecation()
- return Ok(Some(guess));
- }
- }
+ if a.len() > b.len() {
+ return -1;
}
- 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 = package_json
- .path
- .parent()
- .unwrap()
- .join(index_file_name)
- .clean();
- if Fs::is_file(&guess) {
- // TODO(bartlomieju): emitLegacyIndexDeprecation()
- return Ok(Some(guess));
- }
+ if b.len() > a.len() {
+ return 1;
}
- Ok(None)
+ 0
}
#[cfg(test)]
mod tests {
+ use deno_core::serde_json::json;
+
use super::*;
#[test]
+ fn test_resolve_bin_entry_value() {
+ // should resolve the specified value
+ let value = json!({
+ "bin1": "./value1",
+ "bin2": "./value2",
+ "test": "./value3",
+ });
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.1.1").unwrap(),
+ Some("bin1"),
+ &value
+ )
+ .unwrap(),
+ "./value1"
+ );
+
+ // should resolve the value with the same name when not specified
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.1.1").unwrap(),
+ None,
+ &value
+ )
+ .unwrap(),
+ "./value3"
+ );
+
+ // should not resolve when specified value does not exist
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.1.1").unwrap(),
+ Some("other"),
+ &value
+ )
+ .err()
+ .unwrap()
+ .to_string(),
+ concat!(
+ "package 'test@1.1.1' did not have a bin entry for 'other' in its package.json\n",
+ "\n",
+ "Possibilities:\n",
+ " * npm:test@1.1.1/bin1\n",
+ " * npm:test@1.1.1/bin2\n",
+ " * npm:test@1.1.1/test"
+ )
+ );
+
+ // should not resolve when default value can't be determined
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("asdf@1.2.3").unwrap(),
+ None,
+ &value
+ )
+ .err()
+ .unwrap()
+ .to_string(),
+ concat!(
+ "package 'asdf@1.2.3' did not have a bin entry for 'asdf' in its package.json\n",
+ "\n",
+ "Possibilities:\n",
+ " * npm:asdf@1.2.3/bin1\n",
+ " * npm:asdf@1.2.3/bin2\n",
+ " * npm:asdf@1.2.3/test"
+ )
+ );
+
+ // should resolve since all the values are the same
+ let value = json!({
+ "bin1": "./value",
+ "bin2": "./value",
+ });
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.2.3").unwrap(),
+ None,
+ &value
+ )
+ .unwrap(),
+ "./value"
+ );
+
+ // should not resolve when specified and is a string
+ let value = json!("./value");
+ assert_eq!(
+ resolve_bin_entry_value(
+ &NpmPackageNv::from_str("test@1.2.3").unwrap(),
+ Some("path"),
+ &value
+ )
+ .err()
+ .unwrap()
+ .to_string(),
+ "package 'test@1.2.3' did not have a bin entry for 'path' in its package.json"
+ );
+ }
+
+ #[test]
fn test_parse_package_name() {
let dummy_referrer = Url::parse("http://example.com").unwrap();
diff --git a/ext/node/resolver.rs b/ext/node/resolver.rs
deleted file mode 100644
index 41e1cf4d4..000000000
--- a/ext/node/resolver.rs
+++ /dev/null
@@ -1,686 +0,0 @@
-// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-
-use std::path::Path;
-use std::path::PathBuf;
-
-use deno_core::anyhow::bail;
-use deno_core::anyhow::Context;
-use deno_core::error::generic_error;
-use deno_core::error::AnyError;
-use deno_core::serde_json::Value;
-use deno_core::url::Url;
-use deno_core::ModuleSpecifier;
-use deno_media_type::MediaType;
-use deno_semver::npm::NpmPackageNv;
-use deno_semver::npm::NpmPackageNvReference;
-use deno_semver::npm::NpmPackageReqReference;
-
-use crate::errors;
-use crate::get_closest_package_json;
-use crate::legacy_main_resolve;
-use crate::package_exports_resolve;
-use crate::package_imports_resolve;
-use crate::package_resolve;
-use crate::path_to_declaration_path;
-use crate::AllowAllNodePermissions;
-use crate::NodeFs;
-use crate::NodeModuleKind;
-use crate::NodePermissions;
-use crate::NodeResolutionMode;
-use crate::NpmResolver;
-use crate::PackageJson;
-use crate::DEFAULT_CONDITIONS;
-
-#[derive(Debug)]
-pub enum NodeResolution {
- Esm(ModuleSpecifier),
- CommonJs(ModuleSpecifier),
- BuiltIn(String),
-}
-
-impl NodeResolution {
- pub fn into_url(self) -> ModuleSpecifier {
- match self {
- Self::Esm(u) => u,
- Self::CommonJs(u) => u,
- Self::BuiltIn(specifier) => {
- if specifier.starts_with("node:") {
- ModuleSpecifier::parse(&specifier).unwrap()
- } else {
- ModuleSpecifier::parse(&format!("node:{specifier}")).unwrap()
- }
- }
- }
- }
-
- pub fn into_specifier_and_media_type(
- resolution: Option<Self>,
- ) -> (ModuleSpecifier, 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 => (
- ModuleSpecifier::parse("internal:///missing_dependency.d.ts").unwrap(),
- MediaType::Dts,
- ),
- }
- }
-}
-
-#[derive(Debug)]
-pub struct NodeResolver<TRequireNpmResolver: NpmResolver> {
- npm_resolver: TRequireNpmResolver,
-}
-
-impl<TRequireNpmResolver: NpmResolver> NodeResolver<TRequireNpmResolver> {
- pub fn new(require_npm_resolver: TRequireNpmResolver) -> Self {
- Self {
- npm_resolver: require_npm_resolver,
- }
- }
-
- pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> 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<Fs: NodeFs>(
- &self,
- specifier: &str,
- referrer: &ModuleSpecifier,
- mode: NodeResolutionMode,
- permissions: &mut dyn NodePermissions,
- ) -> Result<Option<NodeResolution>, AnyError> {
- // 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 crate::is_builtin_node_module(specifier) {
- return Ok(Some(NodeResolution::BuiltIn(specifier.to_string())));
- }
-
- if let Ok(url) = Url::parse(specifier) {
- if url.scheme() == "data" {
- return Ok(Some(NodeResolution::Esm(url)));
- }
-
- let protocol = url.scheme();
-
- if protocol == "node" {
- let split_specifier = url.as_str().split(':');
- let specifier = split_specifier.skip(1).collect::<String>();
-
- if crate::is_builtin_node_module(&specifier) {
- return Ok(Some(NodeResolution::BuiltIn(specifier)));
- }
- }
-
- if protocol != "file" && protocol != "data" {
- return Err(errors::err_unsupported_esm_url_scheme(&url));
- }
-
- // todo(dsherret): this seems wrong
- if referrer.scheme() == "data" {
- let url = referrer.join(specifier).map_err(AnyError::from)?;
- return Ok(Some(NodeResolution::Esm(url)));
- }
- }
-
- let url = self.module_resolve::<Fs>(
- specifier,
- referrer,
- DEFAULT_CONDITIONS,
- mode,
- permissions,
- )?;
- let url = match url {
- Some(url) => url,
- None => return Ok(None),
- };
- let url = match mode {
- NodeResolutionMode::Execution => url,
- NodeResolutionMode::Types => {
- let path = url.to_file_path().unwrap();
- // todo(16370): the module kind is not correct here. I think we need
- // typescript to tell us if the referrer is esm or cjs
- let path =
- match path_to_declaration_path::<Fs>(path, NodeModuleKind::Esm) {
- Some(path) => path,
- None => return Ok(None),
- };
- ModuleSpecifier::from_file_path(path).unwrap()
- }
- };
-
- let resolve_response = self.url_to_node_resolution::<Fs>(url)?;
- // TODO(bartlomieju): skipped checking errors for commonJS resolution and
- // "preserveSymlinksMain"/"preserveSymlinks" options.
- Ok(Some(resolve_response))
- }
-
- fn module_resolve<Fs: NodeFs>(
- &self,
- specifier: &str,
- referrer: &ModuleSpecifier,
- conditions: &[&str],
- mode: NodeResolutionMode,
- permissions: &mut dyn NodePermissions,
- ) -> Result<Option<ModuleSpecifier>, AnyError> {
- // note: if we're here, the referrer is an esm module
- let url = if should_be_treated_as_relative_or_absolute_path(specifier) {
- let resolved_specifier = referrer.join(specifier)?;
- if mode.is_types() {
- let file_path = to_file_path(&resolved_specifier);
- // todo(dsherret): the node module kind is not correct and we
- // should use the value provided by typescript instead
- let declaration_path =
- path_to_declaration_path::<Fs>(file_path, NodeModuleKind::Esm);
- declaration_path.map(|declaration_path| {
- ModuleSpecifier::from_file_path(declaration_path).unwrap()
- })
- } else {
- Some(resolved_specifier)
- }
- } else if specifier.starts_with('#') {
- Some(
- package_imports_resolve::<Fs>(
- specifier,
- referrer,
- NodeModuleKind::Esm,
- conditions,
- mode,
- &self.npm_resolver,
- permissions,
- )
- .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?,
- )
- } else if let Ok(resolved) = Url::parse(specifier) {
- Some(resolved)
- } else {
- package_resolve::<Fs>(
- specifier,
- referrer,
- NodeModuleKind::Esm,
- conditions,
- mode,
- &self.npm_resolver,
- permissions,
- )?
- .map(|p| ModuleSpecifier::from_file_path(p).unwrap())
- };
- Ok(match url {
- Some(url) => Some(finalize_resolution::<Fs>(url, referrer)?),
- None => None,
- })
- }
-
- pub fn resolve_npm_req_reference<Fs: NodeFs>(
- &self,
- reference: &NpmPackageReqReference,
- mode: NodeResolutionMode,
- permissions: &mut dyn NodePermissions,
- ) -> Result<Option<NodeResolution>, AnyError> {
- let reference = self
- .npm_resolver
- .resolve_nv_ref_from_pkg_req_ref(reference)?;
- self.resolve_npm_reference::<Fs>(&reference, mode, permissions)
- }
-
- pub fn resolve_npm_reference<Fs: NodeFs>(
- &self,
- reference: &NpmPackageNvReference,
- mode: NodeResolutionMode,
- permissions: &mut dyn NodePermissions,
- ) -> Result<Option<NodeResolution>, AnyError> {
- let package_folder = self
- .npm_resolver
- .resolve_package_folder_from_deno_module(&reference.nv)?;
- let node_module_kind = NodeModuleKind::Esm;
- let maybe_resolved_path = package_config_resolve::<Fs>(
- &reference
- .sub_path
- .as_ref()
- .map(|s| format!("./{s}"))
- .unwrap_or_else(|| ".".to_string()),
- &package_folder,
- node_module_kind,
- DEFAULT_CONDITIONS,
- mode,
- &self.npm_resolver,
- permissions,
- )
- .with_context(|| {
- format!("Error resolving package config for '{reference}'")
- })?;
- let resolved_path = match maybe_resolved_path {
- Some(resolved_path) => resolved_path,
- None => return Ok(None),
- };
- let resolved_path = match mode {
- NodeResolutionMode::Execution => resolved_path,
- NodeResolutionMode::Types => {
- match path_to_declaration_path::<Fs>(resolved_path, node_module_kind) {
- Some(path) => path,
- None => return Ok(None),
- }
- }
- };
- let url = ModuleSpecifier::from_file_path(resolved_path).unwrap();
- let resolve_response = self.url_to_node_resolution::<Fs>(url)?;
- // TODO(bartlomieju): skipped checking errors for commonJS resolution and
- // "preserveSymlinksMain"/"preserveSymlinks" options.
- Ok(Some(resolve_response))
- }
-
- pub fn resolve_binary_commands<Fs: NodeFs>(
- &self,
- pkg_nv: &NpmPackageNv,
- ) -> Result<Vec<String>, AnyError> {
- let package_folder = self
- .npm_resolver
- .resolve_package_folder_from_deno_module(pkg_nv)?;
- let package_json_path = package_folder.join("package.json");
- let package_json = PackageJson::load::<Fs>(
- &self.npm_resolver,
- &mut AllowAllNodePermissions,
- package_json_path,
- )?;
-
- Ok(match package_json.bin {
- Some(Value::String(_)) => vec![pkg_nv.name.to_string()],
- Some(Value::Object(o)) => {
- o.into_iter().map(|(key, _)| key).collect::<Vec<_>>()
- }
- _ => Vec::new(),
- })
- }
-
- pub fn resolve_binary_export<Fs: NodeFs>(
- &self,
- pkg_ref: &NpmPackageReqReference,
- ) -> Result<NodeResolution, AnyError> {
- let pkg_nv = self
- .npm_resolver
- .resolve_pkg_id_from_pkg_req(&pkg_ref.req)?
- .nv;
- let bin_name = pkg_ref.sub_path.as_deref();
- let package_folder = self
- .npm_resolver
- .resolve_package_folder_from_deno_module(&pkg_nv)?;
- let package_json_path = package_folder.join("package.json");
- let package_json = PackageJson::load::<Fs>(
- &self.npm_resolver,
- &mut AllowAllNodePermissions,
- package_json_path,
- )?;
- let bin = match &package_json.bin {
- Some(bin) => bin,
- None => bail!(
- "package '{}' did not have a bin property in its package.json",
- &pkg_nv.name,
- ),
- };
- let bin_entry = resolve_bin_entry_value(&pkg_nv, bin_name, bin)?;
- let url =
- ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap();
-
- let resolve_response = self.url_to_node_resolution::<Fs>(url)?;
- // TODO(bartlomieju): skipped checking errors for commonJS resolution and
- // "preserveSymlinksMain"/"preserveSymlinks" options.
- Ok(resolve_response)
- }
-
- pub fn url_to_node_resolution<Fs: NodeFs>(
- &self,
- url: ModuleSpecifier,
- ) -> Result<NodeResolution, AnyError> {
- let url_str = url.as_str().to_lowercase();
- if url_str.starts_with("http") {
- Ok(NodeResolution::Esm(url))
- } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") {
- let package_config = get_closest_package_json::<Fs>(
- &url,
- &self.npm_resolver,
- &mut AllowAllNodePermissions,
- )?;
- if package_config.typ == "module" {
- Ok(NodeResolution::Esm(url))
- } else {
- Ok(NodeResolution::CommonJs(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") {
- Err(generic_error(format!(
- "TypeScript files are not supported in npm packages: {url}"
- )))
- } else {
- Ok(NodeResolution::CommonJs(url))
- }
- }
-}
-
-fn resolve_bin_entry_value<'a>(
- pkg_nv: &NpmPackageNv,
- bin_name: Option<&str>,
- bin: &'a Value,
-) -> Result<&'a str, AnyError> {
- let bin_entry = match bin {
- Value::String(_) => {
- if bin_name.is_some() && bin_name.unwrap() != pkg_nv.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 {
- o.get(&pkg_nv.name)
- }
- },
- _ => bail!("package '{}' did not have a bin property with a string or object value in its package.json", pkg_nv),
- };
- let bin_entry = match bin_entry {
- Some(e) => e,
- None => {
- let keys = bin
- .as_object()
- .map(|o| {
- o.keys()
- .map(|k| format!(" * npm:{pkg_nv}/{k}"))
- .collect::<Vec<_>>()
- })
- .unwrap_or_default();
- bail!(
- "package '{}' did not have a bin entry for '{}' in its package.json{}",
- pkg_nv,
- bin_name.unwrap_or(&pkg_nv.name),
- if keys.is_empty() {
- "".to_string()
- } else {
- format!("\n\nPossibilities:\n{}", keys.join("\n"))
- }
- )
- }
- };
- match bin_entry {
- Value::String(s) => Ok(s),
- _ => bail!(
- "package '{}' had a non-string sub property of bin in its package.json",
- pkg_nv,
- ),
- }
-}
-
-fn package_config_resolve<Fs: NodeFs>(
- package_subpath: &str,
- package_dir: &Path,
- referrer_kind: NodeModuleKind,
- conditions: &[&str],
- mode: NodeResolutionMode,
- npm_resolver: &dyn NpmResolver,
- permissions: &mut dyn NodePermissions,
-) -> Result<Option<PathBuf>, AnyError> {
- let package_json_path = package_dir.join("package.json");
- let referrer = ModuleSpecifier::from_directory_path(package_dir).unwrap();
- let package_config = PackageJson::load::<Fs>(
- npm_resolver,
- permissions,
- package_json_path.clone(),
- )?;
- if let Some(exports) = &package_config.exports {
- let result = package_exports_resolve::<Fs>(
- &package_json_path,
- package_subpath.to_string(),
- exports,
- &referrer,
- referrer_kind,
- conditions,
- mode,
- npm_resolver,
- permissions,
- );
- match result {
- Ok(found) => return Ok(Some(found)),
- Err(exports_err) => {
- if mode.is_types() && package_subpath == "." {
- if let Ok(Some(path)) =
- legacy_main_resolve::<Fs>(&package_config, referrer_kind, mode)
- {
- return Ok(Some(path));
- } else {
- return Ok(None);
- }
- }
- return Err(exports_err);
- }
- }
- }
- if package_subpath == "." {
- return legacy_main_resolve::<Fs>(&package_config, referrer_kind, mode);
- }
-
- Ok(Some(package_dir.join(package_subpath)))
-}
-
-fn finalize_resolution<Fs: NodeFs>(
- resolved: ModuleSpecifier,
- base: &ModuleSpecifier,
-) -> Result<ModuleSpecifier, AnyError> {
- let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C");
-
- if encoded_sep_re.is_match(resolved.path()) {
- return Err(errors::err_invalid_module_specifier(
- resolved.path(),
- "must not include encoded \"/\" or \"\\\\\" characters",
- Some(to_file_path_string(base)),
- ));
- }
-
- 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) = Fs::metadata(p) {
- (stats.is_dir, stats.is_file)
- } else {
- (false, false)
- };
- if is_dir {
- return Err(errors::err_unsupported_dir_import(
- resolved.as_str(),
- base.as_str(),
- ));
- } else if !is_file {
- return Err(errors::err_module_not_found(
- resolved.as_str(),
- base.as_str(),
- "module",
- ));
- }
-
- Ok(resolved)
-}
-
-fn to_file_path(url: &ModuleSpecifier) -> PathBuf {
- url
- .to_file_path()
- .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}"))
-}
-
-fn to_file_path_string(url: &ModuleSpecifier) -> 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().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
-}
-
-#[cfg(test)]
-mod tests {
- use deno_core::serde_json::json;
-
- use super::*;
-
- #[test]
- fn test_resolve_bin_entry_value() {
- // should resolve the specified value
- let value = json!({
- "bin1": "./value1",
- "bin2": "./value2",
- "test": "./value3",
- });
- assert_eq!(
- resolve_bin_entry_value(
- &NpmPackageNv::from_str("test@1.1.1").unwrap(),
- Some("bin1"),
- &value
- )
- .unwrap(),
- "./value1"
- );
-
- // should resolve the value with the same name when not specified
- assert_eq!(
- resolve_bin_entry_value(
- &NpmPackageNv::from_str("test@1.1.1").unwrap(),
- None,
- &value
- )
- .unwrap(),
- "./value3"
- );
-
- // should not resolve when specified value does not exist
- assert_eq!(
- resolve_bin_entry_value(
- &NpmPackageNv::from_str("test@1.1.1").unwrap(),
- Some("other"),
- &value
- )
- .err()
- .unwrap()
- .to_string(),
- concat!(
- "package 'test@1.1.1' did not have a bin entry for 'other' in its package.json\n",
- "\n",
- "Possibilities:\n",
- " * npm:test@1.1.1/bin1\n",
- " * npm:test@1.1.1/bin2\n",
- " * npm:test@1.1.1/test"
- )
- );
-
- // should not resolve when default value can't be determined
- assert_eq!(
- resolve_bin_entry_value(
- &NpmPackageNv::from_str("asdf@1.2.3").unwrap(),
- None,
- &value
- )
- .err()
- .unwrap()
- .to_string(),
- concat!(
- "package 'asdf@1.2.3' did not have a bin entry for 'asdf' in its package.json\n",
- "\n",
- "Possibilities:\n",
- " * npm:asdf@1.2.3/bin1\n",
- " * npm:asdf@1.2.3/bin2\n",
- " * npm:asdf@1.2.3/test"
- )
- );
-
- // should resolve since all the values are the same
- let value = json!({
- "bin1": "./value",
- "bin2": "./value",
- });
- assert_eq!(
- resolve_bin_entry_value(
- &NpmPackageNv::from_str("test@1.2.3").unwrap(),
- None,
- &value
- )
- .unwrap(),
- "./value"
- );
-
- // should not resolve when specified and is a string
- let value = json!("./value");
- assert_eq!(
- resolve_bin_entry_value(
- &NpmPackageNv::from_str("test@1.2.3").unwrap(),
- Some("path"),
- &value
- )
- .err()
- .unwrap()
- .to_string(),
- "package 'test@1.2.3' did not have a bin entry for 'path' in its package.json"
- );
- }
-}