diff --git a/.dprint.json b/.dprint.json index 303f79d1b5..d20b1673ba 100644 --- a/.dprint.json +++ b/.dprint.json @@ -27,6 +27,7 @@ "cli/tsc/dts/lib.es*.d.ts", "cli/tsc/dts/typescript.d.ts", "cli/tests/node_compat/test", + "cli/tests/testdata/file_extensions/ts_with_js_extension.js", "cli/tests/testdata/fmt/badly_formatted.json", "cli/tests/testdata/fmt/badly_formatted.md", "cli/tests/testdata/byte_order_mark.ts", diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 9650b9612e..236352f24b 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -124,13 +124,11 @@ pub struct DocFlags { pub struct EvalFlags { pub print: bool, pub code: String, - pub ext: String, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct FmtFlags { pub check: bool, - pub ext: String, pub files: FileFlags, pub use_tabs: Option, pub line_width: Option, @@ -335,6 +333,7 @@ pub struct Flags { pub node_modules_dir: Option, pub coverage_dir: Option, pub enable_testing_features: bool, + pub ext: Option, pub ignore: Vec, pub import_map_path: Option, pub inspect_brk: Option, @@ -837,6 +836,7 @@ fn bundle_subcommand<'a>() -> Command<'a> { ) .arg(watch_arg(false)) .arg(no_clear_screen_arg()) + .arg(executable_ext_arg()) .about("Bundle module and dependencies into single file") .long_about( "Output a single JavaScript file with all dependencies. @@ -943,6 +943,7 @@ fn compile_subcommand<'a>() -> Command<'a> { "aarch64-apple-darwin", ]), ) + .arg(executable_ext_arg()) .about("UNSTABLE: Compile the script into a self contained executable") .long_about( "UNSTABLE: Compiles the given script into a self contained executable. @@ -1164,22 +1165,16 @@ This command has implicit access to all permissions (--allow-all).", .arg( // TODO(@satyarohith): remove this argument in 2.0. Arg::new("ts") + .conflicts_with("ext") .long("ts") .short('T') - .help("Treat eval input as TypeScript") + .help("deprecated: Treat eval input as TypeScript") .takes_value(false) .multiple_occurrences(false) .multiple_values(false) .hide(true), ) - .arg( - Arg::new("ext") - .long("ext") - .help("Set standard input (stdin) content type") - .takes_value(true) - .default_value("js") - .possible_values(["ts", "tsx", "js", "jsx"]), - ) + .arg(executable_ext_arg()) .arg( Arg::new("print") .long("print") @@ -1232,8 +1227,9 @@ Ignore formatting a file by adding an ignore comment at the top of the file: .arg( Arg::new("ext") .long("ext") - .help("Set standard input (stdin) content type") + .help("Set content type of the supplied file") .takes_value(true) + // prefer using ts for formatting instead of js because ts works in more scenarios .default_value("ts") .possible_values(["ts", "tsx", "js", "jsx", "md", "json", "jsonc"]), ) @@ -1615,6 +1611,7 @@ fn run_subcommand<'a>() -> Command<'a> { .conflicts_with("inspect-brk"), ) .arg(no_clear_screen_arg()) + .arg(executable_ext_arg()) .trailing_var_arg(true) .arg(script_arg().required(true)) .about("Run a JavaScript or TypeScript program") @@ -2168,6 +2165,18 @@ fn cached_only_arg<'a>() -> Arg<'a> { .help("Require that remote dependencies are already cached") } +/// Used for subcommands that operate on executable scripts only. +/// `deno fmt` has its own `--ext` arg because its possible values differ. +/// If --ext is not provided and the script doesn't have a file extension, +/// deno_graph::parse_module() defaults to js. +fn executable_ext_arg<'a>() -> Arg<'a> { + Arg::new("ext") + .long("ext") + .help("Set content type of the supplied file") + .takes_value(true) + .possible_values(["ts", "tsx", "js", "jsx"]) +} + fn location_arg<'a>() -> Arg<'a> { Arg::new("location") .long("location") @@ -2456,6 +2465,7 @@ fn bundle_parse(flags: &mut Flags, matches: &clap::ArgMatches) { }; watch_arg_parse(flags, matches, false); + ext_arg_parse(flags, matches); flags.subcommand = DenoSubcommand::Bundle(BundleFlags { source_file, @@ -2505,6 +2515,7 @@ fn compile_parse(flags: &mut Flags, matches: &clap::ArgMatches) { Some(f) => f.map(String::from).collect(), None => vec![], }; + ext_arg_parse(flags, matches); flags.subcommand = DenoSubcommand::Compile(CompileFlags { source_file, @@ -2614,13 +2625,22 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.allow_write = Some(vec![]); flags.allow_ffi = Some(vec![]); flags.allow_hrtime = true; + + ext_arg_parse(flags, matches); + // TODO(@satyarohith): remove this flag in 2.0. let as_typescript = matches.is_present("ts"); - let ext = if as_typescript { - "ts".to_string() - } else { - matches.value_of("ext").unwrap().to_string() - }; + + if as_typescript { + eprintln!( + "{}", + crate::colors::yellow( + "Warning: --ts/-T flag is deprecated. Use --ext=ts instead." + ), + ); + + flags.ext = Some("ts".to_string()); + } let print = matches.is_present("print"); let mut code: Vec = matches @@ -2634,12 +2654,13 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) { for v in code_args { flags.argv.push(v); } - flags.subcommand = DenoSubcommand::Eval(EvalFlags { print, code, ext }); + flags.subcommand = DenoSubcommand::Eval(EvalFlags { print, code }); } fn fmt_parse(flags: &mut Flags, matches: &clap::ArgMatches) { config_args_parse(flags, matches); watch_arg_parse(flags, matches, false); + ext_arg_parse(flags, matches); let include = match matches.values_of("files") { Some(f) => f.map(PathBuf::from).collect(), @@ -2649,7 +2670,6 @@ fn fmt_parse(flags: &mut Flags, matches: &clap::ArgMatches) { Some(f) => f.map(PathBuf::from).collect(), None => vec![], }; - let ext = matches.value_of("ext").unwrap().to_string(); let use_tabs = optional_bool_parse(matches, "use-tabs"); let line_width = if matches.is_present("line-width") { @@ -2674,7 +2694,6 @@ fn fmt_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.subcommand = DenoSubcommand::Fmt(FmtFlags { check: matches.is_present("check"), - ext, files: FileFlags { include, ignore }, use_tabs, line_width, @@ -2827,6 +2846,8 @@ fn run_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.argv.push(v); } + ext_arg_parse(flags, matches); + watch_arg_parse(flags, matches, true); flags.subcommand = DenoSubcommand::Run(RunFlags { script }); } @@ -3228,6 +3249,10 @@ fn cached_only_arg_parse(flags: &mut Flags, matches: &ArgMatches) { } } +fn ext_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + flags.ext = matches.value_of("ext").map(String::from); +} + fn location_arg_parse(flags: &mut Flags, matches: &clap::ArgMatches) { flags.location = matches .value_of("location") @@ -3694,7 +3719,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![ PathBuf::from("script_1.ts"), @@ -3709,6 +3733,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), ..Flags::default() } ); @@ -3719,7 +3744,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: true, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3731,6 +3755,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), ..Flags::default() } ); @@ -3741,7 +3766,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3753,6 +3777,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), ..Flags::default() } ); @@ -3763,7 +3788,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3775,6 +3799,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), watch: Some(vec![]), ..Flags::default() } @@ -3787,7 +3812,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3799,6 +3823,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), watch: Some(vec![]), no_clear_screen: true, ..Flags::default() @@ -3818,7 +3843,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: true, - ext: "ts".to_string(), files: FileFlags { include: vec![PathBuf::from("foo.ts")], ignore: vec![PathBuf::from("bar.js")], @@ -3830,6 +3854,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), watch: Some(vec![]), ..Flags::default() } @@ -3841,7 +3866,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3853,6 +3877,7 @@ mod tests { prose_wrap: None, no_semicolons: None, }), + ext: Some("ts".to_string()), config_flag: ConfigFlag::Path("deno.jsonc".to_string()), ..Flags::default() } @@ -3871,7 +3896,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![PathBuf::from("foo.ts")], ignore: vec![], @@ -3884,6 +3908,7 @@ mod tests { no_semicolons: None, }), config_flag: ConfigFlag::Path("deno.jsonc".to_string()), + ext: Some("ts".to_string()), watch: Some(vec![]), ..Flags::default() } @@ -3907,7 +3932,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3919,6 +3943,7 @@ mod tests { prose_wrap: Some("never".to_string()), no_semicolons: Some(true), }), + ext: Some("ts".to_string()), ..Flags::default() } ); @@ -3936,7 +3961,6 @@ mod tests { Flags { subcommand: DenoSubcommand::Fmt(FmtFlags { check: false, - ext: "ts".to_string(), files: FileFlags { include: vec![], ignore: vec![], @@ -3948,6 +3972,7 @@ mod tests { prose_wrap: None, no_semicolons: Some(false), }), + ext: Some("ts".to_string()), ..Flags::default() } ); @@ -4362,7 +4387,6 @@ mod tests { subcommand: DenoSubcommand::Eval(EvalFlags { print: false, code: "'console.log(\"hello\")'".to_string(), - ext: "js".to_string(), }), allow_net: Some(vec![]), allow_env: Some(vec![]), @@ -4386,7 +4410,6 @@ mod tests { subcommand: DenoSubcommand::Eval(EvalFlags { print: true, code: "1+2".to_string(), - ext: "js".to_string(), }), allow_net: Some(vec![]), allow_env: Some(vec![]), @@ -4411,7 +4434,6 @@ mod tests { subcommand: DenoSubcommand::Eval(EvalFlags { print: false, code: "'console.log(\"hello\")'".to_string(), - ext: "ts".to_string(), }), allow_net: Some(vec![]), allow_env: Some(vec![]), @@ -4421,6 +4443,7 @@ mod tests { allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, + ext: Some("ts".to_string()), ..Flags::default() } ); @@ -4436,7 +4459,6 @@ mod tests { subcommand: DenoSubcommand::Eval(EvalFlags { print: false, code: "42".to_string(), - ext: "js".to_string(), }), import_map_path: Some("import_map.json".to_string()), no_remote: true, @@ -4479,7 +4501,6 @@ mod tests { subcommand: DenoSubcommand::Eval(EvalFlags { print: false, code: "console.log(Deno.args)".to_string(), - ext: "js".to_string(), }), argv: svec!["arg1", "arg2"], allow_net: Some(vec![]), diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 848f50eb44..fb44c0a8fe 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -10,6 +10,8 @@ pub mod package_json; pub use self::import_map::resolve_import_map_from_specifier; use self::package_json::PackageJsonDeps; use ::import_map::ImportMap; +use deno_core::resolve_url_or_path; +use deno_graph::npm::NpmPackageReqReference; use indexmap::IndexMap; use crate::npm::NpmRegistryApi; @@ -50,6 +52,7 @@ use deno_runtime::deno_tls::webpki_roots; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::permissions::PermissionsOptions; use once_cell::sync::Lazy; +use std::collections::HashMap; use std::env; use std::io::BufReader; use std::io::Cursor; @@ -139,7 +142,6 @@ impl BenchOptions { pub struct FmtOptions { pub is_stdin: bool, pub check: bool, - pub ext: String, pub options: FmtOptionsConfig, pub files: FilesConfig, } @@ -166,10 +168,6 @@ impl FmtOptions { Ok(Self { is_stdin, check: maybe_fmt_flags.as_ref().map(|f| f.check).unwrap_or(false), - ext: maybe_fmt_flags - .as_ref() - .map(|f| f.ext.to_string()) - .unwrap_or_else(|| "ts".to_string()), options: resolve_fmt_options( maybe_fmt_flags.as_ref(), maybe_config_options, @@ -675,6 +673,73 @@ impl CliOptions { .map(Some) } + pub fn resolve_main_module(&self) -> Result { + match &self.flags.subcommand { + DenoSubcommand::Bundle(bundle_flags) => { + resolve_url_or_path(&bundle_flags.source_file, self.initial_cwd()) + .map_err(AnyError::from) + } + DenoSubcommand::Compile(compile_flags) => { + resolve_url_or_path(&compile_flags.source_file, self.initial_cwd()) + .map_err(AnyError::from) + } + DenoSubcommand::Eval(_) => { + resolve_url_or_path("./$deno$eval", self.initial_cwd()) + .map_err(AnyError::from) + } + DenoSubcommand::Repl(_) => { + resolve_url_or_path("./$deno$repl.ts", self.initial_cwd()) + .map_err(AnyError::from) + } + DenoSubcommand::Run(run_flags) => { + if run_flags.is_stdin() { + std::env::current_dir() + .context("Unable to get CWD") + .and_then(|cwd| { + resolve_url_or_path("./$deno$stdin", &cwd).map_err(AnyError::from) + }) + } else if self.flags.watch.is_some() { + resolve_url_or_path(&run_flags.script, self.initial_cwd()) + .map_err(AnyError::from) + } else if NpmPackageReqReference::from_str(&run_flags.script).is_ok() { + ModuleSpecifier::parse(&run_flags.script).map_err(AnyError::from) + } else { + resolve_url_or_path(&run_flags.script, self.initial_cwd()) + .map_err(AnyError::from) + } + } + _ => { + bail!("No main module.") + } + } + } + + pub fn resolve_file_header_overrides( + &self, + ) -> HashMap> { + let maybe_main_specifier = self.resolve_main_module().ok(); + // TODO(Cre3per): This mapping moved to deno_ast with https://github.com/denoland/deno_ast/issues/133 and should be available in deno_ast >= 0.25.0 via `MediaType::from_path(...).as_media_type()` + let maybe_content_type = + self.flags.ext.as_ref().and_then(|el| match el.as_str() { + "ts" => Some("text/typescript"), + "tsx" => Some("text/tsx"), + "js" => Some("text/javascript"), + "jsx" => Some("text/jsx"), + _ => None, + }); + + if let (Some(main_specifier), Some(content_type)) = + (maybe_main_specifier, maybe_content_type) + { + HashMap::from([( + main_specifier, + HashMap::from([("content-type".to_string(), content_type.to_string())]), + )]) + } else { + HashMap::default() + } + } + pub async fn resolve_npm_resolution_snapshot( &self, api: &NpmRegistryApi, @@ -936,6 +1001,10 @@ impl CliOptions { self.flags.enable_testing_features } + pub fn ext_flag(&self) -> &Option { + &self.flags.ext + } + /// If the --inspect or --inspect-brk flags are used. pub fn is_inspecting(&self) -> bool { self.flags.inspect.is_some() diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index ddb419e54d..1aea67058d 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -11,6 +11,7 @@ use deno_graph::source::LoadFuture; use deno_graph::source::LoadResponse; use deno_graph::source::Loader; use deno_runtime::permissions::PermissionsContainer; +use std::collections::HashMap; use std::sync::Arc; mod check; @@ -43,6 +44,7 @@ pub struct FetchCacher { emit_cache: EmitCache, dynamic_permissions: PermissionsContainer, file_fetcher: Arc, + file_header_overrides: HashMap>, root_permissions: PermissionsContainer, cache_info_enabled: bool, maybe_local_node_modules_url: Option, @@ -52,6 +54,7 @@ impl FetchCacher { pub fn new( emit_cache: EmitCache, file_fetcher: Arc, + file_header_overrides: HashMap>, root_permissions: PermissionsContainer, dynamic_permissions: PermissionsContainer, maybe_local_node_modules_url: Option, @@ -60,6 +63,7 @@ impl FetchCacher { emit_cache, dynamic_permissions, file_fetcher, + file_header_overrides, root_permissions, cache_info_enabled: false, maybe_local_node_modules_url, @@ -123,6 +127,7 @@ impl Loader for FetchCacher { self.root_permissions.clone() }; let file_fetcher = self.file_fetcher.clone(); + let file_header_overrides = self.file_header_overrides.clone(); let specifier = specifier.clone(); async move { @@ -130,9 +135,18 @@ impl Loader for FetchCacher { .fetch(&specifier, permissions) .await .map(|file| { + let maybe_headers = + match (file.maybe_headers, file_header_overrides.get(&specifier)) { + (Some(headers), Some(overrides)) => { + Some(headers.into_iter().chain(overrides.clone()).collect()) + } + (Some(headers), None) => Some(headers), + (None, Some(overrides)) => Some(overrides.clone()), + (None, None) => None, + }; Ok(Some(LoadResponse::Module { specifier: file.specifier, - maybe_headers: file.maybe_headers, + maybe_headers, content: file.source, })) }) diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 6136c5691d..6c38dccadc 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -162,6 +162,7 @@ pub async fn create_graph_and_maybe_check( let mut cache = cache::FetchCacher::new( ps.emit_cache.clone(), ps.file_fetcher.clone(), + ps.options.resolve_file_header_overrides(), PermissionsContainer::allow_all(), PermissionsContainer::allow_all(), ps.options.node_modules_dir_specifier(), diff --git a/cli/main.rs b/cli/main.rs index 3a479a3136..af373fac2f 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -88,7 +88,7 @@ async fn run_subcommand(flags: Flags) -> Result { Ok(0) } DenoSubcommand::Fmt(fmt_flags) => { - let cli_options = CliOptions::from_flags(flags)?; + let cli_options = CliOptions::from_flags(flags.clone())?; let fmt_options = cli_options.resolve_fmt_options(fmt_flags)?; tools::fmt::format(cli_options, fmt_options).await?; Ok(0) @@ -130,7 +130,7 @@ async fn run_subcommand(flags: Flags) -> Result { if run_flags.is_stdin() { tools::run::run_from_stdin(flags).await } else { - tools::run::run_script(flags, run_flags).await + tools::run::run_script(flags).await } } DenoSubcommand::Task(task_flags) => { diff --git a/cli/proc_state.rs b/cli/proc_state.rs index f6a54a5028..45f3bed5f3 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -346,6 +346,7 @@ impl ProcState { let mut cache = cache::FetchCacher::new( self.emit_cache.clone(), self.file_fetcher.clone(), + self.options.resolve_file_header_overrides(), root_permissions, dynamic_permissions, self.options.node_modules_dir_specifier(), @@ -639,6 +640,7 @@ impl ProcState { cache::FetchCacher::new( self.emit_cache.clone(), self.file_fetcher.clone(), + self.options.resolve_file_header_overrides(), PermissionsContainer::allow_all(), PermissionsContainer::allow_all(), self.options.node_modules_dir_specifier(), diff --git a/cli/tests/integration/bundle_tests.rs b/cli/tests/integration/bundle_tests.rs index 08665091ec..5a86a3bb9c 100644 --- a/cli/tests/integration/bundle_tests.rs +++ b/cli/tests/integration/bundle_tests.rs @@ -466,6 +466,16 @@ itest!(check_local_by_default_type_error { exit_code: 1, }); +itest!(ts_without_extension { + args: "bundle --ext ts file_extensions/ts_without_extension", + output: "bundle/file_extensions/ts_without_extension.out", +}); + +itest!(js_without_extension { + args: "bundle --ext js file_extensions/js_without_extension", + output: "bundle/file_extensions/js_without_extension.out", +}); + itest!(bundle_shebang_file { args: "bundle subdir/shebang_file.js", output: "bundle/shebang_file.bundle.out", diff --git a/cli/tests/integration/compile_tests.rs b/cli/tests/integration/compile_tests.rs index 810cf5f801..957beed30a 100644 --- a/cli/tests/integration/compile_tests.rs +++ b/cli/tests/integration/compile_tests.rs @@ -411,6 +411,82 @@ fn standalone_runtime_flags() { .contains("PermissionDenied: Requires write access")); } +#[test] +fn standalone_ext_flag_ts() { + let dir = TempDir::new(); + let exe = if cfg!(windows) { + dir.path().join("ext_flag_ts.exe") + } else { + dir.path().join("ext_flag_ts") + }; + let output = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("compile") + .arg("--unstable") + .arg("--ext") + .arg("ts") + .arg("--output") + .arg(&exe) + .arg("./file_extensions/ts_without_extension") + .stdout(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let output = Command::new(exe) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let stdout_str = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + util::strip_ansi_codes(&stdout_str), + "executing typescript with no extension\n" + ); +} + +#[test] +fn standalone_ext_flag_js() { + let dir = TempDir::new(); + let exe = if cfg!(windows) { + dir.path().join("ext_flag_js.exe") + } else { + dir.path().join("ext_flag_js") + }; + let output = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("compile") + .arg("--unstable") + .arg("--ext") + .arg("js") + .arg("--output") + .arg(&exe) + .arg("./file_extensions/js_without_extension") + .stdout(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let output = Command::new(exe) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let stdout_str = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + util::strip_ansi_codes(&stdout_str), + "executing javascript with no extension\n" + ); +} + #[test] fn standalone_import_map() { let dir = TempDir::new(); diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 4c78a5cf5a..e4d6709350 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -3823,6 +3823,30 @@ itest!(error_cause_recursive { exit_code: 1, }); +itest!(default_file_extension_is_js { + args: "run --check file_extensions/js_without_extension", + output: "file_extensions/js_without_extension.out", + exit_code: 0, +}); + +itest!(js_without_extension { + args: "run --ext js --check file_extensions/js_without_extension", + output: "file_extensions/js_without_extension.out", + exit_code: 0, +}); + +itest!(ts_without_extension { + args: "run --ext ts file_extensions/ts_without_extension", + output: "file_extensions/ts_without_extension.out", + exit_code: 0, +}); + +itest!(ext_flag_takes_precedence_over_extension { + args: "run --ext ts file_extensions/ts_with_js_extension.js", + output: "file_extensions/ts_with_extension.out", + exit_code: 0, +}); + #[test] fn websocket() { let _g = util::http_server(); diff --git a/cli/tests/integration/watcher_tests.rs b/cli/tests/integration/watcher_tests.rs index 0c9b8c29f0..e50ac04e7c 100644 --- a/cli/tests/integration/watcher_tests.rs +++ b/cli/tests/integration/watcher_tests.rs @@ -58,6 +58,24 @@ fn wait_contains(s: &str, lines: &mut impl Iterator) { wait_for(|msg| msg.contains(s), lines) } +/// Before test cases touch files, they need to wait for the watcher to be +/// ready. Waiting for subcommand output is insufficient. +/// The file watcher takes a moment to start watching files due to +/// asynchronicity. It is possible for the watched subcommand to finish before +/// any files are being watched. +/// deno must be running with --log-level=debug +/// file_name should be the file name and, optionally, extension. file_name +/// may not be a full path, as it is not portable. +fn wait_for_watcher( + file_name: &str, + stderr_lines: &mut impl Iterator, +) { + wait_for( + |m| m.contains("Watching paths") && m.contains(file_name), + stderr_lines, + ); +} + fn read_line(s: &str, lines: &mut impl Iterator) -> String { lines.find(|m| m.contains(s)).unwrap() } @@ -508,20 +526,14 @@ fn run_watch_no_dynamic() { let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Hello world", &mut stdout_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("file_to_watch.js"), - &mut stderr_lines, - ); + wait_for_watcher("file_to_watch.js", &mut stderr_lines); // Change content of the file write(&file_to_watch, "console.log('Hello world2');").unwrap(); wait_contains("Restarting", &mut stderr_lines); wait_contains("Hello world2", &mut stdout_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("file_to_watch.js"), - &mut stderr_lines, - ); + wait_for_watcher("file_to_watch.js", &mut stderr_lines); // Add dependency let another_file = t.path().join("another_file.js"); @@ -534,30 +546,21 @@ fn run_watch_no_dynamic() { wait_contains("Restarting", &mut stderr_lines); wait_contains("0", &mut stdout_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("another_file.js"), - &mut stderr_lines, - ); + wait_for_watcher("another_file.js", &mut stderr_lines); // Confirm that restarting occurs when a new file is updated write(&another_file, "export const foo = 42;").unwrap(); wait_contains("Restarting", &mut stderr_lines); wait_contains("42", &mut stdout_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("file_to_watch.js"), - &mut stderr_lines, - ); + wait_for_watcher("file_to_watch.js", &mut stderr_lines); // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax write(&file_to_watch, "syntax error ^^").unwrap(); wait_contains("Restarting", &mut stderr_lines); wait_contains("error:", &mut stderr_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("file_to_watch.js"), - &mut stderr_lines, - ); + wait_for_watcher("file_to_watch.js", &mut stderr_lines); // Then restore the file write( @@ -568,20 +571,14 @@ fn run_watch_no_dynamic() { wait_contains("Restarting", &mut stderr_lines); wait_contains("42", &mut stdout_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("another_file.js"), - &mut stderr_lines, - ); + wait_for_watcher("another_file.js", &mut stderr_lines); // Update the content of the imported file with invalid syntax write(&another_file, "syntax error ^^").unwrap(); wait_contains("Restarting", &mut stderr_lines); wait_contains("error:", &mut stderr_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("another_file.js"), - &mut stderr_lines, - ); + wait_for_watcher("another_file.js", &mut stderr_lines); // Modify the imported file and make sure that restarting occurs write(&another_file, "export const foo = 'modified!';").unwrap(); @@ -629,12 +626,7 @@ fn run_watch_external_watch_files() { let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines); wait_contains("Hello world", &mut stdout_lines); - wait_for( - |m| { - m.contains("Watching paths") && m.contains("external_file_to_watch.txt") - }, - &mut stderr_lines, - ); + wait_for_watcher("external_file_to_watch.txt", &mut stderr_lines); // Change content of the external file write(&external_file_to_watch, "Hello world2").unwrap(); @@ -685,10 +677,7 @@ fn run_watch_load_unload_events() { // Wait for the first load event to fire wait_contains("load", &mut stdout_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("file_to_watch.js"), - &mut stderr_lines, - ); + wait_for_watcher("file_to_watch.js", &mut stderr_lines); // Change content of the file, this time without an interval to keep it alive. write( @@ -743,10 +732,7 @@ fn run_watch_not_exit() { wait_contains("Process started", &mut stderr_lines); wait_contains("error:", &mut stderr_lines); - wait_for( - |m| m.contains("Watching paths") && m.contains("file_to_watch.js"), - &mut stderr_lines, - ); + wait_for_watcher("file_to_watch.js", &mut stderr_lines); // Make sure the watcher actually restarts and works fine with the proper syntax write(&file_to_watch, "console.log(42);").unwrap(); @@ -807,6 +793,47 @@ fn run_watch_with_import_map_and_relative_paths() { check_alive_then_kill(child); } +#[test] +fn run_watch_with_ext_flag() { + let t = TempDir::new(); + let file_to_watch = t.path().join("file_to_watch"); + write(&file_to_watch, "interface I{}; console.log(42);").unwrap(); + + let mut child = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("run") + .arg("--watch") + .arg("--log-level") + .arg("debug") + .arg("--ext") + .arg("ts") + .arg(&file_to_watch) + .env("NO_COLOR", "1") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); + + wait_contains("42", &mut stdout_lines); + + // Make sure the watcher actually restarts and works fine with the proper language + wait_for_watcher("file_to_watch", &mut stderr_lines); + wait_contains("Process finished", &mut stderr_lines); + + write( + &file_to_watch, + "type Bear = 'polar' | 'grizzly'; console.log(123);", + ) + .unwrap(); + + wait_contains("Restarting!", &mut stderr_lines); + wait_contains("123", &mut stdout_lines); + wait_contains("Process finished", &mut stderr_lines); + + check_alive_then_kill(child); +} + #[test] fn run_watch_error_messages() { let t = TempDir::new(); @@ -1193,10 +1220,7 @@ fn run_watch_dynamic_imports() { &mut stdout_lines, ); - wait_for( - |m| m.contains("Watching paths") && m.contains("imported2.js"), - &mut stderr_lines, - ); + wait_for_watcher("imported2.js", &mut stderr_lines); wait_contains("finished", &mut stderr_lines); write( diff --git a/cli/tests/testdata/bundle/file_extensions/js_without_extension.out b/cli/tests/testdata/bundle/file_extensions/js_without_extension.out new file mode 100644 index 0000000000..0273e6207f --- /dev/null +++ b/cli/tests/testdata/bundle/file_extensions/js_without_extension.out @@ -0,0 +1,8 @@ +[WILDCARD] +// deno-fmt-ignore-file +// deno-lint-ignore-file +// This code was bundled using `deno bundle` and it's not recommended to edit it manually + +"hello"; +console.log("executing javascript with no extension"); + diff --git a/cli/tests/testdata/bundle/file_extensions/ts_without_extension.out b/cli/tests/testdata/bundle/file_extensions/ts_without_extension.out new file mode 100644 index 0000000000..39e355d146 --- /dev/null +++ b/cli/tests/testdata/bundle/file_extensions/ts_without_extension.out @@ -0,0 +1,7 @@ +[WILDCARD] +// deno-fmt-ignore-file +// deno-lint-ignore-file +// This code was bundled using `deno bundle` and it's not recommended to edit it manually + +console.log("executing typescript with no extension"); + diff --git a/cli/tests/testdata/file_extensions/js_without_extension b/cli/tests/testdata/file_extensions/js_without_extension new file mode 100644 index 0000000000..4774be3263 --- /dev/null +++ b/cli/tests/testdata/file_extensions/js_without_extension @@ -0,0 +1,3 @@ +let i = 123; +i = "hello" +console.log("executing javascript with no extension"); diff --git a/cli/tests/testdata/file_extensions/js_without_extension.out b/cli/tests/testdata/file_extensions/js_without_extension.out new file mode 100644 index 0000000000..1236c1e534 --- /dev/null +++ b/cli/tests/testdata/file_extensions/js_without_extension.out @@ -0,0 +1 @@ +executing javascript with no extension diff --git a/cli/tests/testdata/file_extensions/ts_with_extension.out b/cli/tests/testdata/file_extensions/ts_with_extension.out new file mode 100644 index 0000000000..181959ee23 --- /dev/null +++ b/cli/tests/testdata/file_extensions/ts_with_extension.out @@ -0,0 +1 @@ +executing typescript with extension diff --git a/cli/tests/testdata/file_extensions/ts_with_extension.ts b/cli/tests/testdata/file_extensions/ts_with_extension.ts new file mode 100644 index 0000000000..3c49f74846 --- /dev/null +++ b/cli/tests/testdata/file_extensions/ts_with_extension.ts @@ -0,0 +1,5 @@ +interface Lollipop { + _: number; +} + +console.log("executing typescript with extension"); diff --git a/cli/tests/testdata/file_extensions/ts_with_js_extension.js b/cli/tests/testdata/file_extensions/ts_with_js_extension.js new file mode 100644 index 0000000000..3c49f74846 --- /dev/null +++ b/cli/tests/testdata/file_extensions/ts_with_js_extension.js @@ -0,0 +1,5 @@ +interface Lollipop { + _: number; +} + +console.log("executing typescript with extension"); diff --git a/cli/tests/testdata/file_extensions/ts_without_extension b/cli/tests/testdata/file_extensions/ts_without_extension new file mode 100644 index 0000000000..f10891d7a2 --- /dev/null +++ b/cli/tests/testdata/file_extensions/ts_without_extension @@ -0,0 +1,3 @@ +interface Lollipop {} + +console.log("executing typescript with no extension"); diff --git a/cli/tests/testdata/file_extensions/ts_without_extension.out b/cli/tests/testdata/file_extensions/ts_without_extension.out new file mode 100644 index 0000000000..b15c063c85 --- /dev/null +++ b/cli/tests/testdata/file_extensions/ts_without_extension.out @@ -0,0 +1 @@ +executing typescript with no extension diff --git a/cli/tests/unit/process_test.ts b/cli/tests/unit/process_test.ts index 1799a01905..e6c4bfe595 100644 --- a/cli/tests/unit/process_test.ts +++ b/cli/tests/unit/process_test.ts @@ -658,6 +658,6 @@ Deno.test( p.close(); p.stdout.close(); assertStrictEquals(code, 1); - assertStringIncludes(stderr, "No such file or directory"); + assertStringIncludes(stderr, "Failed getting cwd."); }, ); diff --git a/cli/tools/bundle.rs b/cli/tools/bundle.rs index 9420d9c8fb..e5531d7e1a 100644 --- a/cli/tools/bundle.rs +++ b/cli/tools/bundle.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use deno_core::error::AnyError; use deno_core::futures::FutureExt; -use deno_core::resolve_url_or_path; use deno_graph::Module; use deno_runtime::colors; @@ -35,8 +34,7 @@ pub async fn bundle( "Use alternative bundlers like \"deno_emit\", \"esbuild\" or \"rollup\" instead." ); - let module_specifier = - resolve_url_or_path(&bundle_flags.source_file, cli_options.initial_cwd())?; + let module_specifier = cli_options.resolve_main_module()?; let resolver = |_| { let cli_options = cli_options.clone(); diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index a2d9a3027b..547dc379b2 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -49,7 +49,14 @@ pub async fn format( fmt_options: FmtOptions, ) -> Result<(), AnyError> { if fmt_options.is_stdin { - return format_stdin(fmt_options); + return format_stdin( + fmt_options, + cli_options + .ext_flag() + .as_ref() + .map(|s| s.as_str()) + .unwrap_or("ts"), + ); } let files = fmt_options.files; @@ -456,14 +463,14 @@ fn format_ensure_stable( } /// Format stdin and write result to stdout. -/// Treats input as TypeScript or as set by `--ext` flag. +/// Treats input as set by `--ext` flag. /// Compatible with `--check` flag. -fn format_stdin(fmt_options: FmtOptions) -> Result<(), AnyError> { +fn format_stdin(fmt_options: FmtOptions, ext: &str) -> Result<(), AnyError> { let mut source = String::new(); if stdin().read_to_string(&mut source).is_err() { bail!("Failed to read from stdin"); } - let file_path = PathBuf::from(format!("_stdin.{}", fmt_options.ext)); + let file_path = PathBuf::from(format!("_stdin.{ext}")); let formatted_text = format_file(&file_path, &source, &fmt_options.options)?; if fmt_options.check { if formatted_text.is_some() { diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index 7224eb45f7..fcc31a764d 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -6,7 +6,6 @@ use crate::colors; use crate::proc_state::ProcState; use crate::worker::create_main_worker; use deno_core::error::AnyError; -use deno_core::resolve_path; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use rustyline::error::ReadlineError; @@ -82,8 +81,7 @@ async fn read_eval_file( pub async fn run(flags: Flags, repl_flags: ReplFlags) -> Result { let ps = ProcState::build(flags).await?; - let main_module = - resolve_path("./$deno$repl.ts", ps.options.initial_cwd()).unwrap(); + let main_module = ps.options.resolve_main_module()?; let mut worker = create_main_worker( &ps, main_module, diff --git a/cli/tools/run.rs b/cli/tools/run.rs index 84ec75e1a5..007e0fb2a4 100644 --- a/cli/tools/run.rs +++ b/cli/tools/run.rs @@ -5,26 +5,18 @@ use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; -use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::resolve_path; -use deno_core::resolve_url_or_path; -use deno_graph::npm::NpmPackageReqReference; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use crate::args::EvalFlags; use crate::args::Flags; -use crate::args::RunFlags; use crate::file_fetcher::File; use crate::proc_state::ProcState; use crate::util; use crate::worker::create_main_worker; -pub async fn run_script( - flags: Flags, - run_flags: RunFlags, -) -> Result { +pub async fn run_script(flags: Flags) -> Result { if !flags.has_permission() && flags.has_permission_in_argv() { log::warn!( "{}", @@ -37,7 +29,7 @@ To grant permissions, set them before the script argument. For example: } if flags.watch.is_some() { - return run_with_watch(flags, run_flags.script).await; + return run_with_watch(flags).await; } // TODO(bartlomieju): actually I think it will also fail if there's an import @@ -52,12 +44,8 @@ To grant permissions, set them before the script argument. For example: ps.dir.upgrade_check_file_path(), ); - let main_module = - if NpmPackageReqReference::from_str(&run_flags.script).is_ok() { - ModuleSpecifier::parse(&run_flags.script)? - } else { - resolve_url_or_path(&run_flags.script, ps.options.initial_cwd())? - }; + let main_module = ps.options.resolve_main_module()?; + let permissions = PermissionsContainer::new(Permissions::from_options( &ps.options.permissions_options(), )?); @@ -69,8 +57,8 @@ To grant permissions, set them before the script argument. For example: pub async fn run_from_stdin(flags: Flags) -> Result { let ps = ProcState::build(flags).await?; - let cwd = std::env::current_dir().context("Unable to get CWD")?; - let main_module = resolve_path("./$deno$stdin.ts", &cwd).unwrap(); + let main_module = ps.options.resolve_main_module()?; + let mut worker = create_main_worker( &ps, main_module.clone(), @@ -101,12 +89,12 @@ pub async fn run_from_stdin(flags: Flags) -> Result { // TODO(bartlomieju): this function is not handling `exit_code` set by the runtime // code properly. -async fn run_with_watch(flags: Flags, script: String) -> Result { +async fn run_with_watch(flags: Flags) -> Result { let flags = Arc::new(flags); let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); let mut ps = ProcState::build_for_file_watcher((*flags).clone(), sender.clone()).await?; - let main_module = resolve_url_or_path(&script, ps.options.initial_cwd())?; + let main_module = ps.options.resolve_main_module()?; let operation = |main_module: ModuleSpecifier| { ps.reset_for_file_watcher(); @@ -140,13 +128,8 @@ pub async fn eval_command( flags: Flags, eval_flags: EvalFlags, ) -> Result { - // deno_graph works off of extensions for local files to determine the media - // type, and so our "fake" specifier needs to have the proper extension. let ps = ProcState::build(flags).await?; - let main_module = resolve_path( - &format!("./$deno$eval.{}", eval_flags.ext), - ps.options.initial_cwd(), - )?; + let main_module = ps.options.resolve_main_module()?; let permissions = PermissionsContainer::new(Permissions::from_options( &ps.options.permissions_options(), )?); diff --git a/cli/tools/standalone.rs b/cli/tools/standalone.rs index dcd2f5d437..93c3aebf0e 100644 --- a/cli/tools/standalone.rs +++ b/cli/tools/standalone.rs @@ -39,8 +39,7 @@ pub async fn compile( compile_flags: CompileFlags, ) -> Result<(), AnyError> { let ps = ProcState::build(flags).await?; - let module_specifier = - resolve_url_or_path(&compile_flags.source_file, ps.options.initial_cwd())?; + let module_specifier = ps.options.resolve_main_module()?; let module_roots = { let mut vec = Vec::with_capacity(compile_flags.include.len() + 1); vec.push(module_specifier.clone()); diff --git a/tools/lint.js b/tools/lint.js index 36ab12e841..f77ddbaf7d 100755 --- a/tools/lint.js +++ b/tools/lint.js @@ -50,6 +50,7 @@ async function dlint() { ":!:cli/tsc/dts/**", ":!:cli/tests/testdata/encoding/**", ":!:cli/tests/testdata/error_syntax.js", + ":!:cli/tests/testdata/file_extensions/ts_with_js_extension.js", ":!:cli/tests/testdata/fmt/**", ":!:cli/tests/testdata/npm/**", ":!:cli/tests/testdata/lint/**",