/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/pm/list.rs
429 строк
14 KB
Bartek Iwańczuk
feat(cli): add deno list subcommand to list declared dependencies (#34972)
22 июн 2026, 18:08
Не верифицирован
22 июн 2026, 18:08
755552e
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::collections::HashMap; use std::sync::Arc; use deno_cache_dir::GlobalOrLocalHttpCache; use deno_cache_dir::file_fetcher::CacheSetting; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_semver::package::PackageKind; use deno_semver::package::PackageReq; use deno_terminal::colors; use super::deps::DepKind; use super::deps::DepManager; use super::deps::DepManagerArgs; use super::outdated::filter::FilterSet; use crate::args::Flags; use crate::args::ListFlags; use crate::factory::CliFactory; use crate::file_fetcher::CreateCliFileFetcherOptions; use crate::file_fetcher::create_cli_file_fetcher; use crate::jsr::JsrFetchResolver; use crate::npm::NpmFetchResolver; /// Internal id for a package across npm and jsr lockfile entries. The string is /// the lockfile key, e.g. `express@4.22.1` (npm, may carry a peer suffix) or /// `@std/async@1.2.0` (jsr). type PkgId = (PackageKind, String); /// Strip the peer-dependency suffix from an npm lockfile key, e.g. /// `react-dom@18.2.0_react@18.2.0` becomes `react-dom@18.2.0`. fn strip_peer_suffix(key: &str) -> &str { let Some(at_pos) = key[1..].find('@').map(|p| p + 1) else { return key; }; if let Some(underscore_pos) = key[at_pos + 1..].find('_') { &key[..at_pos + 1 + underscore_pos] } else { key } } /// Split a `name@version` lockfile key into `(name, version)`. fn split_name_version(s: &str) -> (&str, &str) { match s[1..].find('@').map(|p| p + 1) { Some(at_pos) => (&s[..at_pos], &s[at_pos + 1..]), None => (s, ""), } } /// The resolved dependency graph of the workspace, derived from the lockfile. struct DepGraph { /// Forward edges: package -> its (resolved) dependencies. forward: HashMap<PkgId, Vec<PkgId>>, /// `name@version` (peer suffix stripped) -> full npm lockfile key, used to /// map a resolved root requirement onto its lockfile node. npm_by_base: HashMap<String, String>, } impl DepGraph { fn from_lockfile_content(content: &deno_lockfile::PackagesContent) -> Self { let mut forward: HashMap<PkgId, Vec<PkgId>> = HashMap::new(); let mut npm_by_base: HashMap<String, String> = HashMap::new(); for (key, info) in content.npm.iter() { // When several lockfile entries share a base `name@version` but differ // only by peer suffix, we keep the first one. A declared root maps to a // single node, so the tree shows that node's peer resolution; this only // matters for the rare case of the same package present with distinct // peer sets. npm_by_base .entry(strip_peer_suffix(key).to_string()) .or_insert_with(|| key.to_string()); let deps = info .dependencies .values() .chain(info.optional_dependencies.values()) .chain(info.optional_peers.values()) .map(|dep_key| (PackageKind::Npm, dep_key.to_string())) .collect(); forward.insert((PackageKind::Npm, key.to_string()), deps); } for (nv, info) in content.jsr.iter() { let deps = info .dependencies .iter() .filter_map(|dep_req| { let resolved = content.specifiers.get(dep_req)?; let id = match dep_req.kind { PackageKind::Jsr => ( PackageKind::Jsr, format!("{}@{}", dep_req.req.name, resolved), ), PackageKind::Npm => { // `specifiers` records an npm resolution as a bare version, // possibly with a peer suffix. Usually `name@<resolved>` is the // `content.npm` key verbatim (preferred: it pins the exact peer // variant), but the suffix is sometimes formatted differently // (`version__peer@1.0.0` vs the key's `name@version_peer@1`), so // fall back to mapping the base `name@version` onto its node. let exact = format!("{}@{}", dep_req.req.name, resolved); let key = if forward.contains_key(&(PackageKind::Npm, exact.clone())) { exact } else { let base_version = resolved.split('_').next().unwrap_or(resolved); let base = format!("{}@{}", dep_req.req.name, base_version); npm_by_base.get(&base).cloned().unwrap_or(exact) }; (PackageKind::Npm, key) } }; Some(id) }) .collect(); forward.insert((PackageKind::Jsr, nv.to_string()), deps); } Self { forward, npm_by_base, } } /// Resolve a declared root (kind + `name@version`) to its lockfile node. fn root_id(&self, kind: DepKind, name_version: &str) -> Option<PkgId> { match kind { DepKind::Npm => self .npm_by_base .get(name_version) .map(|key| (PackageKind::Npm, key.clone())), DepKind::Jsr => { let id = (PackageKind::Jsr, name_version.to_string()); self.forward.contains_key(&id).then_some(id) } } } fn children(&self, id: &PkgId) -> &[PkgId] { self.forward.get(id).map(Vec::as_slice).unwrap_or(&[]) } } /// Scheme prefix for a lockfile package kind (`npm` / `jsr`). For declared /// [`DepKind`] roots use `DepKind::scheme()` instead. fn scheme(kind: PackageKind) -> &'static str { match kind { PackageKind::Npm => "npm", PackageKind::Jsr => "jsr", } } /// A declared root dependency, after filtering. struct Root { alias: Option<String>, kind: DepKind, name: String, required: String, resolved: Option<String>, } impl Root { fn label(&self) -> String { let mut label = format!("{}:{}", self.kind.scheme(), self.name); if let Some(alias) = &self.alias { label = format!("{} ({})", label, alias); } match &self.resolved { Some(v) => format!("{} {}", label, colors::gray(v)), None => format!("{} {}", label, colors::gray("(unresolved)")), } } } // --------------------------------------------------------------------------- // Flat output (default, --depth 0) // --------------------------------------------------------------------------- #[allow(clippy::print_stdout, reason = "list output")] fn print_flat_table(roots: &[Root]) { const HEADINGS: &[&str] = &["Package", "Required", "Resolved"]; let name_of = |r: &Root| match &r.alias { Some(a) => format!("{}:{} ({})", r.kind.scheme(), r.name, a), None => format!("{}:{}", r.kind.scheme(), r.name), }; let resolved_of = |r: &Root| r.resolved.clone().unwrap_or("-".into()); // Column widths use byte length. Package names, versions and requirements // are ASCII, so this matches display width here. let mut w = [HEADINGS[0].len(), HEADINGS[1].len(), HEADINGS[2].len()]; for r in roots { w[0] = w[0].max(name_of(r).len()); w[1] = w[1].max(r.required.len()); w[2] = w[2].max(resolved_of(r).len()); } let pad = |s: &str, n: usize| format!("{}{}", s, " ".repeat(n - s.len())); let fills: Vec<String> = w.iter().map(|n| "─".repeat(n + 2)).collect(); println!("┌{}┬{}┬{}┐", fills[0], fills[1], fills[2]); println!( "│ {} │ {} │ {} │", colors::intense_blue(pad(HEADINGS[0], w[0])), colors::intense_blue(pad(HEADINGS[1], w[1])), colors::intense_blue(pad(HEADINGS[2], w[2])), ); for r in roots { println!("├{}┼{}┼{}┤", fills[0], fills[1], fills[2]); println!( "│ {} │ {} │ {} │", pad(&name_of(r), w[0]), pad(&r.required, w[1]), pad(&resolved_of(r), w[2]), ); } println!("└{}┴{}┴{}┘", fills[0], fills[1], fills[2]); } // --------------------------------------------------------------------------- // Tree output (--depth >= 1) // --------------------------------------------------------------------------- #[allow(clippy::print_stdout, reason = "list output")] fn print_tree(roots: &[Root], graph: &DepGraph, max_depth: u16) { fn walk( id: &PkgId, graph: &DepGraph, prefix: &str, depth: u16, max_depth: u16, on_path: &mut Vec<PkgId>, ) { if depth >= max_depth { return; } let mut children: Vec<&PkgId> = graph.children(id).iter().collect(); children.sort(); children.dedup(); let mut iter = children.iter().peekable(); while let Some(child) = iter.next() { let last = iter.peek().is_none(); let (branch, cont) = if last { ("└─ ", " ") } else { ("├─ ", "│ ") }; let (name, version) = split_name_version(strip_peer_suffix(&child.1)); let cyclic = on_path.contains(child); let suffix = if cyclic { colors::gray(" (cycle)").to_string() } else { String::new() }; println!( "{}{}{}:{} {}{}", prefix, branch, scheme(child.0), name, colors::gray(version), suffix, ); if !cyclic { on_path.push((*child).clone()); walk( child, graph, &format!("{}{}", prefix, cont), depth + 1, max_depth, on_path, ); on_path.pop(); } } } for root in roots { println!("{}", root.label()); if let Some(resolved) = &root.resolved { let name_version = format!("{}@{}", root.name, resolved); if let Some(id) = graph.root_id(root.kind, &name_version) { let mut on_path = vec![id.clone()]; walk(&id, graph, "", 0, max_depth, &mut on_path); } } } } // --------------------------------------------------------------------------- pub async fn list( flags: Arc<Flags>, list_flags: ListFlags, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags.clone()); let cli_options = factory.cli_options()?; let workspace = cli_options.workspace(); let http_client = factory.http_client_provider(); let deps_http_cache = factory.global_http_cache()?; let file_fetcher = create_cli_file_fetcher( factory.blob_store().clone(), GlobalOrLocalHttpCache::Global(deps_http_cache.clone()), http_client.clone(), factory.memory_files().clone(), factory.sys(), CreateCliFileFetcherOptions { allow_remote: true, cache_setting: CacheSetting::RespectHeaders, download_log_level: log::Level::Trace, progress_bar: None, }, ); let file_fetcher = Arc::new(file_fetcher); let npm_fetch_resolver = Arc::new(NpmFetchResolver::new( file_fetcher.clone(), factory.npmrc()?.clone(), factory.npm_version_resolver()?.clone(), )); let jsr_fetch_resolver = Arc::new(JsrFetchResolver::new( file_fetcher.clone(), factory.jsr_version_resolver()?.clone(), )); if !cli_options.start_dir.has_deno_or_pkg_json() { bail!( "No deno.json or package.json in \"{}\".", cli_options.initial_cwd().display(), ); } let args = DepManagerArgs { module_load_preparer: factory.module_load_preparer().await?.clone(), jsr_fetch_resolver, npm_fetch_resolver, npm_resolver: factory.npm_resolver().await?.clone(), npm_installer: factory.npm_installer().await?.clone(), npm_version_resolver: factory.npm_version_resolver()?.clone(), progress_bar: factory.text_only_progress_bar().clone(), permissions_container: factory.root_permissions_container()?.clone(), main_module_graph_container: factory .main_module_graph_container() .await? .clone(), lockfile: factory.maybe_lockfile().await?.cloned(), }; // Name filters (positional args, with wildcard support) are applied while // collecting declared deps, so we don't resolve packages we won't display. let filter_set = FilterSet::from_filter_strings( list_flags.filters.iter().map(|s| s.as_str()), )?; let filter_fn = |alias: Option<&str>, req: &PackageReq, _: DepKind| { if filter_set.is_empty() { return true; } filter_set.matches(alias.unwrap_or(&req.name)) }; let mut deps = if list_flags.recursive { DepManager::from_workspace(workspace, filter_fn, args)? } else { DepManager::from_workspace_dir(&cli_options.start_dir, filter_fn, args)? }; // Resolve concrete versions (and, when a tree is requested, populate the // lockfile graph). Best effort: if resolution fails we still list the // declared requirements. let resolved_ok = deps.resolve_current_versions().await.is_ok(); // Build the filtered set of declared root dependencies. let mut roots: Vec<Root> = Vec::new(); for (id, dep) in deps.deps_with_ids() { if list_flags.prod && dep.is_dev() { continue; } if list_flags.dev && !dep.is_dev() { continue; } let alias = (dep.alias_or_name() != dep.req.name) .then(|| dep.alias_or_name().to_string()); let resolved = if resolved_ok { deps.resolved_version(id).map(|nv| nv.version.to_string()) } else { None }; roots.push(Root { alias, kind: dep.kind, name: dep.req.name.to_string(), required: dep.req.version_req.to_string(), resolved, }); } roots.sort_by(|a, b| { (a.kind.scheme(), &a.name, &a.required).cmp(&( b.kind.scheme(), &b.name, &b.required, )) }); if roots.is_empty() { log::info!("No matching dependencies."); return Ok(()); } // Tree output requires the resolved dependency graph from the lockfile. if list_flags.depth >= 1 { let lockfile = factory.maybe_lockfile().await?.cloned(); let graph = match &lockfile { Some(lockfile) => { let guard = lockfile.lock(); DepGraph::from_lockfile_content(&guard.content.packages) } None => DepGraph::from_lockfile_content(&Default::default()), }; print_tree(&roots, &graph, list_flags.depth); return Ok(()); } print_flat_table(&roots); Ok(()) }