pub mod fs;
mod error;
mod manifest;
mod util;
mod zip;
use std::{
collections::hash_map::Entry,
path::{Path, PathBuf},
sync::OnceLock,
};
use fancy_regex::Regex;
pub use crate::{
error::{
BadSpecifier, Error, FailedManifestHydration, MissingDependency, MissingPeerDependency,
UndeclaredDependency,
},
manifest::{Manifest, PackageDependency, PackageInformation, PackageLocator},
};
#[derive(Debug)]
pub enum Resolution {
Resolved(PathBuf, Option<String>),
Skipped,
}
pub struct ResolutionHost {
#[allow(clippy::type_complexity)]
pub find_pnp_manifest: Box<dyn Fn(&Path) -> Result<Option<Manifest>, Error>>,
}
impl Default for ResolutionHost {
fn default() -> ResolutionHost {
ResolutionHost { find_pnp_manifest: Box::new(find_pnp_manifest) }
}
}
#[derive(Default)]
pub struct ResolutionConfig {
pub host: ResolutionHost,
}
fn parse_scoped_package_name(specifier: &str) -> Option<(String, Option<String>)> {
let mut segments = specifier.splitn(3, '/');
let scope = segments.next()?;
let name = segments.next()?;
let package_name = specifier[..scope.len() + name.len() + 1].to_string();
let subpath = segments.next().map(|v| v.to_string());
Some((package_name, subpath))
}
fn parse_global_package_name(specifier: &str) -> Option<(String, Option<String>)> {
let mut segments = specifier.splitn(2, '/');
let name = segments.next()?;
let package_name = name.to_string();
let subpath = segments.next().map(|v| v.to_string());
Some((package_name, subpath))
}
fn via_suffix(ident: &str, specifier: &str) -> String {
if ident != specifier { format!(" (via \"{specifier}\")") } else { String::new() }
}
pub fn parse_bare_identifier(specifier: &str) -> Result<(String, Option<String>), Error> {
let name = if specifier.starts_with('@') {
parse_scoped_package_name(specifier)
} else {
parse_global_package_name(specifier)
};
name.ok_or_else(|| {
Error::BadSpecifier(Box::new(BadSpecifier {
message: String::from("Invalid specifier"),
specifier: specifier.to_string(),
}))
})
}
pub fn find_closest_pnp_manifest_path(path: &Path) -> Option<PathBuf> {
for p in path.ancestors() {
let pnp_path = p.join(".pnp.cjs");
if pnp_path.exists() {
return Some(pnp_path);
}
}
None
}
pub fn load_pnp_manifest(p: &Path) -> Result<Manifest, Error> {
let manifest_content = std::fs::read_to_string(p).map_err(|err| {
Error::FailedManifestHydration(Box::new(FailedManifestHydration {
message: format!(
"We failed to read the content of the manifest.\n\nOriginal error: {err}"
),
manifest_path: p.to_path_buf(),
}))
})?;
static RE: OnceLock<Regex> = OnceLock::new();
let manifest_match =
RE.get_or_init(|| {
Regex::new(
"(const[ \\r\\n]+RAW_RUNTIME_STATE[ \\r\\n]*=[ \\r\\n]*|hydrateRuntimeState\\(JSON\\.parse\\()'"
)
.unwrap()
})
.find(&manifest_content)
.unwrap_or_default()
.ok_or_else(|| Error::FailedManifestHydration(Box::new(FailedManifestHydration {
message: String::from("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?"),
manifest_path: p.to_path_buf(),
})))?;
let iter = manifest_content.chars().skip(manifest_match.end());
let mut json_string = String::default();
let mut escaped = false;
for c in iter {
match c {
'\'' if !escaped => {
break;
}
'\\' if !escaped => {
escaped = true;
}
_ => {
escaped = false;
json_string.push(c);
}
}
}
let mut manifest: Manifest = serde_json::from_str(&json_string)
.map_err(|err| Error::FailedManifestHydration(Box::new(FailedManifestHydration {
message: format!("We failed to parse the PnP data payload as proper JSON; Did you manually edit the file?\n\nOriginal error: {err}"),
manifest_path: p.to_path_buf(),
})))?;
init_pnp_manifest(&mut manifest, p);
Ok(manifest)
}
pub fn init_pnp_manifest(manifest: &mut Manifest, p: &Path) {
manifest.manifest_path = p.to_path_buf();
manifest.manifest_dir = p
.parent()
.unwrap_or_else(|| panic!("Should have a parent directory for path {}", p.display()))
.to_owned();
for (name, ranges) in manifest.package_registry_data.iter_mut() {
for (reference, info) in ranges.iter_mut() {
let package_location = manifest.manifest_dir.join(info.package_location.clone());
let normalized_location = util::normalize_path(package_location.to_string_lossy());
info.package_location = PathBuf::from(normalized_location);
if !info.discard_from_lookup {
manifest.location_trie.insert(
&info.package_location,
PackageLocator { name: name.clone(), reference: reference.clone() },
);
}
}
}
let top_level_pkg = manifest
.package_registry_data
.get("")
.expect("Assertion failed: Should have a top-level name key")
.get("")
.expect("Assertion failed: Should have a top-level range key");
for (name, dependency) in &top_level_pkg.package_dependencies {
if let Entry::Vacant(entry) = manifest.fallback_pool.entry(name.clone()) {
entry.insert(dependency.clone());
}
}
}
pub fn find_pnp_manifest(parent: &Path) -> Result<Option<Manifest>, Error> {
find_closest_pnp_manifest_path(parent).map_or(Ok(None), |p| Ok(Some(load_pnp_manifest(&p)?)))
}
pub fn is_dependency_tree_root<'a>(manifest: &'a Manifest, locator: &'a PackageLocator) -> bool {
manifest.dependency_tree_roots.contains(locator)
}
pub fn find_locator<'a>(manifest: &'a Manifest, path: &Path) -> Option<&'a PackageLocator> {
let rel_path = pathdiff::diff_paths(path, &manifest.manifest_dir).unwrap_or_else(|| {
panic!("Assertion failed: Provided path should be absolute but received {}", path.display())
});
if let Some(regex) = &manifest.ignore_pattern_data {
if regex.0.is_match(&util::normalize_path(rel_path.to_string_lossy())).unwrap() {
return None;
}
}
let path = util::normalize_path(path.to_string_lossy());
manifest.location_trie.get_ancestor_value(&path)
}
pub fn get_package<'a>(
manifest: &'a Manifest,
locator: &PackageLocator,
) -> Result<&'a PackageInformation, Error> {
let references = manifest.package_registry_data.get(&locator.name).unwrap_or_else(|| {
panic!("Should have an entry in the package registry for {}", locator.name)
});
let info = references.get(&locator.reference).unwrap_or_else(|| {
panic!("Should have an entry in the package registry for {}", locator.reference)
});
Ok(info)
}
pub fn is_excluded_from_fallback(manifest: &Manifest, locator: &PackageLocator) -> bool {
manifest
.fallback_exclusion_list
.get(&locator.name)
.is_some_and(|references| references.contains(&locator.reference))
}
pub fn find_broken_peer_dependencies(
_dependency: &str,
_initial_package: &PackageLocator,
) -> Vec<PackageLocator> {
Vec::new()
}
pub fn resolve_to_unqualified_via_manifest(
manifest: &Manifest,
specifier: &str,
parent: &Path,
) -> Result<Resolution, Error> {
let (ident, module_path) = parse_bare_identifier(specifier)?;
if let Some(parent_locator) = find_locator(manifest, parent) {
let parent_pkg = get_package(manifest, parent_locator)?;
let mut reference_or_alias: Option<PackageDependency> = None;
let mut is_set = false;
if !is_set {
if let Some(Some(binding)) = parent_pkg.package_dependencies.get(&ident) {
reference_or_alias = Some(binding.clone());
is_set = true;
}
}
if !is_set
&& manifest.enable_top_level_fallback
&& !is_excluded_from_fallback(manifest, parent_locator)
{
if let Some(fallback_resolution) = manifest.fallback_pool.get(&ident) {
reference_or_alias = fallback_resolution.clone();
is_set = true;
}
}
if !is_set {
let message = if nodejs_built_in_modules::is_nodejs_builtin_module(specifier) {
if is_dependency_tree_root(manifest, parent_locator) {
format!(
"Your application tried to access {dependency_name}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since {dependency_name} isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: ${issuer_path}",
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
} else {
format!(
"${issuer_locator_name} tried to access {dependency_name}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since {dependency_name} isn't otherwise declared in ${issuer_locator_name}'s dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: ${issuer_path}",
issuer_locator_name = &parent_locator.name,
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
}
} else if is_dependency_tree_root(manifest, parent_locator) {
format!(
"Your application tried to access {dependency_name}, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_path}",
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
} else {
format!(
"{issuer_locator_name} tried to access {dependency_name}, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_locator_name}@{issuer_locator_reference} (via {issuer_path})",
issuer_locator_name = &parent_locator.name,
issuer_locator_reference = &parent_locator.reference,
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
};
return Err(Error::UndeclaredDependency(Box::new(UndeclaredDependency {
message,
request: specifier.to_string(),
dependency_name: ident,
issuer_locator: parent_locator.clone(),
issuer_path: parent.to_path_buf(),
})));
}
if let Some(resolution) = reference_or_alias {
let dependency_pkg = match resolution {
PackageDependency::Reference(reference) => {
get_package(manifest, &PackageLocator { name: ident, reference })
}
PackageDependency::Alias(name, reference) => {
get_package(manifest, &PackageLocator { name, reference })
}
}?;
Ok(Resolution::Resolved(dependency_pkg.package_location.clone(), module_path))
} else {
let broken_ancestors = find_broken_peer_dependencies(specifier, parent_locator);
let message = if is_dependency_tree_root(manifest, parent_locator) {
format!(
"Your application tried to access {dependency_name} (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_path}",
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
} else if !broken_ancestors.is_empty()
&& broken_ancestors.iter().all(|locator| is_dependency_tree_root(manifest, locator))
{
format!(
"{issuer_locator_name} tried to access {dependency_name} (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_locator_name}@{issuer_locator_reference} (via {issuer_path})",
issuer_locator_name = &parent_locator.name,
issuer_locator_reference = &parent_locator.reference,
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
} else {
format!(
"{issuer_locator_name} tried to access {dependency_name} (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_locator_name}@{issuer_locator_reference} (via {issuer_path})",
issuer_locator_name = &parent_locator.name,
issuer_locator_reference = &parent_locator.reference,
dependency_name = &ident,
via = via_suffix(&ident, specifier),
issuer_path = parent.to_string_lossy(),
)
};
Err(Error::MissingPeerDependency(Box::new(MissingPeerDependency {
message,
request: specifier.to_string(),
dependency_name: ident,
issuer_locator: parent_locator.clone(),
issuer_path: parent.to_path_buf(),
broken_ancestors: Vec::new(),
})))
}
} else {
Ok(Resolution::Skipped)
}
}
pub fn resolve_to_unqualified(
specifier: &str,
parent: &Path,
config: &ResolutionConfig,
) -> Result<Resolution, Error> {
if let Some(manifest) = (config.host.find_pnp_manifest)(parent)? {
resolve_to_unqualified_via_manifest(&manifest, specifier, parent)
} else {
Ok(Resolution::Skipped)
}
}
#[cfg(test)]
mod lib_tests;