diff --git a/Cargo.lock b/Cargo.lock index 447fea548d..fd7fbb1038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1141,6 +1141,7 @@ name = "deno_node" version = "0.3.0" dependencies = [ "deno_core", + "path-clean", "regex", "serde", ] diff --git a/cli/node/mod.rs b/cli/node/mod.rs index 71046b4b74..66b0f32f1b 100644 --- a/cli/node/mod.rs +++ b/cli/node/mod.rs @@ -17,11 +17,13 @@ use deno_core::serde_json::Value; use deno_core::url::Url; use deno_core::JsRuntime; use deno_graph::source::ResolveResponse; +use deno_runtime::deno_node::get_closest_package_json; use deno_runtime::deno_node::legacy_main_resolve; use deno_runtime::deno_node::package_exports_resolve; use deno_runtime::deno_node::package_imports_resolve; use deno_runtime::deno_node::package_resolve; use deno_runtime::deno_node::DenoDirNpmResolver; +use deno_runtime::deno_node::NodeModuleKind; use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_node::DEFAULT_CONDITIONS; use once_cell::sync::Lazy; @@ -342,6 +344,8 @@ pub fn node_resolve( referrer: &ModuleSpecifier, npm_resolver: &dyn DenoDirNpmResolver, ) -> Result, 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 // NOTE(bartlomieju): this will force `ProcState` to use Node.js polyfill for @@ -385,7 +389,7 @@ pub fn node_resolve( return Err(errors::err_unsupported_esm_url_scheme(&url)); } - // todo(THIS PR): I think this is handled upstream so can be removed? + // todo(dsherret): this seems wrong if referrer.scheme() == "data" { let url = referrer.join(specifier).map_err(AnyError::from)?; return Ok(Some(ResolveResponse::Specifier(url))); @@ -412,7 +416,7 @@ pub fn node_resolve_npm_reference( let package_folder = npm_resolver .resolve_package_from_deno_module(&reference.req)? .folder_path; - let maybe_url = package_config_resolve( + let resolved_path = package_config_resolve( &reference .sub_path .as_ref() @@ -420,16 +424,13 @@ pub fn node_resolve_npm_reference( .unwrap_or_else(|| ".".to_string()), &package_folder, npm_resolver, + NodeModuleKind::Esm, ) - .map(Some) .with_context(|| { format!("Error resolving package config for '{}'.", reference) })?; - let url = match maybe_url { - Some(url) => url, - None => return Ok(None), - }; + let url = ModuleSpecifier::from_file_path(resolved_path).unwrap(); let resolve_response = url_to_resolve_response(url, npm_resolver)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. @@ -521,33 +522,30 @@ fn package_config_resolve( package_subpath: &str, package_dir: &Path, npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { + referrer_kind: NodeModuleKind, +) -> Result { let package_json_path = package_dir.join("package.json"); - // todo(dsherret): remove base from this code - let base = + let referrer = ModuleSpecifier::from_directory_path(package_json_path.parent().unwrap()) .unwrap(); let package_config = PackageJson::load(npm_resolver, package_json_path.clone())?; - let package_json_url = - ModuleSpecifier::from_file_path(&package_json_path).unwrap(); if let Some(exports) = &package_config.exports { return package_exports_resolve( - package_json_url, + &package_json_path, package_subpath.to_string(), exports, - &base, + &referrer, + referrer_kind, DEFAULT_CONDITIONS, npm_resolver, ); } if package_subpath == "." { - return legacy_main_resolve(&package_json_url, &package_config, &base); + return legacy_main_resolve(&package_config, referrer_kind); } - package_json_url - .join(package_subpath) - .map_err(AnyError::from) + Ok(package_dir.join(package_subpath)) } fn url_to_resolve_response( @@ -570,37 +568,6 @@ fn url_to_resolve_response( }) } -fn get_closest_package_json( - url: &ModuleSpecifier, - npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { - let package_json_path = get_closest_package_json_path(url, npm_resolver)?; - PackageJson::load(npm_resolver, package_json_path) -} - -fn get_closest_package_json_path( - url: &ModuleSpecifier, - npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { - let file_path = url.to_file_path().unwrap(); - let mut current_dir = file_path.parent().unwrap(); - let package_json_path = current_dir.join("package.json"); - if package_json_path.exists() { - return Ok(package_json_path); - } - let root_folder = npm_resolver - .resolve_package_folder_from_path(&url.to_file_path().unwrap())?; - while current_dir.starts_with(&root_folder) { - current_dir = current_dir.parent().unwrap(); - let package_json_path = current_dir.join("./package.json"); - if package_json_path.exists() { - return Ok(package_json_path); - } - } - - bail!("did not find package.json in {}", root_folder.display()) -} - fn finalize_resolution( resolved: ModuleSpecifier, base: &ModuleSpecifier, @@ -667,25 +634,34 @@ fn module_resolve( conditions: &[&str], npm_resolver: &dyn DenoDirNpmResolver, ) -> Result, 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)?; Some(resolved_specifier) } else if specifier.starts_with('#') { - Some(package_imports_resolve( - specifier, - referrer, - conditions, - npm_resolver, - )?) + Some( + package_imports_resolve( + specifier, + referrer, + NodeModuleKind::Esm, + conditions, + npm_resolver, + ) + .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?, + ) } else if let Ok(resolved) = Url::parse(specifier) { Some(resolved) } else { - Some(package_resolve( - specifier, - referrer, - conditions, - npm_resolver, - )?) + Some( + package_resolve( + specifier, + referrer, + NodeModuleKind::Esm, + conditions, + npm_resolver, + ) + .map(|p| ModuleSpecifier::from_file_path(p).unwrap())?, + ) }; Ok(match url { Some(url) => Some(finalize_resolution(url, referrer)?), diff --git a/cli/tests/integration/npm_tests.rs b/cli/tests/integration/npm_tests.rs index 059fac99bf..6d04954541 100644 --- a/cli/tests/integration/npm_tests.rs +++ b/cli/tests/integration/npm_tests.rs @@ -75,6 +75,13 @@ itest!(conditional_exports { http_server: true, }); +itest!(dual_cjs_esm { + args: "run --unstable -A --quiet npm/dual_cjs_esm/main.ts", + output: "npm/dual_cjs_esm/main.out", + envs: env_vars(), + http_server: true, +}); + itest!(dynamic_import { args: "run --allow-read --allow-env --unstable npm/dynamic_import/main.ts", output: "npm/dynamic_import/main.out", diff --git a/cli/tests/testdata/npm/dual_cjs_esm/main.out b/cli/tests/testdata/npm/dual_cjs_esm/main.out new file mode 100644 index 0000000000..3d329be7a4 --- /dev/null +++ b/cli/tests/testdata/npm/dual_cjs_esm/main.out @@ -0,0 +1 @@ +esm diff --git a/cli/tests/testdata/npm/dual_cjs_esm/main.ts b/cli/tests/testdata/npm/dual_cjs_esm/main.ts new file mode 100644 index 0000000000..3e80402c6d --- /dev/null +++ b/cli/tests/testdata/npm/dual_cjs_esm/main.ts @@ -0,0 +1,3 @@ +import { getKind } from "npm:@denotest/dual-cjs-esm"; + +console.log(getKind()); diff --git a/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/index.cjs b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/index.cjs new file mode 100644 index 0000000000..9906055273 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/index.cjs @@ -0,0 +1,3 @@ +exports.getKind = function() { + return "cjs"; +}; diff --git a/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/index.mjs b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/index.mjs new file mode 100644 index 0000000000..b48b9a3a63 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/index.mjs @@ -0,0 +1,3 @@ +export function getKind() { + return "esm"; +} diff --git a/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/package.json b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/package.json new file mode 100644 index 0000000000..e0315b7f67 --- /dev/null +++ b/cli/tests/testdata/npm/registry/@denotest/dual-cjs-esm/1.0.0/package.json @@ -0,0 +1,7 @@ +{ + "name": "@denotest/dual-cjs-esm", + "version": "1.0.0", + "type": "module", + "main": "./index.cjs", + "module": "./index.mjs" +} diff --git a/ext/node/02_require.js b/ext/node/02_require.js index 99587472a9..4f2a998b22 100644 --- a/ext/node/02_require.js +++ b/ext/node/02_require.js @@ -390,8 +390,8 @@ return false; }; - Module._nodeModulePaths = function (from) { - return ops.op_require_node_module_paths(from); + Module._nodeModulePaths = function (fromPath) { + return ops.op_require_node_module_paths(fromPath); }; Module._resolveLookupPaths = function (request, parent) { @@ -728,7 +728,7 @@ const content = ops.op_require_read_file(filename); if (StringPrototypeEndsWith(filename, ".js")) { - const pkg = core.ops.op_require_read_package_scope(filename); + const pkg = core.ops.op_require_read_closest_package_json(filename); if (pkg && pkg.exists && pkg.typ == "module") { let message = `Trying to import ESM module: ${filename}`; diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 2b72309d84..2391fbed0c 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -15,5 +15,6 @@ path = "lib.rs" [dependencies] deno_core = { version = "0.148.0", path = "../../core" } +path-clean = "=0.1.0" regex = "1" serde = "1.0.136" diff --git a/ext/node/errors.rs b/ext/node/errors.rs index 8d1822f7ba..9dc6c7e7e6 100644 --- a/ext/node/errors.rs +++ b/ext/node/errors.rs @@ -56,7 +56,7 @@ pub fn err_invalid_package_target( key: String, target: String, is_import: bool, - maybe_base: Option, + maybe_referrer: Option, ) -> AnyError { let rel_error = !is_import && !target.is_empty() && !target.starts_with("./"); let mut msg = "[ERR_INVALID_PACKAGE_TARGET]".to_string(); @@ -69,7 +69,7 @@ pub fn err_invalid_package_target( msg = format!("{} Invalid \"{}\" target {} defined for '{}' in the package config {}package.json", msg, ie, target, key, pkg_path) }; - if let Some(base) = maybe_base { + if let Some(base) = maybe_referrer { msg = format!("{} imported from {}", msg, base); }; if rel_error { @@ -82,7 +82,7 @@ pub fn err_invalid_package_target( pub fn err_package_path_not_exported( pkg_path: String, subpath: String, - maybe_base: Option, + maybe_referrer: Option, ) -> AnyError { let mut msg = "[ERR_PACKAGE_PATH_NOT_EXPORTED]".to_string(); @@ -95,8 +95,8 @@ pub fn err_package_path_not_exported( msg = format!("{} Package subpath \'{}\' is not defined by \"exports\" in {}package.json", msg, subpath, pkg_path); }; - if let Some(base) = maybe_base { - msg = format!("{} imported from {}", msg, base); + if let Some(referrer) = maybe_referrer { + msg = format!("{} imported from {}", msg, referrer); } generic_error(msg) diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f96259797f..6f31734706 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -12,11 +12,13 @@ use std::path::PathBuf; use std::rc::Rc; pub use package_json::PackageJson; +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::NodeModuleKind; pub use resolution::DEFAULT_CONDITIONS; pub trait NodePermissions { @@ -77,6 +79,7 @@ pub fn init( op_require_read_file::decl::

(), op_require_as_file_path::decl(), op_require_resolve_exports::decl(), + op_require_read_closest_package_json::decl::

(), op_require_read_package_scope::decl(), op_require_package_imports_resolve::decl::

(), ]) @@ -485,17 +488,18 @@ fn op_require_try_self( return Ok(None); } - let base = deno_core::url::Url::from_file_path(PathBuf::from("/")).unwrap(); + let referrer = deno_core::url::Url::from_file_path(&pkg.path).unwrap(); if let Some(exports) = &pkg.exports { resolution::package_exports_resolve( - deno_core::url::Url::from_file_path(&pkg.path).unwrap(), + &pkg.path, expansion, exports, - &base, + &referrer, + NodeModuleKind::Cjs, resolution::REQUIRE_CONDITIONS, &*resolver, ) - .map(|r| Some(r.as_str().to_string())) + .map(|r| Some(r.to_string_lossy().to_string())) } else { Ok(None) } @@ -550,21 +554,42 @@ fn op_require_resolve_exports( )?; if let Some(exports) = &pkg.exports { - let base = Url::from_file_path(parent_path).unwrap(); + let referrer = Url::from_file_path(parent_path).unwrap(); resolution::package_exports_resolve( - deno_core::url::Url::from_directory_path(pkg_path).unwrap(), + &pkg.path, format!(".{}", expansion), exports, - &base, + &referrer, + NodeModuleKind::Cjs, resolution::REQUIRE_CONDITIONS, &*resolver, ) - .map(|r| Some(r.to_file_path().unwrap().to_string_lossy().to_string())) + .map(|r| Some(r.to_string_lossy().to_string())) } else { Ok(None) } } +#[op] +fn op_require_read_closest_package_json

( + state: &mut OpState, + filename: String, +) -> Result +where + P: NodePermissions + 'static, +{ + check_unstable(state); + ensure_read_permission::

( + state, + PathBuf::from(&filename).parent().unwrap(), + )?; + let resolver = state.borrow::>().clone(); + resolution::get_closest_package_json( + &Url::from_file_path(filename).unwrap(), + &*resolver, + ) +} + #[op] fn op_require_read_package_scope( state: &mut OpState, @@ -600,10 +625,11 @@ where let r = resolution::package_imports_resolve( &request, &referrer, + NodeModuleKind::Cjs, resolution::REQUIRE_CONDITIONS, &*resolver, ) - .map(|r| Some(r.as_str().to_string())); + .map(|r| Some(Url::from_file_path(r).unwrap().to_string())); state.put(resolver); r } else { diff --git a/ext/node/package_json.rs b/ext/node/package_json.rs index 19a79da961..15b5ab9203 100644 --- a/ext/node/package_json.rs +++ b/ext/node/package_json.rs @@ -19,6 +19,7 @@ pub struct PackageJson { pub imports: Option>, pub bin: Option, pub main: Option, + pub module: Option, pub name: Option, pub path: PathBuf, pub typ: String, @@ -33,6 +34,7 @@ impl PackageJson { imports: None, bin: None, main: None, + module: None, name: None, path, typ: "none".to_string(), @@ -66,6 +68,7 @@ impl PackageJson { let imports_val = package_json.get("imports"); let main_val = package_json.get("main"); + let module_val = package_json.get("module"); let name_val = package_json.get("name"); let type_val = package_json.get("type"); let bin = package_json.get("bin").map(ToOwned::to_owned); @@ -79,21 +82,12 @@ impl PackageJson { } }); - let imports = if let Some(imp) = imports_val { - imp.as_object().map(|imp| imp.to_owned()) - } else { - None - }; - let main = if let Some(m) = main_val { - m.as_str().map(|m| m.to_string()) - } else { - None - }; - let name = if let Some(n) = name_val { - n.as_str().map(|n| n.to_string()) - } else { - None - }; + let imports = imports_val + .and_then(|imp| imp.as_object()) + .map(|imp| imp.to_owned()); + let main = main_val.and_then(|s| s.as_str()).map(|s| s.to_string()); + let name = name_val.and_then(|s| s.as_str()).map(|s| s.to_string()); + let module = module_val.and_then(|s| s.as_str()).map(|s| s.to_string()); // Ignore unknown types for forwards compatibility let typ = if let Some(t) = type_val { @@ -121,6 +115,7 @@ impl PackageJson { path, main, name, + module, typ, types, exports, diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs index b77e6f690e..21ce589c25 100644 --- a/ext/node/resolution.rs +++ b/ext/node/resolution.rs @@ -1,13 +1,16 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use std::path::Path; use std::path::PathBuf; +use deno_core::anyhow::bail; use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::serde_json::Map; use deno_core::serde_json::Value; use deno_core::url::Url; use deno_core::ModuleSpecifier; +use path_clean::PathClean; use regex::Regex; use crate::errors; @@ -17,25 +20,29 @@ use crate::DenoDirNpmResolver; pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; -fn to_file_path(url: &ModuleSpecifier) -> PathBuf { - url - .to_file_path() - .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {}", url)) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NodeModuleKind { + Esm, + Cjs, } -fn to_file_path_string(url: &ModuleSpecifier) -> String { - to_file_path(url).display().to_string() +fn to_specifier_display_string(url: &ModuleSpecifier) -> String { + if let Ok(path) = url.to_file_path() { + path.display().to_string() + } else { + url.to_string() + } } fn throw_import_not_defined( specifier: &str, - package_json_url: Option, + package_json_path: Option<&Path>, base: &ModuleSpecifier, ) -> AnyError { errors::err_package_import_not_defined( specifier, - package_json_url.map(|u| to_file_path_string(&u.join(".").unwrap())), - &to_file_path_string(base), + package_json_path.map(|p| p.parent().unwrap().display().to_string()), + &to_specifier_display_string(base), ) } @@ -84,30 +91,32 @@ fn pattern_key_compare(a: &str, b: &str) -> i32 { pub fn package_imports_resolve( name: &str, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, conditions: &[&str], npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { +) -> Result { 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_file_path_string(referrer)), + Some(to_specifier_display_string(referrer)), )); } let package_config = get_package_scope_config(referrer, npm_resolver)?; - let mut package_json_url = None; + let mut package_json_path = None; if package_config.exists { - package_json_url = Some(Url::from_file_path(package_config.path).unwrap()); + package_json_path = Some(package_config.path.clone()); if let Some(imports) = &package_config.imports { if imports.contains_key(name) && !name.contains('*') { let maybe_resolved = resolve_package_target( - package_json_url.clone().unwrap(), + package_json_path.as_ref().unwrap(), imports.get(name).unwrap().to_owned(), "".to_string(), name.to_string(), referrer, + referrer_kind, false, true, conditions, @@ -143,11 +152,12 @@ pub fn package_imports_resolve( if !best_match.is_empty() { let target = imports.get(best_match).unwrap().to_owned(); let maybe_resolved = resolve_package_target( - package_json_url.clone().unwrap(), + package_json_path.as_ref().unwrap(), target, best_match_subpath.unwrap(), best_match.to_string(), referrer, + referrer_kind, true, true, conditions, @@ -161,41 +171,45 @@ pub fn package_imports_resolve( } } - Err(throw_import_not_defined(name, package_json_url, referrer)) + Err(throw_import_not_defined( + name, + package_json_path.as_deref(), + referrer, + )) } fn throw_invalid_package_target( subpath: String, target: String, - package_json_url: &ModuleSpecifier, + package_json_path: &Path, internal: bool, - base: &ModuleSpecifier, + referrer: &ModuleSpecifier, ) -> AnyError { errors::err_invalid_package_target( - to_file_path_string(&package_json_url.join(".").unwrap()), + package_json_path.parent().unwrap().display().to_string(), subpath, target, internal, - Some(base.as_str().to_string()), + Some(referrer.as_str().to_string()), ) } fn throw_invalid_subpath( subpath: String, - package_json_url: &ModuleSpecifier, + package_json_path: &Path, internal: bool, - base: &ModuleSpecifier, + referrer: &ModuleSpecifier, ) -> AnyError { let ie = if internal { "imports" } else { "exports" }; let reason = format!( "request is not a valid subpath for the \"{}\" resolution of {}", ie, - to_file_path_string(package_json_url) + package_json_path.display(), ); errors::err_invalid_module_specifier( &subpath, &reason, - Some(to_file_path_string(base)), + Some(to_specifier_display_string(referrer)), ) } @@ -204,20 +218,21 @@ fn resolve_package_target_string( target: String, subpath: String, match_: String, - package_json_url: ModuleSpecifier, - base: &ModuleSpecifier, + package_json_path: &Path, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, pattern: bool, internal: bool, conditions: &[&str], npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { +) -> Result { if !subpath.is_empty() && !pattern && !target.ends_with('/') { return Err(throw_invalid_package_target( match_, target, - &package_json_url, + package_json_path, internal, - base, + referrer, )); } let invalid_segment_re = @@ -234,9 +249,12 @@ fn resolve_package_target_string( } else { format!("{}{}", target, subpath) }; + let package_json_url = + ModuleSpecifier::from_file_path(package_json_path).unwrap(); return package_resolve( &export_target, &package_json_url, + referrer_kind, conditions, npm_resolver, ); @@ -245,35 +263,33 @@ fn resolve_package_target_string( return Err(throw_invalid_package_target( match_, target, - &package_json_url, + package_json_path, internal, - base, + referrer, )); } if invalid_segment_re.is_match(&target[2..]) { return Err(throw_invalid_package_target( match_, target, - &package_json_url, + package_json_path, internal, - base, + referrer, )); } - let resolved = package_json_url.join(&target)?; - let resolved_path = resolved.path(); - let package_url = package_json_url.join(".").unwrap(); - let package_path = package_url.path(); + 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_url, + package_json_path, internal, - base, + referrer, )); } if subpath.is_empty() { - return Ok(resolved); + return Ok(resolved_path); } if invalid_segment_re.is_match(&subpath) { let request = if pattern { @@ -283,39 +299,43 @@ fn resolve_package_target_string( }; return Err(throw_invalid_subpath( request, - &package_json_url, + package_json_path, internal, - base, + referrer, )); } if pattern { + let resolved_path_str = resolved_path.to_string_lossy(); let replaced = pattern_re - .replace(resolved.as_str(), |_caps: ®ex::Captures| subpath.clone()); - let url = Url::parse(&replaced)?; - return Ok(url); + .replace(&resolved_path_str, |_caps: ®ex::Captures| { + subpath.clone() + }); + return Ok(PathBuf::from(replaced.to_string())); } - Ok(resolved.join(&subpath)?) + Ok(resolved_path.join(&subpath).clean()) } #[allow(clippy::too_many_arguments)] fn resolve_package_target( - package_json_url: ModuleSpecifier, + package_json_path: &Path, target: Value, subpath: String, package_subpath: String, - base: &ModuleSpecifier, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, pattern: bool, internal: bool, conditions: &[&str], npm_resolver: &dyn DenoDirNpmResolver, -) -> Result, AnyError> { +) -> Result, AnyError> { if let Some(target) = target.as_str() { return Ok(Some(resolve_package_target_string( target.to_string(), subpath, package_subpath, - package_json_url, - base, + package_json_path, + referrer, + referrer_kind, pattern, internal, conditions, @@ -329,11 +349,12 @@ fn resolve_package_target( let mut last_error = None; for target_item in target_arr { let resolved_result = resolve_package_target( - package_json_url.clone(), + package_json_path, target_item.to_owned(), subpath.clone(), package_subpath.clone(), - base, + referrer, + referrer_kind, pattern, internal, conditions, @@ -371,11 +392,12 @@ fn resolve_package_target( if key == "default" || conditions.contains(&key.as_str()) { let condition_target = target_obj.get(key).unwrap().to_owned(); let resolved = resolve_package_target( - package_json_url.clone(), + package_json_path, condition_target, subpath.clone(), package_subpath.clone(), - base, + referrer, + referrer_kind, pattern, internal, conditions, @@ -394,43 +416,45 @@ fn resolve_package_target( Err(throw_invalid_package_target( package_subpath, target.to_string(), - &package_json_url, + package_json_path, internal, - base, + referrer, )) } fn throw_exports_not_found( subpath: String, - package_json_url: &ModuleSpecifier, - base: &ModuleSpecifier, + package_json_path: &Path, + referrer: &ModuleSpecifier, ) -> AnyError { errors::err_package_path_not_exported( - to_file_path_string(&package_json_url.join(".").unwrap()), + package_json_path.parent().unwrap().display().to_string(), subpath, - Some(to_file_path_string(base)), + Some(to_specifier_display_string(referrer)), ) } pub fn package_exports_resolve( - package_json_url: ModuleSpecifier, + package_json_path: &Path, package_subpath: String, package_exports: &Map, - base: &ModuleSpecifier, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, conditions: &[&str], npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { +) -> Result { if package_exports.contains_key(&package_subpath) && package_subpath.find('*').is_none() && !package_subpath.ends_with('/') { let target = package_exports.get(&package_subpath).unwrap().to_owned(); let resolved = resolve_package_target( - package_json_url.clone(), + package_json_path, target, "".to_string(), package_subpath.to_string(), - base, + referrer, + referrer_kind, false, false, conditions, @@ -439,8 +463,8 @@ pub fn package_exports_resolve( if resolved.is_none() { return Err(throw_exports_not_found( package_subpath, - &package_json_url, - base, + package_json_path, + referrer, )); } return Ok(resolved.unwrap()); @@ -483,11 +507,12 @@ pub fn package_exports_resolve( if !best_match.is_empty() { let target = package_exports.get(best_match).unwrap().to_owned(); let maybe_resolved = resolve_package_target( - package_json_url.clone(), + package_json_path, target, best_match_subpath.unwrap(), best_match.to_string(), - base, + referrer, + referrer_kind, true, false, conditions, @@ -498,22 +523,22 @@ pub fn package_exports_resolve( } else { return Err(throw_exports_not_found( package_subpath, - &package_json_url, - base, + package_json_path, + referrer, )); } } Err(throw_exports_not_found( package_subpath, - &package_json_url, - base, + package_json_path, + referrer, )) } fn parse_package_name( specifier: &str, - base: &ModuleSpecifier, + referrer: &ModuleSpecifier, ) -> Result<(String, String, bool), AnyError> { let mut separator_index = specifier.find('/'); let mut valid_package_name = true; @@ -547,7 +572,7 @@ fn parse_package_name( return Err(errors::err_invalid_module_specifier( specifier, "is not a valid package name", - Some(to_file_path_string(base)), + Some(to_specifier_display_string(referrer)), )); } @@ -563,27 +588,28 @@ fn parse_package_name( pub fn package_resolve( specifier: &str, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, conditions: &[&str], npm_resolver: &dyn DenoDirNpmResolver, -) -> Result { +) -> Result { let (package_name, package_subpath, _is_scoped) = parse_package_name(specifier, referrer)?; // ResolveSelf let package_config = get_package_scope_config(referrer, npm_resolver)?; - if package_config.exists { - let package_json_url = Url::from_file_path(&package_config.path).unwrap(); - if package_config.name.as_ref() == Some(&package_name) { - if let Some(exports) = &package_config.exports { - return package_exports_resolve( - package_json_url, - package_subpath, - exports, - referrer, - conditions, - npm_resolver, - ); - } + if package_config.exists + && package_config.name.as_ref() == Some(&package_name) + { + if let Some(exports) = &package_config.exports { + return package_exports_resolve( + &package_config.path, + package_subpath, + exports, + referrer, + referrer_kind, + conditions, + npm_resolver, + ); } } @@ -592,8 +618,6 @@ pub fn package_resolve( &referrer.to_file_path().unwrap(), )?; let package_json_path = package_dir_path.join("package.json"); - let package_json_url = - ModuleSpecifier::from_file_path(&package_json_path).unwrap(); // todo: error with this instead when can't find package // Err(errors::err_module_not_found( @@ -612,21 +636,20 @@ pub fn package_resolve( let package_json = PackageJson::load(npm_resolver, package_json_path)?; if let Some(exports) = &package_json.exports { return package_exports_resolve( - package_json_url, + &package_json.path, package_subpath, exports, referrer, + referrer_kind, conditions, npm_resolver, ); } if package_subpath == "." { - return legacy_main_resolve(&package_json_url, &package_json, referrer); + return legacy_main_resolve(&package_json, referrer_kind); } - package_json_url - .join(&package_subpath) - .map_err(AnyError::from) + Ok(package_json.path.parent().unwrap().join(&package_subpath)) } pub fn get_package_scope_config( @@ -635,12 +658,43 @@ pub fn get_package_scope_config( ) -> Result { let root_folder = npm_resolver .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?; - let package_json_path = root_folder.join("./package.json"); + let package_json_path = root_folder.join("package.json"); PackageJson::load(npm_resolver, package_json_path) } -fn file_exists(path_url: &ModuleSpecifier) -> bool { - if let Ok(stats) = std::fs::metadata(to_file_path(path_url)) { +pub fn get_closest_package_json( + url: &ModuleSpecifier, + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + let package_json_path = get_closest_package_json_path(url, npm_resolver)?; + PackageJson::load(npm_resolver, package_json_path) +} + +fn get_closest_package_json_path( + url: &ModuleSpecifier, + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + let file_path = url.to_file_path().unwrap(); + let mut current_dir = file_path.parent().unwrap(); + let package_json_path = current_dir.join("package.json"); + if package_json_path.exists() { + return Ok(package_json_path); + } + let root_pkg_folder = npm_resolver + .resolve_package_folder_from_path(&url.to_file_path().unwrap())?; + while current_dir.starts_with(&root_pkg_folder) { + current_dir = current_dir.parent().unwrap(); + let package_json_path = current_dir.join("package.json"); + if package_json_path.exists() { + return Ok(package_json_path); + } + } + + bail!("did not find package.json in {}", root_pkg_folder.display()) +} + +fn file_exists(path: &Path) -> bool { + if let Ok(stats) = std::fs::metadata(path) { stats.is_file() } else { false @@ -648,28 +702,56 @@ fn file_exists(path_url: &ModuleSpecifier) -> bool { } pub fn legacy_main_resolve( - package_json_url: &ModuleSpecifier, package_json: &PackageJson, - _base: &ModuleSpecifier, -) -> Result { + referrer_kind: NodeModuleKind, +) -> Result { + let maybe_main = + if referrer_kind == NodeModuleKind::Esm && package_json.typ == "module" { + &package_json.module + } else { + &package_json.main + }; let mut guess; - if let Some(main) = &package_json.main { - guess = package_json_url.join(&format!("./{}", main))?; + if let Some(main) = maybe_main { + guess = package_json.path.parent().unwrap().join(main).clean(); if file_exists(&guess) { return Ok(guess); } let mut found = false; - for ext in [ - ".js", - ".json", - ".node", - "/index.js", - "/index.json", - "/index.node", - ] { - guess = package_json_url.join(&format!("./{}{}", main, ext))?; + // todo(dsherret): investigate exactly how node handles this + let endings = match referrer_kind { + NodeModuleKind::Cjs => vec![ + ".js", + ".cjs", + ".json", + ".node", + "/index.js", + "/index.cjs", + "/index.json", + "/index.node", + ], + NodeModuleKind::Esm => vec![ + ".js", + ".mjs", + ".json", + ".node", + "/index.js", + "/index.mjs", + ".cjs", + "/index.cjs", + "/index.json", + "/index.node", + ], + }; + for ending in endings { + guess = package_json + .path + .parent() + .unwrap() + .join(&format!("{}{}", main, ending)) + .clean(); if file_exists(&guess) { found = true; break; @@ -682,8 +764,25 @@ pub fn legacy_main_resolve( } } - for p in ["./index.js", "./index.json", "./index.node"] { - guess = package_json_url.join(p)?; + let index_file_names = match referrer_kind { + NodeModuleKind::Cjs => { + vec!["index.js", "index.cjs", "index.json", "index.node"] + } + NodeModuleKind::Esm => vec![ + "index.js", + "index.mjs", + "index.cjs", + "index.json", + "index.node", + ], + }; + for index_file_name in index_file_names { + guess = package_json + .path + .parent() + .unwrap() + .join(index_file_name) + .clean(); if file_exists(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() return Ok(guess);