diff options
Diffstat (limited to 'cli/tools')
-rw-r--r-- | cli/tools/coverage.rs | 238 | ||||
-rw-r--r-- | cli/tools/fmt.rs | 283 | ||||
-rw-r--r-- | cli/tools/installer.rs | 813 | ||||
-rw-r--r-- | cli/tools/lint.rs | 364 | ||||
-rw-r--r-- | cli/tools/mod.rs | 9 | ||||
-rw-r--r-- | cli/tools/repl.rs | 612 | ||||
-rw-r--r-- | cli/tools/test_runner.rs | 173 | ||||
-rw-r--r-- | cli/tools/upgrade.rs | 276 |
8 files changed, 2768 insertions, 0 deletions
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<InspectorSession>, +} + +impl CoverageCollector { + pub fn new(session: Box<InspectorSession>) -> 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<Vec<Coverage>, 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<Coverage> = 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<CoverageRange>, + 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<FunctionCoverage>, +} + +#[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<ScriptCoverage>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetScriptSourceResult { + pub script_source: String, + pub bytecode: Option<String>, +} + +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::<Vec<_>>(); + + let mut covered_lines: Vec<usize> = Vec::new(); + let mut uncovered_lines: Vec<usize> = 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<Coverage>, + test_file_url: Url, + test_modules: Vec<Url>, +) -> Vec<Coverage> { + 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::<Vec<Coverage>>() +} 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<PathBuf>, + check: bool, + exclude: Vec<PathBuf>, +) -> 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<PathBuf>, +) -> 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<PathBuf>, +) -> 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<FileContents, AnyError> { + 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<F>( + file_paths: Vec<PathBuf>, + 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::<Vec<_>>(); + 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<String>, +) -> Result<(), AnyError> { + let args: Vec<String> = 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<String>, +) -> Result<(), AnyError> { + use shell_escape::escape; + let args: Vec<String> = 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<PathBuf, io::Error> { + 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<String> { + 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<String>, + name: Option<String>, + root: Option<PathBuf>, + 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<dyn LintReporter + Send> { + match kind { + LintReporterKind::Pretty => Box::new(PrettyLintReporter::new()), + LintReporterKind::Json => Box::new(JsonLintReporter::new()), + } +} + +pub async fn lint_files( + args: Vec<PathBuf>, + ignore: Vec<PathBuf>, + 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<dyn LintRule>) -> 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<serde_json::Value> = + 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<Box<dyn LintRule>>) -> 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<LintDiagnostic>, 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<LintDiagnostic>, + errors: Vec<LintError>, +} + +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<LintDiagnostic>) { + // 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<Value>)>, + response_rx: Receiver<Result<Value, AnyError>>, + highlighter: LineHighlighter, +} + +impl Helper { + fn post_message( + &self, + method: &str, + params: Option<Value>, + ) -> Result<Value, AnyError> { + 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<String>), 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<ValidationResult, ReadlineError> { + let mut stack: Vec<char> = Vec::new(); + let mut literal: Option<char> = 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<comment>(?:/\*[\s\S]*?\*/|//[^\n]*)) | + (?P<string>(?:"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|`([^`\\]|\\.)*`)) | + (?P<regexp>/(?:(?:\\/|[^\n/]))*?/[gimsuy]*) | + (?P<number>\b\d+(?:\.\d+)?(?:e[+-]?\d+)*n?\b) | + (?P<infinity>\b(?:Infinity|NaN)\b) | + (?P<hexnumber>\b0x[a-fA-F0-9]+\b) | + (?P<octalnumber>\b0o[0-7]+\b) | + (?P<binarynumber>\b0b[01]+\b) | + (?P<boolean>\b(?:true|false)\b) | + (?P<null>\b(?:null)\b) | + (?P<undefined>\b(?:undefined)\b) | + (?P<keyword>\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<Value>, +) -> Result<Value, AnyError> { + 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<Value>)>, + response_tx: &Sender<Result<Value, AnyError>>, + editor: Arc<Mutex<Editor<Helper>>>, +) -> Result<String, ReadlineError> { + 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<bool, AnyError> { + 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<String>, + root_path: &PathBuf, +) -> Result<Vec<Url>, AnyError> { + let (include_paths, include_urls): (Vec<String>, Vec<String>) = + 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::<Vec<Url>>(); + 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<Url>, + fail_fast: bool, + quiet: bool, + filter: Option<String>, +) -> 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<Url> = 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<Url> = 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<Version, AnyError> { + 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<String>, + output: Option<PathBuf>, + ca_file: Option<String>, +) -> 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<Box<dyn Future<Output = Result<Vec<u8>, 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<Url, AnyError> { + 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<String, AnyError> { + 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<u8>) -> Result<PathBuf, std::io::Error> { + // 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 = "<html><body>You are being <a href=\"https://github.com/denoland/deno/releases/tag/v0.36.0\">redirected</a>.</body></html>"; + assert_eq!(find_version(url).unwrap(), "0.36.0".to_string()); +} |