0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

feat: support npm specifiers in deno info for display text output only (#16470)

This commit is contained in:
David Sherret 2022-10-28 16:19:55 -04:00 committed by GitHub
parent 2c674dcd20
commit edaceecec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 591 additions and 556 deletions

View file

@ -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()),

View file

@ -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<T: AsRef<[u8]>>(
@ -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<u64> {
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::*;

View file

@ -697,6 +697,13 @@ impl NpmResolution {
Ok(snapshot)
}
pub fn resolve_package_from_id(
&self,
id: &NpmPackageId,
) -> Option<NpmResolutionPackage> {
self.snapshot.read().package_from_id(id).cloned()
}
pub fn resolve_package_from_package(
&self,
name: &str,

View file

@ -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<PathBuf, AnyError>;
fn package_size(&self, package_id: &NpmPackageId) -> Result<u64, AnyError>;
fn has_packages(&self) -> bool;
fn add_package_reqs(

View file

@ -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<u64, AnyError> {
let package_folder = self.package_folder(package_id);
Ok(fs_util::dir_size(&package_folder)?)
}
fn has_packages(&self) -> bool {
self.resolution.has_packages()
}

View file

@ -177,6 +177,22 @@ impl InnerNpmPackageResolver for LocalNpmPackageResolver {
Ok(package_root_path)
}
fn package_size(&self, package_id: &NpmPackageId) -> Result<u64, AnyError> {
match self.resolution.resolve_package_from_id(package_id) {
Some(package) => Ok(fs_util::dir_size(
// package is stored at:
// node_modules/.deno/<package_id>/node_modules/<package_name>
&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()
}

View file

@ -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<u64, AnyError> {
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)
}

View file

@ -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()),

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -1,5 +1,7 @@
[WILDCARD]
local: [WILDCARD]test.ts
type: TypeScript
dependencies: 7 unique (total [WILDCARD])
dependencies: 7 unique
size: [WILDCARD]
[WILDCARD]

View file

@ -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])

View file

@ -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)

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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])

View file

@ -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<TreeNode>,
}
fn fmt_module_graph(graph: &ModuleGraph, f: &mut impl Write) -> fmt::Result {
if graph.roots.is_empty() || graph.roots.len() > 1 {
impl TreeNode {
pub fn from_text(text: String) -> Self {
Self {
text,
children: Default::default(),
}
}
}
fn print_tree_node<TWrite: Write>(
tree_node: &TreeNode,
writer: &mut TWrite,
) -> fmt::Result {
fn print_children<TWrite: Write>(
writer: &mut TWrite,
prefix: &str,
children: &Vec<TreeNode>,
) -> 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<NpmPackageId, u64>,
resolved_reqs: HashMap<NpmPackageReq, NpmPackageId>,
packages: HashMap<NpmPackageId, NpmResolutionPackage>,
specifiers: HashMap<ModuleSpecifier, NpmPackageReq>,
}
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<String>,
}
impl<'a> GraphDisplayContext<'a> {
pub fn write<TWrite: 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,
npm_info,
seen: Default::default(),
}
.into_writer(writer)
}
fn into_writer<TWrite: Write>(mut self, writer: &mut TWrite) -> fmt::Result {
if self.graph.roots.is_empty() || self.graph.roots.len() > 1 {
return writeln!(
f,
writer,
"{} 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) {
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!(
f,
writer,
"{} {}",
colors::bold("local:"),
local.to_string_lossy()
)?;
}
if let Some(emit) = &cache_info.emit {
writeln!(f, "{} {}", colors::bold("emit:"), emit.to_string_lossy())?;
writeln!(
writer,
"{} {}",
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 {}",
writer,
"{} {}",
colors::bold("map:"),
map.to_string_lossy()
)?;
}
}
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::<f64>();
let total_npm_package_size = self
.npm_info
.package_sizes
.values()
.map(|s| *s as f64)
.sum::<f64>();
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,
colors::gray(format!("(total {})", display::human_size(total_size)))
)?;
writeln!(
f,
"\n{} {}",
root_specifier,
colors::gray(format!("({})", display::human_size(root.size() as f64)))
writer,
"{} {}",
colors::bold("size:"),
display::human_size(total_size),
)?;
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,
)?;
}
writeln!(writer)?;
let root_node = self.build_module_info(root, false);
print_tree_node(&root_node, writer)?;
Ok(())
}
Err(ModuleGraphError::Missing(_)) => {
writeln!(f, "{} module could not be found", colors::red("error:"))
writeln!(
writer,
"{} module could not be found",
colors::red("error:")
)
}
Err(err) => {
writeln!(f, "{} {}", colors::red("error:"), err)
writeln!(writer, "{} {}", colors::red("error:"), err)
}
Ok(None) => {
writeln!(f, "{} an internal error occurred", colors::red("error:"))
writeln!(
writer,
"{} an internal error occurred",
colors::red("error:")
)
}
}
}
}
fn fmt_dep_info<S: AsRef<str> + fmt::Display + Clone>(
dep: &Dependency,
f: &mut impl Write,
prefix: S,
last: bool,
graph: &ModuleGraph,
seen: &mut HashSet<ModuleSpecifier>,
) -> fmt::Result {
fn build_dep_info(&mut self, dep: &Dependency) -> Vec<TreeNode> {
let mut children = Vec::with_capacity(2);
if !dep.maybe_code.is_none() {
fmt_resolved_info(
&dep.maybe_code,
f,
prefix.clone(),
dep.maybe_type.is_none() && last,
graph,
false,
seen,
)?;
if let Some(child) = self.build_resolved_info(&dep.maybe_code, false) {
children.push(child);
}
}
if !dep.maybe_type.is_none() {
fmt_resolved_info(&dep.maybe_type, f, prefix, last, graph, true, seen)?;
if let Some(child) = self.build_resolved_info(&dep.maybe_type, true) {
children.push(child);
}
}
children
}
Ok(())
}
fn fmt_module_info<S: AsRef<str> + fmt::Display + Clone>(
module: &Module,
f: &mut impl Write,
prefix: S,
last: bool,
graph: &ModuleGraph,
type_dep: bool,
seen: &mut HashSet<ModuleSpecifier>,
) -> 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 {
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()
};
(specifier_str, colors::gray(" *").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 size_str =
colors::gray(format!(" ({})", display::human_size(module.size() as f64)))
.to_string();
(specifier_str, size_str)
let header_text = match &package_or_specifier {
Package(package) => {
format!("{} - {}", specifier_str, package.id.version)
}
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))
};
seen.insert(module.specifier.clone());
fmt_info_msg(
f,
prefix.clone(),
last,
children,
format!("{}{}", specifier_str, size_str),
)?;
let mut tree_node = TreeNode::from_text(header_text);
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,
)?;
if let Some(child) = self.build_resolved_info(type_dep, true) {
tree_node.children.push(child);
}
}
Ok(())
}
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 fmt_error_info<S: AsRef<str> + fmt::Display + Clone>(
fn build_npm_deps(
&mut self,
package: &NpmResolutionPackage,
) -> Vec<TreeNode> {
let mut deps = package.dependencies.values().collect::<Vec<_>>();
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,
f: &mut impl Write,
prefix: S,
last: bool,
specifier: &ModuleSpecifier,
seen: &mut HashSet<ModuleSpecifier>,
) -> fmt::Result {
seen.insert(specifier.clone());
) -> TreeNode {
self.seen.insert(specifier.to_string());
match err {
ModuleGraphError::InvalidSource(_, _) => {
fmt_error_msg(f, prefix, last, specifier, "(invalid source)")
self.build_error_msg(specifier, "(invalid source)")
}
ModuleGraphError::InvalidTypeAssertion { .. } => {
fmt_error_msg(f, prefix, last, specifier, "(invalid import assertion)")
self.build_error_msg(specifier, "(invalid import assertion)")
}
ModuleGraphError::LoadingErr(_, _) => {
fmt_error_msg(f, prefix, last, specifier, "(loading error)")
self.build_error_msg(specifier, "(loading error)")
}
ModuleGraphError::ParseErr(_, _) => {
fmt_error_msg(f, prefix, last, specifier, "(parsing error)")
self.build_error_msg(specifier, "(parsing error)")
}
ModuleGraphError::ResolutionError(_) => {
fmt_error_msg(f, prefix, last, specifier, "(resolution error)")
self.build_error_msg(specifier, "(resolution error)")
}
ModuleGraphError::UnsupportedImportAssertionType(_, _) => {
self.build_error_msg(specifier, "(unsupported import assertion)")
}
ModuleGraphError::UnsupportedImportAssertionType(_, _) => fmt_error_msg(
f,
prefix,
last,
specifier,
"(unsupported import assertion)",
),
ModuleGraphError::UnsupportedMediaType(_, _) => {
fmt_error_msg(f, prefix, last, specifier, "(unsupported)")
self.build_error_msg(specifier, "(unsupported)")
}
ModuleGraphError::Missing(_) => {
fmt_error_msg(f, prefix, last, specifier, "(missing)")
self.build_error_msg(specifier, "(missing)")
}
}
}
}
fn fmt_info_msg<S, M>(
f: &mut impl Write,
prefix: S,
last: bool,
children: bool,
msg: M,
) -> fmt::Result
where
S: AsRef<str> + fmt::Display + Clone,
M: AsRef<str> + 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<S, M>(
f: &mut impl Write,
prefix: S,
last: bool,
fn build_error_msg(
&self,
specifier: &ModuleSpecifier,
error_msg: M,
) -> fmt::Result
where
S: AsRef<str> + fmt::Display + Clone,
M: AsRef<str> + fmt::Display,
{
fmt_info_msg(
f,
prefix,
last,
false,
format!("{} {}", colors::red(specifier), colors::red_bold(error_msg)),
)
}
error_msg: &str,
) -> TreeNode {
TreeNode::from_text(format!(
"{} {}",
colors::red(specifier),
colors::red_bold(error_msg)
))
}
fn fmt_resolved_info<S: AsRef<str> + fmt::Display + Clone>(
fn build_resolved_info(
&mut self,
resolved: &Resolved,
f: &mut impl Write,
prefix: S,
last: bool,
graph: &ModuleGraph,
type_dep: bool,
seen: &mut HashSet<ModuleSpecifier>,
) -> fmt::Result {
) -> Option<TreeNode> {
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)
}
Err(err) => {
fmt_error_info(&err, f, prefix, last, &resolved_specifier, seen)
}
Ok(None) => fmt_info_msg(
f,
prefix,
last,
false,
format!(
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<u64>) -> String {
colors::gray(format!(
"({})",
match maybe_size {
Some(size) => display::human_size(size as f64),
None => "unknown".to_string(),
}
))
.to_string()
}