aube_resolver/resolve.rs
1mod driver;
2mod fetch;
3mod seed;
4mod vulnerable;
5
6use crate::local_source::is_non_registry_specifier;
7use crate::semver_util::version_satisfies;
8use crate::{
9 Error, FxHashMap, PeerContextOptions, Resolver, apply_peer_contexts, catalog,
10 hoist_auto_installed_peers,
11};
12use aube_lockfile::{DirectDep, LockedPackage, LockfileGraph};
13use aube_manifest::PackageJson;
14use std::collections::{BTreeMap, HashMap};
15
16impl Resolver {
17 /// Resolve all dependencies from a package.json.
18 ///
19 /// Uses batch-parallel BFS: each "wave" drains the queue, identifies
20 /// uncached package names, fetches their packuments concurrently, then
21 /// processes the entire batch before starting the next wave.
22 pub async fn resolve(
23 &mut self,
24 manifest: &PackageJson,
25 existing: Option<&LockfileGraph>,
26 ) -> Result<LockfileGraph, Error> {
27 self.resolve_workspace(
28 &[(".".to_string(), manifest.clone())],
29 existing,
30 &HashMap::new(),
31 )
32 .await
33 }
34
35 /// Resolve all dependencies for a workspace (multiple importers).
36 ///
37 /// `manifests` is a list of (importer_path, PackageJson) — e.g. (".", root), ("packages/app", app).
38 /// `workspace_packages` maps package name → version. Used both for
39 /// explicit `workspace:` protocol resolution and for yarn/npm/bun
40 /// style linkage where a bare semver range on a workspace-package
41 /// name resolves to the local copy when its version satisfies the
42 /// range.
43 pub async fn resolve_workspace(
44 &mut self,
45 manifests: &[(String, PackageJson)],
46 existing: Option<&LockfileGraph>,
47 workspace_packages: &HashMap<String, String>,
48 ) -> Result<LockfileGraph, Error> {
49 driver::ResolveDriver::new(self, manifests, existing, workspace_packages)
50 .run()
51 .await
52 }
53
54 /// Is `(name, range)` safe to speculatively prefetch against the
55 /// registry?
56 ///
57 /// Returns false for any spec that won't go through the registry
58 /// resolver at all — workspace/catalog/npm-alias/jsr ranges, local
59 /// (`file:`/`link:`/`git:`) specifiers, and bare ranges that match
60 /// a workspace package. Also false for any name listed in
61 /// `pnpm.overrides`, since the override may rewrite the spec into
62 /// one of the above and we can't cheaply tell ahead of time.
63 fn is_prefetchable(
64 &self,
65 name: &str,
66 range: &str,
67 workspace_packages: &HashMap<String, String>,
68 ) -> bool {
69 let workspace_hit = workspace_packages
70 .get(name)
71 .is_some_and(|ws_v| version_satisfies(ws_v, range));
72 !aube_util::pkg::is_workspace_spec(range)
73 && !aube_util::pkg::is_catalog_spec(range)
74 && !aube_util::pkg::is_npm_spec(range)
75 && !aube_util::pkg::is_jsr_spec(range)
76 && !is_non_registry_specifier(range)
77 && !self.overrides.contains_key(name)
78 && !workspace_hit
79 }
80
81 /// Build the final `LockfileGraph` from accumulated resolver state.
82 ///
83 /// Runs the catalog-pick materialization, hoists auto-installed
84 /// peers when `auto_install_peers` is on, and applies peer-context
85 /// suffixes. Returns the post-peer-context graph ready for lockfile
86 /// emission.
87 fn finalize_resolved_graph(
88 &self,
89 importers: BTreeMap<String, Vec<DirectDep>>,
90 resolved: BTreeMap<String, LockedPackage>,
91 resolved_versions: &FxHashMap<String, Vec<String>>,
92 resolved_times: BTreeMap<String, String>,
93 skipped_optional_dependencies: BTreeMap<String, BTreeMap<String, String>>,
94 catalog_picks: BTreeMap<String, BTreeMap<String, String>>,
95 ) -> Result<LockfileGraph, Error> {
96 let resolved_catalogs =
97 catalog::materialize_catalog_picks(catalog_picks, resolved_versions);
98
99 let canonical = LockfileGraph {
100 importers,
101 packages: resolved,
102 settings: aube_lockfile::LockfileSettings {
103 auto_install_peers: self.auto_install_peers,
104 exclude_links_from_lockfile: self.exclude_links_from_lockfile,
105 // Tarball-URL recording is a lockfile-writer concern; the
106 // resolver never populates URLs itself. Install flips this
107 // on after the graph is built when the setting is active.
108 lockfile_include_tarball_url: false,
109 },
110 // Stamp the resolver's overrides into the output graph so the
111 // lockfile writer can round-trip them and the next install's
112 // drift check can compare them against the manifest.
113 overrides: self.overrides.clone(),
114 ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
115 times: resolved_times,
116 skipped_optional_dependencies,
117 catalogs: resolved_catalogs,
118 // Resolver output is format-agnostic; the bun writer layer
119 // defaults `configVersion` to 1 when emitting a fresh
120 // lockfile.
121 bun_config_version: None,
122 // Fresh resolves don't carry over unknown blocks; the
123 // install-side merge (`overlay_metadata_from`) copies
124 // them back from the prior lockfile when round-tripping.
125 patched_dependencies: BTreeMap::new(),
126 trusted_dependencies: Vec::new(),
127 runtimes: BTreeMap::new(),
128 extra_fields: BTreeMap::new(),
129 workspace_extra_fields: BTreeMap::new(),
130 };
131
132 // Second pass: hoist every auto-installed peer to its importer's
133 // direct deps so pnpm-style `node_modules/<peer>` top-level
134 // symlinks get created and the lockfile's `importers.` section
135 // lists them the way pnpm does with `auto-install-peers=true`.
136 // Skipped entirely when the setting is off — matches pnpm, which
137 // leaves the importer's `dependencies` untouched in that mode.
138 let hoisted = if self.auto_install_peers {
139 hoist_auto_installed_peers(canonical)
140 } else {
141 canonical
142 };
143
144 // Third pass: compute peer-context suffixes for every reachable
145 // package. See `apply_peer_contexts` for the details.
146 let peer_options = PeerContextOptions {
147 dedupe_peer_dependents: self.dedupe_peer_dependents,
148 dedupe_peers: self.dedupe_peers,
149 resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
150 peers_suffix_max_length: self.peers_suffix_max_length,
151 };
152 let _diag_peer =
153 aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
154 let contextualized = apply_peer_contexts(hoisted, &peer_options)?;
155 drop(_diag_peer);
156 tracing::debug!(
157 "peer-context pass produced {} contextualized packages",
158 contextualized.packages.len()
159 );
160 Ok(contextualized)
161 }
162}