1pub mod config;
2pub mod document;
3pub mod file_watcher;
4pub mod handlers;
5pub mod progress;
6pub mod server;
7
8#[cfg(test)]
9mod test_utils;
10
11use std::sync::Arc;
12use std::sync::atomic::AtomicBool;
13
14pub use deps_core::{DepsError, EcosystemRegistry, HttpCache, Result};
15pub use server::Backend;
16
17#[derive(Debug, Clone)]
22pub struct EcosystemRuntime {
23 pub policy: Arc<deps_core::net_policy::RegistryAccessPolicy>,
26 pub nuget_user_profile_sources: Arc<AtomicBool>,
30 pub gitlab_instance_host: Arc<std::sync::RwLock<Option<String>>>,
36}
37
38macro_rules! ecosystem {
40 ($feature:literal, $crate_name:ident, $ecosystem:ident, [$($types:ident),* $(,)?]) => {
41 #[cfg(feature = $feature)]
42 pub use $crate_name::{$ecosystem, $($types),*};
43 };
44}
45
46macro_rules! register {
48 ($feature:literal, $ecosystem:ident, $registry:expr, $cache:expr) => {
49 #[cfg(feature = $feature)]
50 $registry.register(Arc::new($ecosystem::new(Arc::clone($cache))));
51 };
52}
53
54ecosystem!(
59 "cargo",
60 deps_cargo,
61 CargoEcosystem,
62 [
63 CargoParser,
64 CargoVersion,
65 CrateInfo,
66 CratesIoRegistry,
67 DependencySection,
68 DependencySource,
69 ParseResult,
70 ParsedDependency,
71 parse_cargo_toml,
72 ]
73);
74
75ecosystem!(
76 "npm",
77 deps_npm,
78 NpmEcosystem,
79 [
80 NpmDependency,
81 NpmDependencySection,
82 NpmPackage,
83 NpmParseResult,
84 NpmRegistry,
85 NpmVersion,
86 parse_package_json,
87 ]
88);
89
90ecosystem!(
91 "pypi",
92 deps_pypi,
93 PypiEcosystem,
94 [
95 PypiDependency,
96 PypiDependencySection,
97 PypiParser,
98 PypiRegistry,
99 PypiVersion,
100 ]
101);
102
103ecosystem!(
104 "go",
105 deps_go,
106 GoEcosystem,
107 [
108 GoDependency,
109 GoDirective,
110 GoParseResult,
111 GoRegistry,
112 GoVersion,
113 parse_go_mod,
114 ]
115);
116
117ecosystem!(
118 "bundler",
119 deps_bundler,
120 BundlerEcosystem,
121 [
122 BundlerDependency,
123 BundlerParseResult,
124 BundlerVersion,
125 DependencyGroup,
126 GemInfo,
127 GemfileLockParser,
128 RubyGemsRegistry,
129 parse_gemfile,
130 ]
131);
132
133ecosystem!(
134 "dart",
135 deps_dart,
136 DartEcosystem,
137 [
138 DartDependency,
139 DartParseResult,
140 DartVersion,
141 DartFormatter,
142 PackageInfo,
143 PubDevRegistry,
144 PubspecLockParser,
145 parse_pubspec_yaml,
146 ]
147);
148
149ecosystem!(
150 "maven",
151 deps_maven,
152 MavenEcosystem,
153 [
154 MavenDependency,
155 MavenParseResult,
156 MavenVersion,
157 MavenFormatter,
158 ArtifactInfo,
159 MavenCentralRegistry,
160 parse_pom_xml,
161 ]
162);
163
164ecosystem!(
165 "gradle",
166 deps_gradle,
167 GradleEcosystem,
168 [
169 GradleDependency,
170 GradleParseResult,
171 GradleVersion,
172 GradleFormatter,
173 parse_gradle,
174 ]
175);
176
177ecosystem!(
178 "swift",
179 deps_swift,
180 SwiftEcosystem,
181 [
182 SwiftDependency,
183 SwiftParseResult,
184 SwiftVersion,
185 SwiftPackage,
186 SwiftFormatter,
187 SwiftRegistry,
188 SwiftLockParser,
189 parse_package_swift,
190 ]
191);
192
193ecosystem!(
194 "composer",
195 deps_composer,
196 ComposerEcosystem,
197 [
198 ComposerDependency,
199 ComposerSection,
200 ComposerPackage,
201 ComposerParseResult,
202 PackagistRegistry,
203 ComposerVersion,
204 parse_composer_json,
205 ]
206);
207
208ecosystem!(
212 "nuget",
213 deps_nuget,
214 NuGetEcosystem,
215 [
216 NuGetDependency,
217 NuGetParseResult,
218 NuGetVersion,
219 NuGetFormatter,
220 NuGetRegistry,
221 NuGetLockParser,
222 parse_project_file,
223 ]
224);
225
226ecosystem!(
227 "deno",
228 deps_deno,
229 DenoEcosystem,
230 [
231 DenoDependency,
232 DenoDependencySection,
233 DenoFormatter,
234 DenoMetadata,
235 DenoParseResult,
236 DenoRegistry,
237 JsrPackage,
238 JsrRegistry,
239 JsrVersion,
240 parse_deno_json,
241 ]
242);
243
244ecosystem!(
245 "github-actions",
246 deps_github_actions,
247 GithubActionsEcosystem,
248 [
249 GithubActionsDependency,
250 GithubActionsFormatter,
251 GithubActionsParseResult,
252 GithubActionsRegistry,
253 GithubActionsVersion,
254 parse_workflow_yaml,
255 ]
256);
257
258ecosystem!(
259 "gitlab-ci",
260 deps_gitlab_ci,
261 GitlabCiEcosystem,
262 [
263 GitlabCiDependency,
264 GitlabCiFormatter,
265 GitlabCiParseResult,
266 GitlabCiRegistry,
267 GitlabCiVersion,
268 parse_gitlab_ci_yaml,
269 ]
270);
271
272pub fn register_ecosystems(
298 registry: &EcosystemRegistry,
299 cache: Arc<HttpCache>,
300 runtime: &EcosystemRuntime,
301) -> Vec<&'static str> {
302 let policy = Arc::clone(&runtime.policy);
303 let _ = &policy;
305 let mut workspace_registry_ecosystems = Vec::new();
306
307 #[cfg(feature = "cargo")]
308 {
309 let context = deps_cargo::parser::CargoParseContext {
310 policy: Arc::clone(&policy),
311 config_cache: Arc::new(deps_cargo::config::ConfigFileCache::new()),
312 };
313 registry.register(Arc::new(CargoEcosystem::with_context(
314 Arc::clone(&cache),
315 context,
316 )));
317 workspace_registry_ecosystems.push("cargo");
318 }
319
320 #[cfg(all(feature = "npm", feature = "deno"))]
321 {
322 let npm_context = deps_npm::config::NpmParseContext {
323 policy: Arc::clone(&policy),
324 config_cache: Arc::new(deps_npm::config::NpmConfigCache::new()),
325 workspace_cache: Arc::new(deps_npm::catalog::PnpmWorkspaceCache::new()),
326 };
327 let npm_registry = Arc::new(NpmRegistry::new(Arc::clone(&cache)));
328 registry.register(Arc::new(NpmEcosystem::with_context(
329 Arc::clone(&npm_registry),
330 npm_context,
331 )));
332 workspace_registry_ecosystems.push("npm");
333 registry.register(Arc::new(DenoEcosystem::with_npm(
337 Arc::clone(&cache),
338 npm_registry.as_ref().clone(),
339 )));
340 }
341 #[cfg(all(feature = "npm", not(feature = "deno")))]
346 {
347 let npm_context = deps_npm::config::NpmParseContext {
348 policy: Arc::clone(&policy),
349 config_cache: Arc::new(deps_npm::config::NpmConfigCache::new()),
350 workspace_cache: Arc::new(deps_npm::catalog::PnpmWorkspaceCache::new()),
351 };
352 registry.register(Arc::new(NpmEcosystem::with_context(
353 Arc::new(NpmRegistry::new(Arc::clone(&cache))),
354 npm_context,
355 )));
356 workspace_registry_ecosystems.push("npm");
357 }
358 #[cfg(all(feature = "deno", not(feature = "npm")))]
359 register!("deno", DenoEcosystem, registry, &cache);
360
361 #[cfg(feature = "pypi")]
366 {
367 registry.register(Arc::new(PypiEcosystem::with_policy(
368 Arc::new(PypiRegistry::new(Arc::clone(&cache))),
369 Arc::clone(&policy),
370 )));
371 workspace_registry_ecosystems.push("pypi");
372 }
373
374 #[cfg(feature = "go")]
379 {
380 let go_context = deps_go::config::GoParseContext {
381 policy: Arc::clone(&policy),
382 config_cache: Arc::new(deps_go::config::GoEnvCache::new()),
383 goenv_path: deps_go::config::goenv_path(),
384 };
385 registry.register(Arc::new(GoEcosystem::with_context(
386 Arc::new(GoRegistry::new(Arc::clone(&cache))),
387 go_context,
388 )));
389 workspace_registry_ecosystems.push("go");
390 }
391 register!("bundler", BundlerEcosystem, registry, &cache);
392 register!("dart", DartEcosystem, registry, &cache);
393 register!("maven", MavenEcosystem, registry, &cache);
394 register!("gradle", GradleEcosystem, registry, &cache);
395 register!("swift", SwiftEcosystem, registry, &cache);
396 register!("composer", ComposerEcosystem, registry, &cache);
397
398 #[cfg(feature = "nuget")]
403 {
404 let nuget_context = deps_nuget::config::NuGetParseContext::new(
405 Arc::clone(&policy),
406 Arc::new(deps_nuget::config::NuGetConfigCache::new()),
407 Arc::clone(&runtime.nuget_user_profile_sources),
408 );
409 registry.register(Arc::new(NuGetEcosystem::with_context(
410 Arc::new(NuGetRegistry::new(Arc::clone(&cache))),
411 nuget_context,
412 )));
413 workspace_registry_ecosystems.push("nuget");
414 }
415
416 register!("github-actions", GithubActionsEcosystem, registry, &cache);
417
418 #[cfg(feature = "gitlab-ci")]
424 registry.register(Arc::new(GitlabCiEcosystem::with_context(
425 Arc::clone(&cache),
426 Arc::clone(&policy),
427 Arc::clone(&runtime.gitlab_instance_host),
428 )));
429
430 workspace_registry_ecosystems
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 fn test_runtime() -> EcosystemRuntime {
438 EcosystemRuntime {
439 policy: Arc::new(deps_core::net_policy::RegistryAccessPolicy::default()),
440 nuget_user_profile_sources: Arc::new(AtomicBool::new(false)),
441 gitlab_instance_host: Arc::new(std::sync::RwLock::new(None)),
442 }
443 }
444
445 #[test]
446 fn test_register_ecosystems() {
447 let registry = Arc::new(EcosystemRegistry::new());
448 let cache = Arc::new(HttpCache::new());
449 register_ecosystems(®istry, Arc::clone(&cache), &test_runtime());
450
451 #[cfg(feature = "cargo")]
452 assert!(registry.get("cargo").is_some());
453 #[cfg(feature = "npm")]
454 assert!(registry.get("npm").is_some());
455 #[cfg(feature = "pypi")]
456 assert!(registry.get("pypi").is_some());
457 #[cfg(feature = "go")]
458 assert!(registry.get("go").is_some());
459 #[cfg(feature = "bundler")]
460 assert!(registry.get("bundler").is_some());
461 #[cfg(feature = "dart")]
462 assert!(registry.get("dart").is_some());
463 #[cfg(feature = "maven")]
464 assert!(registry.get("maven").is_some());
465 #[cfg(feature = "gradle")]
466 assert!(registry.get("gradle").is_some());
467 #[cfg(feature = "swift")]
468 assert!(registry.get("swift").is_some());
469 #[cfg(feature = "composer")]
470 assert!(registry.get("composer").is_some());
471 #[cfg(feature = "nuget")]
472 assert!(registry.get("nuget").is_some());
473 #[cfg(feature = "deno")]
474 assert!(registry.get("deno").is_some());
475 #[cfg(feature = "github-actions")]
476 assert!(registry.get("github-actions").is_some());
477 #[cfg(feature = "gitlab-ci")]
478 assert!(registry.get("gitlab-ci").is_some());
479 }
480
481 #[test]
489 #[allow(
490 clippy::vec_init_then_push,
491 reason = "each push is independently feature-gated, so a `vec![]` literal can't \
492 express the feature-conditional membership"
493 )]
494 fn test_register_ecosystems_workspace_registry_list_matches_registered_ecosystems() {
495 let registry = Arc::new(EcosystemRegistry::new());
496 let cache = Arc::new(HttpCache::new());
497 let workspace_registry_ecosystems =
498 register_ecosystems(®istry, Arc::clone(&cache), &test_runtime());
499
500 for id in &workspace_registry_ecosystems {
501 assert!(
502 registry.get(id).is_some(),
503 "{id:?} was returned as policy-consuming but is not a registered ecosystem"
504 );
505 }
506
507 let mut expected = Vec::new();
508 #[cfg(feature = "cargo")]
509 expected.push("cargo");
510 #[cfg(feature = "npm")]
511 expected.push("npm");
512 #[cfg(feature = "pypi")]
513 expected.push("pypi");
514 #[cfg(feature = "go")]
515 expected.push("go");
516 #[cfg(feature = "nuget")]
517 expected.push("nuget");
518 expected.sort_unstable();
519 let mut actual = workspace_registry_ecosystems.clone();
520 actual.sort_unstable();
521 assert_eq!(
522 actual, expected,
523 "workspace-registry-policy ecosystem set changed — update this test's `expected` \
524 list alongside whatever registration change caused it"
525 );
526 }
527
528 #[test]
536 fn test_ecosystem_id_matches_registered_ecosystems() {
537 let registry = Arc::new(EcosystemRegistry::new());
538 let cache = Arc::new(HttpCache::new());
539 register_ecosystems(®istry, Arc::clone(&cache), &test_runtime());
540
541 for id in registry.ecosystem_ids() {
542 let parsed: deps_core::EcosystemId = id.parse().unwrap_or_else(|_| {
543 panic!("registered ecosystem id {id:?} has no matching EcosystemId variant")
544 });
545 assert_eq!(parsed.id(), id);
546 }
547
548 #[cfg(feature = "cargo")]
549 assert!(registry.get(deps_core::EcosystemId::Cargo.id()).is_some());
550 #[cfg(feature = "npm")]
551 assert!(registry.get(deps_core::EcosystemId::Npm.id()).is_some());
552 #[cfg(feature = "pypi")]
553 assert!(registry.get(deps_core::EcosystemId::Pypi.id()).is_some());
554 #[cfg(feature = "go")]
555 assert!(registry.get(deps_core::EcosystemId::Go.id()).is_some());
556 #[cfg(feature = "bundler")]
557 assert!(registry.get(deps_core::EcosystemId::Bundler.id()).is_some());
558 #[cfg(feature = "dart")]
559 assert!(registry.get(deps_core::EcosystemId::Dart.id()).is_some());
560 #[cfg(feature = "maven")]
561 assert!(registry.get(deps_core::EcosystemId::Maven.id()).is_some());
562 #[cfg(feature = "gradle")]
563 assert!(registry.get(deps_core::EcosystemId::Gradle.id()).is_some());
564 #[cfg(feature = "swift")]
565 assert!(registry.get(deps_core::EcosystemId::Swift.id()).is_some());
566 #[cfg(feature = "composer")]
567 assert!(
568 registry
569 .get(deps_core::EcosystemId::Composer.id())
570 .is_some()
571 );
572 #[cfg(feature = "nuget")]
573 assert!(registry.get(deps_core::EcosystemId::NuGet.id()).is_some());
574 #[cfg(feature = "deno")]
575 assert!(registry.get(deps_core::EcosystemId::Deno.id()).is_some());
576 #[cfg(feature = "github-actions")]
577 assert!(
578 registry
579 .get(deps_core::EcosystemId::GithubActions.id())
580 .is_some()
581 );
582 #[cfg(feature = "gitlab-ci")]
583 assert!(
584 registry
585 .get(deps_core::EcosystemId::GitlabCi.id())
586 .is_some()
587 );
588 }
589
590 #[test]
606 fn test_select_latest_matching_resolves_advisory_deprecated_for_every_ecosystem() {
607 use deps_core::{RemovalStatus, Version, VersionReq};
608 use std::any::Any;
609
610 struct StatusVersion {
611 version: deps_core::ConcreteVersion,
612 status: RemovalStatus,
613 }
614
615 impl Version for StatusVersion {
616 fn version_string(&self) -> &deps_core::ConcreteVersion {
617 &self.version
618 }
619
620 fn removal_status(&self) -> RemovalStatus {
621 self.status
622 }
623
624 fn as_any(&self) -> &dyn Any {
625 self
626 }
627 }
628
629 fn fixture(status: RemovalStatus) -> Vec<Box<dyn Version>> {
630 vec![
631 Box::new(StatusVersion {
632 version: "2.0.0".into(),
633 status,
634 }),
635 Box::new(StatusVersion {
636 version: "1.2.3".into(),
637 status,
638 }),
639 ]
640 }
641
642 struct PrereleaseOnlyVersion {
650 version: deps_core::ConcreteVersion,
651 }
652
653 impl Version for PrereleaseOnlyVersion {
654 fn version_string(&self) -> &deps_core::ConcreteVersion {
655 &self.version
656 }
657
658 fn is_prerelease(&self) -> bool {
659 true
660 }
661
662 fn as_any(&self) -> &dyn Any {
663 self
664 }
665 }
666
667 fn prerelease_only_fixture() -> Vec<Box<dyn Version>> {
668 vec![
669 Box::new(PrereleaseOnlyVersion {
670 version: "2.0.0-beta2".into(),
671 }),
672 Box::new(PrereleaseOnlyVersion {
673 version: "2.0.0-beta1".into(),
674 }),
675 ]
676 }
677
678 let registry = Arc::new(EcosystemRegistry::new());
679 let cache = Arc::new(HttpCache::new());
680 register_ecosystems(®istry, Arc::clone(&cache), &test_runtime());
681
682 let req = VersionReq::new("*");
683 for id in registry.ecosystem_ids() {
684 let ecosystem = registry.get(id).expect("id came from ecosystem_ids()");
685 let ecosystem_registry = ecosystem.registry();
686
687 let control =
688 ecosystem_registry.select_latest_matching(&fixture(RemovalStatus::Available), &req);
689 assert!(
690 control.is_some(),
691 "{id}: control fixture (all Available) must resolve under a wildcard \
692 requirement — a `None` here means the fixture itself doesn't fit this \
693 ecosystem's matcher, not that the advisory flag broke anything"
694 );
695
696 let subject = ecosystem_registry
697 .select_latest_matching(&fixture(RemovalStatus::AdvisoryDeprecated), &req);
698 assert!(
699 subject.is_some(),
700 "{id}: an advisory-only flag must not hide an existing package under a \
701 wildcard requirement (#347)"
702 );
703
704 if matches!(id, "go") {
718 continue;
719 }
720
721 let prerelease_subject =
722 ecosystem_registry.select_latest_matching(&prerelease_only_fixture(), &req);
723 assert!(
724 prerelease_subject.is_some(),
725 "{id}: a package whose only releases so far are prerelease must still \
726 resolve under a wildcard requirement (#421)"
727 );
728 }
729 }
730}