From edaceecec771cf0395639175b5a21d20530f6080 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 28 Oct 2022 16:19:55 -0400 Subject: [PATCH] feat: support npm specifiers in `deno info` for display text output only (#16470) --- cli/args/flags.rs | 2 + cli/fs_util.rs | 24 +- cli/npm/resolution.rs | 7 + cli/npm/resolvers/common.rs | 3 + cli/npm/resolvers/global.rs | 6 + cli/npm/resolvers/local.rs | 16 + cli/npm/resolvers/mod.rs | 15 +- cli/tests/integration/npm_tests.rs | 25 + cli/tests/testdata/cert/cafile_info.ts.out | 3 +- cli/tests/testdata/info/031_info_ts_error.out | 4 +- .../info/049_info_flag_script_jsx.out | 3 +- .../testdata/info/054_info_local_imports.out | 3 +- .../testdata/info/065_import_map_info.out | 4 +- .../info/data_null_error/data_null_error.out | 3 +- .../testdata/info/info_missing_module.out | 3 +- .../info/info_recursive_imports_test.out | 3 +- cli/tests/testdata/info/info_type_import.out | 4 +- cli/tests/testdata/info/multiple_imports.out | 3 +- cli/tests/testdata/info/types_header.out | 3 +- .../testdata/info/with_config/with_config.out | 3 +- .../testdata/npm/cjs_with_deps/main.info.out | 22 + cli/tests/testdata/npm/deno_info_chalk.out | 10 + .../testdata/run/017_import_redirect_info.out | 3 +- cli/tools/info.rs | 975 ++++++++---------- 24 files changed, 591 insertions(+), 556 deletions(-) create mode 100644 cli/tests/testdata/npm/cjs_with_deps/main.info.out create mode 100644 cli/tests/testdata/npm/deno_info_chalk.out diff --git a/cli/args/flags.rs b/cli/args/flags.rs index c313515e33..90899704e6 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -1212,6 +1212,7 @@ TypeScript compiler cache: Subdirectory containing TS compiler output.", .arg(no_config_arg()) .arg(config_arg()) .arg(import_map_arg()) + .arg(local_npm_arg()) .arg( Arg::new("json") .long("json") @@ -2512,6 +2513,7 @@ fn info_parse(flags: &mut Flags, matches: &clap::ArgMatches) { import_map_arg_parse(flags, matches); location_arg_parse(flags, matches); ca_file_arg_parse(flags, matches); + local_npm_args_parse(flags, matches); let json = matches.is_present("json"); flags.subcommand = DenoSubcommand::Info(InfoFlags { file: matches.value_of("file").map(|f| f.to_string()), diff --git a/cli/fs_util.rs b/cli/fs_util.rs index 365c9b4301..fa1535469d 100644 --- a/cli/fs_util.rs +++ b/cli/fs_util.rs @@ -1,7 +1,8 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use deno_core::anyhow::Context; -use deno_core::error::{uri_error, AnyError}; +use deno_core::error::uri_error; +use deno_core::error::AnyError; pub use deno_core::normalize_path; use deno_core::ModuleSpecifier; use deno_runtime::deno_crypto::rand; @@ -9,8 +10,11 @@ use deno_runtime::deno_node::PathClean; use std::borrow::Cow; use std::env::current_dir; use std::fs::OpenOptions; -use std::io::{Error, ErrorKind, Write}; -use std::path::{Path, PathBuf}; +use std::io::Error; +use std::io::ErrorKind; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; use walkdir::WalkDir; pub fn atomic_write_file>( @@ -573,6 +577,20 @@ pub fn root_url_to_safe_local_dirname(root: &ModuleSpecifier) -> PathBuf { result } +/// Gets the total size (in bytes) of a directory. +pub fn dir_size(path: &Path) -> std::io::Result { + let entries = std::fs::read_dir(path)?; + let mut total = 0; + for entry in entries { + let entry = entry?; + total += match entry.metadata()? { + data if data.is_dir() => dir_size(&entry.path())?, + data => data.len(), + }; + } + Ok(total) +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/npm/resolution.rs b/cli/npm/resolution.rs index 7cd4df124c..28a42bc333 100644 --- a/cli/npm/resolution.rs +++ b/cli/npm/resolution.rs @@ -697,6 +697,13 @@ impl NpmResolution { Ok(snapshot) } + pub fn resolve_package_from_id( + &self, + id: &NpmPackageId, + ) -> Option { + self.snapshot.read().package_from_id(id).cloned() + } + pub fn resolve_package_from_package( &self, name: &str, diff --git a/cli/npm/resolvers/common.rs b/cli/npm/resolvers/common.rs index e114f3f8a6..07996c4e10 100644 --- a/cli/npm/resolvers/common.rs +++ b/cli/npm/resolvers/common.rs @@ -15,6 +15,7 @@ use crate::lockfile::Lockfile; use crate::npm::cache::should_sync_download; use crate::npm::resolution::NpmResolutionSnapshot; use crate::npm::NpmCache; +use crate::npm::NpmPackageId; use crate::npm::NpmPackageReq; use crate::npm::NpmResolutionPackage; @@ -36,6 +37,8 @@ pub trait InnerNpmPackageResolver: Send + Sync { specifier: &ModuleSpecifier, ) -> Result; + fn package_size(&self, package_id: &NpmPackageId) -> Result; + fn has_packages(&self) -> bool; fn add_package_reqs( diff --git a/cli/npm/resolvers/global.rs b/cli/npm/resolvers/global.rs index 996f55c2d6..42090415ae 100644 --- a/cli/npm/resolvers/global.rs +++ b/cli/npm/resolvers/global.rs @@ -15,6 +15,7 @@ use deno_core::url::Url; use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_node::TYPES_CONDITIONS; +use crate::fs_util; use crate::lockfile::Lockfile; use crate::npm::resolution::NpmResolution; use crate::npm::resolution::NpmResolutionSnapshot; @@ -110,6 +111,11 @@ impl InnerNpmPackageResolver for GlobalNpmPackageResolver { Ok(self.package_folder(&pkg_id)) } + fn package_size(&self, package_id: &NpmPackageId) -> Result { + let package_folder = self.package_folder(package_id); + Ok(fs_util::dir_size(&package_folder)?) + } + fn has_packages(&self) -> bool { self.resolution.has_packages() } diff --git a/cli/npm/resolvers/local.rs b/cli/npm/resolvers/local.rs index 6c4c4ef6c2..cad940d563 100644 --- a/cli/npm/resolvers/local.rs +++ b/cli/npm/resolvers/local.rs @@ -177,6 +177,22 @@ impl InnerNpmPackageResolver for LocalNpmPackageResolver { Ok(package_root_path) } + fn package_size(&self, package_id: &NpmPackageId) -> Result { + match self.resolution.resolve_package_from_id(package_id) { + Some(package) => Ok(fs_util::dir_size( + // package is stored at: + // node_modules/.deno//node_modules/ + &self + .root_node_modules_path + .join(".deno") + .join(package.id.to_string()) + .join("node_modules") + .join(package.id.name), + )?), + None => bail!("Could not find package folder for '{}'", package_id), + } + } + fn has_packages(&self) -> bool { self.resolution.has_packages() } diff --git a/cli/npm/resolvers/mod.rs b/cli/npm/resolvers/mod.rs index 3d55170ac2..71c2abc00d 100644 --- a/cli/npm/resolvers/mod.rs +++ b/cli/npm/resolvers/mod.rs @@ -27,6 +27,7 @@ use crate::lockfile::Lockfile; use self::common::InnerNpmPackageResolver; use self::local::LocalNpmPackageResolver; use super::NpmCache; +use super::NpmPackageId; use super::NpmPackageReq; use super::NpmRegistryApi; use super::NpmResolutionSnapshot; @@ -212,6 +213,14 @@ impl NpmPackageResolver { Ok(path) } + /// Attempts to get the package size in bytes. + pub fn package_size( + &self, + package_id: &NpmPackageId, + ) -> Result { + self.inner.package_size(package_id) + } + /// Gets if the provided specifier is in an npm package. pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { self @@ -301,10 +310,14 @@ impl NpmPackageResolver { self.unstable, self.no_npm, self.local_node_modules_path.clone(), - Some(self.inner.snapshot()), + Some(self.snapshot()), ) } + pub fn snapshot(&self) -> NpmResolutionSnapshot { + self.inner.snapshot() + } + pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { self.inner.lock(lockfile) } diff --git a/cli/tests/integration/npm_tests.rs b/cli/tests/integration/npm_tests.rs index 9e3ef56782..0ca1ae910e 100644 --- a/cli/tests/integration/npm_tests.rs +++ b/cli/tests/integration/npm_tests.rs @@ -733,6 +733,31 @@ itest!(compile_errors { http_server: true, }); +itest!(info_chalk { + args: "info --quiet --unstable npm/cjs_with_deps/main.js", + output: "npm/cjs_with_deps/main.info.out", + exit_code: 0, + envs: env_vars(), + http_server: true, +}); + +itest!(info_chalk_node_modules_dir { + args: "info --quiet --unstable --node-modules-dir $TESTDATA/npm/cjs_with_deps/main.js", + output: "npm/cjs_with_deps/main.info.out", + exit_code: 0, + envs: env_vars(), + http_server: true, + temp_cwd: true, +}); + +itest!(info_cli_chalk { + args: "info --quiet --unstable npm:chalk@4", + output: "npm/deno_info_chalk.out", + exit_code: 0, + envs: env_vars(), + http_server: true, +}); + fn env_vars_no_sync_download() -> Vec<(String, String)> { vec![ ("DENO_NODE_COMPAT_URL".to_string(), util::std_file_url()), diff --git a/cli/tests/testdata/cert/cafile_info.ts.out b/cli/tests/testdata/cert/cafile_info.ts.out index 4c7e6c6c7c..ddece30198 100644 --- a/cli/tests/testdata/cert/cafile_info.ts.out +++ b/cli/tests/testdata/cert/cafile_info.ts.out @@ -1,6 +1,7 @@ local: [WILDCARD]https[WILDCARD]localhost_PORT5545[WILDCARD] type: TypeScript -dependencies: 8 unique (total [WILDCARD]) +dependencies: 8 unique +size: [WILDCARD] https://localhost:5545/cert/cafile_info.ts ([WILDCARD]) ├── https://localhost:5545/subdir/mt_application_ecmascript.j2.js ([WILDCARD]) diff --git a/cli/tests/testdata/info/031_info_ts_error.out b/cli/tests/testdata/info/031_info_ts_error.out index 2dc7bb4e84..49df500123 100644 --- a/cli/tests/testdata/info/031_info_ts_error.out +++ b/cli/tests/testdata/info/031_info_ts_error.out @@ -1,5 +1,7 @@ [WILDCARD] local: [WILDCARD]031_info_ts_error.ts type: TypeScript -dependencies: 0 unique (total [WILDCARD]) +dependencies: 0 unique +size: [WILDCARD] + [WILDCARD]031_info_ts_error.ts ([WILDCARD]) diff --git a/cli/tests/testdata/info/049_info_flag_script_jsx.out b/cli/tests/testdata/info/049_info_flag_script_jsx.out index 16736c8933..244541696d 100644 --- a/cli/tests/testdata/info/049_info_flag_script_jsx.out +++ b/cli/tests/testdata/info/049_info_flag_script_jsx.out @@ -1,7 +1,8 @@ [WILDCARD] local: [WILDCARD]http[WILDCARD]127.0.0.1_PORT4545[WILDCARD] type: TypeScript -dependencies: 8 unique (total [WILDCARD]) +dependencies: 8 unique +size: [WILDCARD] http://127.0.0.1:4545/run/048_media_types_jsx.ts ([WILDCARD]) ├── http://localhost:4545/subdir/mt_application_ecmascript_jsx.j2.jsx ([WILDCARD]) diff --git a/cli/tests/testdata/info/054_info_local_imports.out b/cli/tests/testdata/info/054_info_local_imports.out index cde5ff3aba..ee1773b76b 100644 --- a/cli/tests/testdata/info/054_info_local_imports.out +++ b/cli/tests/testdata/info/054_info_local_imports.out @@ -1,6 +1,7 @@ local: [WILDCARD]005_more_imports.ts type: TypeScript -dependencies: 3 unique (total [WILDCARD]) +dependencies: 3 unique +size: [WILDCARD] file://[WILDCARD]/005_more_imports.ts ([WILDCARD]) └─┬ file://[WILDCARD]/subdir/mod1.ts ([WILDCARD]) diff --git a/cli/tests/testdata/info/065_import_map_info.out b/cli/tests/testdata/info/065_import_map_info.out index 8771d05941..657510b897 100644 --- a/cli/tests/testdata/info/065_import_map_info.out +++ b/cli/tests/testdata/info/065_import_map_info.out @@ -1,5 +1,7 @@ [WILDCARD] local: [WILDCARD]test.ts type: TypeScript -dependencies: 7 unique (total [WILDCARD]) +dependencies: 7 unique +size: [WILDCARD] + [WILDCARD] diff --git a/cli/tests/testdata/info/data_null_error/data_null_error.out b/cli/tests/testdata/info/data_null_error/data_null_error.out index 89961be656..065396f189 100644 --- a/cli/tests/testdata/info/data_null_error/data_null_error.out +++ b/cli/tests/testdata/info/data_null_error/data_null_error.out @@ -1,6 +1,7 @@ local: [WILDCARD]mod.ts type: TypeScript -dependencies: 1 unique (total [WILDCARD]) +dependencies: 1 unique +size: [WILDCARD] file://[WILDCARD]/mod.ts ([WILDCARD]) └── file://[WILDCARD]/types.d.ts ([WILDCARD]) diff --git a/cli/tests/testdata/info/info_missing_module.out b/cli/tests/testdata/info/info_missing_module.out index 07f893eecc..c62d690c18 100644 --- a/cli/tests/testdata/info/info_missing_module.out +++ b/cli/tests/testdata/info/info_missing_module.out @@ -1,6 +1,7 @@ local: [WILDCARD]error_009_missing_js_module.js type: JavaScript -dependencies: 0 unique (total 26B) +dependencies: 0 unique +size: 26B file://[WILDCARD]/error_009_missing_js_module.js (26B) └── file://[WILDCARD]/bad-module.js (missing) diff --git a/cli/tests/testdata/info/info_recursive_imports_test.out b/cli/tests/testdata/info/info_recursive_imports_test.out index 10ee545340..3340f38590 100644 --- a/cli/tests/testdata/info/info_recursive_imports_test.out +++ b/cli/tests/testdata/info/info_recursive_imports_test.out @@ -1,6 +1,7 @@ local: [WILDCARD]info_recursive_imports_test.ts type: TypeScript -dependencies: 4 unique (total [WILDCARD]) +dependencies: 4 unique +size: [WILDCARD] file://[WILDCARD]/info_recursive_imports_test.ts ([WILDCARD]) └─┬ file://[WILDCARD]/recursive_imports/A.ts ([WILDCARD]) diff --git a/cli/tests/testdata/info/info_type_import.out b/cli/tests/testdata/info/info_type_import.out index 6b9869f1f4..0423efe822 100644 --- a/cli/tests/testdata/info/info_type_import.out +++ b/cli/tests/testdata/info/info_type_import.out @@ -1,5 +1,7 @@ local: [WILDCARD]info_type_import.ts type: TypeScript -dependencies: 1 unique (total [WILDCARD]) +dependencies: 1 unique +size: [WILDCARD] + [WILDCARD]info_type_import.ts ([WILDCARD]) └── [WILDCARD]type_and_code.ts ([WILDCARD]) diff --git a/cli/tests/testdata/info/multiple_imports.out b/cli/tests/testdata/info/multiple_imports.out index d4e3153970..ea35e69c8d 100644 --- a/cli/tests/testdata/info/multiple_imports.out +++ b/cli/tests/testdata/info/multiple_imports.out @@ -1,7 +1,8 @@ [WILDCARD] local: [WILDCARD]http[WILDCARD]127.0.0.1_PORT4545[WILDCARD] type: TypeScript -dependencies: 8 unique (total [WILDCARD]) +dependencies: 8 unique +size: [WILDCARD] http://127.0.0.1:4545/run/019_media_types.ts ([WILDCARD]) ├── http://localhost:4545/subdir/mt_application_ecmascript.j2.js ([WILDCARD]) diff --git a/cli/tests/testdata/info/types_header.out b/cli/tests/testdata/info/types_header.out index d3b6e8c4e7..722e02f775 100644 --- a/cli/tests/testdata/info/types_header.out +++ b/cli/tests/testdata/info/types_header.out @@ -1,7 +1,8 @@ [WILDCARD] local: [WILDCARD]type_directives_01.ts type: TypeScript -dependencies: 2 unique (total [WILDCARD]) +dependencies: 2 unique +size: [WILDCARD] [WILDCARD]/type_directives_01.ts ([WILDCARD]) └─┬ http://127.0.0.1:4545/xTypeScriptTypes.js ([WILDCARD]) diff --git a/cli/tests/testdata/info/with_config/with_config.out b/cli/tests/testdata/info/with_config/with_config.out index b707c24d0e..95a1f30bce 100644 --- a/cli/tests/testdata/info/with_config/with_config.out +++ b/cli/tests/testdata/info/with_config/with_config.out @@ -1,6 +1,7 @@ Warning the configuration file "[WILDCARD]/deno-override.json" contains an entry for "importMap" that is being ignored. local: [WILDCARD]test.ts type: TypeScript -dependencies: 0 unique (total [WILDCARD]) +dependencies: 0 unique +size: [WILDCARD] file:///[WILDCARD]/test.ts ([WILDCARD]) diff --git a/cli/tests/testdata/npm/cjs_with_deps/main.info.out b/cli/tests/testdata/npm/cjs_with_deps/main.info.out new file mode 100644 index 0000000000..345583a902 --- /dev/null +++ b/cli/tests/testdata/npm/cjs_with_deps/main.info.out @@ -0,0 +1,22 @@ +local: [WILDCARD]main.js +type: JavaScript +dependencies: 14 unique +size: [WILDCARD] + +file:///[WILDCARD]/npm/cjs_with_deps/main.js ([WILDCARD]) +├─┬ npm:chai@4.3 - 4.3.6 ([WILDCARD]) +│ ├── npm:assertion-error@1.1.0 ([WILDCARD]) +│ ├── npm:check-error@1.0.2 ([WILDCARD]) +│ ├─┬ npm:deep-eql@3.0.1 ([WILDCARD]) +│ │ └── npm:type-detect@4.0.8 ([WILDCARD]) +│ ├── npm:get-func-name@2.0.0 ([WILDCARD]) +│ ├─┬ npm:loupe@2.3.4 ([WILDCARD]) +│ │ └── npm:get-func-name@2.0.0 ([WILDCARD]) +│ ├── npm:pathval@1.1.1 ([WILDCARD]) +│ └── npm:type-detect@4.0.8 ([WILDCARD]) +└─┬ npm:chalk@4 - 4.1.2 ([WILDCARD]) + ├─┬ npm:ansi-styles@4.3.0 ([WILDCARD]) + │ └─┬ npm:color-convert@2.0.1 ([WILDCARD]) + │ └── npm:color-name@1.1.4 ([WILDCARD]) + └─┬ npm:supports-color@7.2.0 ([WILDCARD]) + └── npm:has-flag@4.0.0 ([WILDCARD]) diff --git a/cli/tests/testdata/npm/deno_info_chalk.out b/cli/tests/testdata/npm/deno_info_chalk.out new file mode 100644 index 0000000000..89ea05e713 --- /dev/null +++ b/cli/tests/testdata/npm/deno_info_chalk.out @@ -0,0 +1,10 @@ +type: Unknown +dependencies: 5 unique +size: [WILDCARD] + +npm:chalk@4 - 4.1.2 ([WILDCARD]) +├─┬ npm:ansi-styles@4.3.0 ([WILDCARD]) +│ └─┬ npm:color-convert@2.0.1 ([WILDCARD]) +│ └── npm:color-name@1.1.4 ([WILDCARD]) +└─┬ npm:supports-color@7.2.0 ([WILDCARD]) + └── npm:has-flag@4.0.0 ([WILDCARD]) diff --git a/cli/tests/testdata/run/017_import_redirect_info.out b/cli/tests/testdata/run/017_import_redirect_info.out index d3a2e86fc7..d1850ccb5e 100644 --- a/cli/tests/testdata/run/017_import_redirect_info.out +++ b/cli/tests/testdata/run/017_import_redirect_info.out @@ -1,6 +1,7 @@ local: [WILDCARD]017_import_redirect.ts type: TypeScript -dependencies: 1 unique (total 278B) +dependencies: 1 unique +size: 278B file:///[WILDCARD]/017_import_redirect.ts ([WILDCARD]) └── https://gist.githubusercontent.com/ry/f12b2aa3409e6b52645bc346a9e22929/raw/79318f239f51d764384a8bded8d7c6a833610dde/print_hello.ts ([WILDCARD]) diff --git a/cli/tools/info.rs b/cli/tools/info.rs index b5cae32ee0..e45958b34b 100644 --- a/cli/tools/info.rs +++ b/cli/tools/info.rs @@ -1,5 +1,6 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use std::collections::HashMap; use std::collections::HashSet; use std::fmt; use std::fmt::Write; @@ -22,6 +23,12 @@ use crate::args::InfoFlags; use crate::checksum; use crate::display; use crate::lsp; +use crate::npm::NpmPackageId; +use crate::npm::NpmPackageReference; +use crate::npm::NpmPackageReq; +use crate::npm::NpmPackageResolver; +use crate::npm::NpmResolutionPackage; +use crate::npm::NpmResolutionSnapshot; use crate::proc_state::ProcState; pub async fn info(flags: Flags, info_flags: InfoFlags) -> Result<(), AnyError> { @@ -34,7 +41,7 @@ pub async fn info(flags: Flags, info_flags: InfoFlags) -> Result<(), AnyError> { display::write_json_to_stdout(&json!(graph))?; } else { let mut output = String::new(); - fmt_module_graph(&graph, &mut output)?; + GraphDisplayContext::write(&graph, &ps.npm_resolver, &mut output)?; display::write_to_stdout_ignore_sigpipe(output.as_bytes())?; } } else { @@ -121,564 +128,454 @@ fn print_cache_info( } } -const SIBLING_CONNECTOR: char = '├'; -const LAST_SIBLING_CONNECTOR: char = '└'; -const CHILD_DEPS_CONNECTOR: char = '┬'; -const CHILD_NO_DEPS_CONNECTOR: char = '─'; -const VERTICAL_CONNECTOR: char = '│'; -const EMPTY_CONNECTOR: char = ' '; +struct TreeNode { + text: String, + children: Vec, +} -fn fmt_module_graph(graph: &ModuleGraph, f: &mut impl Write) -> fmt::Result { - if graph.roots.is_empty() || graph.roots.len() > 1 { - return writeln!( - f, - "{} displaying graphs that have multiple roots is not supported.", - colors::red("error:") - ); - } - let root_specifier = graph.resolve(&graph.roots[0].0); - match graph.try_get(&root_specifier) { - Ok(Some(root)) => { - if let Some(cache_info) = root.maybe_cache_info.as_ref() { - if let Some(local) = &cache_info.local { - writeln!( - f, - "{} {}", - colors::bold("local:"), - local.to_string_lossy() - )?; - } - if let Some(emit) = &cache_info.emit { - writeln!(f, "{} {}", colors::bold("emit:"), emit.to_string_lossy())?; - } - if let Some(map) = &cache_info.map { - writeln!(f, "{} {}", colors::bold("map:"), map.to_string_lossy())?; - } - } - writeln!(f, "{} {}", colors::bold("type:"), root.media_type)?; - let modules = graph.modules(); - let total_size: f64 = modules.iter().map(|m| m.size() as f64).sum(); - let dep_count = modules.len() - 1; - writeln!( - f, - "{} {} unique {}", - colors::bold("dependencies:"), - dep_count, - colors::gray(format!("(total {})", display::human_size(total_size))) - )?; - writeln!( - f, - "\n{} {}", - root_specifier, - colors::gray(format!("({})", display::human_size(root.size() as f64))) - )?; - let mut seen = HashSet::new(); - let dep_len = root.dependencies.len(); - for (idx, (_, dep)) in root.dependencies.iter().enumerate() { - fmt_dep_info( - dep, - f, - "", - idx == dep_len - 1 && root.maybe_types_dependency.is_none(), - graph, - &mut seen, - )?; - } - Ok(()) - } - Err(ModuleGraphError::Missing(_)) => { - writeln!(f, "{} module could not be found", colors::red("error:")) - } - Err(err) => { - writeln!(f, "{} {}", colors::red("error:"), err) - } - Ok(None) => { - writeln!(f, "{} an internal error occurred", colors::red("error:")) +impl TreeNode { + pub fn from_text(text: String) -> Self { + Self { + text, + children: Default::default(), } } } -fn fmt_dep_info + fmt::Display + Clone>( - dep: &Dependency, - f: &mut impl Write, - prefix: S, - last: bool, - graph: &ModuleGraph, - seen: &mut HashSet, +fn print_tree_node( + tree_node: &TreeNode, + writer: &mut TWrite, ) -> fmt::Result { - if !dep.maybe_code.is_none() { - fmt_resolved_info( - &dep.maybe_code, - f, - prefix.clone(), - dep.maybe_type.is_none() && last, + fn print_children( + writer: &mut TWrite, + prefix: &str, + children: &Vec, + ) -> fmt::Result { + const SIBLING_CONNECTOR: char = '├'; + const LAST_SIBLING_CONNECTOR: char = '└'; + const CHILD_DEPS_CONNECTOR: char = '┬'; + const CHILD_NO_DEPS_CONNECTOR: char = '─'; + const VERTICAL_CONNECTOR: char = '│'; + const EMPTY_CONNECTOR: char = ' '; + + let child_len = children.len(); + for (index, child) in children.iter().enumerate() { + let is_last = index + 1 == child_len; + let sibling_connector = if is_last { + LAST_SIBLING_CONNECTOR + } else { + SIBLING_CONNECTOR + }; + let child_connector = if child.children.is_empty() { + CHILD_NO_DEPS_CONNECTOR + } else { + CHILD_DEPS_CONNECTOR + }; + writeln!( + writer, + "{} {}", + colors::gray(format!( + "{}{}─{}", + prefix, sibling_connector, child_connector + )), + child.text + )?; + let child_prefix = format!( + "{}{}{}", + prefix, + if is_last { + EMPTY_CONNECTOR + } else { + VERTICAL_CONNECTOR + }, + EMPTY_CONNECTOR + ); + print_children(writer, &child_prefix, &child.children)?; + } + + Ok(()) + } + + writeln!(writer, "{}", tree_node.text)?; + print_children(writer, "", &tree_node.children)?; + Ok(()) +} + +/// Precached information about npm packages that are used in deno info. +#[derive(Default)] +struct NpmInfo { + package_sizes: HashMap, + resolved_reqs: HashMap, + packages: HashMap, + specifiers: HashMap, +} + +impl NpmInfo { + pub fn build<'a>( + graph: &'a ModuleGraph, + npm_resolver: &'a NpmPackageResolver, + npm_snapshot: &'a NpmResolutionSnapshot, + ) -> Self { + let mut info = NpmInfo::default(); + if !npm_resolver.has_packages() { + return info; // skip going over the specifiers if there's no npm packages + } + + for (specifier, _) in graph.specifiers() { + if let Ok(reference) = NpmPackageReference::from_specifier(&specifier) { + info + .specifiers + .insert(specifier.clone(), reference.req.clone()); + if let Ok(package) = + npm_snapshot.resolve_package_from_deno_module(&reference.req) + { + info.resolved_reqs.insert(reference.req, package.id.clone()); + if !info.packages.contains_key(&package.id) { + info.fill_package_info(package, npm_resolver, npm_snapshot); + } + } + } + } + + info + } + + fn fill_package_info<'a>( + &mut self, + package: &NpmResolutionPackage, + npm_resolver: &'a NpmPackageResolver, + npm_snapshot: &'a NpmResolutionSnapshot, + ) { + self.packages.insert(package.id.clone(), package.clone()); + if let Ok(size) = npm_resolver.package_size(&package.id) { + self.package_sizes.insert(package.id.clone(), size); + } + for id in package.dependencies.values() { + if !self.packages.contains_key(id) { + if let Some(package) = npm_snapshot.package_from_id(id) { + self.fill_package_info(package, npm_resolver, npm_snapshot); + } + } + } + } + + pub fn package_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Option<&NpmResolutionPackage> { + self + .specifiers + .get(specifier) + .and_then(|package_req| self.resolved_reqs.get(package_req)) + .and_then(|id| self.packages.get(id)) + } +} + +struct GraphDisplayContext<'a> { + graph: &'a ModuleGraph, + npm_info: NpmInfo, + seen: HashSet, +} + +impl<'a> GraphDisplayContext<'a> { + pub fn write( + graph: &'a ModuleGraph, + npm_resolver: &'a NpmPackageResolver, + writer: &mut TWrite, + ) -> fmt::Result { + let npm_snapshot = npm_resolver.snapshot(); + let npm_info = NpmInfo::build(graph, npm_resolver, &npm_snapshot); + Self { graph, - false, - seen, - )?; + npm_info, + seen: Default::default(), + } + .into_writer(writer) } - if !dep.maybe_type.is_none() { - fmt_resolved_info(&dep.maybe_type, f, prefix, last, graph, true, seen)?; - } - Ok(()) -} -fn fmt_module_info + fmt::Display + Clone>( - module: &Module, - f: &mut impl Write, - prefix: S, - last: bool, - graph: &ModuleGraph, - type_dep: bool, - seen: &mut HashSet, -) -> fmt::Result { - let was_seen = seen.contains(&module.specifier); - let children = !((module.dependencies.is_empty() - && module.maybe_types_dependency.is_none()) - || was_seen); - let (specifier_str, size_str) = if was_seen { - let specifier_str = if type_dep { - colors::italic_gray(&module.specifier).to_string() - } else { - colors::gray(&module.specifier).to_string() - }; - (specifier_str, colors::gray(" *").to_string()) - } else { - let specifier_str = if type_dep { - colors::italic(&module.specifier).to_string() - } else { - module.specifier.to_string() - }; - let size_str = - colors::gray(format!(" ({})", display::human_size(module.size() as f64))) - .to_string(); - (specifier_str, size_str) - }; + fn into_writer(mut self, writer: &mut TWrite) -> fmt::Result { + if self.graph.roots.is_empty() || self.graph.roots.len() > 1 { + return writeln!( + writer, + "{} displaying graphs that have multiple roots is not supported.", + colors::red("error:") + ); + } - seen.insert(module.specifier.clone()); - - fmt_info_msg( - f, - prefix.clone(), - last, - children, - format!("{}{}", specifier_str, size_str), - )?; - - if !was_seen { - let mut prefix = prefix.to_string(); - if last { - prefix.push(EMPTY_CONNECTOR); - } else { - prefix.push(VERTICAL_CONNECTOR); - } - prefix.push(EMPTY_CONNECTOR); - let dep_len = module.dependencies.len(); - if let Some((_, type_dep)) = &module.maybe_types_dependency { - fmt_resolved_info(type_dep, f, &prefix, dep_len == 0, graph, true, seen)?; - } - for (idx, (_, dep)) in module.dependencies.iter().enumerate() { - fmt_dep_info( - dep, - f, - &prefix, - idx == dep_len - 1 && module.maybe_types_dependency.is_none(), - graph, - seen, - )?; - } - } - Ok(()) -} - -fn fmt_error_info + fmt::Display + Clone>( - err: &ModuleGraphError, - f: &mut impl Write, - prefix: S, - last: bool, - specifier: &ModuleSpecifier, - seen: &mut HashSet, -) -> fmt::Result { - seen.insert(specifier.clone()); - match err { - ModuleGraphError::InvalidSource(_, _) => { - fmt_error_msg(f, prefix, last, specifier, "(invalid source)") - } - ModuleGraphError::InvalidTypeAssertion { .. } => { - fmt_error_msg(f, prefix, last, specifier, "(invalid import assertion)") - } - ModuleGraphError::LoadingErr(_, _) => { - fmt_error_msg(f, prefix, last, specifier, "(loading error)") - } - ModuleGraphError::ParseErr(_, _) => { - fmt_error_msg(f, prefix, last, specifier, "(parsing error)") - } - ModuleGraphError::ResolutionError(_) => { - fmt_error_msg(f, prefix, last, specifier, "(resolution error)") - } - ModuleGraphError::UnsupportedImportAssertionType(_, _) => fmt_error_msg( - f, - prefix, - last, - specifier, - "(unsupported import assertion)", - ), - ModuleGraphError::UnsupportedMediaType(_, _) => { - fmt_error_msg(f, prefix, last, specifier, "(unsupported)") - } - ModuleGraphError::Missing(_) => { - fmt_error_msg(f, prefix, last, specifier, "(missing)") - } - } -} - -fn fmt_info_msg( - f: &mut impl Write, - prefix: S, - last: bool, - children: bool, - msg: M, -) -> fmt::Result -where - S: AsRef + fmt::Display + Clone, - M: AsRef + fmt::Display, -{ - let sibling_connector = if last { - LAST_SIBLING_CONNECTOR - } else { - SIBLING_CONNECTOR - }; - let child_connector = if children { - CHILD_DEPS_CONNECTOR - } else { - CHILD_NO_DEPS_CONNECTOR - }; - writeln!( - f, - "{} {}", - colors::gray(format!( - "{}{}─{}", - prefix, sibling_connector, child_connector - )), - msg - ) -} - -fn fmt_error_msg( - f: &mut impl Write, - prefix: S, - last: bool, - specifier: &ModuleSpecifier, - error_msg: M, -) -> fmt::Result -where - S: AsRef + fmt::Display + Clone, - M: AsRef + fmt::Display, -{ - fmt_info_msg( - f, - prefix, - last, - false, - format!("{} {}", colors::red(specifier), colors::red_bold(error_msg)), - ) -} - -fn fmt_resolved_info + fmt::Display + Clone>( - resolved: &Resolved, - f: &mut impl Write, - prefix: S, - last: bool, - graph: &ModuleGraph, - type_dep: bool, - seen: &mut HashSet, -) -> fmt::Result { - match resolved { - Resolved::Ok { specifier, .. } => { - let resolved_specifier = graph.resolve(specifier); - match graph.try_get(&resolved_specifier) { - Ok(Some(module)) => { - fmt_module_info(module, f, prefix, last, graph, type_dep, seen) + let root_specifier = self.graph.resolve(&self.graph.roots[0].0); + match self.graph.try_get(&root_specifier) { + Ok(Some(root)) => { + if let Some(cache_info) = root.maybe_cache_info.as_ref() { + if let Some(local) = &cache_info.local { + writeln!( + writer, + "{} {}", + colors::bold("local:"), + local.to_string_lossy() + )?; + } + if let Some(emit) = &cache_info.emit { + writeln!( + writer, + "{} {}", + colors::bold("emit:"), + emit.to_string_lossy() + )?; + } + if let Some(map) = &cache_info.map { + writeln!( + writer, + "{} {}", + colors::bold("map:"), + map.to_string_lossy() + )?; + } } - Err(err) => { - fmt_error_info(&err, f, prefix, last, &resolved_specifier, seen) + writeln!(writer, "{} {}", colors::bold("type:"), root.media_type)?; + let modules = self.graph.modules(); + let total_modules_size = + modules.iter().map(|m| m.size() as f64).sum::(); + let total_npm_package_size = self + .npm_info + .package_sizes + .values() + .map(|s| *s as f64) + .sum::(); + let total_size = total_modules_size + total_npm_package_size; + let dep_count = modules.len() - 1 + self.npm_info.packages.len() + - self.npm_info.resolved_reqs.len(); + writeln!( + writer, + "{} {} unique", + colors::bold("dependencies:"), + dep_count, + )?; + writeln!( + writer, + "{} {}", + colors::bold("size:"), + display::human_size(total_size), + )?; + writeln!(writer)?; + let root_node = self.build_module_info(root, false); + print_tree_node(&root_node, writer)?; + Ok(()) + } + Err(ModuleGraphError::Missing(_)) => { + writeln!( + writer, + "{} module could not be found", + colors::red("error:") + ) + } + Err(err) => { + writeln!(writer, "{} {}", colors::red("error:"), err) + } + Ok(None) => { + writeln!( + writer, + "{} an internal error occurred", + colors::red("error:") + ) + } + } + } + + fn build_dep_info(&mut self, dep: &Dependency) -> Vec { + let mut children = Vec::with_capacity(2); + if !dep.maybe_code.is_none() { + if let Some(child) = self.build_resolved_info(&dep.maybe_code, false) { + children.push(child); + } + } + if !dep.maybe_type.is_none() { + if let Some(child) = self.build_resolved_info(&dep.maybe_type, true) { + children.push(child); + } + } + children + } + + fn build_module_info(&mut self, module: &Module, type_dep: bool) -> TreeNode { + enum PackageOrSpecifier { + Package(NpmResolutionPackage), + Specifier(ModuleSpecifier), + } + + use PackageOrSpecifier::*; + + let package_or_specifier = + match self.npm_info.package_from_specifier(&module.specifier) { + Some(package) => Package(package.clone()), + None => Specifier(module.specifier.clone()), + }; + let was_seen = !self.seen.insert(match &package_or_specifier { + Package(package) => package.id.to_string(), + Specifier(specifier) => specifier.to_string(), + }); + let header_text = if was_seen { + let specifier_str = if type_dep { + colors::italic_gray(&module.specifier).to_string() + } else { + colors::gray(&module.specifier).to_string() + }; + format!("{} {}", specifier_str, colors::gray("*")) + } else { + let specifier_str = if type_dep { + colors::italic(&module.specifier).to_string() + } else { + module.specifier.to_string() + }; + let header_text = match &package_or_specifier { + Package(package) => { + format!("{} - {}", specifier_str, package.id.version) } - Ok(None) => fmt_info_msg( - f, - prefix, - last, - false, - format!( + Specifier(_) => specifier_str, + }; + let maybe_size = match &package_or_specifier { + Package(package) => self + .npm_info + .package_sizes + .get(&package.id) + .map(|s| *s as u64), + Specifier(_) => module + .maybe_source + .as_ref() + .map(|s| s.as_bytes().len() as u64), + }; + format!("{} {}", header_text, maybe_size_to_text(maybe_size)) + }; + + let mut tree_node = TreeNode::from_text(header_text); + + if !was_seen { + if let Some((_, type_dep)) = &module.maybe_types_dependency { + if let Some(child) = self.build_resolved_info(type_dep, true) { + tree_node.children.push(child); + } + } + match &package_or_specifier { + Package(package) => { + tree_node.children.extend(self.build_npm_deps(package)); + } + Specifier(_) => { + for dep in module.dependencies.values() { + tree_node.children.extend(self.build_dep_info(dep)); + } + } + } + } + tree_node + } + + fn build_npm_deps( + &mut self, + package: &NpmResolutionPackage, + ) -> Vec { + let mut deps = package.dependencies.values().collect::>(); + deps.sort(); + let mut children = Vec::with_capacity(deps.len()); + for dep_id in deps.into_iter() { + let maybe_size = self.npm_info.package_sizes.get(dep_id).cloned(); + let size_str = maybe_size_to_text(maybe_size); + let mut child = + TreeNode::from_text(format!("npm:{} {}", dep_id, size_str)); + if let Some(package) = self.npm_info.packages.get(dep_id) { + if !package.dependencies.is_empty() { + if self.seen.contains(&package.id.to_string()) { + child.text = format!("{} {}", child.text, colors::gray("*")); + } else { + let package = package.clone(); + child.children.extend(self.build_npm_deps(&package)); + } + } + } + children.push(child); + } + children + } + + fn build_error_info( + &mut self, + err: &ModuleGraphError, + specifier: &ModuleSpecifier, + ) -> TreeNode { + self.seen.insert(specifier.to_string()); + match err { + ModuleGraphError::InvalidSource(_, _) => { + self.build_error_msg(specifier, "(invalid source)") + } + ModuleGraphError::InvalidTypeAssertion { .. } => { + self.build_error_msg(specifier, "(invalid import assertion)") + } + ModuleGraphError::LoadingErr(_, _) => { + self.build_error_msg(specifier, "(loading error)") + } + ModuleGraphError::ParseErr(_, _) => { + self.build_error_msg(specifier, "(parsing error)") + } + ModuleGraphError::ResolutionError(_) => { + self.build_error_msg(specifier, "(resolution error)") + } + ModuleGraphError::UnsupportedImportAssertionType(_, _) => { + self.build_error_msg(specifier, "(unsupported import assertion)") + } + ModuleGraphError::UnsupportedMediaType(_, _) => { + self.build_error_msg(specifier, "(unsupported)") + } + ModuleGraphError::Missing(_) => { + self.build_error_msg(specifier, "(missing)") + } + } + } + + fn build_error_msg( + &self, + specifier: &ModuleSpecifier, + error_msg: &str, + ) -> TreeNode { + TreeNode::from_text(format!( + "{} {}", + colors::red(specifier), + colors::red_bold(error_msg) + )) + } + + fn build_resolved_info( + &mut self, + resolved: &Resolved, + type_dep: bool, + ) -> Option { + match resolved { + Resolved::Ok { specifier, .. } => { + let resolved_specifier = self.graph.resolve(specifier); + Some(match self.graph.try_get(&resolved_specifier) { + Ok(Some(module)) => self.build_module_info(module, type_dep), + Err(err) => self.build_error_info(&err, &resolved_specifier), + Ok(None) => TreeNode::from_text(format!( "{} {}", colors::red(specifier), colors::red_bold("(missing)") - ), - ), + )), + }) } - } - Resolved::Err(err) => fmt_info_msg( - f, - prefix, - last, - false, - format!( + Resolved::Err(err) => Some(TreeNode::from_text(format!( "{} {}", colors::italic(err.to_string()), colors::red_bold("(resolve error)") - ), - ), - _ => Ok(()), + ))), + _ => None, + } } } -#[cfg(test)] -mod tests { - use deno_graph::source::CacheInfo; - use deno_graph::source::MemoryLoader; - use deno_graph::source::Source; - use test_util::strip_ansi_codes; - - use super::*; - use std::path::PathBuf; - - #[tokio::test] - async fn test_info_graph() { - let mut loader = MemoryLoader::new( - vec![ - ( - "https://deno.land/x/example/a.ts", - Source::Module { - specifier: "https://deno.land/x/example/a.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"import * as b from "./b.ts"; - import type { F } from "./f.d.ts"; - import * as g from "./g.js"; - "#, - }, - ), - ( - "https://deno.land/x/example/b.ts", - Source::Module { - specifier: "https://deno.land/x/example/b.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#" - // @deno-types="./c.d.ts" - import * as c from "./c.js"; - import * as d from "./d.ts";"#, - }, - ), - ( - "https://deno.land/x/example/c.js", - Source::Module { - specifier: "https://deno.land/x/example/c.js", - maybe_headers: Some(vec![( - "content-type", - "application/javascript", - )]), - content: r#"export const c = "c";"#, - }, - ), - ( - "https://deno.land/x/example/c.d.ts", - Source::Module { - specifier: "https://deno.land/x/example/c.d.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"export const c: "c";"#, - }, - ), - ( - "https://deno.land/x/example/d.ts", - Source::Module { - specifier: "https://deno.land/x/example/d.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"import * as e from "./e.ts"; - export const d = "d";"#, - }, - ), - ( - "https://deno.land/x/example/e.ts", - Source::Module { - specifier: "https://deno.land/x/example/e.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"import * as b from "./b.ts"; - export const e = "e";"#, - }, - ), - ( - "https://deno.land/x/example/f.d.ts", - Source::Module { - specifier: "https://deno.land/x/example/f.d.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"export interface F { }"#, - }, - ), - ( - "https://deno.land/x/example/g.js", - Source::Module { - specifier: "https://deno.land/x/example/g.js", - maybe_headers: Some(vec![ - ("content-type", "application/javascript"), - ("x-typescript-types", "./g.d.ts"), - ]), - content: r#"export const g = "g";"#, - }, - ), - ( - "https://deno.land/x/example/g.d.ts", - Source::Module { - specifier: "https://deno.land/x/example/g.d.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"export const g: "g";"#, - }, - ), - ], - vec![( - "https://deno.land/x/example/a.ts", - CacheInfo { - local: Some(PathBuf::from( - "/cache/deps/https/deno.land/x/example/a.ts", - )), - emit: Some(PathBuf::from( - "/cache/deps/https/deno.land/x/example/a.js", - )), - ..Default::default() - }, - )], - ); - let root_specifier = - ModuleSpecifier::parse("https://deno.land/x/example/a.ts").unwrap(); - let graph = deno_graph::create_graph( - vec![(root_specifier, ModuleKind::Esm)], - &mut loader, - deno_graph::GraphOptions { - is_dynamic: false, - imports: None, - resolver: None, - locker: None, - module_analyzer: None, - reporter: None, - }, - ) - .await; - let mut output = String::new(); - fmt_module_graph(&graph, &mut output).unwrap(); - assert_eq!( - strip_ansi_codes(&output), - r#"local: /cache/deps/https/deno.land/x/example/a.ts -emit: /cache/deps/https/deno.land/x/example/a.js -type: TypeScript -dependencies: 8 unique (total 477B) - -https://deno.land/x/example/a.ts (129B) -├─┬ https://deno.land/x/example/b.ts (120B) -│ ├── https://deno.land/x/example/c.js (21B) -│ ├── https://deno.land/x/example/c.d.ts (20B) -│ └─┬ https://deno.land/x/example/d.ts (62B) -│ └─┬ https://deno.land/x/example/e.ts (62B) -│ └── https://deno.land/x/example/b.ts * -├── https://deno.land/x/example/f.d.ts (22B) -└─┬ https://deno.land/x/example/g.js (21B) - └── https://deno.land/x/example/g.d.ts (20B) -"# - ); - } - - #[tokio::test] - async fn test_info_graph_import_assertion() { - let mut loader = MemoryLoader::new( - vec![ - ( - "https://deno.land/x/example/a.ts", - Source::Module { - specifier: "https://deno.land/x/example/a.ts", - maybe_headers: Some(vec![( - "content-type", - "application/typescript", - )]), - content: r#"import b from "./b.json" assert { type: "json" }; - const c = await import("./c.json", { assert: { type: "json" } }); - "#, - }, - ), - ( - "https://deno.land/x/example/b.json", - Source::Module { - specifier: "https://deno.land/x/example/b.json", - maybe_headers: Some(vec![("content-type", "application/json")]), - content: r#"{"b":"c"}"#, - }, - ), - ( - "https://deno.land/x/example/c.json", - Source::Module { - specifier: "https://deno.land/x/example/c.json", - maybe_headers: Some(vec![("content-type", "application/json")]), - content: r#"{"c":1}"#, - }, - ), - ], - vec![( - "https://deno.land/x/example/a.ts", - CacheInfo { - local: Some(PathBuf::from( - "/cache/deps/https/deno.land/x/example/a.ts", - )), - emit: Some(PathBuf::from( - "/cache/deps/https/deno.land/x/example/a.js", - )), - ..Default::default() - }, - )], - ); - let root_specifier = - ModuleSpecifier::parse("https://deno.land/x/example/a.ts").unwrap(); - let graph = deno_graph::create_graph( - vec![(root_specifier, ModuleKind::Esm)], - &mut loader, - deno_graph::GraphOptions { - is_dynamic: false, - imports: None, - resolver: None, - locker: None, - module_analyzer: None, - reporter: None, - }, - ) - .await; - let mut output = String::new(); - fmt_module_graph(&graph, &mut output).unwrap(); - assert_eq!( - strip_ansi_codes(&output), - r#"local: /cache/deps/https/deno.land/x/example/a.ts -emit: /cache/deps/https/deno.land/x/example/a.js -type: TypeScript -dependencies: 2 unique (total 156B) - -https://deno.land/x/example/a.ts (140B) -├── https://deno.land/x/example/b.json (9B) -└── https://deno.land/x/example/c.json (7B) -"# - ); - } +fn maybe_size_to_text(maybe_size: Option) -> String { + colors::gray(format!( + "({})", + match maybe_size { + Some(size) => display::human_size(size as f64), + None => "unknown".to_string(), + } + )) + .to_string() }