aube_resolver/lib.rs
1mod builder;
2mod catalog;
3mod direct_dep_info;
4mod error;
5mod local_source;
6// `locked_index` is an internal lockfile-reuse index. It is only `pub` under
7// the `bench` feature so the `locked_lookup` bench can reach `LockedIndex`;
8// the default build keeps it `pub(crate)`, off the public surface.
9#[cfg(feature = "bench")]
10pub mod locked_index;
11#[cfg(not(feature = "bench"))]
12pub(crate) mod locked_index;
13pub mod override_rule;
14mod package_ext;
15mod peer_context;
16pub mod platform;
17mod primer;
18mod resolve;
19mod semver_util;
20mod trust;
21mod types;
22
23pub use direct_dep_info::{AgeGatedUpdate, DirectDepInfo};
24pub use error::{AgeGateDetails, CatalogDetails, Error, ExoticSubdepDetails, NoMatchDetails};
25pub use local_source::resolve_exec_script_path;
26pub use package_ext::is_deprecation_allowed;
27pub use peer_context::{
28 PeerContextOptions, UnmetPeer, apply_peer_contexts, detect_unmet_peers,
29 hoist_auto_installed_peers,
30};
31pub use platform::{SupportedArchitectures, is_supported};
32pub use primer::{
33 PruneStats as PrimerPruneStats, popular_package_names, prune_cache as prune_primer_cache,
34};
35pub use semver_util::{PickResult, pick_version_for_add};
36pub use trust::{
37 MissingTimeDetails as MissingTrustTimeDetails, PriorTrustEvidence, TrustCheckError,
38 TrustDowngradeDetails, check_no_downgrade, evidence_for, strongest_prior_evidence,
39};
40pub use trust::{PackageVersionPolicy, TrustEvidence, TrustExcludeParseError, TrustExcludeRules};
41pub use types::{
42 DependencyPolicy, MinimumReleaseAge, PackageExtension, ReadPackageHook, ResolutionMode,
43 ResolvedPackage, TrustPolicy,
44};
45
46pub const YARN_EXEC_WRAPPER: &str = r#"
47const env = JSON.parse(process.env.AUBE_YARN_EXEC_ENV);
48globalThis.execEnv = env;
49for (const name of ['fs', 'path', 'child_process', 'os', 'crypto', 'url', 'util', 'stream', 'buffer']) {
50 globalThis[name] = require(name);
51}
52(async () => {
53 await import(url.pathToFileURL(process.argv[1]).href);
54})().catch((err) => {
55 console.error(err);
56 process.exit(1);
57});
58"#;
59
60use semver_util::version_satisfies;
61
62#[cfg(test)]
63use aube_lockfile::{DirectDep, LocalSource, LockedPackage, LockfileGraph};
64#[cfg(test)]
65use aube_manifest::PackageJson;
66#[cfg(test)]
67use error::{
68 RegistryErrorKind, build_age_gate, build_no_match, classify_registry_error,
69 format_registry_help,
70};
71#[cfg(test)]
72use local_source::{dep_path_for, should_block_exotic_subdep};
73#[cfg(test)]
74use package_ext::{
75 apply_package_extensions, apply_package_extensions_to_deps, package_selector_matches,
76 pick_override_spec,
77};
78#[cfg(test)]
79use peer_context::{
80 apply_dedupe_peers_to_key, contains_canonical_back_ref, dedupe_peer_suffixes,
81 dedupe_peer_variants, effective_peer_suffix, is_hashed_peer_suffix,
82};
83#[cfg(test)]
84use semver_util::{pick_version, strip_alias_prefix};
85#[cfg(test)]
86use types::format_iso8601_utc;
87
88use aube_lockfile::DepType;
89use aube_registry::Packument;
90use aube_registry::client::RegistryClient;
91use std::collections::{BTreeMap, BTreeSet};
92use std::path::PathBuf;
93use std::sync::Arc;
94use tokio::sync::mpsc;
95
96// Re-export shared aube-util collection aliases under the original
97// FxHashMap name to avoid touching every call site.
98pub(crate) use aube_util::collections::FxMap as FxHashMap;
99pub(crate) use aube_util::collections::FxSet as FxHashSet;
100
101/// BFS dependency resolver.
102pub struct Resolver {
103 client: Arc<RegistryClient>,
104 cache: FxHashMap<String, Packument>,
105 /// Optional channel to stream resolved packages as they're discovered.
106 resolved_tx: Option<mpsc::Sender<ResolvedPackage>>,
107 /// Optional disk cache directory for packuments (with ETag revalidation).
108 packument_cache_dir: Option<std::path::PathBuf>,
109 /// Separate disk cache for full (non-corgi) packuments; only used
110 /// when `resolution_mode` is `TimeBased` (which needs the `time:`
111 /// map). Defaults to the sibling `packuments-full-v1/` directory
112 /// next to `packument_cache_dir`.
113 packument_full_cache_dir: Option<std::path::PathBuf>,
114 /// When true (pnpm's default), a package's declared `peerDependencies`
115 /// are enqueued like regular transitives and — if not already
116 /// satisfied by the importer — hoisted to the importer's direct deps.
117 /// When false, peers neither get auto-installed as transitives nor
118 /// hoisted; unmet peers still surface as warnings via
119 /// `detect_unmet_peers`, but the user is on the hook for adding them
120 /// explicitly to `package.json`.
121 auto_install_peers: bool,
122 /// pnpm's `exclude-links-from-lockfile`. Round-tripped through the
123 /// lockfile's `settings:` header; when true, the pnpm writer omits
124 /// `link:` deps from the importer `dependencies:` maps so a
125 /// sibling symlink change doesn't churn the lockfile. Defaults to
126 /// false (pnpm's default). Does not affect resolution itself, only
127 /// the `canonical.settings.exclude_links_from_lockfile` flag the
128 /// writer reads.
129 exclude_links_from_lockfile: bool,
130 /// User-declared override for the host platform triple, used when
131 /// deciding whether an optional dep's `os`/`cpu`/`libc` constraints
132 /// are satisfied. Empty fields fall back to the host.
133 supported_architectures: SupportedArchitectures,
134 /// Raw dependency override map from the manifest (selector key →
135 /// replacement spec). Round-tripped verbatim through the lockfile
136 /// for drift detection; the compiled form in `override_rules` is
137 /// what the resolver hot loop actually consults.
138 overrides: BTreeMap<String, String>,
139 /// Compiled view of `overrides`. Built by `with_overrides`.
140 /// Unparseable selector keys are dropped at compile time so the
141 /// matcher never has to think about them.
142 override_rules: Vec<override_rule::OverrideRule>,
143 /// Names listed in the root manifest's `pnpm.ignoredOptionalDependencies`.
144 /// Any optional dep (root or transitive) whose name is in this set is
145 /// dropped before enqueueing — the resolver never fetches or locks it.
146 /// Mirrors pnpm's `createOptionalDependenciesRemover` read-package hook.
147 ignored_optional_dependencies: BTreeSet<String>,
148 /// pnpm's `resolution-mode` — `Highest` (default) or `TimeBased`.
149 resolution_mode: ResolutionMode,
150 /// Pick the lowest satisfying version for direct dependencies without
151 /// enabling the publish-time cutoff used by `TimeBased`. Kept separate
152 /// from the public `ResolutionMode` enum so adding pnpm's `lowest-direct`
153 /// mode does not break downstream exhaustive matches in aube v1.
154 lowest_direct: bool,
155 /// Project root used to resolve `file:` / `link:` paths to the
156 /// target directory. Defaults to the current working directory;
157 /// callers set it via `with_project_root`.
158 project_root: PathBuf,
159 /// When true, resolver-time `exec:` generators are blocked the
160 /// same way fetch-time execution is blocked.
161 ignore_scripts: bool,
162 /// pnpm v11's `minimumReleaseAge` triplet. `None` disables the
163 /// supply-chain age gate entirely (matching `minimumReleaseAge: 0`).
164 minimum_release_age: Option<MinimumReleaseAge>,
165 /// Workspace catalog ranges. Outer key is the catalog name
166 /// (`default` for the unnamed `catalog:` field in
167 /// `pnpm-workspace.yaml`); inner key is the package name; value is
168 /// the version range. When the resolver encounters a `catalog:` or
169 /// `catalog:<name>` task range, it rewrites the task in place to
170 /// the matching range *before* the override / npm-alias passes,
171 /// while preserving the original `catalog:...` text in
172 /// `original_specifier` so the lockfile importer keeps the
173 /// reference verbatim.
174 catalogs: BTreeMap<String, BTreeMap<String, String>>,
175 /// Optional `readPackage` hook, invoked once per resolved package
176 /// before its transitive deps are enqueued. See [`ReadPackageHook`].
177 /// Wired up by `aube` when a `.pnpmfile.cjs` is detected and
178 /// `--ignore-pnpmfile` was not set.
179 read_package_hook: Option<Box<dyn ReadPackageHook>>,
180 dependency_policy: DependencyPolicy,
181 /// Advisory ranges to avoid when resolving audit fixes. The map is
182 /// keyed by registry package name and values are npm semver ranges
183 /// from `vulnerable_versions`. When a clean satisfying version
184 /// exists, it wins over locked/sibling reuse and the normal highest
185 /// pick; if not, resolution falls back to the ordinary pick so the
186 /// caller can report the advisory as remaining.
187 vulnerable_ranges: BTreeMap<String, Vec<String>>,
188 /// Hosts for which aube performs shallow git clones, mirroring
189 /// pnpm's `git-shallow-hosts`. When a git dep's URL host is in
190 /// this list, the store attempts `git fetch --depth 1 origin
191 /// <sha>` (falling back to a full fetch if the server refuses);
192 /// otherwise it goes straight to a full fetch. Defaults to an
193 /// empty list — `aube` populates it from the generated
194 /// `aube_settings::resolved::git_shallow_hosts` accessor (which
195 /// carries the pnpm-compat default list baked in from
196 /// `settings.toml`) via [`Self::with_git_shallow_hosts`]. Library
197 /// callers who construct a `Resolver` directly must set it
198 /// explicitly if they want the pnpm list; keeping the list in
199 /// one place (`settings.toml`) avoids drift.
200 git_shallow_hosts: Vec<String>,
201 /// pnpm's `peersSuffixMaxLength`. When the peer-ID suffix body on a
202 /// `dep_path` (the `(name@version)(…)` portion without its outer
203 /// parens) would exceed this many bytes, the post-pass replaces the
204 /// whole suffix with a parenthesized short hash `(<short-hash>)` —
205 /// the first 32 chars of SHA-256 of the body, matching pnpm's
206 /// `createPeerDepGraphHash` lockfile format. Default 1000.
207 peers_suffix_max_length: usize,
208 /// pnpm's `dedupe-peer-dependents`. When true (pnpm's default),
209 /// the peer-context post-pass collapses multiple dep_path variants
210 /// of the same canonical package into a single entry when their
211 /// peer resolutions are pairwise-equivalent. When false, every
212 /// distinct ancestor scope gets its own variant — useful for
213 /// debugging peer-context divergence or mimicking pnpm v6/v7
214 /// behavior.
215 dedupe_peer_dependents: bool,
216 /// pnpm's `dedupe-peers`. When true, peer suffixes in the lockfile
217 /// emit just the resolved version — `(18.2.0)` — instead of the
218 /// full `(react@18.2.0)` form. Shorter dep_paths at the cost of
219 /// peer-name fidelity in the snapshot. Defaults to false.
220 dedupe_peers: bool,
221 /// pnpm's `resolve-peers-from-workspace-root`. When true (pnpm's
222 /// default), an importer's unresolved peer can be satisfied by a
223 /// dependency declared in the root importer's `package.json`, even
224 /// when no ancestor scope carries that dep. Common monorepo knob:
225 /// the workspace root pins shared peers like `react`, and every
226 /// subpackage can peer on it without hoisting the version into
227 /// every sibling.
228 resolve_peers_from_workspace_root: bool,
229 /// pnpm's `registry-supports-time-field`. When true, the resolver
230 /// trusts the abbreviated (corgi) packument to carry the `time:`
231 /// map and keeps using the cheap `fetch_packument_cached` path
232 /// even under time-aware resolution (`TimeBased` or
233 /// `minimumReleaseAge`). Defaults to false — the same assumption
234 /// pnpm and npmjs.org ship with — so the resolver falls back to
235 /// the full-packument fetch to get `time:` reliably. No effect
236 /// when neither time-based resolution nor `minimumReleaseAge` is
237 /// active, since the abbreviated path is already the only one
238 /// running.
239 registry_supports_time_field: bool,
240 /// Use the bundled metadata primer even when the configured
241 /// registry is not npmjs.org. Intended for npm-compatible mirrors
242 /// and controlled benchmarks; tarball URLs are rewritten to the
243 /// active registry before cache seeding so installs still fetch
244 /// package bytes from the configured source.
245 force_metadata_primer: bool,
246 pub(crate) packument_network_concurrency: Option<usize>,
247}
248
249pub(crate) struct ResolveTask {
250 pub(crate) name: String,
251 pub(crate) range: String,
252 dep_type: DepType,
253 is_root: bool,
254 /// The parent dep_path, for wiring up transitive dep references
255 parent: Option<String>,
256 /// Which importer this task belongs to (e.g., "." or "packages/app")
257 pub(crate) importer: String,
258 /// The original specifier from package.json before any rewrites
259 /// (e.g. `"npm:real-pkg@^2.0.0"` for an alias, or `"^4.17.0"` for a normal range).
260 /// Only set for root deps; recorded into the lockfile for drift detection.
261 pub(crate) original_specifier: Option<String>,
262 /// Real registry package name for npm-alias tasks.
263 ///
264 /// When a task arrives with `range` like `"npm:h3@2.0.1-rc.20"`,
265 /// the preprocessing loop strips the prefix and sets this field to
266 /// the real package name (`"h3"`) while *keeping* `name` as the
267 /// user-facing alias (`"h3-v2"`, the key the package.json used).
268 /// Every identity-facing site — dep_path formation, direct-dep
269 /// records, parent `dependencies` wiring, the resolved-versions
270 /// dedupe map — uses `name`, so the alias survives all the way
271 /// to the linker and ends up as `node_modules/<alias>/` with
272 /// `LockedPackage.alias_of = Some(real_name)`. Only registry
273 /// I/O (packument fetch, tarball URL derivation) consults this
274 /// field.
275 ///
276 /// `None` for ordinary (non-aliased) tasks — `name` is already
277 /// the registry name and nothing downstream needs to distinguish.
278 real_name: Option<String>,
279 /// Outermost-first chain of `(name, version)` ancestors above this
280 /// task in the dependency graph, used by `parent>child` override
281 /// selectors. Empty for root/importer deps. Each child-enqueue
282 /// site is responsible for extending its parent's chain with the
283 /// parent's own `(name, version)` frame.
284 ///
285 /// `Arc<[_]>` rather than `Vec` because the chain is immutable once
286 /// built and is shared, unmodified, by every dependency a package
287 /// enqueues: the child chain is materialized into a `Vec` once per
288 /// package, frozen here, and each per-dep enqueue is then a refcount
289 /// bump instead of a full deep clone (clone cost scaled with graph
290 /// edges × depth before this).
291 pub(crate) ancestors: Arc<[(String, String)]>,
292 /// `true` when an override rewrote `range` to a `link:`/`file:`
293 /// path. Override paths are anchored at the project root (where the
294 /// override is declared), not at the consuming workspace package or
295 /// transitive parent — same convention pnpm follows. Without this
296 /// signal the local-source resolver would re-anchor `link:./libs/x`
297 /// against the importer or parent dir and walk to a phantom path.
298 pub(crate) range_from_override: bool,
299}
300
301impl ResolveTask {
302 /// Name to use for registry operations (packument fetch, tarball
303 /// URL). Returns `real_name` for aliased tasks and `name`
304 /// otherwise. Every call site that talks to the registry goes
305 /// through this accessor so alias handling stays localized.
306 fn registry_name(&self) -> &str {
307 self.real_name.as_deref().unwrap_or(&self.name)
308 }
309
310 /// Construct a root-importer task for `(name, range)` under
311 /// `importer`, with the appropriate `dep_type` and no parent/ancestry.
312 /// Every root-dep enqueue site uses this shape; the factory keeps
313 /// the literal in one place so a new field added to `ResolveTask`
314 /// lands consistently across prod/dev/optional loops.
315 fn root(name: String, range: String, dep_type: DepType, importer: String) -> Self {
316 let original = range.clone();
317 Self {
318 name,
319 range,
320 dep_type,
321 is_root: true,
322 parent: None,
323 importer,
324 original_specifier: Some(original),
325 real_name: None,
326 ancestors: Arc::from([]),
327 range_from_override: false,
328 }
329 }
330
331 /// Construct a transitive (non-root) task discovered by walking a
332 /// parent package's dependency map. Carries the parent dep_path
333 /// and inherited ancestor chain for overrides.
334 fn transitive(
335 name: String,
336 range: String,
337 dep_type: DepType,
338 parent: String,
339 importer: String,
340 ancestors: Arc<[(String, String)]>,
341 ) -> Self {
342 Self {
343 name,
344 range,
345 dep_type,
346 is_root: false,
347 parent: Some(parent),
348 importer,
349 original_specifier: None,
350 real_name: None,
351 ancestors,
352 range_from_override: false,
353 }
354 }
355}
356
357#[cfg(test)]
358mod tests;