Skip to content

Commit acc328f

Browse files
authored
feat(outdated): warn about packages skipped due to registry errors (#34974)
Closes #32179
1 parent a5ab2ba commit acc328f

10 files changed

Lines changed: 241 additions & 44 deletions

File tree

cli/jsr.rs

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use deno_semver::package::PackageReq;
1515

1616
use crate::args::jsr_url;
1717
use crate::file_fetcher::CliFileFetcher;
18+
use crate::npm::PackageInfoLoadError;
1819

1920
/// This is similar to a subset of `JsrCacheResolver` which fetches rather than
2021
/// just reads the cache. Keep in sync!
@@ -24,7 +25,8 @@ pub struct JsrFetchResolver {
2425
/// The `module_graph` field of the version infos should be forcibly absent.
2526
/// It can be large and we don't want to store it.
2627
info_by_nv: DashMap<PackageNv, Option<Arc<JsrPackageVersionInfo>>>,
27-
info_by_name: DashMap<String, Option<Arc<JsrPackageInfo>>>,
28+
info_by_name:
29+
DashMap<String, Result<Arc<JsrPackageInfo>, Arc<PackageInfoLoadError>>>,
2830
file_fetcher: Arc<CliFileFetcher>,
2931
jsr_version_resolver: Arc<JsrVersionResolver>,
3032
}
@@ -116,34 +118,57 @@ impl JsrFetchResolver {
116118
.ok()?;
117119
let info = serde_json::from_slice::<JsrPackageInfo>(&file.source).ok()?;
118120
let info = Arc::new(info);
119-
self
120-
.info_by_name
121-
.insert(name.to_string(), Some(info.clone()));
121+
self.info_by_name.insert(name.to_string(), Ok(info.clone()));
122122
Some(info)
123123
}
124124

125125
fn meta_url(&self, name: &str) -> Option<deno_core::url::Url> {
126126
jsr_url().join(&format!("{}/meta.json", name)).ok()
127127
}
128128

129-
// todo(dsherret): this should return error messages and only `None` when the package
130-
// doesn't exist
131129
pub async fn package_info(&self, name: &str) -> Option<Arc<JsrPackageInfo>> {
130+
self.package_info_with_reason(name).await.ok()
131+
}
132+
133+
/// Like [`Self::package_info`], but preserves the reason the fetch failed
134+
/// so callers can surface it instead of silently treating the package as
135+
/// having no available versions.
136+
pub async fn package_info_with_reason(
137+
&self,
138+
name: &str,
139+
) -> Result<Arc<JsrPackageInfo>, Arc<PackageInfoLoadError>> {
132140
if let Some(info) = self.info_by_name.get(name) {
133141
return info.value().clone();
134142
}
135-
let fetch_package_info = || async {
136-
let meta_url = self.meta_url(name)?;
137-
let file = self
138-
.file_fetcher
139-
.fetch_bypass_permissions(&meta_url)
143+
let result =
144+
self
145+
.fetch_package_info(name)
140146
.await
141-
.ok()?;
142-
serde_json::from_slice::<JsrPackageInfo>(&file.source).ok()
143-
};
144-
let info = fetch_package_info().await.map(Arc::new);
145-
self.info_by_name.insert(name.to_string(), info.clone());
146-
info
147+
.map(Arc::new)
148+
.map_err(|reason| {
149+
Arc::new(PackageInfoLoadError {
150+
registry_url: jsr_url().to_string(),
151+
reason,
152+
})
153+
});
154+
self.info_by_name.insert(name.to_string(), result.clone());
155+
result
156+
}
157+
158+
async fn fetch_package_info(
159+
&self,
160+
name: &str,
161+
) -> Result<JsrPackageInfo, String> {
162+
let meta_url = self
163+
.meta_url(name)
164+
.ok_or_else(|| format!("invalid package name: {name}"))?;
165+
let file = self
166+
.file_fetcher
167+
.fetch_bypass_permissions(&meta_url)
168+
.await
169+
.map_err(|e| format!("{e:#}"))?;
170+
serde_json::from_slice::<JsrPackageInfo>(&file.source)
171+
.map_err(|e| format!("failed to parse package metadata: {e}"))
147172
}
148173

149174
pub async fn package_version_info(

cli/npm.rs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,22 @@ fn decompress_gzip(compressed: Vec<u8>) -> Result<Vec<u8>, JsErrorBox> {
240240
Ok(decompressed)
241241
}
242242

243+
/// Why fetching package metadata for a single package failed. Used to surface
244+
/// actionable diagnostics (e.g. in `deno outdated`) instead of silently
245+
/// dropping packages that live on unreachable or unauthorized registries.
246+
#[derive(Clone, Debug)]
247+
pub struct PackageInfoLoadError {
248+
/// The registry URL the metadata was being fetched from.
249+
pub registry_url: String,
250+
/// A human readable description of what went wrong.
251+
pub reason: String,
252+
}
253+
243254
#[derive(Debug)]
244255
pub struct NpmFetchResolver {
245256
nv_by_req: DashMap<PackageReq, Option<PackageNv>>,
246-
info_by_name: DashMap<String, Option<Arc<NpmPackageInfo>>>,
257+
info_by_name:
258+
DashMap<String, Result<Arc<NpmPackageInfo>, Arc<PackageInfoLoadError>>>,
247259
file_fetcher: Arc<CliFileFetcher>,
248260
npmrc: Arc<ResolvedNpmRc>,
249261
version_resolver: Arc<NpmVersionResolver>,
@@ -293,18 +305,44 @@ impl NpmFetchResolver {
293305
}
294306

295307
pub async fn package_info(&self, name: &str) -> Option<Arc<NpmPackageInfo>> {
308+
self.package_info_with_reason(name).await.ok()
309+
}
310+
311+
/// Like [`Self::package_info`], but preserves the reason the fetch failed
312+
/// (e.g. an HTTP 401 from a private registry) so callers can surface it
313+
/// instead of silently treating the package as having no available versions.
314+
pub async fn package_info_with_reason(
315+
&self,
316+
name: &str,
317+
) -> Result<Arc<NpmPackageInfo>, Arc<PackageInfoLoadError>> {
296318
if let Some(info) = self.info_by_name.get(name) {
297319
return info.value().clone();
298320
}
299-
// todo(#27198): use RegistryInfoProvider instead
300-
let fetch_package_info = || async {
301-
let info_url = deno_npm_cache::get_package_url(&self.npmrc, name);
302-
let registry_config = self.npmrc.get_registry_config(name);
303-
// TODO(bartlomieju): this should error out, not use `.ok()`.
304-
let maybe_auth_header =
305-
deno_npm_cache::maybe_auth_header_value_for_npm_registry(
306-
registry_config,
307-
)
321+
let registry_url = self.npmrc.get_registry_url(name).to_string();
322+
let result =
323+
self
324+
.fetch_package_info(name)
325+
.await
326+
.map(Arc::new)
327+
.map_err(|reason| {
328+
Arc::new(PackageInfoLoadError {
329+
registry_url: registry_url.clone(),
330+
reason,
331+
})
332+
});
333+
self.info_by_name.insert(name.to_string(), result.clone());
334+
result
335+
}
336+
337+
// todo(#27198): use RegistryInfoProvider instead
338+
async fn fetch_package_info(
339+
&self,
340+
name: &str,
341+
) -> Result<NpmPackageInfo, String> {
342+
let info_url = deno_npm_cache::get_package_url(&self.npmrc, name);
343+
let registry_config = self.npmrc.get_registry_config(name);
344+
let maybe_auth_header =
345+
deno_npm_cache::maybe_auth_header_value_for_npm_registry(registry_config)
308346
.map_err(AnyError::from)
309347
.and_then(|value| match value {
310348
Some(value) => Ok(Some((
@@ -313,17 +351,14 @@ impl NpmFetchResolver {
313351
))),
314352
None => Ok(None),
315353
})
316-
.ok()?;
317-
let file = self
318-
.file_fetcher
319-
.fetch_bypass_permissions_with_maybe_auth(&info_url, maybe_auth_header)
320-
.await
321-
.ok()?;
322-
serde_json::from_slice::<NpmPackageInfo>(&file.source).ok()
323-
};
324-
let info = fetch_package_info().await.map(Arc::new);
325-
self.info_by_name.insert(name.to_string(), info.clone());
326-
info
354+
.map_err(|e| format!("{e:#}"))?;
355+
let file = self
356+
.file_fetcher
357+
.fetch_bypass_permissions_with_maybe_auth(&info_url, maybe_auth_header)
358+
.await
359+
.map_err(|e| format!("{e:#}"))?;
360+
serde_json::from_slice::<NpmPackageInfo>(&file.source)
361+
.map_err(|e| format!("failed to parse package metadata: {e}"))
327362
}
328363

329364
pub fn applicable_version_infos<'a>(

cli/tools/pm/deps.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use crate::module_loader::ModuleLoadPreparer;
5050
use crate::npm::CliNpmInstaller;
5151
use crate::npm::CliNpmResolver;
5252
use crate::npm::NpmFetchResolver;
53+
use crate::npm::PackageInfoLoadError;
5354
use crate::util::progress_bar::ProgressBar;
5455
use crate::util::sync::AtomicFlag;
5556

@@ -453,6 +454,10 @@ where
453454
pub struct PackageLatestVersion {
454455
pub semver_compatible: Option<PackageNv>,
455456
pub latest: Option<PackageNv>,
457+
/// Set when fetching the package's metadata failed (e.g. an unreachable or
458+
/// unauthorized private registry). The package is dropped from the outdated
459+
/// table, so this is used to warn the user instead of skipping silently.
460+
pub fetch_error: Option<Arc<PackageInfoLoadError>>,
456461
}
457462

458463
pub struct DepManager {
@@ -739,9 +744,13 @@ impl DepManager {
739744
.await
740745
.ok()
741746
.flatten();
742-
let info =
743-
self.npm_fetch_resolver.package_info(&semver_req.name).await;
744-
let latest = info
747+
let info_result = self
748+
.npm_fetch_resolver
749+
.package_info_with_reason(&semver_req.name)
750+
.await;
751+
let fetch_error = info_result.as_ref().err().cloned();
752+
let latest = info_result
753+
.ok()
745754
.and_then(|info| {
746755
let version_resolver =
747756
self.npm_version_resolver.get_for_package(&info);
@@ -781,6 +790,7 @@ impl DepManager {
781790
PackageLatestVersion {
782791
latest,
783792
semver_compatible,
793+
fetch_error,
784794
}
785795
}
786796
.boxed_local(),
@@ -795,9 +805,13 @@ impl DepManager {
795805
.await
796806
.ok()
797807
.flatten();
798-
let info =
799-
self.jsr_fetch_resolver.package_info(&semver_req.name).await;
800-
let latest = info
808+
let info_result = self
809+
.jsr_fetch_resolver
810+
.package_info_with_reason(&semver_req.name)
811+
.await;
812+
let fetch_error = info_result.as_ref().err().cloned();
813+
let latest = info_result
814+
.ok()
801815
.and_then(|info| {
802816
let version_resolver = self
803817
.jsr_fetch_resolver
@@ -831,6 +845,7 @@ impl DepManager {
831845
PackageLatestVersion {
832846
latest,
833847
semver_compatible,
848+
fetch_error,
834849
}
835850
}
836851
.boxed_local(),

cli/tools/pm/outdated/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ use crate::file_fetcher::CreateCliFileFetcherOptions;
2929
use crate::file_fetcher::create_cli_file_fetcher;
3030
use crate::jsr::JsrFetchResolver;
3131
use crate::npm::NpmFetchResolver;
32+
use crate::npm::PackageInfoLoadError;
33+
34+
/// Packages whose metadata could not be fetched (e.g. unreachable or
35+
/// unauthorized private registries) are dropped from the update check. They are
36+
/// collected here, keyed and deduplicated by `(kind, name)`, so we can warn the
37+
/// user instead of skipping them silently.
38+
type SkippedPackages =
39+
std::collections::BTreeMap<(DepKind, StackString), Arc<PackageInfoLoadError>>;
3240

3341
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
3442
struct OutdatedPackage {
@@ -129,11 +137,14 @@ fn print_outdated(
129137
) -> Result<(), AnyError> {
130138
let mut outdated = Vec::new();
131139
let mut seen = std::collections::BTreeSet::new();
140+
let mut skipped = SkippedPackages::new();
132141
for (dep_id, resolved, latest_versions) in
133142
deps.deps_with_resolved_latest_versions()
134143
{
135144
let dep = deps.get_dep(dep_id);
136145

146+
collect_skipped(&mut skipped, dep.kind, &dep.req.name, &latest_versions);
147+
137148
let Some(resolved) = resolved else { continue };
138149

139150
let latest = {
@@ -171,12 +182,75 @@ fn print_outdated(
171182
if !outdated.is_empty() {
172183
outdated.sort();
173184
print_outdated_table(&outdated);
185+
}
186+
187+
print_skipped_warning(&skipped);
188+
189+
if !outdated.is_empty() {
174190
print_suggestion(compatible);
175191
}
176192

177193
Ok(())
178194
}
179195

196+
/// Records a package whose metadata fetch failed, deduplicating by
197+
/// `(kind, name)` and keeping the first reason seen.
198+
fn collect_skipped(
199+
skipped: &mut SkippedPackages,
200+
kind: DepKind,
201+
name: &StackString,
202+
latest_versions: &PackageLatestVersion,
203+
) {
204+
if let Some(error) = &latest_versions.fetch_error {
205+
skipped
206+
.entry((kind, name.clone()))
207+
.or_insert_with(|| error.clone());
208+
}
209+
}
210+
211+
fn print_skipped_warning(skipped: &SkippedPackages) {
212+
if skipped.is_empty() {
213+
return;
214+
}
215+
216+
log::warn!("");
217+
log::warn!(
218+
"{}",
219+
color_print::cformat!(
220+
"<yellow>Warning</>: Unable to check updates for {} package(s), their metadata could not be fetched (e.g. private registry auth or network issues).",
221+
skipped.len(),
222+
)
223+
);
224+
225+
if log::log_enabled!(log::Level::Debug) {
226+
for ((kind, name), error) in skipped {
227+
log::warn!(
228+
"{}",
229+
color_print::cformat!(
230+
" <bold>{}:{}</> (registry: {})",
231+
kind.scheme(),
232+
name,
233+
error.registry_url,
234+
)
235+
);
236+
log::warn!(" Reason: {}", error.reason);
237+
}
238+
log::warn!(
239+
"{}",
240+
color_print::cformat!(
241+
"<p(245)>Verify your registry credentials (.npmrc) and network connectivity.</>"
242+
)
243+
);
244+
} else {
245+
log::warn!(
246+
"{}",
247+
color_print::cformat!(
248+
"<p(245)>Run with </><u>--log-level=debug</><p(245)> for details on which packages could not be checked.</>"
249+
)
250+
);
251+
}
252+
}
253+
180254
pub async fn outdated(
181255
flags: Arc<Flags>,
182256
update_flags: OutdatedFlags,
@@ -367,13 +441,15 @@ async fn update(
367441
let mut to_update = Vec::new();
368442

369443
let mut can_update_to_latest = false;
444+
let mut skipped = SkippedPackages::new();
370445

371446
for (dep_id, resolved, latest_versions) in deps
372447
.deps_with_resolved_latest_versions()
373448
.into_iter()
374449
.collect::<Vec<_>>()
375450
{
376451
let dep = deps.get_dep(dep_id);
452+
collect_skipped(&mut skipped, dep.kind, &dep.req.name, &latest_versions);
377453
let new_version_req = choose_new_version_req(
378454
dep,
379455
resolved.as_ref(),
@@ -552,6 +628,8 @@ async fn update(
552628
}
553629
}
554630

631+
print_skipped_warning(&skipped);
632+
555633
Ok(())
556634
}
557635

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@acme:registry=http://localhost:9/

0 commit comments

Comments
 (0)