Skip to main content

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}