From d17f4590a246675b12ced7272b93b670b37f9f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 21 Nov 2024 00:03:11 +0000 Subject: [PATCH] feat(init): add --npm flag to initialize npm projects (#26896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds support for `deno init --npm `. Running this will actually call to `npm:create-` package that is equivalent to running `npm create `. User will be prompted if they want to allow all permissions and lifecycle scripts to be executed. Closes https://github.com/denoland/deno/issues/26461 --------- Signed-off-by: Bartek Iwańczuk Co-authored-by: crowlkats Co-authored-by: David Sherret --- cli/args/flags.rs | 121 ++++++++++++++++++++++++++-- cli/args/mod.rs | 3 +- cli/main.rs | 4 +- cli/tools/init/mod.rs | 68 +++++++++++++++- tests/specs/init/npm/__test__.jsonc | 6 ++ tests/specs/init/npm/init.out | 1 + 6 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 tests/specs/init/npm/__test__.jsonc create mode 100644 tests/specs/init/npm/init.out diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 3464766728..f6d53cd15e 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -222,6 +222,8 @@ impl FmtFlags { #[derive(Clone, Debug, Eq, PartialEq)] pub struct InitFlags { + pub package: Option, + pub package_args: Vec, pub dir: Option, pub lib: bool, pub serve: bool, @@ -1395,7 +1397,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { "doc" => doc_parse(&mut flags, &mut m)?, "eval" => eval_parse(&mut flags, &mut m)?, "fmt" => fmt_parse(&mut flags, &mut m)?, - "init" => init_parse(&mut flags, &mut m), + "init" => init_parse(&mut flags, &mut m)?, "info" => info_parse(&mut flags, &mut m)?, "install" => install_parse(&mut flags, &mut m)?, "json_reference" => json_reference_parse(&mut flags, &mut m, app), @@ -2448,7 +2450,19 @@ fn init_subcommand() -> Command { command("init", "scaffolds a basic Deno project with a script, test, and configuration file", UnstableArgsConfig::None).defer( |cmd| { cmd - .arg(Arg::new("dir").value_hint(ValueHint::DirPath)) + .arg(Arg::new("args") + .num_args(0..) + .action(ArgAction::Append) + .value_name("DIRECTORY OR PACKAGE") + .trailing_var_arg(true) + ) + .arg( + Arg::new("npm") + .long("npm") + .help("Generate a npm create-* project") + .conflicts_with_all(["lib", "serve"]) + .action(ArgAction::SetTrue), + ) .arg( Arg::new("lib") .long("lib") @@ -4820,12 +4834,44 @@ fn fmt_parse( Ok(()) } -fn init_parse(flags: &mut Flags, matches: &mut ArgMatches) { +fn init_parse( + flags: &mut Flags, + matches: &mut ArgMatches, +) -> Result<(), clap::Error> { + let mut lib = matches.get_flag("lib"); + let mut serve = matches.get_flag("serve"); + let mut dir = None; + let mut package = None; + let mut package_args = vec![]; + + if let Some(mut args) = matches.remove_many::("args") { + let name = args.next().unwrap(); + let mut args = args.collect::>(); + + if matches.get_flag("npm") { + package = Some(name); + package_args = args; + } else { + dir = Some(name); + + if !args.is_empty() { + args.insert(0, "init".to_string()); + let inner_matches = init_subcommand().try_get_matches_from_mut(args)?; + lib = inner_matches.get_flag("lib"); + serve = inner_matches.get_flag("serve"); + } + } + } + flags.subcommand = DenoSubcommand::Init(InitFlags { - dir: matches.remove_one::("dir"), - lib: matches.get_flag("lib"), - serve: matches.get_flag("serve"), + package, + package_args, + dir, + lib, + serve, }); + + Ok(()) } fn info_parse( @@ -10907,6 +10953,8 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Init(InitFlags { + package: None, + package_args: vec![], dir: None, lib: false, serve: false, @@ -10920,6 +10968,8 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Init(InitFlags { + package: None, + package_args: vec![], dir: Some(String::from("foo")), lib: false, serve: false, @@ -10933,6 +10983,8 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Init(InitFlags { + package: None, + package_args: vec![], dir: None, lib: false, serve: false, @@ -10947,6 +10999,8 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Init(InitFlags { + package: None, + package_args: vec![], dir: None, lib: true, serve: false, @@ -10960,6 +11014,8 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Init(InitFlags { + package: None, + package_args: vec![], dir: None, lib: false, serve: true, @@ -10973,6 +11029,8 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Init(InitFlags { + package: None, + package_args: vec![], dir: Some(String::from("foo")), lib: true, serve: false, @@ -10980,6 +11038,57 @@ mod tests { ..Flags::default() } ); + + let r = flags_from_vec(svec!["deno", "init", "--lib", "--npm", "vite"]); + assert!(r.is_err()); + + let r = flags_from_vec(svec!["deno", "init", "--serve", "--npm", "vite"]); + assert!(r.is_err()); + + let r = flags_from_vec(svec!["deno", "init", "--npm", "vite", "--lib"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Init(InitFlags { + package: Some("vite".to_string()), + package_args: svec!["--lib"], + dir: None, + lib: false, + serve: false, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "init", "--npm", "vite", "--serve"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Init(InitFlags { + package: Some("vite".to_string()), + package_args: svec!["--serve"], + dir: None, + lib: false, + serve: false, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "init", "--npm", "vite", "new_dir"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Init(InitFlags { + package: Some("vite".to_string()), + package_args: svec!["new_dir"], + dir: None, + lib: false, + serve: false, + }), + ..Flags::default() + } + ); } #[test] diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 37e1f00ffa..a1a9c49cbe 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -1628,9 +1628,10 @@ impl CliOptions { DenoSubcommand::Install(_) | DenoSubcommand::Add(_) | DenoSubcommand::Remove(_) + | DenoSubcommand::Init(_) | DenoSubcommand::Outdated(_) ) { - // For `deno install/add/remove` we want to force the managed resolver so it can set up `node_modules/` directory. + // For `deno install/add/remove/init` we want to force the managed resolver so it can set up `node_modules/` directory. return false; } if self.node_modules_dir().ok().flatten().is_none() diff --git a/cli/main.rs b/cli/main.rs index 29f5914bbf..c49c8a83a6 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -144,9 +144,7 @@ async fn run_subcommand(flags: Arc) -> Result { } DenoSubcommand::Init(init_flags) => { spawn_subcommand(async { - // make compiler happy since init_project is sync - tokio::task::yield_now().await; - tools::init::init_project(init_flags) + tools::init::init_project(init_flags).await }) } DenoSubcommand::Info(info_flags) => { diff --git a/cli/tools/init/mod.rs b/cli/tools/init/mod.rs index 4e4a686c5f..8f486dad53 100644 --- a/cli/tools/init/mod.rs +++ b/cli/tools/init/mod.rs @@ -1,15 +1,28 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use crate::args::DenoSubcommand; +use crate::args::Flags; use crate::args::InitFlags; +use crate::args::PackagesAllowedScripts; +use crate::args::PermissionFlags; +use crate::args::RunFlags; use crate::colors; +use color_print::cformat; +use color_print::cstr; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::serde_json::json; +use deno_runtime::WorkerExecutionMode; use log::info; +use std::io::IsTerminal; use std::io::Write; use std::path::Path; -pub fn init_project(init_flags: InitFlags) -> Result<(), AnyError> { +pub async fn init_project(init_flags: InitFlags) -> Result { + if let Some(package) = &init_flags.package { + return init_npm(package, init_flags.package_args).await; + } + let cwd = std::env::current_dir().context("Can't read current working directory.")?; let dir = if let Some(dir) = &init_flags.dir { @@ -235,7 +248,58 @@ Deno.test(function addTest() { info!(" {}", colors::gray("# Run the tests")); info!(" deno test"); } - Ok(()) + Ok(0) +} + +async fn init_npm(name: &str, args: Vec) -> Result { + let script_name = format!("npm:create-{}", name); + + fn print_manual_usage(script_name: &str, args: &[String]) -> i32 { + log::info!("{}", cformat!("You can initialize project manually by running deno run {} {} and applying desired permissions.", script_name, args.join(" "))); + 1 + } + + if std::io::stdin().is_terminal() { + log::info!( + cstr!("⚠️ Do you fully trust {} package? Deno will invoke code from it with all permissions. Do you want to continue? [y/n]"), + script_name + ); + loop { + let _ = std::io::stdout().write(b"> ")?; + std::io::stdout().flush()?; + let mut answer = String::new(); + if std::io::stdin().read_line(&mut answer).is_ok() { + let answer = answer.trim().to_ascii_lowercase(); + if answer != "y" { + return Ok(print_manual_usage(&script_name, &args)); + } else { + break; + } + } + } + } else { + return Ok(print_manual_usage(&script_name, &args)); + } + + let new_flags = Flags { + permissions: PermissionFlags { + allow_all: true, + ..Default::default() + }, + allow_scripts: PackagesAllowedScripts::All, + argv: args, + subcommand: DenoSubcommand::Run(RunFlags { + script: script_name, + ..Default::default() + }), + ..Default::default() + }; + crate::tools::run::run_script( + WorkerExecutionMode::Run, + new_flags.into(), + None, + ) + .await } fn create_json_file( diff --git a/tests/specs/init/npm/__test__.jsonc b/tests/specs/init/npm/__test__.jsonc new file mode 100644 index 0000000000..ccc9d181dd --- /dev/null +++ b/tests/specs/init/npm/__test__.jsonc @@ -0,0 +1,6 @@ +{ + "tempDir": true, + "args": "init --npm vite my-project", + "output": "init.out", + "exitCode": 1 +} diff --git a/tests/specs/init/npm/init.out b/tests/specs/init/npm/init.out new file mode 100644 index 0000000000..f4ba939437 --- /dev/null +++ b/tests/specs/init/npm/init.out @@ -0,0 +1 @@ +You can initialize project manually by running deno run npm:create-vite my-project and applying desired permissions.