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