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