summaryrefslogtreecommitdiff
path: root/cli/tools
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools')
-rw-r--r--cli/tools/coverage.rs238
-rw-r--r--cli/tools/fmt.rs283
-rw-r--r--cli/tools/installer.rs813
-rw-r--r--cli/tools/lint.rs364
-rw-r--r--cli/tools/mod.rs9
-rw-r--r--cli/tools/repl.rs612
-rw-r--r--cli/tools/test_runner.rs173
-rw-r--r--cli/tools/upgrade.rs276
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());
+}