diff --git a/Cargo.lock b/Cargo.lock index a12141b85f5235..7e67d6e3726a06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,7 +344,7 @@ dependencies = [ [[package]] name = "deno" -version = "0.32.0" +version = "0.33.0" dependencies = [ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", @@ -353,8 +353,8 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", - "deno_core 0.32.0", - "deno_typescript 0.32.0", + "deno_core 0.33.0", + "deno_typescript 0.33.0", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "dlopen 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "dprint-plugin-typescript 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -392,7 +392,7 @@ dependencies = [ [[package]] name = "deno_core" -version = "0.32.0" +version = "0.33.0" dependencies = [ "derive_deref 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "downcast-rs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -408,9 +408,9 @@ dependencies = [ [[package]] name = "deno_typescript" -version = "0.32.0" +version = "0.33.0" dependencies = [ - "deno_core 0.32.0", + "deno_core 0.33.0", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1909,8 +1909,8 @@ dependencies = [ name = "test_plugin" version = "0.0.1" dependencies = [ - "deno 0.32.0", - "deno_core 0.32.0", + "deno 0.33.0", + "deno_core 0.33.0", "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Releases.md b/Releases.md index f0a45b2d460918..1a3306ecef62d8 100644 --- a/Releases.md +++ b/Releases.md @@ -6,6 +6,34 @@ https://github.com/denoland/deno/releases We also have one-line install commands at https://github.com/denoland/deno_install +### v0.33.0 / 2020.02.13 + +- feat(std/http): support trailer headers (#3938, #3989) +- feat(std/node): Add readlink, readlinkSync (#3926) +- feat(std/node): Event emitter node polyfill (#3944, #3959, #3960) +- feat(deno install): add --force flag and remove yes/no prompt (#3917) +- feat: Improve support for diagnostics from runtime compiler APIs (#3911) +- feat: `deno fmt -` formats stdin and print to stdout (#3920) +- feat: add std/signal (#3913) +- feat: make testing API built-in Deno.test() (#3865, #3930, #3973) +- fix(std/http): align serve and serveTLS APIs (#3881) +- fix(std/http/file_server): don't crash on "%" pathname (#3953) +- fix(std/path): Use non-capturing groups in globrex() (#3898) +- fix(deno types): don't panic when piped to head (#3910) +- fix(deno fmt): support top-level await (#3952) +- fix: Correctly determine a --cached-only error (#3979) +- fix: No longer require aligned buffer for shared queue (#3935) +- fix: Prevent providing --allow-env flag twice (#3906) +- fix: Remove unnecessary EOF check in Deno.toAsyncIterable (#3914) +- fix: WASM imports loaded HTTP (#3856) +- fix: better WebWorker API compatibility (#3828 ) +- fix: deno fmt improvements (#3988) +- fix: make WebSocket.send() exclusive (#3885) +- refactor: Improve `deno bundle` by using System instead of AMD (#3965) +- refactor: Remove conditionals from installer (#3909) +- refactor: peg workers to a single thread (#3844, #3968, #3931, #3903, #3912, + #3907, #3904) + ### v0.32.0 / 2020.02.03 - BREAKING CHANGE: Replace formatter for "deno fmt", use dprint (#3820, #3824, diff --git a/cli/Cargo.toml b/cli/Cargo.toml index cdc783d9d64c9a..a8f2e1dd31276a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno" -version = "0.32.0" +version = "0.33.0" license = "MIT" authors = ["the Deno authors"] edition = "2018" @@ -19,12 +19,12 @@ name = "deno" path = "main.rs" [build-dependencies] -deno_core = { path = "../core", version = "0.32.0" } -deno_typescript = { path = "../deno_typescript", version = "0.32.0" } +deno_core = { path = "../core", version = "0.33.0" } +deno_typescript = { path = "../deno_typescript", version = "0.33.0" } [dependencies] -deno_core = { path = "../core", version = "0.32.0" } -deno_typescript = { path = "../deno_typescript", version = "0.32.0" } +deno_core = { path = "../core", version = "0.33.0" } +deno_typescript = { path = "../deno_typescript", version = "0.33.0" } ansi_term = "0.11.0" atty = "0.2.13" diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index ea918a8629affe..e69756b20ab962 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -206,18 +206,20 @@ impl SourceFileFetcher { } else { "".to_owned() }; - let err = if err_kind == ErrorKind::NotFound { + // Hack: Check error message for "--cached-only" because the kind + // conflicts with other errors. + let err = if err.to_string().contains("--cached-only") { let msg = format!( - r#"Cannot resolve module "{}"{}"#, + r#"Cannot find module "{}"{} in cache, --cached-only is specified"#, module_url, referrer_suffix ); DenoError::new(ErrorKind::NotFound, msg).into() - } else if err_kind == ErrorKind::PermissionDenied { + } else if err_kind == ErrorKind::NotFound { let msg = format!( - r#"Cannot find module "{}"{} in cache, --cached-only is specified"#, + r#"Cannot resolve module "{}"{}"#, module_url, referrer_suffix ); - DenoError::new(ErrorKind::PermissionDenied, msg).into() + DenoError::new(ErrorKind::NotFound, msg).into() } else { err }; @@ -428,9 +430,9 @@ impl SourceFileFetcher { // We can't fetch remote file - bail out return futures::future::err( std::io::Error::new( - std::io::ErrorKind::PermissionDenied, + std::io::ErrorKind::NotFound, format!( - "cannot find remote file '{}' in cache", + "Cannot find remote file '{}' in cache, --cached-only is specified", module_url.to_string() ), ) @@ -1451,7 +1453,7 @@ mod tests { .await; assert!(result.is_err()); let err = result.err().unwrap(); - assert_eq!(err.kind(), ErrorKind::PermissionDenied); + assert_eq!(err.kind(), ErrorKind::NotFound); // download and cache file let result = fetcher_1 diff --git a/cli/flags.rs b/cli/flags.rs index 4a037c52187906..82cd59ca764f58 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -37,9 +37,9 @@ pub enum DenoSubcommand { Fetch { files: Vec, }, - Format { + Fmt { check: bool, - files: Option>, + files: Vec, }, Help, Info { @@ -302,19 +302,13 @@ fn types_parse(flags: &mut DenoFlags, _matches: &clap::ArgMatches) { } fn fmt_parse(flags: &mut DenoFlags, matches: &clap::ArgMatches) { - let maybe_files = match matches.values_of("files") { - Some(f) => { - let files: Vec = f.map(PathBuf::from).collect(); - Some(files) - } - None => None, + let files = match matches.values_of("files") { + Some(f) => f.map(String::from).collect(), + None => vec![], }; - - let check = matches.is_present("check"); - - flags.subcommand = DenoSubcommand::Format { - check, - files: maybe_files, + flags.subcommand = DenoSubcommand::Fmt { + check: matches.is_present("check"), + files, } } @@ -543,7 +537,7 @@ The declaration file could be saved and used for typing information.", fn fmt_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("fmt") - .about("Format files") + .about("Format source files") .long_about( "Auto-format JavaScript/TypeScript source code @@ -1391,12 +1385,9 @@ mod tests { assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Format { + subcommand: DenoSubcommand::Fmt { check: false, - files: Some(vec![ - PathBuf::from("script_1.ts"), - PathBuf::from("script_2.ts") - ]) + files: vec!["script_1.ts".to_string(), "script_2.ts".to_string()] }, ..DenoFlags::default() } @@ -1406,9 +1397,9 @@ mod tests { assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Format { + subcommand: DenoSubcommand::Fmt { check: true, - files: None + files: vec![], }, ..DenoFlags::default() } @@ -1418,9 +1409,9 @@ mod tests { assert_eq!( r.unwrap(), DenoFlags { - subcommand: DenoSubcommand::Format { + subcommand: DenoSubcommand::Fmt { check: false, - files: None + files: vec![], }, ..DenoFlags::default() } diff --git a/cli/fmt.rs b/cli/fmt.rs index f02df560a4c3fe..c9b0ef82660b5b 100644 --- a/cli/fmt.rs +++ b/cli/fmt.rs @@ -7,12 +7,9 @@ //! the future it can be easily extended to provide //! the same functions as ops available in JS runtime. -use crate::deno_error::DenoError; -use crate::deno_error::ErrorKind; use deno_core::ErrBox; use dprint_plugin_typescript as dprint; use glob; -use regex::Regex; use std::fs; use std::io::stdin; use std::io::stdout; @@ -22,16 +19,20 @@ use std::path::Path; use std::path::PathBuf; use std::time::Instant; -lazy_static! { - static ref TYPESCRIPT_LIB: Regex = Regex::new(".d.ts$").unwrap(); - static ref TYPESCRIPT: Regex = Regex::new(".tsx?$").unwrap(); - static ref JAVASCRIPT: Regex = Regex::new(".jsx?$").unwrap(); -} - fn is_supported(path: &Path) -> bool { - let path_str = path.to_string_lossy(); - !TYPESCRIPT_LIB.is_match(&path_str) - && (TYPESCRIPT.is_match(&path_str) || JAVASCRIPT.is_match(&path_str)) + if let Some(ext) = path.extension() { + if ext == "tsx" || ext == "js" || ext == "jsx" { + true + } else if ext == "ts" { + // Currently dprint does not support d.ts files. + // https://github.com/dsherret/dprint/issues/100 + !path.as_os_str().to_string_lossy().ends_with(".d.ts") + } else { + false + } + } else { + false + } } fn get_config() -> dprint::configuration::Configuration { @@ -47,22 +48,10 @@ fn get_config() -> dprint::configuration::Configuration { .build() } -fn get_supported_files(paths: Vec) -> Vec { - let mut files_to_check = vec![]; - - for path in paths { - if is_supported(&path) { - files_to_check.push(path.to_owned()); - } - } - - files_to_check -} - fn check_source_files( config: dprint::configuration::Configuration, paths: Vec, -) { +) -> Result<(), ErrBox> { let start = Instant::now(); let mut not_formatted_files = vec![]; @@ -75,7 +64,6 @@ fn check_source_files( } Ok(Some(formatted_text)) => { if formatted_text != file_contents { - println!("Not formatted {}", file_path_str); not_formatted_files.push(file_path); } } @@ -88,20 +76,20 @@ fn check_source_files( let duration = Instant::now() - start; - if !not_formatted_files.is_empty() { + if not_formatted_files.is_empty() { + Ok(()) + } else { let f = if not_formatted_files.len() == 1 { "file" } else { "files" }; - - eprintln!( + Err(crate::deno_error::other_error(format!( "Found {} not formatted {} in {:?}", not_formatted_files.len(), f, duration - ); - std::process::exit(1); + ))) } } @@ -147,56 +135,57 @@ fn format_source_files( ); } -fn get_matching_files(glob_paths: Vec) -> Vec { - let mut target_files = Vec::with_capacity(128); - - for path in glob_paths { - let files = glob::glob(&path.to_str().unwrap()) - .expect("Failed to execute glob.") - .filter_map(Result::ok); - target_files.extend(files); - } - - target_files +pub fn source_files_in_subtree(root: PathBuf) -> Vec { + assert!(root.is_dir()); + // TODO(ry) Use WalkDir instead of globs. + let g = root.join("**/*"); + glob::glob(&g.into_os_string().into_string().unwrap()) + .expect("Failed to execute glob.") + .filter_map(|result| { + if let Ok(p) = result { + if is_supported(&p) { + Some(p) + } else { + None + } + } else { + None + } + }) + .collect() } /// Format JavaScript/TypeScript files. /// /// First argument supports globs, and if it is `None` /// then the current directory is recursively walked. -pub fn format_files( - maybe_files: Option>, - check: bool, -) -> Result<(), ErrBox> { - // TODO: improve glob to look for tsx?/jsx? files only - let glob_paths = maybe_files.unwrap_or_else(|| vec![PathBuf::from("**/*")]); - - for glob_path in glob_paths.iter() { - if glob_path.to_str().unwrap() == "-" { - // If the only given path is '-', format stdin. - if glob_paths.len() == 1 { - format_stdin(check); +pub fn format_files(args: Vec, check: bool) -> Result<(), ErrBox> { + if args.len() == 1 && args[0] == "-" { + format_stdin(check); + return Ok(()); + } + + let mut target_files: Vec = vec![]; + + if args.is_empty() { + target_files + .extend(source_files_in_subtree(std::env::current_dir().unwrap())); + } else { + for arg in args { + let p = PathBuf::from(arg); + if p.is_dir() { + target_files.extend(source_files_in_subtree(p)); } else { - // Otherwise it is an error - return Err(ErrBox::from(DenoError::new( - ErrorKind::Other, - "Ambiguous filename input. To format stdin, provide a single '-' instead.".to_owned() - ))); - } - return Ok(()); + target_files.push(p); + }; } } - - let matching_files = get_matching_files(glob_paths); - let matching_files = get_supported_files(matching_files); let config = get_config(); - if check { - check_source_files(config, matching_files); + check_source_files(config, target_files)?; } else { - format_source_files(config, matching_files); + format_source_files(config, target_files); } - Ok(()) } @@ -211,10 +200,7 @@ fn format_stdin(check: bool) { let config = get_config(); match dprint::format_text("_stdin.ts", &source, &config) { - Ok(None) => { - // Should not happen. - unreachable!(); - } + Ok(None) => unreachable!(), Ok(Some(formatted_text)) => { if check { if formatted_text != source { @@ -231,3 +217,22 @@ fn format_stdin(check: bool) { } } } + +#[test] +fn test_is_supported() { + assert!(!is_supported(Path::new("tests/subdir/redirects"))); + assert!(!is_supported(Path::new("README.md"))); + assert!(!is_supported(Path::new("lib/typescript.d.ts"))); + assert!(is_supported(Path::new("cli/tests/001_hello.js"))); + assert!(is_supported(Path::new("cli/tests/002_hello.ts"))); + assert!(is_supported(Path::new("foo.jsx"))); + assert!(is_supported(Path::new("foo.tsx"))); +} + +#[test] +fn check_tests_dir() { + // Because of cli/tests/error_syntax.js the following should fail but not + // crash. + let r = format_files(vec!["./tests".to_string()], true); + assert!(r.is_err()); +} diff --git a/cli/import_map.rs b/cli/import_map.rs index f9c50345d31a1f..51c03a4e12d803 100644 --- a/cli/import_map.rs +++ b/cli/import_map.rs @@ -147,7 +147,7 @@ impl ImportMap { /// Parse provided key as import map specifier. /// - /// Specifiers must be valid URLs (eg. "https://deno.land/x/std/testing/mod.ts") + /// Specifiers must be valid URLs (eg. "https://deno.land/x/std/testing/asserts.ts") /// or "bare" specifiers (eg. "moment"). // TODO: add proper error handling: https://github.com/WICG/import-maps/issues/100 fn normalize_specifier_key( diff --git a/cli/installer.rs b/cli/installer.rs index cce35f5de7dc6c..d2d263447c1795 100644 --- a/cli/installer.rs +++ b/cli/installer.rs @@ -208,6 +208,10 @@ mod tests { fn install_basic() { 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"); env::set_var("HOME", &temp_dir_str); @@ -326,15 +330,10 @@ mod tests { #[test] fn install_force() { let temp_dir = TempDir::new().expect("tempdir fail"); - let temp_dir_str = temp_dir.path().to_string_lossy().to_string(); - let original_home = env::var_os("HOME"); - let original_user_profile = env::var_os("HOME"); - env::set_var("HOME", &temp_dir_str); - env::set_var("USERPROFILE", &temp_dir_str); install( DenoFlags::default(), - None, + Some(temp_dir.path().to_path_buf()), "echo_test", "http://localhost:4545/cli/tests/echo_server.ts", vec![], @@ -342,7 +341,7 @@ mod tests { ) .expect("Install failed"); - let mut file_path = temp_dir.path().join(".deno/bin/echo_test"); + let mut file_path = temp_dir.path().join("echo_test"); if cfg!(windows) { file_path = file_path.with_extension(".cmd"); } @@ -351,7 +350,7 @@ mod tests { // No force. Install failed. let no_force_result = install( DenoFlags::default(), - None, + Some(temp_dir.path().to_path_buf()), "echo_test", "http://localhost:4545/cli/tests/cat.ts", // using a different URL vec![], @@ -369,7 +368,7 @@ mod tests { // Force. Install success. let force_result = install( DenoFlags::default(), - None, + Some(temp_dir.path().to_path_buf()), "echo_test", "http://localhost:4545/cli/tests/cat.ts", // using a different URL vec![], @@ -379,12 +378,5 @@ mod tests { // Assert modified let file_content_2 = fs::read_to_string(&file_path).unwrap(); assert!(file_content_2.contains("cat.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); - } } } diff --git a/cli/js/compiler_api_test.ts b/cli/js/compiler_api_test.ts index 72b65ab5d1b24e..eef12c8cc96a33 100644 --- a/cli/js/compiler_api_test.ts +++ b/cli/js/compiler_api_test.ts @@ -78,15 +78,15 @@ test(async function bundleApiSources() { "/bar.ts": `export const bar = "bar";\n` }); assert(diagnostics == null); - assert(actual.includes(`instantiate("foo")`)); - assert(actual.includes(`__rootExports["bar"]`)); + assert(actual.includes(`__inst("foo")`)); + assert(actual.includes(`__exp["bar"]`)); }); test(async function bundleApiNoSources() { const [diagnostics, actual] = await bundle("./cli/tests/subdir/mod1.ts"); assert(diagnostics == null); - assert(actual.includes(`instantiate("mod1")`)); - assert(actual.includes(`__rootExports["printHello3"]`)); + assert(actual.includes(`__inst("mod1")`)); + assert(actual.includes(`__exp["printHello3"]`)); }); test(async function bundleApiConfig() { diff --git a/cli/js/compiler_bootstrap.ts b/cli/js/compiler_bootstrap.ts index 585aec016b3782..817486d121ce79 100644 --- a/cli/js/compiler_bootstrap.ts +++ b/cli/js/compiler_bootstrap.ts @@ -55,4 +55,4 @@ export const TS_SNAPSHOT_PROGRAM = ts.createProgram({ * We read all static assets during the snapshotting process, which is * why this is located in compiler_bootstrap. */ -export const BUNDLE_LOADER = getAsset("bundle_loader.js"); +export const SYSTEM_LOADER = getAsset("system_loader.js"); diff --git a/cli/js/compiler_bundler.ts b/cli/js/compiler_bundler.ts index d334b0fc343873..b9893620ae7a1c 100644 --- a/cli/js/compiler_bundler.ts +++ b/cli/js/compiler_bundler.ts @@ -1,6 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { BUNDLE_LOADER } from "./compiler_bootstrap.ts"; +import { SYSTEM_LOADER } from "./compiler_bootstrap.ts"; import { assert, commonPath, @@ -44,18 +44,18 @@ export function buildBundle( .replace(/\.\w+$/i, ""); let instantiate: string; if (rootExports && rootExports.length) { - instantiate = `const __rootExports = instantiate("${rootName}");\n`; + instantiate = `const __exp = await __inst("${rootName}");\n`; for (const rootExport of rootExports) { if (rootExport === "default") { - instantiate += `export default __rootExports["${rootExport}"];\n`; + instantiate += `export default __exp["${rootExport}"];\n`; } else { - instantiate += `export const ${rootExport} = __rootExports["${rootExport}"];\n`; + instantiate += `export const ${rootExport} = __exp["${rootExport}"];\n`; } } } else { - instantiate = `instantiate("${rootName}");\n`; + instantiate = `await __inst("${rootName}");\n`; } - return `${BUNDLE_LOADER}\n${data}\n${instantiate}`; + return `${SYSTEM_LOADER}\n${data}\n${instantiate}`; } /** Set the rootExports which will by the `emitBundle()` */ @@ -80,6 +80,7 @@ export function setRootExports(program: ts.Program, rootModule: string): void { // out, so inspecting SymbolFlags that might be present that are type only .filter( sym => + sym.flags & ts.SymbolFlags.Class || !( sym.flags & ts.SymbolFlags.Interface || sym.flags & ts.SymbolFlags.TypeLiteral || diff --git a/cli/js/compiler_host.ts b/cli/js/compiler_host.ts index 291f6fbc52d146..0f6aa4d089a131 100644 --- a/cli/js/compiler_host.ts +++ b/cli/js/compiler_host.ts @@ -34,7 +34,7 @@ export const ASSETS = "$asset$"; * runtime). */ export const defaultBundlerOptions: ts.CompilerOptions = { inlineSourceMap: false, - module: ts.ModuleKind.AMD, + module: ts.ModuleKind.System, outDir: undefined, outFile: `${OUT_DIR}/bundle.js`, // disabled until we have effective way to modify source maps diff --git a/cli/lib.rs b/cli/lib.rs index f89eb4bd74c602..e646c4199925fe 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -413,7 +413,7 @@ async fn run_script( Ok(()) } -async fn fmt_command(files: Option>, check: bool) { +async fn fmt_command(files: Vec, check: bool) { if let Err(err) = fmt::format_files(files, check) { print_err_and_exit(err); } @@ -493,9 +493,7 @@ pub fn main() { } DenoSubcommand::Eval { code } => eval_command(flags, code).await, DenoSubcommand::Fetch { files } => fetch_command(flags, files).await, - DenoSubcommand::Format { check, files } => { - fmt_command(files, check).await - } + DenoSubcommand::Fmt { check, files } => fmt_command(files, check).await, DenoSubcommand::Info { file } => info_command(flags, file).await, DenoSubcommand::Install { dir, diff --git a/cli/test_util.rs b/cli/test_util.rs index cf820f3f150f86..7ab4b629e3d004 100644 --- a/cli/test_util.rs +++ b/cli/test_util.rs @@ -8,11 +8,13 @@ use std::path::PathBuf; use std::process::Child; use std::process::Command; use std::process::Stdio; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use std::sync::Mutex; -use std::sync::MutexGuard; lazy_static! { - static ref GUARD: Mutex<()> = Mutex::new(()); + static ref SERVER: Mutex> = Mutex::new(None); + static ref SERVER_COUNT: AtomicUsize = AtomicUsize::new(0); } pub fn root_path() -> PathBuf { @@ -35,47 +37,58 @@ pub fn deno_exe_path() -> PathBuf { p } -pub struct HttpServerGuard<'a> { - #[allow(dead_code)] - g: MutexGuard<'a, ()>, - child: Child, -} +pub struct HttpServerGuard {} -impl<'a> Drop for HttpServerGuard<'a> { +impl Drop for HttpServerGuard { fn drop(&mut self) { - match self.child.try_wait() { - Ok(None) => { - self.child.kill().expect("failed to kill http_server.py"); - } - Ok(Some(status)) => { - panic!("http_server.py exited unexpectedly {}", status) - } - Err(e) => panic!("http_server.py err {}", e), + let count = SERVER_COUNT.fetch_sub(1, Ordering::SeqCst); + // If no more tests hold guard we can kill the server + + if count == 1 { + kill_http_server(); } } } -/// Starts tools/http_server.py when the returned guard is dropped, the server -/// will be killed. -pub fn http_server<'a>() -> HttpServerGuard<'a> { - // TODO(ry) Allow tests to use the http server in parallel. - let g = GUARD.lock().unwrap(); - - println!("tools/http_server.py starting..."); - let mut child = Command::new("python") - .current_dir(root_path()) - .args(&["-u", "tools/http_server.py"]) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to execute child"); +fn kill_http_server() { + let mut server_guard = SERVER.lock().unwrap(); + let mut child = server_guard + .take() + .expect("Trying to kill server but already killed"); + match child.try_wait() { + Ok(None) => { + child.kill().expect("failed to kill http_server.py"); + } + Ok(Some(status)) => panic!("http_server.py exited unexpectedly {}", status), + Err(e) => panic!("http_server.py error: {}", e), + } + drop(server_guard); +} - let stdout = child.stdout.as_mut().unwrap(); - use std::io::{BufRead, BufReader}; - let mut lines = BufReader::new(stdout).lines(); - let line = lines.next().unwrap().unwrap(); - assert!(line.starts_with("ready")); +pub fn http_server() -> HttpServerGuard { + SERVER_COUNT.fetch_add(1, Ordering::SeqCst); + + { + let mut server_guard = SERVER.lock().unwrap(); + if server_guard.is_none() { + println!("tools/http_server.py starting..."); + let mut child = Command::new("python") + .current_dir(root_path()) + .args(&["-u", "tools/http_server.py"]) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + + let stdout = child.stdout.as_mut().unwrap(); + use std::io::{BufRead, BufReader}; + let mut lines = BufReader::new(stdout).lines(); + let line = lines.next().unwrap().unwrap(); + assert!(line.starts_with("ready")); + server_guard.replace(child); + } + } - HttpServerGuard { child, g } + HttpServerGuard {} } pub fn https_server<'a>() -> HttpServerGuard<'a> { diff --git a/cli/tests/bundle.test.out b/cli/tests/bundle.test.out index 23b7de35e00a9f..1379eb7e558fd2 100644 --- a/cli/tests/bundle.test.out +++ b/cli/tests/bundle.test.out @@ -1,22 +1,24 @@ [WILDCARD] -let define; +let System; +let __inst; [WILDCARD] -let instantiate; -[WILDCARD] -(function() { +(() => { [WILDCARD] })(); -define("print_hello", ["require", "exports"], function (require, exports) { +System.register("print_hello", [], function (exports_1, context_1) { [WILDCARD] }); -define("mod1", ["require", "exports", "subdir2/mod2"], function (require, exports, mod2_ts_1) { +System.register("subdir2/mod2", ["print_hello"], function (exports_2, context_2) { +[WILDCARD] +}); +System.register("mod1", ["subdir2/mod2"], function (exports_3, context_3) { [WILDCARD] }); -const __rootExports = instantiate("mod1"); -export const returnsHi = __rootExports["returnsHi"]; -export const returnsFoo2 = __rootExports["returnsFoo2"]; -export const printHello3 = __rootExports["printHello3"]; -export const throwsError = __rootExports["throwsError"]; - +const __exp = await __inst("mod1"); +export const returnsHi = __exp["returnsHi"]; +export const returnsFoo2 = __exp["returnsFoo2"]; +export const printHello3 = __exp["printHello3"]; +export const throwsError = __exp["throwsError"]; +[WILDCARD] diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index bbaf9adbc07773..dd24feca434d20 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -252,6 +252,40 @@ fn bundle_exports() { assert_eq!(output.stderr, b""); } +#[test] +fn bundle_circular() { + use tempfile::TempDir; + + // First we have to generate a bundle of some module that has exports. + let circular1 = util::root_path().join("cli/tests/subdir/circular1.ts"); + assert!(circular1.is_file()); + let t = TempDir::new().expect("tempdir fail"); + let bundle = t.path().join("circular1.bundle.js"); + let mut deno = util::deno_cmd() + .current_dir(util::root_path()) + .arg("bundle") + .arg(circular1) + .arg(&bundle) + .spawn() + .expect("failed to spawn script"); + let status = deno.wait().expect("failed to wait for the child process"); + assert!(status.success()); + assert!(bundle.is_file()); + + let output = util::deno_cmd() + .current_dir(util::root_path()) + .arg("run") + .arg(&bundle) + .output() + .expect("failed to spawn script"); + // check the output of the the bundle program. + assert!(std::str::from_utf8(&output.stdout) + .unwrap() + .trim() + .ends_with("f1\nf2")); + assert_eq!(output.stderr, b""); +} + // TODO(#2933): Rewrite this test in rust. #[test] fn repl_test() { @@ -607,14 +641,6 @@ itest!(fmt_stdin { output_str: Some("const a = 1;\n"), }); -itest!(fmt_stdin_ambiguous { - args: "fmt - file.ts", - input: Some("const a = 1\n"), - check_stderr: true, - exit_code: 1, - output_str: Some("Ambiguous filename input. To format stdin, provide a single '-' instead.\n"), -}); - itest!(fmt_stdin_check_formatted { args: "fmt --check -", input: Some("const a = 1;\n"), diff --git a/cli/tests/subdir/circular1.ts b/cli/tests/subdir/circular1.ts new file mode 100644 index 00000000000000..f9054338ff94ae --- /dev/null +++ b/cli/tests/subdir/circular1.ts @@ -0,0 +1,7 @@ +import * as circular2 from "./circular2.ts"; + +export function f1(): void { + console.log("f1"); +} + +circular2.f2(); diff --git a/cli/tests/subdir/circular2.ts b/cli/tests/subdir/circular2.ts new file mode 100644 index 00000000000000..a96ffb13d1bb7f --- /dev/null +++ b/cli/tests/subdir/circular2.ts @@ -0,0 +1,7 @@ +import * as circular1 from "./circular1.ts"; + +export function f2(): void { + console.log("f2"); +} + +circular1.f1(); diff --git a/core/Cargo.toml b/core/Cargo.toml index f0d1fffc8caa19..9b642991b14f7b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_core" -version = "0.32.0" +version = "0.33.0" edition = "2018" description = "A secure JavaScript/TypeScript runtime built with V8, Rust, and Tokio" authors = ["the Deno authors"] diff --git a/deno_typescript/Cargo.toml b/deno_typescript/Cargo.toml index e2e1926356e02d..3d36aad58b48dc 100644 --- a/deno_typescript/Cargo.toml +++ b/deno_typescript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deno_typescript" -version = "0.32.0" +version = "0.33.0" license = "MIT" description = "To compile TypeScript to a snapshot during build.rs" repository = "https://github.com/denoland/deno" @@ -19,6 +19,6 @@ exclude = [ path = "lib.rs" [dependencies] -deno_core = { path = "../core", version = "0.32.0" } +deno_core = { path = "../core", version = "0.33.0" } serde_json = "1.0.44" serde = { version = "1.0.104", features = ["derive"] } diff --git a/deno_typescript/lib.rs b/deno_typescript/lib.rs index 7977b7cfe1de31..9056fd903a7e26 100644 --- a/deno_typescript/lib.rs +++ b/deno_typescript/lib.rs @@ -245,7 +245,7 @@ pub fn get_asset(name: &str) -> Option<&'static str> { }; } match name { - "bundle_loader.js" => Some(include_str!("bundle_loader.js")), + "system_loader.js" => Some(include_str!("system_loader.js")), "bootstrap.ts" => Some("console.log(\"hello deno\");"), "typescript.d.ts" => inc!("typescript.d.ts"), "lib.esnext.d.ts" => inc!("lib.esnext.d.ts"), diff --git a/deno_typescript/system_loader.js b/deno_typescript/system_loader.js new file mode 100644 index 00000000000000..c1365f6c85f524 --- /dev/null +++ b/deno_typescript/system_loader.js @@ -0,0 +1,85 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This is a specialised implementation of a System module loader. + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let System; +let __inst; + +(() => { + const mMap = new Map(); + System = { + register(id, deps, f) { + mMap.set(id, { + id, + deps, + f, + exp: {} + }); + } + }; + + const gC = (data, main) => { + const { id } = data; + return { + id, + import: async id => mMap.get(id)?.exp, + meta: { url: id, main } + }; + }; + + const gE = data => { + const { exp } = data; + return (id, value) => { + const values = typeof id === "string" ? { [id]: value } : id; + for (const [id, value] of Object.entries(values)) { + Object.defineProperty(exp, id, { + value, + writable: true, + enumerable: true + }); + } + }; + }; + + const iQ = []; + + const enq = ids => { + for (const id of ids) { + if (!iQ.includes(id)) { + const { deps } = mMap.get(id); + iQ.push(id); + enq(deps); + } + } + }; + + const dr = async main => { + const rQ = []; + let id; + while ((id = iQ.pop())) { + const m = mMap.get(id); + const { f } = m; + if (!f) { + return; + } + rQ.push([m.deps, f(gE(m), gC(m, id === main))]); + m.f = undefined; + } + let r; + while ((r = rQ.shift())) { + const [deps, { execute, setters }] = r; + for (let i = 0; i < deps.length; i++) setters[i](mMap.get(deps[i])?.exp); + const e = execute(); + if (e) await e; + } + }; + + __inst = async id => { + System = undefined; + __inst = undefined; + enq([id]); + await dr(id); + return mMap.get(id)?.exp; + }; +})(); diff --git a/std/http/file_server.ts b/std/http/file_server.ts index acf272764d5c48..d71b9ad533b9b7 100755 --- a/std/http/file_server.ts +++ b/std/http/file_server.ts @@ -301,9 +301,15 @@ function html(strings: TemplateStringsArray, ...values: unknown[]): string { listenAndServe( addr, async (req): Promise => { - const normalizedUrl = posix.normalize(req.url); - const decodedUrl = decodeURIComponent(normalizedUrl); - const fsPath = posix.join(target, decodedUrl); + let normalizedUrl = posix.normalize(req.url); + try { + normalizedUrl = decodeURIComponent(normalizedUrl); + } catch (e) { + if (!(e instanceof URIError)) { + throw e; + } + } + const fsPath = posix.join(target, normalizedUrl); let response: Response; try { diff --git a/std/http/file_server_test.ts b/std/http/file_server_test.ts index ba625b7c817e00..0329168e785aa0 100644 --- a/std/http/file_server_test.ts +++ b/std/http/file_server_test.ts @@ -83,12 +83,15 @@ test(async function serveFallback(): Promise { } }); -test(async function serveFallback(): Promise { +test(async function serveWithUnorthodoxFilename(): Promise { await startFileServer(); try { - const res = await fetch( - "http://localhost:4500/http/testdata/test%20file.txt" - ); + let res = await fetch("http://localhost:4500/http/testdata/%"); + assert(res.headers.has("access-control-allow-origin")); + assert(res.headers.has("access-control-allow-headers")); + assertEquals(res.status, 200); + + res = await fetch("http://localhost:4500/http/testdata/test%20file.txt"); assert(res.headers.has("access-control-allow-origin")); assert(res.headers.has("access-control-allow-headers")); assertEquals(res.status, 200); diff --git a/std/http/server.ts b/std/http/server.ts index b8a41379f0b6e7..c6a895a1fd8644 100644 --- a/std/http/server.ts +++ b/std/http/server.ts @@ -84,6 +84,7 @@ export async function writeTrailers( ); await writer.write(encoder.encode(`${key}: ${value}\r\n`)); } + await writer.write(encoder.encode("\r\n")); await writer.flush(); } diff --git a/std/http/server_test.ts b/std/http/server_test.ts index a18cd273c86b39..123bc7155c9f50 100644 --- a/std/http/server_test.ts +++ b/std/http/server_test.ts @@ -456,6 +456,7 @@ test("writeResponse with trailer", async () => { "", "deno: land", "node: js", + "", "" ].join("\r\n"); assertEquals(ret, exp); @@ -775,7 +776,7 @@ test("writeTrailer", async () => { new Headers({ "transfer-encoding": "chunked", trailer: "deno,node" }), new Headers({ deno: "land", node: "js" }) ); - assertEquals(w.toString(), "deno: land\r\nnode: js\r\n"); + assertEquals(w.toString(), "deno: land\r\nnode: js\r\n\r\n"); }); test("writeTrailer should throw", async () => { diff --git a/std/http/testdata/% b/std/http/testdata/% new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/std/manual.md b/std/manual.md index e017998e735e95..e5bd6ef75c847c 100644 --- a/std/manual.md +++ b/std/manual.md @@ -465,24 +465,20 @@ The above for-await loop exits after 5 seconds when sig.dispose() is called. In the above examples, we saw that Deno could execute scripts from URLs. Like browser JavaScript, Deno can import libraries directly from URLs. This example -uses a URL to import a test runner library: +uses a URL to import an assertion library: ```ts -import { - assertEquals, - runIfMain, - test -} from "https://deno.land/std/testing/mod.ts"; +import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; -test(function t1() { +Deno.test(function t1() { assertEquals("hello", "hello"); }); -test(function t2() { +Deno.test(function t2() { assertEquals("world", "world"); }); -runIfMain(import.meta); +await Deno.runTests(); ``` Try running this: @@ -534,16 +530,16 @@ a subtly different version of a library? Isn't it error prone to maintain URLs everywhere in a large project?** The solution is to import and re-export your external libraries in a central `deps.ts` file (which serves the same purpose as Node's `package.json` file). For example, let's say you were using the above -testing library across a large project. Rather than importing -`"https://deno.land/std/testing/mod.ts"` everywhere, you could create a +assertion library across a large project. Rather than importing +`"https://deno.land/std/testing/asserts.ts"` everywhere, you could create a `deps.ts` file that exports the third-party code: ```ts export { + assert, assertEquals, - runTests, - test -} from "https://deno.land/std/testing/mod.ts"; + assertStrContains +} from "https://deno.land/std/testing/asserts.ts"; ``` And throughout the same project, you can import from the `deps.ts` and avoid diff --git a/std/node/events.ts b/std/node/events.ts index a085ff86bd12bb..92fe7a7042f291 100644 --- a/std/node/events.ts +++ b/std/node/events.ts @@ -32,6 +32,7 @@ export interface WrappedFunction extends Function { */ export default class EventEmitter { public static defaultMaxListeners = 10; + public static errorMonitor = Symbol("events.errorMonitor"); private maxListeners: number | undefined; private _events: Map>; @@ -88,6 +89,12 @@ export default class EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any public emit(eventName: string | symbol, ...args: any[]): boolean { if (this._events.has(eventName)) { + if ( + eventName === "error" && + this._events.get(EventEmitter.errorMonitor) + ) { + this.emit(EventEmitter.errorMonitor, ...args); + } const listeners = (this._events.get(eventName) as Function[]).slice(); // We copy with slice() so array is not mutated during emit for (const listener of listeners) { try { @@ -98,6 +105,9 @@ export default class EventEmitter { } return true; } else if (eventName === "error") { + if (this._events.get(EventEmitter.errorMonitor)) { + this.emit(EventEmitter.errorMonitor, ...args); + } const errMsg = args.length > 0 ? args[0] : Error("Unhandled error."); throw errMsg; } diff --git a/std/node/events_test.ts b/std/node/events_test.ts index a2ed9011273c83..f86265d724d95a 100644 --- a/std/node/events_test.ts +++ b/std/node/events_test.ts @@ -15,7 +15,7 @@ test({ name: 'When adding a new event, "eventListener" event is fired before adding the listener', fn() { - let eventsFired = []; + let eventsFired: string[] = []; const testEmitter = new EventEmitter(); testEmitter.on("newListener", event => { if (event !== "newListener") { @@ -36,7 +36,7 @@ test({ name: 'When removing a listenert, "removeListener" event is fired after removal', fn() { - const eventsFired = []; + const eventsFired: string[] = []; const testEmitter = new EventEmitter(); testEmitter.on("removeListener", () => { eventsFired.push("removeListener"); @@ -80,7 +80,7 @@ test({ name: "Emitted events are called synchronously in the order they were added", fn() { const testEmitter = new EventEmitter(); - const eventsFired = []; + const eventsFired: string[] = []; testEmitter.on("event", oneArg => { eventsFired.push("event(" + oneArg + ")"); }); @@ -162,7 +162,7 @@ test({ test({ name: "Events can be registered to only fire once", fn() { - let eventsFired = []; + let eventsFired: string[] = []; const testEmitter = new EventEmitter(); //prove multiple emits on same event first (when registered with 'on') testEmitter.on("multiple event", () => { @@ -187,7 +187,7 @@ test({ name: "You can inject a listener into the start of the stack, rather than at the end", fn() { - const eventsFired = []; + const eventsFired: string[] = []; const testEmitter = new EventEmitter(); testEmitter.on("event", () => { eventsFired.push("first"); @@ -206,7 +206,7 @@ test({ test({ name: 'You can prepend a "once" listener', fn() { - const eventsFired = []; + const eventsFired: string[] = []; const testEmitter = new EventEmitter(); testEmitter.on("event", () => { eventsFired.push("first"); @@ -288,7 +288,7 @@ test({ name: "all listeners complete execution even if removed before execution", fn() { const testEmitter = new EventEmitter(); - let eventsProcessed = []; + let eventsProcessed: string[] = []; const listenerB = (): number => eventsProcessed.push("B"); const listenerA = (): void => { eventsProcessed.push("A"); @@ -311,7 +311,7 @@ test({ name: 'Raw listener will return event listener or wrapped "once" function', fn() { const testEmitter = new EventEmitter(); - const eventsProcessed = []; + const eventsProcessed: string[] = []; const listenerA = (): number => eventsProcessed.push("A"); const listenerB = (): number => eventsProcessed.push("B"); testEmitter.on("event", listenerA); @@ -335,7 +335,7 @@ test({ "Once wrapped raw listeners may be executed multiple times, until the wrapper is executed", fn() { const testEmitter = new EventEmitter(); - let eventsProcessed = []; + let eventsProcessed: string[] = []; const listenerA = (): number => eventsProcessed.push("A"); testEmitter.once("once-event", listenerA); @@ -356,7 +356,7 @@ test({ test({ name: "Can add once event listener to EventEmitter via standalone function", async fn() { - const ee: EventEmitter = new EventEmitter(); + const ee = new EventEmitter(); setTimeout(() => { ee.emit("event", 42, "foo"); }, 0); @@ -383,7 +383,7 @@ test({ test({ name: "Only valid integers are allowed for max listeners", fn() { - const ee: EventEmitter = new EventEmitter(); + const ee = new EventEmitter(); ee.setMaxListeners(0); assertThrows( () => { @@ -401,3 +401,41 @@ test({ ); } }); + +test({ + name: "ErrorMonitor can spy on error events without consuming them", + fn() { + const ee = new EventEmitter(); + let events: string[] = []; + //unhandled error scenario should throw + assertThrows( + () => { + ee.emit("error"); + }, + Error, + "Unhandled error" + ); + + ee.on(EventEmitter.errorMonitor, () => { + events.push("errorMonitor event"); + }); + + //error is still unhandled but also intercepted by error monitor + assertThrows( + () => { + ee.emit("error"); + }, + Error, + "Unhandled error" + ); + assertEquals(events, ["errorMonitor event"]); + + //A registered error handler won't throw, but still be monitored + events = []; + ee.on("error", () => { + events.push("error"); + }); + ee.emit("error"); + assertEquals(events, ["errorMonitor event", "error"]); + } +}); diff --git a/std/style_guide.md b/std/style_guide.md index 2ef657004ce8ac..5a976e1171de82 100644 --- a/std/style_guide.md +++ b/std/style_guide.md @@ -295,10 +295,9 @@ Example of test: ```ts import { assertEquals } from "https://deno.land/std@v0.11/testing/asserts.ts"; -import { test } from "https://deno.land/std@v0.11/testing/mod.ts"; import { foo } from "./mod.ts"; -test(function myTestFunction() { +Deno.test(function myTestFunction() { assertEquals(foo(), { bar: "bar" }); }); ``` diff --git a/std/testing/README.md b/std/testing/README.md index afaf76b863709d..a3640e7281ea8d 100644 --- a/std/testing/README.md +++ b/std/testing/README.md @@ -5,14 +5,9 @@ in Deno. ## Usage -The module exports a `test` function which is the test harness in Deno. It -accepts either a function (including async functions) or an object which -contains a `name` property and a `fn` property. When running tests and -outputting the results, the name of the past function is used, or if the object -is passed, the `name` property is used to identify the test. If the assertion is -false an `AssertionError` will be thrown. - -Asserts are exposed in `testing/asserts.ts` module. +`testing/asserts.ts` module provides range of assertion helpers. If the +assertion is false an `AssertionError` will be thrown which will result in +pretty-printed diff of failing assertion. - `equal()` - Deep comparison function, where `actual` and `expected` are compared deeply, and if they vary, `equal` returns `false`. @@ -39,22 +34,12 @@ Asserts are exposed in `testing/asserts.ts` module. - `unimplemented()` - Use this to stub out methods that will throw when invoked - `unreachable()` - Used to assert unreachable code -`runTests()` executes the declared tests. It accepts a `RunOptions` parameter: - -- parallel : Execute tests in a parallel way. -- exitOnFail : if one test fails, test will throw an error and stop the tests. - If not all tests will be processed. - Basic usage: ```ts -import { - assertEquals, - runTests, - test -} from "https://deno.land/std/testing/mod.ts"; +import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; -test({ +Deno.test({ name: "testing example", fn(): void { assertEquals("world", "world"); @@ -62,13 +47,13 @@ test({ } }); -runTests(); +await Deno.runTests(); ``` Short syntax (named function instead of object): ```ts -test(function example(): void { +Deno.test(function example(): void { assertEquals("world", "world"); assertEquals({ hello: "world" }, { hello: "world" }); }); @@ -77,14 +62,14 @@ test(function example(): void { Using `assertStrictEq()`: ```ts -test(function isStrictlyEqual(): void { +Deno.test(function isStrictlyEqual(): void { const a = {}; const b = a; assertStrictEq(a, b); }); // This test fails -test(function isNotStrictlyEqual(): void { +Deno.test(function isNotStrictlyEqual(): void { const a = {}; const b = {}; assertStrictEq(a, b); @@ -94,7 +79,7 @@ test(function isNotStrictlyEqual(): void { Using `assertThrows()`: ```ts -test(function doesThrow(): void { +Deno.test(function doesThrow(): void { assertThrows((): void => { throw new TypeError("hello world!"); }); @@ -111,7 +96,7 @@ test(function doesThrow(): void { }); // This test will not pass -test(function fails(): void { +Deno.test(function fails(): void { assertThrows((): void => { console.log("Hello world"); }); @@ -121,7 +106,7 @@ test(function fails(): void { Using `assertThrowsAsync()`: ```ts -test(async function doesThrow(): Promise { +Deno.test(async function doesThrow(): Promise { await assertThrowsAsync( async (): Promise => { throw new TypeError("hello world!"); @@ -145,7 +130,7 @@ test(async function doesThrow(): Promise { }); // This test will not pass -test(async function fails(): Promise { +Deno.test(async function fails(): Promise { await assertThrowsAsync( async (): Promise => { console.log("Hello world");