diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index 7806f0b98..383d3bffd 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -44,7 +44,7 @@ Crossed out test files are irrelevant. - [x] alias.test.js (need to fix a todo) - [x] browserField.test.js (reading the browser field is currently static - not read from the `browserField` option) - [ ] dependencies.test.js -- [ ] exportsField.test.js +- [x] exportsField.test.js - [x] extension-alias.test.js - [x] extensions.test.js - [x] fallback.test.js (need to fix a todo) diff --git a/crates/oxc_resolver/src/error.rs b/crates/oxc_resolver/src/error.rs index 5690d118c..b7302dbdb 100644 --- a/crates/oxc_resolver/src/error.rs +++ b/crates/oxc_resolver/src/error.rs @@ -43,6 +43,12 @@ pub enum ResolveError { // TODO: Invalid package config /xxx/package.json. "exports" cannot contain some keys starting with '.' and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only. InvalidPackageConfig(PathBuf), + + // TODO: Default condition should be last one + InvalidPackageConfigDefault(PathBuf), + + // TODO: Expecting folder to folder mapping. "./data/timezones" should end with "/" + InvalidPackageConfigDirectory(PathBuf), } #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index 23f9de48e..8f4875134 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -27,14 +27,15 @@ use std::{ use crate::{ cache::{Cache, CacheValue}, file_system::FileSystemOs, - package_json::{ExportsField, ExportsKey, MatchObject, PackageJson}, - path::PathUtil, + package_json::{ExportsKey, MatchObject, PackageJson}, request::{Request, RequestPath}, }; pub use crate::{ error::{JSONError, ResolveError}, file_system::{FileMetadata, FileSystem}, options::{Alias, AliasValue, ResolveOptions}, + package_json::ExportsField, + path::PathUtil, resolution::Resolution, }; @@ -533,8 +534,8 @@ impl ResolverGeneric { /// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions) /// - /// - fn package_exports_resolve( + /// # Errors + pub fn package_exports_resolve( &self, package_url: &Path, subpath: &str, @@ -559,42 +560,58 @@ impl ResolverGeneric { // 2. If subpath is equal to ".", then // Note: subpath is not prepended with a dot when passed in. if subpath.is_empty() { - // 1. Let mainExport be undefined. - // 2. If exports is a String or Array, or an Object containing no keys starting with ".", then - // 1. Set mainExport to exports. - match exports { - ExportsField::None => {} + // 1. Let mainExport be undefined. + let main_export = match exports { + ExportsField::None => None, + // 2. If exports is a String or Array, or an Object containing no keys starting with ".", then ExportsField::String(_) | ExportsField::Array(_) => { - return self.package_target_resolve(package_url, exports, None, conditions); + // 1. Set mainExport to exports. + Some(exports) } // 3. Otherwise if exports is an Object containing a "." property, then - // 1. Set mainExport to exports["."]. ExportsField::Map(map) => { - // 4. If mainExport is not undefined, then - if let Some(main_export) = map.get(&ExportsKey::Main) { - // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions). - // 2. If resolved is not null or undefined, return resolved. - return self.package_target_resolve( - package_url, - main_export, - None, - conditions, - ); - } + // 1. Set mainExport to exports["."]. + map.get(&ExportsKey::Main).map_or_else( + || { + if map.keys().any(|key| matches!(key, ExportsKey::Pattern(_))) { + None + } else { + Some(exports) + } + }, + Some, + ) + } + }; + // 4. If mainExport is not undefined, then + if let Some(main_export) = main_export { + // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions). + let resolved = self.package_target_resolve( + package_url, + ".", + main_export, + None, + /* is_imports */ false, + conditions, + )?; + // 2. If resolved is not null or undefined, return resolved. + if let Some(path) = resolved { + return Ok(Some(path)); } } } // 3. Otherwise, if exports is an Object and all keys of exports start with ".", then if let ExportsField::Map(exports) = exports { - // 1. Let matchKey be the string "./" concatenated with subpath. + // 1. Let matchKey be the string "./" concatenated with subpath. + // Note: `package_imports_exports_resolve` does not require the leading dot. let match_key = subpath; - // 2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions). + // 2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions). if let Some(path) = self.package_imports_exports_resolve(match_key, exports, package_url, conditions)? { + // 3. If resolved is not null or undefined, return resolved. return Ok(Some(path)); } - // 3. If resolved is not null or undefined, return resolved. } // 4. Throw a Package Path Not Exported error. Err(ResolveError::PackagePathNotExported(format!(".{subpath}"))) @@ -608,65 +625,76 @@ impl ResolverGeneric { package_url: &Path, conditions: &[String], ) -> ResolveState { + // enhanced_resolve behaves differently, it throws + // Error: Resolving to directories is not possible with the exports field (request was ./dist/) + if match_key.ends_with('/') { + return Ok(None); + } // 1. If matchKey is a key of matchObj and does not contain "*", then if !match_key.contains('*') { // 1. Let target be the value of matchObj[matchKey]. if let Some(target) = match_obj.get(&ExportsKey::Pattern(match_key.to_string())) { // 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions). - return self.package_target_resolve(package_url, target, None, conditions); + return self.package_target_resolve( + package_url, + match_key, + target, + None, + /* is_imports */ false, + conditions, + ); } } + + let mut best_target = None; + let mut best_match = ""; + let mut best_key = ""; // 2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity. // 3. For each key expansionKey in expansionKeys, do - let mut best_key = ""; - let mut best_match = ""; - let mut best_target = None; for (expansion_key, target) in match_obj { if let ExportsKey::Pattern(expansion_key) = expansion_key { // 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character. if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') { // 2. If matchKey starts with but is not equal to patternBase, then if match_key.starts_with(pattern_base) + // 1. Let patternTrailer be the substring of expansionKey from the index after the first "*" character. && !pattern_trailer.contains('*') + // 2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then && (pattern_trailer.is_empty() || (match_key.len() >= expansion_key.len() && match_key.ends_with(pattern_trailer))) - && Self::pattern_key_compare(best_key, expansion_key) == Ordering::Greater + && Self::pattern_key_compare(best_key, expansion_key).is_gt() { - best_key = expansion_key; + // 1. Let target be the value of matchObj[expansionKey]. + best_target = Some(target); + // 2. Let patternMatch be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer. best_match = &match_key[pattern_base.len()..match_key.len() - pattern_trailer.len()]; - best_target = Some(target); - // 1. Let patternTrailer be the substring of expansionKey from the index after the first "*" character. - // 2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then - // 1. Let target be the value of matchObj[expansionKey]. - // 2. Let patternMatch be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer. - // 3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions). + best_key = expansion_key; } - } else { + } else if expansion_key.ends_with('/') + && match_key.starts_with(expansion_key) + && Self::pattern_key_compare(best_key, expansion_key).is_gt() + { // TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json. - // Update this package.json to use a subpath pattern like "./dist/*". - if let Some(pattern_match) = match_key.strip_prefix(expansion_key) { - return self.package_target_resolve( - package_url, - target, - Some(pattern_match), - conditions, - ); - } + best_target = Some(target); + best_match = &match_key[expansion_key.len()..]; + best_key = expansion_key; } } } - - if !best_key.is_empty() { + if let Some(best_target) = best_target { + // 3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions). return self.package_target_resolve( package_url, - best_target.unwrap(), + best_key, + best_target, Some(best_match), + /* is_imports */ false, conditions, ); } - // Return null. + // 4. Return null. Ok(None) } @@ -674,122 +702,183 @@ impl ResolverGeneric { fn package_target_resolve( &self, package_url: &Path, + target_key: &str, target: &ExportsField, pattern_match: Option<&str>, + is_imports: bool, conditions: &[String], ) -> ResolveState { - // 1. If target is a String, then match target { ExportsField::None => {} + // 1. If target is a String, then ExportsField::String(target) => { - // 1. If target does not start with "./", then - // 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then - // 1. Throw an Invalid Package Target error. - // 2. If patternMatch is a String, then - // 1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, packageURL + "/"). - // 3. Return PACKAGE_RESOLVE(target, packageURL + "/"). - // 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error. - if Path::new(target).is_invalid_exports_target() { + // 1. If target does not start with "./", then + if !target.starts_with("./") { + // 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then + if !is_imports || target.starts_with("../") || target.starts_with('/') { + // 1. Throw an Invalid Package Target error. + // TODO: + // Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" target "/a/" defined for './utils/*' in the package config /Users/bytedance/github/test-resolver/node_modules/foo/package.json; targets must start with "./" + return Err(ResolveError::InvalidPackageTarget(target.to_string())); + } + // 2. If patternMatch is a String, then + // 1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, packageURL + "/"). + // 3. Return PACKAGE_RESOLVE(target, packageURL + "/"). + } + // 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error. + let target = if let Some(pattern_match) = pattern_match { + if !target_key.contains('*') && !target.contains('*') { + // enhanced_resolve behaviour + // TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json. + if target_key.ends_with('/') && target.ends_with('/') { + Cow::Owned(format!("{target}{pattern_match}")) + } else { + return Err(ResolveError::InvalidPackageConfigDirectory( + package_url.join("package.json"), + )); + } + } else { + // if !target.contains('*') && target.ends_with('/') { + // Cow::Owned(format!("{target}{pattern_match}")) + // } else { + Cow::Owned(target.replace('*', pattern_match)) + } + // } + } else { + Cow::Borrowed(target) + }; + + if Path::new(target.as_str()).is_invalid_exports_target() { return Err(ResolveError::InvalidPackageTarget(target.to_string())); } - // 3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target. - let resolved_target = package_url.join(target); - // 4. Assert: resolvedTarget is contained in packageURL. - // 5. If patternMatch is null, then - let Some(pattern_match) = pattern_match else { - // 1. Return resolvedTarget. - return Ok(Some(self.cache.value(&resolved_target))) - }; - // 6. If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error. - if Path::new(pattern_match).is_invalid_exports_target() { - return Err(ResolveError::InvalidModuleSpecifier(pattern_match.to_string())); - } - // 7. Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch. - let path = if target.contains('*') { - package_url.join(target.replace('*', pattern_match)) - } else { - // DEP0148 behaviour - package_url.join(target).join(pattern_match) - } - .normalize(); - return Ok(Some(self.cache.value(&path))); + // 3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target. + let resolved_target = package_url.join(target.as_str()).normalize(); + // 4. Assert: resolvedTarget is contained in packageURL. + // 5. If patternMatch is null, then + // 6. If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error. + // 7. Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch. + return Ok(Some(self.cache.value(&resolved_target))); } // 2. Otherwise, if target is a non-null Object, then ExportsField::Map(target) => { - // 1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error. - // 2. For each property p of target, in object insertion order as, - for (key, target_value) in target { - // 1. If p equals "default" or conditions contains an entry for p, then - if matches!(key, ExportsKey::CustomCondition(condition) if condition == "default" || conditions.contains(condition)) + // 1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error. + // 2. For each property p of target, in object insertion order as, + for (i, (key, target_value)) in target.iter().enumerate() { + // https://nodejs.org/api/packages.html#conditional-exports + // "default" - the generic fallback that always matches. Can be a CommonJS or ES module file. This condition should always come last. + // Note: node.js does not throw this but enhanced_resolve does. + let is_default = matches!(key, ExportsKey::CustomCondition(condition) if condition == "default"); + if i < target.len() - 1 && is_default { + return Err(ResolveError::InvalidPackageConfigDefault( + package_url.join("package.json"), + )); + } + + // 1. If p equals "default" or conditions contains an entry for p, then + if is_default + || matches!(key, ExportsKey::CustomCondition(condition) if conditions.contains(condition)) { - // 1. Let targetValue be the value of the p property in target. - // 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions). - // 3. If resolved is equal to undefined, continue the loop. - // 4. Return resolved. - let x = self.package_target_resolve( + // 1. Let targetValue be the value of the p property in target. + // 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions). + let resolved = self.package_target_resolve( package_url, + target_key, target_value, pattern_match, + /* is_imports */ false, conditions, ); - if let Some(path) = x? { + // 3. If resolved is equal to undefined, continue the loop. + if let Some(path) = resolved? { + // 4. Return resolved. return Ok(Some(path)); } } - // 3. Return undefined. } - } // 3. Otherwise, if target is an Array, then + // 3. Return undefined. + return Ok(None); + } + // 3. Otherwise, if target is an Array, then ExportsField::Array(targets) => { - // 1. If _target.length is zero, return null. - // 2. For each item targetValue in target, do - for target_value in targets { - // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error. - // 2. If resolved is undefined, continue the loop. - if let Some(path) = self.package_target_resolve( + // 1. If _target.length is zero, return null. + if targets.is_empty() { + // Note: return PackagePathNotExported has the same effect as return because there are no matches. + return Err(ResolveError::PackagePathNotExported(format!( + ".{}", + pattern_match.unwrap_or(".") + ))); + } + // 2. For each item targetValue in target, do + for (i, target_value) in targets.iter().enumerate() { + // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error. + let resolved = self.package_target_resolve( package_url, + target_key, target_value, pattern_match, + /* is_imports */ false, conditions, - )? { + ); + + if resolved.is_err() && i == targets.len() { + return resolved; + } + + // 2. If resolved is undefined, continue the loop. + if let Ok(Some(path)) = resolved { + // 3. Return resolved. return Ok(Some(path)); } - // 3. Return resolved. - // 3. Return or throw the last fallback resolution null return or error. } + // 3. Return or throw the last fallback resolution null return or error. + // Note: see `resolved.is_err() && i == targets.len()` } } // 4. Otherwise, if target is null, return null. - // 5. Otherwise throw an Invalid Package Target error. Ok(None) + // 5. Otherwise throw an Invalid Package Target error. } /// PATTERN_KEY_COMPARE(keyA, keyB) - fn pattern_key_compare(a: &str, b: &str) -> Ordering { - // 1. Assert: keyA ends with "/" or contains only a single "*". - // 2. Assert: keyB ends with "/" or contains only a single "*". - // 3. Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise. - let a_pos = a.chars().position(|c| c == '*'); - // 4. Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise. - let b_pos = b.chars().position(|c| c == '*'); - // 5. If baseLengthA is greater than baseLengthB, return -1. - let base_length_a = a_pos.map_or(a.len(), |p| p + 1); - // 6. If baseLengthB is greater than baseLengthA, return 1. - let base_length_b = b_pos.map_or(b.len(), |p| p + 1); - // 7. If keyA does not contain "*", return 1. - // 8. If keyB does not contain "*", return -1. - // 9. If the length of keyA is greater than the length of keyB, return -1. - // 10. If the length of keyB is greater than the length of keyA, return 1. - // 11. Return 0. - let cmp = base_length_b.cmp(&base_length_a); - if cmp != Ordering::Equal { - return cmp; - } - if a_pos.is_none() { + fn pattern_key_compare(key_a: &str, key_b: &str) -> Ordering { + if key_a.is_empty() { return Ordering::Greater; } - if b_pos.is_none() { + // 1. Assert: keyA ends with "/" or contains only a single "*". + debug_assert!(key_a.ends_with('/') || key_a.match_indices('*').count() == 1, "{key_a}"); + // 2. Assert: keyB ends with "/" or contains only a single "*". + debug_assert!(key_b.ends_with('/') || key_b.match_indices('*').count() == 1, "{key_b}"); + // 3. Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise. + let a_pos = key_a.chars().position(|c| c == '*'); + let base_length_a = a_pos.map_or(key_a.len(), |p| p + 1); + // 4. Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise. + let b_pos = key_b.chars().position(|c| c == '*'); + let base_length_b = b_pos.map_or(key_b.len(), |p| p + 1); + // 5. If baseLengthA is greater than baseLengthB, return -1. + if base_length_a > base_length_b { return Ordering::Less; } - b.len().cmp(&a.len()) + // 6. If baseLengthB is greater than baseLengthA, return 1. + if base_length_b > base_length_a { + return Ordering::Greater; + } + // 7. If keyA does not contain "*", return 1. + if !key_a.contains('*') { + return Ordering::Greater; + } + // 8. If keyB does not contain "*", return -1. + if !key_b.contains('*') { + return Ordering::Less; + } + // 9. If the length of keyA is greater than the length of keyB, return -1. + if key_a.len() > key_b.len() { + return Ordering::Less; + } + // 10. If the length of keyB is greater than the length of keyA, return 1. + if key_b.len() > key_a.len() { + return Ordering::Greater; + } + // 11. Return 0. + Ordering::Equal } } diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/exports_field.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/exports_field.rs index 72797e854..3d0cafea9 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/exports_field.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/exports_field.rs @@ -2,10 +2,12 @@ //! //! The resolution tests are at the bottom of the file. -use oxc_resolver::{Resolution, ResolveError, ResolveOptions, Resolver}; +use oxc_resolver::{ExportsField, PathUtil, Resolution, ResolveError, ResolveOptions, Resolver}; +use serde_json::json; +use std::path::{Path, PathBuf}; #[test] -fn exports_field() { +fn test() { let f = super::fixture().join("exports-field"); let f2 = super::fixture().join("exports-field2"); let f4 = super::fixture().join("exports-field-error"); @@ -22,7 +24,10 @@ fn exports_field() { let pass = [ ("resolve root using exports field, not a main field", f.clone(), "exports-field", f.join("node_modules/exports-field/x.js")), ("resolver should respect condition names", f.clone(), "exports-field/dist/main.js", f.join("node_modules/exports-field/lib/lib2/main.js")), - // TODO: ("resolver should respect fallback", f2.clone(), "exports-field/dist/browser.js", f2.join("node_modules/exports-field/lib/browser.js")), + // enhanced_resolve behaves differently to node.js. enhanced_resolve fallbacks when an + // array item is unresolved, where as node.js fallbacks when an array has an + // InvalidPackageTarget error. + // ("resolver should respect fallback", f2.clone(), "exports-field/dist/browser.js", f2.join("node_modules/exports-field/lib/browser.js")), // TODO: ("resolver should respect query parameters #1", f2.clone(), "exports-field/dist/browser.js?foo", f2.join("node_modules/exports-field/lib/browser.js?foo")), // TODO: ("resolver should respect fragment parameters #1", f2.clone(), "exports-field/dist/browser.js#foo", f2.join("node_modules/exports-field/lib/browser.js#foo")), ("relative path should work, if relative path as request is used", f.clone(), "./node_modules/exports-field/lib/main.js", f.join("node_modules/exports-field/lib/main.js")), @@ -52,12 +57,12 @@ fn exports_field() { // TODO: ("resolver should respect query parameters #2. Direct matching", f2.clone(), "exports-field?foo", ResolveError::NotFound(f2.join("").into_boxed_path())), // TODO: ("resolver should respect fragment parameters #2. Direct matching", f2.clone(), "exports-field#foo", ResolveError::NotFound(f2.join("").into_boxed_path())), ("relative path should not work with exports field", f.clone(), "./node_modules/exports-field/dist/main.js", ResolveError::NotFound(f.join("node_modules/exports-field/dist/main.js").into_boxed_path())), - ("backtracking should not work for request", f.clone(), "exports-field/dist/../../../a.js", ResolveError::InvalidModuleSpecifier("../../../a.js".to_string())), + ("backtracking should not work for request", f.clone(), "exports-field/dist/../../../a.js", ResolveError::InvalidPackageTarget("./lib/../../../a.js".to_string())), ("backtracking should not work for exports field target", f.clone(), "exports-field/dist/a.js", ResolveError::InvalidPackageTarget("./../../a.js".to_string())), ("not exported error", f.clone(), "exports-field/anything/else", ResolveError::PackagePathNotExported("./anything/else".to_string())), ("request ending with slash #1", f.clone(), "exports-field/", ResolveError::PackagePathNotExported("./".to_string())), - // TODO: ("request ending with slash #2", f.clone(), "exports-field/dist/", ResolveError::PackagePathNotExported("".to_string())), - // TODO: ("request ending with slash #3", f.clone(), "exports-field/lib/", ResolveError::PackagePathNotExported("".to_string())), + ("request ending with slash #2", f.clone(), "exports-field/dist/", ResolveError::PackagePathNotExported("./dist/".to_string())), + ("request ending with slash #3", f.clone(), "exports-field/lib/", ResolveError::PackagePathNotExported("./lib/".to_string())), ("should throw error if target is invalid", f4, "exports-field", ResolveError::InvalidPackageTarget("./a/../b/../../pack1/index.js".to_string())), ("throw error if exports field is invalid", f.clone(), "invalid-exports-field", ResolveError::InvalidPackageConfig(f.join("node_modules/invalid-exports-field/package.json"))), ("should throw error if target is 'null'", f5, "m/features/internal/file.js", ResolveError::PackagePathNotExported("./features/internal/file.js".to_string())), @@ -200,3 +205,2267 @@ fn extension_alias_throw_error() { assert_eq!(resolution, Err(error), "{comment} {path:?} {request}"); } } + +// Small script for generating the test cases from enhanced_resolve +// for (c of testCases) { +// console.log("TestCase {") +// console.log(`name: ${JSON.stringify(c.name)},`) +// if (c.expect instanceof Error) { +// console.log(`expect: None,`) +// } else { +// console.log(`expect: Some(vec!${JSON.stringify(c.expect)}),`) +// } +// console.log(`exports_field: exports_field(json!(${JSON.stringify(c.suite[0], null, 2)})),`) +// console.log(`request: "${c.suite[1]}",`) +// console.log(`condition_names: vec!${JSON.stringify(c.suite[2])},`) +// console.log("},") +//} +struct TestCase { + name: &'static str, + expect: Option>, + exports_field: ExportsField, + request: &'static str, + condition_names: Vec<&'static str>, +} + +#[allow(clippy::needless_pass_by_value)] +fn exports_field(value: serde_json::Value) -> ExportsField { + let s = serde_json::to_string(&value).unwrap(); + serde_json::from_str(&s).unwrap() +} + +#[test] +fn entry_points() { + let test_cases = [ + TestCase { + name: "sample #1", + expect: Some(vec!["./dist/test/file.js"]), + exports_field: exports_field(json!({ + "./foo/": { + "import": [ + "./dist/", + "./src/" + ], + "webpack": "./wp/" + }, + ".": "./main.js" + })), + request: "./foo/test/file.js", + condition_names: vec!["import", "webpack"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "sample #1", + expect: Some(vec!["./src/test/file.js"]), + exports_field: exports_field(json!({ + "./foo/": { + "import": [ + "./src/" + ], + "webpack": "./wp/" + }, + ".": "./main.js" + })), + request: "./foo/test/file.js", + condition_names: vec!["import", "webpack"], + }, + TestCase { + name: "sample #1 (wildcard)", + expect: Some(vec!["./dist/test/file.js"]), + exports_field: exports_field(json!({ + "./foo/*": { + "import": [ + "./dist/*", + "./src/*" + ], + "webpack": "./wp/*" + }, + ".": "./main.js" + })), + request: "./foo/test/file.js", + condition_names: vec!["import", "webpack"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "sample #1 (wildcard)", + expect: Some(vec!["./src/test/file.js"]), + exports_field: exports_field(json!({ + "./foo/*": { + "import": [ + "./src/*" + ], + "webpack": "./wp/*" + }, + ".": "./main.js" + })), + request: "./foo/test/file.js", + condition_names: vec!["import", "webpack"], + }, + TestCase { + name: "sample #2", + expect: Some(vec!["./data/timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./timezones/": "./data/timezones/" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "sample #2 (wildcard)", + expect: Some(vec!["./data/timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./timezones/*": "./data/timezones/*" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "sample #3", + expect: Some(vec!["./data/timezones/timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./": "./data/timezones/" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "sample #3 (wildcard)", + expect: Some(vec!["./data/timezones/timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./*": "./data/timezones/*.mjs" + })), + request: "./timezones/pdt", + condition_names: vec![], + }, + TestCase { + name: "sample #4", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./lib/": { + "browser": [ + "./browser/" + ] + }, + "./dist/index.js": { + "node": "./index.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "sample #4 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./lib/*": { + "browser": [ + "./browser/*" + ] + }, + "./dist/index.js": { + "node": "./index.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "sample #5", + expect: Some(vec!["./browser/index.js"]), + exports_field: exports_field(json!({ + "./lib/": { + "browser": [ + "./browser/" + ] + }, + "./dist/index.js": { + "node": "./index.js", + "default": "./browser/index.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "sample #5 (wildcard)", + expect: Some(vec!["./browser/index.js"]), + exports_field: exports_field(json!({ + "./lib/*": { + "browser": [ + "./browser/*" + ] + }, + "./dist/index.js": { + "node": "./index.js", + "default": "./browser/index.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "sample #6", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./dist/a": "./dist/index.js" + })), + request: "./dist/aaa", + condition_names: vec![], + }, + TestCase { + name: "sample #7", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./dist/a/a/": "./dist/index.js" + })), + request: "./dist/a/a", + condition_names: vec![], + }, + TestCase { + name: "sample #7 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./dist/a/a/*": "./dist/index.js" + })), + request: "./dist/a/a", + condition_names: vec![], + }, + TestCase { + name: "sample #8", + expect: Some(vec![]), + exports_field: exports_field(json!({ + ".": "./index.js" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "sample #9", + expect: Some(vec!["./main.js"]), + exports_field: exports_field(json!({ + "./index.js": "./main.js" + })), + request: "./index.js", + condition_names: vec![], + }, + TestCase { + name: "sample #10", + expect: Some(vec!["./ok.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./#foo", + condition_names: vec![], + }, + TestCase { + name: "sample #11", + expect: Some(vec!["./ok.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./bar#foo", + condition_names: vec![], + }, + TestCase { + name: "sample #12", + expect: Some(vec!["./ok.js#abc"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./#zapp/ok.js#abc", + condition_names: vec![], + }, + TestCase { + name: "sample #12", + expect: Some(vec!["./ok.js#abc"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./#zapp/ok.js#abc", + condition_names: vec![], + }, + TestCase { + name: "sample #13", + expect: Some(vec!["./ok.js?abc"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./#zapp/ok.js?abc", + condition_names: vec![], + }, + TestCase { + name: "sample #14", + expect: Some(vec!["./🎉.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./#zapp/🎉.js", + condition_names: vec![], + }, + TestCase { + name: "sample #15", + expect: Some(vec!["./%F0%9F%8E%89.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./#zapp/%F0%9F%8E%89.js", + condition_names: vec![], + }, + TestCase { + name: "sample #16", + expect: Some(vec!["./ok.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./🎉", + condition_names: vec![], + }, + TestCase { + name: "sample #17", + expect: Some(vec!["./other.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./%F0%9F%8E%89", + condition_names: vec![], + }, + TestCase { + name: "sample #18", + expect: Some(vec!["./ok.js"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./module", + condition_names: vec![], + }, + TestCase { + name: "sample #19", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./module#foo", + condition_names: vec![], + }, + TestCase { + name: "sample #20", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./zzz*" + })), + request: "./module?foo", + condition_names: vec![], + }, + TestCase { + name: "sample #21", + expect: Some(vec!["./zizizi"]), + exports_field: exports_field(json!({ + "./#foo": "./ok.js", + "./module": "./ok.js", + "./🎉": "./ok.js", + "./%F0%9F%8E%89": "./other.js", + "./bar#foo": "./ok.js", + "./#zapp/": "./", + "./#zipp*": "./z*z*z*" + })), + request: "./#zippi", + condition_names: vec![], + }, + TestCase { + name: "sample #22", + expect: Some(vec!["./d?e?f"]), + exports_field: exports_field(json!({ + "./a?b?c/": "./" + })), + request: "./a?b?c/d?e?f", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #1", + expect: Some(vec!["./dist/index.js"]), + exports_field: exports_field(json!({ + ".": "./dist/index.js" + })), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #2", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./": "./", + "./*": "./*", + "./dist/index.js": "./dist/index.js" + })), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #3", + expect: Some(vec!["./dist/a.js"]), + exports_field: exports_field(json!({ + "./dist/": "./dist/", + "./dist/*": "./dist/*", + "./dist*": "./dist*", + "./dist/index.js": "./dist/a.js" + })), + request: "./dist/index.js", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #4", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "./": { + "browser": [ + "./browser/" + ] + }, + "./*": { + "browser": [ + "./browser/*" + ] + }, + "./dist/index.js": { + "browser": "./index.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "Direct mapping #5", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./": { + "browser": [ + "./browser/" + ] + }, + "./*": { + "browser": [ + "./browser/*" + ] + }, + "./dist/index.js": { + "node": "./node.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "Direct mapping #6", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + ".": { + "browser": "./index.js", + "node": "./src/node/index.js", + "default": "./src/index.js" + } + })), + request: ".", + condition_names: vec!["browser"], + }, + TestCase { + name: "Direct mapping #7", + expect: None, + exports_field: exports_field(json!({ + ".": { + "default": "./src/index.js", + "browser": "./index.js", + "node": "./src/node/index.js" + } + })), + request: ".", + condition_names: vec!["browser"], + }, + TestCase { + name: "Direct mapping #8", + expect: Some(vec!["./src/index.js"]), + exports_field: exports_field(json!({ + ".": { + "browser": "./index.js", + "node": "./src/node/index.js", + "default": "./src/index.js" + } + })), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #9", + expect: Some(vec!["./index"]), + exports_field: exports_field(json!({ + ".": "./index" + })), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #10", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "./index": "./index.js" + })), + request: "./index", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #11", + expect: Some(vec!["./foo.js"]), + exports_field: exports_field(json!({ + "./": "./", + "./*": "./*", + "./dist/index.js": "./dist/index.js" + })), + request: "./foo.js", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #12", + expect: Some(vec!["./foo/bar/baz.js"]), + exports_field: exports_field(json!({ + "./": "./", + "./*": "./*", + "./dist/index.js": "./dist/index.js" + })), + request: "./foo/bar/baz.js", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #13", + expect: Some(vec!["./foo/bar/baz.js"]), + exports_field: exports_field(json!({ + "./": "./", + "./dist/index.js": "./dist/index.js" + })), + request: "./foo/bar/baz.js", + condition_names: vec![], + }, + TestCase { + name: "Direct mapping #14", + expect: Some(vec!["./foo/bar/baz.js"]), + exports_field: exports_field(json!({ + "./*": "./*", + "./dist/index.js": "./dist/index.js" + })), + request: "./foo/bar/baz.js", + condition_names: vec![], + }, + TestCase { + name: "Direct and conditional mapping #1", + expect: Some(vec![]), + exports_field: exports_field(json!({ + ".": [{ + "browser": "./browser.js" + }, { + "require": "./require.js" + }, { + "import": "./import.mjs" + }] + })), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "Direct and conditional mapping #2", + expect: Some(vec!["./import.mjs"]), + exports_field: exports_field(json!({ + ".": [{ + "browser": "./browser.js" + }, { + "require": "./require.js" + }, { + "import": "./import.mjs" + }] + })), + request: ".", + condition_names: vec!["import"], + }, + TestCase { + name: "Direct and conditional mapping #3", + expect: Some(vec!["./require.js"]), + exports_field: exports_field(json!({ + ".": [ + { + "browser": "./browser.js" + }, + { + "require": "./require.js" + }, + { + "import": "./import.mjs" + } + ] + })), + request: ".", + condition_names: vec!["import", "require"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "Direct and conditional mapping #3", + expect: Some(vec!["./import.mjs"]), + exports_field: exports_field(json!({ + ".": [{ + "browser": "./browser.js" + }, { + "import": "./import.mjs" + }] + })), + request: ".", + condition_names: vec!["import", "require"], + }, + TestCase { + name: "Direct and conditional mapping #4", + expect: Some(vec!["./require.js"]), + exports_field: exports_field(json!({ + ".": [{ + "browser": "./browser.js" + }, { + "require": [ + "./require.js" + ] + }, { + "import": [ + "./import.mjs", + "./import.js" + ] + }] + })), + request: ".", + condition_names: vec!["import", "require"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "Direct and conditional mapping #4", + expect: Some(vec!["./import.mjs"]), + exports_field: exports_field(json!({ + ".": [ + { + "browser": "./browser.js" + }, + { + "import": [ + "./import.mjs", + "./import.js" + ] + } + ] + })), + request: ".", + condition_names: vec!["import", "require"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "Direct and conditional mapping #4", + expect: Some(vec!["./import.js"]), + exports_field: exports_field(json!({ + ".": [ + { + "browser": "./browser.js" + }, + { + "import": [ + "./import.js" + ] + } + ] + })), + request: ".", + condition_names: vec!["import", "require"], + }, + TestCase { + name: "mapping to a folder root #1", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./timezones": "./data/timezones/" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #2", + expect: None, + exports_field: exports_field(json!({ + "./timezones/": "./data/timezones" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #3", + expect: Some(vec!["./data/timezones/pdt/index.mjs"]), + exports_field: exports_field(json!({ + "./timezones/pdt/": "./data/timezones/pdt/" + })), + request: "./timezones/pdt/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #3 (wildcard)", + expect: Some(vec!["./data/timezones/pdt/index.mjs"]), + exports_field: exports_field(json!({ + "./timezones/pdt/*": "./data/timezones/pdt/*" + })), + request: "./timezones/pdt/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #4", + expect: Some(vec!["./timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./": "./timezones/" + })), + request: "./pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #4 (wildcard)", + expect: Some(vec!["./timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./*": "./timezones/*" + })), + request: "./pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #5", + expect: Some(vec!["./timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./": "./" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #5 (wildcard)", + expect: Some(vec!["./timezones/pdt.mjs"]), + exports_field: exports_field(json!({ + "./*": "./*" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #6", + expect: None, + exports_field: exports_field(json!({ + "./": "." + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #6 (wildcard)", + expect: None, + exports_field: exports_field(json!({ + "./*": "." + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #7", + expect: Some(vec![]), + exports_field: exports_field(json!({ + ".": "./" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "mapping to a folder root #7 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + ".": "./*" + })), + request: "./timezones/pdt.mjs", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #1", + expect: Some(vec!["./lib/index.mjs"]), + exports_field: exports_field(json!({ + "./": "./", + "./dist/": "./lib/" + })), + request: "./dist/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #1 (wildcard)", + expect: Some(vec!["./lib/index.mjs"]), + exports_field: exports_field(json!({ + "./*": "./*", + "./dist/*": "./lib/*" + })), + request: "./dist/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #2", + expect: Some(vec!["./dist/utils/index.js"]), + exports_field: exports_field(json!({ + "./dist/utils/": "./dist/utils/", + "./dist/": "./lib/" + })), + request: "./dist/utils/index.js", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #2 (wildcard)", + expect: Some(vec!["./dist/utils/index.js"]), + exports_field: exports_field(json!({ + "./dist/utils/*": "./dist/utils/*", + "./dist/*": "./lib/*" + })), + request: "./dist/utils/index.js", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #3", + expect: Some(vec!["./dist/utils/index.js"]), + exports_field: exports_field(json!({ + "./dist/utils/index.js": "./dist/utils/index.js", + "./dist/utils/": "./dist/utils/index.mjs", + "./dist/": "./lib/" + })), + request: "./dist/utils/index.js", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #3 (wildcard)", + expect: Some(vec!["./dist/utils/index.js"]), + exports_field: exports_field(json!({ + "./dist/utils/index.js": "./dist/utils/index.js", + "./dist/utils/*": "./dist/utils/index.mjs", + "./dist/*": "./lib/*" + })), + request: "./dist/utils/index.js", + condition_names: vec![], + }, + TestCase { + name: "the longest matching path prefix is prioritized #4", + expect: Some(vec!["./lib/index.mjs"]), + exports_field: exports_field(json!({ + "./": { + "browser": "./browser/" + }, + "./dist/": "./lib/" + })), + request: "./dist/index.mjs", + condition_names: vec!["browser"], + }, + TestCase { + name: "the longest matching path prefix is prioritized #4 (wildcard)", + expect: Some(vec!["./lib/index.mjs"]), + exports_field: exports_field(json!({ + "./*": { + "browser": "./browser/*" + }, + "./dist/*": "./lib/*" + })), + request: "./dist/index.mjs", + condition_names: vec!["browser"], + }, + TestCase { + name: "conditional mapping folder #1", + // `lodash/` does not start with './' so fallbacks to util + expect: Some(vec!["./utils/index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": [ + "lodash/", + "./utils/" + ], + "node": [ + "./utils-node/" + ] + } + })), + request: "./utils/index.js", + condition_names: vec!["browser"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "conditional mapping folder #1", + expect: Some(vec!["./utils/index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": [ + "./utils/" + ], + "node": [ + "./utils-node/" + ] + } + })), + request: "./utils/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "conditional mapping folder #1 (wildcard)", + // `lodash/` does not start with './' so fallbacks to util + expect: Some(vec!["./utils/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": [ + "lodash/*", + "./utils/*" + ], + "node": [ + "./utils-node/*" + ] + } + })), + request: "./utils/index.js", + condition_names: vec!["browser"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "conditional mapping folder #1 (wildcard)", + expect: Some(vec!["./utils/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": [ + "./utils/*" + ], + "node": [ + "./utils-node/*" + ] + } + })), + request: "./utils/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "conditional mapping folder #2", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/": { + "webpack": "./wpk/", + "browser": [ + "lodash/", + "./utils/" + ], + "node": [ + "./node/" + ] + } + })), + request: "./utils/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "conditional mapping folder #2 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/*": { + "webpack": "./wpk/*", + "browser": [ + "lodash/*", + "./utils/*" + ], + "node": [ + "./node/*" + ] + } + })), + request: "./utils/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "conditional mapping folder #3", + expect: Some(vec!["./wpk/index.mjs"]), + exports_field: exports_field(json!({ + "./utils/": { + "webpack": "./wpk/", + "browser": [ + "lodash/", + "./utils/" + ], + "node": [ + "./utils/" + ] + } + })), + request: "./utils/index.mjs", + condition_names: vec!["browser", "webpack"], + }, + TestCase { + name: "conditional mapping folder #3 (wildcard)", + expect: Some(vec!["./wpk/index.mjs"]), + exports_field: exports_field(json!({ + "./utils/*": { + "webpack": "./wpk/*", + "browser": [ + "lodash/*", + "./utils/*" + ], + "node": [ + "./utils/*" + ] + } + })), + request: "./utils/index.mjs", + condition_names: vec!["browser", "webpack"], + }, + TestCase { + name: "incorrect exports field #1", + expect: None, + exports_field: exports_field(json!({ + "/utils/": "./a/" + })), + request: "./utils/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "incorrect exports field #2", + expect: None, + exports_field: exports_field(json!({ + "./utils/": "/a/" + })), + request: "./utils/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "incorrect exports field #3", + expect: None, + exports_field: exports_field(json!({ + "/utils/": { + "browser": "./a/", + "default": "./b/" + } + })), + request: "./utils/index.mjs", + condition_names: vec!["browser"], + }, + TestCase { + name: "incorrect exports field #4", + expect: None, + exports_field: exports_field(json!({ + "./utils/": { + "browser": "/a/", + "default": "/b/" + } + })), + request: "./utils/index.mjs", + condition_names: vec!["browser"], + }, + TestCase { + name: "incorrect exports field #4 (wildcard)", + expect: None, + exports_field: exports_field(json!({ + "./utils/*": { + "browser": "/a/", + "default": "/b/" + } + })), + request: "./utils/index.mjs", + condition_names: vec!["browser"], + }, + TestCase { + name: "incorrect exports field #5", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/index": "./a/index.js" + })), + request: "./utils/index.mjs", + condition_names: vec![], + }, + TestCase { + name: "incorrect exports field #6", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/index.mjs": "./a/index.js" + })), + request: "./utils/index", + condition_names: vec![], + }, + TestCase { + name: "incorrect exports field #7", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/index": { + "browser": "./a/index.js", + "default": "./b/index.js" + } + })), + request: "./utils/index.mjs", + condition_names: vec!["browser"], + }, + TestCase { + name: "incorrect exports field #8", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/index.mjs": { + "browser": "./a/index.js", + "default": "./b/index.js" + } + })), + request: "./utils/index", + condition_names: vec!["browser"], + }, + // Requests that are not `./` does not apply to `package_exports_resolve` + // TestCase { + // name: "incorrect request #1", + // expect: None, + // exports_field: exports_field(json!({ + // "./utils/": "./a/" + // })), + // request: "/utils/index.mjs", + // condition_names: vec![], + // }, + // TestCase { + // name: "incorrect request #2", + // expect: None, + // exports_field: exports_field(json!({ + // "./utils/": { + // "browser": "./a/", + // "default": "./b/" + // } + // })), + // request: "/utils/index.mjs", + // condition_names: vec!["browser"], + // }, + // TestCase { + // name: "incorrect request #3", + // expect: None, + // exports_field: exports_field(json!({ + // "./utils/": { + // "browser": "./a/", + // "default": "./b/" + // } + // })), + // request: "../utils/index.mjs", + // condition_names: vec!["browser"], + // }, + // TestCase { + // name: "incorrect request #4", + // expect: None, + // exports_field: exports_field(json!({ + // "./utils/": { + // "browser": "./a/", + // "default": "./b/" + // } + // })), + // request: "/utils/index.mjs/", + // condition_names: vec!["browser"], + // }, + TestCase { + name: "backtracking package base #1", + expect: Some(vec!["./dist/index"]), + exports_field: exports_field(json!({ + "./../../utils/": "./dist/" + })), + request: "./../../utils/index", + condition_names: vec![], + }, + TestCase { + name: "backtracking package base #1 (wildcard)", + expect: Some(vec!["./dist/index"]), + exports_field: exports_field(json!({ + "./../../utils/*": "./dist/*" + })), + request: "./../../utils/index", + condition_names: vec![], + }, + TestCase { + name: "backtracking package base #2", + expect: None, + exports_field: exports_field(json!({ + "../../utils/": "./dist/" + })), + request: "../../utils/index", + condition_names: vec![], + }, + TestCase { + name: "backtracking package base #2 (wildcard)", + expect: None, + exports_field: exports_field(json!({ + "../../utils/*": "./dist/*" + })), + request: "../../utils/index", + condition_names: vec![], + }, + TestCase { + name: "backtracking package base #3", + expect: None, + exports_field: exports_field(json!({ + "./utils/": "../src/" + })), + request: "./utils/index", + condition_names: vec![], + }, + TestCase { + name: "backtracking package base #3 (wildcard)", + expect: None, + exports_field: exports_field(json!({ + "./utils/*": "../src/*" + })), + request: "./utils/index", + condition_names: vec![], + }, + // enhanced_resolve does not handle backtracking here + // TestCase { + // name: "backtracking package base #4", + // expect: Some(vec!["./../src/index"]), + // exports_field: exports_field(json!({ + // "./utils/": "./../src/" + // })), + // request: "./utils/index", + // condition_names: vec![], + // }, + // TestCase { + // name: "backtracking package base #4 (wildcard)", + // expect: Some(vec!["./../src/index"]), + // exports_field: exports_field(json!({ + // "./utils/*": "./../src/*" + // })), + // request: "./utils/index", + // condition_names: vec![], + // }, + // TestCase { + // name: "backtracking package base #5", + // expect: Some(vec!["./src/../index.js"]), + // exports_field: exports_field(json!({ + // "./utils/index": "./src/../index.js" + // })), + // request: "./utils/index", + // condition_names: vec![], + // }, + TestCase { + name: "backtracking package base #6", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/../utils/index": "./src/../index.js" + })), + request: "./utils/index", + condition_names: vec![], + }, + TestCase { + name: "backtracking package base #7", + expect: None, + exports_field: exports_field(json!({ + "./utils/": { + "browser": "../this/" + } + })), + request: "./utils/index", + condition_names: vec!["browser"], + }, + TestCase { + name: "backtracking package base #7", + expect: None, + exports_field: exports_field(json!({ + "./utils/*": { + "browser": "../this/*" + } + })), + request: "./utils/index", + condition_names: vec!["browser"], + }, + TestCase { + name: "backtracking package base #8", + // We throw "InvalidPackageTarget" + // expect: Some(vec!["./utils/../index"]), + expect: None, + exports_field: exports_field(json!({ + "./utils/": { + "browser": "./utils/../" + } + })), + request: "./utils/index", + condition_names: vec!["browser"], + }, + TestCase { + name: "backtracking package base #8 (wildcard)", + // We throw "InvalidPackageTarget" + // expect: Some(vec!["./utils/../index"]), + expect: None, + exports_field: exports_field(json!({ + "./utils/*": { + "browser": "./utils/../*" + } + })), + request: "./utils/index", + condition_names: vec!["browser"], + }, + TestCase { + name: "backtracking package base #9", + expect: Some(vec!["./dist/index"]), + exports_field: exports_field(json!({ + "./": "./src/../../", + "./dist/": "./dist/" + })), + request: "./dist/index", + condition_names: vec!["browser"], + }, + TestCase { + name: "backtracking package base #9 (wildcard)", + expect: Some(vec!["./dist/index"]), + exports_field: exports_field(json!({ + "./*": "./src/../../*", + "./dist/*": "./dist/*" + })), + request: "./dist/index", + condition_names: vec!["browser"], + }, + TestCase { + name: "backtracking target folder #1", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./dist/timezone/../../index"]), + exports_field: exports_field(json!({ + "./utils/": "./dist/" + })), + request: "./utils/timezone/../../index", + condition_names: vec![], + }, + TestCase { + name: "backtracking target folder #1 (wildcard)", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./dist/timezone/../../index"]), + exports_field: exports_field(json!({ + "./utils/*": "./dist/*" + })), + request: "./utils/timezone/../../index", + condition_names: vec![], + }, + TestCase { + name: "backtracking target folder #2", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./dist/timezone/../index"]), + exports_field: exports_field(json!({ + "./utils/": "./dist/" + })), + request: "./utils/timezone/../index", + condition_names: vec![], + }, + TestCase { + name: "backtracking target folder #2 (wildcard)", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./dist/timezone/../index"]), + exports_field: exports_field(json!({ + "./utils/*": "./dist/*" + })), + request: "./utils/timezone/../index", + condition_names: vec![], + }, + TestCase { + name: "backtracking target folder #3", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./dist/target/../../index"]), + exports_field: exports_field(json!({ + "./utils/": "./dist/target/" + })), + request: "./utils/../../index", + condition_names: vec![], + }, + TestCase { + name: "backtracking target folder #3 (wildcard)", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./dist/target/../../index"]), + exports_field: exports_field(json!({ + "./utils/*": "./dist/target/*" + })), + request: "./utils/../../index", + condition_names: vec![], + }, + // enhanced-resolve does not handle `node_modules` in target + TestCase { + name: "nested node_modules path #1", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./node_modules/lodash/dist/index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": "./node_modules/" + } + })), + request: "./utils/lodash/dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "nested node_modules path #1 (wildcard)", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./node_modules/lodash/dist/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": "./node_modules/*" + } + })), + request: "./utils/lodash/dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "nested node_modules path #2", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./utils/../node_modules/lodash/dist/index.js"]), + exports_field: exports_field(json!({ + "./utils/": "./utils/../node_modules/" + })), + request: "./utils/lodash/dist/index.js", + condition_names: vec![], + }, + TestCase { + name: "nested node_modules path #2 (wildcard)", + // We return InvalidPackageTarget + expect: None, + // expect: Some(vec!["./utils/../node_modules/lodash/dist/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": "./utils/../node_modules/*" + })), + request: "./utils/lodash/dist/index.js", + condition_names: vec![], + }, + TestCase { + name: "nested mapping #1", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": "./", + "default": { + "node": "./node/" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "nested mapping #1 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": "./*", + "default": { + "node": "./node/*" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "nested mapping #2", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./", + "./node/" + ], + "default": { + "node": "./node/" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "webpack"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "nested mapping #2", + expect: Some(vec!["./node/index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./node/" + ], + "default": { + "node": "./node/" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "webpack"], + }, + TestCase { + name: "nested mapping #2 (wildcard)", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./*", + "./node/*" + ], + "default": { + "node": "./node/*" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "webpack"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "nested mapping #2 (wildcard)", + expect: Some(vec!["./node/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./node/*" + ], + "default": { + "node": "./node/*" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "webpack"], + }, + TestCase { + name: "nested mapping #3", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./", + "./node/" + ], + "default": { + "node": "./node/" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["webpack"], + }, + TestCase { + name: "nested mapping #3 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./*", + "./node/*" + ], + "default": { + "node": "./node/*" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["webpack"], + }, + TestCase { + name: "nested mapping #4", + expect: Some(vec!["./node/index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./", + "./node/" + ], + "default": { + "node": "./node/" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["node", "browser"], + }, + TestCase { + name: "nested mapping #4 (wildcard)", + expect: Some(vec!["./node/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./*", + "./node/*" + ], + "default": { + "node": "./node/*" + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["node", "browser"], + }, + TestCase { + name: "nested mapping #5", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./", + "./node/" + ], + "default": { + "node": { + "webpack": [ + "./wpck/" + ] + } + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "node"], + }, + TestCase { + name: "nested mapping #5 (wildcard)", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./*", + "./node/*" + ], + "default": { + "node": { + "webpack": [ + "./wpck/*" + ] + } + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "node"], + }, + TestCase { + name: "nested mapping #6", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./", + "./node/" + ], + "default": { + "node": { + "webpack": [ + "./wpck/" + ] + } + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "node", "webpack"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "nested mapping #6", + expect: Some(vec!["./node/index.js"]), + exports_field: exports_field(json!({ + "./utils/": { + "browser": { + "webpack": [ + "./node/" + ], + "default": { + "node": { + "webpack": [ + "./wpck/" + ] + } + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "node", "webpack"], + }, + TestCase { + name: "nested mapping #6 (wildcard)", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./*", + "./node/*" + ], + "default": { + "node": { + "webpack": [ + "./wpck/*" + ] + } + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "node", "webpack"], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "nested mapping #6 (wildcard)", + expect: Some(vec!["./node/index.js"]), + exports_field: exports_field(json!({ + "./utils/*": { + "browser": { + "webpack": [ + "./node/*" + ], + "default": { + "node": { + "webpack": [ + "./wpck/*" + ] + } + } + } + } + })), + request: "./utils/index.js", + condition_names: vec!["browser", "node", "webpack"], + }, + TestCase { + name: "nested mapping #7", + expect: Some(vec!["./y.js"]), + exports_field: exports_field(json!({ + "./a.js": { + "abc": { + "def": "./x.js" + }, + "ghi": "./y.js" + } + })), + request: "./a.js", + condition_names: vec!["abc", "ghi"], + }, + TestCase { + name: "nested mapping #8", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "./a.js": { + "abc": { + "def": "./x.js", + "default": [] + }, + "ghi": "./y.js" + } + })), + request: "./a.js", + condition_names: vec!["abc", "ghi"], + }, + TestCase { + name: "syntax sugar #1", + expect: Some(vec!["./main.js"]), + exports_field: exports_field(json!("./main.js")), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "syntax sugar #2", + expect: Some(vec![]), + exports_field: exports_field(json!("./main.js")), + request: "./lib.js", + condition_names: vec![], + }, + TestCase { + name: "syntax sugar #3", + expect: Some(vec!["./a.js"]), + exports_field: exports_field(json!(["./a.js", "./b.js"])), + request: ".", + condition_names: vec![], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "syntax sugar #3", + expect: Some(vec!["./b.js"]), + exports_field: exports_field(json!(["./b.js"])), + request: ".", + condition_names: vec![], + }, + TestCase { + name: "syntax sugar #4", + expect: Some(vec![]), + exports_field: exports_field(json!(["./a.js", "./b.js"])), + request: "./lib.js", + condition_names: vec![], + }, + TestCase { + name: "syntax sugar #5", + expect: Some(vec!["./index.js"]), + exports_field: exports_field(json!({ + "browser": { + "default": "./index.js" + } + })), + request: ".", + condition_names: vec!["browser"], + }, + TestCase { + name: "syntax sugar #6", + expect: Some(vec![]), + exports_field: exports_field(json!({ + "browser": { + "default": "./index.js" + } + })), + request: "./lib.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "syntax sugar #7", + expect: None, + exports_field: exports_field(json!({ + "./node": "./node.js", + "browser": { + "default": "./index.js" + } + })), + request: ".", + condition_names: vec!["browser"], + }, + TestCase { + name: "syntax sugar #8", + expect: None, + exports_field: exports_field(json!({ + "browser": { + "default": "./index.js" + }, + "./node": "./node.js" + })), + request: ".", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard longest #1", + expect: Some(vec!["./abc/d"]), + exports_field: exports_field(json!({ + "./ab*": "./ab/*", + "./abc*": "./abc/*", + "./a*": "./a/*" + })), + request: "./abcd", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard longest #2", + expect: Some(vec!["./abc/d/e"]), + exports_field: exports_field(json!({ + "./ab*": "./ab/*", + "./abc*": "./abc/*", + "./a*": "./a/*" + })), + request: "./abcd/e", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard longest #3", + expect: Some(vec!["./abc/d"]), + exports_field: exports_field(json!({ + "./x/ab*": "./ab/*", + "./x/abc*": "./abc/*", + "./x/a*": "./a/*" + })), + request: "./x/abcd", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard longest #4", + expect: Some(vec!["./abc/d/e"]), + exports_field: exports_field(json!({ + "./x/ab*": "./ab/*", + "./x/abc*": "./abc/*", + "./x/a*": "./a/*" + })), + request: "./x/abcd/e", + condition_names: vec!["browser"], + }, + TestCase { + name: "path tree edge case #1", + expect: Some(vec!["./A/b/d.js"]), + exports_field: exports_field(json!({ + "./a/": "./A/", + "./a/b/c": "./c.js" + })), + request: "./a/b/d.js", + condition_names: vec![], + }, + TestCase { + name: "path tree edge case #1 (wildcard)", + expect: Some(vec!["./A/b/d.js"]), + exports_field: exports_field(json!({ + "./a/*": "./A/*", + "./a/b/c": "./c.js" + })), + request: "./a/b/d.js", + condition_names: vec![], + }, + TestCase { + name: "path tree edge case #2", + expect: Some(vec!["./A/c.js"]), + exports_field: exports_field(json!({ + "./a/": "./A/", + "./a/b": "./b.js" + })), + request: "./a/c.js", + condition_names: vec![], + }, + TestCase { + name: "path tree edge case #2 (wildcard)", + expect: Some(vec!["./A/c.js"]), + exports_field: exports_field(json!({ + "./a/*": "./A/*", + "./a/b": "./b.js" + })), + request: "./a/c.js", + condition_names: vec![], + }, + TestCase { + name: "path tree edge case #3", + expect: Some(vec!["./A/b/d/c.js"]), + exports_field: exports_field(json!({ + "./a/": "./A/", + "./a/b/c/d": "./c.js" + })), + request: "./a/b/d/c.js", + condition_names: vec![], + }, + TestCase { + name: "path tree edge case #3 (wildcard)", + expect: Some(vec!["./A/b/d/c.js"]), + exports_field: exports_field(json!({ + "./a/*": "./A/*", + "./a/b/c/d": "./c.js" + })), + request: "./a/b/d/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #1", + expect: Some(vec!["./A/b.js"]), + exports_field: exports_field(json!({ + "./a/*.js": "./A/*.js" + })), + request: "./a/b.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #2", + expect: Some(vec!["./A/b/c.js"]), + exports_field: exports_field(json!({ + "./a/*.js": "./A/*.js" + })), + request: "./a/b/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #3", + expect: Some(vec!["./A/b/c.js"]), + exports_field: exports_field(json!({ + "./a/*/c.js": "./A/*/c.js" + })), + request: "./a/b/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #4", + expect: Some(vec!["./A/b/b.js"]), + exports_field: exports_field(json!({ + "./a/*/c.js": "./A/*/*.js" + })), + request: "./a/b/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #5", + expect: Some(vec!["./browser/index.js"]), + exports_field: exports_field(json!({ + "./lib/*": { + "browser": [ + "./browser/*" + ] + }, + "./dist/*.js": { + "node": "./*.js", + "default": "./browser/*.js" + } + })), + request: "./dist/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard pattern #5", + expect: Some(vec!["./browser/index.js"]), + exports_field: exports_field(json!({ + "./lib/*": { + "browser": [ + "./browser/*" + ] + }, + "./dist/*.js": { + "node": "./*.js", + "default": "./browser/*.js" + } + })), + request: "./lib/index.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard pattern #6", + expect: Some(vec!["./browser/foo/bar.js"]), + exports_field: exports_field(json!({ + "./lib/*/bar.js": { + "browser": [ + "./browser/*/bar.js" + ] + }, + "./dist/*/bar.js": { + "node": "./*.js", + "default": "./browser/*.js" + } + })), + request: "./lib/foo/bar.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard pattern #6", + expect: Some(vec!["./browser/foo.js"]), + exports_field: exports_field(json!({ + "./lib/*/bar.js": { + "browser": [ + "./browser/*/bar.js" + ] + }, + "./dist/*/bar.js": { + "node": "./*.js", + "default": "./browser/*.js" + } + })), + request: "./dist/foo/bar.js", + condition_names: vec!["browser"], + }, + TestCase { + name: "wildcard pattern #7", + expect: Some(vec!["./browser/foo/default.js"]), + exports_field: exports_field(json!({ + "./lib/*/bar.js": { + "browser": [ + "./browser/*/bar.js" + ] + }, + "./dist/*/bar.js": { + "node": "./*.js", + "default": "./browser/*/default.js" + } + })), + request: "./dist/foo/bar.js", + condition_names: vec!["default"], + }, + TestCase { + name: "wildcard pattern #8", + expect: Some(vec!["./A/b/b/b.js"]), + exports_field: exports_field(json!({ + "./a/*/c.js": "./A/*/*/*.js" + })), + request: "./a/b/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #9", + expect: Some(vec!["./A/b/b/b.js"]), + exports_field: exports_field(json!({ + "./a/*/c.js": [ + "./A/*/*/*.js", + "./B/*/*/*.js" + ] + })), + request: "./a/b/c.js", + condition_names: vec![], + }, + // Duplicated due to not supporting returning an array + TestCase { + name: "wildcard pattern #9", + expect: Some(vec!["./B/b/b/b.js"]), + exports_field: exports_field(json!({ + "./a/*/c.js": [ + "./B/*/*/*.js" + ] + })), + request: "./a/b/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #10", + expect: Some(vec!["./A/b/b/b.js"]), + exports_field: exports_field(json!({ + "./a/foo-*/c.js": "./A/*/*/*.js" + })), + request: "./a/foo-b/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #11", + expect: Some(vec!["./A/b/b/b.js"]), + exports_field: exports_field(json!({ + "./a/*-foo/c.js": "./A/*/*/*.js" + })), + request: "./a/b-foo/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #12", + expect: Some(vec!["./A/b/b/b.js"]), + exports_field: exports_field(json!({ + "./a/foo-*-foo/c.js": "./A/*/*/*.js" + })), + request: "./a/foo-b-foo/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #13", + expect: Some(vec!["./A/b/c/d.js"]), + exports_field: exports_field(json!({ + "./a/foo-*-foo/c.js": "./A/b/c/d.js" + })), + request: "./a/foo-b-foo/c.js", + condition_names: vec![], + }, + TestCase { + name: "wildcard pattern #14", + expect: Some(vec!["./A/b/c/*.js"]), + exports_field: exports_field(json!({ + "./a/foo-foo/c.js": "./A/b/c/*.js" + })), + request: "./a/foo-foo/c.js", + condition_names: vec![], + }, + ]; + + for case in test_cases { + let resolved = Resolver::default() + .package_exports_resolve( + Path::new(""), + case.request.trim_start_matches('.'), + &case.exports_field, + &case.condition_names.iter().map(ToString::to_string).collect::>(), + ) + .map(|p| p.map(|p| p.to_path_buf())); + if let Some(expect) = case.expect { + if expect.is_empty() { + assert!( + matches!(resolved, Err(ResolveError::PackagePathNotExported(_))), + "{} {:?}", + &case.name, + &resolved + ); + } else { + for expect in expect { + assert_eq!( + resolved, + Ok(Some(PathBuf::from(expect).normalize())), + "{}", + &case.name + ); + } + } + } else { + assert!(resolved.is_err(), "{} {resolved:?}", &case.name); + } + } +}