deno/cli/npm/installer/common/mod.rs
Nathan Whitaker ce5b9da11b
fix(install): handle when bin entry info isn't present in package.json but is in registry (#28822)
Apparently things like the `bin` field can appear in the version info
from the registry, but not the package's `package.json`. I'm still not
sure how you actually achieve this, but it's the case for
`esbuild-wasm`. This fixes the following panic:

```
❯ deno i --node-modules-dir npm:esbuild-wasm
Add npm:esbuild-wasm@0.25.2
Initialize ⣯ [00:00]
 - esbuild-wasm@0.25.2



============================================================
Deno has panicked. This is a bug in Deno. Please report this
at https://github.com/denoland/deno/issues/new.
If you can reliably reproduce this panic, include the
reproduction steps and re-run with the RUST_BACKTRACE=1 env
var set and include the backtrace in your report.

Platform: macos aarch64
Version: 2.2.8+58c6c0b
Args: ["deno", "i", "--node-modules-dir", "npm:esbuild-wasm"]

View stack trace at:
https://panic.deno.com/v2.2.8+58c6c0bc9c1b4ee08645be936ff9268f17028f0f/aarch64-apple-darwin/g4h6Jo393pB4k4kqBo-3kqBg6klqBogtyLg13yLw_t0Lw549Hgj8-Hgw__H428-F4yv_HgjkpKww7gIon4gIw54rKwi5MorzMw5y7G42g7Iw---I40s-I4vu4Jw2rEw8z7Dwnr6J4tp7Bo_vvK

thread 'main' panicked at cli/npm/installer/common/bin_entries.rs:108:30:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
2025-04-10 09:36:26 -07:00

172 lines
4.8 KiB
Rust

// Copyright 2018-2025 the Deno authors. MIT license.
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use deno_core::parking_lot::RwLock;
use deno_error::JsErrorBox;
use deno_npm::registry::NpmRegistryApi;
use deno_npm::NpmPackageExtraInfo;
use deno_npm::NpmResolutionPackage;
use deno_semver::package::PackageNv;
use super::PackageCaching;
use crate::npm::CliNpmCache;
pub mod bin_entries;
pub mod lifecycle_scripts;
/// Part of the resolution that interacts with the file system.
#[async_trait(?Send)]
pub trait NpmPackageFsInstaller: std::fmt::Debug + Send + Sync {
async fn cache_packages<'a>(
&self,
caching: PackageCaching<'a>,
) -> Result<(), JsErrorBox>;
}
pub trait NpmPackageExtraInfoProvider: std::fmt::Debug + Send + Sync {
async fn get_package_extra_info(
&self,
package_id: &PackageNv,
expected: ExpectedExtraInfo,
) -> Result<deno_npm::NpmPackageExtraInfo, JsErrorBox>;
}
impl<T: NpmPackageExtraInfoProvider + ?Sized> NpmPackageExtraInfoProvider
for Arc<T>
{
async fn get_package_extra_info(
&self,
package_id: &PackageNv,
expected: ExpectedExtraInfo,
) -> Result<deno_npm::NpmPackageExtraInfo, JsErrorBox> {
self
.as_ref()
.get_package_extra_info(package_id, expected)
.await
}
}
pub struct ExtraInfoProvider {
npm_cache: Arc<CliNpmCache>,
npm_registry_info_provider: Arc<dyn NpmRegistryApi + Send + Sync>,
cache: RwLock<rustc_hash::FxHashMap<PackageNv, NpmPackageExtraInfo>>,
}
impl std::fmt::Debug for ExtraInfoProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtraInfoProvider")
.field("npm_cache", &self.npm_cache)
.field("cache", &self.cache)
.finish()
}
}
impl ExtraInfoProvider {
pub fn new(
npm_cache: Arc<CliNpmCache>,
npm_registry_info_provider: Arc<dyn NpmRegistryApi + Send + Sync>,
) -> Self {
Self {
npm_cache,
npm_registry_info_provider,
cache: RwLock::new(rustc_hash::FxHashMap::default()),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ExpectedExtraInfo {
pub deprecated: bool,
pub bin: bool,
pub scripts: bool,
}
impl ExpectedExtraInfo {
pub fn from_package(package: &NpmResolutionPackage) -> Self {
Self {
deprecated: package.is_deprecated,
bin: package.has_bin,
scripts: package.has_scripts,
}
}
}
impl ExtraInfoProvider {
async fn fetch_from_registry(
&self,
package_nv: &PackageNv,
) -> Result<NpmPackageExtraInfo, JsErrorBox> {
let package_info = self
.npm_registry_info_provider
.package_info(&package_nv.name)
.await
.map_err(JsErrorBox::from_err)?;
let patched_packages = HashMap::new();
let version_info = package_info
.version_info(package_nv, &patched_packages)
.map_err(JsErrorBox::from_err)?;
Ok(NpmPackageExtraInfo {
deprecated: version_info.deprecated.clone(),
bin: version_info.bin.clone(),
scripts: version_info.scripts.clone(),
})
}
async fn fetch_from_package_json(
&self,
package_nv: &PackageNv,
) -> Result<NpmPackageExtraInfo, JsErrorBox> {
let folder_path = self.npm_cache.package_folder_for_nv(package_nv);
let package_json_path = folder_path.join("package.json");
let extra_info: NpmPackageExtraInfo =
tokio::task::spawn_blocking(move || {
let package_json = std::fs::read_to_string(&package_json_path)
.map_err(JsErrorBox::from_err)?;
let extra_info: NpmPackageExtraInfo =
deno_core::serde_json::from_str(&package_json)
.map_err(JsErrorBox::from_err)?;
Ok::<_, JsErrorBox>(extra_info)
})
.await
.map_err(JsErrorBox::from_err)??;
Ok(extra_info)
}
}
impl super::common::NpmPackageExtraInfoProvider for ExtraInfoProvider {
async fn get_package_extra_info(
&self,
package_nv: &PackageNv,
expected: ExpectedExtraInfo,
) -> Result<NpmPackageExtraInfo, JsErrorBox> {
if let Some(extra_info) = self.cache.read().get(package_nv) {
return Ok(extra_info.clone());
}
let extra_info = if expected.deprecated {
// we need the registry version info to get the deprecated string, since it's not in the
// package's package.json
self.fetch_from_registry(package_nv).await?
} else {
let extra_info = self.fetch_from_package_json(package_nv).await?;
// some packages have bin in registry but not in package.json (e.g. esbuild-wasm)
// still not sure how that happens
if (expected.bin && extra_info.bin.is_none())
|| (expected.scripts && extra_info.scripts.is_empty())
{
self.fetch_from_registry(package_nv).await?
} else {
extra_info
}
};
self
.cache
.write()
.insert(package_nv.clone(), extra_info.clone());
Ok(extra_info)
}
}