Repository: denoland/deno
Pull request: fix(install): bust packument cache on version not found when loading extra info from lockfile

Closes https://github.com/denoland/deno/issues/29359

Co-authored-by: nathanwhit <nathanwhit@users.noreply.github.com>


Changed files:
filename: resolvers/npm_cache/registry_info.rs
patch: @@ -226,7 +226,7 @@ impl<THttpClient: NpmCacheHttpClient, TSys: NpmCacheSys>
     self: &Arc<Self>,
     name: &str,
   ) -> Result<Option<Arc<NpmPackageInfo>>, LoadPackageInfoInnerError> {
-    let (cache_item, clear_id) = {
+    let (value_creator, clear_id) = {
       let mut mem_cache = self.memory_cache.lock();
       let cache_item = if let Some(cache_item) = mem_cache.get(name) {
         cache_item.clone()
@@ -240,53 +240,53 @@ impl<THttpClient: NpmCacheHttpClient, TSys: NpmCacheSys>
         mem_cache.insert(name.to_string(), cache_item.clone());
         cache_item
       };
-      (cache_item, mem_cache.clear_id)
+      match cache_item {
+        MemoryCacheItem::FsCached(info) => return Ok(Some(info)),
+        MemoryCacheItem::MemoryCached(maybe_info) => {
+          return maybe_info.map_err(LoadPackageInfoInnerError)
+        }
+        MemoryCacheItem::Pending(value_creator) => {
+          (value_creator, mem_cache.clear_id)
+        }
+      }
     };
 
-    match cache_item {
-      MemoryCacheItem::FsCached(info) => Ok(Some(info)),
-      MemoryCacheItem::MemoryCached(maybe_info) => {
-        maybe_info.clone().map_err(LoadPackageInfoInnerError)
+    match value_creator.get().await {
+      Ok(FutureResult::SavedFsCache(info)) => {
+        // return back the future and mark this package as having
+        // been saved in the cache for next time it's requested
+        self.memory_cache.lock().try_insert(
+          clear_id,
+          name,
+          MemoryCacheItem::FsCached(info.clone()),
+        );
+        Ok(Some(info))
       }
-      MemoryCacheItem::Pending(value_creator) => {
-        match value_creator.get().await {
-          Ok(FutureResult::SavedFsCache(info)) => {
-            // return back the future and mark this package as having
-            // been saved in the cache for next time it's requested
-            self.memory_cache.lock().try_insert(
-              clear_id,
-              name,
-              MemoryCacheItem::FsCached(info.clone()),
-            );
-            Ok(Some(info))
-          }
-          Ok(FutureResult::ErroredFsCache(info)) => {
-            // since saving to the fs cache failed, keep the package information in memory
-            self.memory_cache.lock().try_insert(
-              clear_id,
-              name,
-              MemoryCacheItem::MemoryCached(Ok(Some(info.clone()))),
-            );
-            Ok(Some(info))
-          }
-          Ok(FutureResult::PackageNotExists) => {
-            self.memory_cache.lock().try_insert(
-              clear_id,
-              name,
-              MemoryCacheItem::MemoryCached(Ok(None)),
-            );
-            Ok(None)
-          }
-          Err(err) => {
-            let return_err = err.clone();
-            self.memory_cache.lock().try_insert(
-              clear_id,
-              name,
-              MemoryCacheItem::MemoryCached(Err(err)),
-            );
-            Err(LoadPackageInfoInnerError(return_err))
-          }
-        }
+      Ok(FutureResult::ErroredFsCache(info)) => {
+        // since saving to the fs cache failed, keep the package information in memory
+        self.memory_cache.lock().try_insert(
+          clear_id,
+          name,
+          MemoryCacheItem::MemoryCached(Ok(Some(info.clone()))),
+        );
+        Ok(Some(info))
+      }
+      Ok(FutureResult::PackageNotExists) => {
+        self.memory_cache.lock().try_insert(
+          clear_id,
+          name,
+          MemoryCacheItem::MemoryCached(Ok(None)),
+        );
+        Ok(None)
+      }
+      Err(err) => {
+        let return_err = err.clone();
+        self.memory_cache.lock().try_insert(
+          clear_id,
+          name,
+          MemoryCacheItem::MemoryCached(Err(err)),
+        );
+        Err(LoadPackageInfoInnerError(return_err))
       }
     }
   }

filename: resolvers/npm_installer/extra_info.rs
patch: @@ -134,14 +134,31 @@ impl NpmPackageExtraInfoProvider {
     &self,
     package_nv: &PackageNv,
   ) -> Result<NpmPackageExtraInfo, JsErrorBox> {
-    let package_info = self
+    let mut package_info = self
       .npm_registry_info_provider
       .package_info(&package_nv.name)
       .await
       .map_err(JsErrorBox::from_err)?;
-    let version_info = package_info
+    let version_info = match package_info
       .version_info(package_nv, &self.workspace_patch_packages.0)
-      .map_err(JsErrorBox::from_err)?;
+    {
+      Ok(version_info) => version_info,
+      Err(deno_npm::resolution::NpmPackageVersionNotFound { .. }) => {
+        // Don't bother checking the return value of mark_force_reload to tell
+        // whether to reload because we could race here with another task within
+        // this method. That said, ideally this code would only reload the
+        // specific packument that's out of date to be a bit more efficient.
+        self.npm_registry_info_provider.mark_force_reload();
+        package_info = self
+          .npm_registry_info_provider
+          .package_info(&package_nv.name)
+          .await
+          .map_err(JsErrorBox::from_err)?;
+        package_info
+          .version_info(package_nv, &self.workspace_patch_packages.0)
+          .map_err(JsErrorBox::from_err)?
+      }
+    };
     Ok(NpmPackageExtraInfo {
       deprecated: version_info.deprecated.clone(),
       bin: version_info.bin.clone(),

filename: tests/specs/install/cache_out_of_date_with_lockfile/__test__.jsonc
patch: @@ -0,0 +1,15 @@
+{
+  "tempDir": true,
+  "steps": [{
+    // this would only occur with a lockfile and a package with "extra info" (bin or script)
+    "args": "install npm:@denotest/bin",
+    "output": "[WILDCARD]"
+  }, {
+    "args": "run -A update.ts $DENO_DIR",
+    "output": ""
+  }, {
+    // should not error
+    "args": "install",
+    "output": "[WILDCARD]"
+  }]
+}

filename: tests/specs/install/cache_out_of_date_with_lockfile/package.json
patch: @@ -0,0 +1,2 @@
+{
+}

filename: tests/specs/install/cache_out_of_date_with_lockfile/update.ts
patch: @@ -0,0 +1,16 @@
+const path = Deno.args[0] + "/npm/localhost_4260/@denotest/bin/registry.json";
+const fileText = Deno.readTextFileSync(path);
+const data = JSON.parse(fileText);
+if (data.versions["1.0.0"] == null || data["dist-tags"].latest !== "1.0.0") {
+  throw new Error("Test relies on version 1.0.0 to be the latest version");
+}
+delete data.versions["1.0.0"];
+data["dist-tags"].latest = "0.5.0";
+delete data["_deno.etag"];
+Deno.writeTextFileSync(path, JSON.stringify(data));
+Deno.remove("node_modules", { recursive: true });
+
+// assert this exists
+if (!Deno.statSync("deno.lock").isFile) {
+  throw new Error("Expected a deno.lock file.");
+}