Skip to main content

aube_resolver/
builder.rs

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