diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs
index dd33e38ce222..9020190bb9ae 100644
--- a/src/install/PackageManager.rs
+++ b/src/install/PackageManager.rs
@@ -281,6 +281,13 @@ pub type AsyncNetworkTaskQueue = UnboundedQueue;
pub(crate) type SuccessFn = fn(&mut PackageManager, DependencyID, PackageID);
pub(crate) type FailFn = fn(&mut PackageManager, &Dependency, PackageID, Error);
+/// A peer held back by `find_peer_provider`, with the name and range it resolves under (after aliases, overrides and catalogs).
+pub(crate) struct DeferredFolderPeer {
+ pub(crate) dependency_id: DependencyID,
+ pub(crate) name_hash: PackageNameHash,
+ pub(crate) version: crate::dependency::Version,
+}
+
// Default to a maximum of 64 simultaneous HTTP requests for bun install if no proxy is specified
// if a proxy IS specified, default to 64. We have different values because we might change this in the future.
// https://github.com/npm/cli/issues/7072
@@ -393,6 +400,11 @@ pub struct PackageManager {
pub(crate) peer_dependencies: LinearFifo>,
+ /// Peers a root `file:` folder or link could satisfy, held back until nothing else can join the dependency graph.
+ pub(crate) deferred_folder_peers: Vec,
+ /// Set while the deferred peers are decided: the graph is complete, so `find_peer_provider` may bind a root folder.
+ pub(crate) peer_graph_complete: bool,
+
// name hash from alias package name -> aliased package dependency version info
pub(crate) known_npm_aliases: NpmAliasMap,
@@ -2122,6 +2134,8 @@ pub fn init(
peer_dependencies,
LinearFifo::>::init()
);
+ wr!(deferred_folder_peers, Vec::new());
+ wr!(peer_graph_complete, false);
wr!(known_npm_aliases, NpmAliasMap::default());
wr!(trusted_deps_to_add_to_package_json, Vec::new());
wr!(any_failed_to_install, false);
@@ -2581,6 +2595,8 @@ fn init_with_runtime_once(
peer_dependencies,
LinearFifo::>::init()
);
+ wr!(deferred_folder_peers, Vec::new());
+ wr!(peer_graph_complete, false);
wr!(known_npm_aliases, NpmAliasMap::default());
wr!(trusted_deps_to_add_to_package_json, Vec::new());
wr!(any_failed_to_install, false);
diff --git a/src/install/PackageManager/PackageManagerEnqueue.rs b/src/install/PackageManager/PackageManagerEnqueue.rs
index adf08bded22a..70c1035e445f 100644
--- a/src/install/PackageManager/PackageManagerEnqueue.rs
+++ b/src/install/PackageManager/PackageManagerEnqueue.rs
@@ -21,9 +21,9 @@ use crate::lockfile::PackageIndexEntry;
use crate::lockfile::package::Package;
use crate::lockfile_real as Lockfile;
use crate::package_manager_real::{
- self, FailFn, PackageManager, SuccessFn, TaskCallbackList, determine_preinstall_state,
- get_cache_directory, get_preinstall_state, get_temporary_directory, run_tasks,
- set_preinstall_state,
+ self, DeferredFolderPeer, FailFn, PackageManager, SuccessFn, TaskCallbackList,
+ determine_preinstall_state, get_cache_directory, get_preinstall_state, get_temporary_directory,
+ run_tasks, set_preinstall_state,
};
use crate::package_manager_task as Task;
use crate::patch_install::EnqueueAfterState;
@@ -1101,6 +1101,12 @@ pub fn enqueue_dependency_with_main_and_success_fn(
result.package.meta.id,
);
}
+ } else if this
+ .deferred_folder_peers
+ .last()
+ .is_some_and(|deferred| deferred.dependency_id == id)
+ {
+ // Held back by `find_peer_provider`; `resolve_deferred_folder_peers` decides it.
} else if version.tag.is_npm() {
// reshaped for borrowck — `name_str` borrows
// `this.lockfile.buffers.string_bytes`. Route the whole
@@ -2502,6 +2508,18 @@ fn get_or_put_resolved_package(
..Default::default()
}));
}
+
+ if let Some(handled) = bind_peer_provider(
+ this,
+ &[existing_id],
+ dependency,
+ dependency_id,
+ name_hash,
+ version,
+ success_fn,
+ ) {
+ return Ok(handled);
+ }
}
}
PackageIndexEntry::Ids(list) => {
@@ -2554,6 +2572,19 @@ fn get_or_put_resolved_package(
}));
}
}
+
+ let candidates = list.to_vec();
+ if let Some(handled) = bind_peer_provider(
+ this,
+ &candidates,
+ dependency,
+ dependency_id,
+ name_hash,
+ version,
+ success_fn,
+ ) {
+ return Ok(handled);
+ }
}
}
}
@@ -3078,6 +3109,119 @@ fn locked_version_in_lockfile<'a>(
.map(|locked| (locked, buf))
}
+/// Binds a non-registry provider to the peer, or holds the peer back for `resolve_deferred_folder_peers`; `None` means the callers' registry path takes over.
+fn bind_peer_provider(
+ this: &mut PackageManager,
+ candidates: &[PackageID],
+ dependency: &Dependency,
+ dependency_id: DependencyID,
+ name_hash: PackageNameHash,
+ version: &dependency::Version,
+ success_fn: SuccessFn,
+) -> Option