diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 08b48d1d5b..a70b2c6751 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -119,6 +119,11 @@ pub struct FmtFlags { pub prose_wrap: Option, } +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct InitFlags { + pub dir: Option, +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct InfoFlags { pub json: bool, @@ -217,6 +222,7 @@ pub enum DenoSubcommand { Doc(DocFlags), Eval(EvalFlags), Fmt(FmtFlags), + Init(InitFlags), Info(InfoFlags), Install(InstallFlags), Uninstall(UninstallFlags), @@ -554,6 +560,7 @@ pub fn flags_from_vec(args: Vec) -> clap::Result { Some(("doc", m)) => doc_parse(&mut flags, m), Some(("eval", m)) => eval_parse(&mut flags, m), Some(("fmt", m)) => fmt_parse(&mut flags, m), + Some(("init", m)) => init_parse(&mut flags, m), Some(("info", m)) => info_parse(&mut flags, m), Some(("install", m)) => install_parse(&mut flags, m), Some(("lint", m)) => lint_parse(&mut flags, m), @@ -629,6 +636,7 @@ fn clap_root(version: &str) -> Command { .subcommand(doc_subcommand()) .subcommand(eval_subcommand()) .subcommand(fmt_subcommand()) + .subcommand(init_subcommand()) .subcommand(info_subcommand()) .subcommand(install_subcommand()) .subcommand(uninstall_subcommand()) @@ -1140,6 +1148,15 @@ Ignore formatting a file by adding an ignore comment at the top of the file: ) } +fn init_subcommand<'a>() -> Command<'a> { + Command::new("init").about("Initialize a new project").arg( + Arg::new("dir") + .takes_value(true) + .required(false) + .value_hint(ValueHint::DirPath), + ) +} + fn info_subcommand<'a>() -> Command<'a> { Command::new("info") .about("Show info about cache or info related to source file") @@ -2436,6 +2453,12 @@ fn fmt_parse(flags: &mut Flags, matches: &clap::ArgMatches) { }); } +fn init_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + flags.subcommand = DenoSubcommand::Init(InitFlags { + dir: matches.value_of("dir").map(|f| f.to_string()), + }); +} + fn info_parse(flags: &mut Flags, matches: &clap::ArgMatches) { reload_arg_parse(flags, matches); config_args_parse(flags, matches); @@ -5951,4 +5974,27 @@ mod tests { ]); assert!(r.is_err()); } + + #[test] + fn init() { + let r = flags_from_vec(svec!["deno", "init"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Init(InitFlags { dir: None }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "init", "foo"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Init(InitFlags { + dir: Some(String::from("foo")), + }), + ..Flags::default() + } + ); + } } diff --git a/cli/main.rs b/cli/main.rs index b157b35823..db625e4042 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -51,6 +51,7 @@ use crate::args::EvalFlags; use crate::args::Flags; use crate::args::FmtFlags; use crate::args::InfoFlags; +use crate::args::InitFlags; use crate::args::InstallFlags; use crate::args::LintFlags; use crate::args::ReplFlags; @@ -273,6 +274,14 @@ async fn compile_command( Ok(0) } +async fn init_command( + _flags: Flags, + init_flags: InitFlags, +) -> Result { + tools::init::init_project(init_flags).await?; + Ok(0) +} + async fn info_command( flags: Flags, info_flags: InfoFlags, @@ -941,6 +950,9 @@ fn get_subcommand( DenoSubcommand::Fmt(fmt_flags) => { format_command(flags, fmt_flags).boxed_local() } + DenoSubcommand::Init(init_flags) => { + init_command(flags, init_flags).boxed_local() + } DenoSubcommand::Info(info_flags) => { info_command(flags, info_flags).boxed_local() } diff --git a/cli/tests/integration/init_tests.rs b/cli/tests/integration/init_tests.rs new file mode 100644 index 0000000000..a7a54f06bf --- /dev/null +++ b/cli/tests/integration/init_tests.rs @@ -0,0 +1,110 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::process::Stdio; +use test_util as util; +use test_util::TempDir; +use util::assert_contains; + +#[test] +fn init_subcommand_without_dir() { + let temp_dir = TempDir::new(); + let cwd = temp_dir.path(); + let deno_dir = util::new_deno_dir(); + + let mut deno_cmd = util::deno_cmd_with_deno_dir(&deno_dir); + let output = deno_cmd + .current_dir(cwd) + .arg("init") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_contains!(stdout, "Project initialized"); + assert!(!stdout.contains("cd")); + assert_contains!(stdout, "deno run main.ts"); + assert_contains!(stdout, "deno test"); + + let mut deno_cmd = util::deno_cmd_with_deno_dir(&deno_dir); + let output = deno_cmd + .current_dir(cwd) + .env("NO_COLOR", "1") + .arg("run") + .arg("main.ts") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + assert_eq!(output.stdout, b"Add 2 + 3 = 5\n"); + + let mut deno_cmd = util::deno_cmd_with_deno_dir(&deno_dir); + let output = deno_cmd + .current_dir(cwd) + .env("NO_COLOR", "1") + .arg("test") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_contains!(stdout, "1 passed"); +} + +#[test] +fn init_subcommand_with_dir_arg() { + let temp_dir = TempDir::new(); + let cwd = temp_dir.path(); + let deno_dir = util::new_deno_dir(); + + let mut deno_cmd = util::deno_cmd_with_deno_dir(&deno_dir); + let output = deno_cmd + .current_dir(cwd) + .arg("init") + .arg("my_dir") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_contains!(stdout, "Project initialized"); + assert_contains!(stdout, "cd my_dir"); + assert_contains!(stdout, "deno run main.ts"); + assert_contains!(stdout, "deno test"); + + let mut deno_cmd = util::deno_cmd_with_deno_dir(&deno_dir); + let output = deno_cmd + .current_dir(cwd) + .env("NO_COLOR", "1") + .arg("run") + .arg("my_dir/main.ts") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + assert_eq!(output.stdout, b"Add 2 + 3 = 5\n"); + + let mut deno_cmd = util::deno_cmd_with_deno_dir(&deno_dir); + let output = deno_cmd + .current_dir(cwd) + .env("NO_COLOR", "1") + .arg("test") + .arg("my_dir/main_test.ts") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_contains!(stdout, "1 passed"); +} diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs index 3101d8dc76..53c83c009b 100644 --- a/cli/tests/integration/mod.rs +++ b/cli/tests/integration/mod.rs @@ -74,6 +74,8 @@ mod eval; mod fmt; #[path = "info_tests.rs"] mod info; +#[path = "init_tests.rs"] +mod init; #[path = "inspector_tests.rs"] mod inspector; #[path = "install_tests.rs"] diff --git a/cli/tools/init/mod.rs b/cli/tools/init/mod.rs new file mode 100644 index 0000000000..e47b88702f --- /dev/null +++ b/cli/tools/init/mod.rs @@ -0,0 +1,49 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::args::InitFlags; +use crate::compat; +use deno_core::{anyhow::Context, error::AnyError}; +use std::io::Write; +use std::path::Path; + +fn create_file( + dir: &Path, + filename: &str, + content: &str, +) -> Result<(), AnyError> { + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(dir.join(filename)) + .with_context(|| format!("Failed to create {} file", filename))?; + file.write_all(content.as_bytes())?; + Ok(()) +} + +pub async fn init_project(init_flags: InitFlags) -> Result<(), AnyError> { + let cwd = + std::env::current_dir().context("Can't read current working directory.")?; + let dir = if let Some(dir) = &init_flags.dir { + let dir = cwd.join(dir); + std::fs::create_dir_all(&dir)?; + dir + } else { + cwd + }; + + let main_ts = include_str!("./templates/main.ts"); + create_file(&dir, "main.ts", main_ts)?; + + let main_test_ts = include_str!("./templates/main_test.ts") + .replace("{CURRENT_STD_URL}", compat::STD_URL_STR); + create_file(&dir, "main_test.ts", &main_test_ts)?; + + println!("✅ Project initialized"); + println!("Run these commands to get started"); + if let Some(dir) = init_flags.dir { + println!(" cd {}", dir); + } + println!(" deno run main.ts"); + println!(" deno test"); + Ok(()) +} diff --git a/cli/tools/init/templates/main.ts b/cli/tools/init/templates/main.ts new file mode 100644 index 0000000000..be043e97c8 --- /dev/null +++ b/cli/tools/init/templates/main.ts @@ -0,0 +1,8 @@ +export function add(a: number, b: number): number { + return a + b; +} + +// Learn more at https://deno.land/manual/examples/module_metadata#concepts +if (import.meta.main) { + console.log("Add 2 + 3 =", add(2, 3)); +} diff --git a/cli/tools/init/templates/main_test.ts b/cli/tools/init/templates/main_test.ts new file mode 100644 index 0000000000..5f60b571c4 --- /dev/null +++ b/cli/tools/init/templates/main_test.ts @@ -0,0 +1,6 @@ +import { assertEquals } from "{CURRENT_STD_URL}testing/asserts.ts"; +import { add } from "./main.ts"; + +Deno.test(function addTest() { + assertEquals(add(2, 3), 5); +}); diff --git a/cli/tools/mod.rs b/cli/tools/mod.rs index 7c5d79744c..11c9047769 100644 --- a/cli/tools/mod.rs +++ b/cli/tools/mod.rs @@ -4,6 +4,7 @@ pub mod bench; pub mod coverage; pub mod doc; pub mod fmt; +pub mod init; pub mod installer; pub mod lint; pub mod repl;