Repository: denoland/deno
Pull request: fix(install): force refresh if cannot find version

Fixes #30464.


Changed files:
filename: cli/jsr.rs
patch: @@ -5,7 +5,9 @@ use std::sync::Arc;
 use dashmap::DashMap;
 use deno_core::serde_json;
 use deno_graph::packages::JsrPackageInfo;
+use deno_graph::packages::JsrPackageInfoVersion;
 use deno_graph::packages::JsrPackageVersionInfo;
+use deno_semver::Version;
 use deno_semver::package::PackageNv;
 use deno_semver::package::PackageReq;
 
@@ -24,6 +26,21 @@ pub struct JsrFetchResolver {
   file_fetcher: Arc<CliFileFetcher>,
 }
 
+fn select_version<'a, I>(versions: I, req: &PackageReq) -> Option<Version>
+where
+  I: IntoIterator<Item = (&'a Version, &'a JsrPackageInfoVersion)>,
+{
+  let mut versions = versions.into_iter().collect::<Vec<_>>();
+  versions.sort_by_key(|(v, _)| *v);
+  versions
+    .into_iter()
+    .rev()
+    .find(|(v, i)| {
+      !i.yanked && req.version_req.tag().is_none() && req.version_req.matches(v)
+    })
+    .map(|(v, _)| v.clone())
+}
+
 impl JsrFetchResolver {
   pub fn new(file_fetcher: Arc<CliFileFetcher>) -> Self {
     Self {
@@ -40,32 +57,74 @@ impl JsrFetchResolver {
     }
     let maybe_get_nv = || async {
       let name = req.name.clone();
-      let package_info = self.package_info(&name).await?;
+      let package_info = self.package_info(&name).await;
+      if package_info.is_none() {
+        log::debug!("no package info found for jsr:{name}");
+        return None;
+      }
+      let package_info = package_info?;
       // Find the first matching version of the package.
-      let mut versions = package_info.versions.iter().collect::<Vec<_>>();
-      versions.sort_by_key(|(v, _)| *v);
-      let version = versions
-        .into_iter()
-        .rev()
-        .find(|(v, i)| {
-          !i.yanked
-            && req.version_req.tag().is_none()
-            && req.version_req.matches(v)
-        })
-        .map(|(v, _)| v.clone())?;
+      let version = select_version(&package_info.versions, req);
+      let version = if let Some(version) = version {
+        version
+      } else {
+        let info = self.force_refresh_package_info(&name).await;
+        let Some(info) = info else {
+          log::debug!("no package info found for jsr:{name}");
+          return None;
+        };
+        let version = select_version(&info.versions, req);
+        let Some(version) = version else {
+          log::debug!("no matching version found for jsr:{req}");
+          return None;
+        };
+        version
+      };
       Some(PackageNv { name, version })
     };
     let nv = maybe_get_nv().await;
+
     self.nv_by_req.insert(req.clone(), nv.clone());
     nv
   }
 
+  pub async fn force_refresh_package_info(
+    &self,
+    name: &str,
+  ) -> Option<Arc<JsrPackageInfo>> {
+    let meta_url = self.meta_url(name)?;
+    let file_fetcher = self.file_fetcher.clone();
+    let file = file_fetcher
+      .fetch_with_options(
+        &meta_url,
+        deno_resolver::file_fetcher::FetchPermissionsOptionRef::AllowAll,
+        deno_resolver::file_fetcher::FetchOptions {
+          maybe_cache_setting: Some(
+            &deno_cache_dir::file_fetcher::CacheSetting::ReloadAll,
+          ),
+          ..Default::default()
+        },
+      )
+      .await
+      .ok()?;
+    let info = serde_json::from_slice::<JsrPackageInfo>(&file.source).ok()?;
+    let info = Arc::new(info);
+    self
+      .info_by_name
+      .insert(name.to_string(), Some(info.clone()));
+    Some(info)
+  }
+
+  fn meta_url(&self, name: &str) -> Option<deno_core::url::Url> {
+    jsr_url().join(&format!("{}/meta.json", name)).ok()
+  }
+
   pub async fn package_info(&self, name: &str) -> Option<Arc<JsrPackageInfo>> {
     if let Some(info) = self.info_by_name.get(name) {
       return info.value().clone();
     }
     let fetch_package_info = || async {
-      let meta_url = jsr_url().join(&format!("{}/meta.json", name)).ok()?;
+      let meta_url = self.meta_url(name)?;
       let file = self
         .file_fetcher
         .fetch_bypass_permissions(&meta_url)

filename: tests/integration/jsr_tests.rs
patch: @@ -188,47 +188,47 @@ console.log(version);"#,
     .assert_matches_text("0.1.0\n");
 }
 
-#[test]
-fn reload_info_not_found_cache_but_exists_remote() {
-  fn remove_version(registry_json: &mut Value, version: &str) {
-    registry_json
-      .as_object_mut()
-      .unwrap()
-      .get_mut("versions")
-      .unwrap()
-      .as_object_mut()
-      .unwrap()
-      .remove(version);
-  }
+fn remove_version_from_meta_json(registry_json: &mut Value, version: &str) {
+  registry_json
+    .as_object_mut()
+    .unwrap()
+    .get_mut("versions")
+    .unwrap()
+    .as_object_mut()
+    .unwrap()
+    .remove(version);
+}
 
-  fn remove_version_for_package(
-    deno_dir: &util::TempDir,
-    package: &str,
-    version: &str,
-  ) {
-    let specifier =
-      Url::parse(&format!("http://127.0.0.1:4250/{}/meta.json", package))
-        .unwrap();
-    let cache = deno_cache_dir::GlobalHttpCache::new(
-      sys_traits::impls::RealSys,
-      deno_dir.path().join("remote").to_path_buf(),
-    );
-    let entry = cache
-      .get(&cache.cache_item_key(&specifier).unwrap(), None)
-      .unwrap()
-      .unwrap();
-    let mut registry_json: serde_json::Value =
-      serde_json::from_slice(&entry.content).unwrap();
-    remove_version(&mut registry_json, version);
-    cache
-      .set(
-        &specifier,
-        entry.metadata.headers.clone(),
-        registry_json.to_string().as_bytes(),
-      )
+fn remove_version_for_package(
+  deno_dir: &util::TempDir,
+  package: &str,
+  version: &str,
+) {
+  let specifier =
+    Url::parse(&format!("http://127.0.0.1:4250/{}/meta.json", package))
       .unwrap();
-  }
+  let cache = deno_cache_dir::GlobalHttpCache::new(
+    sys_traits::impls::RealSys,
+    deno_dir.path().join("remote").to_path_buf(),
+  );
+  let entry = cache
+    .get(&cache.cache_item_key(&specifier).unwrap(), None)
+    .unwrap()
+    .unwrap();
+  let mut registry_json: serde_json::Value =
+    serde_json::from_slice(&entry.content).unwrap();
+  remove_version_from_meta_json(&mut registry_json, version);
+  cache
+    .set(
+      &specifier,
+      entry.metadata.headers.clone(),
+      registry_json.to_string().as_bytes(),
+    )
+    .unwrap();
+}
 
+#[test]
+fn reload_info_not_found_cache_but_exists_remote() {
   // This tests that when a local machine doesn't have a version
   // specified in a dependency that exists in the npm registry
   let test_context = TestContextBuilder::for_jsr().use_temp_cwd().build();
@@ -273,6 +273,48 @@ fn reload_info_not_found_cache_but_exists_remote() {
     .assert_exit_code(0);
 }
 
+#[test]
+fn install_cache_busts_if_version_not_found() {
+  // Tests that if you try to deno install a package, if we can't find the version in the cached
+  // meta.json, we bust the cache to .
+  let test_context = TestContextBuilder::for_jsr().use_temp_cwd().build();
+  let deno_dir = test_context.deno_dir();
+  let temp_dir = test_context.temp_dir();
+  temp_dir.write(
+    "deno.json",
+    r#"{ "imports": { "@denotest/has-pre-release": "jsr:@denotest/has-pre-release@2.0.0-beta.1" } }"#,
+  );
+
+  // cache successfully to the deno_dir
+  let output = test_context.new_command().args("install").run();
+  output.assert_matches_text(concat!(
+    "Download http://127.0.0.1:4250/@denotest/has-pre-release/meta.json\n",
+    "Download http://127.0.0.1:4250/@denotest/has-pre-release/2.0.0-beta.1_meta.json\n",
+    "Download http://127.0.0.1:4250/@denotest/has-pre-release/2.0.0-beta.1/mod.ts\n",
+  ));
+
+  // modify the package information in the cache to remove the latest version
+  remove_version_for_package(
+    deno_dir,
+    "@denotest/has-pre-release",
+    "2.0.0-beta.2",
+  );
+
+  temp_dir.write(
+    "deno.json",
+    r#"{ "imports": { "@denotest/has-pre-release": "jsr:@denotest/has-pre-release@2.0.0-beta.2" } }"#,
+  );
+
+  // should error when `--cache-only` is used now because the version is not in the cache
+  let output = test_context.new_command().args("install").run();
+  output.assert_matches_text(concat!(
+    "Download http://127.0.0.1:4250/@denotest/has-pre-release/meta.json\n",
+    "Download http://127.0.0.1:4250/@denotest/has-pre-release/2.0.0-beta.2_meta.json\n",
+    "Download http://127.0.0.1:4250/@denotest/has-pre-release/2.0.0-beta.2/mod.ts\n",
+  ));
+  output.assert_exit_code(0);
+}
+
 #[tokio::test]
 async fn lockfile_bad_package_integrity() {
   let test_context = TestContextBuilder::for_jsr().use_temp_cwd().build();