1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
mod driver;
mod fetch;
mod seed;
mod vulnerable;
use crate::local_source::is_non_registry_specifier;
use crate::semver_util::version_satisfies;
use crate::{
Error, FxHashMap, PeerContextOptions, Resolver, apply_peer_contexts, catalog,
hoist_auto_installed_peers,
};
use aube_lockfile::{DirectDep, LockedPackage, LockfileGraph};
use aube_manifest::PackageJson;
use std::collections::{BTreeMap, HashMap};
impl Resolver {
/// Resolve all dependencies from a package.json.
///
/// Uses batch-parallel BFS: each "wave" drains the queue, identifies
/// uncached package names, fetches their packuments concurrently, then
/// processes the entire batch before starting the next wave.
pub async fn resolve(
&mut self,
manifest: &PackageJson,
existing: Option<&LockfileGraph>,
) -> Result<LockfileGraph, Error> {
self.resolve_workspace(
&[(".".to_string(), manifest.clone())],
existing,
&HashMap::new(),
)
.await
}
/// Resolve all dependencies for a workspace (multiple importers).
///
/// `manifests` is a list of (importer_path, PackageJson) — e.g. (".", root), ("packages/app", app).
/// `workspace_packages` maps package name → version. Used both for
/// explicit `workspace:` protocol resolution and for yarn/npm/bun
/// style linkage where a bare semver range on a workspace-package
/// name resolves to the local copy when its version satisfies the
/// range.
pub async fn resolve_workspace(
&mut self,
manifests: &[(String, PackageJson)],
existing: Option<&LockfileGraph>,
workspace_packages: &HashMap<String, String>,
) -> Result<LockfileGraph, Error> {
driver::ResolveDriver::new(self, manifests, existing, workspace_packages)
.run()
.await
}
/// Is `(name, range)` safe to speculatively prefetch against the
/// registry?
///
/// Returns false for any spec that won't go through the registry
/// resolver at all — workspace/catalog/npm-alias/jsr ranges, local
/// (`file:`/`link:`/`git:`) specifiers, and bare ranges that match
/// a workspace package. Also false for any name listed in
/// `pnpm.overrides`, since the override may rewrite the spec into
/// one of the above and we can't cheaply tell ahead of time.
fn is_prefetchable(
&self,
name: &str,
range: &str,
workspace_packages: &HashMap<String, String>,
) -> bool {
let workspace_hit = workspace_packages
.get(name)
.is_some_and(|ws_v| version_satisfies(ws_v, range));
!aube_util::pkg::is_workspace_spec(range)
&& !aube_util::pkg::is_catalog_spec(range)
&& !aube_util::pkg::is_npm_spec(range)
&& !aube_util::pkg::is_jsr_spec(range)
&& !is_non_registry_specifier(range)
&& !self.overrides.contains_key(name)
&& !workspace_hit
}
/// Build the final `LockfileGraph` from accumulated resolver state.
///
/// Runs the catalog-pick materialization, hoists auto-installed
/// peers when `auto_install_peers` is on, and applies peer-context
/// suffixes. Returns the post-peer-context graph ready for lockfile
/// emission.
fn finalize_resolved_graph(
&self,
importers: BTreeMap<String, Vec<DirectDep>>,
resolved: BTreeMap<String, LockedPackage>,
resolved_versions: &FxHashMap<String, Vec<String>>,
resolved_times: BTreeMap<String, String>,
skipped_optional_dependencies: BTreeMap<String, BTreeMap<String, String>>,
catalog_picks: BTreeMap<String, BTreeMap<String, String>>,
) -> Result<LockfileGraph, Error> {
let resolved_catalogs =
catalog::materialize_catalog_picks(catalog_picks, resolved_versions);
let canonical = LockfileGraph {
importers,
packages: resolved,
settings: aube_lockfile::LockfileSettings {
auto_install_peers: self.auto_install_peers,
exclude_links_from_lockfile: self.exclude_links_from_lockfile,
// Tarball-URL recording is a lockfile-writer concern; the
// resolver never populates URLs itself. Install flips this
// on after the graph is built when the setting is active.
lockfile_include_tarball_url: false,
},
// Stamp the resolver's overrides into the output graph so the
// lockfile writer can round-trip them and the next install's
// drift check can compare them against the manifest.
overrides: self.overrides.clone(),
ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
times: resolved_times,
skipped_optional_dependencies,
catalogs: resolved_catalogs,
// Resolver output is format-agnostic; the bun writer layer
// defaults `configVersion` to 1 when emitting a fresh
// lockfile.
bun_config_version: None,
// Fresh resolves don't carry over unknown blocks; the
// install-side merge (`overlay_metadata_from`) copies
// them back from the prior lockfile when round-tripping.
patched_dependencies: BTreeMap::new(),
trusted_dependencies: Vec::new(),
runtimes: BTreeMap::new(),
extra_fields: BTreeMap::new(),
workspace_extra_fields: BTreeMap::new(),
};
// Second pass: hoist every auto-installed peer to its importer's
// direct deps so pnpm-style `node_modules/<peer>` top-level
// symlinks get created and the lockfile's `importers.` section
// lists them the way pnpm does with `auto-install-peers=true`.
// Skipped entirely when the setting is off — matches pnpm, which
// leaves the importer's `dependencies` untouched in that mode.
let hoisted = if self.auto_install_peers {
hoist_auto_installed_peers(canonical)
} else {
canonical
};
// Third pass: compute peer-context suffixes for every reachable
// package. See `apply_peer_contexts` for the details.
let peer_options = PeerContextOptions {
dedupe_peer_dependents: self.dedupe_peer_dependents,
dedupe_peers: self.dedupe_peers,
resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
peers_suffix_max_length: self.peers_suffix_max_length,
};
let _diag_peer =
aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
let contextualized = apply_peer_contexts(hoisted, &peer_options)?;
drop(_diag_peer);
tracing::debug!(
"peer-context pass produced {} contextualized packages",
contextualized.packages.len()
);
Ok(contextualized)
}
}