1use nargo_types::{Error, Result, Span};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{debug, info, warn};
7
8use crate::{
9 conflict::{Conflict, ConflictDetector, ConflictSolution},
10 graph::{DependencyEdge, DependencyGraph, DependencyNode, PackageSource},
11};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ResolveOptions {
16 pub include_dev: bool,
18 pub include_optional: bool,
20 pub allow_prerelease: bool,
22 pub max_depth: usize,
24 pub registry: String,
26 pub parallel_jobs: usize,
28 pub resolve_workspace: bool,
30 pub use_lock_file: bool,
32}
33
34impl Default for ResolveOptions {
35 fn default() -> Self {
36 Self { include_dev: true, include_optional: true, allow_prerelease: false, max_depth: 100, registry: "https://registry.npmjs.org".to_string(), parallel_jobs: num_cpus::get(), resolve_workspace: true, use_lock_file: true }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct ResolveResult {
43 pub graph: DependencyGraph,
45 pub conflicts: Vec<Conflict>,
47 pub solutions: Vec<ConflictSolution>,
49 pub stats: ResolveStats,
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct ResolveStats {
56 pub total_packages: usize,
58 pub production_deps: usize,
60 pub dev_deps: usize,
62 pub peer_deps: usize,
64 pub optional_deps: usize,
66 pub conflicts: usize,
68 pub registry_deps: usize,
70 pub git_deps: usize,
72 pub path_deps: usize,
74 pub workspace_deps: usize,
76 pub resolution_time_ms: u64,
78}
79
80#[derive(Debug)]
82pub struct Resolver {
83 options: ResolveOptions,
85 conflict_detector: ConflictDetector,
87 resolved_cache: HashMap<String, DependencyNode>,
89 workspace_packages: HashMap<String, String>,
91}
92
93impl Resolver {
94 pub fn new() -> Self {
96 Self::with_options(ResolveOptions::default())
97 }
98
99 pub fn with_options(options: ResolveOptions) -> Self {
101 Self { options, conflict_detector: ConflictDetector::new(), resolved_cache: HashMap::new(), workspace_packages: HashMap::new() }
102 }
103
104 pub fn with_workspace_packages(mut self, packages: HashMap<String, String>) -> Self {
106 self.workspace_packages = packages;
107 self
108 }
109
110 pub async fn resolve(&mut self, dependencies: &HashMap<String, nargo_config::Dependency>, dev_dependencies: &HashMap<String, nargo_config::Dependency>) -> Result<ResolveResult> {
112 let start = std::time::Instant::now();
113 let mut graph = DependencyGraph::new();
114
115 info!("Starting dependency resolution with {} parallel jobs...", self.options.parallel_jobs);
116
117 let mut version_map: HashMap<String, Vec<(String, String)>> = HashMap::new();
118 let mut peer_deps: HashMap<String, (String, Option<String>)> = HashMap::new();
119 let mut resolved_versions: HashMap<String, String> = HashMap::new();
120
121 self.resolve_deps_recursive(dependencies, false, &mut graph, &mut version_map, &mut resolved_versions, 0).await?;
122
123 if self.options.include_dev {
124 self.resolve_deps_recursive(dev_dependencies, true, &mut graph, &mut version_map, &mut resolved_versions, 0).await?;
125 }
126
127 self.conflict_detector.detect_version_conflicts(&version_map);
128 self.conflict_detector.detect_peer_conflicts(&peer_deps, &resolved_versions);
129
130 let conflicts: Vec<Conflict> = self.conflict_detector.conflicts().to_vec();
131 let solutions = self.conflict_detector.find_solutions();
132
133 let stats = self.calculate_stats(&graph, &conflicts, start.elapsed().as_millis() as u64);
134
135 info!("Resolution complete: {} packages, {} conflicts in {}ms", stats.total_packages, stats.conflicts, stats.resolution_time_ms);
136
137 Ok(ResolveResult { graph, conflicts, solutions, stats })
138 }
139
140 pub async fn resolve_single(&mut self, name: &str, dependency: &nargo_config::Dependency) -> Result<DependencyNode> {
142 let (version, source) = self.parse_dependency(dependency)?;
143
144 let mut node = DependencyNode::new(name, &version).with_source(source);
145
146 if let nargo_config::Dependency::Detailed(detail) = dependency {
147 if !detail.features.is_empty() {
148 node = node.with_features(detail.features.clone());
149 }
150 if detail.optional {
151 node = node.as_optional();
152 }
153 }
154
155 Ok(node)
156 }
157
158 pub fn topological_sort(&self, graph: &DependencyGraph) -> Result<Vec<DependencyNode>> {
160 graph.topological_sort()
161 }
162
163 pub fn detect_cycles(&self, graph: &DependencyGraph) -> Option<Vec<String>> {
165 graph.detect_cycle()
166 }
167
168 #[allow(clippy::too_many_arguments)]
169 async fn resolve_deps_recursive(&mut self, deps: &HashMap<String, nargo_config::Dependency>, is_dev: bool, graph: &mut DependencyGraph, version_map: &mut HashMap<String, Vec<(String, String)>>, resolved_versions: &mut HashMap<String, String>, depth: usize) -> Result<()> {
170 if depth >= self.options.max_depth {
171 warn!("Max depth {} reached, stopping resolution", depth);
172 return Ok(());
173 }
174
175 for (name, dep) in deps {
176 if self.resolved_cache.contains_key(name) {
177 debug!("Using cached resolution for {}", name);
178 continue;
179 }
180
181 let (version, source) = self.parse_dependency(dep)?;
182
183 if self.options.resolve_workspace && self.is_workspace_dependency(dep) {
184 if let Some(ws_version) = self.workspace_packages.get(name) {
185 version_map.entry(name.clone()).or_default().push((ws_version.clone(), "workspace".to_string()));
186 resolved_versions.insert(name.clone(), ws_version.clone());
187 continue;
188 }
189 }
190
191 version_map.entry(name.clone()).or_default().push((version.clone(), "root".to_string()));
192
193 resolved_versions.insert(name.clone(), version.clone());
194
195 let mut node = DependencyNode::new(name, &version).with_source(source).as_dev(is_dev);
196
197 if let nargo_config::Dependency::Detailed(detail) = dep {
198 if !detail.features.is_empty() {
199 node = node.with_features(detail.features.clone());
200 }
201 if detail.optional {
202 node = node.as_optional();
203 }
204 }
205
206 let node_idx = graph.add_node(node.clone());
207 self.resolved_cache.insert(name.clone(), node);
208
209 if depth + 1 < self.options.max_depth {
210 self.resolve_transitive_deps(name, &version, node_idx, is_dev, graph, version_map, resolved_versions, depth + 1).await?;
211 }
212 }
213
214 Ok(())
215 }
216
217 async fn resolve_transitive_deps(&mut self, _parent_name: &str, _parent_version: &str, _parent_idx: petgraph::graph::NodeIndex, _is_dev: bool, _graph: &mut DependencyGraph, _version_map: &mut HashMap<String, Vec<(String, String)>>, _resolved_versions: &mut HashMap<String, String>, _depth: usize) -> Result<()> {
218 Ok(())
219 }
220
221 fn parse_dependency(&self, dep: &nargo_config::Dependency) -> Result<(String, PackageSource)> {
222 match dep {
223 nargo_config::Dependency::Version(v) => {
224 if v.starts_with("workspace:") {
225 Ok((v.clone(), PackageSource::Workspace))
226 }
227 else if v.starts_with("file:") || v.starts_with("./") || v.starts_with("../") {
228 let path = v.strip_prefix("file:").unwrap_or(v);
229 Ok(("path".to_string(), PackageSource::Path { path: path.to_string() }))
230 }
231 else if v.starts_with("git+") || v.starts_with("git://") {
232 let url = v.strip_prefix("git+").unwrap_or(v);
233 Ok(("git".to_string(), PackageSource::Git { url: url.to_string(), reference: None }))
234 }
235 else if v.starts_with("github:") {
236 let repo = v.strip_prefix("github:").unwrap_or(v);
237 Ok(("github".to_string(), PackageSource::Github { repo: repo.to_string(), reference: None }))
238 }
239 else {
240 Ok((v.clone(), PackageSource::Registry { registry: self.options.registry.clone() }))
241 }
242 }
243 nargo_config::Dependency::Detailed(detail) => {
244 if detail.workspace.unwrap_or(false) {
245 Ok(("workspace:*".to_string(), PackageSource::Workspace))
246 }
247 else if let Some(ref version) = detail.version {
248 Ok((version.clone(), PackageSource::Registry { registry: detail.registry.clone().unwrap_or_else(|| self.options.registry.clone()) }))
249 }
250 else if let Some(ref git) = detail.git {
251 let reference = detail.tag.clone().or_else(|| detail.branch.clone()).or_else(|| detail.rev.clone());
252 Ok(("git".to_string(), PackageSource::Git { url: git.clone(), reference }))
253 }
254 else if let Some(ref path) = detail.path {
255 Ok(("path".to_string(), PackageSource::Path { path: path.to_string_lossy().to_string() }))
256 }
257 else {
258 Err(Error::external_error("resolver".to_string(), "Invalid dependency specification".to_string(), Span::unknown()))
259 }
260 }
261 }
262 }
263
264 fn is_workspace_dependency(&self, dep: &nargo_config::Dependency) -> bool {
265 match dep {
266 nargo_config::Dependency::Version(v) => v.starts_with("workspace:"),
267 nargo_config::Dependency::Detailed(d) => d.workspace.unwrap_or(false),
268 }
269 }
270
271 fn calculate_stats(&self, graph: &DependencyGraph, conflicts: &[Conflict], resolution_time_ms: u64) -> ResolveStats {
272 let nodes: Vec<&DependencyNode> = graph.nodes().collect();
273
274 ResolveStats { total_packages: nodes.len(), production_deps: nodes.iter().filter(|n| !n.is_dev && !n.is_optional).count(), dev_deps: nodes.iter().filter(|n| n.is_dev).count(), peer_deps: 0, optional_deps: nodes.iter().filter(|n| n.is_optional).count(), conflicts: conflicts.len(), registry_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Registry { .. })).count(), git_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Git { .. })).count(), path_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Path { .. })).count(), workspace_deps: nodes.iter().filter(|n| matches!(n.source, PackageSource::Workspace)).count(), resolution_time_ms }
275 }
276
277 pub fn clear_cache(&mut self) {
279 self.resolved_cache.clear();
280 self.conflict_detector.clear();
281 }
282
283 pub fn cache(&self) -> &HashMap<String, DependencyNode> {
285 &self.resolved_cache
286 }
287}
288
289impl Default for Resolver {
290 fn default() -> Self {
291 Self::new()
292 }
293}