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