1use crate::specifier::{Scheme, is_dot_prefixed, split_scheme, split_scoped};
11use crate::types::{DenoMetadata, JsrPackage, JsrVersion};
12use deps_core::{
13 DepsError, FreshnessSettings, HttpCache, Metadata, PackageName, Registry, Result, Version,
14 VersionReq, lsp_helpers::warn_rejected_value,
15};
16use deps_npm::NpmRegistry;
17use serde::Deserialize;
18use std::any::Any;
19use std::collections::HashMap;
20use std::sync::Arc;
21
22const JSR_BASE: &str = "https://jsr.io";
23const JSR_API_BASE: &str = "https://api.jsr.io";
24
25pub const REGISTRY: &str = "jsr";
27
28#[must_use]
36pub fn jsr_package_url(scope: &str, name: &str) -> String {
37 format!(
38 "{JSR_BASE}/@{}/{}",
39 urlencoding::encode(scope),
40 urlencoding::encode(name)
41 )
42}
43
44fn meta_json_url(base: &str, scope: &str, name: &str) -> String {
45 format!(
46 "{base}/@{}/{}/meta.json",
47 urlencoding::encode(scope),
48 urlencoding::encode(name)
49 )
50}
51
52const MAX_SCOPE_SEARCH_OVERFETCH: usize = 40;
58
59fn split_scope_query(query: &str) -> Option<(&str, &str)> {
64 let after_at = query.strip_prefix('@')?;
65 after_at.split_once('/')
66}
67
68fn package_scope(pkg: &JsrPackage) -> &str {
71 pkg.name
72 .as_str()
73 .strip_prefix("jsr:@")
74 .and_then(|s| s.split_once('/'))
75 .map_or("", |(scope, _)| scope)
76}
77
78fn not_found_or(err: DepsError, full_name: &str) -> DepsError {
81 if matches!(err, DepsError::HttpStatus { status: 404, .. }) {
82 DepsError::PackageNotFound {
83 package: full_name.to_string(),
84 registry: REGISTRY,
85 }
86 } else {
87 err
88 }
89}
90
91fn unroutable(name: &PackageName) -> DepsError {
100 DepsError::PackageNotFound {
101 package: name.to_string(),
102 registry: "deno",
103 }
104}
105
106#[derive(Deserialize, Default)]
108#[serde(rename_all = "camelCase")]
109struct MetaVersionEntry {
110 #[serde(default)]
111 yanked: bool,
112 #[serde(default)]
113 created_at: Option<String>,
114}
115
116#[derive(Deserialize)]
118struct MetaJson {
119 versions: HashMap<String, MetaVersionEntry>,
120}
121
122#[derive(Deserialize)]
124struct SearchItem {
125 scope: String,
126 name: String,
127 #[serde(default)]
128 description: Option<String>,
129 #[serde(rename = "latestVersion", default)]
130 latest_version: Option<String>,
131 #[serde(rename = "githubRepository", default)]
132 github_repository: Option<GithubRepository>,
133}
134
135#[derive(Deserialize)]
136struct GithubRepository {
137 owner: String,
138 name: String,
139}
140
141#[derive(Deserialize)]
142struct SearchResponse {
143 items: Vec<SearchItem>,
144}
145
146#[derive(Clone)]
153pub struct JsrRegistry {
154 cache: Arc<HttpCache>,
155 base: String,
156 api_base: String,
157}
158
159impl JsrRegistry {
160 #[must_use]
162 pub fn new(cache: Arc<HttpCache>) -> Self {
163 Self::with_bases(cache, JSR_BASE.to_string(), JSR_API_BASE.to_string())
164 }
165
166 fn with_bases(cache: Arc<HttpCache>, base: String, api_base: String) -> Self {
167 Self {
168 cache,
169 base,
170 api_base,
171 }
172 }
173
174 pub async fn get_versions(&self, scope: &str, name: &str) -> Result<Vec<JsrVersion>> {
209 let full_name = format!("@{scope}/{name}");
210 if is_dot_prefixed(scope) || is_dot_prefixed(name) {
217 warn_rejected_value("is_dot_prefixed", "jsr meta.json request URL", &full_name);
218 return Err(DepsError::PackageNotFound {
219 package: full_name,
220 registry: REGISTRY,
221 });
222 }
223 let url = meta_json_url(&self.base, scope, name);
224 let data = self
225 .cache
226 .get_cached(&url)
227 .await
228 .map_err(|e| not_found_or(e, &full_name))?;
229 parse_meta_json(&data)
230 }
231
232 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<JsrPackage>> {
264 let Some((scope, pkg_prefix)) = split_scope_query(query) else {
265 return self.fetch_search(query, limit).await;
266 };
267
268 let text_query = if pkg_prefix.is_empty() {
271 scope
272 } else {
273 pkg_prefix
274 };
275 let fetch_limit = limit
281 .saturating_mul(4)
282 .min(MAX_SCOPE_SEARCH_OVERFETCH.max(limit));
283
284 let mut results = self.fetch_search(text_query, fetch_limit).await?;
285 results.sort_by_key(|p| !package_scope(p).eq_ignore_ascii_case(scope));
286 results.truncate(limit);
287 Ok(results)
288 }
289
290 async fn fetch_search(&self, query: &str, limit: usize) -> Result<Vec<JsrPackage>> {
294 let url = format!(
295 "{}/packages?query={}&limit={}",
296 self.api_base,
297 urlencoding::encode(query),
298 limit
299 );
300 let data = self.cache.get_cached(&url).await?;
301 parse_search_response(&data)
302 }
303}
304
305fn parse_meta_json(data: &[u8]) -> Result<Vec<JsrVersion>> {
307 let meta: MetaJson = deps_core::parse_json_checked(data)?;
308
309 let mut versions_with_parsed: Vec<(JsrVersion, node_semver::Version)> = meta
310 .versions
311 .into_iter()
312 .filter_map(|(version, entry)| {
313 let parsed = node_semver::Version::parse(&version).ok()?;
314 let published_at = entry
315 .created_at
316 .as_deref()
317 .and_then(deps_core::PublishTime::parse_rfc3339);
318 Some((
319 JsrVersion {
320 version: version.into(),
321 yanked: entry.yanked,
322 published_at,
323 },
324 parsed,
325 ))
326 })
327 .collect();
328
329 versions_with_parsed.sort_unstable_by(|a, b| b.1.cmp(&a.1));
330 Ok(versions_with_parsed.into_iter().map(|(v, _)| v).collect())
331}
332
333fn parse_search_response(data: &[u8]) -> Result<Vec<JsrPackage>> {
336 let response: SearchResponse = deps_core::parse_json_checked(data)?;
337
338 Ok(response
339 .items
340 .into_iter()
341 .map(|item| {
342 let repository = item
343 .github_repository
344 .map(|repo| format!("https://github.com/{}/{}", repo.owner, repo.name));
345 let description = item.description.filter(|d| !d.is_empty());
346 JsrPackage {
347 name: PackageName::new(format!("jsr:@{}/{}", item.scope, item.name)),
348 description,
349 repository,
350 documentation: None,
351 latest_version: item.latest_version.unwrap_or_default().into(),
352 }
353 })
354 .collect())
355}
356
357pub struct DenoRegistry {
362 jsr: JsrRegistry,
363 npm: NpmRegistry,
364}
365
366impl DenoRegistry {
367 #[must_use]
380 pub fn new(cache: Arc<HttpCache>) -> Self {
381 Self::with_npm(Arc::clone(&cache), NpmRegistry::new(cache))
382 }
383
384 #[must_use]
395 pub fn with_npm(cache: Arc<HttpCache>, npm: NpmRegistry) -> Self {
396 Self {
397 jsr: JsrRegistry::new(cache),
398 npm,
399 }
400 }
401}
402
403impl Registry for DenoRegistry {
404 fn get_versions<'a>(
405 &'a self,
406 name: &'a PackageName,
407 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn Version>>>> {
408 Box::pin(async move {
409 match split_scheme(name.as_str()) {
410 Some((Scheme::Jsr, rest)) => {
411 let (scope, pkg) = split_scoped(rest).ok_or_else(|| unroutable(name))?;
412 let versions = self.jsr.get_versions(scope, pkg).await?;
413 Ok(versions
414 .into_iter()
415 .map(|v| Box::new(v) as Box<dyn Version>)
416 .collect())
417 }
418 Some((Scheme::Npm, rest)) => {
419 if rest.is_empty() {
420 return Err(unroutable(name));
421 }
422 let bare = PackageName::new(rest);
423 Registry::get_versions(&self.npm, &bare).await
428 }
429 None => Err(unroutable(name)),
430 }
431 })
432 }
433
434 fn get_versions_with<'a>(
435 &'a self,
436 name: &'a PackageName,
437 freshness: FreshnessSettings,
438 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn Version>>>> {
439 Box::pin(async move {
440 match split_scheme(name.as_str()) {
441 Some((Scheme::Npm, rest)) => {
442 if rest.is_empty() {
443 return Err(unroutable(name));
444 }
445 let bare = PackageName::new(rest);
446 Registry::get_versions_with(&self.npm, &bare, freshness).await
447 }
448 _ => self.get_versions(name).await,
451 }
452 })
453 }
454
455 fn get_latest_matching<'a>(
456 &'a self,
457 name: &'a PackageName,
458 req: &'a VersionReq,
459 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn Version>>>> {
460 Box::pin(async move {
461 match split_scheme(name.as_str()) {
462 Some((Scheme::Jsr, rest)) => {
463 let (scope, pkg) = split_scoped(rest).ok_or_else(|| unroutable(name))?;
464 let versions = self.jsr.get_versions(scope, pkg).await?;
465 let parsed_req = node_semver::Range::parse(req.as_str())
466 .map_err(|e| DepsError::InvalidVersionReq(e.to_string()))?;
467 Ok(versions
468 .into_iter()
469 .find(|v| {
470 node_semver::Version::parse(&v.version)
471 .is_ok_and(|ver| parsed_req.satisfies(&ver) && !v.yanked)
472 })
473 .map(|v| Box::new(v) as Box<dyn Version>))
474 }
475 Some((Scheme::Npm, rest)) => {
476 if rest.is_empty() {
477 return Err(unroutable(name));
478 }
479 let bare = PackageName::new(rest);
480 Registry::get_latest_matching(&self.npm, &bare, req).await
481 }
482 None => Err(unroutable(name)),
483 }
484 })
485 }
486
487 fn search<'a>(
488 &'a self,
489 query: &'a str,
490 limit: usize,
491 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn Metadata>>>> {
492 Box::pin(async move {
493 match split_scheme(query) {
494 Some((Scheme::Jsr, rest)) => {
495 let packages = self.jsr.search(rest, limit).await?;
496 Ok(packages
497 .into_iter()
498 .map(|p| Box::new(p) as Box<dyn Metadata>)
499 .collect())
500 }
501 Some((Scheme::Npm, rest)) => {
502 let results = Registry::search(&self.npm, rest, limit).await?;
503 Ok(results
504 .into_iter()
505 .map(|m| {
506 let prefixed = PackageName::new(format!("npm:{}", m.name()));
507 Box::new(DenoMetadata::new(prefixed, m)) as Box<dyn Metadata>
508 })
509 .collect())
510 }
511 None => Ok(vec![]),
513 }
514 })
515 }
516
517 fn select_latest_matching(
518 &self,
519 versions: &[Box<dyn Version>],
520 req: &VersionReq,
521 ) -> Option<usize> {
522 self.npm.select_latest_matching(versions, req)
530 }
531
532 fn as_any(&self) -> &dyn Any {
533 self
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 use deps_core::test_util::capture_tracing_output_async;
542 use std::assert_matches;
543
544 #[test]
545 fn test_jsr_package_url() {
546 assert_eq!(jsr_package_url("std", "fs"), "https://jsr.io/@std/fs");
547 }
548
549 #[test]
552 fn test_jsr_package_url_encodes_malicious_scope() {
553 let url = jsr_package_url("evil)[pkg](https://evil.example", "x");
554 assert!(!url.contains(')'));
555 assert!(!url.contains('('));
556 assert!(!url.contains('['));
557 assert!(!url.contains(']'));
558 }
559
560 #[test]
561 fn test_jsr_package_url_encodes_malicious_name_segment() {
562 let url = jsr_package_url("std", "evil)[pkg](https://evil.example");
563 assert!(!url.contains(')'));
564 assert!(!url.contains('('));
565 }
566
567 #[test]
568 fn test_jsr_package_url_encodes_newline_and_percent() {
569 let url = jsr_package_url("evil\n<%", "pkg");
570 assert!(!url.contains('\n'));
571 assert!(!url.contains('<'));
572 assert!(url.contains("%25"));
573 }
574
575 #[test]
576 fn test_meta_json_url_encodes_malicious_segments() {
577 let url = meta_json_url(JSR_BASE, "evil/../secret?x=1#frag", "pkg");
588 assert!(!url.contains('?'));
589 assert!(!url.contains('#'));
590 let parsed = url::Url::parse(&url).unwrap();
591 assert_eq!(
592 parsed.path(),
593 "/@evil%2F..%2Fsecret%3Fx%3D1%23frag/pkg/meta.json"
594 );
595 }
596
597 #[test]
601 fn test_meta_json_url_dot_segment_sweep() {
602 deps_core::test_util::assert_dot_segment_gated_or_contained(
603 |seg| (!is_dot_prefixed(seg)).then(|| meta_json_url(JSR_BASE, seg, "pkg")),
604 "jsr.io",
605 "/@",
606 );
607 deps_core::test_util::assert_dot_segment_gated_or_contained(
608 |seg| (!is_dot_prefixed(seg)).then(|| meta_json_url(JSR_BASE, "scope", seg)),
609 "jsr.io",
610 "/@",
611 );
612 }
613
614 #[test]
615 fn test_parse_meta_json_sorts_newest_first() {
616 let json = r#"{
619 "versions": {
620 "1.0.19": {"createdAt": "2025-07-01T07:43:44Z"},
621 "0.200.0": {"createdAt": "2024-04-24T06:44:45Z"},
622 "1.0.24": {"createdAt": "2026-05-26T09:57:22Z"},
623 "0.229.0": {"yanked": true, "createdAt": "2024-04-29T17:22:46Z"},
624 "1.0.9": {"createdAt": "2025-01-10T08:22:54Z"}
625 }
626}"#;
627
628 let versions = parse_meta_json(json.as_bytes()).unwrap();
629 let strings: Vec<&str> = versions.iter().map(|v| v.version.as_str()).collect();
630 assert_eq!(
631 strings,
632 vec!["1.0.24", "1.0.19", "1.0.9", "0.229.0", "0.200.0"]
633 );
634 }
635
636 #[test]
637 fn test_parse_meta_json_yanked_and_published_at() {
638 let json = r#"{
639 "versions": {
640 "1.0.0": {"createdAt": "2024-01-01T00:00:00Z"},
641 "0.9.0": {"yanked": true, "createdAt": "2023-01-01T00:00:00Z"}
642 }
643}"#;
644
645 let versions = parse_meta_json(json.as_bytes()).unwrap();
646 let v1 = versions.iter().find(|v| v.version == "1.0.0").unwrap();
647 assert!(!v1.yanked);
648 assert!(v1.published_at.is_some());
649
650 let v09 = versions.iter().find(|v| v.version == "0.9.0").unwrap();
651 assert!(v09.yanked);
652 }
653
654 #[test]
655 fn test_parse_meta_json_skips_non_semver_keys() {
656 let json = r#"{
657 "versions": {
658 "1.0.0": {"createdAt": "2024-01-01T00:00:00Z"},
659 "not-a-version": {"createdAt": "2024-01-01T00:00:00Z"}
660 }
661}"#;
662
663 let versions = parse_meta_json(json.as_bytes()).unwrap();
664 assert_eq!(versions.len(), 1);
665 assert_eq!(versions[0].version, "1.0.0");
666 }
667
668 #[test]
669 fn test_parse_meta_json_nesting_at_max_depth_accepted() {
670 let depth = deps_core::MAX_JSON_NESTING_DEPTH;
671 let json = format!(
672 r#"{{"versions": {{}}, "extra": {}1{}}}"#,
673 "[".repeat(depth - 1),
674 "]".repeat(depth - 1)
675 );
676 assert!(parse_meta_json(json.as_bytes()).is_ok());
677 }
678
679 #[test]
680 fn test_parse_meta_json_nesting_over_max_depth_rejected() {
681 let depth = deps_core::MAX_JSON_NESTING_DEPTH + 1;
682 let json = format!(
683 r#"{{"versions": {{}}, "extra": {}1{}}}"#,
684 "[".repeat(depth),
685 "]".repeat(depth)
686 );
687 assert!(parse_meta_json(json.as_bytes()).is_err());
688 }
689
690 #[test]
691 fn test_parse_search_response_prefixes_name_with_scheme_and_maps_github_repo() {
692 let json = r#"{
693 "items": [
694 {
695 "scope": "std",
696 "name": "fs",
697 "description": "File system utilities",
698 "latestVersion": "1.0.24",
699 "githubRepository": {"owner": "denoland", "name": "std"}
700 }
701 ]
702}"#;
703
704 let packages = parse_search_response(json.as_bytes()).unwrap();
705 assert_eq!(packages.len(), 1);
706 assert_eq!(packages[0].name, "jsr:@std/fs");
707 assert_eq!(
708 packages[0].description,
709 Some("File system utilities".to_string())
710 );
711 assert_eq!(
712 packages[0].repository,
713 Some("https://github.com/denoland/std".to_string())
714 );
715 assert_eq!(packages[0].latest_version, "1.0.24");
716 }
717
718 #[test]
719 fn test_parse_search_response_nesting_at_max_depth_accepted() {
720 let depth = deps_core::MAX_JSON_NESTING_DEPTH;
721 let json = format!(
722 r#"{{"items": [], "extra": {}1{}}}"#,
723 "[".repeat(depth - 1),
724 "]".repeat(depth - 1)
725 );
726 assert!(parse_search_response(json.as_bytes()).is_ok());
727 }
728
729 #[test]
730 fn test_parse_search_response_nesting_over_max_depth_rejected() {
731 let depth = deps_core::MAX_JSON_NESTING_DEPTH + 1;
732 let json = format!(
733 r#"{{"items": [], "extra": {}1{}}}"#,
734 "[".repeat(depth),
735 "]".repeat(depth)
736 );
737 assert!(parse_search_response(json.as_bytes()).is_err());
738 }
739
740 #[test]
741 fn test_parse_search_response_empty_description_becomes_none() {
742 let json = r#"{
743 "items": [
744 {"scope": "anabranch", "name": "fs", "description": "", "latestVersion": "0.3.1"}
745 ]
746}"#;
747
748 let packages = parse_search_response(json.as_bytes()).unwrap();
749 assert_eq!(packages[0].description, None);
750 assert_eq!(packages[0].repository, None);
751 }
752
753 #[test]
754 fn test_not_found_or_maps_404() {
755 let err = DepsError::HttpStatus {
756 url: "https://jsr.io/@std/fs/meta.json".into(),
757 status: 404,
758 };
759 let result = not_found_or(err, "@std/fs");
760 assert_matches!(
761 result,
762 DepsError::PackageNotFound { package, registry }
763 if package == "@std/fs" && registry == REGISTRY
764 );
765 }
766
767 #[test]
768 fn test_not_found_or_passes_through_non_404() {
769 let err = DepsError::HttpStatus {
770 url: "https://jsr.io/@std/fs/meta.json".into(),
771 status: 500,
772 };
773 let result = not_found_or(err, "@std/fs");
774 assert_matches!(result, DepsError::HttpStatus { status: 500, .. });
775 }
776
777 fn unreachable_npm(cache: Arc<HttpCache>) -> NpmRegistry {
784 NpmRegistry::with_registry_base(cache, "http://127.0.0.1:1".to_string())
785 }
786
787 #[tokio::test]
788 async fn test_deno_registry_get_versions_rejects_empty_npm_name() {
789 let cache = Arc::new(HttpCache::new());
790 let npm = unreachable_npm(Arc::clone(&cache));
791 let registry = DenoRegistry::with_npm(cache, npm);
792
793 let Err(err) = Registry::get_versions(®istry, &PackageName::new("npm:")).await else {
794 panic!("expected an error for an empty npm: name");
795 };
796 assert_matches!(
797 err,
798 DepsError::PackageNotFound {
799 registry: "deno",
800 ..
801 }
802 );
803 }
804
805 #[tokio::test]
806 async fn test_deno_registry_get_versions_with_rejects_empty_npm_name() {
807 let cache = Arc::new(HttpCache::new());
808 let npm = unreachable_npm(Arc::clone(&cache));
809 let registry = DenoRegistry::with_npm(cache, npm);
810
811 let Err(err) = Registry::get_versions_with(
812 ®istry,
813 &PackageName::new("npm:"),
814 FreshnessSettings::default(),
815 )
816 .await
817 else {
818 panic!("expected an error for an empty npm: name");
819 };
820 assert_matches!(
821 err,
822 DepsError::PackageNotFound {
823 registry: "deno",
824 ..
825 }
826 );
827 }
828
829 #[tokio::test]
830 async fn test_deno_registry_get_latest_matching_rejects_empty_npm_name() {
831 let cache = Arc::new(HttpCache::new());
832 let npm = unreachable_npm(Arc::clone(&cache));
833 let registry = DenoRegistry::with_npm(cache, npm);
834
835 let Err(err) = Registry::get_latest_matching(
836 ®istry,
837 &PackageName::new("npm:"),
838 &VersionReq::new("*"),
839 )
840 .await
841 else {
842 panic!("expected an error for an empty npm: name");
843 };
844 assert_matches!(
845 err,
846 DepsError::PackageNotFound {
847 registry: "deno",
848 ..
849 }
850 );
851 }
852
853 #[tokio::test]
858 async fn test_deno_registry_get_versions_rejects_dot_segment_npm_package() {
859 let cache = Arc::new(HttpCache::new());
860 let npm = unreachable_npm(Arc::clone(&cache));
861 let registry = DenoRegistry::with_npm(cache, npm);
862
863 let Err(err) = Registry::get_versions(®istry, &PackageName::new("npm:@a/..")).await
864 else {
865 panic!("expected an error for a dot-segment npm: package name");
866 };
867 assert!(err.is_not_found());
868 }
869
870 #[tokio::test]
871 async fn test_deno_registry_get_versions_dispatches_jsr_via_mock() {
872 let mut server = mockito::Server::new_async().await;
873 let cache = Arc::new(HttpCache::new());
874 let jsr = JsrRegistry::with_bases(Arc::clone(&cache), server.url(), server.url());
875 let registry = DenoRegistry {
876 jsr,
877 npm: NpmRegistry::new(cache),
878 };
879
880 let mock = server
881 .mock("GET", "/@std/fs/meta.json")
882 .with_status(200)
883 .with_body(r#"{"versions": {"1.0.0": {"createdAt": "2024-01-01T00:00:00Z"}}}"#)
884 .create_async()
885 .await;
886
887 let versions = Registry::get_versions(®istry, &PackageName::new("jsr:@std/fs"))
888 .await
889 .unwrap();
890
891 assert_eq!(versions.len(), 1);
892 assert_eq!(versions[0].version_string(), "1.0.0");
893 mock.assert_async().await;
894 }
895
896 fn unreachable_jsr(cache: Arc<HttpCache>) -> JsrRegistry {
902 JsrRegistry::with_bases(
903 cache,
904 "http://127.0.0.1:1".to_string(),
905 "http://127.0.0.1:1".to_string(),
906 )
907 }
908
909 #[tokio::test]
910 async fn test_jsr_registry_get_versions_rejects_dot_prefixed_package_segment() {
911 let registry = unreachable_jsr(Arc::new(HttpCache::new()));
916
917 let err = registry.get_versions("std", "..").await.unwrap_err();
918 assert_matches!(
919 err,
920 DepsError::PackageNotFound {
921 registry: REGISTRY,
922 ..
923 }
924 );
925
926 let err = registry.get_versions("std", ".").await.unwrap_err();
927 assert_matches!(
928 err,
929 DepsError::PackageNotFound {
930 registry: REGISTRY,
931 ..
932 }
933 );
934 }
935
936 #[tokio::test]
937 async fn test_jsr_registry_get_versions_rejects_dot_prefixed_scope_segment() {
938 let registry = unreachable_jsr(Arc::new(HttpCache::new()));
942
943 let err = registry.get_versions("..", "pkg").await.unwrap_err();
944 assert_matches!(
945 err,
946 DepsError::PackageNotFound {
947 registry: REGISTRY,
948 ..
949 }
950 );
951 }
952
953 #[tokio::test]
954 async fn test_jsr_registry_get_versions_rejection_logs_warn_rejected_value() {
955 let registry = unreachable_jsr(Arc::new(HttpCache::new()));
958 let output = capture_tracing_output_async(async {
959 let _ = registry.get_versions("std", "..").await;
960 })
961 .await;
962 assert!(output.contains("is_dot_prefixed"), "output was: {output}");
963 assert!(
964 output.contains("jsr meta.json request URL"),
965 "output was: {output}"
966 );
967 }
968
969 #[tokio::test]
970 async fn test_deno_registry_get_versions_rejects_dot_prefixed_jsr_package_via_mock() {
971 let mut server = mockito::Server::new_async().await;
974 let cache = Arc::new(HttpCache::new());
975 let jsr = JsrRegistry::with_bases(Arc::clone(&cache), server.url(), server.url());
976 let registry = DenoRegistry {
977 jsr,
978 npm: NpmRegistry::new(cache),
979 };
980
981 let mock = server
982 .mock("GET", mockito::Matcher::Any)
983 .expect(0)
984 .create_async()
985 .await;
986
987 let Err(err) = Registry::get_versions(®istry, &PackageName::new("jsr:@std/..")).await
988 else {
989 panic!("expected an error for a dot-prefixed jsr: package segment");
990 };
991 assert_matches!(
992 err,
993 DepsError::PackageNotFound {
994 registry: REGISTRY,
995 ..
996 }
997 );
998
999 let Err(err) = Registry::get_versions_with(
1000 ®istry,
1001 &PackageName::new("jsr:@std/.."),
1002 FreshnessSettings::default(),
1003 )
1004 .await
1005 else {
1006 panic!("expected an error for a dot-prefixed jsr: package segment");
1007 };
1008 assert_matches!(
1009 err,
1010 DepsError::PackageNotFound {
1011 registry: REGISTRY,
1012 ..
1013 }
1014 );
1015
1016 let Err(err) = Registry::get_latest_matching(
1017 ®istry,
1018 &PackageName::new("jsr:@std/.."),
1019 &VersionReq::new("*"),
1020 )
1021 .await
1022 else {
1023 panic!("expected an error for a dot-prefixed jsr: package segment");
1024 };
1025 assert_matches!(
1026 err,
1027 DepsError::PackageNotFound {
1028 registry: REGISTRY,
1029 ..
1030 }
1031 );
1032
1033 mock.assert_async().await;
1034 }
1035
1036 #[test]
1039 fn test_split_scope_query() {
1040 assert_eq!(split_scope_query("@std/fs"), Some(("std", "fs")));
1041 assert_eq!(split_scope_query("@std/f"), Some(("std", "f")));
1042 assert_eq!(split_scope_query("@std/"), Some(("std", "")));
1043 assert_eq!(split_scope_query("@std"), None);
1044 assert_eq!(split_scope_query("fs"), None);
1045 }
1046
1047 #[test]
1048 fn test_package_scope_extracts_from_prefixed_name() {
1049 let pkg = JsrPackage {
1050 name: PackageName::new("jsr:@std/fs"),
1051 description: None,
1052 repository: None,
1053 documentation: None,
1054 latest_version: deps_core::ConcreteVersion::new(""),
1055 };
1056 assert_eq!(package_scope(&pkg), "std");
1057 }
1058
1059 #[tokio::test]
1060 async fn test_jsr_registry_search_scoped_query_sends_only_package_name_segment() {
1061 let mut server = mockito::Server::new_async().await;
1064 let registry =
1065 JsrRegistry::with_bases(Arc::new(HttpCache::new()), server.url(), server.url());
1066
1067 let mock = server
1068 .mock("GET", "/packages")
1069 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
1070 "query".into(),
1071 "fs".into(),
1072 )]))
1073 .with_status(200)
1074 .with_body(r#"{"items": [{"scope": "std", "name": "fs", "latestVersion": "1.0.24"}]}"#)
1075 .create_async()
1076 .await;
1077
1078 let results = registry.search("@std/fs", 5).await.unwrap();
1079
1080 assert_eq!(results.len(), 1);
1081 assert_eq!(results[0].name, "jsr:@std/fs");
1082 mock.assert_async().await;
1083 }
1084
1085 #[tokio::test]
1086 async fn test_jsr_registry_search_scoped_query_reorders_exact_scope_match_first() {
1087 let mut server = mockito::Server::new_async().await;
1090 let registry =
1091 JsrRegistry::with_bases(Arc::new(HttpCache::new()), server.url(), server.url());
1092
1093 let body = r#"{"items": [
1094 {"scope": "other", "name": "fs", "latestVersion": "1.0.0"},
1095 {"scope": "another", "name": "fs-utils", "latestVersion": "3.0.0"},
1096 {"scope": "std", "name": "fs", "latestVersion": "2.0.0"}
1097 ]}"#;
1098 let mock = server
1099 .mock("GET", "/packages")
1100 .match_query(mockito::Matcher::Any)
1101 .with_status(200)
1102 .with_body(body)
1103 .create_async()
1104 .await;
1105
1106 let results = registry.search("@std/fs", 2).await.unwrap();
1107
1108 assert_eq!(results.len(), 2);
1109 assert_eq!(results[0].name, "jsr:@std/fs");
1110 mock.assert_async().await;
1111 }
1112
1113 #[tokio::test]
1114 async fn test_jsr_registry_search_unscoped_query_unaffected() {
1115 let mut server = mockito::Server::new_async().await;
1116 let registry =
1117 JsrRegistry::with_bases(Arc::new(HttpCache::new()), server.url(), server.url());
1118
1119 let mock = server
1120 .mock("GET", "/packages")
1121 .match_query(mockito::Matcher::UrlEncoded("query".into(), "fs".into()))
1122 .with_status(200)
1123 .with_body(r#"{"items": [{"scope": "std", "name": "fs", "latestVersion": "1.0.24"}]}"#)
1124 .create_async()
1125 .await;
1126
1127 let results = registry.search("fs", 5).await.unwrap();
1128
1129 assert_eq!(results.len(), 1);
1130 mock.assert_async().await;
1131 }
1132
1133 #[tokio::test]
1134 async fn test_jsr_registry_search_scoped_query_limit_above_overfetch_cap_does_not_panic() {
1135 let mut server = mockito::Server::new_async().await;
1138 let registry =
1139 JsrRegistry::with_bases(Arc::new(HttpCache::new()), server.url(), server.url());
1140
1141 let mock = server
1142 .mock("GET", "/packages")
1143 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
1144 "limit".into(),
1145 "50".into(),
1146 )]))
1147 .with_status(200)
1148 .with_body(r#"{"items": [{"scope": "std", "name": "fs", "latestVersion": "1.0.24"}]}"#)
1149 .create_async()
1150 .await;
1151
1152 let results = registry.search("@std/fs", 50).await.unwrap();
1153
1154 assert_eq!(results.len(), 1);
1155 mock.assert_async().await;
1156 }
1157
1158 #[tokio::test]
1159 async fn test_deno_registry_with_npm_shares_freshness_cache_across_instances() {
1160 let mut server = mockito::Server::new_async().await;
1166 let base = server.url();
1167 let http_cache = Arc::new(HttpCache::new());
1168 let shared_npm = NpmRegistry::with_registry_base(Arc::clone(&http_cache), base);
1169
1170 let abbrev_mock = server
1171 .mock("GET", "/widget")
1172 .match_header("accept", "application/vnd.npm.install-v1+json")
1173 .with_status(200)
1174 .with_body(r#"{"versions": {"1.0.0": {}}}"#)
1175 .expect(2)
1176 .create_async()
1177 .await;
1178 let full_mock = server
1179 .mock("GET", "/widget")
1180 .match_header("accept", "application/json")
1181 .with_status(200)
1182 .with_body(r#"{"time": {"1.0.0": "2020-01-01T00:00:00Z"}}"#)
1183 .expect(1)
1184 .create_async()
1185 .await;
1186
1187 let npm_side = shared_npm.clone();
1189 let from_npm = Registry::get_versions_with(
1190 &npm_side,
1191 &PackageName::new("widget"),
1192 FreshnessSettings::default(),
1193 )
1194 .await
1195 .unwrap();
1196 assert!(from_npm[0].published_at().is_some());
1197
1198 let deno_registry = DenoRegistry::with_npm(Arc::clone(&http_cache), shared_npm);
1201 let from_deno = Registry::get_versions_with(
1202 &deno_registry,
1203 &PackageName::new("npm:widget"),
1204 FreshnessSettings::default(),
1205 )
1206 .await
1207 .unwrap();
1208 assert!(from_deno[0].published_at().is_some());
1209
1210 abbrev_mock.assert_async().await;
1211 full_mock.assert_async().await;
1212 }
1213
1214 #[tokio::test]
1215 #[ignore] async fn test_deno_registry_get_versions_dispatches_npm_live() {
1217 let cache = Arc::new(HttpCache::new());
1222 let registry = DenoRegistry::new(cache);
1223 let versions = Registry::get_versions(®istry, &PackageName::new("npm:react"))
1224 .await
1225 .unwrap();
1226 assert!(!versions.is_empty());
1227 }
1228
1229 #[tokio::test]
1235 #[ignore]
1236 async fn test_live_jsr_get_versions_std_fs() {
1237 let registry = JsrRegistry::new(Arc::new(HttpCache::new()));
1238 let versions = registry.get_versions("std", "fs").await.unwrap();
1239
1240 assert!(!versions.is_empty());
1241 assert!(versions.iter().any(|v| v.version == "1.0.24"));
1244 assert!(versions.iter().any(|v| v.version == "0.229.0" && v.yanked));
1246 let parsed: Vec<node_semver::Version> = versions
1248 .iter()
1249 .map(|v| node_semver::Version::parse(&v.version).unwrap())
1250 .collect();
1251 assert!(parsed.windows(2).all(|w| w[0] >= w[1]));
1252 }
1253
1254 #[tokio::test]
1255 #[ignore]
1256 async fn test_live_jsr_search_fs() {
1257 let registry = JsrRegistry::new(Arc::new(HttpCache::new()));
1258 let results = registry.search("fs", 5).await.unwrap();
1259
1260 assert!(!results.is_empty());
1261 assert!(results.iter().all(|p| p.name.as_str().starts_with("jsr:@")));
1262 }
1263
1264 #[tokio::test]
1265 #[ignore]
1266 async fn test_live_jsr_search_scoped_query_ranks_exact_scope_first() {
1267 let registry = JsrRegistry::new(Arc::new(HttpCache::new()));
1270 let results = registry.search("@std/fs", 5).await.unwrap();
1271
1272 assert!(!results.is_empty());
1273 assert_eq!(results[0].name, "jsr:@std/fs");
1274 }
1275
1276 #[tokio::test]
1277 #[ignore]
1278 async fn test_live_jsr_get_versions_missing_package_is_not_found() {
1279 let registry = JsrRegistry::new(Arc::new(HttpCache::new()));
1280 let err = registry
1281 .get_versions("this-scope-does-not-exist-12345", "nope")
1282 .await
1283 .unwrap_err();
1284 assert_matches!(err, DepsError::PackageNotFound { .. });
1285 }
1286}