// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::colors; use deno_ast::MediaType; use deno_core::resolve_url; use deno_core::serde::Serialize; use deno_core::ModuleSpecifier; use std::collections::HashSet; use std::fmt; use std::iter::Iterator; use std::path::PathBuf; 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 = ' '; #[derive(Debug, Serialize, Ord, PartialOrd, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ModuleGraphInfoDep { pub specifier: String, #[serde(skip_serializing_if = "is_false")] pub is_dynamic: bool, #[serde(rename = "code", skip_serializing_if = "Option::is_none")] pub maybe_code: Option, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub maybe_type: Option, } fn is_false(b: &bool) -> bool { !b } impl ModuleGraphInfoDep { fn write_info + fmt::Display + Clone>( &self, f: &mut fmt::Formatter, prefix: S, last: bool, modules: &[ModuleGraphInfoMod], seen: &mut HashSet, ) -> fmt::Result { let maybe_code = self .maybe_code .as_ref() .and_then(|s| modules.iter().find(|m| &m.specifier == s)); let maybe_type = self .maybe_type .as_ref() .and_then(|s| modules.iter().find(|m| &m.specifier == s)); match (maybe_code, maybe_type) { (Some(code), Some(types)) => { code.write_info(f, prefix.clone(), false, false, modules, seen)?; types.write_info(f, prefix, last, true, modules, seen) } (Some(code), None) => { code.write_info(f, prefix, last, false, modules, seen) } (None, Some(types)) => { types.write_info(f, prefix, last, true, modules, seen) } _ => Ok(()), } } } #[derive(Debug, Serialize, Ord, PartialOrd, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ModuleGraphInfoMod { pub specifier: ModuleSpecifier, pub dependencies: Vec, #[serde(rename = "typeDependency", skip_serializing_if = "Option::is_none")] pub maybe_type_dependency: Option, #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub media_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub local: Option, #[serde(skip_serializing_if = "Option::is_none")] pub checksum: Option, #[serde(skip_serializing_if = "Option::is_none")] pub emit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub map: Option, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } impl Default for ModuleGraphInfoMod { fn default() -> Self { ModuleGraphInfoMod { specifier: resolve_url("https://deno.land/x/mod.ts").unwrap(), dependencies: Vec::new(), maybe_type_dependency: None, size: None, media_type: None, local: None, checksum: None, emit: None, map: None, error: None, } } } impl ModuleGraphInfoMod { fn write_info + fmt::Display>( &self, f: &mut fmt::Formatter, prefix: S, last: bool, type_dep: bool, modules: &[ModuleGraphInfoMod], seen: &mut HashSet, ) -> fmt::Result { let was_seen = seen.contains(&self.specifier); let sibling_connector = if last { LAST_SIBLING_CONNECTOR } else { SIBLING_CONNECTOR }; let child_connector = if (self.dependencies.is_empty() && self.maybe_type_dependency.is_none()) || was_seen { CHILD_NO_DEPS_CONNECTOR } else { CHILD_DEPS_CONNECTOR }; let (size, specifier) = if self.error.is_some() { ( colors::red_bold(" (error)").to_string(), colors::red(&self.specifier).to_string(), ) } else if was_seen { let name = if type_dep { colors::italic_gray(&self.specifier).to_string() } else { colors::gray(&self.specifier).to_string() }; (colors::gray(" *").to_string(), name) } else { let name = if type_dep { colors::italic(&self.specifier).to_string() } else { self.specifier.to_string() }; ( colors::gray(format!( " ({})", human_size(self.size.unwrap_or(0) as f64) )) .to_string(), name, ) }; seen.insert(self.specifier.clone()); writeln!( f, "{} {}{}", colors::gray(format!( "{}{}─{}", prefix, sibling_connector, child_connector )), specifier, size )?; 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_count = self.dependencies.len(); for (idx, dep) in self.dependencies.iter().enumerate() { dep.write_info( f, &prefix, idx == dep_count - 1 && self.maybe_type_dependency.is_none(), modules, seen, )?; } if let Some(dep) = &self.maybe_type_dependency { dep.write_info(f, &prefix, true, modules, seen)?; } } Ok(()) } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ModuleGraphInfo { pub root: ModuleSpecifier, pub modules: Vec, pub size: usize, } impl fmt::Display for ModuleGraphInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let root = self .modules .iter() .find(|m| m.specifier == self.root) .unwrap(); if let Some(err) = &root.error { writeln!(f, "{} {}", colors::red("error:"), err) } else { if let Some(local) = &root.local { writeln!(f, "{} {}", colors::bold("local:"), local.to_string_lossy())?; } if let Some(media_type) = &root.media_type { writeln!(f, "{} {}", colors::bold("type:"), media_type)?; } if let Some(emit) = &root.emit { writeln!(f, "{} {}", colors::bold("emit:"), emit.to_string_lossy())?; } if let Some(map) = &root.map { writeln!(f, "{} {}", colors::bold("map:"), map.to_string_lossy())?; } let dep_count = self.modules.len() - 1; writeln!( f, "{} {} unique {}", colors::bold("dependencies:"), dep_count, colors::gray(format!("(total {})", human_size(self.size as f64))) )?; writeln!( f, "\n{} {}", self.root, colors::gray(format!( "({})", human_size(root.size.unwrap_or(0) as f64) )) )?; let mut seen = HashSet::new(); let dep_len = root.dependencies.len(); for (idx, dep) in root.dependencies.iter().enumerate() { dep.write_info( f, "", idx == dep_len - 1 && root.maybe_type_dependency.is_none(), &self.modules, &mut seen, )?; } if let Some(dep) = &root.maybe_type_dependency { dep.write_info(f, "", true, &self.modules, &mut seen)?; } Ok(()) } } } /// An entry in the `ModuleInfoMap` the provides the size of the module and /// a vector of its dependencies, which should also be available as entries /// in the map. #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ModuleInfoMapItem { pub deps: Vec, pub size: usize, } /// A function that converts a float to a string the represents a human /// readable version of that number. pub fn human_size(size: f64) -> String { let negative = if size.is_sign_positive() { "" } else { "-" }; let size = size.abs(); let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; if size < 1_f64 { return format!("{}{}{}", negative, size, "B"); } let delimiter = 1024_f64; let exponent = std::cmp::min( (size.ln() / delimiter.ln()).floor() as i32, (units.len() - 1) as i32, ); let pretty_bytes = format!("{:.2}", size / delimiter.powi(exponent)) .parse::() .unwrap() * 1_f64; let unit = units[exponent as usize]; format!("{}{}{}", negative, pretty_bytes, unit) } #[cfg(test)] mod test { use super::*; use deno_core::resolve_url; use deno_core::serde_json::json; #[test] fn human_size_test() { assert_eq!(human_size(16_f64), "16B"); assert_eq!(human_size((16 * 1024) as f64), "16KB"); assert_eq!(human_size((16 * 1024 * 1024) as f64), "16MB"); assert_eq!(human_size(16_f64 * 1024_f64.powf(3.0)), "16GB"); assert_eq!(human_size(16_f64 * 1024_f64.powf(4.0)), "16TB"); assert_eq!(human_size(16_f64 * 1024_f64.powf(5.0)), "16PB"); assert_eq!(human_size(16_f64 * 1024_f64.powf(6.0)), "16EB"); assert_eq!(human_size(16_f64 * 1024_f64.powf(7.0)), "16ZB"); assert_eq!(human_size(16_f64 * 1024_f64.powf(8.0)), "16YB"); } fn get_fixture() -> ModuleGraphInfo { let specifier_a = resolve_url("https://deno.land/x/a.ts").unwrap(); let specifier_b = resolve_url("https://deno.land/x/b.ts").unwrap(); let specifier_c_js = resolve_url("https://deno.land/x/c.js").unwrap(); let specifier_c_dts = resolve_url("https://deno.land/x/c.d.ts").unwrap(); let specifier_d_js = resolve_url("https://deno.land/x/d.js").unwrap(); let specifier_d_dts = resolve_url("https://deno.land/x/d.d.ts").unwrap(); let modules = vec![ ModuleGraphInfoMod { specifier: specifier_a.clone(), dependencies: vec![ModuleGraphInfoDep { specifier: "./b.ts".to_string(), is_dynamic: false, maybe_code: Some(specifier_b.clone()), maybe_type: None, }], size: Some(123), media_type: Some(MediaType::TypeScript), local: Some(PathBuf::from("/cache/deps/https/deno.land/x/a.ts")), checksum: Some("abcdef".to_string()), emit: Some(PathBuf::from("/cache/emit/https/deno.land/x/a.js")), ..Default::default() }, ModuleGraphInfoMod { specifier: specifier_b, dependencies: vec![ModuleGraphInfoDep { specifier: "./c.js".to_string(), is_dynamic: false, maybe_code: Some(specifier_c_js.clone()), maybe_type: Some(specifier_c_dts.clone()), }], size: Some(456), media_type: Some(MediaType::TypeScript), local: Some(PathBuf::from("/cache/deps/https/deno.land/x/b.ts")), checksum: Some("def123".to_string()), emit: Some(PathBuf::from("/cache/emit/https/deno.land/x/b.js")), ..Default::default() }, ModuleGraphInfoMod { specifier: specifier_c_js, dependencies: vec![ModuleGraphInfoDep { specifier: "./d.js".to_string(), is_dynamic: false, maybe_code: Some(specifier_d_js.clone()), maybe_type: None, }], size: Some(789), media_type: Some(MediaType::JavaScript), local: Some(PathBuf::from("/cache/deps/https/deno.land/x/c.js")), checksum: Some("9876abcef".to_string()), ..Default::default() }, ModuleGraphInfoMod { specifier: specifier_c_dts, size: Some(999), media_type: Some(MediaType::Dts), local: Some(PathBuf::from("/cache/deps/https/deno.land/x/c.d.ts")), checksum: Some("a2b3c4d5".to_string()), ..Default::default() }, ModuleGraphInfoMod { specifier: specifier_d_js, size: Some(987), maybe_type_dependency: Some(ModuleGraphInfoDep { specifier: "/x/d.d.ts".to_string(), is_dynamic: false, maybe_code: None, maybe_type: Some(specifier_d_dts.clone()), }), media_type: Some(MediaType::JavaScript), local: Some(PathBuf::from("/cache/deps/https/deno.land/x/d.js")), checksum: Some("5k6j7h8g".to_string()), ..Default::default() }, ModuleGraphInfoMod { specifier: specifier_d_dts, size: Some(67), media_type: Some(MediaType::Dts), local: Some(PathBuf::from("/cache/deps/https/deno.land/x/d.d.ts")), checksum: Some("0h0h0h0h0h".to_string()), ..Default::default() }, ]; ModuleGraphInfo { root: specifier_a, modules, size: 99999, } } #[test] fn text_module_graph_info_display() { let fixture = get_fixture(); let text = fixture.to_string(); let actual = colors::strip_ansi_codes(&text); let expected = r#"local: /cache/deps/https/deno.land/x/a.ts type: TypeScript emit: /cache/emit/https/deno.land/x/a.js dependencies: 5 unique (total 97.66KB) https://deno.land/x/a.ts (123B) └─┬ https://deno.land/x/b.ts (456B) ├─┬ https://deno.land/x/c.js (789B) │ └─┬ https://deno.land/x/d.js (987B) │ └── https://deno.land/x/d.d.ts (67B) └── https://deno.land/x/c.d.ts (999B) "#; assert_eq!(actual, expected); } #[test] fn test_module_graph_info_json() { let fixture = get_fixture(); let actual = json!(fixture); assert_eq!( actual, json!({ "root": "https://deno.land/x/a.ts", "modules": [ { "specifier": "https://deno.land/x/a.ts", "dependencies": [ { "specifier": "./b.ts", "code": "https://deno.land/x/b.ts" } ], "size": 123, "mediaType": "TypeScript", "local": "/cache/deps/https/deno.land/x/a.ts", "checksum": "abcdef", "emit": "/cache/emit/https/deno.land/x/a.js" }, { "specifier": "https://deno.land/x/b.ts", "dependencies": [ { "specifier": "./c.js", "code": "https://deno.land/x/c.js", "type": "https://deno.land/x/c.d.ts" } ], "size": 456, "mediaType": "TypeScript", "local": "/cache/deps/https/deno.land/x/b.ts", "checksum": "def123", "emit": "/cache/emit/https/deno.land/x/b.js" }, { "specifier": "https://deno.land/x/c.js", "dependencies": [ { "specifier": "./d.js", "code": "https://deno.land/x/d.js" } ], "size": 789, "mediaType": "JavaScript", "local": "/cache/deps/https/deno.land/x/c.js", "checksum": "9876abcef" }, { "specifier": "https://deno.land/x/c.d.ts", "dependencies": [], "size": 999, "mediaType": "Dts", "local": "/cache/deps/https/deno.land/x/c.d.ts", "checksum": "a2b3c4d5" }, { "specifier": "https://deno.land/x/d.js", "dependencies": [], "typeDependency": { "specifier": "/x/d.d.ts", "type": "https://deno.land/x/d.d.ts" }, "size": 987, "mediaType": "JavaScript", "local": "/cache/deps/https/deno.land/x/d.js", "checksum": "5k6j7h8g" }, { "specifier": "https://deno.land/x/d.d.ts", "dependencies": [], "size": 67, "mediaType": "Dts", "local": "/cache/deps/https/deno.land/x/d.d.ts", "checksum": "0h0h0h0h0h" } ], "size": 99999 }) ); } }