diff --git a/cli/args/flags.rs b/cli/args/flags.rs index c6bb904309..b9908f413a 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -83,6 +83,11 @@ pub struct AddFlags { pub packages: Vec, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct RemoveFlags { + pub packages: Vec, +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct BenchFlags { pub files: FileFlags, @@ -428,6 +433,7 @@ pub struct HelpFlags { #[derive(Clone, Debug, Eq, PartialEq)] pub enum DenoSubcommand { Add(AddFlags), + Remove(RemoveFlags), Bench(BenchFlags), Bundle(BundleFlags), Cache(CacheFlags), @@ -1216,6 +1222,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { if let Some((subcommand, mut m)) = matches.remove_subcommand() { match subcommand.as_str() { "add" => add_parse(&mut flags, &mut m), + "remove" => remove_parse(&mut flags, &mut m), "bench" => bench_parse(&mut flags, &mut m), "bundle" => bundle_parse(&mut flags, &mut m), "cache" => cache_parse(&mut flags, &mut m), @@ -1442,6 +1449,7 @@ pub fn clap_root() -> Command { .defer(|cmd| { let cmd = cmd .subcommand(add_subcommand()) + .subcommand(remove_subcommand()) .subcommand(bench_subcommand()) .subcommand(bundle_subcommand()) .subcommand(cache_subcommand()) @@ -1515,6 +1523,31 @@ You can add multiple dependencies at once: }) } +fn remove_subcommand() -> Command { + Command::new("remove") + .alias("rm") + .about("Remove dependencies") + .long_about( + "Remove dependencies from the configuration file. + + deno remove @std/path + +You can remove multiple dependencies at once: + + deno remove @std/path @std/assert +", + ) + .defer(|cmd| { + cmd.arg( + Arg::new("packages") + .help("List of packages to remove") + .required(true) + .num_args(1..) + .action(ArgAction::Append), + ) + }) +} + fn bench_subcommand() -> Command { Command::new("bench") .about( @@ -3726,6 +3759,12 @@ fn add_parse_inner( AddFlags { packages } } +fn remove_parse(flags: &mut Flags, matches: &mut ArgMatches) { + flags.subcommand = DenoSubcommand::Remove(RemoveFlags { + packages: matches.remove_many::("packages").unwrap().collect(), + }); +} + fn bench_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.type_check_mode = TypeCheckMode::Local; @@ -10247,6 +10286,35 @@ mod tests { ); } + #[test] + fn remove_subcommand() { + let r = flags_from_vec(svec!["deno", "remove"]); + r.unwrap_err(); + + let r = flags_from_vec(svec!["deno", "remove", "@david/which"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Remove(RemoveFlags { + packages: svec!["@david/which"], + }), + ..Flags::default() + } + ); + + let r = + flags_from_vec(svec!["deno", "remove", "@david/which", "@luca/hello"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Remove(RemoveFlags { + packages: svec!["@david/which", "@luca/hello"], + }), + ..Flags::default() + } + ); + } + #[test] fn run_with_frozen_lockfile() { let cases = [ diff --git a/cli/factory.rs b/cli/factory.rs index ed288b22f7..942aefd25a 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -355,7 +355,7 @@ impl CliFactory { let fs = self.fs(); let cli_options = self.cli_options()?; // For `deno install` we want to force the managed resolver so it can set up `node_modules/` directory. - create_cli_npm_resolver(if cli_options.use_byonm() && !matches!(cli_options.sub_command(), DenoSubcommand::Install(_) | DenoSubcommand::Add(_)) { + create_cli_npm_resolver(if cli_options.use_byonm() && !matches!(cli_options.sub_command(), DenoSubcommand::Install(_) | DenoSubcommand::Add(_) | DenoSubcommand::Remove(_)) { CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions { fs: fs.clone(), root_node_modules_dir: Some(match cli_options.node_modules_dir_path() { diff --git a/cli/main.rs b/cli/main.rs index 4955b79d09..1b2640758b 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -100,6 +100,9 @@ async fn run_subcommand(flags: Arc) -> Result { DenoSubcommand::Add(add_flags) => spawn_subcommand(async { tools::registry::add(flags, add_flags, tools::registry::AddCommandName::Add).await }), + DenoSubcommand::Remove(remove_flags) => spawn_subcommand(async { + tools::registry::remove(flags, remove_flags).await + }), DenoSubcommand::Bench(bench_flags) => spawn_subcommand(async { if bench_flags.watch.is_some() { tools::bench::run_benchmarks_with_watch(flags, bench_flags).await diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs index b3bed77217..34e803c732 100644 --- a/cli/tools/registry/mod.rs +++ b/cli/tools/registry/mod.rs @@ -64,6 +64,7 @@ mod unfurl; use auth::get_auth_method; use auth::AuthMethod; pub use pm::add; +pub use pm::remove; pub use pm::AddCommandName; use publish_order::PublishOrderGraph; use unfurl::SpecifierUnfurler; diff --git a/cli/tools/registry/pm.rs b/cli/tools/registry/pm.rs index 86596df9dc..90238b8903 100644 --- a/cli/tools/registry/pm.rs +++ b/cli/tools/registry/pm.rs @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::borrow::Cow; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -23,6 +24,7 @@ use jsonc_parser::ast::Value; use crate::args::AddFlags; use crate::args::CacheSetting; use crate::args::Flags; +use crate::args::RemoveFlags; use crate::factory::CliFactory; use crate::file_fetcher::FileFetcher; use crate::jsr::JsrFetchResolver; @@ -337,9 +339,7 @@ pub async fn add( // make a new CliFactory to pick up the updated config file let cli_factory = CliFactory::from_flags(flags); // cache deps - if cli_factory.cli_options()?.enable_future_features() { - crate::module_loader::load_top_level_deps(&cli_factory).await?; - } + crate::module_loader::load_top_level_deps(&cli_factory).await?; Ok(()) } @@ -513,6 +513,85 @@ fn generate_imports(packages_to_version: Vec<(String, String)>) -> String { contents.join("\n") } +fn remove_from_config( + config_path: &Path, + keys: &[&'static str], + packages_to_remove: &[String], + removed_packages: &mut Vec, + fmt_options: &FmtOptionsConfig, +) -> Result<(), AnyError> { + let mut json: serde_json::Value = + serde_json::from_slice(&std::fs::read(config_path)?)?; + for key in keys { + let Some(obj) = json.get_mut(*key).and_then(|v| v.as_object_mut()) else { + continue; + }; + for package in packages_to_remove { + if obj.shift_remove(package).is_some() { + removed_packages.push(package.clone()); + } + } + } + + let config = serde_json::to_string_pretty(&json)?; + let config = + crate::tools::fmt::format_json(config_path, &config, fmt_options) + .ok() + .flatten() + .unwrap_or(config); + + std::fs::write(config_path, config) + .context("Failed to update configuration file")?; + + Ok(()) +} + +pub async fn remove( + flags: Arc, + remove_flags: RemoveFlags, +) -> Result<(), AnyError> { + let (config_file, factory) = DenoOrPackageJson::from_flags(flags.clone())?; + let options = factory.cli_options()?; + let start_dir = &options.start_dir; + let fmt_config_options = config_file.fmt_options(); + + let mut removed_packages = Vec::new(); + + if let Some(deno_json) = start_dir.maybe_deno_json() { + remove_from_config( + &deno_json.specifier.to_file_path().unwrap(), + &["imports"], + &remove_flags.packages, + &mut removed_packages, + &fmt_config_options, + )?; + } + + if let Some(pkg_json) = start_dir.maybe_pkg_json() { + remove_from_config( + &pkg_json.path, + &["dependencies", "devDependencies"], + &remove_flags.packages, + &mut removed_packages, + &fmt_config_options, + )?; + } + + if removed_packages.is_empty() { + log::info!("No packages were removed"); + } else { + for package in &removed_packages { + log::info!("Removed {}", crate::colors::green(package)); + } + // Update deno.lock + node_resolver::PackageJsonThreadLocalCache::clear(); + let cli_factory = CliFactory::from_flags(flags); + crate::module_loader::load_top_level_deps(&cli_factory).await?; + } + + Ok(()) +} + fn update_config_file_content( obj: jsonc_parser::ast::Object, config_file_contents: &str, diff --git a/tests/specs/add/dist_tag/__test__.jsonc b/tests/specs/add/dist_tag/__test__.jsonc index ac8a65654c..93104966d9 100644 --- a/tests/specs/add/dist_tag/__test__.jsonc +++ b/tests/specs/add/dist_tag/__test__.jsonc @@ -3,7 +3,7 @@ "steps": [ { "args": "add npm:ajv@latest", - "output": "Add npm:ajv@8.11.0\n" + "output": "add.out" } ] } diff --git a/tests/specs/add/dist_tag/add.out b/tests/specs/add/dist_tag/add.out new file mode 100644 index 0000000000..928eb6d6c7 --- /dev/null +++ b/tests/specs/add/dist_tag/add.out @@ -0,0 +1,15 @@ +Add npm:ajv@8.11.0 +[UNORDERED_START] +Download http://localhost:4260/ajv +Download http://localhost:4260/fast-deep-equal +Download http://localhost:4260/json-schema-traverse +Download http://localhost:4260/require-from-string +Download http://localhost:4260/uri-js +Download http://localhost:4260/punycode +Download http://localhost:4260/ajv/ajv-8.11.0.tgz +Download http://localhost:4260/require-from-string/require-from-string-2.0.2.tgz +Download http://localhost:4260/uri-js/uri-js-4.4.1.tgz +Download http://localhost:4260/fast-deep-equal/fast-deep-equal-3.1.3.tgz +Download http://localhost:4260/json-schema-traverse/json-schema-traverse-1.0.0.tgz +Download http://localhost:4260/punycode/punycode-2.1.1.tgz +[UNORDERED_END] diff --git a/tests/specs/remove/basic/__test__.jsonc b/tests/specs/remove/basic/__test__.jsonc new file mode 100644 index 0000000000..2f4d82c88f --- /dev/null +++ b/tests/specs/remove/basic/__test__.jsonc @@ -0,0 +1,16 @@ +{ + "tempDir": true, + "steps": [{ + "args": ["add", "@std/assert", "@std/http"], + "output": "add.out" + }, { + "args": ["eval", "console.log(Deno.readTextFileSync('deno.lock').trim())"], + "output": "add_lock.out" + }, { + "args": ["remove", "@std/assert", "@std/http"], + "output": "rm.out" + }, { + "args": ["eval", "console.log(Deno.readTextFileSync('deno.lock').trim())"], + "output": "remove_lock.out" + }] +} diff --git a/tests/specs/remove/basic/add.out b/tests/specs/remove/basic/add.out new file mode 100644 index 0000000000..a93b0ab528 --- /dev/null +++ b/tests/specs/remove/basic/add.out @@ -0,0 +1,12 @@ +Created deno.json configuration file. +Add jsr:@std/assert@1.0.0 +Add jsr:@std/http@1.0.0 +[UNORDERED_START] +Download http://127.0.0.1:4250/@std/http/1.0.0_meta.json +Download http://127.0.0.1:4250/@std/assert/1.0.0_meta.json +Download http://127.0.0.1:4250/@std/http/1.0.0/mod.ts +Download http://127.0.0.1:4250/@std/assert/1.0.0/mod.ts +Download http://127.0.0.1:4250/@std/assert/1.0.0/assert_equals.ts +Download http://127.0.0.1:4250/@std/assert/1.0.0/assert.ts +Download http://127.0.0.1:4250/@std/assert/1.0.0/fail.ts +[UNORDERED_END] diff --git a/tests/specs/remove/basic/add_lock.out b/tests/specs/remove/basic/add_lock.out new file mode 100644 index 0000000000..a5a45e854b --- /dev/null +++ b/tests/specs/remove/basic/add_lock.out @@ -0,0 +1,24 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert@^1.0.0": "jsr:@std/assert@1.0.0", + "jsr:@std/http@^1.0.0": "jsr:@std/http@1.0.0" + }, + "jsr": { + "@std/assert@1.0.0": { + "integrity": "7ae268c58de9693b4997fd93d9b303a47df336664e2008378ccb93c3458d092a" + }, + "@std/http@1.0.0": { + "integrity": "d75bd303c21123a9b58f7249e38b4c0aa3a09f7d76b13f9d7e7842d89052091a" + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.0", + "jsr:@std/http@^1.0.0" + ] + } +} diff --git a/tests/specs/remove/basic/remove_lock.out b/tests/specs/remove/basic/remove_lock.out new file mode 100644 index 0000000000..37f10ce95e --- /dev/null +++ b/tests/specs/remove/basic/remove_lock.out @@ -0,0 +1,4 @@ +{ + "version": "3", + "remote": {} +} diff --git a/tests/specs/remove/basic/rm.out b/tests/specs/remove/basic/rm.out new file mode 100644 index 0000000000..083ab8c057 --- /dev/null +++ b/tests/specs/remove/basic/rm.out @@ -0,0 +1,2 @@ +Removed @std/assert +Removed @std/http