Skip to content

Commit d541f00

Browse files
Boshenclaude
andcommitted
feat: improve PackagePathNotExported error message with condition names
Improves the error message to match enhanced-resolve behavior from commit f1bc1c2, providing better developer experience when debugging export resolution failures. Changes: - Updated `ResolveError::PackagePathNotExported` to include condition names - Added `ConditionNames` type with custom Display formatting - Error message now shows which conditions were checked Before: Package subpath './foo' is not defined by "exports" in /path/to/package.json After: "./foo" is not exported under the conditions ["node", "import"] from package /path/to (see exports field in /path/to/package.json) Fixes: Cross-reference with enhanced-resolve #460 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 94406bd commit d541f00

File tree

3 files changed

+50
-16
lines changed

3 files changed

+50
-16
lines changed

src/error.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,13 @@ pub enum ResolveError {
8888
#[error(r#"Invalid "exports" target "{0}" defined for '{1}' in the package config {2}"#)]
8989
InvalidPackageTarget(String, String, PathBuf),
9090

91-
#[error(r#"Package subpath '{0}' is not defined by "exports" in {1}"#)]
92-
PackagePathNotExported(String, PathBuf),
91+
#[error(r#""{subpath}" is not exported under {conditions} from package {package_path} (see exports field in {package_json_path})"#)]
92+
PackagePathNotExported {
93+
subpath: String,
94+
package_path: PathBuf,
95+
package_json_path: PathBuf,
96+
conditions: ConditionNames,
97+
},
9398

9499
#[error(r#"Invalid package config "{0}", "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."#)]
95100
InvalidPackageConfig(PathBuf),
@@ -201,6 +206,31 @@ impl From<Vec<PathBuf>> for CircularPathBufs {
201206
}
202207
}
203208

209+
/// Helper type for formatting condition names in error messages
210+
#[derive(Debug, Clone, PartialEq, Eq)]
211+
pub struct ConditionNames(Vec<String>);
212+
213+
impl From<Vec<String>> for ConditionNames {
214+
fn from(conditions: Vec<String>) -> Self {
215+
Self(conditions)
216+
}
217+
}
218+
219+
impl Display for ConditionNames {
220+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221+
match self.0.len() {
222+
0 => write!(f, "no conditions"),
223+
1 => write!(f, "the condition \"{}\"", self.0[0]),
224+
_ => {
225+
write!(f, "the conditions ")?;
226+
let conditions_str =
227+
self.0.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(", ");
228+
write!(f, "[{conditions_str}]")
229+
}
230+
}
231+
}
232+
}
233+
204234
#[test]
205235
fn test_into_io_error() {
206236
use std::io::{self, ErrorKind};

src/lib.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,10 +1718,12 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
17181718
}
17191719
}
17201720
// 4. Throw a Package Path Not Exported error.
1721-
Err(ResolveError::PackagePathNotExported(
1722-
subpath.to_string(),
1723-
package_url.path().join("package.json"),
1724-
))
1721+
Err(ResolveError::PackagePathNotExported {
1722+
subpath: subpath.to_string(),
1723+
package_path: package_url.path().to_path_buf(),
1724+
package_json_path: package_url.path().join("package.json"),
1725+
conditions: self.options.condition_names.clone().into(),
1726+
})
17251727
}
17261728

17271729
/// PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
@@ -1977,10 +1979,12 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
19771979
// 1. If _target.length is zero, return null.
19781980
if targets.is_empty() {
19791981
// Note: return PackagePathNotExported has the same effect as return because there are no matches.
1980-
return Err(ResolveError::PackagePathNotExported(
1981-
pattern_match.unwrap_or(".").to_string(),
1982-
package_url.path().join("package.json"),
1983-
));
1982+
return Err(ResolveError::PackagePathNotExported {
1983+
subpath: pattern_match.unwrap_or(".").to_string(),
1984+
package_path: package_url.path().to_path_buf(),
1985+
package_json_path: package_url.path().join("package.json"),
1986+
conditions: self.options.condition_names.clone().into(),
1987+
});
19841988
}
19851989
// 2. For each item targetValue in target, do
19861990
for (i, target_value) in targets.iter().enumerate() {

src/tests/exports_field.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ fn test_simple() {
7171
("relative path should not work with exports field", f.clone(), "./node_modules/exports-field/dist/main.js", ResolveError::NotFound("./node_modules/exports-field/dist/main.js".into())),
7272
("backtracking should not work for request", f.clone(), "exports-field/dist/../../../a.js", ResolveError::InvalidPackageTarget("./lib/../../../a.js".to_string(), "./dist/".to_string(), p.clone())),
7373
("backtracking should not work for exports field target", f.clone(), "exports-field/dist/a.js", ResolveError::InvalidPackageTarget("./../../a.js".to_string(), "./dist/a.js".to_string(), p.clone())),
74-
("not exported error", f.clone(), "exports-field/anything/else", ResolveError::PackagePathNotExported("./anything/else".to_string(), p.clone())),
75-
("request ending with slash #1", f.clone(), "exports-field/", ResolveError::PackagePathNotExported("./".to_string(), p.clone())),
76-
("request ending with slash #2", f.clone(), "exports-field/dist/", ResolveError::PackagePathNotExported("./dist/".to_string(), p.clone())),
77-
("request ending with slash #3", f.clone(), "exports-field/lib/", ResolveError::PackagePathNotExported("./lib/".to_string(), p)),
74+
("not exported error", f.clone(), "exports-field/anything/else", ResolveError::PackagePathNotExported { subpath: "./anything/else".to_string(), package_path: f.join("node_modules/exports-field"), package_json_path: p.clone(), conditions: vec!["webpack".into()].into() }),
75+
("request ending with slash #1", f.clone(), "exports-field/", ResolveError::PackagePathNotExported { subpath: "./".to_string(), package_path: f.join("node_modules/exports-field"), package_json_path: p.clone(), conditions: vec!["webpack".into()].into() }),
76+
("request ending with slash #2", f.clone(), "exports-field/dist/", ResolveError::PackagePathNotExported { subpath: "./dist/".to_string(), package_path: f.join("node_modules/exports-field"), package_json_path: p.clone(), conditions: vec!["webpack".into()].into() }),
77+
("request ending with slash #3", f.clone(), "exports-field/lib/", ResolveError::PackagePathNotExported { subpath: "./lib/".to_string(), package_path: f.join("node_modules/exports-field"), package_json_path: p, conditions: vec!["webpack".into()].into() }),
7878
("should throw error if target is invalid", f4, "exports-field", ResolveError::InvalidPackageTarget("./a/../b/../../pack1/index.js".to_string(), ".".to_string(), p4)),
7979
("throw error if exports field is invalid", f.clone(), "invalid-exports-field", ResolveError::InvalidPackageConfig(f.join("node_modules/invalid-exports-field/package.json"))),
80-
("should throw error if target is 'null'", f5, "m/features/internal/file.js", ResolveError::PackagePathNotExported("./features/internal/file.js".to_string(), p5)),
80+
("should throw error if target is 'null'", f5.clone(), "m/features/internal/file.js", ResolveError::PackagePathNotExported { subpath: "./features/internal/file.js".to_string(), package_path: f5.join("node_modules/m"), package_json_path: p5, conditions: vec!["webpack".into()].into() }),
8181
];
8282

8383
for (comment, path, request, error) in fail {
@@ -2560,7 +2560,7 @@ fn test_cases() {
25602560
if let Some(expect) = case.expect {
25612561
if expect.is_empty() {
25622562
assert!(
2563-
matches!(resolved_path, Err(ResolveError::PackagePathNotExported(_, _))),
2563+
matches!(resolved_path, Err(ResolveError::PackagePathNotExported { .. })),
25642564
"{} {:?}",
25652565
&case.name,
25662566
&resolved_path

0 commit comments

Comments
 (0)