aube_resolver/builder.rs
1use crate::FxHashMap;
2use crate::{
3 DependencyPolicy, MinimumReleaseAge, ReadPackageHook, ResolutionMode, ResolvedPackage,
4 Resolver, SupportedArchitectures, override_rule,
5};
6use aube_registry::client::RegistryClient;
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::PathBuf;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11
12impl Resolver {
13 const DEFAULT_STREAM_CAPACITY: usize = 64;
14
15 pub fn new(client: Arc<RegistryClient>) -> Self {
16 Self {
17 client,
18 // 1024 covers typical monorepo without rehash. 5000-pkg tail pays one grow.
19 cache: FxHashMap::with_capacity_and_hasher(1024, Default::default()),
20 resolved_tx: None,
21 packument_cache_dir: None,
22 packument_full_cache_dir: None,
23 auto_install_peers: true,
24 exclude_links_from_lockfile: false,
25 supported_architectures: SupportedArchitectures::default(),
26 overrides: BTreeMap::new(),
27 override_rules: Vec::new(),
28 ignored_optional_dependencies: BTreeSet::new(),
29 resolution_mode: ResolutionMode::Highest,
30 project_root: PathBuf::from("."),
31 minimum_release_age: None,
32 catalogs: BTreeMap::new(),
33 read_package_hook: None,
34 dependency_policy: DependencyPolicy::default(),
35 vulnerable_ranges: BTreeMap::new(),
36 git_shallow_hosts: Vec::new(),
37 peers_suffix_max_length: 1000,
38 dedupe_peer_dependents: true,
39 dedupe_peers: false,
40 resolve_peers_from_workspace_root: true,
41 registry_supports_time_field: false,
42 force_metadata_primer: false,
43 packument_network_concurrency: None,
44 }
45 }
46
47 /// Create a resolver that streams resolved packages through a channel.
48 /// Returns `(resolver, receiver)`. The receiver yields packages as they're
49 /// discovered, allowing tarball fetches to start during resolution.
50 pub fn with_stream(client: Arc<RegistryClient>) -> (Self, mpsc::Receiver<ResolvedPackage>) {
51 Self::with_stream_capacity(client, Self::DEFAULT_STREAM_CAPACITY)
52 }
53
54 /// Create a streaming resolver with a bounded resolved-package buffer.
55 pub fn with_stream_capacity(
56 client: Arc<RegistryClient>,
57 capacity: usize,
58 ) -> (Self, mpsc::Receiver<ResolvedPackage>) {
59 let (tx, rx) = mpsc::channel(capacity.max(1));
60 (
61 Self {
62 client,
63 // 1024 covers typical monorepo without rehash. 5000-pkg tail pays one grow.
64 cache: FxHashMap::with_capacity_and_hasher(1024, Default::default()),
65 resolved_tx: Some(tx),
66 packument_cache_dir: None,
67 packument_full_cache_dir: None,
68 auto_install_peers: true,
69 exclude_links_from_lockfile: false,
70 supported_architectures: SupportedArchitectures::default(),
71 overrides: BTreeMap::new(),
72 override_rules: Vec::new(),
73 ignored_optional_dependencies: BTreeSet::new(),
74 resolution_mode: ResolutionMode::Highest,
75 project_root: PathBuf::from("."),
76 minimum_release_age: None,
77 catalogs: BTreeMap::new(),
78 read_package_hook: None,
79 dependency_policy: DependencyPolicy::default(),
80 vulnerable_ranges: BTreeMap::new(),
81 git_shallow_hosts: Vec::new(),
82 peers_suffix_max_length: 1000,
83 dedupe_peer_dependents: true,
84 dedupe_peers: false,
85 resolve_peers_from_workspace_root: true,
86 registry_supports_time_field: false,
87 force_metadata_primer: false,
88 packument_network_concurrency: None,
89 },
90 rx,
91 )
92 }
93
94 pub fn with_packument_network_concurrency(mut self, n: Option<usize>) -> Self {
95 self.packument_network_concurrency = n.filter(|&n| n > 0);
96 self
97 }
98
99 /// Enable disk-backed packument caching with ETag/Last-Modified revalidation.
100 pub fn with_packument_cache(mut self, cache_dir: std::path::PathBuf) -> Self {
101 self.packument_cache_dir = Some(cache_dir);
102 self
103 }
104
105 /// Disk cache for full (non-corgi) packuments, used in
106 /// `ResolutionMode::TimeBased` so we can read the `time:` map.
107 pub fn with_packument_full_cache(mut self, cache_dir: std::path::PathBuf) -> Self {
108 self.packument_full_cache_dir = Some(cache_dir);
109 self
110 }
111
112 /// Set the resolution mode. Defaults to `Highest` (pnpm's classic
113 /// behavior). `TimeBased` switches direct deps to lowest-satisfying
114 /// and constrains transitives by a publish-date cutoff.
115 pub fn with_resolution_mode(mut self, mode: ResolutionMode) -> Self {
116 self.resolution_mode = mode;
117 self
118 }
119
120 /// Configure pnpm v11's `minimumReleaseAge` family of settings.
121 /// Pass `None` (or a config with `minutes == 0`) to disable.
122 pub fn with_minimum_release_age(mut self, mra: Option<MinimumReleaseAge>) -> Self {
123 self.minimum_release_age = mra.filter(|m| m.minutes > 0);
124 self
125 }
126
127 /// Whether the resolver should round-trip registry `time:` entries
128 /// into the output graph. pnpm only writes `time:` to its lockfile
129 /// when one of `resolution-mode=time-based` / `minimumReleaseAge`
130 /// is active — otherwise the field is dead weight and, worse, shows
131 /// up as churn in a pnpm ↔ aube diff. Gate the insertion at the
132 /// two `resolved_times.insert` call sites on this predicate so
133 /// Highest-mode installs never populate the map.
134 pub(crate) fn should_record_times(&self) -> bool {
135 self.resolution_mode == ResolutionMode::TimeBased
136 || self.minimum_release_age.is_some()
137 || self.dependency_policy.trust_policy == crate::TrustPolicy::NoDowngrade
138 }
139
140 /// Override the default `auto-install-peers=true` behavior. pnpm reads
141 /// this from `.npmrc` or `pnpm-workspace.yaml`; aube's install command
142 /// plumbs the resolved value through here before running resolution.
143 pub fn with_auto_install_peers(mut self, auto_install_peers: bool) -> Self {
144 self.auto_install_peers = auto_install_peers;
145 self
146 }
147
148 /// Configure pnpm's `peersSuffixMaxLength`. When the peer suffix on a
149 /// `dep_path` would exceed this many bytes, the post-pass replaces it
150 /// with `_<10-char-sha256-hex>`. Default 1000 (pnpm's default).
151 pub fn with_peers_suffix_max_length(mut self, max_length: usize) -> Self {
152 self.peers_suffix_max_length = max_length;
153 self
154 }
155
156 /// Override the default `dedupe-peer-dependents=true` behavior. When
157 /// false, the peer-context pass keeps every distinct ancestor-scope
158 /// variant of a package instead of collapsing peer-equivalent ones
159 /// into a single dep_path. Plumbed from `.npmrc` /
160 /// `pnpm-workspace.yaml` via the install command.
161 pub fn with_dedupe_peer_dependents(mut self, value: bool) -> Self {
162 self.dedupe_peer_dependents = value;
163 self
164 }
165
166 /// Override the default `dedupe-peers=false` behavior. When true,
167 /// peer suffixes in the lockfile drop the peer name and emit only
168 /// the resolved version — `(18.2.0)` instead of `(react@18.2.0)`.
169 /// Plumbed from `.npmrc` / `pnpm-workspace.yaml` via the install
170 /// command.
171 pub fn with_dedupe_peers(mut self, value: bool) -> Self {
172 self.dedupe_peers = value;
173 self
174 }
175
176 /// Override the default `resolve-peers-from-workspace-root=true`
177 /// behavior. When false, peer resolution stops at the importer's
178 /// own scope + BFS-auto-installed transitives instead of consulting
179 /// the workspace root's direct deps as a fallback tier. Plumbed
180 /// from `.npmrc` / `pnpm-workspace.yaml` via the install command.
181 pub fn with_resolve_peers_from_workspace_root(mut self, value: bool) -> Self {
182 self.resolve_peers_from_workspace_root = value;
183 self
184 }
185
186 /// Configure pnpm's `registry-supports-time-field`. When true,
187 /// the resolver keeps using the abbreviated (corgi) packument
188 /// path even when `time:` is needed, saving one full-packument
189 /// fetch per distinct package. Safe for registries that embed
190 /// `time` in their abbreviated responses (Verdaccio 5.15.1+, JSR,
191 /// most in-house mirrors); leave at the default `false` for
192 /// npmjs.org.
193 pub fn with_registry_supports_time_field(mut self, value: bool) -> Self {
194 self.registry_supports_time_field = value;
195 self
196 }
197
198 /// Force the bundled metadata primer on for npm-compatible
199 /// mirrors. Normally the primer only seeds npmjs.org cache entries
200 /// because it was generated from npmjs metadata.
201 pub fn with_force_metadata_primer(mut self, value: bool) -> Self {
202 self.force_metadata_primer = value;
203 self
204 }
205
206 /// Configure pnpm's `exclude-links-from-lockfile` setting. Only
207 /// affects lockfile serialization — the resolver still builds the
208 /// same graph either way, but the value is stamped into
209 /// `LockfileGraph::settings` so the pnpm writer can filter `link:`
210 /// importer entries on write.
211 pub fn with_exclude_links_from_lockfile(mut self, value: bool) -> Self {
212 self.exclude_links_from_lockfile = value;
213 self
214 }
215
216 /// Override the host platform triple used when filtering optional
217 /// dependencies. See [`platform::SupportedArchitectures`].
218 pub fn with_supported_architectures(mut self, value: SupportedArchitectures) -> Self {
219 self.supported_architectures = value;
220 self
221 }
222
223 /// Provide dependency overrides. The map's keys are selector
224 /// strings — bare name, `parent>child`, `foo@<2`, `**/foo`, or any
225 /// combination thereof — and values are version specifiers (or
226 /// `npm:` aliases). Keys are compiled into `override_rule`
227 /// structures; unparseable keys are dropped. Whenever the resolver
228 /// encounters a task matching a rule (by name + ancestor chain +
229 /// optional version constraints), the requested range is replaced
230 /// with the rule's replacement before any packument fetch or
231 /// version pick. Workspace + manifest sources are merged by the
232 /// caller.
233 pub fn with_overrides(mut self, overrides: BTreeMap<String, String>) -> Self {
234 self.override_rules = override_rule::compile(&overrides);
235 self.overrides = overrides;
236 self
237 }
238
239 /// Provide workspace catalog ranges. Outer key is the catalog name
240 /// (`default` for the unnamed `catalog:` field in
241 /// `pnpm-workspace.yaml`); inner key is the package name. The
242 /// resolver rewrites `catalog:` and `catalog:<name>` task ranges
243 /// against this map before the override / npm-alias passes, and
244 /// records the picks in the output graph's `catalogs` field.
245 pub fn with_catalogs(mut self, catalogs: BTreeMap<String, BTreeMap<String, String>>) -> Self {
246 self.catalogs = catalogs;
247 self
248 }
249
250 /// Set the project root used to resolve `file:` / `link:` paths.
251 /// `file:./vendor/foo` resolves against this directory, and a
252 /// matching directory / tarball is read to drive resolution of the
253 /// local package's transitive deps.
254 pub fn with_project_root(mut self, project_root: PathBuf) -> Self {
255 self.project_root = project_root;
256 self
257 }
258
259 /// Names to strip from every `optionalDependencies` map before
260 /// enqueueing (pnpm's `pnpm.ignoredOptionalDependencies`). Applied
261 /// to both root and transitive optional deps. Empty by default.
262 pub fn with_ignored_optional_dependencies(mut self, ignored: BTreeSet<String>) -> Self {
263 self.ignored_optional_dependencies = ignored;
264 self
265 }
266
267 /// Install a `readPackage` hook. The resolver calls it once per
268 /// version-picked packument before enqueueing transitives; see
269 /// [`ReadPackageHook`] for what mutations are honored.
270 pub fn with_read_package_hook(mut self, hook: Box<dyn ReadPackageHook>) -> Self {
271 self.read_package_hook = Some(hook);
272 self
273 }
274
275 /// Configure dependency resolution policy settings such as
276 /// `packageExtensions`, `allowedDeprecatedVersions`, `trustPolicy*`,
277 /// and `blockExoticSubdeps`.
278 pub fn with_dependency_policy(mut self, policy: DependencyPolicy) -> Self {
279 self.dependency_policy = policy;
280 self
281 }
282
283 /// Prefer non-vulnerable versions for the supplied audit ranges.
284 /// Used by `audit --fix=update` to reuse the normal resolver while
285 /// steering only vulnerable packages away from affected versions.
286 pub fn with_vulnerable_ranges(mut self, ranges: BTreeMap<String, Vec<String>>) -> Self {
287 self.vulnerable_ranges = ranges;
288 self
289 }
290
291 /// Set the `git-shallow-hosts` list used when cloning git deps.
292 /// When a git URL's host matches an entry here (exact match,
293 /// same as pnpm), aube attempts a shallow fetch by SHA; other
294 /// hosts get a plain `git fetch origin`. An empty list forces
295 /// every git dep through the full-fetch path.
296 pub fn with_git_shallow_hosts(mut self, hosts: Vec<String>) -> Self {
297 self.git_shallow_hosts = hosts;
298 self
299 }
300}