/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/pm/why.rs
350 строк
10 KB
Bartek Iwańczuk
fix(npm): support `catalog:` protocol in deno.json imports (#35168)
13 июн 2026, 10:22
Не верифицирован
13 июн 2026, 10:22
8297980
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_package_json::PackageJsonDepValue; use deno_semver::jsr::JsrDepPackageReq; use deno_semver::package::PackageKind; use crate::args::Flags; use crate::args::WhyFlags; use crate::colors; use crate::factory::CliFactory; const MAX_PATHS_PER_VERSION: usize = 15; /// Strip the peer-dependency suffix from an npm lockfile key. /// The peer suffix `_` only appears after the version, so we find the `@` /// version separator first, then look for `_` only in the version portion. /// e.g. "react-dom@18.2.0_react@18.2.0" → "react-dom@18.2.0" /// "is_odd@3.0.1" → "is_odd@3.0.1" fn strip_peer_suffix(key: &str) -> &str { // Find the @ that separates name from version (skip leading @ for scoped pkgs) let Some(at_pos) = key[1..].find('@').map(|p| p + 1) else { return key; }; // Look for _ only after the version separator if let Some(underscore_pos) = key[at_pos + 1..].find('_') { &key[..at_pos + 1 + underscore_pos] } else { key } } /// Split `name@version` into `(name, version)`. fn split_name_version(s: &str) -> Option<(&str, &str)> { let at_pos = s[1..].find('@').map(|p| p + 1)?; Some((&s[..at_pos], &s[at_pos + 1..])) } /// Internal id for a package across both npm and jsr lockfile entries. /// /// The string portion is the lockfile's key: /// - npm: e.g. "express@4.22.1" or "react-dom@18.2.0_react@18.2.0" /// - jsr: e.g. "@std/async@1.2.0" type PkgId = (PackageKind, String); fn display_base(id: &PkgId) -> &str { match id.0 { PackageKind::Npm => strip_peer_suffix(&id.1), PackageKind::Jsr => &id.1, } } pub async fn why( flags: Arc<Flags>, why_flags: WhyFlags, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags); let lockfile = factory.maybe_lockfile().await?.cloned().ok_or_else(|| { deno_core::anyhow::anyhow!("No lockfile found. Run `deno install` first.") })?; // Workspace-declared root dependency requirements (from deno.json `imports`, // `scopes`, `jsxImportSource`, etc., and from package.json // `dependencies`/`devDependencies`). These are matched against the lockfile's // `specifiers` map to decide which entries are user-imported roots vs. // transitive resolutions that the lockfile also stores under `specifiers` // (as JSR does for every dep req). let workspace = factory.cli_options()?.workspace(); let mut root_reqs: HashSet<JsrDepPackageReq> = HashSet::new(); for deno_json in workspace.deno_jsons() { root_reqs.extend(deno_json.dependencies(workspace.catalogs())); } for pkg_json in workspace.package_jsons() { let deps = pkg_json.resolve_local_package_json_deps(); for dep in deps .dependencies .values() .chain(deps.dev_dependencies.values()) { if let Ok(PackageJsonDepValue::Req(req)) = dep { root_reqs.insert(JsrDepPackageReq::npm(req.clone())); } } } let lockfile_guard = lockfile.lock(); let content = &lockfile_guard.content; // Parse the query: optional kind prefix ("jsr:"/"npm:") and optional version. let query = &why_flags.package; let (query_kind, name_and_version) = if let Some(rest) = query.strip_prefix("jsr:") { (Some(PackageKind::Jsr), rest) } else if let Some(rest) = query.strip_prefix("npm:") { (Some(PackageKind::Npm), rest) } else { (None, query.as_str()) }; let (query_name, query_version) = if let Some(at_pos) = name_and_version[1..].find('@') { let at_pos = at_pos + 1; ( &name_and_version[..at_pos], Some(name_and_version[at_pos + 1..].to_string()), ) } else { (name_and_version, None) }; // Collect all packages from both npm and jsr sections, with their deps as // unified PkgId references. We also build a forward dep map so that we can // derive the reverse map below. let mut all_packages: Vec<PkgId> = Vec::new(); let mut forward_deps: HashMap<PkgId, Vec<PkgId>> = HashMap::new(); for key in content.packages.npm.keys() { let id: PkgId = (PackageKind::Npm, key.to_string()); all_packages.push(id.clone()); let info = &content.packages.npm[key]; let deps: Vec<PkgId> = info .dependencies .values() .chain(info.optional_dependencies.values()) .chain(info.optional_peers.values()) .map(|dep_key| (PackageKind::Npm, dep_key.to_string())) .collect(); forward_deps.insert(id, deps); } for (nv, info) in content.packages.jsr.iter() { let id: PkgId = (PackageKind::Jsr, nv.to_string()); all_packages.push(id.clone()); let deps: Vec<PkgId> = info .dependencies .iter() .filter_map(|dep_req| { let resolved = content.packages.specifiers.get(dep_req)?; let dep_name = dep_req.req.name.as_str(); Some((dep_req.kind, format!("{}@{}", dep_name, resolved))) }) .collect(); forward_deps.insert(id, deps); } // Find matching packages (filtered by optional kind, name, and version). let matching: Vec<&PkgId> = all_packages .iter() .filter(|id| { if let Some(kind) = query_kind && kind != id.0 { return false; } let base = display_base(id); let Some((name, version)) = split_name_version(base) else { return false; }; name == query_name && query_version.as_deref().is_none_or(|v| version == v) }) .collect(); if matching.is_empty() { bail!("package '{}' not found in the dependency tree", query); } // Build reverse-dep map. let mut reverse_deps: BTreeMap<PkgId, Vec<PkgId>> = BTreeMap::new(); for (parent, deps) in &forward_deps { for dep in deps { reverse_deps .entry(dep.clone()) .or_default() .push(parent.clone()); } } for parents in reverse_deps.values_mut() { parents.sort(); parents.dedup(); } // A lockfile specifier is a root iff it matches a req the user declared in // a deno.json or package.json. JSR transitive deps also appear in // `specifiers` (the lockfile normalizes JSR deps through the specifiers // map), so we can't treat every entry as a root the way the original // npm-only implementation did. let mut root_specifier_to_pkg: HashMap<PkgId, Vec<String>> = HashMap::new(); for (req, resolved) in content.packages.specifiers.iter() { if !root_reqs.contains(req) { continue; } let name = req.req.name.as_str(); let pkg_key = format!("{}@{}", name, resolved); root_specifier_to_pkg .entry((req.kind, pkg_key)) .or_default() .push(req.to_string()); } for id in &matching { let base = display_base(id); log::info!("{}", colors::bold(base)); if let Some(specifiers) = root_specifier_to_pkg.get(*id) { for spec in specifiers { log::info!(" {}", colors::green(spec)); } } let mut paths = find_paths_to_root( id, &reverse_deps, &root_specifier_to_pkg, MAX_PATHS_PER_VERSION, ); paths.sort_by_key(|p| p.len()); if paths.is_empty() && !root_specifier_to_pkg.contains_key(*id) { log::info!( " (no dependency path found -- try running `deno install` to refresh the lockfile)" ); } if paths.len() > MAX_PATHS_PER_VERSION { for path in &paths[..MAX_PATHS_PER_VERSION] { log::info!("{}", format_dependency_path(path, &root_specifier_to_pkg)); } log::info!( " ... and {} more paths", paths.len() - MAX_PATHS_PER_VERSION ); } else { for path in &paths { log::info!("{}", format_dependency_path(path, &root_specifier_to_pkg)); } } log::info!(""); } Ok(()) } /// Find dependency paths from root packages to the target. /// /// `max_paths` caps how many paths are collected; once reached the /// search short-circuits to avoid exponential expansion on wide /// diamond dependency graphs (e.g. `ms` reachable via 30+ routes /// in a Next.js app). fn find_paths_to_root<'a>( target_id: &'a PkgId, reverse_deps: &'a BTreeMap<PkgId, Vec<PkgId>>, root_specifiers: &HashMap<PkgId, Vec<String>>, max_paths: usize, ) -> Vec<Vec<&'a PkgId>> { let mut paths: Vec<Vec<&'a PkgId>> = Vec::new(); let mut current_path: Vec<&'a PkgId> = vec![target_id]; fn dfs<'a>( current_id: &'a PkgId, target_id: &'a PkgId, current_path: &mut Vec<&'a PkgId>, paths: &mut Vec<Vec<&'a PkgId>>, reverse_deps: &'a BTreeMap<PkgId, Vec<PkgId>>, root_specifiers: &HashMap<PkgId, Vec<String>>, max_paths: usize, ) { if paths.len() >= max_paths { return; } // Check if we reached a root (but not the starting node itself, // since direct specifiers are printed separately by the caller) if current_id != target_id && root_specifiers.contains_key(current_id) { paths.push(current_path.iter().rev().copied().collect()); return; } if let Some(parents) = reverse_deps.get(current_id) { for parent in parents { if paths.len() >= max_paths { return; } // Path-based cycle check: only skip nodes already on the // current path. This prevents infinite loops while still // allowing a node to appear in multiple independent paths. if current_path.contains(&parent) { continue; } current_path.push(parent); dfs( parent, target_id, current_path, paths, reverse_deps, root_specifiers, max_paths, ); current_path.pop(); } } } dfs( target_id, target_id, &mut current_path, &mut paths, reverse_deps, root_specifiers, max_paths, ); paths } /// Format a dependency path as a string. fn format_dependency_path( path: &[&PkgId], root_specifiers: &HashMap<PkgId, Vec<String>>, ) -> String { if path.is_empty() { return String::new(); } let root = path[0]; let base = display_base(root); let mut out = String::new(); if let Some(specs) = root_specifiers.get(root) { out.push_str(&format!(" {}", colors::green(&specs[0]))); } else { out.push_str(&format!(" {}", base)); } for id in &path[1..] { out.push_str(&format!(" > {}", display_base(id))); } out }