mirror of
https://github.com/denoland/deno.git
synced 2024-11-23 15:16:54 -05:00
2fee8394a9
Yearly tradition of creating extra noise in git.
673 lines
20 KiB
Rust
673 lines
20 KiB
Rust
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::fmt;
|
|
use std::fmt::Write;
|
|
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::resolve_url_or_path;
|
|
use deno_core::serde_json;
|
|
use deno_core::serde_json::json;
|
|
use deno_graph::Dependency;
|
|
use deno_graph::Module;
|
|
use deno_graph::ModuleGraph;
|
|
use deno_graph::ModuleGraphError;
|
|
use deno_graph::ModuleKind;
|
|
use deno_graph::Resolved;
|
|
use deno_runtime::colors;
|
|
|
|
use crate::args::Flags;
|
|
use crate::args::InfoFlags;
|
|
use crate::display;
|
|
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;
|
|
use crate::util::checksum;
|
|
|
|
pub async fn info(flags: Flags, info_flags: InfoFlags) -> Result<(), AnyError> {
|
|
let ps = ProcState::build(flags).await?;
|
|
if let Some(specifier) = info_flags.file {
|
|
let specifier = resolve_url_or_path(&specifier)?;
|
|
let graph = ps.create_graph(vec![(specifier, ModuleKind::Esm)]).await?;
|
|
|
|
if info_flags.json {
|
|
let mut json_graph = json!(graph);
|
|
add_npm_packages_to_json(&mut json_graph, &ps.npm_resolver);
|
|
display::write_json_to_stdout(&json_graph)?;
|
|
} else {
|
|
let mut output = String::new();
|
|
GraphDisplayContext::write(&graph, &ps.npm_resolver, &mut output)?;
|
|
display::write_to_stdout_ignore_sigpipe(output.as_bytes())?;
|
|
}
|
|
} else {
|
|
// If it was just "deno info" print location of caches and exit
|
|
print_cache_info(
|
|
&ps,
|
|
info_flags.json,
|
|
ps.options.location_flag().as_ref(),
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn print_cache_info(
|
|
state: &ProcState,
|
|
json: bool,
|
|
location: Option<&deno_core::url::Url>,
|
|
) -> Result<(), AnyError> {
|
|
let deno_dir = &state.dir.root_path_for_display();
|
|
let modules_cache = &state.file_fetcher.get_http_cache_location();
|
|
let npm_cache = &state.npm_cache.as_readonly().get_cache_location();
|
|
let typescript_cache = &state.dir.gen_cache.location;
|
|
let registry_cache = &state.dir.registries_folder_path();
|
|
let mut origin_dir = state.dir.origin_data_folder_path();
|
|
|
|
if let Some(location) = &location {
|
|
origin_dir =
|
|
origin_dir.join(checksum::gen(&[location.to_string().as_bytes()]));
|
|
}
|
|
|
|
let local_storage_dir = origin_dir.join("local_storage");
|
|
|
|
if json {
|
|
let mut output = json!({
|
|
"denoDir": deno_dir.to_string(),
|
|
"modulesCache": modules_cache,
|
|
"npmCache": npm_cache,
|
|
"typescriptCache": typescript_cache,
|
|
"registryCache": registry_cache,
|
|
"originStorage": origin_dir,
|
|
});
|
|
|
|
if location.is_some() {
|
|
output["localStorage"] = serde_json::to_value(local_storage_dir)?;
|
|
}
|
|
|
|
display::write_json_to_stdout(&output)
|
|
} else {
|
|
println!("{} {}", colors::bold("DENO_DIR location:"), deno_dir);
|
|
println!(
|
|
"{} {}",
|
|
colors::bold("Remote modules cache:"),
|
|
modules_cache.display()
|
|
);
|
|
println!(
|
|
"{} {}",
|
|
colors::bold("npm modules cache:"),
|
|
npm_cache.display()
|
|
);
|
|
println!(
|
|
"{} {}",
|
|
colors::bold("Emitted modules cache:"),
|
|
typescript_cache.display()
|
|
);
|
|
println!(
|
|
"{} {}",
|
|
colors::bold("Language server registries cache:"),
|
|
registry_cache.display(),
|
|
);
|
|
println!(
|
|
"{} {}",
|
|
colors::bold("Origin storage:"),
|
|
origin_dir.display()
|
|
);
|
|
if location.is_some() {
|
|
println!(
|
|
"{} {}",
|
|
colors::bold("Local Storage:"),
|
|
local_storage_dir.display(),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn add_npm_packages_to_json(
|
|
json: &mut serde_json::Value,
|
|
npm_resolver: &NpmPackageResolver,
|
|
) {
|
|
// ideally deno_graph could handle this, but for now we just modify the json here
|
|
let snapshot = npm_resolver.snapshot();
|
|
let json = json.as_object_mut().unwrap();
|
|
let modules = json.get_mut("modules").and_then(|m| m.as_array_mut());
|
|
if let Some(modules) = modules {
|
|
if modules.len() == 1
|
|
&& modules[0].get("kind").and_then(|k| k.as_str()) == Some("external")
|
|
{
|
|
// If there is only one module and it's "external", then that means
|
|
// someone provided an npm specifier as a cli argument. In this case,
|
|
// we want to show which npm package the cli argument resolved to.
|
|
let module = &mut modules[0];
|
|
let maybe_package = module
|
|
.get("specifier")
|
|
.and_then(|k| k.as_str())
|
|
.and_then(|specifier| NpmPackageReference::from_str(specifier).ok())
|
|
.and_then(|package_ref| {
|
|
snapshot
|
|
.resolve_package_from_deno_module(&package_ref.req)
|
|
.ok()
|
|
});
|
|
if let Some(pkg) = maybe_package {
|
|
if let Some(module) = module.as_object_mut() {
|
|
module
|
|
.insert("npmPackage".to_string(), pkg.id.as_serialized().into());
|
|
// change the "kind" to be "npm"
|
|
module.insert("kind".to_string(), "npm".into());
|
|
}
|
|
}
|
|
} else {
|
|
// Filter out npm package references from the modules and instead
|
|
// have them only listed as dependencies. This is done because various
|
|
// npm specifiers modules in the graph are really just unresolved
|
|
// references. So there could be listed multiple npm specifiers
|
|
// that would resolve to a single npm package.
|
|
for i in (0..modules.len()).rev() {
|
|
if modules[i].get("kind").and_then(|k| k.as_str()) == Some("external") {
|
|
modules.remove(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
for module in modules.iter_mut() {
|
|
let dependencies = module
|
|
.get_mut("dependencies")
|
|
.and_then(|d| d.as_array_mut());
|
|
if let Some(dependencies) = dependencies {
|
|
for dep in dependencies.iter_mut() {
|
|
if let serde_json::Value::Object(dep) = dep {
|
|
let specifier = dep.get("specifier").and_then(|s| s.as_str());
|
|
if let Some(specifier) = specifier {
|
|
if let Ok(npm_ref) = NpmPackageReference::from_str(specifier) {
|
|
if let Ok(pkg) =
|
|
snapshot.resolve_package_from_deno_module(&npm_ref.req)
|
|
{
|
|
dep.insert(
|
|
"npmPackage".to_string(),
|
|
pkg.id.as_serialized().into(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut sorted_packages = snapshot.all_packages();
|
|
sorted_packages.sort_by(|a, b| a.id.cmp(&b.id));
|
|
let mut json_packages = serde_json::Map::with_capacity(sorted_packages.len());
|
|
for pkg in sorted_packages {
|
|
let mut kv = serde_json::Map::new();
|
|
kv.insert("name".to_string(), pkg.id.name.to_string().into());
|
|
kv.insert("version".to_string(), pkg.id.version.to_string().into());
|
|
let mut deps = pkg.dependencies.values().collect::<Vec<_>>();
|
|
deps.sort();
|
|
let deps = deps
|
|
.into_iter()
|
|
.map(|id| serde_json::Value::String(id.as_serialized()))
|
|
.collect::<Vec<_>>();
|
|
kv.insert("dependencies".to_string(), deps.into());
|
|
|
|
json_packages.insert(pkg.id.as_serialized(), kv.into());
|
|
}
|
|
|
|
json.insert("npmPackages".to_string(), json_packages.into());
|
|
}
|
|
|
|
struct TreeNode {
|
|
text: String,
|
|
children: Vec<TreeNode>,
|
|
}
|
|
|
|
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!(
|
|
writer,
|
|
"{} displaying graphs that have multiple roots is not supported.",
|
|
colors::red("error:")
|
|
);
|
|
}
|
|
|
|
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()
|
|
)?;
|
|
}
|
|
}
|
|
writeln!(writer, "{} {}", colors::bold("type:"), root.media_type)?;
|
|
let total_modules_size =
|
|
self.graph.modules().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 = self.graph.modules().count() - 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<TreeNode> {
|
|
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.as_serialized(),
|
|
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)
|
|
}
|
|
Specifier(_) => specifier_str,
|
|
};
|
|
let maybe_size = match &package_or_specifier {
|
|
Package(package) => {
|
|
self.npm_info.package_sizes.get(&package.id).copied()
|
|
}
|
|
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<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.as_serialized(),
|
|
size_str
|
|
));
|
|
if let Some(package) = self.npm_info.packages.get(dep_id) {
|
|
if !package.dependencies.is_empty() {
|
|
let was_seen = !self.seen.insert(package.id.as_serialized());
|
|
if was_seen {
|
|
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::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<TreeNode> {
|
|
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) => Some(TreeNode::from_text(format!(
|
|
"{} {}",
|
|
colors::italic(err.to_string()),
|
|
colors::red_bold("(resolve error)")
|
|
))),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|