1use crate::errors::{DnxError, Result};
2use crate::hooks::{HookPackage, Hooks};
3use crate::registry::{PackageMetadata, RegistryClient, VersionMetadata};
4use serde::{Deserialize, Serialize};
5use std::cell::RefCell;
6use std::collections::{HashMap, VecDeque};
7use std::sync::Arc;
8use tracing::{debug, trace, warn};
9
10thread_local! {
12 static VERSION_CACHE: RefCell<HashMap<String, Option<node_semver::Version>>> =
13 RefCell::new(HashMap::new());
14 static RANGE_CACHE: RefCell<HashMap<String, Option<node_semver::Range>>> =
15 RefCell::new(HashMap::new());
16}
17
18fn cached_parse_version(s: &str) -> Option<node_semver::Version> {
19 VERSION_CACHE.with(|cache| {
20 let mut cache = cache.borrow_mut();
21 cache
22 .entry(s.to_string())
23 .or_insert_with(|| node_semver::Version::parse(s).ok())
24 .clone()
25 })
26}
27
28fn cached_parse_range(s: &str) -> Option<node_semver::Range> {
29 RANGE_CACHE.with(|cache| {
30 let mut cache = cache.borrow_mut();
31 cache
32 .entry(s.to_string())
33 .or_insert_with(|| node_semver::Range::parse(s).ok())
34 .clone()
35 })
36}
37
38#[derive(Debug, Clone)]
44pub enum DependencySpec {
45 Semver { range: String },
47 Alias { real_name: String, range: String },
50 Git {
52 url: String,
53 reference: Option<String>,
54 },
55 Github {
57 user: String,
58 repo: String,
59 reference: Option<String>,
60 },
61 File { path: String },
63 Link { path: String },
65 Workspace { range: String },
67 Catalog { catalog_name: Option<String> },
69}
70
71impl DependencySpec {
72 pub fn parse(spec: &str) -> Self {
74 let spec = spec.trim();
75
76 if let Some(suffix) = spec.strip_prefix("workspace:") {
78 return DependencySpec::Workspace {
79 range: suffix.to_string(),
80 };
81 }
82
83 if let Some(suffix) = spec.strip_prefix("catalog:") {
85 return DependencySpec::Catalog {
86 catalog_name: if suffix.is_empty() {
87 None
88 } else {
89 Some(suffix.to_string())
90 },
91 };
92 }
93
94 if let Some(rest) = spec.strip_prefix("npm:") {
96 let (real_name, range) = if let Some(stripped) = rest.strip_prefix('@') {
99 if let Some(idx) = stripped.find('@') {
101 (rest[..idx + 1].to_string(), stripped[idx + 1..].to_string())
102 } else {
103 (rest.to_string(), "*".to_string())
104 }
105 } else if let Some(idx) = rest.find('@') {
106 (rest[..idx].to_string(), rest[idx + 1..].to_string())
107 } else {
108 (rest.to_string(), "*".to_string())
109 };
110 return DependencySpec::Alias { real_name, range };
111 }
112
113 if let Some(suffix) = spec.strip_prefix("link:") {
115 return DependencySpec::Link {
116 path: suffix.to_string(),
117 };
118 }
119
120 if let Some(suffix) = spec.strip_prefix("file:") {
122 return DependencySpec::File {
123 path: suffix.to_string(),
124 };
125 }
126
127 if spec.starts_with("git+") || spec.starts_with("git://") {
129 let url_part = if let Some(stripped) = spec.strip_prefix("git+") {
130 stripped
131 } else {
132 spec
133 };
134 let (url, reference) = if let Some(idx) = url_part.find('#') {
135 (
136 url_part[..idx].to_string(),
137 Some(url_part[idx + 1..].to_string()),
138 )
139 } else {
140 (url_part.to_string(), None)
141 };
142 return DependencySpec::Git { url, reference };
143 }
144
145 if (spec.starts_with("https://") || spec.starts_with("http://")) && spec.contains(".git") {
147 let (url, reference) = if let Some(idx) = spec.find('#') {
148 (spec[..idx].to_string(), Some(spec[idx + 1..].to_string()))
149 } else {
150 (spec.to_string(), None)
151 };
152 return DependencySpec::Git { url, reference };
153 }
154
155 if !spec.starts_with('@') && !spec.contains("://") && spec.contains('/') {
158 let (base, reference) = if let Some(idx) = spec.find('#') {
159 (&spec[..idx], Some(spec[idx + 1..].to_string()))
160 } else {
161 (spec, None)
162 };
163 let parts: Vec<&str> = base.splitn(2, '/').collect();
164 if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
165 return DependencySpec::Github {
166 user: parts[0].to_string(),
167 repo: parts[1].to_string(),
168 reference,
169 };
170 }
171 }
172
173 DependencySpec::Semver {
175 range: spec.to_string(),
176 }
177 }
178
179 pub fn is_semver(&self) -> bool {
181 matches!(self, DependencySpec::Semver { .. })
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ResolvedPackage {
192 pub name: String,
194 pub version: String,
196 pub tarball_url: String,
198 pub integrity: String,
201 pub dependencies: Vec<String>,
203 #[serde(default, skip_serializing_if = "Vec::is_empty")]
205 pub peer_dependencies: Vec<String>,
206 pub bin: HashMap<String, String>,
208 #[serde(default)]
210 pub has_install_script: bool,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct DependencyGraph {
216 pub packages: Vec<ResolvedPackage>,
217}
218
219pub struct Resolver {
228 registry: Arc<RegistryClient>,
229 overrides: HashMap<String, String>,
231 workspace_packages: HashMap<String, (String, std::path::PathBuf)>,
234 auto_install_peers: bool,
236 hooks: RefCell<Hooks>,
238}
239
240#[derive(Debug)]
246struct QueueEntry {
247 name: String,
248 range: String,
250 optional: bool,
252}
253
254fn find_best_version<'a>(
264 metadata: &'a PackageMetadata,
265 range_str: &str,
266 override_version: Option<&str>,
267) -> Result<(String, &'a VersionMetadata)> {
268 let range_str = override_version.unwrap_or(range_str);
270 let range_str = range_str.trim();
271
272 if let Some(tag_version) = metadata.dist_tags.get(range_str) {
274 if let Some(vm) = metadata.versions.get(tag_version) {
275 return Ok((tag_version.clone(), vm));
276 }
277 }
280
281 let effective_range = match range_str {
283 "" | "*" | "latest" => ">=0.0.0".to_string(),
284 s => s.to_string(),
285 };
286
287 let range = cached_parse_range(&effective_range).ok_or_else(|| {
289 DnxError::Resolution(format!(
290 "Invalid semver range '{}' for package '{}': parse error",
291 range_str, metadata.name
292 ))
293 })?;
294
295 let mut candidates: Vec<(node_semver::Version, &str)> = Vec::new();
297
298 for ver_str in metadata.versions.keys() {
299 if let Some(v) = cached_parse_version(ver_str) {
300 if !v.pre_release.is_empty() && !range_str.contains('-') {
302 continue;
303 }
304 candidates.push((v, ver_str.as_str()));
305 }
306 }
307
308 candidates.sort_by(|a, b| b.0.cmp(&a.0));
310
311 for (ver, ver_str) in &candidates {
312 if range.satisfies(ver) {
313 let vm = metadata.versions.get(*ver_str).unwrap();
314 return Ok((ver_str.to_string(), vm));
315 }
316 }
317
318 Err(DnxError::Resolution(format!(
319 "No version of '{}' satisfies range '{}'",
320 metadata.name, range_str
321 )))
322}
323
324fn is_platform_compatible(version_meta: &VersionMetadata) -> bool {
330 if let Some(ref os_list) = version_meta.os {
331 if !os_list.is_empty() && !check_platform_list(os_list, current_os()) {
332 return false;
333 }
334 }
335 if let Some(ref cpu_list) = version_meta.cpu {
336 if !cpu_list.is_empty() && !check_platform_list(cpu_list, current_cpu()) {
337 return false;
338 }
339 }
340 true
341}
342
343fn check_platform_list(list: &[String], target: &str) -> bool {
346 let has_negations = list.iter().any(|s| s.starts_with('!'));
347 let has_positives = list.iter().any(|s| !s.starts_with('!'));
348
349 if has_negations {
351 for entry in list {
352 if let Some(excluded) = entry.strip_prefix('!') {
353 if excluded == target {
354 return false;
355 }
356 }
357 }
358 }
359
360 if has_positives {
362 return list.iter().any(|s| !s.starts_with('!') && s == target);
363 }
364
365 true
367}
368
369fn current_os() -> &'static str {
371 if cfg!(target_os = "windows") {
372 "win32"
373 } else if cfg!(target_os = "macos") {
374 "darwin"
375 } else if cfg!(target_os = "linux") {
376 "linux"
377 } else if cfg!(target_os = "freebsd") {
378 "freebsd"
379 } else if cfg!(target_os = "openbsd") {
380 "openbsd"
381 } else {
382 "unknown"
383 }
384}
385
386fn current_cpu() -> &'static str {
388 if cfg!(target_arch = "x86_64") {
389 "x64"
390 } else if cfg!(target_arch = "aarch64") {
391 "arm64"
392 } else if cfg!(target_arch = "x86") {
393 "ia32"
394 } else if cfg!(target_arch = "arm") {
395 "arm"
396 } else {
397 "unknown"
398 }
399}
400
401fn extract_bin(bin: &Option<serde_json::Value>, package_name: &str) -> HashMap<String, String> {
408 let mut map = HashMap::new();
409 match bin {
410 None => {}
411 Some(serde_json::Value::String(s)) => {
412 let cmd = if let Some(idx) = package_name.rfind('/') {
414 &package_name[idx + 1..]
415 } else {
416 package_name
417 };
418 map.insert(cmd.to_string(), s.clone());
419 }
420 Some(serde_json::Value::Object(obj)) => {
421 for (key, val) in obj {
422 if let serde_json::Value::String(s) = val {
423 map.insert(key.clone(), s.clone());
424 }
425 }
426 }
427 _ => {
428 warn!(
429 "Unexpected bin format for package '{}': {:?}",
430 package_name, bin
431 );
432 }
433 }
434 map
435}
436
437fn integrity_string(dist: &crate::registry::DistInfo) -> String {
441 if let Some(ref integrity) = dist.integrity {
442 if !integrity.is_empty() && integrity.starts_with("sha512-") {
443 return integrity.clone();
444 }
445 }
446 if let Some(ref integrity) = dist.integrity {
448 if !integrity.is_empty() && !integrity.starts_with("sha1-") {
449 return integrity.clone();
450 }
451 }
452 String::new()
455}
456
457type OptionalDepsTriple = (
459 Option<HashMap<String, String>>,
460 Option<HashMap<String, String>>,
461 Option<HashMap<String, String>>,
462);
463
464impl Resolver {
469 pub fn new(registry: Arc<RegistryClient>) -> Self {
471 Self {
472 registry,
473 overrides: HashMap::new(),
474 workspace_packages: HashMap::new(),
475 auto_install_peers: true,
476 hooks: RefCell::new(Hooks::Noop),
477 }
478 }
479
480 pub fn with_overrides(
482 registry: Arc<RegistryClient>,
483 overrides: HashMap<String, String>,
484 ) -> Self {
485 Self {
486 registry,
487 overrides,
488 workspace_packages: HashMap::new(),
489 auto_install_peers: true,
490 hooks: RefCell::new(Hooks::Noop),
491 }
492 }
493
494 pub fn with_workspace(
496 registry: Arc<RegistryClient>,
497 overrides: HashMap<String, String>,
498 workspace_packages: HashMap<String, (String, std::path::PathBuf)>,
499 ) -> Self {
500 Self {
501 registry,
502 overrides,
503 workspace_packages,
504 auto_install_peers: true,
505 hooks: RefCell::new(Hooks::Noop),
506 }
507 }
508
509 pub fn set_hooks(&self, hooks: Hooks) {
511 *self.hooks.borrow_mut() = hooks;
512 }
513
514 pub fn set_auto_install_peers(&mut self, enabled: bool) {
516 self.auto_install_peers = enabled;
517 }
518
519 fn apply_read_package_hook(
522 &self,
523 name: &str,
524 version: &str,
525 deps: &Option<HashMap<String, String>>,
526 opt_deps: &Option<HashMap<String, String>>,
527 peer_deps: &Option<HashMap<String, String>>,
528 ) -> OptionalDepsTriple {
529 let mut hooks = self.hooks.borrow_mut();
530 if !hooks.is_active() {
531 return (deps.clone(), opt_deps.clone(), peer_deps.clone());
532 }
533
534 let hook_pkg = HookPackage {
535 name: name.to_string(),
536 version: version.to_string(),
537 dependencies: deps.clone().unwrap_or_default(),
538 dev_dependencies: HashMap::new(),
539 peer_dependencies: peer_deps.clone().unwrap_or_default(),
540 optional_dependencies: opt_deps.clone().unwrap_or_default(),
541 };
542
543 match hooks.read_package(hook_pkg) {
544 Ok(modified) => {
545 let new_deps = if modified.dependencies.is_empty() {
546 None
547 } else {
548 Some(modified.dependencies)
549 };
550 let new_opt = if modified.optional_dependencies.is_empty() {
551 None
552 } else {
553 Some(modified.optional_dependencies)
554 };
555 let new_peers = if modified.peer_dependencies.is_empty() {
556 None
557 } else {
558 Some(modified.peer_dependencies)
559 };
560 (new_deps, new_opt, new_peers)
561 }
562 Err(e) => {
563 warn!("readPackage hook failed for {}@{}: {}", name, version, e);
564 (deps.clone(), opt_deps.clone(), peer_deps.clone())
565 }
566 }
567 }
568
569 pub async fn resolve(&self, root_deps: &HashMap<String, String>) -> Result<DependencyGraph> {
577 if root_deps.is_empty() {
578 return Ok(DependencyGraph {
579 packages: Vec::new(),
580 });
581 }
582
583 let mut metadata_cache: HashMap<String, Arc<PackageMetadata>> = HashMap::new();
585
586 let mut root_names: Vec<String> = Vec::new();
589 for (name, range) in root_deps {
590 let spec = DependencySpec::parse(range);
591 if let DependencySpec::Alias { ref real_name, .. } = spec {
592 root_names.push(real_name.clone());
593 } else {
594 root_names.push(name.clone());
595 }
596 }
597 let batch_results = self.registry.fetch_metadata_batch(root_names).await;
598 for (name, result) in batch_results {
599 match result {
600 Ok(meta) => {
601 metadata_cache.insert(name, Arc::new(meta));
602 }
603 Err(e) => {
604 debug!("Failed to prefetch metadata for '{}': {}", name, e);
605 }
606 }
607 }
608
609 let mut resolved: HashMap<String, Vec<(String, ResolvedPackage)>> = HashMap::new();
613
614 let mut queue: VecDeque<QueueEntry> = VecDeque::new();
616
617 let mut auto_installed_peers: std::collections::HashSet<String> =
619 std::collections::HashSet::new();
620
621 for (name, range) in root_deps {
623 queue.push_back(QueueEntry {
624 name: name.clone(),
625 range: range.clone(),
626 optional: false,
627 });
628 }
629
630 const BATCH_SIZE: usize = 32;
631
632 while !queue.is_empty() {
633 let batch: Vec<QueueEntry> = queue.drain(..queue.len().min(BATCH_SIZE)).collect();
635
636 let missing_names: Vec<String> = batch
639 .iter()
640 .filter(|entry| !has_satisfying_version(&resolved, &entry.name, &entry.range))
641 .map(|entry| {
642 let spec = DependencySpec::parse(&entry.range);
643 if let DependencySpec::Alias { ref real_name, .. } = spec {
644 real_name.clone()
645 } else {
646 entry.name.clone()
647 }
648 })
649 .filter(|name| !metadata_cache.contains_key(name))
650 .collect::<std::collections::HashSet<_>>()
651 .into_iter()
652 .collect();
653
654 if !missing_names.is_empty() {
656 let batch_results = self.registry.fetch_metadata_batch(missing_names).await;
657 for (name, result) in batch_results {
658 match result {
659 Ok(meta) => {
660 metadata_cache.insert(name, Arc::new(meta));
661 }
662 Err(e) => {
663 debug!("Failed to fetch metadata for '{}': {}", name, e);
664 }
666 }
667 }
668 }
669
670 for entry in batch {
672 if let Some(versions) = resolved.get(&entry.name) {
674 let any_satisfies = versions
676 .iter()
677 .any(|(ver, _)| version_satisfies(ver, &entry.range));
678 if any_satisfies {
679 trace!(
680 "{} already has a version satisfying '{}'",
681 entry.name,
682 entry.range
683 );
684 continue;
685 }
686 debug!(
688 "Resolving additional version of '{}' for range '{}' (existing: {})",
689 entry.name,
690 entry.range,
691 versions
692 .iter()
693 .map(|(v, _)| v.as_str())
694 .collect::<Vec<_>>()
695 .join(", ")
696 );
697 }
698
699 if let Some((ws_version, ws_path)) = self.workspace_packages.get(&entry.name) {
701 let spec = DependencySpec::parse(&entry.range);
702 let is_ws_dep = matches!(spec, DependencySpec::Workspace { .. })
703 || self.workspace_packages.contains_key(&entry.name);
704
705 if is_ws_dep {
706 if let DependencySpec::Workspace { ref range } = spec {
708 if range != "*" && !range.is_empty() {
709 let effective_range = if range == "^" {
711 format!("^{}", ws_version)
712 } else if range == "~" {
713 format!("~{}", ws_version)
714 } else {
715 range.clone()
716 };
717 if !version_satisfies(ws_version, &effective_range) {
718 warn!(
719 "Workspace package '{}@{}' does not satisfy range '{}'",
720 entry.name, ws_version, effective_range
721 );
722 }
723 }
724 }
725
726 let relative_path = ws_path.to_string_lossy().replace('\\', "/");
727 let pkg = ResolvedPackage {
728 name: entry.name.clone(),
729 version: format!("workspace:{}", relative_path),
730 tarball_url: String::new(),
731 integrity: String::new(),
732 dependencies: Vec::new(),
733 peer_dependencies: Vec::new(),
734 bin: HashMap::new(),
735 has_install_script: false,
736 };
737 resolved
738 .entry(entry.name.clone())
739 .or_default()
740 .push((pkg.version.clone(), pkg));
741 continue;
742 }
743 }
744
745 let spec = DependencySpec::parse(&entry.range);
747 if !spec.is_semver() {
748 match &spec {
749 DependencySpec::Alias { real_name, range } => {
750 let alias_meta = match metadata_cache.get(real_name) {
753 Some(m) => Arc::clone(m),
754 None => {
755 match self.registry.fetch_package_metadata(real_name).await {
756 Ok(m) => {
757 let arc = Arc::new(m);
758 metadata_cache
759 .insert(real_name.clone(), Arc::clone(&arc));
760 arc
761 }
762 Err(e) => {
763 if entry.optional {
764 debug!(
765 "Skipping optional alias dep '{}': {}",
766 entry.name, e
767 );
768 continue;
769 }
770 return Err(e);
771 }
772 }
773 }
774 };
775
776 let override_ver = self.overrides.get(&entry.name).map(|s| s.as_str());
777 let (version, version_meta) =
778 match find_best_version(&alias_meta, range, override_ver) {
779 Ok(v) => v,
780 Err(e) => {
781 if entry.optional {
782 debug!(
783 "Skipping optional alias dep '{}': {}",
784 entry.name, e
785 );
786 continue;
787 }
788 return Err(e);
789 }
790 };
791
792 if !is_platform_compatible(version_meta) {
793 debug!(
794 "Skipping {}@{}: incompatible platform",
795 entry.name, version
796 );
797 continue;
798 }
799
800 debug!("Resolved alias {} → {}@{}", entry.name, real_name, version);
801
802 let (hooked_deps, hooked_opt_deps, hooked_peers) = self
804 .apply_read_package_hook(
805 real_name,
806 &version,
807 &version_meta.dependencies,
808 &version_meta.optional_dependencies,
809 &version_meta.peer_dependencies,
810 );
811
812 let mut dep_refs: Vec<String> = Vec::new();
813 let mut peer_dep_refs: Vec<String> = Vec::new();
814
815 if let Some(ref deps) = hooked_deps {
816 for (dep_name, dep_range) in deps {
817 dep_refs.push(format!("{}@{}", dep_name, dep_range));
818 if !has_satisfying_version(&resolved, dep_name, dep_range) {
819 queue.push_back(QueueEntry {
820 name: dep_name.clone(),
821 range: dep_range.clone(),
822 optional: false,
823 });
824 }
825 }
826 }
827
828 if let Some(ref opt_deps) = hooked_opt_deps {
829 for (dep_name, dep_range) in opt_deps {
830 dep_refs.push(format!("{}@{}", dep_name, dep_range));
831 if !has_satisfying_version(&resolved, dep_name, dep_range) {
832 queue.push_back(QueueEntry {
833 name: dep_name.clone(),
834 range: dep_range.clone(),
835 optional: true,
836 });
837 }
838 }
839 }
840
841 if let Some(ref peers) = hooked_peers {
842 for (dep_name, dep_range) in peers {
843 peer_dep_refs.push(format!("{}@{}", dep_name, dep_range));
844
845 if self.auto_install_peers {
846 let peer_key = format!("{}@{}", dep_name, dep_range);
847
848 if !has_satisfying_version(&resolved, dep_name, dep_range)
849 && !auto_installed_peers.contains(&peer_key)
850 {
851 debug!(
852 "Auto-installing peer dependency: {} requires {}",
853 entry.name, peer_key
854 );
855 queue.push_back(QueueEntry {
856 name: dep_name.clone(),
857 range: dep_range.clone(),
858 optional: false,
859 });
860 auto_installed_peers.insert(peer_key);
861 }
862 }
863 }
864 }
865
866 let pkg = ResolvedPackage {
867 name: entry.name.clone(),
868 version: version.clone(),
869 tarball_url: version_meta.dist.tarball.clone(),
870 integrity: integrity_string(&version_meta.dist),
871 dependencies: dep_refs,
872 peer_dependencies: peer_dep_refs,
873 bin: extract_bin(&version_meta.bin, &entry.name),
874 has_install_script: version_meta
875 .has_install_script
876 .unwrap_or(false),
877 };
878
879 resolved
880 .entry(entry.name.clone())
881 .or_default()
882 .push((version, pkg));
883 continue;
884 }
885 DependencySpec::Workspace { .. } => {
886 warn!(
889 "Workspace dependency '{}' not found in workspace members",
890 entry.name
891 );
892 continue;
893 }
894 DependencySpec::Catalog { .. } => {
895 warn!(
897 "Unresolved catalog dependency '{}': catalog refs must be resolved before resolution",
898 entry.name
899 );
900 continue;
901 }
902 DependencySpec::Link { path } => {
903 debug!(
904 "Link dependency '{}' -> {} (will be symlinked by linker)",
905 entry.name, path
906 );
907 let pkg = ResolvedPackage {
908 name: entry.name.clone(),
909 version: format!("link:{}", path),
910 tarball_url: String::new(),
911 integrity: String::new(),
912 dependencies: Vec::new(),
913 peer_dependencies: Vec::new(),
914 bin: HashMap::new(),
915 has_install_script: false,
916 };
917 resolved
918 .entry(entry.name.clone())
919 .or_default()
920 .push((pkg.version.clone(), pkg));
921 continue;
922 }
923 DependencySpec::File { path } => {
924 debug!("File dependency '{}' -> {}", entry.name, path);
925 let mut dep_refs: Vec<String> = Vec::new();
927 let file_path = std::path::Path::new(&path);
928 let pkg_json_path = file_path.join("package.json");
929 let mut has_scripts = false;
930 let mut file_bin = HashMap::new();
931 if let Ok(content) = std::fs::read_to_string(&pkg_json_path) {
932 if let Ok(json) =
933 serde_json::from_str::<serde_json::Value>(&content)
934 {
935 if let Some(deps) =
937 json.get("dependencies").and_then(|d| d.as_object())
938 {
939 for (dep_name, dep_range) in deps {
940 if let Some(range) = dep_range.as_str() {
941 dep_refs.push(format!("{}@{}", dep_name, range));
942 if !has_satisfying_version(
943 &resolved, dep_name, range,
944 ) {
945 queue.push_back(QueueEntry {
946 name: dep_name.clone(),
947 range: range.to_string(),
948 optional: false,
949 });
950 }
951 }
952 }
953 }
954 if let Some(scripts) =
956 json.get("scripts").and_then(|s| s.as_object())
957 {
958 has_scripts = scripts.contains_key("preinstall")
959 || scripts.contains_key("install")
960 || scripts.contains_key("postinstall");
961 }
962 file_bin = extract_bin(&json.get("bin").cloned(), &entry.name);
964 }
965 }
966 let pkg = ResolvedPackage {
967 name: entry.name.clone(),
968 version: format!("file:{}", path),
969 tarball_url: String::new(),
970 integrity: String::new(),
971 dependencies: dep_refs,
972 peer_dependencies: Vec::new(),
973 bin: file_bin,
974 has_install_script: has_scripts,
975 };
976 resolved
977 .entry(entry.name.clone())
978 .or_default()
979 .push((pkg.version.clone(), pkg));
980 continue;
981 }
982 DependencySpec::Git { url, reference } => {
983 debug!(
984 "Git dependency '{}' -> {}#{}",
985 entry.name,
986 url,
987 reference.as_deref().unwrap_or("HEAD")
988 );
989 let version =
990 format!("git+{}#{}", url, reference.as_deref().unwrap_or("HEAD"));
991 let pkg = ResolvedPackage {
992 name: entry.name.clone(),
993 version: version.clone(),
994 tarball_url: String::new(),
995 integrity: String::new(),
996 dependencies: Vec::new(),
997 peer_dependencies: Vec::new(),
998 bin: HashMap::new(),
999 has_install_script: false,
1000 };
1001 resolved
1002 .entry(entry.name.clone())
1003 .or_default()
1004 .push((version, pkg));
1005 continue;
1006 }
1007 DependencySpec::Github {
1008 user,
1009 repo,
1010 reference,
1011 } => {
1012 debug!(
1013 "GitHub dependency '{}' -> {}/{}#{}",
1014 entry.name,
1015 user,
1016 repo,
1017 reference.as_deref().unwrap_or("HEAD")
1018 );
1019 let version = format!(
1020 "github:{}/{}#{}",
1021 user,
1022 repo,
1023 reference.as_deref().unwrap_or("HEAD")
1024 );
1025 let pkg = ResolvedPackage {
1026 name: entry.name.clone(),
1027 version: version.clone(),
1028 tarball_url: format!(
1029 "https://codeload.github.com/{}/{}/tar.gz/{}",
1030 user,
1031 repo,
1032 reference.as_deref().unwrap_or("HEAD")
1033 ),
1034 integrity: String::new(),
1035 dependencies: Vec::new(),
1036 peer_dependencies: Vec::new(),
1037 bin: HashMap::new(),
1038 has_install_script: false,
1039 };
1040 resolved
1041 .entry(entry.name.clone())
1042 .or_default()
1043 .push((version, pkg));
1044 continue;
1045 }
1046 _ => {} }
1048 }
1049
1050 let metadata = match metadata_cache.get(&entry.name) {
1052 Some(m) => Arc::clone(m),
1053 None => {
1054 match self.registry.fetch_package_metadata(&entry.name).await {
1056 Ok(m) => {
1057 let arc = Arc::new(m);
1058 metadata_cache.insert(entry.name.clone(), Arc::clone(&arc));
1059 arc
1060 }
1061 Err(e) => {
1062 if entry.optional {
1063 debug!("Skipping optional dependency '{}': {}", entry.name, e);
1064 continue;
1065 }
1066 return Err(e);
1067 }
1068 }
1069 }
1070 };
1071
1072 let override_ver = self.overrides.get(&entry.name).map(|s| s.as_str());
1074 let (version, version_meta) =
1075 match find_best_version(&metadata, &entry.range, override_ver) {
1076 Ok(v) => v,
1077 Err(e) => {
1078 if entry.optional {
1079 debug!("Skipping optional dependency '{}': {}", entry.name, e);
1080 continue;
1081 }
1082 return Err(e);
1083 }
1084 };
1085
1086 if !is_platform_compatible(version_meta) {
1088 debug!(
1089 "Skipping {}@{}: incompatible with current platform (os={}, cpu={})",
1090 entry.name,
1091 version,
1092 current_os(),
1093 current_cpu()
1094 );
1095 continue;
1096 }
1097
1098 debug!("Resolved {}@{}", entry.name, version);
1099
1100 let (hooked_deps, hooked_opt_deps, hooked_peers) = self.apply_read_package_hook(
1102 &entry.name,
1103 &version,
1104 &version_meta.dependencies,
1105 &version_meta.optional_dependencies,
1106 &version_meta.peer_dependencies,
1107 );
1108
1109 let mut dep_refs: Vec<String> = Vec::new();
1111 let mut peer_dep_refs: Vec<String> = Vec::new();
1112
1113 if let Some(ref deps) = hooked_deps {
1114 for (dep_name, dep_range) in deps {
1115 dep_refs.push(format!("{}@{}", dep_name, dep_range));
1116 if !has_satisfying_version(&resolved, dep_name, dep_range) {
1117 queue.push_back(QueueEntry {
1118 name: dep_name.clone(),
1119 range: dep_range.clone(),
1120 optional: false,
1121 });
1122 }
1123 }
1124 }
1125
1126 if let Some(ref opt_deps) = hooked_opt_deps {
1128 for (dep_name, dep_range) in opt_deps {
1129 dep_refs.push(format!("{}@{}", dep_name, dep_range));
1130 if !has_satisfying_version(&resolved, dep_name, dep_range) {
1131 queue.push_back(QueueEntry {
1132 name: dep_name.clone(),
1133 range: dep_range.clone(),
1134 optional: true,
1135 });
1136 }
1137 }
1138 }
1139
1140 if let Some(ref peers) = hooked_peers {
1142 for (dep_name, dep_range) in peers {
1143 peer_dep_refs.push(format!("{}@{}", dep_name, dep_range));
1144
1145 if self.auto_install_peers {
1146 let peer_key = format!("{}@{}", dep_name, dep_range);
1147
1148 if !has_satisfying_version(&resolved, dep_name, dep_range)
1149 && !auto_installed_peers.contains(&peer_key)
1150 {
1151 debug!(
1152 "Auto-installing peer dependency: {} requires {}",
1153 entry.name, peer_key
1154 );
1155 queue.push_back(QueueEntry {
1156 name: dep_name.clone(),
1157 range: dep_range.clone(),
1158 optional: false,
1159 });
1160 auto_installed_peers.insert(peer_key);
1161 }
1162 }
1163 }
1164 }
1165
1166 let pkg = ResolvedPackage {
1168 name: entry.name.clone(),
1169 version: version.clone(),
1170 tarball_url: version_meta.dist.tarball.clone(),
1171 integrity: integrity_string(&version_meta.dist),
1172 dependencies: dep_refs,
1173 peer_dependencies: peer_dep_refs,
1174 bin: extract_bin(&version_meta.bin, &entry.name),
1175 has_install_script: version_meta.has_install_script.unwrap_or(false),
1176 };
1177
1178 resolved
1179 .entry(entry.name.clone())
1180 .or_default()
1181 .push((version, pkg));
1182 }
1183 }
1184
1185 let resolved_versions: HashMap<String, Vec<String>> = resolved
1188 .iter()
1189 .map(|(name, versions)| {
1190 (
1191 name.clone(),
1192 versions.iter().map(|(ver, _)| ver.clone()).collect(),
1193 )
1194 })
1195 .collect();
1196
1197 let packages: Vec<ResolvedPackage> = resolved
1199 .into_values()
1200 .flat_map(|versions| versions.into_iter().map(|(_, pkg)| pkg))
1201 .map(|mut pkg| {
1202 pkg.dependencies = pkg
1203 .dependencies
1204 .iter()
1205 .filter_map(|dep_ref| {
1206 let (dep_name, dep_range) = split_dep_ref(dep_ref);
1207 find_best_resolved_version(&resolved_versions, dep_name, dep_range)
1208 .map(|ver| format!("{}@{}", dep_name, ver))
1209 })
1210 .collect();
1211 pkg.peer_dependencies = pkg
1213 .peer_dependencies
1214 .iter()
1215 .filter_map(|dep_ref| {
1216 let (dep_name, dep_range) = split_dep_ref(dep_ref);
1217 find_best_resolved_version(&resolved_versions, dep_name, dep_range)
1218 .map(|ver| format!("{}@{}", dep_name, ver))
1219 })
1220 .collect();
1221 pkg
1222 })
1223 .collect();
1224
1225 if !self.auto_install_peers {
1229 let pkg_map: HashMap<&str, &ResolvedPackage> =
1230 packages.iter().map(|p| (p.name.as_str(), p)).collect();
1231 for pkg in &packages {
1232 if pkg.peer_dependencies.is_empty() {
1233 continue;
1234 }
1235 for peer_ref in &pkg.peer_dependencies {
1236 let (peer_name, _) = split_dep_ref(peer_ref);
1237 if !pkg_map.contains_key(peer_name) {
1238 warn!(
1239 "Unmet peer dependency: '{}' requires peer '{}' but it is not installed.",
1240 pkg.name, peer_ref
1241 );
1242 }
1243 }
1244 }
1245 }
1246
1247 Ok(DependencyGraph { packages })
1248 }
1249}
1250
1251fn split_dep_ref(dep_ref: &str) -> (&str, &str) {
1259 if let Some(stripped) = dep_ref.strip_prefix('@') {
1262 if let Some(idx) = stripped.find('@') {
1263 return (&dep_ref[..idx + 1], &stripped[idx + 1..]);
1264 }
1265 return (dep_ref, "");
1267 }
1268 if let Some(idx) = dep_ref.find('@') {
1270 (&dep_ref[..idx], &dep_ref[idx + 1..])
1271 } else {
1272 (dep_ref, "")
1273 }
1274}
1275
1276fn has_satisfying_version(
1278 resolved: &HashMap<String, Vec<(String, ResolvedPackage)>>,
1279 name: &str,
1280 range: &str,
1281) -> bool {
1282 match resolved.get(name) {
1283 None => false,
1284 Some(versions) => versions
1285 .iter()
1286 .any(|(ver, _)| version_satisfies(ver, range)),
1287 }
1288}
1289
1290fn find_best_resolved_version(
1293 resolved_versions: &HashMap<String, Vec<String>>,
1294 name: &str,
1295 range: &str,
1296) -> Option<String> {
1297 resolved_versions.get(name).and_then(|versions| {
1298 versions
1300 .iter()
1301 .find(|ver| version_satisfies(ver, range))
1302 .or_else(|| versions.first())
1303 .cloned()
1304 })
1305}
1306
1307fn version_satisfies(version_str: &str, range_str: &str) -> bool {
1309 let range_str = range_str.trim();
1310
1311 let effective = match range_str {
1313 "" | "*" | "latest" => ">=0.0.0",
1314 s => s,
1315 };
1316
1317 let version = match cached_parse_version(version_str) {
1318 Some(v) => v,
1319 None => return false,
1320 };
1321
1322 let range = match cached_parse_range(effective) {
1323 Some(r) => r,
1324 None => return false,
1325 };
1326
1327 range.satisfies(&version)
1328}
1329
1330#[cfg(test)]
1335mod tests {
1336 use super::*;
1337 use crate::registry::DistInfo;
1338
1339 fn make_metadata(name: &str, versions: &[&str]) -> PackageMetadata {
1341 let mut ver_map = HashMap::new();
1342 for v in versions {
1343 ver_map.insert(
1344 v.to_string(),
1345 VersionMetadata {
1346 name: name.to_string(),
1347 version: v.to_string(),
1348 dependencies: None,
1349 dev_dependencies: None,
1350 peer_dependencies: None,
1351 optional_dependencies: None,
1352 dist: DistInfo {
1353 tarball: format!(
1354 "https://registry.npmjs.org/{}/-/{}-{}.tgz",
1355 name, name, v
1356 ),
1357 shasum: "abc123".to_string(),
1358 integrity: Some("sha512-test".to_string()),
1359 },
1360 bin: None,
1361 os: None,
1362 cpu: None,
1363 has_install_script: None,
1364 },
1365 );
1366 }
1367 let latest = versions.last().unwrap_or(&"0.0.0").to_string();
1368 let mut dist_tags = HashMap::new();
1369 dist_tags.insert("latest".to_string(), latest);
1370 PackageMetadata {
1371 name: name.to_string(),
1372 versions: ver_map,
1373 dist_tags,
1374 }
1375 }
1376
1377 #[test]
1378 fn test_find_best_version_caret() {
1379 let meta = make_metadata("foo", &["1.0.0", "1.2.3", "1.5.0", "2.0.0"]);
1380 let (ver, _) = find_best_version(&meta, "^1.2.0", None).unwrap();
1381 assert_eq!(ver, "1.5.0");
1382 }
1383
1384 #[test]
1385 fn test_find_best_version_tilde() {
1386 let meta = make_metadata("foo", &["1.0.0", "1.2.3", "1.2.9", "1.3.0"]);
1387 let (ver, _) = find_best_version(&meta, "~1.2.0", None).unwrap();
1388 assert_eq!(ver, "1.2.9");
1389 }
1390
1391 #[test]
1392 fn test_find_best_version_wildcard() {
1393 let meta = make_metadata("foo", &["1.0.0", "2.0.0", "3.0.0"]);
1394 let (ver, _) = find_best_version(&meta, "*", None).unwrap();
1395 assert_eq!(ver, "3.0.0");
1396 }
1397
1398 #[test]
1399 fn test_find_best_version_exact() {
1400 let meta = make_metadata("foo", &["1.0.0", "1.2.3", "2.0.0"]);
1401 let (ver, _) = find_best_version(&meta, "1.2.3", None).unwrap();
1402 assert_eq!(ver, "1.2.3");
1403 }
1404
1405 #[test]
1406 fn test_find_best_version_latest_tag() {
1407 let meta = make_metadata("foo", &["1.0.0", "2.0.0"]);
1408 let (ver, _) = find_best_version(&meta, "latest", None).unwrap();
1409 assert_eq!(ver, "2.0.0");
1410 }
1411
1412 #[test]
1413 fn test_find_best_version_no_match() {
1414 let meta = make_metadata("foo", &["1.0.0", "1.1.0"]);
1415 let result = find_best_version(&meta, "^2.0.0", None);
1416 assert!(result.is_err());
1417 }
1418
1419 #[test]
1420 fn test_extract_bin_none() {
1421 let result = extract_bin(&None, "my-pkg");
1422 assert!(result.is_empty());
1423 }
1424
1425 #[test]
1426 fn test_extract_bin_string() {
1427 let val = serde_json::Value::String("./bin/cli.js".to_string());
1428 let result = extract_bin(&Some(val), "my-pkg");
1429 assert_eq!(result.get("my-pkg").unwrap(), "./bin/cli.js");
1430 }
1431
1432 #[test]
1433 fn test_extract_bin_string_scoped() {
1434 let val = serde_json::Value::String("./bin/cli.js".to_string());
1435 let result = extract_bin(&Some(val), "@scope/my-pkg");
1436 assert_eq!(result.get("my-pkg").unwrap(), "./bin/cli.js");
1437 }
1438
1439 #[test]
1440 fn test_extract_bin_object() {
1441 let obj = serde_json::json!({
1442 "cmd1": "./bin/cmd1.js",
1443 "cmd2": "./bin/cmd2.js"
1444 });
1445 let result = extract_bin(&Some(obj), "my-pkg");
1446 assert_eq!(result.len(), 2);
1447 assert_eq!(result.get("cmd1").unwrap(), "./bin/cmd1.js");
1448 assert_eq!(result.get("cmd2").unwrap(), "./bin/cmd2.js");
1449 }
1450
1451 #[test]
1452 fn test_split_dep_ref_simple() {
1453 let (name, range) = split_dep_ref("react@^18.0.0");
1454 assert_eq!(name, "react");
1455 assert_eq!(range, "^18.0.0");
1456 }
1457
1458 #[test]
1459 fn test_split_dep_ref_scoped() {
1460 let (name, range) = split_dep_ref("@babel/core@^7.0.0");
1461 assert_eq!(name, "@babel/core");
1462 assert_eq!(range, "^7.0.0");
1463 }
1464
1465 #[test]
1466 fn test_version_satisfies_basic() {
1467 assert!(version_satisfies("1.5.0", "^1.2.0"));
1468 assert!(!version_satisfies("2.0.0", "^1.2.0"));
1469 assert!(version_satisfies("3.0.0", "*"));
1470 assert!(version_satisfies("1.0.0", "latest"));
1471 }
1472}