From 9eaa1fb71d03679367ebca0e0361fa0e47a1274f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 19 Nov 2020 19:19:34 +0100 Subject: refactor(cli): move tooling to cli/tools/ (#8424) This commit moves following tools into a single "tools" module located at "cli/tools/mod.rs": - formatter - linter - test runner - coverage collector - installer - binary upgrader - repl --- cli/coverage.rs | 238 -------------- cli/fmt.rs | 283 ----------------- cli/installer.rs | 813 ----------------------------------------------- cli/lint.rs | 364 --------------------- cli/main.rs | 42 ++- cli/repl.rs | 612 ----------------------------------- cli/test_runner.rs | 173 ---------- cli/tools/coverage.rs | 238 ++++++++++++++ cli/tools/fmt.rs | 283 +++++++++++++++++ cli/tools/installer.rs | 813 +++++++++++++++++++++++++++++++++++++++++++++++ cli/tools/lint.rs | 364 +++++++++++++++++++++ cli/tools/mod.rs | 9 + cli/tools/repl.rs | 612 +++++++++++++++++++++++++++++++++++ cli/tools/test_runner.rs | 173 ++++++++++ cli/tools/upgrade.rs | 276 ++++++++++++++++ cli/upgrade.rs | 276 ---------------- 16 files changed, 2788 insertions(+), 2781 deletions(-) delete mode 100644 cli/coverage.rs delete mode 100644 cli/fmt.rs delete mode 100644 cli/installer.rs delete mode 100644 cli/lint.rs delete mode 100644 cli/repl.rs delete mode 100644 cli/test_runner.rs create mode 100644 cli/tools/coverage.rs create mode 100644 cli/tools/fmt.rs create mode 100644 cli/tools/installer.rs create mode 100644 cli/tools/lint.rs create mode 100644 cli/tools/mod.rs create mode 100644 cli/tools/repl.rs create mode 100644 cli/tools/test_runner.rs create mode 100644 cli/tools/upgrade.rs delete mode 100644 cli/upgrade.rs (limited to 'cli') diff --git a/cli/coverage.rs b/cli/coverage.rs deleted file mode 100644 index 85ba3f559..000000000 --- a/cli/coverage.rs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::colors; -use crate::inspector::InspectorSession; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::url::Url; -use serde::Deserialize; - -pub struct CoverageCollector { - session: Box, -} - -impl CoverageCollector { - pub fn new(session: Box) -> Self { - Self { session } - } - - pub async fn start_collecting(&mut self) -> Result<(), AnyError> { - self.session.post_message("Debugger.enable", None).await?; - - self.session.post_message("Profiler.enable", None).await?; - - self - .session - .post_message( - "Profiler.startPreciseCoverage", - Some(json!({"callCount": true, "detailed": true})), - ) - .await?; - - Ok(()) - } - - pub async fn collect(&mut self) -> Result, AnyError> { - let result = self - .session - .post_message("Profiler.takePreciseCoverage", None) - .await?; - - let take_coverage_result: TakePreciseCoverageResult = - serde_json::from_value(result)?; - - let mut coverages: Vec = Vec::new(); - for script_coverage in take_coverage_result.result { - let result = self - .session - .post_message( - "Debugger.getScriptSource", - Some(json!({ - "scriptId": script_coverage.script_id, - })), - ) - .await?; - - let get_script_source_result: GetScriptSourceResult = - serde_json::from_value(result)?; - - coverages.push(Coverage { - script_coverage, - script_source: get_script_source_result.script_source, - }) - } - - Ok(coverages) - } - - pub async fn stop_collecting(&mut self) -> Result<(), AnyError> { - self - .session - .post_message("Profiler.stopPreciseCoverage", None) - .await?; - self.session.post_message("Profiler.disable", None).await?; - self.session.post_message("Debugger.disable", None).await?; - - Ok(()) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CoverageRange { - pub start_offset: usize, - pub end_offset: usize, - pub count: usize, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FunctionCoverage { - pub function_name: String, - pub ranges: Vec, - pub is_block_coverage: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ScriptCoverage { - pub script_id: String, - pub url: String, - pub functions: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Coverage { - pub script_coverage: ScriptCoverage, - pub script_source: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TakePreciseCoverageResult { - result: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetScriptSourceResult { - pub script_source: String, - pub bytecode: Option, -} - -pub struct PrettyCoverageReporter { - quiet: bool, -} - -// TODO(caspervonb) add support for lcov output (see geninfo(1) for format spec). -impl PrettyCoverageReporter { - pub fn new(quiet: bool) -> PrettyCoverageReporter { - PrettyCoverageReporter { quiet } - } - - pub fn visit_coverage(&mut self, coverage: &Coverage) { - let lines = coverage.script_source.lines().collect::>(); - - let mut covered_lines: Vec = Vec::new(); - let mut uncovered_lines: Vec = Vec::new(); - - let mut line_start_offset = 0; - for (index, line) in lines.iter().enumerate() { - let line_end_offset = line_start_offset + line.len(); - - let mut count = 0; - for function in &coverage.script_coverage.functions { - for range in &function.ranges { - if range.start_offset <= line_start_offset - && range.end_offset >= line_end_offset - { - if range.count == 0 { - count = 0; - break; - } - - count += range.count; - } - } - - line_start_offset = line_end_offset; - } - if count > 0 { - covered_lines.push(index); - } else { - uncovered_lines.push(index); - } - } - - if !self.quiet { - print!("cover {} ... ", coverage.script_coverage.url); - - let line_coverage_ratio = covered_lines.len() as f32 / lines.len() as f32; - let line_coverage = format!( - "{:.3}% ({}/{})", - line_coverage_ratio * 100.0, - covered_lines.len(), - lines.len() - ); - - if line_coverage_ratio >= 0.9 { - println!("{}", colors::green(&line_coverage)); - } else if line_coverage_ratio >= 0.75 { - println!("{}", colors::yellow(&line_coverage)); - } else { - println!("{}", colors::red(&line_coverage)); - } - - for line_index in uncovered_lines { - println!( - "{:width$}{} {}", - line_index + 1, - colors::gray(" |"), - colors::red(&lines[line_index]), - width = 4 - ); - } - } - } -} - -pub fn filter_script_coverages( - coverages: Vec, - test_file_url: Url, - test_modules: Vec, -) -> Vec { - coverages - .into_iter() - .filter(|e| { - if let Ok(url) = Url::parse(&e.script_coverage.url) { - if url.path().ends_with("__anonymous__") { - return false; - } - - if url == test_file_url { - return false; - } - - for test_module_url in &test_modules { - if &url == test_module_url { - return false; - } - } - - if let Ok(path) = url.to_file_path() { - for test_module_url in &test_modules { - if let Ok(test_module_path) = test_module_url.to_file_path() { - if path.starts_with(test_module_path.parent().unwrap()) { - return true; - } - } - } - } - } - - false - }) - .collect::>() -} diff --git a/cli/fmt.rs b/cli/fmt.rs deleted file mode 100644 index 0036436c1..000000000 --- a/cli/fmt.rs +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -//! This module provides file formatting utilities using -//! [`dprint-plugin-typescript`](https://github.com/dprint/dprint-plugin-typescript). -//! -//! At the moment it is only consumed using CLI but in -//! the future it can be easily extended to provide -//! the same functions as ops available in JS runtime. - -use crate::colors; -use crate::diff::diff; -use crate::fs_util::{collect_files, is_supported_ext}; -use crate::text_encoding; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::futures; -use dprint_plugin_typescript as dprint; -use std::fs; -use std::io::stdin; -use std::io::stdout; -use std::io::Read; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; - -const BOM_CHAR: char = '\u{FEFF}'; - -/// Format JavaScript/TypeScript files. -/// -/// First argument and ignore supports globs, and if it is `None` -/// then the current directory is recursively walked. -pub async fn format( - args: Vec, - check: bool, - exclude: Vec, -) -> Result<(), AnyError> { - if args.len() == 1 && args[0].to_string_lossy() == "-" { - return format_stdin(check); - } - // collect the files that are to be formatted - let target_files = collect_files(args, exclude, is_supported_ext)?; - let config = get_config(); - if check { - check_source_files(config, target_files).await - } else { - format_source_files(config, target_files).await - } -} - -async fn check_source_files( - config: dprint::configuration::Configuration, - paths: Vec, -) -> Result<(), AnyError> { - let not_formatted_files_count = Arc::new(AtomicUsize::new(0)); - let checked_files_count = Arc::new(AtomicUsize::new(0)); - - // prevent threads outputting at the same time - let output_lock = Arc::new(Mutex::new(0)); - - run_parallelized(paths, { - let not_formatted_files_count = not_formatted_files_count.clone(); - let checked_files_count = checked_files_count.clone(); - move |file_path| { - checked_files_count.fetch_add(1, Ordering::Relaxed); - let file_text = read_file_contents(&file_path)?.text; - let r = dprint::format_text(&file_path, &file_text, &config); - match r { - Ok(formatted_text) => { - if formatted_text != file_text { - not_formatted_files_count.fetch_add(1, Ordering::Relaxed); - let _g = output_lock.lock().unwrap(); - let diff = diff(&file_text, &formatted_text); - info!(""); - info!("{} {}:", colors::bold("from"), file_path.display()); - info!("{}", diff); - } - } - Err(e) => { - let _g = output_lock.lock().unwrap(); - eprintln!("Error checking: {}", file_path.to_string_lossy()); - eprintln!(" {}", e); - } - } - Ok(()) - } - }) - .await?; - - let not_formatted_files_count = - not_formatted_files_count.load(Ordering::Relaxed); - let checked_files_count = checked_files_count.load(Ordering::Relaxed); - let checked_files_str = - format!("{} {}", checked_files_count, files_str(checked_files_count)); - if not_formatted_files_count == 0 { - info!("Checked {}", checked_files_str); - Ok(()) - } else { - let not_formatted_files_str = files_str(not_formatted_files_count); - Err(generic_error(format!( - "Found {} not formatted {} in {}", - not_formatted_files_count, not_formatted_files_str, checked_files_str, - ))) - } -} - -async fn format_source_files( - config: dprint::configuration::Configuration, - paths: Vec, -) -> Result<(), AnyError> { - let formatted_files_count = Arc::new(AtomicUsize::new(0)); - let checked_files_count = Arc::new(AtomicUsize::new(0)); - let output_lock = Arc::new(Mutex::new(0)); // prevent threads outputting at the same time - - run_parallelized(paths, { - let formatted_files_count = formatted_files_count.clone(); - let checked_files_count = checked_files_count.clone(); - move |file_path| { - checked_files_count.fetch_add(1, Ordering::Relaxed); - let file_contents = read_file_contents(&file_path)?; - let r = dprint::format_text(&file_path, &file_contents.text, &config); - match r { - Ok(formatted_text) => { - if formatted_text != file_contents.text { - write_file_contents( - &file_path, - FileContents { - had_bom: file_contents.had_bom, - text: formatted_text, - }, - )?; - formatted_files_count.fetch_add(1, Ordering::Relaxed); - let _g = output_lock.lock().unwrap(); - info!("{}", file_path.to_string_lossy()); - } - } - Err(e) => { - let _g = output_lock.lock().unwrap(); - eprintln!("Error formatting: {}", file_path.to_string_lossy()); - eprintln!(" {}", e); - } - } - Ok(()) - } - }) - .await?; - - let formatted_files_count = formatted_files_count.load(Ordering::Relaxed); - debug!( - "Formatted {} {}", - formatted_files_count, - files_str(formatted_files_count), - ); - - let checked_files_count = checked_files_count.load(Ordering::Relaxed); - info!( - "Checked {} {}", - checked_files_count, - files_str(checked_files_count) - ); - - Ok(()) -} - -/// Format stdin and write result to stdout. -/// Treats input as TypeScript. -/// Compatible with `--check` flag. -fn format_stdin(check: bool) -> Result<(), AnyError> { - let mut source = String::new(); - if stdin().read_to_string(&mut source).is_err() { - return Err(generic_error("Failed to read from stdin")); - } - let config = get_config(); - - // dprint will fallback to jsx parsing if parsing this as a .ts file doesn't work - match dprint::format_text(&PathBuf::from("_stdin.ts"), &source, &config) { - Ok(formatted_text) => { - if check { - if formatted_text != source { - println!("Not formatted stdin"); - } - } else { - stdout().write_all(formatted_text.as_bytes())?; - } - } - Err(e) => { - return Err(generic_error(e)); - } - } - Ok(()) -} - -fn files_str(len: usize) -> &'static str { - if len <= 1 { - "file" - } else { - "files" - } -} - -fn get_config() -> dprint::configuration::Configuration { - use dprint::configuration::*; - ConfigurationBuilder::new().deno().build() -} - -struct FileContents { - text: String, - had_bom: bool, -} - -fn read_file_contents(file_path: &Path) -> Result { - let file_bytes = fs::read(&file_path)?; - let charset = text_encoding::detect_charset(&file_bytes); - let file_text = text_encoding::convert_to_utf8(&file_bytes, charset)?; - let had_bom = file_text.starts_with(BOM_CHAR); - let text = if had_bom { - // remove the BOM - String::from(&file_text[BOM_CHAR.len_utf8()..]) - } else { - String::from(file_text) - }; - - Ok(FileContents { text, had_bom }) -} - -fn write_file_contents( - file_path: &Path, - file_contents: FileContents, -) -> Result<(), AnyError> { - let file_text = if file_contents.had_bom { - // add back the BOM - format!("{}{}", BOM_CHAR, file_contents.text) - } else { - file_contents.text - }; - - Ok(fs::write(file_path, file_text)?) -} - -pub async fn run_parallelized( - file_paths: Vec, - f: F, -) -> Result<(), AnyError> -where - F: FnOnce(PathBuf) -> Result<(), AnyError> + Send + 'static + Clone, -{ - let handles = file_paths.iter().map(|file_path| { - let f = f.clone(); - let file_path = file_path.clone(); - tokio::task::spawn_blocking(move || f(file_path)) - }); - let join_results = futures::future::join_all(handles).await; - - // find the tasks that panicked and let the user know which files - let panic_file_paths = join_results - .iter() - .enumerate() - .filter_map(|(i, join_result)| { - join_result - .as_ref() - .err() - .map(|_| file_paths[i].to_string_lossy()) - }) - .collect::>(); - if !panic_file_paths.is_empty() { - panic!("Panic formatting: {}", panic_file_paths.join(", ")) - } - - // check for any errors and if so return the first one - let mut errors = join_results.into_iter().filter_map(|join_result| { - join_result - .ok() - .map(|handle_result| handle_result.err()) - .flatten() - }); - - if let Some(e) = errors.next() { - Err(e) - } else { - Ok(()) - } -} diff --git a/cli/installer.rs b/cli/installer.rs deleted file mode 100644 index e0a99873a..000000000 --- a/cli/installer.rs +++ /dev/null @@ -1,813 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::flags::Flags; -use crate::fs_util::canonicalize_path; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::url::Url; -use log::Level; -use regex::{Regex, RegexBuilder}; -use std::env; -use std::fs; -use std::fs::File; -use std::io; -use std::io::Write; -#[cfg(not(windows))] -use std::os::unix::fs::PermissionsExt; -use std::path::PathBuf; - -lazy_static! { - static ref EXEC_NAME_RE: Regex = RegexBuilder::new( - r"^[a-z][\w-]*$" - ).case_insensitive(true).build().unwrap(); - // Regular expression to test disk driver letter. eg "C:\\User\username\path\to" - static ref DRIVE_LETTER_REG: Regex = RegexBuilder::new( - r"^[c-z]:" - ).case_insensitive(true).build().unwrap(); -} - -pub fn is_remote_url(module_url: &str) -> bool { - let lower = module_url.to_lowercase(); - lower.starts_with("http://") || lower.starts_with("https://") -} - -fn validate_name(exec_name: &str) -> Result<(), AnyError> { - if EXEC_NAME_RE.is_match(exec_name) { - Ok(()) - } else { - Err(generic_error(format!( - "Invalid executable name: {}", - exec_name - ))) - } -} - -#[cfg(windows)] -/// On Windows if user is using Powershell .cmd extension is need to run the -/// installed module. -/// Generate batch script to satisfy that. -fn generate_executable_file( - file_path: PathBuf, - args: Vec, -) -> Result<(), AnyError> { - let args: Vec = args.iter().map(|c| format!("\"{}\"", c)).collect(); - let template = format!( - "% generated by deno install %\n@deno.exe {} %*\n", - args.join(" ") - ); - let mut file = File::create(&file_path)?; - file.write_all(template.as_bytes())?; - Ok(()) -} - -#[cfg(not(windows))] -fn generate_executable_file( - file_path: PathBuf, - args: Vec, -) -> Result<(), AnyError> { - use shell_escape::escape; - let args: Vec = args - .into_iter() - .map(|c| escape(c.into()).into_owned()) - .collect(); - let template = format!( - r#"#!/bin/sh -# generated by deno install -exec deno {} "$@" -"#, - args.join(" "), - ); - let mut file = File::create(&file_path)?; - file.write_all(template.as_bytes())?; - let _metadata = fs::metadata(&file_path)?; - let mut permissions = _metadata.permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&file_path, permissions)?; - Ok(()) -} - -fn get_installer_root() -> Result { - if let Ok(env_dir) = env::var("DENO_INSTALL_ROOT") { - if !env_dir.is_empty() { - return canonicalize_path(&PathBuf::from(env_dir)); - } - } - // Note: on Windows, the $HOME environment variable may be set by users or by - // third party software, but it is non-standard and should not be relied upon. - let home_env_var = if cfg!(windows) { "USERPROFILE" } else { "HOME" }; - let mut home_path = - env::var_os(home_env_var) - .map(PathBuf::from) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - format!("${} is not defined", home_env_var), - ) - })?; - home_path.push(".deno"); - Ok(home_path) -} - -fn infer_name_from_url(url: &Url) -> Option { - let path = PathBuf::from(url.path()); - let mut stem = match path.file_stem() { - Some(stem) => stem.to_string_lossy().to_string(), - None => return None, - }; - if stem == "main" || stem == "mod" || stem == "index" || stem == "cli" { - if let Some(parent_name) = path.parent().and_then(|p| p.file_name()) { - stem = parent_name.to_string_lossy().to_string(); - } - } - let stem = stem.splitn(2, '@').next().unwrap().to_string(); - Some(stem) -} - -pub fn install( - flags: Flags, - module_url: &str, - args: Vec, - name: Option, - root: Option, - force: bool, -) -> Result<(), AnyError> { - let root = if let Some(root) = root { - canonicalize_path(&root)? - } else { - get_installer_root()? - }; - let installation_dir = root.join("bin"); - - // ensure directory exists - if let Ok(metadata) = fs::metadata(&installation_dir) { - if !metadata.is_dir() { - return Err(generic_error("Installation path is not a directory")); - } - } else { - fs::create_dir_all(&installation_dir)?; - }; - - // Check if module_url is remote - let module_url = if is_remote_url(module_url) { - Url::parse(module_url).expect("Should be valid url") - } else { - let module_path = PathBuf::from(module_url); - let module_path = if module_path.is_absolute() { - module_path - } else { - let cwd = env::current_dir().unwrap(); - cwd.join(module_path) - }; - Url::from_file_path(module_path).expect("Path should be absolute") - }; - - let name = name.or_else(|| infer_name_from_url(&module_url)); - - let name = match name { - Some(name) => name, - None => return Err(generic_error( - "An executable name was not provided. One could not be inferred from the URL. Aborting.", - )), - }; - - validate_name(name.as_str())?; - let mut file_path = installation_dir.join(&name); - - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - if file_path.exists() && !force { - return Err(generic_error( - "Existing installation found. Aborting (Use -f to overwrite).", - )); - }; - - let mut extra_files: Vec<(PathBuf, String)> = vec![]; - - let mut executable_args = vec!["run".to_string()]; - executable_args.extend_from_slice(&flags.to_permission_args()); - if let Some(ca_file) = flags.ca_file { - executable_args.push("--cert".to_string()); - executable_args.push(ca_file) - } - if let Some(log_level) = flags.log_level { - if log_level == Level::Error { - executable_args.push("--quiet".to_string()); - } else { - executable_args.push("--log-level".to_string()); - let log_level = match log_level { - Level::Debug => "debug", - Level::Info => "info", - _ => { - return Err(generic_error(format!("invalid log level {}", log_level))) - } - }; - executable_args.push(log_level.to_string()); - } - } - - if flags.no_check { - executable_args.push("--no-check".to_string()); - } - - if flags.unstable { - executable_args.push("--unstable".to_string()); - } - - if flags.no_remote { - executable_args.push("--no-remote".to_string()); - } - - if flags.lock_write { - executable_args.push("--lock-write".to_string()); - } - - if flags.cached_only { - executable_args.push("--cached_only".to_string()); - } - - if let Some(v8_flags) = flags.v8_flags { - executable_args.push(format!("--v8-flags={}", v8_flags.join(","))); - } - - if let Some(seed) = flags.seed { - executable_args.push("--seed".to_string()); - executable_args.push(seed.to_string()); - } - - if let Some(inspect) = flags.inspect { - executable_args.push(format!("--inspect={}", inspect.to_string())); - } - - if let Some(inspect_brk) = flags.inspect_brk { - executable_args.push(format!("--inspect-brk={}", inspect_brk.to_string())); - } - - if let Some(import_map_path) = flags.import_map_path { - let mut copy_path = file_path.clone(); - copy_path.set_extension("import_map.json"); - executable_args.push("--import-map".to_string()); - executable_args.push(copy_path.to_str().unwrap().to_string()); - extra_files.push((copy_path, fs::read_to_string(import_map_path)?)); - } - - if let Some(config_path) = flags.config_path { - let mut copy_path = file_path.clone(); - copy_path.set_extension("tsconfig.json"); - executable_args.push("--config".to_string()); - executable_args.push(copy_path.to_str().unwrap().to_string()); - extra_files.push((copy_path, fs::read_to_string(config_path)?)); - } - - if let Some(lock_path) = flags.lock { - let mut copy_path = file_path.clone(); - copy_path.set_extension("lock.json"); - executable_args.push("--lock".to_string()); - executable_args.push(copy_path.to_str().unwrap().to_string()); - extra_files.push((copy_path, fs::read_to_string(lock_path)?)); - } - - executable_args.push(module_url.to_string()); - executable_args.extend_from_slice(&args); - - generate_executable_file(file_path.to_owned(), executable_args)?; - for (path, contents) in extra_files { - fs::write(path, contents)?; - } - - println!("✅ Successfully installed {}", name); - println!("{}", file_path.to_string_lossy()); - let installation_dir_str = installation_dir.to_string_lossy(); - - if !is_in_path(&installation_dir) { - println!("ℹ️ Add {} to PATH", installation_dir_str); - if cfg!(windows) { - println!(" set PATH=%PATH%;{}", installation_dir_str); - } else { - println!(" export PATH=\"{}:$PATH\"", installation_dir_str); - } - } - - Ok(()) -} - -fn is_in_path(dir: &PathBuf) -> bool { - if let Some(paths) = env::var_os("PATH") { - for p in env::split_paths(&paths) { - if *dir == p { - return true; - } - } - } - false -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::Mutex; - use tempfile::TempDir; - - lazy_static! { - pub static ref ENV_LOCK: Mutex<()> = Mutex::new(()); - } - - #[test] - fn test_is_remote_url() { - assert!(is_remote_url("https://deno.land/std/http/file_server.ts")); - assert!(is_remote_url("http://deno.land/std/http/file_server.ts")); - assert!(is_remote_url("HTTP://deno.land/std/http/file_server.ts")); - assert!(is_remote_url("HTTp://deno.land/std/http/file_server.ts")); - assert!(!is_remote_url("file:///dev/deno_std/http/file_server.ts")); - assert!(!is_remote_url("./dev/deno_std/http/file_server.ts")); - } - - #[test] - fn install_infer_name_from_url() { - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc/server.ts").unwrap() - ), - Some("server".to_string()) - ); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc/main.ts").unwrap() - ), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc/mod.ts").unwrap() - ), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc/index.ts").unwrap() - ), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc/cli.ts").unwrap() - ), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url(&Url::parse("https://example.com/main.ts").unwrap()), - Some("main".to_string()) - ); - assert_eq!( - infer_name_from_url(&Url::parse("https://example.com").unwrap()), - None - ); - assert_eq!( - infer_name_from_url(&Url::parse("file:///abc/server.ts").unwrap()), - Some("server".to_string()) - ); - assert_eq!( - infer_name_from_url(&Url::parse("file:///abc/main.ts").unwrap()), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url(&Url::parse("file:///main.ts").unwrap()), - Some("main".to_string()) - ); - assert_eq!(infer_name_from_url(&Url::parse("file:///").unwrap()), None); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc@0.1.0").unwrap() - ), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc@0.1.0/main.ts").unwrap() - ), - Some("abc".to_string()) - ); - assert_eq!( - infer_name_from_url( - &Url::parse("https://example.com/abc@def@ghi").unwrap() - ), - Some("abc".to_string()) - ); - } - - #[test] - fn install_basic() { - let _guard = ENV_LOCK.lock().ok(); - let temp_dir = TempDir::new().expect("tempdir fail"); - let temp_dir_str = temp_dir.path().to_string_lossy().to_string(); - // NOTE: this test overrides environmental variables - // don't add other tests in this file that mess with "HOME" and "USEPROFILE" - // otherwise transient failures are possible because tests are run in parallel. - // It means that other test can override env vars when this test is running. - let original_home = env::var_os("HOME"); - let original_user_profile = env::var_os("HOME"); - let original_install_root = env::var_os("DENO_INSTALL_ROOT"); - env::set_var("HOME", &temp_dir_str); - env::set_var("USERPROFILE", &temp_dir_str); - env::set_var("DENO_INSTALL_ROOT", ""); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/echo_server.ts", - vec![], - Some("echo_test".to_string()), - None, - false, - ) - .expect("Install failed"); - - let mut file_path = temp_dir.path().join(".deno/bin/echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - - let content = fs::read_to_string(file_path).unwrap(); - // It's annoying when shell scripts don't have NL at the end. - assert_eq!(content.chars().last().unwrap(), '\n'); - - if cfg!(windows) { - assert!(content - .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); - } else { - assert!(content - .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); - } - if let Some(home) = original_home { - env::set_var("HOME", home); - } - if let Some(user_profile) = original_user_profile { - env::set_var("USERPROFILE", user_profile); - } - if let Some(install_root) = original_install_root { - env::set_var("DENO_INSTALL_ROOT", install_root); - } - } - - #[test] - fn install_unstable() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags { - unstable: true, - ..Flags::default() - }, - "http://localhost:4545/cli/tests/echo_server.ts", - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - - let content = fs::read_to_string(file_path).unwrap(); - println!("this is the file path {:?}", content); - if cfg!(windows) { - assert!(content.contains( - r#""run" "--unstable" "http://localhost:4545/cli/tests/echo_server.ts""# - )); - } else { - assert!(content.contains( - r#"run --unstable 'http://localhost:4545/cli/tests/echo_server.ts'"# - )); - } - } - - #[test] - fn install_inferred_name() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/echo_server.ts", - vec![], - None, - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_server"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - if cfg!(windows) { - assert!(content - .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); - } else { - assert!(content - .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); - } - } - - #[test] - fn install_inferred_name_from_parent() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/subdir/main.ts", - vec![], - None, - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("subdir"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - if cfg!(windows) { - assert!(content - .contains(r#""run" "http://localhost:4545/cli/tests/subdir/main.ts""#)); - } else { - assert!(content - .contains(r#"run 'http://localhost:4545/cli/tests/subdir/main.ts'"#)); - } - } - - #[test] - fn install_custom_dir_option() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/echo_server.ts", - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - if cfg!(windows) { - assert!(content - .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); - } else { - assert!(content - .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); - } - } - - #[test] - fn install_custom_dir_env_var() { - let _guard = ENV_LOCK.lock().ok(); - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - let original_install_root = env::var_os("DENO_INSTALL_ROOT"); - env::set_var("DENO_INSTALL_ROOT", temp_dir.path().to_path_buf()); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/echo_server.ts", - vec![], - Some("echo_test".to_string()), - None, - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - if cfg!(windows) { - assert!(content - .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); - } else { - assert!(content - .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); - } - if let Some(install_root) = original_install_root { - env::set_var("DENO_INSTALL_ROOT", install_root); - } - } - - #[test] - fn install_with_flags() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags { - allow_net: true, - allow_read: true, - no_check: true, - log_level: Some(Level::Error), - ..Flags::default() - }, - "http://localhost:4545/cli/tests/echo_server.ts", - vec!["--foobar".to_string()], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - if cfg!(windows) { - assert!(content.contains(r#""run" "--allow-read" "--allow-net" "--quiet" "--no-check" "http://localhost:4545/cli/tests/echo_server.ts" "--foobar""#)); - } else { - assert!(content.contains(r#"run --allow-read --allow-net --quiet --no-check 'http://localhost:4545/cli/tests/echo_server.ts' --foobar"#)); - } - } - - #[test] - fn install_local_module() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - let local_module = env::current_dir().unwrap().join("echo_server.ts"); - let local_module_url = Url::from_file_path(&local_module).unwrap(); - let local_module_str = local_module.to_string_lossy(); - - install( - Flags::default(), - &local_module_str, - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - assert!(content.contains(&local_module_url.to_string())); - } - - #[test] - fn install_force() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/echo_server.ts", - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - assert!(file_path.exists()); - - // No force. Install failed. - let no_force_result = install( - Flags::default(), - "http://localhost:4545/cli/tests/cat.ts", // using a different URL - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ); - assert!(no_force_result.is_err()); - assert!(no_force_result - .unwrap_err() - .to_string() - .contains("Existing installation found")); - // Assert not modified - let file_content = fs::read_to_string(&file_path).unwrap(); - assert!(file_content.contains("echo_server.ts")); - - // Force. Install success. - let force_result = install( - Flags::default(), - "http://localhost:4545/cli/tests/cat.ts", // using a different URL - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - true, - ); - assert!(force_result.is_ok()); - // Assert modified - let file_content_2 = fs::read_to_string(&file_path).unwrap(); - assert!(file_content_2.contains("cat.ts")); - } - - #[test] - fn install_with_config() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - let config_file_path = temp_dir.path().join("test_tsconfig.json"); - let config = "{}"; - let mut config_file = File::create(&config_file_path).unwrap(); - let result = config_file.write_all(config.as_bytes()); - assert!(result.is_ok()); - - let result = install( - Flags { - config_path: Some(config_file_path.to_string_lossy().to_string()), - ..Flags::default() - }, - "http://localhost:4545/cli/tests/cat.ts", - vec![], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - true, - ); - eprintln!("result {:?}", result); - assert!(result.is_ok()); - - let config_file_name = "echo_test.tsconfig.json"; - - let file_path = bin_dir.join(config_file_name.to_string()); - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - assert!(content == "{}"); - } - - // TODO: enable on Windows after fixing batch escaping - #[cfg(not(windows))] - #[test] - fn install_shell_escaping() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let bin_dir = temp_dir.path().join("bin"); - std::fs::create_dir(&bin_dir).unwrap(); - - install( - Flags::default(), - "http://localhost:4545/cli/tests/echo_server.ts", - vec!["\"".to_string()], - Some("echo_test".to_string()), - Some(temp_dir.path().to_path_buf()), - false, - ) - .expect("Install failed"); - - let mut file_path = bin_dir.join("echo_test"); - if cfg!(windows) { - file_path = file_path.with_extension("cmd"); - } - - assert!(file_path.exists()); - let content = fs::read_to_string(file_path).unwrap(); - println!("{}", content); - if cfg!(windows) { - // TODO: see comment above this test - } else { - assert!(content.contains( - r#"run 'http://localhost:4545/cli/tests/echo_server.ts' '"'"# - )); - } - } -} diff --git a/cli/lint.rs b/cli/lint.rs deleted file mode 100644 index e319a7be6..000000000 --- a/cli/lint.rs +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -//! This module provides file formating utilities using -//! [`deno_lint`](https://github.com/denoland/deno_lint). -//! -//! At the moment it is only consumed using CLI but in -//! the future it can be easily extended to provide -//! the same functions as ops available in JS runtime. -use crate::ast; -use crate::colors; -use crate::fmt::run_parallelized; -use crate::fmt_errors; -use crate::fs_util::{collect_files, is_supported_ext}; -use crate::media_type::MediaType; -use deno_core::error::{generic_error, AnyError, JsStackFrame}; -use deno_core::serde_json; -use deno_lint::diagnostic::LintDiagnostic; -use deno_lint::linter::Linter; -use deno_lint::linter::LinterBuilder; -use deno_lint::rules; -use deno_lint::rules::LintRule; -use serde::Serialize; -use std::fs; -use std::io::{stdin, Read}; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use swc_ecmascript::parser::Syntax; - -pub enum LintReporterKind { - Pretty, - Json, -} - -fn create_reporter(kind: LintReporterKind) -> Box { - match kind { - LintReporterKind::Pretty => Box::new(PrettyLintReporter::new()), - LintReporterKind::Json => Box::new(JsonLintReporter::new()), - } -} - -pub async fn lint_files( - args: Vec, - ignore: Vec, - json: bool, -) -> Result<(), AnyError> { - if args.len() == 1 && args[0].to_string_lossy() == "-" { - return lint_stdin(json); - } - let target_files = collect_files(args, ignore, is_supported_ext)?; - debug!("Found {} files", target_files.len()); - let target_files_len = target_files.len(); - - let has_error = Arc::new(AtomicBool::new(false)); - - let reporter_kind = if json { - LintReporterKind::Json - } else { - LintReporterKind::Pretty - }; - let reporter_lock = Arc::new(Mutex::new(create_reporter(reporter_kind))); - - run_parallelized(target_files, { - let reporter_lock = reporter_lock.clone(); - let has_error = has_error.clone(); - move |file_path| { - let r = lint_file(file_path.clone()); - let mut reporter = reporter_lock.lock().unwrap(); - - match r { - Ok((mut file_diagnostics, source)) => { - sort_diagnostics(&mut file_diagnostics); - for d in file_diagnostics.iter() { - has_error.store(true, Ordering::Relaxed); - reporter.visit_diagnostic(&d, source.split('\n').collect()); - } - } - Err(err) => { - has_error.store(true, Ordering::Relaxed); - reporter.visit_error(&file_path.to_string_lossy().to_string(), &err); - } - } - Ok(()) - } - }) - .await?; - - let has_error = has_error.load(Ordering::Relaxed); - - reporter_lock.lock().unwrap().close(target_files_len); - - if has_error { - std::process::exit(1); - } - - Ok(()) -} - -fn rule_to_json(rule: Box) -> serde_json::Value { - serde_json::json!({ - "code": rule.code(), - "tags": rule.tags(), - "docs": rule.docs(), - }) -} - -pub fn print_rules_list(json: bool) { - let lint_rules = rules::get_recommended_rules(); - - if json { - let json_rules: Vec = - lint_rules.into_iter().map(rule_to_json).collect(); - let json_str = serde_json::to_string_pretty(&json_rules).unwrap(); - println!("{}", json_str); - } else { - // The rules should still be printed even if `--quiet` option is enabled, - // so use `println!` here instead of `info!`. - println!("Available rules:"); - for rule in lint_rules { - println!(" - {}", rule.code()); - } - } -} - -fn create_linter(syntax: Syntax, rules: Vec>) -> Linter { - LinterBuilder::default() - .ignore_file_directive("deno-lint-ignore-file") - .ignore_diagnostic_directive("deno-lint-ignore") - .lint_unused_ignore_directives(true) - // TODO(bartlomieju): switch to true - .lint_unknown_rules(false) - .syntax(syntax) - .rules(rules) - .build() -} - -fn lint_file( - file_path: PathBuf, -) -> Result<(Vec, String), AnyError> { - let file_name = file_path.to_string_lossy().to_string(); - let source_code = fs::read_to_string(&file_path)?; - let media_type = MediaType::from(&file_path); - let syntax = ast::get_syntax(&media_type); - - let lint_rules = rules::get_recommended_rules(); - let mut linter = create_linter(syntax, lint_rules); - - let (_, file_diagnostics) = linter.lint(file_name, source_code.clone())?; - - Ok((file_diagnostics, source_code)) -} - -/// Lint stdin and write result to stdout. -/// Treats input as TypeScript. -/// Compatible with `--json` flag. -fn lint_stdin(json: bool) -> Result<(), AnyError> { - let mut source = String::new(); - if stdin().read_to_string(&mut source).is_err() { - return Err(generic_error("Failed to read from stdin")); - } - - let reporter_kind = if json { - LintReporterKind::Json - } else { - LintReporterKind::Pretty - }; - let mut reporter = create_reporter(reporter_kind); - let lint_rules = rules::get_recommended_rules(); - let syntax = ast::get_syntax(&MediaType::TypeScript); - let mut linter = create_linter(syntax, lint_rules); - let mut has_error = false; - let pseudo_file_name = "_stdin.ts"; - match linter - .lint(pseudo_file_name.to_string(), source.clone()) - .map_err(|e| e.into()) - { - Ok((_, diagnostics)) => { - for d in diagnostics { - has_error = true; - reporter.visit_diagnostic(&d, source.split('\n').collect()); - } - } - Err(err) => { - has_error = true; - reporter.visit_error(pseudo_file_name, &err); - } - } - - reporter.close(1); - - if has_error { - std::process::exit(1); - } - - Ok(()) -} - -trait LintReporter { - fn visit_diagnostic(&mut self, d: &LintDiagnostic, source_lines: Vec<&str>); - fn visit_error(&mut self, file_path: &str, err: &AnyError); - fn close(&mut self, check_count: usize); -} - -#[derive(Serialize)] -struct LintError { - file_path: String, - message: String, -} - -struct PrettyLintReporter { - lint_count: u32, -} - -impl PrettyLintReporter { - fn new() -> PrettyLintReporter { - PrettyLintReporter { lint_count: 0 } - } -} - -impl LintReporter for PrettyLintReporter { - fn visit_diagnostic(&mut self, d: &LintDiagnostic, source_lines: Vec<&str>) { - self.lint_count += 1; - - let pretty_message = - format!("({}) {}", colors::gray(&d.code), d.message.clone()); - - let message = format_diagnostic( - &pretty_message, - &source_lines, - d.range.clone(), - d.hint.as_ref(), - &fmt_errors::format_location(&JsStackFrame::from_location( - Some(d.filename.clone()), - Some(d.range.start.line as i64), - Some(d.range.start.col as i64), - )), - ); - - eprintln!("{}\n", message); - } - - fn visit_error(&mut self, file_path: &str, err: &AnyError) { - eprintln!("Error linting: {}", file_path); - eprintln!(" {}", err); - } - - fn close(&mut self, check_count: usize) { - match self.lint_count { - 1 => info!("Found 1 problem"), - n if n > 1 => info!("Found {} problems", self.lint_count), - _ => (), - } - - match check_count { - n if n <= 1 => info!("Checked {} file", n), - n if n > 1 => info!("Checked {} files", n), - _ => unreachable!(), - } - } -} - -pub fn format_diagnostic( - message_line: &str, - source_lines: &[&str], - range: deno_lint::diagnostic::Range, - maybe_hint: Option<&String>, - formatted_location: &str, -) -> String { - let mut lines = vec![]; - - for i in range.start.line..=range.end.line { - lines.push(source_lines[i - 1].to_string()); - if range.start.line == range.end.line { - lines.push(format!( - "{}{}", - " ".repeat(range.start.col), - colors::red(&"^".repeat(range.end.col - range.start.col)) - )); - } else { - let line_len = source_lines[i - 1].len(); - if range.start.line == i { - lines.push(format!( - "{}{}", - " ".repeat(range.start.col), - colors::red(&"^".repeat(line_len - range.start.col)) - )); - } else if range.end.line == i { - lines.push(colors::red(&"^".repeat(range.end.col)).to_string()); - } else if line_len != 0 { - lines.push(colors::red(&"^".repeat(line_len)).to_string()); - } - } - } - - if let Some(hint) = maybe_hint { - format!( - "{}\n{}\n at {}\n\n {} {}", - message_line, - lines.join("\n"), - formatted_location, - colors::gray("hint:"), - hint, - ) - } else { - format!( - "{}\n{}\n at {}", - message_line, - lines.join("\n"), - formatted_location - ) - } -} - -#[derive(Serialize)] -struct JsonLintReporter { - diagnostics: Vec, - errors: Vec, -} - -impl JsonLintReporter { - fn new() -> JsonLintReporter { - JsonLintReporter { - diagnostics: Vec::new(), - errors: Vec::new(), - } - } -} - -impl LintReporter for JsonLintReporter { - fn visit_diagnostic(&mut self, d: &LintDiagnostic, _source_lines: Vec<&str>) { - self.diagnostics.push(d.clone()); - } - - fn visit_error(&mut self, file_path: &str, err: &AnyError) { - self.errors.push(LintError { - file_path: file_path.to_string(), - message: err.to_string(), - }); - } - - fn close(&mut self, _check_count: usize) { - sort_diagnostics(&mut self.diagnostics); - let json = serde_json::to_string_pretty(&self); - eprintln!("{}", json.unwrap()); - } -} - -fn sort_diagnostics(diagnostics: &mut Vec) { - // Sort so that we guarantee a deterministic output which is useful for tests - diagnostics.sort_by(|a, b| { - use std::cmp::Ordering; - let file_order = a.filename.cmp(&b.filename); - match file_order { - Ordering::Equal => { - let line_order = a.range.start.line.cmp(&b.range.start.line); - match line_order { - Ordering::Equal => a.range.start.col.cmp(&b.range.start.col), - _ => line_order, - } - } - _ => file_order, - } - }); -} diff --git a/cli/main.rs b/cli/main.rs index 11674a8b6..160b98674 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -10,7 +10,6 @@ extern crate log; mod ast; mod checksum; mod colors; -mod coverage; mod deno_dir; mod diagnostics; mod diff; @@ -20,7 +19,6 @@ mod file_fetcher; mod file_watcher; mod flags; mod flags_allow_net; -mod fmt; mod fmt_errors; mod fs_util; mod http_cache; @@ -28,9 +26,7 @@ mod http_util; mod import_map; mod info; mod inspector; -mod installer; mod js; -mod lint; mod lockfile; mod media_type; mod metrics; @@ -39,22 +35,18 @@ mod module_loader; mod ops; mod permissions; mod program_state; -mod repl; mod resolve_addr; mod signal; mod source_maps; mod specifier_handler; -mod test_runner; mod text_encoding; mod tokio_util; +mod tools; mod tsc; mod tsc_config; -mod upgrade; mod version; mod worker; -use crate::coverage::CoverageCollector; -use crate::coverage::PrettyCoverageReporter; use crate::file_fetcher::File; use crate::file_fetcher::FileFetcher; use crate::media_type::MediaType; @@ -88,7 +80,6 @@ use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; -use upgrade::upgrade_command; fn write_to_stdout_ignore_sigpipe(bytes: &[u8]) -> Result<(), std::io::Error> { use std::io::ErrorKind; @@ -213,7 +204,7 @@ async fn install_command( MainWorker::new(&program_state, main_module.clone(), permissions); // First, fetch and compile the module; this step ensures that the module exists. worker.preload_module(&main_module).await?; - installer::install(flags, &module_url, args, name, root, force) + tools::installer::install(flags, &module_url, args, name, root, force) } async fn lint_command( @@ -228,11 +219,11 @@ async fn lint_command( } if list_rules { - lint::print_rules_list(json); + tools::lint::print_rules_list(json); return Ok(()); } - lint::lint_files(files, ignore, json).await + tools::lint::lint_files(files, ignore, json).await } async fn cache_command( @@ -523,7 +514,7 @@ async fn run_repl(flags: Flags) -> Result<(), AnyError> { MainWorker::new(&program_state, main_module.clone(), permissions); worker.run_event_loop().await?; - repl::run(&program_state, worker).await + tools::repl::run(&program_state, worker).await } async fn run_from_stdin(flags: Flags) -> Result<(), AnyError> { @@ -643,7 +634,8 @@ async fn test_command( let permissions = Permissions::from_flags(&flags); let cwd = std::env::current_dir().expect("No current directory"); let include = include.unwrap_or_else(|| vec![".".to_string()]); - let test_modules = test_runner::prepare_test_modules_urls(include, &cwd)?; + let test_modules = + tools::test_runner::prepare_test_modules_urls(include, &cwd)?; if test_modules.is_empty() { println!("No matching test modules found"); @@ -656,7 +648,7 @@ async fn test_command( let test_file_path = cwd.join("$deno$test.ts"); let test_file_url = Url::from_file_path(&test_file_path).expect("Should be valid file url"); - let test_file = test_runner::render_test_file( + let test_file = tools::test_runner::render_test_file( test_modules.clone(), fail_fast, quiet, @@ -680,7 +672,8 @@ async fn test_command( let mut maybe_coverage_collector = if flags.coverage { let session = worker.create_inspector_session(); - let mut coverage_collector = CoverageCollector::new(session); + let mut coverage_collector = + tools::coverage::CoverageCollector::new(session); coverage_collector.start_collecting().await?; Some(coverage_collector) @@ -699,10 +692,14 @@ async fn test_command( let coverages = coverage_collector.collect().await?; coverage_collector.stop_collecting().await?; - let filtered_coverages = - coverage::filter_script_coverages(coverages, test_file_url, test_modules); + let filtered_coverages = tools::coverage::filter_script_coverages( + coverages, + test_file_url, + test_modules, + ); - let mut coverage_reporter = PrettyCoverageReporter::new(quiet); + let mut coverage_reporter = + tools::coverage::PrettyCoverageReporter::new(quiet); for coverage in filtered_coverages { coverage_reporter.visit_coverage(&coverage); } @@ -796,7 +793,7 @@ pub fn main() { check, files, ignore, - } => fmt::format(files, check, ignore).boxed_local(), + } => tools::fmt::format(files, check, ignore).boxed_local(), DenoSubcommand::Info { file, json } => { info_command(flags, file, json).boxed_local() } @@ -847,7 +844,8 @@ pub fn main() { output, ca_file, } => { - upgrade_command(dry_run, force, version, output, ca_file).boxed_local() + tools::upgrade::upgrade_command(dry_run, force, version, output, ca_file) + .boxed_local() } }; diff --git a/cli/repl.rs b/cli/repl.rs deleted file mode 100644 index be85dc813..000000000 --- a/cli/repl.rs +++ /dev/null @@ -1,612 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::colors; -use crate::inspector::InspectorSession; -use crate::program_state::ProgramState; -use crate::worker::MainWorker; -use crate::worker::Worker; -use deno_core::error::AnyError; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use regex::Captures; -use regex::Regex; -use rustyline::completion::Completer; -use rustyline::error::ReadlineError; -use rustyline::highlight::Highlighter; -use rustyline::validate::ValidationContext; -use rustyline::validate::ValidationResult; -use rustyline::validate::Validator; -use rustyline::Context; -use rustyline::Editor; -use rustyline_derive::{Helper, Hinter}; -use std::borrow::Cow; -use std::sync::mpsc::channel; -use std::sync::mpsc::sync_channel; -use std::sync::mpsc::Receiver; -use std::sync::mpsc::Sender; -use std::sync::mpsc::SyncSender; -use std::sync::Arc; -use std::sync::Mutex; - -// Provides helpers to the editor like validation for multi-line edits, completion candidates for -// tab completion. -#[derive(Helper, Hinter)] -struct Helper { - context_id: u64, - message_tx: SyncSender<(String, Option)>, - response_rx: Receiver>, - highlighter: LineHighlighter, -} - -impl Helper { - fn post_message( - &self, - method: &str, - params: Option, - ) -> Result { - self.message_tx.send((method.to_string(), params))?; - self.response_rx.recv()? - } -} - -fn is_word_boundary(c: char) -> bool { - if c == '.' { - false - } else { - char::is_ascii_whitespace(&c) || char::is_ascii_punctuation(&c) - } -} - -impl Completer for Helper { - type Candidate = String; - - fn complete( - &self, - line: &str, - pos: usize, - _ctx: &Context<'_>, - ) -> Result<(usize, Vec), ReadlineError> { - let start = line[..pos].rfind(is_word_boundary).map_or_else(|| 0, |i| i); - let end = line[pos..] - .rfind(is_word_boundary) - .map_or_else(|| pos, |i| pos + i); - - let word = &line[start..end]; - let word = word.strip_prefix(is_word_boundary).unwrap_or(word); - let word = word.strip_suffix(is_word_boundary).unwrap_or(word); - - let fallback = format!(".{}", word); - - let (prefix, suffix) = match word.rfind('.') { - Some(index) => word.split_at(index), - None => ("globalThis", fallback.as_str()), - }; - - let evaluate_response = self - .post_message( - "Runtime.evaluate", - Some(json!({ - "contextId": self.context_id, - "expression": prefix, - "throwOnSideEffect": true, - "timeout": 200, - })), - ) - .unwrap(); - - if evaluate_response.get("exceptionDetails").is_some() { - let candidates = Vec::new(); - return Ok((pos, candidates)); - } - - if let Some(result) = evaluate_response.get("result") { - if let Some(object_id) = result.get("objectId") { - let get_properties_response = self - .post_message( - "Runtime.getProperties", - Some(json!({ - "objectId": object_id, - })), - ) - .unwrap(); - - if let Some(result) = get_properties_response.get("result") { - let candidates = result - .as_array() - .unwrap() - .iter() - .map(|r| r.get("name").unwrap().as_str().unwrap().to_string()) - .filter(|r| r.starts_with(&suffix[1..])) - .collect(); - - return Ok((pos - (suffix.len() - 1), candidates)); - } - } - } - - Ok((pos, Vec::new())) - } -} - -impl Validator for Helper { - fn validate( - &self, - ctx: &mut ValidationContext, - ) -> Result { - let mut stack: Vec = Vec::new(); - let mut literal: Option = None; - let mut escape: bool = false; - - for c in ctx.input().chars() { - if escape { - escape = false; - continue; - } - - if c == '\\' { - escape = true; - continue; - } - - if let Some(v) = literal { - if c == v { - literal = None - } - - continue; - } else { - literal = match c { - '`' | '"' | '/' | '\'' => Some(c), - _ => None, - }; - } - - match c { - '(' | '[' | '{' => stack.push(c), - ')' | ']' | '}' => match (stack.pop(), c) { - (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} - (Some(left), _) => { - return Ok(ValidationResult::Invalid(Some(format!( - "Mismatched pairs: {:?} is not properly closed", - left - )))) - } - (None, _) => { - // While technically invalid when unpaired, it should be V8's task to output error instead. - // Thus marked as valid with no info. - return Ok(ValidationResult::Valid(None)); - } - }, - _ => {} - } - } - - if !stack.is_empty() || literal == Some('`') { - return Ok(ValidationResult::Incomplete); - } - - Ok(ValidationResult::Valid(None)) - } -} - -impl Highlighter for Helper { - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - hint.into() - } - - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { - self.highlighter.highlight(line, pos) - } - - fn highlight_candidate<'c>( - &self, - candidate: &'c str, - _completion: rustyline::CompletionType, - ) -> Cow<'c, str> { - self.highlighter.highlight(candidate, 0) - } - - fn highlight_char(&self, line: &str, _: usize) -> bool { - !line.is_empty() - } -} - -struct LineHighlighter { - regex: Regex, -} - -impl LineHighlighter { - fn new() -> Self { - let regex = Regex::new( - r#"(?x) - (?P(?:/\*[\s\S]*?\*/|//[^\n]*)) | - (?P(?:"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|`([^`\\]|\\.)*`)) | - (?P/(?:(?:\\/|[^\n/]))*?/[gimsuy]*) | - (?P\b\d+(?:\.\d+)?(?:e[+-]?\d+)*n?\b) | - (?P\b(?:Infinity|NaN)\b) | - (?P\b0x[a-fA-F0-9]+\b) | - (?P\b0o[0-7]+\b) | - (?P\b0b[01]+\b) | - (?P\b(?:true|false)\b) | - (?P\b(?:null)\b) | - (?P\b(?:undefined)\b) | - (?P\b(?:await|async|var|let|for|if|else|in|of|class|const|function|yield|return|with|case|break|switch|import|export|new|while|do|throw|catch|this)\b) | - "#, - ) - .unwrap(); - - Self { regex } - } -} - -impl Highlighter for LineHighlighter { - fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { - self - .regex - .replace_all(&line.to_string(), |caps: &Captures<'_>| { - if let Some(cap) = caps.name("comment") { - colors::gray(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("string") { - colors::green(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("regexp") { - colors::red(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("number") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("boolean") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("null") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("undefined") { - colors::gray(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("keyword") { - colors::cyan(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("infinity") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("classes") { - colors::green_bold(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("hexnumber") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("octalnumber") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("binarynumber") { - colors::yellow(cap.as_str()).to_string() - } else { - caps[0].to_string() - } - }) - .to_string() - .into() - } -} - -async fn post_message_and_poll( - worker: &mut Worker, - session: &mut InspectorSession, - method: &str, - params: Option, -) -> Result { - let response = session.post_message(method, params); - tokio::pin!(response); - - loop { - tokio::select! { - result = &mut response => { - return result - } - - _ = worker.run_event_loop() => { - // A zero delay is long enough to yield the thread in order to prevent the loop from - // running hot for messages that are taking longer to resolve like for example an - // evaluation of top level await. - tokio::time::delay_for(tokio::time::Duration::from_millis(0)).await; - } - } - } -} - -async fn read_line_and_poll( - worker: &mut Worker, - session: &mut InspectorSession, - message_rx: &Receiver<(String, Option)>, - response_tx: &Sender>, - editor: Arc>>, -) -> Result { - let mut line = - tokio::task::spawn_blocking(move || editor.lock().unwrap().readline("> ")); - - let mut poll_worker = true; - - loop { - for (method, params) in message_rx.try_iter() { - response_tx - .send(session.post_message(&method, params).await) - .unwrap(); - } - - // Because an inspector websocket client may choose to connect at anytime when we have an - // inspector server we need to keep polling the worker to pick up new connections. - let mut timeout = - tokio::time::delay_for(tokio::time::Duration::from_millis(100)); - - tokio::select! { - result = &mut line => { - return result.unwrap(); - } - _ = worker.run_event_loop(), if poll_worker => { - poll_worker = false; - } - _ = &mut timeout => { - poll_worker = true - } - } - } -} - -static PRELUDE: &str = r#" -Object.defineProperty(globalThis, "_", { - configurable: true, - get: () => Deno[Deno.internal].lastEvalResult, - set: (value) => { - Object.defineProperty(globalThis, "_", { - value: value, - writable: true, - enumerable: true, - configurable: true, - }); - console.log("Last evaluation result is no longer saved to _."); - }, -}); - -Object.defineProperty(globalThis, "_error", { - configurable: true, - get: () => Deno[Deno.internal].lastThrownError, - set: (value) => { - Object.defineProperty(globalThis, "_error", { - value: value, - writable: true, - enumerable: true, - configurable: true, - }); - - console.log("Last thrown error is no longer saved to _error."); - }, -}); -"#; - -async fn inject_prelude( - worker: &mut MainWorker, - session: &mut InspectorSession, - context_id: u64, -) -> Result<(), AnyError> { - post_message_and_poll( - worker, - session, - "Runtime.evaluate", - Some(json!({ - "expression": PRELUDE, - "contextId": context_id, - })), - ) - .await?; - - Ok(()) -} - -pub async fn is_closing( - worker: &mut MainWorker, - session: &mut InspectorSession, - context_id: u64, -) -> Result { - let closed = post_message_and_poll( - worker, - session, - "Runtime.evaluate", - Some(json!({ - "expression": "(globalThis.closed)", - "contextId": context_id, - })), - ) - .await? - .get("result") - .unwrap() - .get("value") - .unwrap() - .as_bool() - .unwrap(); - - Ok(closed) -} - -pub async fn run( - program_state: &ProgramState, - mut worker: MainWorker, -) -> Result<(), AnyError> { - let mut session = worker.create_inspector_session(); - - let history_file = program_state.dir.root.join("deno_history.txt"); - - post_message_and_poll(&mut *worker, &mut session, "Runtime.enable", None) - .await?; - - // Enabling the runtime domain will always send trigger one executionContextCreated for each - // context the inspector knows about so we grab the execution context from that since - // our inspector does not support a default context (0 is an invalid context id). - let mut context_id: u64 = 0; - for notification in session.notifications() { - let method = notification.get("method").unwrap().as_str().unwrap(); - let params = notification.get("params").unwrap(); - - if method == "Runtime.executionContextCreated" { - context_id = params - .get("context") - .unwrap() - .get("id") - .unwrap() - .as_u64() - .unwrap(); - } - } - - let (message_tx, message_rx) = sync_channel(1); - let (response_tx, response_rx) = channel(); - - let helper = Helper { - context_id, - message_tx, - response_rx, - highlighter: LineHighlighter::new(), - }; - - let editor = Arc::new(Mutex::new(Editor::new())); - - editor.lock().unwrap().set_helper(Some(helper)); - - editor - .lock() - .unwrap() - .load_history(history_file.to_str().unwrap()) - .unwrap_or(()); - - println!("Deno {}", crate::version::DENO); - println!("exit using ctrl+d or close()"); - - inject_prelude(&mut worker, &mut session, context_id).await?; - - while !is_closing(&mut worker, &mut session, context_id).await? { - let line = read_line_and_poll( - &mut *worker, - &mut session, - &message_rx, - &response_tx, - editor.clone(), - ) - .await; - match line { - Ok(line) => { - // It is a bit unexpected that { "foo": "bar" } is interpreted as a block - // statement rather than an object literal so we interpret it as an expression statement - // to match the behavior found in a typical prompt including browser developer tools. - let wrapped_line = if line.trim_start().starts_with('{') - && !line.trim_end().ends_with(';') - { - format!("({})", &line) - } else { - line.clone() - }; - - let evaluate_response = post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.evaluate", - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &wrapped_line), - "contextId": context_id, - "replMode": true, - })), - ) - .await?; - - // If that fails, we retry it without wrapping in parens letting the error bubble up to the - // user if it is still an error. - let evaluate_response = - if evaluate_response.get("exceptionDetails").is_some() - && wrapped_line != line - { - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.evaluate", - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &line), - "contextId": context_id, - "replMode": true, - })), - ) - .await? - } else { - evaluate_response - }; - - let evaluate_result = evaluate_response.get("result").unwrap(); - let evaluate_exception_details = - evaluate_response.get("exceptionDetails"); - - if evaluate_exception_details.is_some() { - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", - "arguments": [ - evaluate_result, - ], - })), - ).await?; - } else { - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }", - "arguments": [ - evaluate_result, - ], - })), - ).await?; - } - - // TODO(caspervonb) we should investigate using previews here but to keep things - // consistent with the previous implementation we just get the preview result from - // Deno.inspectArgs. - let inspect_response = - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object], { colors: !Deno.noColor }); }", - "arguments": [ - evaluate_result, - ], - })), - ).await?; - - let inspect_result = inspect_response.get("result").unwrap(); - - let value = inspect_result.get("value").unwrap().as_str().unwrap(); - let output = match evaluate_exception_details { - Some(_) => format!("Uncaught {}", value), - None => value.to_string(), - }; - - println!("{}", output); - - editor.lock().unwrap().add_history_entry(line.as_str()); - } - Err(ReadlineError::Interrupted) => { - println!("exit using ctrl+d or close()"); - continue; - } - Err(ReadlineError::Eof) => { - break; - } - Err(err) => { - println!("Error: {:?}", err); - break; - } - } - } - - std::fs::create_dir_all(history_file.parent().unwrap())?; - editor - .lock() - .unwrap() - .save_history(history_file.to_str().unwrap())?; - - Ok(()) -} diff --git a/cli/test_runner.rs b/cli/test_runner.rs deleted file mode 100644 index cd8a394c5..000000000 --- a/cli/test_runner.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::fs_util; -use crate::installer::is_remote_url; -use deno_core::error::AnyError; -use deno_core::serde_json::json; -use deno_core::url::Url; -use std::path::Path; -use std::path::PathBuf; - -fn is_supported(p: &Path) -> bool { - use std::path::Component; - if let Some(Component::Normal(basename_os_str)) = p.components().next_back() { - let basename = basename_os_str.to_string_lossy(); - basename.ends_with("_test.ts") - || basename.ends_with("_test.tsx") - || basename.ends_with("_test.js") - || basename.ends_with("_test.mjs") - || basename.ends_with("_test.jsx") - || basename.ends_with(".test.ts") - || basename.ends_with(".test.tsx") - || basename.ends_with(".test.js") - || basename.ends_with(".test.mjs") - || basename.ends_with(".test.jsx") - || basename == "test.ts" - || basename == "test.tsx" - || basename == "test.js" - || basename == "test.mjs" - || basename == "test.jsx" - } else { - false - } -} - -pub fn prepare_test_modules_urls( - include: Vec, - root_path: &PathBuf, -) -> Result, AnyError> { - let (include_paths, include_urls): (Vec, Vec) = - include.into_iter().partition(|n| !is_remote_url(n)); - - let mut prepared = vec![]; - - for path in include_paths { - let p = fs_util::normalize_path(&root_path.join(path)); - if p.is_dir() { - let test_files = - crate::fs_util::collect_files(vec![p], vec![], is_supported).unwrap(); - let test_files_as_urls = test_files - .iter() - .map(|f| Url::from_file_path(f).unwrap()) - .collect::>(); - prepared.extend(test_files_as_urls); - } else { - let url = Url::from_file_path(p).unwrap(); - prepared.push(url); - } - } - - for remote_url in include_urls { - let url = Url::parse(&remote_url)?; - prepared.push(url); - } - - Ok(prepared) -} - -pub fn render_test_file( - modules: Vec, - fail_fast: bool, - quiet: bool, - filter: Option, -) -> String { - let mut test_file = "".to_string(); - - for module in modules { - test_file.push_str(&format!("import \"{}\";\n", module.to_string())); - } - - let options = if let Some(filter) = filter { - json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet, "filter": filter }) - } else { - json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet }) - }; - - test_file.push_str("// @ts-ignore\n"); - - test_file.push_str(&format!( - "await Deno[Deno.internal].runTests({});\n", - options - )); - - test_file -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_prepare_test_modules_urls() { - let test_data_path = test_util::root_path().join("cli/tests/subdir"); - let mut matched_urls = prepare_test_modules_urls( - vec![ - "https://example.com/colors_test.ts".to_string(), - "./mod1.ts".to_string(), - "./mod3.js".to_string(), - "subdir2/mod2.ts".to_string(), - "http://example.com/printf_test.ts".to_string(), - ], - &test_data_path, - ) - .unwrap(); - let test_data_url = - Url::from_file_path(test_data_path).unwrap().to_string(); - - let expected: Vec = vec![ - format!("{}/mod1.ts", test_data_url), - format!("{}/mod3.js", test_data_url), - format!("{}/subdir2/mod2.ts", test_data_url), - "http://example.com/printf_test.ts".to_string(), - "https://example.com/colors_test.ts".to_string(), - ] - .into_iter() - .map(|f| Url::parse(&f).unwrap()) - .collect(); - matched_urls.sort(); - assert_eq!(matched_urls, expected); - } - - #[test] - fn test_is_supported() { - assert!(is_supported(Path::new("tests/subdir/foo_test.ts"))); - assert!(is_supported(Path::new("tests/subdir/foo_test.tsx"))); - assert!(is_supported(Path::new("tests/subdir/foo_test.js"))); - assert!(is_supported(Path::new("tests/subdir/foo_test.jsx"))); - assert!(is_supported(Path::new("bar/foo.test.ts"))); - assert!(is_supported(Path::new("bar/foo.test.tsx"))); - assert!(is_supported(Path::new("bar/foo.test.js"))); - assert!(is_supported(Path::new("bar/foo.test.jsx"))); - assert!(is_supported(Path::new("foo/bar/test.js"))); - assert!(is_supported(Path::new("foo/bar/test.jsx"))); - assert!(is_supported(Path::new("foo/bar/test.ts"))); - assert!(is_supported(Path::new("foo/bar/test.tsx"))); - assert!(!is_supported(Path::new("README.md"))); - assert!(!is_supported(Path::new("lib/typescript.d.ts"))); - assert!(!is_supported(Path::new("notatest.js"))); - assert!(!is_supported(Path::new("NotAtest.ts"))); - } - - #[test] - fn supports_dirs() { - let root = test_util::root_path().join("std").join("http"); - println!("root {:?}", root); - let mut matched_urls = - prepare_test_modules_urls(vec![".".to_string()], &root).unwrap(); - matched_urls.sort(); - let root_url = Url::from_file_path(root).unwrap().to_string(); - println!("root_url {}", root_url); - let expected: Vec = vec![ - format!("{}/_io_test.ts", root_url), - format!("{}/cookie_test.ts", root_url), - format!("{}/file_server_test.ts", root_url), - format!("{}/racing_server_test.ts", root_url), - format!("{}/server_test.ts", root_url), - format!("{}/test.ts", root_url), - ] - .into_iter() - .map(|f| Url::parse(&f).unwrap()) - .collect(); - assert_eq!(matched_urls, expected); - } -} diff --git a/cli/tools/coverage.rs b/cli/tools/coverage.rs new file mode 100644 index 000000000..85ba3f559 --- /dev/null +++ b/cli/tools/coverage.rs @@ -0,0 +1,238 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::colors; +use crate::inspector::InspectorSession; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::url::Url; +use serde::Deserialize; + +pub struct CoverageCollector { + session: Box, +} + +impl CoverageCollector { + pub fn new(session: Box) -> Self { + Self { session } + } + + pub async fn start_collecting(&mut self) -> Result<(), AnyError> { + self.session.post_message("Debugger.enable", None).await?; + + self.session.post_message("Profiler.enable", None).await?; + + self + .session + .post_message( + "Profiler.startPreciseCoverage", + Some(json!({"callCount": true, "detailed": true})), + ) + .await?; + + Ok(()) + } + + pub async fn collect(&mut self) -> Result, AnyError> { + let result = self + .session + .post_message("Profiler.takePreciseCoverage", None) + .await?; + + let take_coverage_result: TakePreciseCoverageResult = + serde_json::from_value(result)?; + + let mut coverages: Vec = Vec::new(); + for script_coverage in take_coverage_result.result { + let result = self + .session + .post_message( + "Debugger.getScriptSource", + Some(json!({ + "scriptId": script_coverage.script_id, + })), + ) + .await?; + + let get_script_source_result: GetScriptSourceResult = + serde_json::from_value(result)?; + + coverages.push(Coverage { + script_coverage, + script_source: get_script_source_result.script_source, + }) + } + + Ok(coverages) + } + + pub async fn stop_collecting(&mut self) -> Result<(), AnyError> { + self + .session + .post_message("Profiler.stopPreciseCoverage", None) + .await?; + self.session.post_message("Profiler.disable", None).await?; + self.session.post_message("Debugger.disable", None).await?; + + Ok(()) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoverageRange { + pub start_offset: usize, + pub end_offset: usize, + pub count: usize, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FunctionCoverage { + pub function_name: String, + pub ranges: Vec, + pub is_block_coverage: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScriptCoverage { + pub script_id: String, + pub url: String, + pub functions: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Coverage { + pub script_coverage: ScriptCoverage, + pub script_source: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TakePreciseCoverageResult { + result: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetScriptSourceResult { + pub script_source: String, + pub bytecode: Option, +} + +pub struct PrettyCoverageReporter { + quiet: bool, +} + +// TODO(caspervonb) add support for lcov output (see geninfo(1) for format spec). +impl PrettyCoverageReporter { + pub fn new(quiet: bool) -> PrettyCoverageReporter { + PrettyCoverageReporter { quiet } + } + + pub fn visit_coverage(&mut self, coverage: &Coverage) { + let lines = coverage.script_source.lines().collect::>(); + + let mut covered_lines: Vec = Vec::new(); + let mut uncovered_lines: Vec = Vec::new(); + + let mut line_start_offset = 0; + for (index, line) in lines.iter().enumerate() { + let line_end_offset = line_start_offset + line.len(); + + let mut count = 0; + for function in &coverage.script_coverage.functions { + for range in &function.ranges { + if range.start_offset <= line_start_offset + && range.end_offset >= line_end_offset + { + if range.count == 0 { + count = 0; + break; + } + + count += range.count; + } + } + + line_start_offset = line_end_offset; + } + if count > 0 { + covered_lines.push(index); + } else { + uncovered_lines.push(index); + } + } + + if !self.quiet { + print!("cover {} ... ", coverage.script_coverage.url); + + let line_coverage_ratio = covered_lines.len() as f32 / lines.len() as f32; + let line_coverage = format!( + "{:.3}% ({}/{})", + line_coverage_ratio * 100.0, + covered_lines.len(), + lines.len() + ); + + if line_coverage_ratio >= 0.9 { + println!("{}", colors::green(&line_coverage)); + } else if line_coverage_ratio >= 0.75 { + println!("{}", colors::yellow(&line_coverage)); + } else { + println!("{}", colors::red(&line_coverage)); + } + + for line_index in uncovered_lines { + println!( + "{:width$}{} {}", + line_index + 1, + colors::gray(" |"), + colors::red(&lines[line_index]), + width = 4 + ); + } + } + } +} + +pub fn filter_script_coverages( + coverages: Vec, + test_file_url: Url, + test_modules: Vec, +) -> Vec { + coverages + .into_iter() + .filter(|e| { + if let Ok(url) = Url::parse(&e.script_coverage.url) { + if url.path().ends_with("__anonymous__") { + return false; + } + + if url == test_file_url { + return false; + } + + for test_module_url in &test_modules { + if &url == test_module_url { + return false; + } + } + + if let Ok(path) = url.to_file_path() { + for test_module_url in &test_modules { + if let Ok(test_module_path) = test_module_url.to_file_path() { + if path.starts_with(test_module_path.parent().unwrap()) { + return true; + } + } + } + } + } + + false + }) + .collect::>() +} diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs new file mode 100644 index 000000000..0036436c1 --- /dev/null +++ b/cli/tools/fmt.rs @@ -0,0 +1,283 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! This module provides file formatting utilities using +//! [`dprint-plugin-typescript`](https://github.com/dprint/dprint-plugin-typescript). +//! +//! At the moment it is only consumed using CLI but in +//! the future it can be easily extended to provide +//! the same functions as ops available in JS runtime. + +use crate::colors; +use crate::diff::diff; +use crate::fs_util::{collect_files, is_supported_ext}; +use crate::text_encoding; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::futures; +use dprint_plugin_typescript as dprint; +use std::fs; +use std::io::stdin; +use std::io::stdout; +use std::io::Read; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +const BOM_CHAR: char = '\u{FEFF}'; + +/// Format JavaScript/TypeScript files. +/// +/// First argument and ignore supports globs, and if it is `None` +/// then the current directory is recursively walked. +pub async fn format( + args: Vec, + check: bool, + exclude: Vec, +) -> Result<(), AnyError> { + if args.len() == 1 && args[0].to_string_lossy() == "-" { + return format_stdin(check); + } + // collect the files that are to be formatted + let target_files = collect_files(args, exclude, is_supported_ext)?; + let config = get_config(); + if check { + check_source_files(config, target_files).await + } else { + format_source_files(config, target_files).await + } +} + +async fn check_source_files( + config: dprint::configuration::Configuration, + paths: Vec, +) -> Result<(), AnyError> { + let not_formatted_files_count = Arc::new(AtomicUsize::new(0)); + let checked_files_count = Arc::new(AtomicUsize::new(0)); + + // prevent threads outputting at the same time + let output_lock = Arc::new(Mutex::new(0)); + + run_parallelized(paths, { + let not_formatted_files_count = not_formatted_files_count.clone(); + let checked_files_count = checked_files_count.clone(); + move |file_path| { + checked_files_count.fetch_add(1, Ordering::Relaxed); + let file_text = read_file_contents(&file_path)?.text; + let r = dprint::format_text(&file_path, &file_text, &config); + match r { + Ok(formatted_text) => { + if formatted_text != file_text { + not_formatted_files_count.fetch_add(1, Ordering::Relaxed); + let _g = output_lock.lock().unwrap(); + let diff = diff(&file_text, &formatted_text); + info!(""); + info!("{} {}:", colors::bold("from"), file_path.display()); + info!("{}", diff); + } + } + Err(e) => { + let _g = output_lock.lock().unwrap(); + eprintln!("Error checking: {}", file_path.to_string_lossy()); + eprintln!(" {}", e); + } + } + Ok(()) + } + }) + .await?; + + let not_formatted_files_count = + not_formatted_files_count.load(Ordering::Relaxed); + let checked_files_count = checked_files_count.load(Ordering::Relaxed); + let checked_files_str = + format!("{} {}", checked_files_count, files_str(checked_files_count)); + if not_formatted_files_count == 0 { + info!("Checked {}", checked_files_str); + Ok(()) + } else { + let not_formatted_files_str = files_str(not_formatted_files_count); + Err(generic_error(format!( + "Found {} not formatted {} in {}", + not_formatted_files_count, not_formatted_files_str, checked_files_str, + ))) + } +} + +async fn format_source_files( + config: dprint::configuration::Configuration, + paths: Vec, +) -> Result<(), AnyError> { + let formatted_files_count = Arc::new(AtomicUsize::new(0)); + let checked_files_count = Arc::new(AtomicUsize::new(0)); + let output_lock = Arc::new(Mutex::new(0)); // prevent threads outputting at the same time + + run_parallelized(paths, { + let formatted_files_count = formatted_files_count.clone(); + let checked_files_count = checked_files_count.clone(); + move |file_path| { + checked_files_count.fetch_add(1, Ordering::Relaxed); + let file_contents = read_file_contents(&file_path)?; + let r = dprint::format_text(&file_path, &file_contents.text, &config); + match r { + Ok(formatted_text) => { + if formatted_text != file_contents.text { + write_file_contents( + &file_path, + FileContents { + had_bom: file_contents.had_bom, + text: formatted_text, + }, + )?; + formatted_files_count.fetch_add(1, Ordering::Relaxed); + let _g = output_lock.lock().unwrap(); + info!("{}", file_path.to_string_lossy()); + } + } + Err(e) => { + let _g = output_lock.lock().unwrap(); + eprintln!("Error formatting: {}", file_path.to_string_lossy()); + eprintln!(" {}", e); + } + } + Ok(()) + } + }) + .await?; + + let formatted_files_count = formatted_files_count.load(Ordering::Relaxed); + debug!( + "Formatted {} {}", + formatted_files_count, + files_str(formatted_files_count), + ); + + let checked_files_count = checked_files_count.load(Ordering::Relaxed); + info!( + "Checked {} {}", + checked_files_count, + files_str(checked_files_count) + ); + + Ok(()) +} + +/// Format stdin and write result to stdout. +/// Treats input as TypeScript. +/// Compatible with `--check` flag. +fn format_stdin(check: bool) -> Result<(), AnyError> { + let mut source = String::new(); + if stdin().read_to_string(&mut source).is_err() { + return Err(generic_error("Failed to read from stdin")); + } + let config = get_config(); + + // dprint will fallback to jsx parsing if parsing this as a .ts file doesn't work + match dprint::format_text(&PathBuf::from("_stdin.ts"), &source, &config) { + Ok(formatted_text) => { + if check { + if formatted_text != source { + println!("Not formatted stdin"); + } + } else { + stdout().write_all(formatted_text.as_bytes())?; + } + } + Err(e) => { + return Err(generic_error(e)); + } + } + Ok(()) +} + +fn files_str(len: usize) -> &'static str { + if len <= 1 { + "file" + } else { + "files" + } +} + +fn get_config() -> dprint::configuration::Configuration { + use dprint::configuration::*; + ConfigurationBuilder::new().deno().build() +} + +struct FileContents { + text: String, + had_bom: bool, +} + +fn read_file_contents(file_path: &Path) -> Result { + let file_bytes = fs::read(&file_path)?; + let charset = text_encoding::detect_charset(&file_bytes); + let file_text = text_encoding::convert_to_utf8(&file_bytes, charset)?; + let had_bom = file_text.starts_with(BOM_CHAR); + let text = if had_bom { + // remove the BOM + String::from(&file_text[BOM_CHAR.len_utf8()..]) + } else { + String::from(file_text) + }; + + Ok(FileContents { text, had_bom }) +} + +fn write_file_contents( + file_path: &Path, + file_contents: FileContents, +) -> Result<(), AnyError> { + let file_text = if file_contents.had_bom { + // add back the BOM + format!("{}{}", BOM_CHAR, file_contents.text) + } else { + file_contents.text + }; + + Ok(fs::write(file_path, file_text)?) +} + +pub async fn run_parallelized( + file_paths: Vec, + f: F, +) -> Result<(), AnyError> +where + F: FnOnce(PathBuf) -> Result<(), AnyError> + Send + 'static + Clone, +{ + let handles = file_paths.iter().map(|file_path| { + let f = f.clone(); + let file_path = file_path.clone(); + tokio::task::spawn_blocking(move || f(file_path)) + }); + let join_results = futures::future::join_all(handles).await; + + // find the tasks that panicked and let the user know which files + let panic_file_paths = join_results + .iter() + .enumerate() + .filter_map(|(i, join_result)| { + join_result + .as_ref() + .err() + .map(|_| file_paths[i].to_string_lossy()) + }) + .collect::>(); + if !panic_file_paths.is_empty() { + panic!("Panic formatting: {}", panic_file_paths.join(", ")) + } + + // check for any errors and if so return the first one + let mut errors = join_results.into_iter().filter_map(|join_result| { + join_result + .ok() + .map(|handle_result| handle_result.err()) + .flatten() + }); + + if let Some(e) = errors.next() { + Err(e) + } else { + Ok(()) + } +} diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs new file mode 100644 index 000000000..e0a99873a --- /dev/null +++ b/cli/tools/installer.rs @@ -0,0 +1,813 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::flags::Flags; +use crate::fs_util::canonicalize_path; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::url::Url; +use log::Level; +use regex::{Regex, RegexBuilder}; +use std::env; +use std::fs; +use std::fs::File; +use std::io; +use std::io::Write; +#[cfg(not(windows))] +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +lazy_static! { + static ref EXEC_NAME_RE: Regex = RegexBuilder::new( + r"^[a-z][\w-]*$" + ).case_insensitive(true).build().unwrap(); + // Regular expression to test disk driver letter. eg "C:\\User\username\path\to" + static ref DRIVE_LETTER_REG: Regex = RegexBuilder::new( + r"^[c-z]:" + ).case_insensitive(true).build().unwrap(); +} + +pub fn is_remote_url(module_url: &str) -> bool { + let lower = module_url.to_lowercase(); + lower.starts_with("http://") || lower.starts_with("https://") +} + +fn validate_name(exec_name: &str) -> Result<(), AnyError> { + if EXEC_NAME_RE.is_match(exec_name) { + Ok(()) + } else { + Err(generic_error(format!( + "Invalid executable name: {}", + exec_name + ))) + } +} + +#[cfg(windows)] +/// On Windows if user is using Powershell .cmd extension is need to run the +/// installed module. +/// Generate batch script to satisfy that. +fn generate_executable_file( + file_path: PathBuf, + args: Vec, +) -> Result<(), AnyError> { + let args: Vec = args.iter().map(|c| format!("\"{}\"", c)).collect(); + let template = format!( + "% generated by deno install %\n@deno.exe {} %*\n", + args.join(" ") + ); + let mut file = File::create(&file_path)?; + file.write_all(template.as_bytes())?; + Ok(()) +} + +#[cfg(not(windows))] +fn generate_executable_file( + file_path: PathBuf, + args: Vec, +) -> Result<(), AnyError> { + use shell_escape::escape; + let args: Vec = args + .into_iter() + .map(|c| escape(c.into()).into_owned()) + .collect(); + let template = format!( + r#"#!/bin/sh +# generated by deno install +exec deno {} "$@" +"#, + args.join(" "), + ); + let mut file = File::create(&file_path)?; + file.write_all(template.as_bytes())?; + let _metadata = fs::metadata(&file_path)?; + let mut permissions = _metadata.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&file_path, permissions)?; + Ok(()) +} + +fn get_installer_root() -> Result { + if let Ok(env_dir) = env::var("DENO_INSTALL_ROOT") { + if !env_dir.is_empty() { + return canonicalize_path(&PathBuf::from(env_dir)); + } + } + // Note: on Windows, the $HOME environment variable may be set by users or by + // third party software, but it is non-standard and should not be relied upon. + let home_env_var = if cfg!(windows) { "USERPROFILE" } else { "HOME" }; + let mut home_path = + env::var_os(home_env_var) + .map(PathBuf::from) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("${} is not defined", home_env_var), + ) + })?; + home_path.push(".deno"); + Ok(home_path) +} + +fn infer_name_from_url(url: &Url) -> Option { + let path = PathBuf::from(url.path()); + let mut stem = match path.file_stem() { + Some(stem) => stem.to_string_lossy().to_string(), + None => return None, + }; + if stem == "main" || stem == "mod" || stem == "index" || stem == "cli" { + if let Some(parent_name) = path.parent().and_then(|p| p.file_name()) { + stem = parent_name.to_string_lossy().to_string(); + } + } + let stem = stem.splitn(2, '@').next().unwrap().to_string(); + Some(stem) +} + +pub fn install( + flags: Flags, + module_url: &str, + args: Vec, + name: Option, + root: Option, + force: bool, +) -> Result<(), AnyError> { + let root = if let Some(root) = root { + canonicalize_path(&root)? + } else { + get_installer_root()? + }; + let installation_dir = root.join("bin"); + + // ensure directory exists + if let Ok(metadata) = fs::metadata(&installation_dir) { + if !metadata.is_dir() { + return Err(generic_error("Installation path is not a directory")); + } + } else { + fs::create_dir_all(&installation_dir)?; + }; + + // Check if module_url is remote + let module_url = if is_remote_url(module_url) { + Url::parse(module_url).expect("Should be valid url") + } else { + let module_path = PathBuf::from(module_url); + let module_path = if module_path.is_absolute() { + module_path + } else { + let cwd = env::current_dir().unwrap(); + cwd.join(module_path) + }; + Url::from_file_path(module_path).expect("Path should be absolute") + }; + + let name = name.or_else(|| infer_name_from_url(&module_url)); + + let name = match name { + Some(name) => name, + None => return Err(generic_error( + "An executable name was not provided. One could not be inferred from the URL. Aborting.", + )), + }; + + validate_name(name.as_str())?; + let mut file_path = installation_dir.join(&name); + + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + if file_path.exists() && !force { + return Err(generic_error( + "Existing installation found. Aborting (Use -f to overwrite).", + )); + }; + + let mut extra_files: Vec<(PathBuf, String)> = vec![]; + + let mut executable_args = vec!["run".to_string()]; + executable_args.extend_from_slice(&flags.to_permission_args()); + if let Some(ca_file) = flags.ca_file { + executable_args.push("--cert".to_string()); + executable_args.push(ca_file) + } + if let Some(log_level) = flags.log_level { + if log_level == Level::Error { + executable_args.push("--quiet".to_string()); + } else { + executable_args.push("--log-level".to_string()); + let log_level = match log_level { + Level::Debug => "debug", + Level::Info => "info", + _ => { + return Err(generic_error(format!("invalid log level {}", log_level))) + } + }; + executable_args.push(log_level.to_string()); + } + } + + if flags.no_check { + executable_args.push("--no-check".to_string()); + } + + if flags.unstable { + executable_args.push("--unstable".to_string()); + } + + if flags.no_remote { + executable_args.push("--no-remote".to_string()); + } + + if flags.lock_write { + executable_args.push("--lock-write".to_string()); + } + + if flags.cached_only { + executable_args.push("--cached_only".to_string()); + } + + if let Some(v8_flags) = flags.v8_flags { + executable_args.push(format!("--v8-flags={}", v8_flags.join(","))); + } + + if let Some(seed) = flags.seed { + executable_args.push("--seed".to_string()); + executable_args.push(seed.to_string()); + } + + if let Some(inspect) = flags.inspect { + executable_args.push(format!("--inspect={}", inspect.to_string())); + } + + if let Some(inspect_brk) = flags.inspect_brk { + executable_args.push(format!("--inspect-brk={}", inspect_brk.to_string())); + } + + if let Some(import_map_path) = flags.import_map_path { + let mut copy_path = file_path.clone(); + copy_path.set_extension("import_map.json"); + executable_args.push("--import-map".to_string()); + executable_args.push(copy_path.to_str().unwrap().to_string()); + extra_files.push((copy_path, fs::read_to_string(import_map_path)?)); + } + + if let Some(config_path) = flags.config_path { + let mut copy_path = file_path.clone(); + copy_path.set_extension("tsconfig.json"); + executable_args.push("--config".to_string()); + executable_args.push(copy_path.to_str().unwrap().to_string()); + extra_files.push((copy_path, fs::read_to_string(config_path)?)); + } + + if let Some(lock_path) = flags.lock { + let mut copy_path = file_path.clone(); + copy_path.set_extension("lock.json"); + executable_args.push("--lock".to_string()); + executable_args.push(copy_path.to_str().unwrap().to_string()); + extra_files.push((copy_path, fs::read_to_string(lock_path)?)); + } + + executable_args.push(module_url.to_string()); + executable_args.extend_from_slice(&args); + + generate_executable_file(file_path.to_owned(), executable_args)?; + for (path, contents) in extra_files { + fs::write(path, contents)?; + } + + println!("✅ Successfully installed {}", name); + println!("{}", file_path.to_string_lossy()); + let installation_dir_str = installation_dir.to_string_lossy(); + + if !is_in_path(&installation_dir) { + println!("ℹ️ Add {} to PATH", installation_dir_str); + if cfg!(windows) { + println!(" set PATH=%PATH%;{}", installation_dir_str); + } else { + println!(" export PATH=\"{}:$PATH\"", installation_dir_str); + } + } + + Ok(()) +} + +fn is_in_path(dir: &PathBuf) -> bool { + if let Some(paths) = env::var_os("PATH") { + for p in env::split_paths(&paths) { + if *dir == p { + return true; + } + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + use tempfile::TempDir; + + lazy_static! { + pub static ref ENV_LOCK: Mutex<()> = Mutex::new(()); + } + + #[test] + fn test_is_remote_url() { + assert!(is_remote_url("https://deno.land/std/http/file_server.ts")); + assert!(is_remote_url("http://deno.land/std/http/file_server.ts")); + assert!(is_remote_url("HTTP://deno.land/std/http/file_server.ts")); + assert!(is_remote_url("HTTp://deno.land/std/http/file_server.ts")); + assert!(!is_remote_url("file:///dev/deno_std/http/file_server.ts")); + assert!(!is_remote_url("./dev/deno_std/http/file_server.ts")); + } + + #[test] + fn install_infer_name_from_url() { + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/server.ts").unwrap() + ), + Some("server".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/main.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/mod.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/index.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/cli.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("https://example.com/main.ts").unwrap()), + Some("main".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("https://example.com").unwrap()), + None + ); + assert_eq!( + infer_name_from_url(&Url::parse("file:///abc/server.ts").unwrap()), + Some("server".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("file:///abc/main.ts").unwrap()), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("file:///main.ts").unwrap()), + Some("main".to_string()) + ); + assert_eq!(infer_name_from_url(&Url::parse("file:///").unwrap()), None); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc@0.1.0").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc@0.1.0/main.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc@def@ghi").unwrap() + ), + Some("abc".to_string()) + ); + } + + #[test] + fn install_basic() { + let _guard = ENV_LOCK.lock().ok(); + let temp_dir = TempDir::new().expect("tempdir fail"); + let temp_dir_str = temp_dir.path().to_string_lossy().to_string(); + // NOTE: this test overrides environmental variables + // don't add other tests in this file that mess with "HOME" and "USEPROFILE" + // otherwise transient failures are possible because tests are run in parallel. + // It means that other test can override env vars when this test is running. + let original_home = env::var_os("HOME"); + let original_user_profile = env::var_os("HOME"); + let original_install_root = env::var_os("DENO_INSTALL_ROOT"); + env::set_var("HOME", &temp_dir_str); + env::set_var("USERPROFILE", &temp_dir_str); + env::set_var("DENO_INSTALL_ROOT", ""); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + Some("echo_test".to_string()), + None, + false, + ) + .expect("Install failed"); + + let mut file_path = temp_dir.path().join(".deno/bin/echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + + let content = fs::read_to_string(file_path).unwrap(); + // It's annoying when shell scripts don't have NL at the end. + assert_eq!(content.chars().last().unwrap(), '\n'); + + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + if let Some(home) = original_home { + env::set_var("HOME", home); + } + if let Some(user_profile) = original_user_profile { + env::set_var("USERPROFILE", user_profile); + } + if let Some(install_root) = original_install_root { + env::set_var("DENO_INSTALL_ROOT", install_root); + } + } + + #[test] + fn install_unstable() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags { + unstable: true, + ..Flags::default() + }, + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + + let content = fs::read_to_string(file_path).unwrap(); + println!("this is the file path {:?}", content); + if cfg!(windows) { + assert!(content.contains( + r#""run" "--unstable" "http://localhost:4545/cli/tests/echo_server.ts""# + )); + } else { + assert!(content.contains( + r#"run --unstable 'http://localhost:4545/cli/tests/echo_server.ts'"# + )); + } + } + + #[test] + fn install_inferred_name() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + None, + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_server"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + } + + #[test] + fn install_inferred_name_from_parent() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/subdir/main.ts", + vec![], + None, + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("subdir"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/subdir/main.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/subdir/main.ts'"#)); + } + } + + #[test] + fn install_custom_dir_option() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + } + + #[test] + fn install_custom_dir_env_var() { + let _guard = ENV_LOCK.lock().ok(); + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + let original_install_root = env::var_os("DENO_INSTALL_ROOT"); + env::set_var("DENO_INSTALL_ROOT", temp_dir.path().to_path_buf()); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + Some("echo_test".to_string()), + None, + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } else { + assert!(content + .contains(r#"run 'http://localhost:4545/cli/tests/echo_server.ts'"#)); + } + if let Some(install_root) = original_install_root { + env::set_var("DENO_INSTALL_ROOT", install_root); + } + } + + #[test] + fn install_with_flags() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags { + allow_net: true, + allow_read: true, + no_check: true, + log_level: Some(Level::Error), + ..Flags::default() + }, + "http://localhost:4545/cli/tests/echo_server.ts", + vec!["--foobar".to_string()], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + if cfg!(windows) { + assert!(content.contains(r#""run" "--allow-read" "--allow-net" "--quiet" "--no-check" "http://localhost:4545/cli/tests/echo_server.ts" "--foobar""#)); + } else { + assert!(content.contains(r#"run --allow-read --allow-net --quiet --no-check 'http://localhost:4545/cli/tests/echo_server.ts' --foobar"#)); + } + } + + #[test] + fn install_local_module() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + let local_module = env::current_dir().unwrap().join("echo_server.ts"); + let local_module_url = Url::from_file_path(&local_module).unwrap(); + let local_module_str = local_module.to_string_lossy(); + + install( + Flags::default(), + &local_module_str, + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + assert!(content.contains(&local_module_url.to_string())); + } + + #[test] + fn install_force() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + assert!(file_path.exists()); + + // No force. Install failed. + let no_force_result = install( + Flags::default(), + "http://localhost:4545/cli/tests/cat.ts", // using a different URL + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ); + assert!(no_force_result.is_err()); + assert!(no_force_result + .unwrap_err() + .to_string() + .contains("Existing installation found")); + // Assert not modified + let file_content = fs::read_to_string(&file_path).unwrap(); + assert!(file_content.contains("echo_server.ts")); + + // Force. Install success. + let force_result = install( + Flags::default(), + "http://localhost:4545/cli/tests/cat.ts", // using a different URL + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + true, + ); + assert!(force_result.is_ok()); + // Assert modified + let file_content_2 = fs::read_to_string(&file_path).unwrap(); + assert!(file_content_2.contains("cat.ts")); + } + + #[test] + fn install_with_config() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + let config_file_path = temp_dir.path().join("test_tsconfig.json"); + let config = "{}"; + let mut config_file = File::create(&config_file_path).unwrap(); + let result = config_file.write_all(config.as_bytes()); + assert!(result.is_ok()); + + let result = install( + Flags { + config_path: Some(config_file_path.to_string_lossy().to_string()), + ..Flags::default() + }, + "http://localhost:4545/cli/tests/cat.ts", + vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + true, + ); + eprintln!("result {:?}", result); + assert!(result.is_ok()); + + let config_file_name = "echo_test.tsconfig.json"; + + let file_path = bin_dir.join(config_file_name.to_string()); + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + assert!(content == "{}"); + } + + // TODO: enable on Windows after fixing batch escaping + #[cfg(not(windows))] + #[test] + fn install_shell_escaping() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec!["\"".to_string()], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_test"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + println!("{}", content); + if cfg!(windows) { + // TODO: see comment above this test + } else { + assert!(content.contains( + r#"run 'http://localhost:4545/cli/tests/echo_server.ts' '"'"# + )); + } + } +} diff --git a/cli/tools/lint.rs b/cli/tools/lint.rs new file mode 100644 index 000000000..f17709c8b --- /dev/null +++ b/cli/tools/lint.rs @@ -0,0 +1,364 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! This module provides file formating utilities using +//! [`deno_lint`](https://github.com/denoland/deno_lint). +//! +//! At the moment it is only consumed using CLI but in +//! the future it can be easily extended to provide +//! the same functions as ops available in JS runtime. +use crate::ast; +use crate::colors; +use crate::fmt_errors; +use crate::fs_util::{collect_files, is_supported_ext}; +use crate::media_type::MediaType; +use crate::tools::fmt::run_parallelized; +use deno_core::error::{generic_error, AnyError, JsStackFrame}; +use deno_core::serde_json; +use deno_lint::diagnostic::LintDiagnostic; +use deno_lint::linter::Linter; +use deno_lint::linter::LinterBuilder; +use deno_lint::rules; +use deno_lint::rules::LintRule; +use serde::Serialize; +use std::fs; +use std::io::{stdin, Read}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use swc_ecmascript::parser::Syntax; + +pub enum LintReporterKind { + Pretty, + Json, +} + +fn create_reporter(kind: LintReporterKind) -> Box { + match kind { + LintReporterKind::Pretty => Box::new(PrettyLintReporter::new()), + LintReporterKind::Json => Box::new(JsonLintReporter::new()), + } +} + +pub async fn lint_files( + args: Vec, + ignore: Vec, + json: bool, +) -> Result<(), AnyError> { + if args.len() == 1 && args[0].to_string_lossy() == "-" { + return lint_stdin(json); + } + let target_files = collect_files(args, ignore, is_supported_ext)?; + debug!("Found {} files", target_files.len()); + let target_files_len = target_files.len(); + + let has_error = Arc::new(AtomicBool::new(false)); + + let reporter_kind = if json { + LintReporterKind::Json + } else { + LintReporterKind::Pretty + }; + let reporter_lock = Arc::new(Mutex::new(create_reporter(reporter_kind))); + + run_parallelized(target_files, { + let reporter_lock = reporter_lock.clone(); + let has_error = has_error.clone(); + move |file_path| { + let r = lint_file(file_path.clone()); + let mut reporter = reporter_lock.lock().unwrap(); + + match r { + Ok((mut file_diagnostics, source)) => { + sort_diagnostics(&mut file_diagnostics); + for d in file_diagnostics.iter() { + has_error.store(true, Ordering::Relaxed); + reporter.visit_diagnostic(&d, source.split('\n').collect()); + } + } + Err(err) => { + has_error.store(true, Ordering::Relaxed); + reporter.visit_error(&file_path.to_string_lossy().to_string(), &err); + } + } + Ok(()) + } + }) + .await?; + + let has_error = has_error.load(Ordering::Relaxed); + + reporter_lock.lock().unwrap().close(target_files_len); + + if has_error { + std::process::exit(1); + } + + Ok(()) +} + +fn rule_to_json(rule: Box) -> serde_json::Value { + serde_json::json!({ + "code": rule.code(), + "tags": rule.tags(), + "docs": rule.docs(), + }) +} + +pub fn print_rules_list(json: bool) { + let lint_rules = rules::get_recommended_rules(); + + if json { + let json_rules: Vec = + lint_rules.into_iter().map(rule_to_json).collect(); + let json_str = serde_json::to_string_pretty(&json_rules).unwrap(); + println!("{}", json_str); + } else { + // The rules should still be printed even if `--quiet` option is enabled, + // so use `println!` here instead of `info!`. + println!("Available rules:"); + for rule in lint_rules { + println!(" - {}", rule.code()); + } + } +} + +fn create_linter(syntax: Syntax, rules: Vec>) -> Linter { + LinterBuilder::default() + .ignore_file_directive("deno-lint-ignore-file") + .ignore_diagnostic_directive("deno-lint-ignore") + .lint_unused_ignore_directives(true) + // TODO(bartlomieju): switch to true + .lint_unknown_rules(false) + .syntax(syntax) + .rules(rules) + .build() +} + +fn lint_file( + file_path: PathBuf, +) -> Result<(Vec, String), AnyError> { + let file_name = file_path.to_string_lossy().to_string(); + let source_code = fs::read_to_string(&file_path)?; + let media_type = MediaType::from(&file_path); + let syntax = ast::get_syntax(&media_type); + + let lint_rules = rules::get_recommended_rules(); + let mut linter = create_linter(syntax, lint_rules); + + let (_, file_diagnostics) = linter.lint(file_name, source_code.clone())?; + + Ok((file_diagnostics, source_code)) +} + +/// Lint stdin and write result to stdout. +/// Treats input as TypeScript. +/// Compatible with `--json` flag. +fn lint_stdin(json: bool) -> Result<(), AnyError> { + let mut source = String::new(); + if stdin().read_to_string(&mut source).is_err() { + return Err(generic_error("Failed to read from stdin")); + } + + let reporter_kind = if json { + LintReporterKind::Json + } else { + LintReporterKind::Pretty + }; + let mut reporter = create_reporter(reporter_kind); + let lint_rules = rules::get_recommended_rules(); + let syntax = ast::get_syntax(&MediaType::TypeScript); + let mut linter = create_linter(syntax, lint_rules); + let mut has_error = false; + let pseudo_file_name = "_stdin.ts"; + match linter + .lint(pseudo_file_name.to_string(), source.clone()) + .map_err(|e| e.into()) + { + Ok((_, diagnostics)) => { + for d in diagnostics { + has_error = true; + reporter.visit_diagnostic(&d, source.split('\n').collect()); + } + } + Err(err) => { + has_error = true; + reporter.visit_error(pseudo_file_name, &err); + } + } + + reporter.close(1); + + if has_error { + std::process::exit(1); + } + + Ok(()) +} + +trait LintReporter { + fn visit_diagnostic(&mut self, d: &LintDiagnostic, source_lines: Vec<&str>); + fn visit_error(&mut self, file_path: &str, err: &AnyError); + fn close(&mut self, check_count: usize); +} + +#[derive(Serialize)] +struct LintError { + file_path: String, + message: String, +} + +struct PrettyLintReporter { + lint_count: u32, +} + +impl PrettyLintReporter { + fn new() -> PrettyLintReporter { + PrettyLintReporter { lint_count: 0 } + } +} + +impl LintReporter for PrettyLintReporter { + fn visit_diagnostic(&mut self, d: &LintDiagnostic, source_lines: Vec<&str>) { + self.lint_count += 1; + + let pretty_message = + format!("({}) {}", colors::gray(&d.code), d.message.clone()); + + let message = format_diagnostic( + &pretty_message, + &source_lines, + d.range.clone(), + d.hint.as_ref(), + &fmt_errors::format_location(&JsStackFrame::from_location( + Some(d.filename.clone()), + Some(d.range.start.line as i64), + Some(d.range.start.col as i64), + )), + ); + + eprintln!("{}\n", message); + } + + fn visit_error(&mut self, file_path: &str, err: &AnyError) { + eprintln!("Error linting: {}", file_path); + eprintln!(" {}", err); + } + + fn close(&mut self, check_count: usize) { + match self.lint_count { + 1 => info!("Found 1 problem"), + n if n > 1 => info!("Found {} problems", self.lint_count), + _ => (), + } + + match check_count { + n if n <= 1 => info!("Checked {} file", n), + n if n > 1 => info!("Checked {} files", n), + _ => unreachable!(), + } + } +} + +pub fn format_diagnostic( + message_line: &str, + source_lines: &[&str], + range: deno_lint::diagnostic::Range, + maybe_hint: Option<&String>, + formatted_location: &str, +) -> String { + let mut lines = vec![]; + + for i in range.start.line..=range.end.line { + lines.push(source_lines[i - 1].to_string()); + if range.start.line == range.end.line { + lines.push(format!( + "{}{}", + " ".repeat(range.start.col), + colors::red(&"^".repeat(range.end.col - range.start.col)) + )); + } else { + let line_len = source_lines[i - 1].len(); + if range.start.line == i { + lines.push(format!( + "{}{}", + " ".repeat(range.start.col), + colors::red(&"^".repeat(line_len - range.start.col)) + )); + } else if range.end.line == i { + lines.push(colors::red(&"^".repeat(range.end.col)).to_string()); + } else if line_len != 0 { + lines.push(colors::red(&"^".repeat(line_len)).to_string()); + } + } + } + + if let Some(hint) = maybe_hint { + format!( + "{}\n{}\n at {}\n\n {} {}", + message_line, + lines.join("\n"), + formatted_location, + colors::gray("hint:"), + hint, + ) + } else { + format!( + "{}\n{}\n at {}", + message_line, + lines.join("\n"), + formatted_location + ) + } +} + +#[derive(Serialize)] +struct JsonLintReporter { + diagnostics: Vec, + errors: Vec, +} + +impl JsonLintReporter { + fn new() -> JsonLintReporter { + JsonLintReporter { + diagnostics: Vec::new(), + errors: Vec::new(), + } + } +} + +impl LintReporter for JsonLintReporter { + fn visit_diagnostic(&mut self, d: &LintDiagnostic, _source_lines: Vec<&str>) { + self.diagnostics.push(d.clone()); + } + + fn visit_error(&mut self, file_path: &str, err: &AnyError) { + self.errors.push(LintError { + file_path: file_path.to_string(), + message: err.to_string(), + }); + } + + fn close(&mut self, _check_count: usize) { + sort_diagnostics(&mut self.diagnostics); + let json = serde_json::to_string_pretty(&self); + eprintln!("{}", json.unwrap()); + } +} + +fn sort_diagnostics(diagnostics: &mut Vec) { + // Sort so that we guarantee a deterministic output which is useful for tests + diagnostics.sort_by(|a, b| { + use std::cmp::Ordering; + let file_order = a.filename.cmp(&b.filename); + match file_order { + Ordering::Equal => { + let line_order = a.range.start.line.cmp(&b.range.start.line); + match line_order { + Ordering::Equal => a.range.start.col.cmp(&b.range.start.col), + _ => line_order, + } + } + _ => file_order, + } + }); +} diff --git a/cli/tools/mod.rs b/cli/tools/mod.rs new file mode 100644 index 000000000..be76968fb --- /dev/null +++ b/cli/tools/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +pub mod coverage; +pub mod fmt; +pub mod installer; +pub mod lint; +pub mod repl; +pub mod test_runner; +pub mod upgrade; diff --git a/cli/tools/repl.rs b/cli/tools/repl.rs new file mode 100644 index 000000000..be85dc813 --- /dev/null +++ b/cli/tools/repl.rs @@ -0,0 +1,612 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::colors; +use crate::inspector::InspectorSession; +use crate::program_state::ProgramState; +use crate::worker::MainWorker; +use crate::worker::Worker; +use deno_core::error::AnyError; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use regex::Captures; +use regex::Regex; +use rustyline::completion::Completer; +use rustyline::error::ReadlineError; +use rustyline::highlight::Highlighter; +use rustyline::validate::ValidationContext; +use rustyline::validate::ValidationResult; +use rustyline::validate::Validator; +use rustyline::Context; +use rustyline::Editor; +use rustyline_derive::{Helper, Hinter}; +use std::borrow::Cow; +use std::sync::mpsc::channel; +use std::sync::mpsc::sync_channel; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; +use std::sync::mpsc::SyncSender; +use std::sync::Arc; +use std::sync::Mutex; + +// Provides helpers to the editor like validation for multi-line edits, completion candidates for +// tab completion. +#[derive(Helper, Hinter)] +struct Helper { + context_id: u64, + message_tx: SyncSender<(String, Option)>, + response_rx: Receiver>, + highlighter: LineHighlighter, +} + +impl Helper { + fn post_message( + &self, + method: &str, + params: Option, + ) -> Result { + self.message_tx.send((method.to_string(), params))?; + self.response_rx.recv()? + } +} + +fn is_word_boundary(c: char) -> bool { + if c == '.' { + false + } else { + char::is_ascii_whitespace(&c) || char::is_ascii_punctuation(&c) + } +} + +impl Completer for Helper { + type Candidate = String; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> Result<(usize, Vec), ReadlineError> { + let start = line[..pos].rfind(is_word_boundary).map_or_else(|| 0, |i| i); + let end = line[pos..] + .rfind(is_word_boundary) + .map_or_else(|| pos, |i| pos + i); + + let word = &line[start..end]; + let word = word.strip_prefix(is_word_boundary).unwrap_or(word); + let word = word.strip_suffix(is_word_boundary).unwrap_or(word); + + let fallback = format!(".{}", word); + + let (prefix, suffix) = match word.rfind('.') { + Some(index) => word.split_at(index), + None => ("globalThis", fallback.as_str()), + }; + + let evaluate_response = self + .post_message( + "Runtime.evaluate", + Some(json!({ + "contextId": self.context_id, + "expression": prefix, + "throwOnSideEffect": true, + "timeout": 200, + })), + ) + .unwrap(); + + if evaluate_response.get("exceptionDetails").is_some() { + let candidates = Vec::new(); + return Ok((pos, candidates)); + } + + if let Some(result) = evaluate_response.get("result") { + if let Some(object_id) = result.get("objectId") { + let get_properties_response = self + .post_message( + "Runtime.getProperties", + Some(json!({ + "objectId": object_id, + })), + ) + .unwrap(); + + if let Some(result) = get_properties_response.get("result") { + let candidates = result + .as_array() + .unwrap() + .iter() + .map(|r| r.get("name").unwrap().as_str().unwrap().to_string()) + .filter(|r| r.starts_with(&suffix[1..])) + .collect(); + + return Ok((pos - (suffix.len() - 1), candidates)); + } + } + } + + Ok((pos, Vec::new())) + } +} + +impl Validator for Helper { + fn validate( + &self, + ctx: &mut ValidationContext, + ) -> Result { + let mut stack: Vec = Vec::new(); + let mut literal: Option = None; + let mut escape: bool = false; + + for c in ctx.input().chars() { + if escape { + escape = false; + continue; + } + + if c == '\\' { + escape = true; + continue; + } + + if let Some(v) = literal { + if c == v { + literal = None + } + + continue; + } else { + literal = match c { + '`' | '"' | '/' | '\'' => Some(c), + _ => None, + }; + } + + match c { + '(' | '[' | '{' => stack.push(c), + ')' | ']' | '}' => match (stack.pop(), c) { + (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} + (Some(left), _) => { + return Ok(ValidationResult::Invalid(Some(format!( + "Mismatched pairs: {:?} is not properly closed", + left + )))) + } + (None, _) => { + // While technically invalid when unpaired, it should be V8's task to output error instead. + // Thus marked as valid with no info. + return Ok(ValidationResult::Valid(None)); + } + }, + _ => {} + } + } + + if !stack.is_empty() || literal == Some('`') { + return Ok(ValidationResult::Incomplete); + } + + Ok(ValidationResult::Valid(None)) + } +} + +impl Highlighter for Helper { + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + hint.into() + } + + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + self.highlighter.highlight(line, pos) + } + + fn highlight_candidate<'c>( + &self, + candidate: &'c str, + _completion: rustyline::CompletionType, + ) -> Cow<'c, str> { + self.highlighter.highlight(candidate, 0) + } + + fn highlight_char(&self, line: &str, _: usize) -> bool { + !line.is_empty() + } +} + +struct LineHighlighter { + regex: Regex, +} + +impl LineHighlighter { + fn new() -> Self { + let regex = Regex::new( + r#"(?x) + (?P(?:/\*[\s\S]*?\*/|//[^\n]*)) | + (?P(?:"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|`([^`\\]|\\.)*`)) | + (?P/(?:(?:\\/|[^\n/]))*?/[gimsuy]*) | + (?P\b\d+(?:\.\d+)?(?:e[+-]?\d+)*n?\b) | + (?P\b(?:Infinity|NaN)\b) | + (?P\b0x[a-fA-F0-9]+\b) | + (?P\b0o[0-7]+\b) | + (?P\b0b[01]+\b) | + (?P\b(?:true|false)\b) | + (?P\b(?:null)\b) | + (?P\b(?:undefined)\b) | + (?P\b(?:await|async|var|let|for|if|else|in|of|class|const|function|yield|return|with|case|break|switch|import|export|new|while|do|throw|catch|this)\b) | + "#, + ) + .unwrap(); + + Self { regex } + } +} + +impl Highlighter for LineHighlighter { + fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { + self + .regex + .replace_all(&line.to_string(), |caps: &Captures<'_>| { + if let Some(cap) = caps.name("comment") { + colors::gray(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("string") { + colors::green(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("regexp") { + colors::red(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("number") { + colors::yellow(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("boolean") { + colors::yellow(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("null") { + colors::yellow(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("undefined") { + colors::gray(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("keyword") { + colors::cyan(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("infinity") { + colors::yellow(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("classes") { + colors::green_bold(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("hexnumber") { + colors::yellow(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("octalnumber") { + colors::yellow(cap.as_str()).to_string() + } else if let Some(cap) = caps.name("binarynumber") { + colors::yellow(cap.as_str()).to_string() + } else { + caps[0].to_string() + } + }) + .to_string() + .into() + } +} + +async fn post_message_and_poll( + worker: &mut Worker, + session: &mut InspectorSession, + method: &str, + params: Option, +) -> Result { + let response = session.post_message(method, params); + tokio::pin!(response); + + loop { + tokio::select! { + result = &mut response => { + return result + } + + _ = worker.run_event_loop() => { + // A zero delay is long enough to yield the thread in order to prevent the loop from + // running hot for messages that are taking longer to resolve like for example an + // evaluation of top level await. + tokio::time::delay_for(tokio::time::Duration::from_millis(0)).await; + } + } + } +} + +async fn read_line_and_poll( + worker: &mut Worker, + session: &mut InspectorSession, + message_rx: &Receiver<(String, Option)>, + response_tx: &Sender>, + editor: Arc>>, +) -> Result { + let mut line = + tokio::task::spawn_blocking(move || editor.lock().unwrap().readline("> ")); + + let mut poll_worker = true; + + loop { + for (method, params) in message_rx.try_iter() { + response_tx + .send(session.post_message(&method, params).await) + .unwrap(); + } + + // Because an inspector websocket client may choose to connect at anytime when we have an + // inspector server we need to keep polling the worker to pick up new connections. + let mut timeout = + tokio::time::delay_for(tokio::time::Duration::from_millis(100)); + + tokio::select! { + result = &mut line => { + return result.unwrap(); + } + _ = worker.run_event_loop(), if poll_worker => { + poll_worker = false; + } + _ = &mut timeout => { + poll_worker = true + } + } + } +} + +static PRELUDE: &str = r#" +Object.defineProperty(globalThis, "_", { + configurable: true, + get: () => Deno[Deno.internal].lastEvalResult, + set: (value) => { + Object.defineProperty(globalThis, "_", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last evaluation result is no longer saved to _."); + }, +}); + +Object.defineProperty(globalThis, "_error", { + configurable: true, + get: () => Deno[Deno.internal].lastThrownError, + set: (value) => { + Object.defineProperty(globalThis, "_error", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + + console.log("Last thrown error is no longer saved to _error."); + }, +}); +"#; + +async fn inject_prelude( + worker: &mut MainWorker, + session: &mut InspectorSession, + context_id: u64, +) -> Result<(), AnyError> { + post_message_and_poll( + worker, + session, + "Runtime.evaluate", + Some(json!({ + "expression": PRELUDE, + "contextId": context_id, + })), + ) + .await?; + + Ok(()) +} + +pub async fn is_closing( + worker: &mut MainWorker, + session: &mut InspectorSession, + context_id: u64, +) -> Result { + let closed = post_message_and_poll( + worker, + session, + "Runtime.evaluate", + Some(json!({ + "expression": "(globalThis.closed)", + "contextId": context_id, + })), + ) + .await? + .get("result") + .unwrap() + .get("value") + .unwrap() + .as_bool() + .unwrap(); + + Ok(closed) +} + +pub async fn run( + program_state: &ProgramState, + mut worker: MainWorker, +) -> Result<(), AnyError> { + let mut session = worker.create_inspector_session(); + + let history_file = program_state.dir.root.join("deno_history.txt"); + + post_message_and_poll(&mut *worker, &mut session, "Runtime.enable", None) + .await?; + + // Enabling the runtime domain will always send trigger one executionContextCreated for each + // context the inspector knows about so we grab the execution context from that since + // our inspector does not support a default context (0 is an invalid context id). + let mut context_id: u64 = 0; + for notification in session.notifications() { + let method = notification.get("method").unwrap().as_str().unwrap(); + let params = notification.get("params").unwrap(); + + if method == "Runtime.executionContextCreated" { + context_id = params + .get("context") + .unwrap() + .get("id") + .unwrap() + .as_u64() + .unwrap(); + } + } + + let (message_tx, message_rx) = sync_channel(1); + let (response_tx, response_rx) = channel(); + + let helper = Helper { + context_id, + message_tx, + response_rx, + highlighter: LineHighlighter::new(), + }; + + let editor = Arc::new(Mutex::new(Editor::new())); + + editor.lock().unwrap().set_helper(Some(helper)); + + editor + .lock() + .unwrap() + .load_history(history_file.to_str().unwrap()) + .unwrap_or(()); + + println!("Deno {}", crate::version::DENO); + println!("exit using ctrl+d or close()"); + + inject_prelude(&mut worker, &mut session, context_id).await?; + + while !is_closing(&mut worker, &mut session, context_id).await? { + let line = read_line_and_poll( + &mut *worker, + &mut session, + &message_rx, + &response_tx, + editor.clone(), + ) + .await; + match line { + Ok(line) => { + // It is a bit unexpected that { "foo": "bar" } is interpreted as a block + // statement rather than an object literal so we interpret it as an expression statement + // to match the behavior found in a typical prompt including browser developer tools. + let wrapped_line = if line.trim_start().starts_with('{') + && !line.trim_end().ends_with(';') + { + format!("({})", &line) + } else { + line.clone() + }; + + let evaluate_response = post_message_and_poll( + &mut *worker, + &mut session, + "Runtime.evaluate", + Some(json!({ + "expression": format!("'use strict'; void 0;\n{}", &wrapped_line), + "contextId": context_id, + "replMode": true, + })), + ) + .await?; + + // If that fails, we retry it without wrapping in parens letting the error bubble up to the + // user if it is still an error. + let evaluate_response = + if evaluate_response.get("exceptionDetails").is_some() + && wrapped_line != line + { + post_message_and_poll( + &mut *worker, + &mut session, + "Runtime.evaluate", + Some(json!({ + "expression": format!("'use strict'; void 0;\n{}", &line), + "contextId": context_id, + "replMode": true, + })), + ) + .await? + } else { + evaluate_response + }; + + let evaluate_result = evaluate_response.get("result").unwrap(); + let evaluate_exception_details = + evaluate_response.get("exceptionDetails"); + + if evaluate_exception_details.is_some() { + post_message_and_poll( + &mut *worker, + &mut session, + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", + "arguments": [ + evaluate_result, + ], + })), + ).await?; + } else { + post_message_and_poll( + &mut *worker, + &mut session, + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }", + "arguments": [ + evaluate_result, + ], + })), + ).await?; + } + + // TODO(caspervonb) we should investigate using previews here but to keep things + // consistent with the previous implementation we just get the preview result from + // Deno.inspectArgs. + let inspect_response = + post_message_and_poll( + &mut *worker, + &mut session, + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": context_id, + "functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object], { colors: !Deno.noColor }); }", + "arguments": [ + evaluate_result, + ], + })), + ).await?; + + let inspect_result = inspect_response.get("result").unwrap(); + + let value = inspect_result.get("value").unwrap().as_str().unwrap(); + let output = match evaluate_exception_details { + Some(_) => format!("Uncaught {}", value), + None => value.to_string(), + }; + + println!("{}", output); + + editor.lock().unwrap().add_history_entry(line.as_str()); + } + Err(ReadlineError::Interrupted) => { + println!("exit using ctrl+d or close()"); + continue; + } + Err(ReadlineError::Eof) => { + break; + } + Err(err) => { + println!("Error: {:?}", err); + break; + } + } + } + + std::fs::create_dir_all(history_file.parent().unwrap())?; + editor + .lock() + .unwrap() + .save_history(history_file.to_str().unwrap())?; + + Ok(()) +} diff --git a/cli/tools/test_runner.rs b/cli/tools/test_runner.rs new file mode 100644 index 000000000..599a95059 --- /dev/null +++ b/cli/tools/test_runner.rs @@ -0,0 +1,173 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::fs_util; +use crate::tools::installer::is_remote_url; +use deno_core::error::AnyError; +use deno_core::serde_json::json; +use deno_core::url::Url; +use std::path::Path; +use std::path::PathBuf; + +fn is_supported(p: &Path) -> bool { + use std::path::Component; + if let Some(Component::Normal(basename_os_str)) = p.components().next_back() { + let basename = basename_os_str.to_string_lossy(); + basename.ends_with("_test.ts") + || basename.ends_with("_test.tsx") + || basename.ends_with("_test.js") + || basename.ends_with("_test.mjs") + || basename.ends_with("_test.jsx") + || basename.ends_with(".test.ts") + || basename.ends_with(".test.tsx") + || basename.ends_with(".test.js") + || basename.ends_with(".test.mjs") + || basename.ends_with(".test.jsx") + || basename == "test.ts" + || basename == "test.tsx" + || basename == "test.js" + || basename == "test.mjs" + || basename == "test.jsx" + } else { + false + } +} + +pub fn prepare_test_modules_urls( + include: Vec, + root_path: &PathBuf, +) -> Result, AnyError> { + let (include_paths, include_urls): (Vec, Vec) = + include.into_iter().partition(|n| !is_remote_url(n)); + + let mut prepared = vec![]; + + for path in include_paths { + let p = fs_util::normalize_path(&root_path.join(path)); + if p.is_dir() { + let test_files = + crate::fs_util::collect_files(vec![p], vec![], is_supported).unwrap(); + let test_files_as_urls = test_files + .iter() + .map(|f| Url::from_file_path(f).unwrap()) + .collect::>(); + prepared.extend(test_files_as_urls); + } else { + let url = Url::from_file_path(p).unwrap(); + prepared.push(url); + } + } + + for remote_url in include_urls { + let url = Url::parse(&remote_url)?; + prepared.push(url); + } + + Ok(prepared) +} + +pub fn render_test_file( + modules: Vec, + fail_fast: bool, + quiet: bool, + filter: Option, +) -> String { + let mut test_file = "".to_string(); + + for module in modules { + test_file.push_str(&format!("import \"{}\";\n", module.to_string())); + } + + let options = if let Some(filter) = filter { + json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet, "filter": filter }) + } else { + json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet }) + }; + + test_file.push_str("// @ts-ignore\n"); + + test_file.push_str(&format!( + "await Deno[Deno.internal].runTests({});\n", + options + )); + + test_file +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_prepare_test_modules_urls() { + let test_data_path = test_util::root_path().join("cli/tests/subdir"); + let mut matched_urls = prepare_test_modules_urls( + vec![ + "https://example.com/colors_test.ts".to_string(), + "./mod1.ts".to_string(), + "./mod3.js".to_string(), + "subdir2/mod2.ts".to_string(), + "http://example.com/printf_test.ts".to_string(), + ], + &test_data_path, + ) + .unwrap(); + let test_data_url = + Url::from_file_path(test_data_path).unwrap().to_string(); + + let expected: Vec = vec![ + format!("{}/mod1.ts", test_data_url), + format!("{}/mod3.js", test_data_url), + format!("{}/subdir2/mod2.ts", test_data_url), + "http://example.com/printf_test.ts".to_string(), + "https://example.com/colors_test.ts".to_string(), + ] + .into_iter() + .map(|f| Url::parse(&f).unwrap()) + .collect(); + matched_urls.sort(); + assert_eq!(matched_urls, expected); + } + + #[test] + fn test_is_supported() { + assert!(is_supported(Path::new("tests/subdir/foo_test.ts"))); + assert!(is_supported(Path::new("tests/subdir/foo_test.tsx"))); + assert!(is_supported(Path::new("tests/subdir/foo_test.js"))); + assert!(is_supported(Path::new("tests/subdir/foo_test.jsx"))); + assert!(is_supported(Path::new("bar/foo.test.ts"))); + assert!(is_supported(Path::new("bar/foo.test.tsx"))); + assert!(is_supported(Path::new("bar/foo.test.js"))); + assert!(is_supported(Path::new("bar/foo.test.jsx"))); + assert!(is_supported(Path::new("foo/bar/test.js"))); + assert!(is_supported(Path::new("foo/bar/test.jsx"))); + assert!(is_supported(Path::new("foo/bar/test.ts"))); + assert!(is_supported(Path::new("foo/bar/test.tsx"))); + assert!(!is_supported(Path::new("README.md"))); + assert!(!is_supported(Path::new("lib/typescript.d.ts"))); + assert!(!is_supported(Path::new("notatest.js"))); + assert!(!is_supported(Path::new("NotAtest.ts"))); + } + + #[test] + fn supports_dirs() { + let root = test_util::root_path().join("std").join("http"); + println!("root {:?}", root); + let mut matched_urls = + prepare_test_modules_urls(vec![".".to_string()], &root).unwrap(); + matched_urls.sort(); + let root_url = Url::from_file_path(root).unwrap().to_string(); + println!("root_url {}", root_url); + let expected: Vec = vec![ + format!("{}/_io_test.ts", root_url), + format!("{}/cookie_test.ts", root_url), + format!("{}/file_server_test.ts", root_url), + format!("{}/racing_server_test.ts", root_url), + format!("{}/server_test.ts", root_url), + format!("{}/test.ts", root_url), + ] + .into_iter() + .map(|f| Url::parse(&f).unwrap()) + .collect(); + assert_eq!(matched_urls, expected); + } +} diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs new file mode 100644 index 000000000..2bc28ec7d --- /dev/null +++ b/cli/tools/upgrade.rs @@ -0,0 +1,276 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! This module provides feature to upgrade deno executable +//! +//! At the moment it is only consumed using CLI but in +//! the future it can be easily extended to provide +//! the same functions as ops available in JS runtime. + +use crate::http_util::fetch_once; +use crate::http_util::FetchOnceResult; +use crate::AnyError; +use deno_core::error::custom_error; +use deno_core::futures::FutureExt; +use deno_core::url::Url; +use deno_fetch::reqwest; +use deno_fetch::reqwest::redirect::Policy; +use deno_fetch::reqwest::Client; +use regex::Regex; +use semver_parser::version::parse as semver_parse; +use semver_parser::version::Version; +use std::fs; +use std::future::Future; +use std::io::prelude::*; +use std::path::Path; +use std::path::PathBuf; +use std::pin::Pin; +use std::process::Command; +use std::process::Stdio; +use std::string::String; +use tempfile::TempDir; + +lazy_static! { + static ref ARCHIVE_NAME: String = format!("deno-{}.zip", env!("TARGET")); +} + +async fn get_latest_version(client: &Client) -> Result { + println!("Checking for latest version"); + let body = client + .get(Url::parse( + "https://github.com/denoland/deno/releases/latest", + )?) + .send() + .await? + .text() + .await?; + let v = find_version(&body)?; + Ok(semver_parse(&v).unwrap()) +} + +/// Asynchronously updates deno executable to greatest version +/// if greatest version is available. +pub async fn upgrade_command( + dry_run: bool, + force: bool, + version: Option, + output: Option, + ca_file: Option, +) -> Result<(), AnyError> { + let mut client_builder = Client::builder().redirect(Policy::none()); + + // If we have been provided a CA Certificate, add it into the HTTP client + if let Some(ca_file) = ca_file { + let buf = std::fs::read(ca_file); + let cert = reqwest::Certificate::from_pem(&buf.unwrap())?; + client_builder = client_builder.add_root_certificate(cert); + } + + let client = client_builder.build()?; + + let current_version = semver_parse(crate::version::DENO).unwrap(); + + let install_version = match version { + Some(passed_version) => match semver_parse(&passed_version) { + Ok(ver) => { + if !force && current_version == ver { + println!("Version {} is already installed", &ver); + return Ok(()); + } else { + ver + } + } + Err(_) => { + eprintln!("Invalid semver passed"); + std::process::exit(1) + } + }, + None => { + let latest_version = get_latest_version(&client).await?; + + if !force && current_version >= latest_version { + println!( + "Local deno version {} is the most recent release", + &crate::version::DENO + ); + return Ok(()); + } else { + latest_version + } + } + }; + + let archive_data = download_package( + &compose_url_to_exec(&install_version)?, + client, + &install_version, + ) + .await?; + let old_exe_path = std::env::current_exe()?; + let new_exe_path = unpack(archive_data)?; + let permissions = fs::metadata(&old_exe_path)?.permissions(); + fs::set_permissions(&new_exe_path, permissions)?; + check_exe(&new_exe_path, &install_version)?; + + if !dry_run { + match output { + Some(path) => { + fs::rename(&new_exe_path, &path) + .or_else(|_| fs::copy(&new_exe_path, &path).map(|_| ()))?; + } + None => replace_exe(&new_exe_path, &old_exe_path)?, + } + } + + println!("Upgrade done successfully"); + + Ok(()) +} + +fn download_package( + url: &Url, + client: Client, + version: &Version, +) -> Pin, AnyError>>>> { + println!("downloading {}", url); + let url = url.clone(); + let version = version.clone(); + let fut = async move { + match fetch_once(client.clone(), &url, None).await { + Ok(result) => { + println!( + "Version has been found\nDeno is upgrading to version {}", + &version + ); + match result { + FetchOnceResult::Code(source, _) => Ok(source), + FetchOnceResult::NotModified => unreachable!(), + FetchOnceResult::Redirect(_url, _) => { + download_package(&_url, client, &version).await + } + } + } + Err(_) => { + println!("Version has not been found, aborting"); + std::process::exit(1) + } + } + }; + fut.boxed_local() +} + +fn compose_url_to_exec(version: &Version) -> Result { + let s = format!( + "https://github.com/denoland/deno/releases/download/v{}/{}", + version, *ARCHIVE_NAME + ); + Url::parse(&s).map_err(AnyError::from) +} + +fn find_version(text: &str) -> Result { + let re = Regex::new(r#"v([^\?]+)?""#)?; + if let Some(_mat) = re.find(text) { + let mat = _mat.as_str(); + return Ok(mat[1..mat.len() - 1].to_string()); + } + Err(custom_error("NotFound", "Cannot read latest tag version")) +} + +fn unpack(archive_data: Vec) -> Result { + // We use into_path so that the tempdir is not automatically deleted. This is + // useful for debugging upgrade, but also so this function can return a path + // to the newly uncompressed file without fear of the tempdir being deleted. + let temp_dir = TempDir::new()?.into_path(); + let exe_ext = if cfg!(windows) { "exe" } else { "" }; + let exe_path = temp_dir.join("deno").with_extension(exe_ext); + assert!(!exe_path.exists()); + + let archive_ext = Path::new(&*ARCHIVE_NAME) + .extension() + .and_then(|ext| ext.to_str()) + .unwrap(); + let unpack_status = match archive_ext { + "gz" => { + let exe_file = fs::File::create(&exe_path)?; + let mut cmd = Command::new("gunzip") + .arg("-c") + .stdin(Stdio::piped()) + .stdout(Stdio::from(exe_file)) + .spawn()?; + cmd.stdin.as_mut().unwrap().write_all(&archive_data)?; + cmd.wait()? + } + "zip" if cfg!(windows) => { + let archive_path = temp_dir.join("deno.zip"); + fs::write(&archive_path, &archive_data)?; + Command::new("powershell.exe") + .arg("-NoLogo") + .arg("-NoProfile") + .arg("-NonInteractive") + .arg("-Command") + .arg( + "& { + param($Path, $DestinationPath) + trap { $host.ui.WriteErrorLine($_.Exception); exit 1 } + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory( + $Path, + $DestinationPath + ); + }", + ) + .arg("-Path") + .arg(format!("'{}'", &archive_path.to_str().unwrap())) + .arg("-DestinationPath") + .arg(format!("'{}'", &temp_dir.to_str().unwrap())) + .spawn()? + .wait()? + } + "zip" => { + let archive_path = temp_dir.join("deno.zip"); + fs::write(&archive_path, &archive_data)?; + Command::new("unzip") + .current_dir(&temp_dir) + .arg(archive_path) + .spawn()? + .wait()? + } + ext => panic!("Unsupported archive type: '{}'", ext), + }; + assert!(unpack_status.success()); + assert!(exe_path.exists()); + Ok(exe_path) +} + +fn replace_exe(new: &Path, old: &Path) -> Result<(), std::io::Error> { + if cfg!(windows) { + // On windows you cannot replace the currently running executable. + // so first we rename it to deno.old.exe + fs::rename(old, old.with_extension("old.exe"))?; + } else { + fs::remove_file(old)?; + } + // Windows cannot rename files across device boundaries, so if rename fails, + // we try again with copy. + fs::rename(new, old).or_else(|_| fs::copy(new, old).map(|_| ()))?; + Ok(()) +} + +fn check_exe( + exe_path: &Path, + expected_version: &Version, +) -> Result<(), AnyError> { + let output = Command::new(exe_path) + .arg("-V") + .stderr(std::process::Stdio::inherit()) + .output()?; + let stdout = String::from_utf8(output.stdout)?; + assert!(output.status.success()); + assert_eq!(stdout.trim(), format!("deno {}", expected_version)); + Ok(()) +} + +#[test] +fn test_find_version() { + let url = "You are being redirected."; + assert_eq!(find_version(url).unwrap(), "0.36.0".to_string()); +} diff --git a/cli/upgrade.rs b/cli/upgrade.rs deleted file mode 100644 index 2bc28ec7d..000000000 --- a/cli/upgrade.rs +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -//! This module provides feature to upgrade deno executable -//! -//! At the moment it is only consumed using CLI but in -//! the future it can be easily extended to provide -//! the same functions as ops available in JS runtime. - -use crate::http_util::fetch_once; -use crate::http_util::FetchOnceResult; -use crate::AnyError; -use deno_core::error::custom_error; -use deno_core::futures::FutureExt; -use deno_core::url::Url; -use deno_fetch::reqwest; -use deno_fetch::reqwest::redirect::Policy; -use deno_fetch::reqwest::Client; -use regex::Regex; -use semver_parser::version::parse as semver_parse; -use semver_parser::version::Version; -use std::fs; -use std::future::Future; -use std::io::prelude::*; -use std::path::Path; -use std::path::PathBuf; -use std::pin::Pin; -use std::process::Command; -use std::process::Stdio; -use std::string::String; -use tempfile::TempDir; - -lazy_static! { - static ref ARCHIVE_NAME: String = format!("deno-{}.zip", env!("TARGET")); -} - -async fn get_latest_version(client: &Client) -> Result { - println!("Checking for latest version"); - let body = client - .get(Url::parse( - "https://github.com/denoland/deno/releases/latest", - )?) - .send() - .await? - .text() - .await?; - let v = find_version(&body)?; - Ok(semver_parse(&v).unwrap()) -} - -/// Asynchronously updates deno executable to greatest version -/// if greatest version is available. -pub async fn upgrade_command( - dry_run: bool, - force: bool, - version: Option, - output: Option, - ca_file: Option, -) -> Result<(), AnyError> { - let mut client_builder = Client::builder().redirect(Policy::none()); - - // If we have been provided a CA Certificate, add it into the HTTP client - if let Some(ca_file) = ca_file { - let buf = std::fs::read(ca_file); - let cert = reqwest::Certificate::from_pem(&buf.unwrap())?; - client_builder = client_builder.add_root_certificate(cert); - } - - let client = client_builder.build()?; - - let current_version = semver_parse(crate::version::DENO).unwrap(); - - let install_version = match version { - Some(passed_version) => match semver_parse(&passed_version) { - Ok(ver) => { - if !force && current_version == ver { - println!("Version {} is already installed", &ver); - return Ok(()); - } else { - ver - } - } - Err(_) => { - eprintln!("Invalid semver passed"); - std::process::exit(1) - } - }, - None => { - let latest_version = get_latest_version(&client).await?; - - if !force && current_version >= latest_version { - println!( - "Local deno version {} is the most recent release", - &crate::version::DENO - ); - return Ok(()); - } else { - latest_version - } - } - }; - - let archive_data = download_package( - &compose_url_to_exec(&install_version)?, - client, - &install_version, - ) - .await?; - let old_exe_path = std::env::current_exe()?; - let new_exe_path = unpack(archive_data)?; - let permissions = fs::metadata(&old_exe_path)?.permissions(); - fs::set_permissions(&new_exe_path, permissions)?; - check_exe(&new_exe_path, &install_version)?; - - if !dry_run { - match output { - Some(path) => { - fs::rename(&new_exe_path, &path) - .or_else(|_| fs::copy(&new_exe_path, &path).map(|_| ()))?; - } - None => replace_exe(&new_exe_path, &old_exe_path)?, - } - } - - println!("Upgrade done successfully"); - - Ok(()) -} - -fn download_package( - url: &Url, - client: Client, - version: &Version, -) -> Pin, AnyError>>>> { - println!("downloading {}", url); - let url = url.clone(); - let version = version.clone(); - let fut = async move { - match fetch_once(client.clone(), &url, None).await { - Ok(result) => { - println!( - "Version has been found\nDeno is upgrading to version {}", - &version - ); - match result { - FetchOnceResult::Code(source, _) => Ok(source), - FetchOnceResult::NotModified => unreachable!(), - FetchOnceResult::Redirect(_url, _) => { - download_package(&_url, client, &version).await - } - } - } - Err(_) => { - println!("Version has not been found, aborting"); - std::process::exit(1) - } - } - }; - fut.boxed_local() -} - -fn compose_url_to_exec(version: &Version) -> Result { - let s = format!( - "https://github.com/denoland/deno/releases/download/v{}/{}", - version, *ARCHIVE_NAME - ); - Url::parse(&s).map_err(AnyError::from) -} - -fn find_version(text: &str) -> Result { - let re = Regex::new(r#"v([^\?]+)?""#)?; - if let Some(_mat) = re.find(text) { - let mat = _mat.as_str(); - return Ok(mat[1..mat.len() - 1].to_string()); - } - Err(custom_error("NotFound", "Cannot read latest tag version")) -} - -fn unpack(archive_data: Vec) -> Result { - // We use into_path so that the tempdir is not automatically deleted. This is - // useful for debugging upgrade, but also so this function can return a path - // to the newly uncompressed file without fear of the tempdir being deleted. - let temp_dir = TempDir::new()?.into_path(); - let exe_ext = if cfg!(windows) { "exe" } else { "" }; - let exe_path = temp_dir.join("deno").with_extension(exe_ext); - assert!(!exe_path.exists()); - - let archive_ext = Path::new(&*ARCHIVE_NAME) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap(); - let unpack_status = match archive_ext { - "gz" => { - let exe_file = fs::File::create(&exe_path)?; - let mut cmd = Command::new("gunzip") - .arg("-c") - .stdin(Stdio::piped()) - .stdout(Stdio::from(exe_file)) - .spawn()?; - cmd.stdin.as_mut().unwrap().write_all(&archive_data)?; - cmd.wait()? - } - "zip" if cfg!(windows) => { - let archive_path = temp_dir.join("deno.zip"); - fs::write(&archive_path, &archive_data)?; - Command::new("powershell.exe") - .arg("-NoLogo") - .arg("-NoProfile") - .arg("-NonInteractive") - .arg("-Command") - .arg( - "& { - param($Path, $DestinationPath) - trap { $host.ui.WriteErrorLine($_.Exception); exit 1 } - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory( - $Path, - $DestinationPath - ); - }", - ) - .arg("-Path") - .arg(format!("'{}'", &archive_path.to_str().unwrap())) - .arg("-DestinationPath") - .arg(format!("'{}'", &temp_dir.to_str().unwrap())) - .spawn()? - .wait()? - } - "zip" => { - let archive_path = temp_dir.join("deno.zip"); - fs::write(&archive_path, &archive_data)?; - Command::new("unzip") - .current_dir(&temp_dir) - .arg(archive_path) - .spawn()? - .wait()? - } - ext => panic!("Unsupported archive type: '{}'", ext), - }; - assert!(unpack_status.success()); - assert!(exe_path.exists()); - Ok(exe_path) -} - -fn replace_exe(new: &Path, old: &Path) -> Result<(), std::io::Error> { - if cfg!(windows) { - // On windows you cannot replace the currently running executable. - // so first we rename it to deno.old.exe - fs::rename(old, old.with_extension("old.exe"))?; - } else { - fs::remove_file(old)?; - } - // Windows cannot rename files across device boundaries, so if rename fails, - // we try again with copy. - fs::rename(new, old).or_else(|_| fs::copy(new, old).map(|_| ()))?; - Ok(()) -} - -fn check_exe( - exe_path: &Path, - expected_version: &Version, -) -> Result<(), AnyError> { - let output = Command::new(exe_path) - .arg("-V") - .stderr(std::process::Stdio::inherit()) - .output()?; - let stdout = String::from_utf8(output.stdout)?; - assert!(output.status.success()); - assert_eq!(stdout.trim(), format!("deno {}", expected_version)); - Ok(()) -} - -#[test] -fn test_find_version() { - let url = "You are being redirected."; - assert_eq!(find_version(url).unwrap(), "0.36.0".to_string()); -} -- cgit v1.2.3