mirror of
https://github.com/denoland/deno.git
synced 2024-11-26 16:09:27 -05:00
7d151efc68
Fixes #11476
517 lines
16 KiB
Rust
517 lines
16 KiB
Rust
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use crate::colors;
|
|
use crate::media_type::serialize_media_type;
|
|
use crate::media_type::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<ModuleSpecifier>,
|
|
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
|
pub maybe_type: Option<ModuleSpecifier>,
|
|
}
|
|
|
|
fn is_false(b: &bool) -> bool {
|
|
!b
|
|
}
|
|
|
|
impl ModuleGraphInfoDep {
|
|
fn write_info<S: AsRef<str> + fmt::Display + Clone>(
|
|
&self,
|
|
f: &mut fmt::Formatter,
|
|
prefix: S,
|
|
last: bool,
|
|
modules: &[ModuleGraphInfoMod],
|
|
seen: &mut HashSet<ModuleSpecifier>,
|
|
) -> 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<ModuleGraphInfoDep>,
|
|
#[serde(rename = "typeDependency", skip_serializing_if = "Option::is_none")]
|
|
pub maybe_type_dependency: Option<ModuleGraphInfoDep>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub size: Option<usize>,
|
|
#[serde(
|
|
skip_serializing_if = "Option::is_none",
|
|
serialize_with = "serialize_media_type"
|
|
)]
|
|
pub media_type: Option<MediaType>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub local: Option<PathBuf>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub checksum: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub emit: Option<PathBuf>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub map: Option<PathBuf>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
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<S: AsRef<str> + fmt::Display>(
|
|
&self,
|
|
f: &mut fmt::Formatter,
|
|
prefix: S,
|
|
last: bool,
|
|
type_dep: bool,
|
|
modules: &[ModuleGraphInfoMod],
|
|
seen: &mut HashSet<ModuleSpecifier>,
|
|
) -> 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<ModuleGraphInfoMod>,
|
|
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<ModuleSpecifier>,
|
|
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::<f64>()
|
|
.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
|
|
})
|
|
);
|
|
}
|
|
}
|