use std::collections::HashSet; use std::fmt; use std::path::PathBuf; use indexmap::IndexMap; use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; /// The parsed representation of the "exports" field in a package.json file. /// See https://nodejs.org/api/packages.html#package-entry-points for syntax. #[derive(Debug)] pub struct Exports { subpaths: IndexMap, } impl Exports { /// Resolves a relative path to candidate target file paths, in priority order. /// The caller should use the first candidate that exists. /// /// As in Node and TypeScript, an exact key wins, and otherwise the most specific /// matching pattern does. That entry alone decides the result: if it gives no /// target, the path isn't exported and less specific patterns aren't tried. pub fn resolve_import_path(&self, rel_path: &str, conditions: &HashSet<&str>) -> Vec { let mut best: Option<(&str, &Subpath, &str)> = None; for (key, subpath) in self.subpaths.iter() { match candidate_matches(key, rel_path) { None => continue, Some(Match::Exact) => { return match subpath.matches(conditions) { Some(SubpathMatch::Targets(paths)) => { paths.into_iter().map(PathBuf::from).collect() } Some(SubpathMatch::Exclude) | None => vec![], }; } Some(Match::Wildcard { replacement }) => { let more_specific = match best { None => true, Some((best_key, ..)) => { pattern_specificity(key) > pattern_specificity(best_key) } }; if more_specific { best = Some((key, subpath, replacement)); } } } } let Some((_, subpath, replacement)) = best else { return vec![]; }; match subpath.matches(conditions) { Some(SubpathMatch::Targets(paths)) => paths .into_iter() .map(|path| path.replace('*', replacement).into()) .collect(), Some(SubpathMatch::Exclude) | None => vec![], } } } /// Orders pattern keys like Node's PATTERN_KEY_COMPARE: a longer prefix before the /// "*" is more specific, then a longer key. fn pattern_specificity(key: &str) -> (usize, usize) { (key.find('*').unwrap_or(key.len()), key.len()) } enum Match<'a> { Exact, Wildcard { replacement: &'a str }, } fn candidate_matches<'a>(candidate: &str, rel_path: &'a str) -> Option> { let candidate = match (candidate, rel_path) { (".", "") => return Some(Match::Exact), (".", _) => return None, (_, _) => { // Strip "./" prefix. let candidate = candidate.strip_prefix("./").unwrap_or(candidate); if candidate == rel_path { return Some(Match::Exact); } candidate } }; if let Some(idx) = candidate.find('*') { let (prefix, suffix) = candidate.split_at(idx); // Strip the "*" from the suffix. let suffix = &suffix[1..]; // The length check keeps the prefix and suffix from overlapping. if rel_path.len() >= prefix.len() + suffix.len() && rel_path.starts_with(prefix) && rel_path.ends_with(suffix) { // Get the middle part of the path, to be injected into the result. let replacement = &rel_path[prefix.len()..(rel_path.len() - suffix.len())]; return Some(Match::Wildcard { replacement }); } } None } #[derive(Debug, PartialEq, Eq)] enum Subpath { Target(String), Conditions(IndexMap), Exclude, } enum SubpathMatch<'a> { /// Candidate targets, in priority order. The first one that exists wins. Targets(Vec<&'a str>), Exclude, } impl Subpath { /// Returns the targets this subpath maps to under the active conditions. /// /// Like TypeScript, a matching condition doesn't end the search: the targets of /// later matching conditions are kept as fallbacks, in object order. So when a /// "types" target doesn't exist, the "import" target is tried next. /// A null under a condition gives no target, at any nesting level; only a /// subpath that maps directly to null is excluded. fn matches(&self, active_conditions: &HashSet<&str>) -> Option> { match self { Subpath::Target(path) => Some(SubpathMatch::Targets(vec![path])), Subpath::Exclude => Some(SubpathMatch::Exclude), Subpath::Conditions(conds) => { let mut targets = Vec::new(); for (cond, subpath) in conds.iter() { if !active_conditions.contains(cond.as_str()) { continue; } // A null target, or nested conditions with no match, gives // nothing: try the next condition. if let Some(SubpathMatch::Targets(paths)) = subpath.matches(active_conditions) { targets.extend(paths); } } if targets.is_empty() { None } else { Some(SubpathMatch::Targets(targets)) } } } } } // The "exports" value can be defined in lots of ways. // // - A single subpath, e.g. "exports": "./index.js". // Syntactic sugar for {".": "./index.js"} // // - A condition spec: {"node": "./index.node.js"} // Conditions can be nested: {"node": {"import": "./index.node.js", "default": "./index.js"}} // // - An ordered map of subpaths key-value pairs: {".": "./index.js", "./foo/*": "./foo/*.js"} // - Each subpath value can be one of: // - A single target path, e.g. "./index.js" (possibly with wildcards, "./foo/*.js") // - A condition spec. // - Null, indicating the subpath is not exported (overriding other exports that may match). impl<'de> Deserialize<'de> for Exports { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct StringOrMap; impl<'de> Visitor<'de> for StringOrMap { type Value = Exports; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_str(self, value: &str) -> Result where E: de::Error, { let mut subpaths = IndexMap::::new(); subpaths.insert(".".into(), Subpath::Target(value.to_string())); Ok(Exports { subpaths }) } fn visit_map(self, mut access: M) -> Result where M: MapAccess<'de>, { let mut subpaths = IndexMap::new(); // Peek at the first entry to decide whether it's a map of subpaths or conditions. let Some((key, value)) = access.next_entry::()? else { // Empty map. return Ok(Exports { subpaths }); }; if !key.starts_with('.') { let mut conditions: IndexMap = IndexMap::new(); conditions.insert(key, value); let rest: IndexMap = Deserialize::deserialize(de::value::MapAccessDeserializer::new(access))?; conditions.extend(rest); subpaths.insert(".".to_string(), Subpath::Conditions(conditions)); return Ok(Exports { subpaths }); } subpaths.insert(key, value); while let Some((key, value)) = access.next_entry::()? { subpaths.insert(key, value); } Ok(Exports { subpaths }) } } deserializer.deserialize_any(StringOrMap) } } struct SubpathVisitor; impl<'de> Visitor<'de> for SubpathVisitor { type Value = Subpath; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_unit(self) -> Result where E: de::Error, { Ok(Subpath::Exclude) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(Subpath::Target(value.to_string())) } fn visit_map(self, map: M) -> Result where M: MapAccess<'de>, { let conditions: IndexMap = Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?; Ok(Subpath::Conditions(conditions)) } } impl<'de> Deserialize<'de> for Subpath { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_any(SubpathVisitor) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_subpaths() { let exports: Exports = serde_json::from_str( r#"{ ".": "./index.js", "./foo/*": "./foo/*.js", "./bar": {"node": "./bar.node.js", "default": "./bar.js"}, "./baz": {"node": "./baz.node.js", "default": null}, "./qux": null }"#, ) .unwrap(); assert_eq!(exports.subpaths.len(), 5); assert_eq!( exports.subpaths.get(".").unwrap(), &Subpath::Target("./index.js".into()) ); assert_eq!( exports.subpaths.get("./foo/*").unwrap(), &Subpath::Target("./foo/*.js".into()) ); assert_eq!( exports.subpaths.get("./bar").unwrap(), &Subpath::Conditions({ let mut map = IndexMap::new(); map.insert("node".to_owned(), Subpath::Target("./bar.node.js".into())); map.insert("default".to_owned(), Subpath::Target("./bar.js".into())); map }) ); assert_eq!( exports.subpaths.get("./baz").unwrap(), &Subpath::Conditions({ let mut map = IndexMap::new(); map.insert("node".to_owned(), Subpath::Target("./baz.node.js".into())); map.insert("default".to_owned(), Subpath::Exclude); map }) ); assert_eq!(exports.subpaths.get("./qux").unwrap(), &Subpath::Exclude); } #[test] fn parse_toplevel_conditions() { let exports: Exports = serde_json::from_str( r#"{ "node": {"import": "./bar.node.js", "default": "./bar.js"}, "default": "./index.js" }"#, ) .unwrap(); assert_eq!(exports.subpaths.len(), 1); assert_eq!( exports.subpaths.get(".").unwrap(), &Subpath::Conditions({ let mut map = IndexMap::new(); map.insert( "node".to_owned(), Subpath::Conditions({ let mut map = IndexMap::new(); map.insert("import".to_owned(), Subpath::Target("./bar.node.js".into())); map.insert("default".to_owned(), Subpath::Target("./bar.js".into())); map }), ); map.insert("default".to_owned(), Subpath::Target("./index.js".into())); map }) ); } #[test] fn toplevel_condition_priority_order() { let exports: Exports = serde_json::from_str( r#"{ "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "default": "./dist/index.js" }"#, ) .unwrap(); let conditions: HashSet<&str> = ["types", "import", "default"].into_iter().collect(); let resolved = exports.resolve_import_path("", &conditions); // "types" must win because it appears first in the JSON. assert_eq!(resolved.first(), Some(&PathBuf::from("./dist/index.d.ts"))); } fn resolve(exports: &str, rel_path: &str, conditions: &[&str]) -> Vec { let exports: Exports = serde_json::from_str(exports).unwrap(); let conditions: HashSet<&str> = conditions.iter().copied().collect(); exports.resolve_import_path(rel_path, &conditions) } fn paths(paths: &[&str]) -> Vec { paths.iter().map(PathBuf::from).collect() } #[test] fn condition_fallback_order() { let exports = r#"{ "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "default": "./dist/index.js" }"#; // Object order decides priority, not the order of the active conditions. assert_eq!( resolve(exports, "", &["default", "import", "types"]), paths(&["./dist/index.d.ts", "./dist/index.mjs", "./dist/index.js"]) ); assert_eq!( resolve(exports, "", &["import", "default"]), paths(&["./dist/index.mjs", "./dist/index.js"]) ); } #[test] fn wildcard_types_falls_back_to_import() { // The "./*" entry of @modelcontextprotocol/sdk >= 1.29.0. For "server/mcp.js" the // "types" target is "dist/esm/server/mcp.js.d.ts", which doesn't exist, so the // "import" target must remain a candidate. let exports = r#"{ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" }, "./*": { "types": "./dist/esm/*.d.ts", "import": "./dist/esm/*", "require": "./dist/cjs/*" } }"#; assert_eq!( resolve(exports, "server/mcp.js", &["types", "import", "default"]), paths(&["./dist/esm/server/mcp.js.d.ts", "./dist/esm/server/mcp.js"]) ); } #[test] fn wildcard_with_suffix() { let exports = r#"{"./lib/*.js": "./dist/*.mjs"}"#; assert_eq!( resolve(exports, "lib/a/b.js", &["default"]), paths(&["./dist/a/b.mjs"]) ); assert_eq!(resolve(exports, "lib/a/b.ts", &["default"]), paths(&[])); } #[test] fn nested_conditions_without_match_fall_through() { let exports = r#"{ "node": {"import": "./index.node.mjs"}, "default": "./index.js" }"#; assert_eq!( resolve(exports, "", &["node", "require", "default"]), paths(&["./index.js"]) ); } #[test] fn null_subpath_is_excluded() { let exports = r#"{ "./*": "./dist/*.js", "./internal/*": null }"#; assert_eq!(resolve(exports, "internal/foo", &["default"]), paths(&[])); assert_eq!( resolve(exports, "foo", &["default"]), paths(&["./dist/foo.js"]) ); assert_eq!( resolve(r#"{"./foo": null}"#, "foo", &["default"]), paths(&[]) ); } #[test] fn null_condition_is_skipped() { // Like TypeScript, a null under a condition gives no candidate and the search // goes on, at every nesting level. assert_eq!( resolve( r#"{"./internal/*": {"node": null, "default": "./internal/*.js"}}"#, "internal/foo", &["node", "default"] ), paths(&["./internal/foo.js"]) ); assert_eq!( resolve( r#"{"types": "./index.d.ts", "node": null, "default": "./index.js"}"#, "", &["types", "node", "default"] ), paths(&["./index.d.ts", "./index.js"]) ); assert_eq!( resolve( r#"{ "outer": {"first": "./missing.js", "second": null}, "default": "./fallback.js" }"#, "", &["outer", "first", "second", "default"] ), paths(&["./missing.js", "./fallback.js"]) ); assert_eq!( resolve( r#"{"outer": {"second": null}, "default": "./fallback.js"}"#, "", &["outer", "second", "default"] ), paths(&["./fallback.js"]) ); // Only nulls under the active conditions: nothing to resolve. assert_eq!( resolve(r#"{"node": null}"#, "", &["node", "default"]), paths(&[]) ); } #[test] fn most_specific_pattern_wins() { // As in Node and TypeScript, the most specific matching pattern decides the // result, whatever its position in the object. let conditions = &["node", "default"]; assert_eq!( resolve( r#"{"./*": "./dist/*.js", "./internal/*": "./internal/*.js"}"#, "internal/foo", conditions ), paths(&["./internal/foo.js"]) ); // A longer prefix before the "*" wins; with equal prefixes, the longer key. assert_eq!( resolve( r#"{"./a/*": "./one/*", "./a/*.js": "./two/*.js"}"#, "a/b.js", conditions ), paths(&["./two/b.js"]) ); // If the most specific pattern resolves to nothing, the path isn't exported: // less specific patterns aren't tried. for exports in [ r#"{"./*": "./dist/*.js", "./internal/*": {"node": null}}"#, r#"{"./internal/*": {"node": null}, "./*": "./dist/*.js"}"#, r#"{"./*": "./dist/*.js", "./internal/*": {"browser": "./browser/*.js"}}"#, ] { assert_eq!( resolve(exports, "internal/foo", conditions), paths(&[]), "{exports}" ); } } #[test] fn pattern_prefix_and_suffix_overlap() { assert_eq!( resolve(r#"{"./ab*bc": "./x/*.js"}"#, "abc", &["default"]), paths(&[]) ); assert_eq!( resolve(r#"{"./ab*bc": "./x/*.js"}"#, "abxbc", &["default"]), paths(&["./x/x.js"]) ); } }