Skip to content

Commit

Permalink
feat(init): add --npm flag to initialize npm projects (#26896)
Browse files Browse the repository at this point in the history
This commit adds support for `deno init --npm <package>`.

Running this will actually call to `npm:create-<package>` package that
is equivalent to running `npm create <package>`.

User will be prompted if they want to allow all permissions and
lifecycle scripts to be executed.
Closes #26461

---------

Signed-off-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Co-authored-by: crowlkats <crowlkats@toaxl.com>
Co-authored-by: David Sherret <dsherret@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 21, 2024
1 parent f0b245c commit d17f459
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 12 deletions.
121 changes: 115 additions & 6 deletions cli/args/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ impl FmtFlags {

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InitFlags {
pub package: Option<String>,
pub package_args: Vec<String>,
pub dir: Option<String>,
pub lib: bool,
pub serve: bool,
Expand Down Expand Up @@ -1395,7 +1397,7 @@ pub fn flags_from_vec(args: Vec<OsString>) -> clap::error::Result<Flags> {
"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),
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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::<String>("args") {
let name = args.next().unwrap();
let mut args = args.collect::<Vec<_>>();

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::<String>("dir"),
lib: matches.get_flag("lib"),
serve: matches.get_flag("serve"),
package,
package_args,
dir,
lib,
serve,
});

Ok(())
}

fn info_parse(
Expand Down Expand Up @@ -10907,6 +10953,8 @@ mod tests {
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Init(InitFlags {
package: None,
package_args: vec![],
dir: None,
lib: false,
serve: false,
Expand All @@ -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,
Expand All @@ -10933,6 +10983,8 @@ mod tests {
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Init(InitFlags {
package: None,
package_args: vec![],
dir: None,
lib: false,
serve: false,
Expand All @@ -10947,6 +10999,8 @@ mod tests {
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Init(InitFlags {
package: None,
package_args: vec![],
dir: None,
lib: true,
serve: false,
Expand All @@ -10960,6 +11014,8 @@ mod tests {
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Init(InitFlags {
package: None,
package_args: vec![],
dir: None,
lib: false,
serve: true,
Expand All @@ -10973,13 +11029,66 @@ mod tests {
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Init(InitFlags {
package: None,
package_args: vec![],
dir: Some(String::from("foo")),
lib: true,
serve: false,
}),
..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]
Expand Down
3 changes: 2 additions & 1 deletion cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 1 addition & 3 deletions cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,7 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
}
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) => {
Expand Down
68 changes: 66 additions & 2 deletions cli/tools/init/mod.rs
Original file line number Diff line number Diff line change
@@ -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<i32, AnyError> {
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 {
Expand Down Expand Up @@ -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<String>) -> Result<i32, AnyError> {
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 <u>deno run {} {}</> and applying desired permissions.", script_name, args.join(" ")));
1
}

if std::io::stdin().is_terminal() {
log::info!(
cstr!("⚠️ Do you fully trust <y>{}</> package? Deno will invoke code from it with all permissions. Do you want to continue? <p(245)>[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(
Expand Down
6 changes: 6 additions & 0 deletions tests/specs/init/npm/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tempDir": true,
"args": "init --npm vite my-project",
"output": "init.out",
"exitCode": 1
}
1 change: 1 addition & 0 deletions tests/specs/init/npm/init.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You can initialize project manually by running deno run npm:create-vite my-project and applying desired permissions.

0 comments on commit d17f459

Please sign in to comment.