aube-resolver 1.18.2

Dependency resolver for Aube
Documentation
mod driver;
mod fetch;
mod seed;
mod vulnerable;

use crate::local_source::is_non_registry_specifier;
use crate::semver_util::version_satisfies;
use crate::{
    Error, FxHashMap, PeerContextOptions, Resolver, apply_peer_contexts, catalog,
    hoist_auto_installed_peers,
};
use aube_lockfile::{DirectDep, LockedPackage, LockfileGraph};
use aube_manifest::PackageJson;
use std::collections::{BTreeMap, HashMap};

impl Resolver {
    /// Resolve all dependencies from a package.json.
    ///
    /// Uses batch-parallel BFS: each "wave" drains the queue, identifies
    /// uncached package names, fetches their packuments concurrently, then
    /// processes the entire batch before starting the next wave.
    pub async fn resolve(
        &mut self,
        manifest: &PackageJson,
        existing: Option<&LockfileGraph>,
    ) -> Result<LockfileGraph, Error> {
        self.resolve_workspace(
            &[(".".to_string(), manifest.clone())],
            existing,
            &HashMap::new(),
        )
        .await
    }

    /// Resolve all dependencies for a workspace (multiple importers).
    ///
    /// `manifests` is a list of (importer_path, PackageJson) — e.g. (".", root), ("packages/app", app).
    /// `workspace_packages` maps package name → version. Used both for
    /// explicit `workspace:` protocol resolution and for yarn/npm/bun
    /// style linkage where a bare semver range on a workspace-package
    /// name resolves to the local copy when its version satisfies the
    /// range.
    pub async fn resolve_workspace(
        &mut self,
        manifests: &[(String, PackageJson)],
        existing: Option<&LockfileGraph>,
        workspace_packages: &HashMap<String, String>,
    ) -> Result<LockfileGraph, Error> {
        driver::ResolveDriver::new(self, manifests, existing, workspace_packages)
            .run()
            .await
    }

    /// Is `(name, range)` safe to speculatively prefetch against the
    /// registry?
    ///
    /// Returns false for any spec that won't go through the registry
    /// resolver at all — workspace/catalog/npm-alias/jsr ranges, local
    /// (`file:`/`link:`/`git:`) specifiers, and bare ranges that match
    /// a workspace package. Also false for any name listed in
    /// `pnpm.overrides`, since the override may rewrite the spec into
    /// one of the above and we can't cheaply tell ahead of time.
    fn is_prefetchable(
        &self,
        name: &str,
        range: &str,
        workspace_packages: &HashMap<String, String>,
    ) -> bool {
        let workspace_hit = workspace_packages
            .get(name)
            .is_some_and(|ws_v| version_satisfies(ws_v, range));
        !aube_util::pkg::is_workspace_spec(range)
            && !aube_util::pkg::is_catalog_spec(range)
            && !aube_util::pkg::is_npm_spec(range)
            && !aube_util::pkg::is_jsr_spec(range)
            && !is_non_registry_specifier(range)
            && !self.overrides.contains_key(name)
            && !workspace_hit
    }

    /// Build the final `LockfileGraph` from accumulated resolver state.
    ///
    /// Runs the catalog-pick materialization, hoists auto-installed
    /// peers when `auto_install_peers` is on, and applies peer-context
    /// suffixes. Returns the post-peer-context graph ready for lockfile
    /// emission.
    fn finalize_resolved_graph(
        &self,
        importers: BTreeMap<String, Vec<DirectDep>>,
        resolved: BTreeMap<String, LockedPackage>,
        resolved_versions: &FxHashMap<String, Vec<String>>,
        resolved_times: BTreeMap<String, String>,
        skipped_optional_dependencies: BTreeMap<String, BTreeMap<String, String>>,
        catalog_picks: BTreeMap<String, BTreeMap<String, String>>,
    ) -> Result<LockfileGraph, Error> {
        let resolved_catalogs =
            catalog::materialize_catalog_picks(catalog_picks, resolved_versions);

        let canonical = LockfileGraph {
            importers,
            packages: resolved,
            settings: aube_lockfile::LockfileSettings {
                auto_install_peers: self.auto_install_peers,
                exclude_links_from_lockfile: self.exclude_links_from_lockfile,
                // Tarball-URL recording is a lockfile-writer concern; the
                // resolver never populates URLs itself. Install flips this
                // on after the graph is built when the setting is active.
                lockfile_include_tarball_url: false,
            },
            // Stamp the resolver's overrides into the output graph so the
            // lockfile writer can round-trip them and the next install's
            // drift check can compare them against the manifest.
            overrides: self.overrides.clone(),
            ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
            times: resolved_times,
            skipped_optional_dependencies,
            catalogs: resolved_catalogs,
            // Resolver output is format-agnostic; the bun writer layer
            // defaults `configVersion` to 1 when emitting a fresh
            // lockfile.
            bun_config_version: None,
            // Fresh resolves don't carry over unknown blocks; the
            // install-side merge (`overlay_metadata_from`) copies
            // them back from the prior lockfile when round-tripping.
            patched_dependencies: BTreeMap::new(),
            trusted_dependencies: Vec::new(),
            extra_fields: BTreeMap::new(),
            workspace_extra_fields: BTreeMap::new(),
        };

        // Second pass: hoist every auto-installed peer to its importer's
        // direct deps so pnpm-style `node_modules/<peer>` top-level
        // symlinks get created and the lockfile's `importers.` section
        // lists them the way pnpm does with `auto-install-peers=true`.
        // Skipped entirely when the setting is off — matches pnpm, which
        // leaves the importer's `dependencies` untouched in that mode.
        let hoisted = if self.auto_install_peers {
            hoist_auto_installed_peers(canonical)
        } else {
            canonical
        };

        // Third pass: compute peer-context suffixes for every reachable
        // package. See `apply_peer_contexts` for the details.
        let peer_options = PeerContextOptions {
            dedupe_peer_dependents: self.dedupe_peer_dependents,
            dedupe_peers: self.dedupe_peers,
            resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
            peers_suffix_max_length: self.peers_suffix_max_length,
        };
        let _diag_peer =
            aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
        let contextualized = apply_peer_contexts(hoisted, &peer_options)?;
        drop(_diag_peer);
        tracing::debug!(
            "peer-context pass produced {} contextualized packages",
            contextualized.packages.len()
        );
        Ok(contextualized)
    }
}