From 56f31628f7a9d4a3dc54c70c4df1067b8a214ec6 Mon Sep 17 00:00:00 2001 From: Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:22:15 -0800 Subject: [PATCH] feat: subcommand to view and update outdated dependencies (#26942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #20487 Currently spelled ``` deno outdated ``` and ``` deno outdated --update ``` Works across package.json and deno.json, and in workspaces. There's a bit of duplicated code, I'll refactor to reduce this in follow ups ## Currently supported: ### Printing outdated deps (current output below which basically mimics pnpm, but requesting feedback / suggestions) ``` deno outdated ``` ![Screenshot 2024-11-19 at 2 01 56 PM](https://github.com/user-attachments/assets/51fea83a-181a-4082-b388-163313ce15e7) ### Updating deps semver compatible: ``` deno outdated --update ``` latest: ``` deno outdated --latest ``` current output is basic, again would love suggestions ![Screenshot 2024-11-19 at 2 13 46 PM](https://github.com/user-attachments/assets/e4c4db87-cd67-4b74-9ea7-4bd80106d5e9) #### Filters ``` deno outdated --update "@std/*" deno outdated --update --latest "@std/* "!@std/fmt" ``` #### Update to specific versions ``` deno outdated --update @std/fmt@1.0.2 @std/cli@^1.0.3 ``` ### Include all workspace members ``` deno outdated --recursive deno outdated --update --recursive ``` ## Future work - interactive update - update deps in js/ts files - better support for transitive deps Known issues (to be fixed in follow ups): - If no top level dependencies have changed, we won't update transitive deps (even if they could be updated) - Can't filter transitive deps, or update them to specific versions ## TODO (in this PR): - ~~spec tests for filters~~ - ~~spec test for mixed workspace (have tested manually)~~ - tweak output - suggestion when you try `deno update` --------- Co-authored-by: Bartek Iwańczuk --- cli/args/flags.rs | 192 ++++ cli/args/mod.rs | 1 + cli/main.rs | 5 + cli/npm/managed/mod.rs | 2 +- cli/tools/registry/mod.rs | 1 + cli/tools/registry/pm.rs | 32 +- cli/tools/registry/pm/cache_deps.rs | 17 +- cli/tools/registry/pm/deps.rs | 964 ++++++++++++++++++ cli/tools/registry/pm/outdated.rs | 661 ++++++++++++ tests/registry/jsr/@denotest/add/0.2.1/mod.ts | 4 + .../jsr/@denotest/add/0.2.1_meta.json | 8 + tests/registry/jsr/@denotest/add/meta.json | 3 +- .../@denotest/multiple-exports/0.2.0/add.ts | 1 + .../multiple-exports/0.2.0/data.json | 3 + .../multiple-exports/0.2.0/subtract.ts | 1 + .../multiple-exports/0.2.0_meta.json | 7 + .../@denotest/multiple-exports/0.5.0/add.ts | 1 + .../multiple-exports/0.5.0/data.json | 3 + .../multiple-exports/0.5.0/subtract.ts | 1 + .../multiple-exports/0.5.0_meta.json | 7 + .../jsr/@denotest/multiple-exports/meta.json | 4 +- .../jsr/@denotest/subtract/0.2.0/mod.ts | 3 + .../jsr/@denotest/subtract/0.2.0_meta.json | 8 + .../registry/jsr/@denotest/subtract/meta.json | 4 +- .../has-patch-versions/0.1.0/package.json | 5 + .../has-patch-versions/0.1.1/package.json | 5 + .../has-patch-versions/0.2.0/package.json | 4 + tests/specs/update/deno_json/__test__.jsonc | 101 ++ tests/specs/update/deno_json/deno.json | 19 + tests/specs/update/deno_json/deno.lock | 43 + .../specs/update/deno_json/deno.lock.orig.out | 43 + .../update/deno_json/filtered/deno.json.out | 19 + .../update/deno_json/filtered/update.out | 10 + tests/specs/update/deno_json/outdated.out | 15 + .../update/deno_json/outdated_compatible.out | 7 + tests/specs/update/deno_json/print_file.ts | 2 + .../deno_json/update_compatible/deno.json.out | 19 + .../deno_json/update_compatible/deno.lock.out | 43 + .../deno_json/update_compatible/update.out | 7 + .../deno_json/update_latest/deno.json.out | 19 + .../deno_json/update_latest/deno.lock.out | 43 + .../update/deno_json/update_latest/update.out | 16 + .../update/mixed_workspace/__test__.jsonc | 153 +++ tests/specs/update/mixed_workspace/deno.json | 6 + tests/specs/update/mixed_workspace/deno.lock | 55 + .../update/mixed_workspace/deno.lock.orig.out | 55 + .../filtered/member_a_deno.json.out | 10 + .../filtered/member_b_package.json.out | 8 + .../mixed_workspace/filtered/update.out | 12 + .../update/mixed_workspace/member-a/deno.json | 10 + .../update/mixed_workspace/member-a/mod.ts | 0 .../mixed_workspace/member-b/package.json | 8 + .../update/mixed_workspace/print_file.ts | 2 + .../print_outdated/member_a.out | 9 + .../print_outdated/member_b.out | 7 + .../print_outdated/recursive.out | 15 + .../mixed_workspace/print_outdated/root.out | 5 + .../update_latest/recursive/update.out | 16 + .../update_latest/root/deno.json.out | 6 + .../update_latest/root/update.out | 3 + .../update_latest/subdir/member_a.out | 10 + .../subdir/member_a_deno.json.out | 10 + .../update_latest/subdir/member_b.out | 7 + .../subdir/member_b_package.json.out | 8 + .../specs/update/package_json/__test__.jsonc | 100 ++ tests/specs/update/package_json/deno.lock | 28 + .../update/package_json/deno.lock.orig.out | 28 + .../package_json/filtered/package.json.out | 9 + .../update/package_json/filtered/update.out | 9 + tests/specs/update/package_json/outdated.out | 9 + .../package_json/outdated_compatible.out | 5 + tests/specs/update/package_json/package.json | 9 + tests/specs/update/package_json/print_file.ts | 2 + .../update_compatible/deno.lock.out | 28 + .../update_compatible/package.json.out | 9 + .../package_json/update_compatible/update.out | 4 + .../package_json/update_latest/deno.lock.out | 28 + .../update_latest/package.json.out | 9 + .../package_json/update_latest/update.out | 14 + tests/util/server/src/npm.rs | 68 +- 80 files changed, 3109 insertions(+), 18 deletions(-) create mode 100644 cli/tools/registry/pm/deps.rs create mode 100644 cli/tools/registry/pm/outdated.rs create mode 100644 tests/registry/jsr/@denotest/add/0.2.1/mod.ts create mode 100644 tests/registry/jsr/@denotest/add/0.2.1_meta.json create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.2.0/add.ts create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.2.0/data.json create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.2.0/subtract.ts create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.2.0_meta.json create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.5.0/add.ts create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.5.0/data.json create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.5.0/subtract.ts create mode 100644 tests/registry/jsr/@denotest/multiple-exports/0.5.0_meta.json create mode 100644 tests/registry/jsr/@denotest/subtract/0.2.0/mod.ts create mode 100644 tests/registry/jsr/@denotest/subtract/0.2.0_meta.json create mode 100644 tests/registry/npm/@denotest/has-patch-versions/0.1.0/package.json create mode 100644 tests/registry/npm/@denotest/has-patch-versions/0.1.1/package.json create mode 100644 tests/registry/npm/@denotest/has-patch-versions/0.2.0/package.json create mode 100644 tests/specs/update/deno_json/__test__.jsonc create mode 100644 tests/specs/update/deno_json/deno.json create mode 100644 tests/specs/update/deno_json/deno.lock create mode 100644 tests/specs/update/deno_json/deno.lock.orig.out create mode 100644 tests/specs/update/deno_json/filtered/deno.json.out create mode 100644 tests/specs/update/deno_json/filtered/update.out create mode 100644 tests/specs/update/deno_json/outdated.out create mode 100644 tests/specs/update/deno_json/outdated_compatible.out create mode 100644 tests/specs/update/deno_json/print_file.ts create mode 100644 tests/specs/update/deno_json/update_compatible/deno.json.out create mode 100644 tests/specs/update/deno_json/update_compatible/deno.lock.out create mode 100644 tests/specs/update/deno_json/update_compatible/update.out create mode 100644 tests/specs/update/deno_json/update_latest/deno.json.out create mode 100644 tests/specs/update/deno_json/update_latest/deno.lock.out create mode 100644 tests/specs/update/deno_json/update_latest/update.out create mode 100644 tests/specs/update/mixed_workspace/__test__.jsonc create mode 100644 tests/specs/update/mixed_workspace/deno.json create mode 100644 tests/specs/update/mixed_workspace/deno.lock create mode 100644 tests/specs/update/mixed_workspace/deno.lock.orig.out create mode 100644 tests/specs/update/mixed_workspace/filtered/member_a_deno.json.out create mode 100644 tests/specs/update/mixed_workspace/filtered/member_b_package.json.out create mode 100644 tests/specs/update/mixed_workspace/filtered/update.out create mode 100644 tests/specs/update/mixed_workspace/member-a/deno.json create mode 100644 tests/specs/update/mixed_workspace/member-a/mod.ts create mode 100644 tests/specs/update/mixed_workspace/member-b/package.json create mode 100644 tests/specs/update/mixed_workspace/print_file.ts create mode 100644 tests/specs/update/mixed_workspace/print_outdated/member_a.out create mode 100644 tests/specs/update/mixed_workspace/print_outdated/member_b.out create mode 100644 tests/specs/update/mixed_workspace/print_outdated/recursive.out create mode 100644 tests/specs/update/mixed_workspace/print_outdated/root.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/recursive/update.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/root/deno.json.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/root/update.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/subdir/member_a.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/subdir/member_a_deno.json.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/subdir/member_b.out create mode 100644 tests/specs/update/mixed_workspace/update_latest/subdir/member_b_package.json.out create mode 100644 tests/specs/update/package_json/__test__.jsonc create mode 100644 tests/specs/update/package_json/deno.lock create mode 100644 tests/specs/update/package_json/deno.lock.orig.out create mode 100644 tests/specs/update/package_json/filtered/package.json.out create mode 100644 tests/specs/update/package_json/filtered/update.out create mode 100644 tests/specs/update/package_json/outdated.out create mode 100644 tests/specs/update/package_json/outdated_compatible.out create mode 100644 tests/specs/update/package_json/package.json create mode 100644 tests/specs/update/package_json/print_file.ts create mode 100644 tests/specs/update/package_json/update_compatible/deno.lock.out create mode 100644 tests/specs/update/package_json/update_compatible/package.json.out create mode 100644 tests/specs/update/package_json/update_compatible/update.out create mode 100644 tests/specs/update/package_json/update_latest/deno.lock.out create mode 100644 tests/specs/update/package_json/update_latest/package.json.out create mode 100644 tests/specs/update/package_json/update_latest/update.out diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 567d4adfb9..6945065574 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -465,6 +465,7 @@ pub enum DenoSubcommand { Serve(ServeFlags), Task(TaskFlags), Test(TestFlags), + Outdated(OutdatedFlags), Types, Upgrade(UpgradeFlags), Vendor, @@ -472,6 +473,19 @@ pub enum DenoSubcommand { Help(HelpFlags), } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum OutdatedKind { + Update { latest: bool }, + PrintOutdated { compatible: bool }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OutdatedFlags { + pub filters: Vec, + pub recursive: bool, + pub kind: OutdatedKind, +} + impl DenoSubcommand { pub fn is_run(&self) -> bool { matches!(self, Self::Run(_)) @@ -1203,6 +1217,7 @@ static DENO_HELP: &str = cstr!( deno add jsr:@std/assert | deno add npm:express install Installs dependencies either in the local project or globally to a bin directory uninstall Uninstalls a dependency or an executable script in the installation root's bin directory + outdated Find and update outdated dependencies remove Remove dependencies from the configuration file Tooling: @@ -1385,6 +1400,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { "jupyter" => jupyter_parse(&mut flags, &mut m), "lint" => lint_parse(&mut flags, &mut m)?, "lsp" => lsp_parse(&mut flags, &mut m), + "outdated" => outdated_parse(&mut flags, &mut m)?, "repl" => repl_parse(&mut flags, &mut m)?, "run" => run_parse(&mut flags, &mut m, app, false)?, "serve" => serve_parse(&mut flags, &mut m, app)?, @@ -1627,6 +1643,7 @@ pub fn clap_root() -> Command { .subcommand(json_reference_subcommand()) .subcommand(jupyter_subcommand()) .subcommand(uninstall_subcommand()) + .subcommand(outdated_subcommand()) .subcommand(lsp_subcommand()) .subcommand(lint_subcommand()) .subcommand(publish_subcommand()) @@ -2617,6 +2634,83 @@ fn jupyter_subcommand() -> Command { .conflicts_with("install")) } +fn outdated_subcommand() -> Command { + command( + "outdated", + cstr!("Find and update outdated dependencies. +By default, outdated dependencies are only displayed. + +Display outdated dependencies: + deno outdated + deno outdated --compatible + +Update dependencies: + deno outdated --update + deno outdated --update --latest + deno outdated --update + +Filters can be used to select which packages to act on. Filters can include wildcards (*) to match multiple packages. + deno outdated --update --latest \"@std/*\" + deno outdated --update --latest \"react*\" +Note that filters act on their aliases configured in deno.json / package.json, not the actual package names: + Given \"foobar\": \"npm:react@17.0.0\" in deno.json or package.json, the filter \"foobar\" would update npm:react to + the latest version. + deno outdated --update --latest foobar +Filters can be combined, and negative filters can be used to exclude results: + deno outdated --update --latest \"@std/*\" \"!@std/fmt*\" + +Specific version requirements to update to can be specified: + deno outdated --update @std/fmt@^1.0.2 +"), + UnstableArgsConfig::None, + ) + .defer(|cmd| { + cmd + .arg( + Arg::new("filters") + .num_args(0..) + .action(ArgAction::Append) + .help(concat!("Filters selecting which packages to act on. Can include wildcards (*) to match multiple packages. ", + "If a version requirement is specified, the matching packages will be updated to the given requirement."), + ) + ) + .arg(no_lock_arg()) + .arg(lock_arg()) + .arg( + Arg::new("latest") + .long("latest") + .action(ArgAction::SetTrue) + .help( + "Update to the latest version, regardless of semver constraints", + ) + .requires("update") + .conflicts_with("compatible"), + ) + .arg( + Arg::new("update") + .long("update") + .short('u') + .action(ArgAction::SetTrue) + .conflicts_with("compatible") + .help("Update dependency versions"), + ) + .arg( + Arg::new("compatible") + .long("compatible") + .action(ArgAction::SetTrue) + .help("Only output versions that satisfy semver requirements") + .conflicts_with("update"), + ) + .arg( + Arg::new("recursive") + .long("recursive") + .short('r') + .action(ArgAction::SetTrue) + .help("include all workspace members"), + ) + }) +} + fn uninstall_subcommand() -> Command { command( "uninstall", @@ -4353,6 +4447,31 @@ fn remove_parse(flags: &mut Flags, matches: &mut ArgMatches) { }); } +fn outdated_parse( + flags: &mut Flags, + matches: &mut ArgMatches, +) -> clap::error::Result<()> { + let filters = match matches.remove_many::("filters") { + Some(f) => f.collect(), + None => vec![], + }; + let recursive = matches.get_flag("recursive"); + let update = matches.get_flag("update"); + let kind = if update { + let latest = matches.get_flag("latest"); + OutdatedKind::Update { latest } + } else { + let compatible = matches.get_flag("compatible"); + OutdatedKind::PrintOutdated { compatible } + }; + flags.subcommand = DenoSubcommand::Outdated(OutdatedFlags { + filters, + recursive, + kind, + }); + Ok(()) +} + fn bench_parse( flags: &mut Flags, matches: &mut ArgMatches, @@ -11299,4 +11418,77 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n" assert!(r.is_err()); } } + + #[test] + fn outdated_subcommand() { + let cases = [ + ( + svec![], + OutdatedFlags { + filters: vec![], + kind: OutdatedKind::PrintOutdated { compatible: false }, + recursive: false, + }, + ), + ( + svec!["--recursive"], + OutdatedFlags { + filters: vec![], + kind: OutdatedKind::PrintOutdated { compatible: false }, + recursive: true, + }, + ), + ( + svec!["--recursive", "--compatible"], + OutdatedFlags { + filters: vec![], + kind: OutdatedKind::PrintOutdated { compatible: true }, + recursive: true, + }, + ), + ( + svec!["--update"], + OutdatedFlags { + filters: vec![], + kind: OutdatedKind::Update { latest: false }, + recursive: false, + }, + ), + ( + svec!["--update", "--latest"], + OutdatedFlags { + filters: vec![], + kind: OutdatedKind::Update { latest: true }, + recursive: false, + }, + ), + ( + svec!["--update", "--recursive"], + OutdatedFlags { + filters: vec![], + kind: OutdatedKind::Update { latest: false }, + recursive: true, + }, + ), + ( + svec!["--update", "@foo/bar"], + OutdatedFlags { + filters: svec!["@foo/bar"], + kind: OutdatedKind::Update { latest: false }, + recursive: false, + }, + ), + ]; + for (input, expected) in cases { + let mut args = svec!["deno", "outdated"]; + args.extend(input); + let r = flags_from_vec(args.clone()).unwrap(); + assert_eq!( + r.subcommand, + DenoSubcommand::Outdated(expected), + "incorrect result for args: {:?}", + args + ); + } + } } diff --git a/cli/args/mod.rs b/cli/args/mod.rs index bbdfa478c1..37e1f00ffa 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -1628,6 +1628,7 @@ impl CliOptions { DenoSubcommand::Install(_) | DenoSubcommand::Add(_) | DenoSubcommand::Remove(_) + | DenoSubcommand::Outdated(_) ) { // For `deno install/add/remove` we want to force the managed resolver so it can set up `node_modules/` directory. return false; diff --git a/cli/main.rs b/cli/main.rs index 3dd2692f05..017e343178 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -188,6 +188,11 @@ async fn run_subcommand(flags: Arc) -> Result { tools::lint::lint(flags, lint_flags).await } }), + DenoSubcommand::Outdated(update_flags) => { + spawn_subcommand(async move { + tools::registry::outdated(flags, update_flags).await + }) + } DenoSubcommand::Repl(repl_flags) => { spawn_subcommand(async move { tools::repl::run(flags, repl_flags).await }) } diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs index 2e64f5f188..88094d5141 100644 --- a/cli/npm/managed/mod.rs +++ b/cli/npm/managed/mod.rs @@ -500,7 +500,7 @@ impl ManagedCliNpmResolver { self.resolve_pkg_folder_from_pkg_id(&pkg_id) } - fn resolve_pkg_id_from_pkg_req( + pub fn resolve_pkg_id_from_pkg_req( &self, req: &PackageReq, ) -> Result { diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs index 89447f04f9..ba61d352d3 100644 --- a/cli/tools/registry/mod.rs +++ b/cli/tools/registry/mod.rs @@ -68,6 +68,7 @@ use auth::get_auth_method; use auth::AuthMethod; pub use pm::add; pub use pm::cache_top_level_deps; +pub use pm::outdated; pub use pm::remove; pub use pm::AddCommandName; pub use pm::AddRmPackageReq; diff --git a/cli/tools/registry/pm.rs b/cli/tools/registry/pm.rs index c1ea2c75ea..5718cd3ec1 100644 --- a/cli/tools/registry/pm.rs +++ b/cli/tools/registry/pm.rs @@ -16,6 +16,7 @@ use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use deno_semver::Version; use deno_semver::VersionReq; +use deps::KeyPath; use jsonc_parser::cst::CstObject; use jsonc_parser::cst::CstObjectProp; use jsonc_parser::cst::CstRootNode; @@ -32,10 +33,13 @@ use crate::jsr::JsrFetchResolver; use crate::npm::NpmFetchResolver; mod cache_deps; +pub(crate) mod deps; +mod outdated; pub use cache_deps::cache_top_level_deps; +pub use outdated::outdated; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Hash)] enum ConfigKind { DenoJson, PackageJson, @@ -86,6 +90,28 @@ impl ConfigUpdater { self.cst.to_string() } + fn get_property_for_mutation( + &mut self, + key_path: &KeyPath, + ) -> Option { + let mut current_node = self.root_object.clone(); + + self.modified = true; + + for (i, part) in key_path.parts.iter().enumerate() { + let s = part.as_str(); + if i < key_path.parts.len().saturating_sub(1) { + let object = current_node.object_value(s)?; + current_node = object; + } else { + // last part + return current_node.get(s); + } + } + + None + } + fn add(&mut self, selected: SelectedPackage, dev: bool) { fn insert_index(object: &CstObject, searching_name: &str) -> usize { object @@ -824,7 +850,7 @@ async fn npm_install_after_modification( flags: Arc, // explicitly provided to prevent redownloading jsr_resolver: Option>, -) -> Result<(), AnyError> { +) -> Result { // clear the previously cached package.json from memory before reloading it node_resolver::PackageJsonThreadLocalCache::clear(); @@ -842,7 +868,7 @@ async fn npm_install_after_modification( lockfile.write_if_changed()?; } - Ok(()) + Ok(cli_factory) } #[cfg(test)] diff --git a/cli/tools/registry/pm/cache_deps.rs b/cli/tools/registry/pm/cache_deps.rs index d3c8da868c..f9d67e4d4f 100644 --- a/cli/tools/registry/pm/cache_deps.rs +++ b/cli/tools/registry/pm/cache_deps.rs @@ -8,7 +8,7 @@ use crate::graph_container::ModuleGraphUpdatePermit; use deno_core::error::AnyError; use deno_core::futures::stream::FuturesUnordered; use deno_core::futures::StreamExt; -use deno_semver::package::PackageReq; +use deno_semver::jsr::JsrPackageReqReference; pub async fn cache_top_level_deps( // todo(dsherret): don't pass the factory into this function. Instead use ctor deps @@ -56,15 +56,20 @@ pub async fn cache_top_level_deps( match specifier.scheme() { "jsr" => { let specifier_str = specifier.as_str(); - let specifier_str = - specifier_str.strip_prefix("jsr:").unwrap_or(specifier_str); - if let Ok(req) = PackageReq::from_str(specifier_str) { - if !seen_reqs.insert(req.clone()) { + if let Ok(req) = JsrPackageReqReference::from_str(specifier_str) { + if let Some(sub_path) = req.sub_path() { + if sub_path.ends_with('/') { + continue; + } + roots.push(specifier.clone()); + continue; + } + if !seen_reqs.insert(req.req().clone()) { continue; } let jsr_resolver = jsr_resolver.clone(); info_futures.push(async move { - if let Some(nv) = jsr_resolver.req_to_nv(&req).await { + if let Some(nv) = jsr_resolver.req_to_nv(req.req()).await { if let Some(info) = jsr_resolver.package_version_info(&nv).await { return Some((specifier.clone(), info)); diff --git a/cli/tools/registry/pm/deps.rs b/cli/tools/registry/pm/deps.rs new file mode 100644 index 0000000000..4778d6f327 --- /dev/null +++ b/cli/tools/registry/pm/deps.rs @@ -0,0 +1,964 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use deno_ast::ModuleSpecifier; +use deno_config::deno_json::ConfigFile; +use deno_config::deno_json::ConfigFileRc; +use deno_config::workspace::Workspace; +use deno_config::workspace::WorkspaceDirectory; +use deno_core::anyhow::bail; +use deno_core::error::AnyError; +use deno_core::futures::future::try_join; +use deno_core::futures::stream::FuturesOrdered; +use deno_core::futures::stream::FuturesUnordered; +use deno_core::futures::FutureExt; +use deno_core::futures::StreamExt; +use deno_core::serde_json; +use deno_graph::FillFromLockfileOptions; +use deno_package_json::PackageJsonDepValue; +use deno_package_json::PackageJsonDepValueParseError; +use deno_package_json::PackageJsonRc; +use deno_runtime::deno_permissions::PermissionsContainer; +use deno_semver::jsr::JsrPackageReqReference; +use deno_semver::npm::NpmPackageReqReference; +use deno_semver::package::PackageNv; +use deno_semver::package::PackageReq; +use deno_semver::package::PackageReqReference; +use deno_semver::VersionReq; +use import_map::ImportMap; +use import_map::ImportMapWithDiagnostics; +use import_map::SpecifierMapEntry; +use indexmap::IndexMap; +use tokio::sync::Semaphore; + +use crate::args::CliLockfile; +use crate::graph_container::MainModuleGraphContainer; +use crate::graph_container::ModuleGraphContainer; +use crate::graph_container::ModuleGraphUpdatePermit; +use crate::jsr::JsrFetchResolver; +use crate::module_loader::ModuleLoadPreparer; +use crate::npm::CliNpmResolver; +use crate::npm::NpmFetchResolver; + +use super::ConfigUpdater; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ImportMapKind { + Inline, + Outline, +} + +#[derive(Clone)] +pub enum DepLocation { + DenoJson(ConfigFileRc, KeyPath, ImportMapKind), + PackageJson(PackageJsonRc, KeyPath), +} + +impl DepLocation { + pub fn is_deno_json(&self) -> bool { + matches!(self, DepLocation::DenoJson(..)) + } + + pub fn file_path(&self) -> Cow { + match self { + DepLocation::DenoJson(arc, _, _) => { + Cow::Owned(arc.specifier.to_file_path().unwrap()) + } + DepLocation::PackageJson(arc, _) => Cow::Borrowed(arc.path.as_ref()), + } + } + fn config_kind(&self) -> super::ConfigKind { + match self { + DepLocation::DenoJson(_, _, _) => super::ConfigKind::DenoJson, + DepLocation::PackageJson(_, _) => super::ConfigKind::PackageJson, + } + } +} + +struct DebugAdapter(T); + +impl<'a> std::fmt::Debug for DebugAdapter<&'a ConfigFileRc> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfigFile") + .field("specifier", &self.0.specifier) + .finish() + } +} +impl<'a> std::fmt::Debug for DebugAdapter<&'a PackageJsonRc> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PackageJson") + .field("path", &self.0.path) + .finish() + } +} + +impl std::fmt::Debug for DepLocation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DepLocation::DenoJson(arc, key_path, kind) => { + let mut debug = f.debug_tuple("DenoJson"); + debug + .field(&DebugAdapter(arc)) + .field(key_path) + .field(kind) + .finish() + } + DepLocation::PackageJson(arc, key_path) => { + let mut debug = f.debug_tuple("PackageJson"); + debug.field(&DebugAdapter(arc)).field(key_path).finish() + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum DepKind { + Jsr, + Npm, +} + +impl DepKind { + pub fn scheme(&self) -> &'static str { + match self { + DepKind::Npm => "npm", + DepKind::Jsr => "jsr", + } + } +} + +#[derive(Clone, Debug)] +pub enum KeyPart { + Imports, + Scopes, + Dependencies, + DevDependencies, + String(String), +} + +impl From for KeyPart { + fn from(value: String) -> Self { + KeyPart::String(value) + } +} + +impl From for KeyPart { + fn from(value: PackageJsonDepKind) -> Self { + match value { + PackageJsonDepKind::Normal => Self::Dependencies, + PackageJsonDepKind::Dev => Self::DevDependencies, + } + } +} + +impl KeyPart { + pub fn as_str(&self) -> &str { + match self { + KeyPart::Imports => "imports", + KeyPart::Scopes => "scopes", + KeyPart::Dependencies => "dependencies", + KeyPart::DevDependencies => "devDependencies", + KeyPart::String(s) => s, + } + } +} + +#[derive(Clone, Debug)] +pub struct KeyPath { + pub parts: Vec, +} + +impl KeyPath { + fn from_parts(parts: impl IntoIterator) -> Self { + Self { + parts: parts.into_iter().collect(), + } + } + fn last(&self) -> Option<&KeyPart> { + self.parts.last() + } + fn push(&mut self, part: KeyPart) { + self.parts.push(part) + } +} + +#[derive(Clone, Debug)] +pub struct Dep { + pub req: PackageReq, + pub kind: DepKind, + pub location: DepLocation, + #[allow(dead_code)] + pub id: DepId, + #[allow(dead_code)] + pub alias: Option, +} + +fn import_map_entries( + import_map: &ImportMap, +) -> impl Iterator)> { + import_map + .imports() + .entries() + .map(|entry| { + ( + KeyPath::from_parts([ + KeyPart::Imports, + KeyPart::String(entry.raw_key.into()), + ]), + entry, + ) + }) + .chain(import_map.scopes().flat_map(|scope| { + let path = KeyPath::from_parts([ + KeyPart::Scopes, + scope.raw_key.to_string().into(), + ]); + + scope.imports.entries().map(move |entry| { + let mut full_path = path.clone(); + full_path.push(KeyPart::String(entry.raw_key.to_string())); + (full_path, entry) + }) + })) +} + +fn to_import_map_value_from_imports( + deno_json: &ConfigFile, +) -> serde_json::Value { + let mut value = serde_json::Map::with_capacity(2); + if let Some(imports) = &deno_json.json.imports { + value.insert("imports".to_string(), imports.clone()); + } + if let Some(scopes) = &deno_json.json.scopes { + value.insert("scopes".to_string(), scopes.clone()); + } + serde_json::Value::Object(value) +} + +fn deno_json_import_map( + deno_json: &ConfigFile, +) -> Result, AnyError> { + let (value, kind) = + if deno_json.json.imports.is_some() || deno_json.json.scopes.is_some() { + ( + to_import_map_value_from_imports(deno_json), + ImportMapKind::Inline, + ) + } else { + match deno_json.to_import_map_path()? { + Some(path) => { + let text = std::fs::read_to_string(&path)?; + let value = serde_json::from_str(&text)?; + (value, ImportMapKind::Outline) + } + None => return Ok(None), + } + }; + + import_map::parse_from_value(deno_json.specifier.clone(), value) + .map_err(Into::into) + .map(|import_map| Some((import_map, kind))) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PackageJsonDepKind { + Normal, + Dev, +} + +type PackageJsonDeps = IndexMap< + String, + Result< + (PackageJsonDepKind, PackageJsonDepValue), + PackageJsonDepValueParseError, + >, +>; + +/// Resolve the package.json's dependencies. +// TODO(nathanwhit): Remove once we update deno_package_json with dev deps split out +fn resolve_local_package_json_deps( + package_json: &PackageJsonRc, +) -> PackageJsonDeps { + /// Gets the name and raw version constraint for a registry info or + /// package.json dependency entry taking into account npm package aliases. + fn parse_dep_entry_name_and_raw_version<'a>( + key: &'a str, + value: &'a str, + ) -> (&'a str, &'a str) { + if let Some(package_and_version) = value.strip_prefix("npm:") { + if let Some((name, version)) = package_and_version.rsplit_once('@') { + // if empty, then the name was scoped and there's no version + if name.is_empty() { + (package_and_version, "*") + } else { + (name, version) + } + } else { + (package_and_version, "*") + } + } else { + (key, value) + } + } + + fn parse_entry( + key: &str, + value: &str, + ) -> Result { + if let Some(workspace_key) = value.strip_prefix("workspace:") { + let version_req = VersionReq::parse_from_npm(workspace_key)?; + return Ok(PackageJsonDepValue::Workspace(version_req)); + } + if value.starts_with("file:") + || value.starts_with("git:") + || value.starts_with("http:") + || value.starts_with("https:") + { + return Err(PackageJsonDepValueParseError::Unsupported { + scheme: value.split(':').next().unwrap().to_string(), + }); + } + let (name, version_req) = parse_dep_entry_name_and_raw_version(key, value); + let result = VersionReq::parse_from_npm(version_req); + match result { + Ok(version_req) => Ok(PackageJsonDepValue::Req(PackageReq { + name: name.to_string(), + version_req, + })), + Err(err) => Err(PackageJsonDepValueParseError::VersionReq(err)), + } + } + + fn insert_deps( + deps: Option<&IndexMap>, + result: &mut PackageJsonDeps, + kind: PackageJsonDepKind, + ) { + if let Some(deps) = deps { + for (key, value) in deps { + result.entry(key.to_string()).or_insert_with(|| { + parse_entry(key, value).map(|entry| (kind, entry)) + }); + } + } + } + + let deps = package_json.dependencies.as_ref(); + let dev_deps = package_json.dev_dependencies.as_ref(); + let mut result = IndexMap::new(); + + // favors the deps over dev_deps + insert_deps(deps, &mut result, PackageJsonDepKind::Normal); + insert_deps(dev_deps, &mut result, PackageJsonDepKind::Dev); + + result +} + +fn add_deps_from_deno_json( + deno_json: &Arc, + mut filter: impl DepFilter, + deps: &mut Vec, +) { + let (import_map, import_map_kind) = match deno_json_import_map(deno_json) { + Ok(Some((import_map, import_map_kind))) => (import_map, import_map_kind), + Ok(None) => return, + Err(e) => { + log::warn!("failed to parse imports from {}: {e}", &deno_json.specifier); + return; + } + }; + for (key_path, entry) in import_map_entries(&import_map.import_map) { + let Some(value) = entry.value else { continue }; + let kind = match value.scheme() { + "npm" => DepKind::Npm, + "jsr" => DepKind::Jsr, + _ => continue, + }; + let req = match parse_req_reference(value.as_str(), kind) { + Ok(req) => req.req.clone(), + Err(err) => { + log::warn!("failed to parse package req \"{}\": {err}", value.as_str()); + continue; + } + }; + let alias: &str = key_path.last().unwrap().as_str().trim_end_matches('/'); + let alias = (alias != req.name).then(|| alias.to_string()); + if !filter.should_include(alias.as_deref(), &req, kind) { + continue; + } + let id = DepId(deps.len()); + deps.push(Dep { + location: DepLocation::DenoJson( + deno_json.clone(), + key_path, + import_map_kind, + ), + kind, + req, + id, + alias, + }); + } +} + +fn add_deps_from_package_json( + package_json: &PackageJsonRc, + mut filter: impl DepFilter, + deps: &mut Vec, +) { + let package_json_deps = resolve_local_package_json_deps(package_json); + for (k, v) in package_json_deps { + let (package_dep_kind, v) = match v { + Ok((k, v)) => (k, v), + Err(e) => { + log::warn!("bad package json dep value: {e}"); + continue; + } + }; + match v { + deno_package_json::PackageJsonDepValue::Req(req) => { + let alias = k.as_str(); + let alias = (alias != req.name).then(|| alias.to_string()); + if !filter.should_include(alias.as_deref(), &req, DepKind::Npm) { + continue; + } + let id = DepId(deps.len()); + deps.push(Dep { + id, + kind: DepKind::Npm, + location: DepLocation::PackageJson( + package_json.clone(), + KeyPath::from_parts([package_dep_kind.into(), k.into()]), + ), + req, + alias, + }) + } + deno_package_json::PackageJsonDepValue::Workspace(_) => continue, + } + } +} + +fn deps_from_workspace( + workspace: &Arc, + dep_filter: impl DepFilter, +) -> Result, AnyError> { + let mut deps = Vec::with_capacity(256); + for deno_json in workspace.deno_jsons() { + add_deps_from_deno_json(deno_json, dep_filter, &mut deps); + } + for package_json in workspace.package_jsons() { + add_deps_from_package_json(package_json, dep_filter, &mut deps); + } + + Ok(deps) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct DepId(usize); + +#[derive(Debug, Clone)] +pub enum Change { + Update(DepId, VersionReq), +} + +pub trait DepFilter: Copy { + fn should_include( + &mut self, + alias: Option<&str>, + package_req: &PackageReq, + dep_kind: DepKind, + ) -> bool; +} + +impl DepFilter for T +where + T: FnMut(Option<&str>, &PackageReq, DepKind) -> bool + Copy, +{ + fn should_include<'a>( + &mut self, + alias: Option<&'a str>, + package_req: &'a PackageReq, + dep_kind: DepKind, + ) -> bool { + (*self)(alias, package_req, dep_kind) + } +} + +#[derive(Clone, Debug)] +pub struct PackageLatestVersion { + pub semver_compatible: Option, + pub latest: Option, +} + +pub struct DepManager { + deps: Vec, + resolved_versions: Vec>, + latest_versions: Vec, + + pending_changes: Vec, + + dependencies_resolved: AtomicBool, + module_load_preparer: Arc, + // TODO(nathanwhit): probably shouldn't be pub + pub(crate) jsr_fetch_resolver: Arc, + pub(crate) npm_fetch_resolver: Arc, + npm_resolver: Arc, + permissions_container: PermissionsContainer, + main_module_graph_container: Arc, + lockfile: Option>, +} + +pub struct DepManagerArgs { + pub module_load_preparer: Arc, + pub jsr_fetch_resolver: Arc, + pub npm_fetch_resolver: Arc, + pub npm_resolver: Arc, + pub permissions_container: PermissionsContainer, + pub main_module_graph_container: Arc, + pub lockfile: Option>, +} + +impl DepManager { + pub fn reloaded_after_modification(self, args: DepManagerArgs) -> Self { + let mut new = Self::with_deps_args(self.deps, args); + new.latest_versions = self.latest_versions; + new + } + fn with_deps_args(deps: Vec, args: DepManagerArgs) -> Self { + let DepManagerArgs { + module_load_preparer, + jsr_fetch_resolver, + npm_fetch_resolver, + npm_resolver, + permissions_container, + main_module_graph_container, + lockfile, + } = args; + Self { + deps, + resolved_versions: Vec::new(), + latest_versions: Vec::new(), + jsr_fetch_resolver, + dependencies_resolved: AtomicBool::new(false), + module_load_preparer, + npm_fetch_resolver, + npm_resolver, + permissions_container, + main_module_graph_container, + lockfile, + pending_changes: Vec::new(), + } + } + pub fn from_workspace_dir( + workspace_dir: &Arc, + dep_filter: impl DepFilter, + args: DepManagerArgs, + ) -> Result { + let mut deps = Vec::with_capacity(256); + if let Some(deno_json) = workspace_dir.maybe_deno_json() { + if deno_json.specifier.scheme() != "file" { + bail!("remote deno.json files are not supported"); + } + let path = deno_json.specifier.to_file_path().unwrap(); + if path.parent().unwrap() == workspace_dir.dir_path() { + add_deps_from_deno_json(deno_json, dep_filter, &mut deps); + } + } + if let Some(package_json) = workspace_dir.maybe_pkg_json() { + add_deps_from_package_json(package_json, dep_filter, &mut deps); + } + + Ok(Self::with_deps_args(deps, args)) + } + pub fn from_workspace( + workspace: &Arc, + dep_filter: impl DepFilter, + args: DepManagerArgs, + ) -> Result { + let deps = deps_from_workspace(workspace, dep_filter)?; + Ok(Self::with_deps_args(deps, args)) + } + + async fn run_dependency_resolution(&self) -> Result<(), AnyError> { + if self + .dependencies_resolved + .load(std::sync::atomic::Ordering::Relaxed) + { + return Ok(()); + } + + let mut graph_permit = self + .main_module_graph_container + .acquire_update_permit() + .await; + let graph = graph_permit.graph_mut(); + // populate the information from the lockfile + if let Some(lockfile) = &self.lockfile { + let lockfile = lockfile.lock(); + graph.fill_from_lockfile(FillFromLockfileOptions { + redirects: lockfile + .content + .redirects + .iter() + .map(|(from, to)| (from.as_str(), to.as_str())), + package_specifiers: lockfile + .content + .packages + .specifiers + .iter() + .map(|(dep, id)| (dep, id.as_str())), + }); + } + + let npm_resolver = self.npm_resolver.as_managed().unwrap(); + if self.deps.iter().all(|dep| match dep.kind { + DepKind::Npm => { + npm_resolver.resolve_pkg_id_from_pkg_req(&dep.req).is_ok() + } + DepKind::Jsr => graph.packages.mappings().contains_key(&dep.req), + }) { + self + .dependencies_resolved + .store(true, std::sync::atomic::Ordering::Relaxed); + graph_permit.commit(); + return Ok(()); + } + + npm_resolver.ensure_top_level_package_json_install().await?; + let mut roots = Vec::new(); + let mut info_futures = FuturesUnordered::new(); + for dep in &self.deps { + if dep.location.is_deno_json() { + match dep.kind { + DepKind::Npm => roots.push( + ModuleSpecifier::parse(&format!("npm:/{}/", dep.req)).unwrap(), + ), + DepKind::Jsr => info_futures.push(async { + if let Some(nv) = self.jsr_fetch_resolver.req_to_nv(&dep.req).await + { + if let Some(info) = + self.jsr_fetch_resolver.package_version_info(&nv).await + { + let specifier = + ModuleSpecifier::parse(&format!("jsr:/{}/", dep.req)) + .unwrap(); + return Some((specifier, info)); + } + } + None + }), + } + } + } + + while let Some(info_future) = info_futures.next().await { + if let Some((specifier, info)) = info_future { + let exports = info.exports(); + for (k, _) in exports { + if let Ok(spec) = specifier.join(k) { + roots.push(spec); + } + } + } + } + + self + .module_load_preparer + .prepare_module_load( + graph, + &roots, + false, + deno_config::deno_json::TsTypeLib::DenoWindow, + self.permissions_container.clone(), + None, + ) + .await?; + + graph_permit.commit(); + + Ok(()) + } + + pub fn resolved_version(&self, id: DepId) -> Option<&PackageNv> { + self.resolved_versions[id.0].as_ref() + } + + pub async fn resolve_current_versions(&mut self) -> Result<(), AnyError> { + self.run_dependency_resolution().await?; + + let graph = self.main_module_graph_container.graph(); + + let mut resolved = Vec::with_capacity(self.deps.len()); + let snapshot = self.npm_resolver.as_managed().unwrap().snapshot(); + let resolved_npm = snapshot.package_reqs(); + let resolved_jsr = graph.packages.mappings(); + for dep in &self.deps { + match dep.kind { + DepKind::Npm => { + let resolved_version = resolved_npm.get(&dep.req).cloned(); + resolved.push(resolved_version); + } + DepKind::Jsr => { + let resolved_version = resolved_jsr.get(&dep.req).cloned(); + resolved.push(resolved_version) + } + } + } + + self.resolved_versions = resolved; + + Ok(()) + } + + async fn load_latest_versions( + &self, + ) -> Result, AnyError> { + if self.latest_versions.len() == self.deps.len() { + return Ok(self.latest_versions.clone()); + } + let latest_tag_req = deno_semver::VersionReq::from_raw_text_and_inner( + "latest".into(), + deno_semver::RangeSetOrTag::Tag("latest".into()), + ); + let mut latest_versions = Vec::with_capacity(self.deps.len()); + + let npm_sema = Semaphore::new(32); + let jsr_sema = Semaphore::new(32); + let mut futs = FuturesOrdered::new(); + + for dep in &self.deps { + match dep.kind { + DepKind::Npm => futs.push_back( + async { + let semver_req = &dep.req; + let latest_req = PackageReq { + name: dep.req.name.clone(), + version_req: latest_tag_req.clone(), + }; + let _permit = npm_sema.acquire().await; + let semver_compatible = + self.npm_fetch_resolver.req_to_nv(semver_req).await; + let latest = self.npm_fetch_resolver.req_to_nv(&latest_req).await; + PackageLatestVersion { + latest, + semver_compatible, + } + } + .boxed_local(), + ), + DepKind::Jsr => futs.push_back( + async { + let semver_req = &dep.req; + let latest_req = PackageReq { + name: dep.req.name.clone(), + version_req: deno_semver::WILDCARD_VERSION_REQ.clone(), + }; + let _permit = jsr_sema.acquire().await; + let semver_compatible = + self.jsr_fetch_resolver.req_to_nv(semver_req).await; + let latest = self.jsr_fetch_resolver.req_to_nv(&latest_req).await; + PackageLatestVersion { + latest, + semver_compatible, + } + } + .boxed_local(), + ), + } + } + while let Some(nv) = futs.next().await { + latest_versions.push(nv); + } + + Ok(latest_versions) + } + + pub async fn resolve_versions(&mut self) -> Result<(), AnyError> { + let (_, latest_versions) = try_join( + self.run_dependency_resolution(), + self.load_latest_versions(), + ) + .await?; + + self.latest_versions = latest_versions; + + self.resolve_current_versions().await?; + + Ok(()) + } + + pub fn deps_with_resolved_latest_versions( + &self, + ) -> impl IntoIterator, PackageLatestVersion)> + '_ + { + self + .resolved_versions + .iter() + .zip(self.latest_versions.iter()) + .enumerate() + .map(|(i, (resolved, latest))| { + (DepId(i), resolved.clone(), latest.clone()) + }) + } + + pub fn get_dep(&self, id: DepId) -> &Dep { + &self.deps[id.0] + } + + pub fn update_dep(&mut self, dep_id: DepId, new_version_req: VersionReq) { + self + .pending_changes + .push(Change::Update(dep_id, new_version_req)); + } + + pub fn commit_changes(&mut self) -> Result<(), AnyError> { + let changes = std::mem::take(&mut self.pending_changes); + let mut config_updaters = HashMap::new(); + for change in changes { + match change { + Change::Update(dep_id, version_req) => { + // TODO: move most of this to ConfigUpdater + let dep = &mut self.deps[dep_id.0]; + dep.req.version_req = version_req.clone(); + match &dep.location { + DepLocation::DenoJson(arc, key_path, import_map_kind) => { + if matches!(import_map_kind, ImportMapKind::Outline) { + // not supported + continue; + } + let updater = + get_or_create_updater(&mut config_updaters, &dep.location)?; + + let Some(property) = updater.get_property_for_mutation(key_path) + else { + log::warn!( + "failed to find property at path {key_path:?} for file {}", + arc.specifier + ); + continue; + }; + let Some(string_value) = cst_string_literal(&property) else { + continue; + }; + let mut req_reference = match dep.kind { + DepKind::Npm => NpmPackageReqReference::from_str(&string_value) + .unwrap() + .into_inner(), + DepKind::Jsr => JsrPackageReqReference::from_str(&string_value) + .unwrap() + .into_inner(), + }; + req_reference.req.version_req = version_req; + let mut new_value = + format!("{}:{}", dep.kind.scheme(), req_reference); + if string_value.ends_with('/') && !new_value.ends_with('/') { + // the display impl for PackageReqReference maps `/` to the root + // subpath, but for the import map the trailing `/` is significant + new_value.push('/'); + } + if string_value + .trim_start_matches(format!("{}:", dep.kind.scheme()).as_str()) + .starts_with('/') + { + // this is gross + new_value = new_value.replace(':', ":/"); + } + property + .set_value(jsonc_parser::cst::CstInputValue::String(new_value)); + } + DepLocation::PackageJson(arc, key_path) => { + let updater = + get_or_create_updater(&mut config_updaters, &dep.location)?; + let Some(property) = updater.get_property_for_mutation(key_path) + else { + log::warn!( + "failed to find property at path {key_path:?} for file {}", + arc.path.display() + ); + continue; + }; + let Some(string_value) = cst_string_literal(&property) else { + continue; + }; + let new_value = if string_value.starts_with("npm:") { + // aliased + let rest = string_value.trim_start_matches("npm:"); + let mut parts = rest.split('@'); + let first = parts.next().unwrap(); + if first.is_empty() { + let scope_and_name = parts.next().unwrap(); + format!("npm:@{scope_and_name}@{version_req}") + } else { + format!("npm:{first}@{version_req}") + } + } else if string_value.contains(":") { + bail!("Unexpected package json dependency string: \"{string_value}\" in {}", arc.path.display()); + } else { + version_req.to_string() + }; + property + .set_value(jsonc_parser::cst::CstInputValue::String(new_value)); + } + } + } + } + } + + for (_, updater) in config_updaters { + updater.commit()?; + } + + Ok(()) + } +} + +fn get_or_create_updater<'a>( + config_updaters: &'a mut HashMap, + location: &DepLocation, +) -> Result<&'a mut ConfigUpdater, AnyError> { + match config_updaters.entry(location.file_path().into_owned()) { + std::collections::hash_map::Entry::Occupied(occupied_entry) => { + Ok(occupied_entry.into_mut()) + } + std::collections::hash_map::Entry::Vacant(vacant_entry) => { + let updater = ConfigUpdater::new( + location.config_kind(), + location.file_path().into_owned(), + )?; + Ok(vacant_entry.insert(updater)) + } + } +} + +fn cst_string_literal( + property: &jsonc_parser::cst::CstObjectProp, +) -> Option { + // TODO(nathanwhit): ensure this unwrap is safe + let value = property.value().unwrap(); + let Some(string) = value.as_string_lit() else { + log::warn!("malformed entry"); + return None; + }; + let Ok(string_value) = string.decoded_value() else { + log::warn!("malformed string: {string:?}"); + return None; + }; + Some(string_value) +} + +fn parse_req_reference( + input: &str, + kind: DepKind, +) -> Result< + PackageReqReference, + deno_semver::package::PackageReqReferenceParseError, +> { + Ok(match kind { + DepKind::Npm => NpmPackageReqReference::from_str(input)?.into_inner(), + DepKind::Jsr => JsrPackageReqReference::from_str(input)?.into_inner(), + }) +} diff --git a/cli/tools/registry/pm/outdated.rs b/cli/tools/registry/pm/outdated.rs new file mode 100644 index 0000000000..2a29014267 --- /dev/null +++ b/cli/tools/registry/pm/outdated.rs @@ -0,0 +1,661 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashSet; +use std::sync::Arc; + +use deno_core::error::AnyError; +use deno_semver::package::PackageNv; +use deno_semver::package::PackageReq; +use deno_semver::VersionReq; +use deno_terminal::colors; + +use crate::args::CacheSetting; +use crate::args::CliOptions; +use crate::args::Flags; +use crate::args::OutdatedFlags; +use crate::factory::CliFactory; +use crate::file_fetcher::FileFetcher; +use crate::jsr::JsrFetchResolver; +use crate::npm::NpmFetchResolver; +use crate::tools::registry::pm::deps::DepKind; + +use super::deps::Dep; +use super::deps::DepManager; +use super::deps::DepManagerArgs; +use super::deps::PackageLatestVersion; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +struct OutdatedPackage { + kind: DepKind, + latest: String, + semver_compatible: String, + current: String, + name: String, +} + +#[allow(clippy::print_stdout)] +fn print_outdated_table(packages: &[OutdatedPackage]) { + const HEADINGS: &[&str] = &["Package", "Current", "Update", "Latest"]; + + let mut longest_package = 0; + let mut longest_current = 0; + let mut longest_update = 0; + let mut longest_latest = 0; + + for package in packages { + let name_len = package.kind.scheme().len() + 1 + package.name.len(); + longest_package = longest_package.max(name_len); + longest_current = longest_current.max(package.current.len()); + longest_update = longest_update.max(package.semver_compatible.len()); + longest_latest = longest_latest.max(package.latest.len()); + } + + let package_column_width = longest_package.max(HEADINGS[0].len()) + 2; + let current_column_width = longest_current.max(HEADINGS[1].len()) + 2; + let update_column_width = longest_update.max(HEADINGS[2].len()) + 2; + let latest_column_width = longest_latest.max(HEADINGS[3].len()) + 2; + + let package_fill = "─".repeat(package_column_width); + let current_fill = "─".repeat(current_column_width); + let update_fill = "─".repeat(update_column_width); + let latest_fill = "─".repeat(latest_column_width); + + println!("┌{package_fill}┬{current_fill}┬{update_fill}┬{latest_fill}┐"); + println!( + "│ {}{} │ {}{} │ {}{} │ {}{} │", + colors::intense_blue(HEADINGS[0]), + " ".repeat(package_column_width - 2 - HEADINGS[0].len()), + colors::intense_blue(HEADINGS[1]), + " ".repeat(current_column_width - 2 - HEADINGS[1].len()), + colors::intense_blue(HEADINGS[2]), + " ".repeat(update_column_width - 2 - HEADINGS[2].len()), + colors::intense_blue(HEADINGS[3]), + " ".repeat(latest_column_width - 2 - HEADINGS[3].len()) + ); + for package in packages { + println!("├{package_fill}┼{current_fill}┼{update_fill}┼{latest_fill}┤",); + + print!( + "│ {: Result<(), AnyError> { + let mut outdated = Vec::new(); + let mut seen = std::collections::BTreeSet::new(); + for (dep_id, resolved, latest_versions) in + deps.deps_with_resolved_latest_versions() + { + let dep = deps.get_dep(dep_id); + + let Some(resolved) = resolved else { continue }; + + let latest = { + let preferred = if compatible { + &latest_versions.semver_compatible + } else { + &latest_versions.latest + }; + if let Some(v) = preferred { + v + } else { + continue; + } + }; + + if latest > &resolved + && seen.insert((dep.kind, dep.req.name.clone(), resolved.version.clone())) + { + outdated.push(OutdatedPackage { + kind: dep.kind, + name: dep.req.name.clone(), + current: resolved.version.to_string(), + latest: latest_versions + .latest + .map(|l| l.version.to_string()) + .unwrap_or_default(), + semver_compatible: latest_versions + .semver_compatible + .map(|l| l.version.to_string()) + .unwrap_or_default(), + }) + } + } + + if !outdated.is_empty() { + outdated.sort(); + print_outdated_table(&outdated); + } + + Ok(()) +} + +pub async fn outdated( + flags: Arc, + update_flags: OutdatedFlags, +) -> Result<(), AnyError> { + let factory = CliFactory::from_flags(flags.clone()); + let cli_options = factory.cli_options()?; + let workspace = cli_options.workspace(); + let http_client = factory.http_client_provider(); + let deps_http_cache = factory.global_http_cache()?; + let mut file_fetcher = FileFetcher::new( + deps_http_cache.clone(), + CacheSetting::RespectHeaders, + true, + http_client.clone(), + Default::default(), + None, + ); + file_fetcher.set_download_log_level(log::Level::Trace); + let file_fetcher = Arc::new(file_fetcher); + let npm_fetch_resolver = Arc::new(NpmFetchResolver::new( + file_fetcher.clone(), + cli_options.npmrc().clone(), + )); + let jsr_fetch_resolver = + Arc::new(JsrFetchResolver::new(file_fetcher.clone())); + + let args = dep_manager_args( + &factory, + cli_options, + npm_fetch_resolver.clone(), + jsr_fetch_resolver.clone(), + ) + .await?; + + let filter_set = filter::FilterSet::from_filter_strings( + update_flags.filters.iter().map(|s| s.as_str()), + )?; + + let filter_fn = |alias: Option<&str>, req: &PackageReq, _: DepKind| { + if filter_set.is_empty() { + return true; + } + let name = alias.unwrap_or(&req.name); + filter_set.matches(name) + }; + let mut deps = if update_flags.recursive { + super::deps::DepManager::from_workspace(workspace, filter_fn, args)? + } else { + super::deps::DepManager::from_workspace_dir( + &cli_options.start_dir, + filter_fn, + args, + )? + }; + + deps.resolve_versions().await?; + + match update_flags.kind { + crate::args::OutdatedKind::Update { latest } => { + update(deps, latest, &filter_set, flags).await?; + } + crate::args::OutdatedKind::PrintOutdated { compatible } => { + print_outdated(&mut deps, compatible)?; + } + } + + Ok(()) +} + +fn choose_new_version_req( + dep: &Dep, + resolved: Option<&PackageNv>, + latest_versions: &PackageLatestVersion, + update_to_latest: bool, + filter_set: &filter::FilterSet, +) -> Option { + let explicit_version_req = filter_set + .matching_filter(dep.alias.as_deref().unwrap_or(&dep.req.name)) + .version_spec() + .cloned(); + + if let Some(version_req) = explicit_version_req { + if let Some(resolved) = resolved { + // todo(nathanwhit): handle tag + if version_req.tag().is_none() && version_req.matches(&resolved.version) { + return None; + } + } + Some(version_req) + } else { + let preferred = if update_to_latest { + latest_versions.latest.as_ref()? + } else { + latest_versions.semver_compatible.as_ref()? + }; + if preferred.version <= resolved?.version { + return None; + } + Some( + VersionReq::parse_from_specifier( + format!("^{}", preferred.version).as_str(), + ) + .unwrap(), + ) + } +} + +async fn update( + mut deps: DepManager, + update_to_latest: bool, + filter_set: &filter::FilterSet, + flags: Arc, +) -> Result<(), AnyError> { + let mut updated = Vec::new(); + + for (dep_id, resolved, latest_versions) in deps + .deps_with_resolved_latest_versions() + .into_iter() + .collect::>() + { + let dep = deps.get_dep(dep_id); + let new_version_req = choose_new_version_req( + dep, + resolved.as_ref(), + &latest_versions, + update_to_latest, + filter_set, + ); + let Some(new_version_req) = new_version_req else { + continue; + }; + + updated.push(( + dep_id, + format!("{}:{}", dep.kind.scheme(), dep.req.name), + deps.resolved_version(dep.id).cloned(), + new_version_req.clone(), + )); + + deps.update_dep(dep_id, new_version_req); + } + + deps.commit_changes()?; + + if !updated.is_empty() { + let factory = super::npm_install_after_modification( + flags.clone(), + Some(deps.jsr_fetch_resolver.clone()), + ) + .await?; + + let mut updated_to_versions = HashSet::new(); + let cli_options = factory.cli_options()?; + let args = dep_manager_args( + &factory, + cli_options, + deps.npm_fetch_resolver.clone(), + deps.jsr_fetch_resolver.clone(), + ) + .await?; + + let mut deps = deps.reloaded_after_modification(args); + deps.resolve_current_versions().await?; + for (dep_id, package_name, maybe_current_version, new_version_req) in + updated + { + if let Some(nv) = deps.resolved_version(dep_id) { + updated_to_versions.insert(( + package_name, + maybe_current_version, + nv.version.clone(), + )); + } else { + log::warn!( + "Failed to resolve version for new version requirement: {} -> {}", + package_name, + new_version_req + ); + } + } + + log::info!( + "Updated {} dependenc{}:", + updated_to_versions.len(), + if updated_to_versions.len() == 1 { + "y" + } else { + "ies" + } + ); + let mut updated_to_versions = + updated_to_versions.into_iter().collect::>(); + updated_to_versions.sort_by(|(k, _, _), (k2, _, _)| k.cmp(k2)); + let max_name = updated_to_versions + .iter() + .map(|(name, _, _)| name.len()) + .max() + .unwrap_or(0); + let max_old = updated_to_versions + .iter() + .map(|(_, maybe_current, _)| { + maybe_current + .as_ref() + .map(|v| v.version.to_string().len()) + .unwrap_or(0) + }) + .max() + .unwrap_or(0); + let max_new = updated_to_versions + .iter() + .map(|(_, _, new_version)| new_version.to_string().len()) + .max() + .unwrap_or(0); + + for (package_name, maybe_current_version, new_version) in + updated_to_versions + { + let current_version = if let Some(current_version) = maybe_current_version + { + current_version.version.to_string() + } else { + "".to_string() + }; + + log::info!( + " - {}{} {}{} -> {}{}", + format!( + "{}{}", + colors::gray(package_name[0..4].to_string()), + package_name[4..].to_string() + ), + " ".repeat(max_name - package_name.len()), + " ".repeat(max_old - current_version.len()), + colors::gray(¤t_version), + " ".repeat(max_new - new_version.to_string().len()), + colors::green(&new_version), + ); + } + } else { + log::info!( + "All {}dependencies are up to date.", + if filter_set.is_empty() { + "" + } else { + "matching " + } + ); + } + + Ok(()) +} + +async fn dep_manager_args( + factory: &CliFactory, + cli_options: &CliOptions, + npm_fetch_resolver: Arc, + jsr_fetch_resolver: Arc, +) -> Result { + Ok(DepManagerArgs { + module_load_preparer: factory.module_load_preparer().await?.clone(), + jsr_fetch_resolver, + npm_fetch_resolver, + npm_resolver: factory.npm_resolver().await?.clone(), + permissions_container: factory.root_permissions_container()?.clone(), + main_module_graph_container: factory + .main_module_graph_container() + .await? + .clone(), + lockfile: cli_options.maybe_lockfile().cloned(), + }) +} + +mod filter { + use deno_core::anyhow::anyhow; + use deno_core::anyhow::Context; + use deno_core::error::AnyError; + use deno_semver::VersionReq; + + enum FilterKind { + Exclude, + Include, + } + pub struct Filter { + kind: FilterKind, + regex: regex::Regex, + version_spec: Option, + } + + fn pattern_to_regex(pattern: &str) -> Result { + let escaped = regex::escape(pattern); + let unescaped_star = escaped.replace(r"\*", ".*"); + Ok(regex::Regex::new(&format!("^{}$", unescaped_star))?) + } + + impl Filter { + pub fn version_spec(&self) -> Option<&VersionReq> { + self.version_spec.as_ref() + } + pub fn from_str(input: &str) -> Result { + let (kind, first_idx) = if input.starts_with('!') { + (FilterKind::Exclude, 1) + } else { + (FilterKind::Include, 0) + }; + let s = &input[first_idx..]; + let (pattern, version_spec) = + if let Some(scope_name) = s.strip_prefix('@') { + if let Some(idx) = scope_name.find('@') { + let (pattern, version_spec) = s.split_at(idx + 1); + ( + pattern, + Some( + VersionReq::parse_from_specifier( + version_spec.trim_start_matches('@'), + ) + .with_context(|| format!("Invalid filter \"{input}\""))?, + ), + ) + } else { + (s, None) + } + } else { + let mut parts = s.split('@'); + let Some(pattern) = parts.next() else { + return Err(anyhow!("Invalid filter \"{input}\"")); + }; + ( + pattern, + parts + .next() + .map(VersionReq::parse_from_specifier) + .transpose() + .with_context(|| format!("Invalid filter \"{input}\""))?, + ) + }; + + Ok(Filter { + kind, + regex: pattern_to_regex(pattern) + .with_context(|| format!("Invalid filter \"{input}\""))?, + version_spec, + }) + } + + pub fn matches(&self, name: &str) -> bool { + self.regex.is_match(name) + } + } + + pub struct FilterSet { + filters: Vec, + has_exclude: bool, + has_include: bool, + } + impl FilterSet { + pub fn from_filter_strings<'a>( + filter_strings: impl IntoIterator, + ) -> Result { + let filters = filter_strings + .into_iter() + .map(Filter::from_str) + .collect::, _>>()?; + let has_exclude = filters + .iter() + .any(|f| matches!(f.kind, FilterKind::Exclude)); + let has_include = filters + .iter() + .any(|f| matches!(f.kind, FilterKind::Include)); + Ok(FilterSet { + filters, + has_exclude, + has_include, + }) + } + + pub fn is_empty(&self) -> bool { + self.filters.is_empty() + } + + pub fn matches(&self, name: &str) -> bool { + self.matching_filter(name).is_included() + } + + pub fn matching_filter(&self, name: &str) -> MatchResult<'_> { + if self.filters.is_empty() { + return MatchResult::Included; + } + let mut matched = None; + for filter in &self.filters { + match filter.kind { + FilterKind::Include => { + if matched.is_none() && filter.matches(name) { + matched = Some(filter); + } + } + FilterKind::Exclude => { + if filter.matches(name) { + return MatchResult::Excluded; + } + } + } + } + if let Some(filter) = matched { + MatchResult::Matches(filter) + } else if self.has_exclude && !self.has_include { + MatchResult::Included + } else { + MatchResult::Excluded + } + } + } + + pub enum MatchResult<'a> { + Matches(&'a Filter), + Included, + Excluded, + } + + impl MatchResult<'_> { + pub fn version_spec(&self) -> Option<&VersionReq> { + match self { + MatchResult::Matches(filter) => filter.version_spec(), + _ => None, + } + } + pub fn is_included(&self) -> bool { + matches!(self, MatchResult::Included | MatchResult::Matches(_)) + } + } + + #[cfg(test)] + mod test { + fn matches_filters<'a, 'b>( + filters: impl IntoIterator, + name: &str, + ) -> bool { + let filters = super::FilterSet::from_filter_strings(filters).unwrap(); + filters.matches(name) + } + + fn version_spec(s: &str) -> deno_semver::VersionReq { + deno_semver::VersionReq::parse_from_specifier(s).unwrap() + } + + #[test] + fn basic_glob() { + assert!(matches_filters(["foo*"], "foo")); + assert!(matches_filters(["foo*"], "foobar")); + assert!(!matches_filters(["foo*"], "barfoo")); + + assert!(matches_filters(["*foo"], "foo")); + assert!(matches_filters(["*foo"], "barfoo")); + assert!(!matches_filters(["*foo"], "foobar")); + + assert!(matches_filters(["@scope/foo*"], "@scope/foobar")); + } + + #[test] + fn basic_glob_with_version() { + assert!(matches_filters(["foo*@1"], "foo",)); + assert!(matches_filters(["foo*@1"], "foobar",)); + assert!(matches_filters(["foo*@1"], "foo-bar",)); + assert!(!matches_filters(["foo*@1"], "barfoo",)); + assert!(matches_filters(["@scope/*@1"], "@scope/foo")); + } + + #[test] + fn glob_exclude() { + assert!(!matches_filters(["!foo*"], "foo")); + assert!(!matches_filters(["!foo*"], "foobar")); + assert!(matches_filters(["!foo*"], "barfoo")); + + assert!(!matches_filters(["!*foo"], "foo")); + assert!(!matches_filters(["!*foo"], "barfoo")); + assert!(matches_filters(["!*foo"], "foobar")); + + assert!(!matches_filters(["!@scope/foo*"], "@scope/foobar")); + } + + #[test] + fn multiple_globs() { + assert!(matches_filters(["foo*", "bar*"], "foo")); + assert!(matches_filters(["foo*", "bar*"], "bar")); + assert!(!matches_filters(["foo*", "bar*"], "baz")); + + assert!(matches_filters(["foo*", "!bar*"], "foo")); + assert!(!matches_filters(["foo*", "!bar*"], "bar")); + assert!(matches_filters(["foo*", "!bar*"], "foobar")); + assert!(!matches_filters(["foo*", "!*bar"], "foobar")); + assert!(!matches_filters(["foo*", "!*bar"], "baz")); + + let filters = + super::FilterSet::from_filter_strings(["foo*@1", "bar*@2"]).unwrap(); + + assert_eq!( + filters.matching_filter("foo").version_spec().cloned(), + Some(version_spec("1")) + ); + + assert_eq!( + filters.matching_filter("bar").version_spec().cloned(), + Some(version_spec("2")) + ); + } + } +} diff --git a/tests/registry/jsr/@denotest/add/0.2.1/mod.ts b/tests/registry/jsr/@denotest/add/0.2.1/mod.ts new file mode 100644 index 0000000000..864e8dd321 --- /dev/null +++ b/tests/registry/jsr/@denotest/add/0.2.1/mod.ts @@ -0,0 +1,4 @@ +// This is renamed to `add()` in 1.0.0. +export function sum(a: number, b: number): number { + return a + b; +} diff --git a/tests/registry/jsr/@denotest/add/0.2.1_meta.json b/tests/registry/jsr/@denotest/add/0.2.1_meta.json new file mode 100644 index 0000000000..6eebe21985 --- /dev/null +++ b/tests/registry/jsr/@denotest/add/0.2.1_meta.json @@ -0,0 +1,8 @@ +{ + "exports": { + ".": "./mod.ts" + }, + "moduleGraph1": { + "/mod.ts": {} + } +} diff --git a/tests/registry/jsr/@denotest/add/meta.json b/tests/registry/jsr/@denotest/add/meta.json index 72aea80cc3..f1a50109d8 100644 --- a/tests/registry/jsr/@denotest/add/meta.json +++ b/tests/registry/jsr/@denotest/add/meta.json @@ -4,6 +4,7 @@ "yanked": true }, "1.0.0": {}, - "0.2.0": {} + "0.2.0": {}, + "0.2.1": {} } } diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.2.0/add.ts b/tests/registry/jsr/@denotest/multiple-exports/0.2.0/add.ts new file mode 100644 index 0000000000..de02f69024 --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.2.0/add.ts @@ -0,0 +1 @@ +export * from "jsr:@denotest/add@1"; diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.2.0/data.json b/tests/registry/jsr/@denotest/multiple-exports/0.2.0/data.json new file mode 100644 index 0000000000..885e71c6cc --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.2.0/data.json @@ -0,0 +1,3 @@ +{ + "a": 1 +} \ No newline at end of file diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.2.0/subtract.ts b/tests/registry/jsr/@denotest/multiple-exports/0.2.0/subtract.ts new file mode 100644 index 0000000000..215c42310d --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.2.0/subtract.ts @@ -0,0 +1 @@ +export * from "jsr:@denotest/subtract@1"; diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.2.0_meta.json b/tests/registry/jsr/@denotest/multiple-exports/0.2.0_meta.json new file mode 100644 index 0000000000..d9f58b9a61 --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.2.0_meta.json @@ -0,0 +1,7 @@ +{ + "exports": { + "./add": "./add.ts", + "./subtract": "./subtract.ts", + "./data-json": "./data.json" + } +} diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.5.0/add.ts b/tests/registry/jsr/@denotest/multiple-exports/0.5.0/add.ts new file mode 100644 index 0000000000..de02f69024 --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.5.0/add.ts @@ -0,0 +1 @@ +export * from "jsr:@denotest/add@1"; diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.5.0/data.json b/tests/registry/jsr/@denotest/multiple-exports/0.5.0/data.json new file mode 100644 index 0000000000..885e71c6cc --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.5.0/data.json @@ -0,0 +1,3 @@ +{ + "a": 1 +} \ No newline at end of file diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.5.0/subtract.ts b/tests/registry/jsr/@denotest/multiple-exports/0.5.0/subtract.ts new file mode 100644 index 0000000000..215c42310d --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.5.0/subtract.ts @@ -0,0 +1 @@ +export * from "jsr:@denotest/subtract@1"; diff --git a/tests/registry/jsr/@denotest/multiple-exports/0.5.0_meta.json b/tests/registry/jsr/@denotest/multiple-exports/0.5.0_meta.json new file mode 100644 index 0000000000..d9f58b9a61 --- /dev/null +++ b/tests/registry/jsr/@denotest/multiple-exports/0.5.0_meta.json @@ -0,0 +1,7 @@ +{ + "exports": { + "./add": "./add.ts", + "./subtract": "./subtract.ts", + "./data-json": "./data.json" + } +} diff --git a/tests/registry/jsr/@denotest/multiple-exports/meta.json b/tests/registry/jsr/@denotest/multiple-exports/meta.json index 02601e4d0d..aaaf18a184 100644 --- a/tests/registry/jsr/@denotest/multiple-exports/meta.json +++ b/tests/registry/jsr/@denotest/multiple-exports/meta.json @@ -1,5 +1,7 @@ { "versions": { - "1.0.0": {} + "1.0.0": {}, + "0.5.0": {}, + "0.2.0": {} } } diff --git a/tests/registry/jsr/@denotest/subtract/0.2.0/mod.ts b/tests/registry/jsr/@denotest/subtract/0.2.0/mod.ts new file mode 100644 index 0000000000..74e49ea6fa --- /dev/null +++ b/tests/registry/jsr/@denotest/subtract/0.2.0/mod.ts @@ -0,0 +1,3 @@ +export function sub(a: number, b: number): number { + return a - b; +} diff --git a/tests/registry/jsr/@denotest/subtract/0.2.0_meta.json b/tests/registry/jsr/@denotest/subtract/0.2.0_meta.json new file mode 100644 index 0000000000..6eebe21985 --- /dev/null +++ b/tests/registry/jsr/@denotest/subtract/0.2.0_meta.json @@ -0,0 +1,8 @@ +{ + "exports": { + ".": "./mod.ts" + }, + "moduleGraph1": { + "/mod.ts": {} + } +} diff --git a/tests/registry/jsr/@denotest/subtract/meta.json b/tests/registry/jsr/@denotest/subtract/meta.json index 02601e4d0d..2f4cee59df 100644 --- a/tests/registry/jsr/@denotest/subtract/meta.json +++ b/tests/registry/jsr/@denotest/subtract/meta.json @@ -1,5 +1,7 @@ { + "latest": "1.0.0", "versions": { - "1.0.0": {} + "1.0.0": {}, + "0.2.0": {} } } diff --git a/tests/registry/npm/@denotest/has-patch-versions/0.1.0/package.json b/tests/registry/npm/@denotest/has-patch-versions/0.1.0/package.json new file mode 100644 index 0000000000..45684d4f52 --- /dev/null +++ b/tests/registry/npm/@denotest/has-patch-versions/0.1.0/package.json @@ -0,0 +1,5 @@ +{ + "name": "@denotest/has-patch-versions", + "version": "0.1.0" + +} \ No newline at end of file diff --git a/tests/registry/npm/@denotest/has-patch-versions/0.1.1/package.json b/tests/registry/npm/@denotest/has-patch-versions/0.1.1/package.json new file mode 100644 index 0000000000..4483df34e5 --- /dev/null +++ b/tests/registry/npm/@denotest/has-patch-versions/0.1.1/package.json @@ -0,0 +1,5 @@ +{ + "name": "@denotest/has-patch-versions", + "version": "0.1.1" + +} \ No newline at end of file diff --git a/tests/registry/npm/@denotest/has-patch-versions/0.2.0/package.json b/tests/registry/npm/@denotest/has-patch-versions/0.2.0/package.json new file mode 100644 index 0000000000..55efaaa326 --- /dev/null +++ b/tests/registry/npm/@denotest/has-patch-versions/0.2.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "@denotest/has-patch-versions", + "version": "0.2.0" +} \ No newline at end of file diff --git a/tests/specs/update/deno_json/__test__.jsonc b/tests/specs/update/deno_json/__test__.jsonc new file mode 100644 index 0000000000..8b4aa26b5c --- /dev/null +++ b/tests/specs/update/deno_json/__test__.jsonc @@ -0,0 +1,101 @@ +{ + "tempDir": true, + "tests": { + // just to make sure install doesn't change the lockfile + "sanity_lockfile_up_to_date": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": [ + "eval", + "const now = Deno.readTextFileSync('./deno.lock'); console.log(now.trim());" + ], + "output": "deno.lock.orig.out" + } + ] + }, + "print_outdated": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated", + "output": "outdated.out" + } + ] + }, + "print_outdated_compatible": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --compatible", + "output": "outdated_compatible.out" + } + ] + }, + "update_compatible": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update", + "output": "./update_compatible/update.out" + }, + { + "args": "-A print_file.ts ./deno.json", + "output": "./update_compatible/deno.json.out" + }, + { + "args": "-A print_file.ts ./deno.lock", + "output": "./update_compatible/deno.lock.out" + } + ] + }, + "update_latest": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest", + "output": "update_latest/update.out" + }, + { + "args": "-A print_file.ts ./deno.json", + "output": "./update_latest/deno.json.out" + }, + { + "args": "-A print_file.ts ./deno.lock", + "output": "./update_latest/deno.lock.out" + } + ] + }, + "update_filtered": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest @denotest/add @denotest/b* !@denotest/breaking* @denotest/with-subpath@0.5.0", + "output": "filtered/update.out" + }, + { + "args": "-A print_file.ts ./deno.json", + "output": "filtered/deno.json.out" + } + ] + } + } +} diff --git a/tests/specs/update/deno_json/deno.json b/tests/specs/update/deno_json/deno.json new file mode 100644 index 0000000000..4f880e6db9 --- /dev/null +++ b/tests/specs/update/deno_json/deno.json @@ -0,0 +1,19 @@ +{ + "imports": { + "@denotest/add": "jsr:@denotest/add@^0.2.0", + "@denotest/add/": "jsr:/@denotest/add@^0.2.0/", + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.2.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@1.0.0", + "@denotest/bin": "npm:@denotest/bin@0.6.0", + "@denotest/has-patch-versions": "npm:@denotest/has-patch-versions@^0.1.0" + }, + "scopes": { + "/foo/": { + "@denotest/add": "jsr:@denotest/add@^0.2.0", + "@denotest/add/": "jsr:/@denotest/add@^0.2.0/", + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.2.0/data-json" + } + } +} diff --git a/tests/specs/update/deno_json/deno.lock b/tests/specs/update/deno_json/deno.lock new file mode 100644 index 0000000000..7c4d5e3690 --- /dev/null +++ b/tests/specs/update/deno_json/deno.lock @@ -0,0 +1,43 @@ +{ + "version": "4", + "specifiers": { + "jsr:@denotest/add@0.2": "0.2.0", + "jsr:@denotest/multiple-exports@0.2.0": "0.2.0", + "jsr:@denotest/subtract@0.2": "0.2.0", + "npm:@denotest/bin@0.6.0": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@0.1": "0.1.0" + }, + "jsr": { + "@denotest/add@0.2.0": { + "integrity": "a9076d30ecb42b2fc6dd95e7055fbf4e6358b53f550741bd7f60089d19f68848" + }, + "@denotest/multiple-exports@0.2.0": { + "integrity": "efe9748a0c0939c7ac245fee04acc0c42bd7a61874ff71a360c4543e4f5f6b36" + }, + "@denotest/subtract@0.2.0": { + "integrity": "c9650fc559ab2430effc0c7fb1540e3aa89888fbdd926335ccfdeac57eb3a64d" + } + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.0": { + "integrity": "sha512-H/MBo0jKDdMsX4AAGEGQbZj70nfNe3oUNZXbohYHhqf9EfpLnXp/7FC29ZdfV4+p6VjEcOGdCtXc6rilE6iYpg==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@denotest/add@0.2", + "jsr:@denotest/multiple-exports@0.2.0", + "jsr:@denotest/subtract@0.2", + "npm:@denotest/bin@0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0", + "npm:@denotest/has-patch-versions@0.1" + ] + } +} diff --git a/tests/specs/update/deno_json/deno.lock.orig.out b/tests/specs/update/deno_json/deno.lock.orig.out new file mode 100644 index 0000000000..7c4d5e3690 --- /dev/null +++ b/tests/specs/update/deno_json/deno.lock.orig.out @@ -0,0 +1,43 @@ +{ + "version": "4", + "specifiers": { + "jsr:@denotest/add@0.2": "0.2.0", + "jsr:@denotest/multiple-exports@0.2.0": "0.2.0", + "jsr:@denotest/subtract@0.2": "0.2.0", + "npm:@denotest/bin@0.6.0": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@0.1": "0.1.0" + }, + "jsr": { + "@denotest/add@0.2.0": { + "integrity": "a9076d30ecb42b2fc6dd95e7055fbf4e6358b53f550741bd7f60089d19f68848" + }, + "@denotest/multiple-exports@0.2.0": { + "integrity": "efe9748a0c0939c7ac245fee04acc0c42bd7a61874ff71a360c4543e4f5f6b36" + }, + "@denotest/subtract@0.2.0": { + "integrity": "c9650fc559ab2430effc0c7fb1540e3aa89888fbdd926335ccfdeac57eb3a64d" + } + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.0": { + "integrity": "sha512-H/MBo0jKDdMsX4AAGEGQbZj70nfNe3oUNZXbohYHhqf9EfpLnXp/7FC29ZdfV4+p6VjEcOGdCtXc6rilE6iYpg==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@denotest/add@0.2", + "jsr:@denotest/multiple-exports@0.2.0", + "jsr:@denotest/subtract@0.2", + "npm:@denotest/bin@0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0", + "npm:@denotest/has-patch-versions@0.1" + ] + } +} diff --git a/tests/specs/update/deno_json/filtered/deno.json.out b/tests/specs/update/deno_json/filtered/deno.json.out new file mode 100644 index 0000000000..4458e2d037 --- /dev/null +++ b/tests/specs/update/deno_json/filtered/deno.json.out @@ -0,0 +1,19 @@ +{ + "imports": { + "@denotest/add": "jsr:@denotest/add@^1.0.0", + "@denotest/add/": "jsr:/@denotest/add@^1.0.0/", + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.5.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@1.0.0", + "@denotest/bin": "npm:@denotest/bin@^1.0.0", + "@denotest/has-patch-versions": "npm:@denotest/has-patch-versions@^0.1.0" + }, + "scopes": { + "/foo/": { + "@denotest/add": "jsr:@denotest/add@^1.0.0", + "@denotest/add/": "jsr:/@denotest/add@^1.0.0/", + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.5.0/data-json" + } + } +} diff --git a/tests/specs/update/deno_json/filtered/update.out b/tests/specs/update/deno_json/filtered/update.out new file mode 100644 index 0000000000..ce16c6f6b7 --- /dev/null +++ b/tests/specs/update/deno_json/filtered/update.out @@ -0,0 +1,10 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/bin/1.0.0.tgz +Download http://127.0.0.1:4250/@denotest/multiple-exports/0.5.0_meta.json +Download http://127.0.0.1:4250/@denotest/multiple-exports/0.5.0/data.json +Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts +[UNORDERED_END] +Updated 3 dependencies: + - jsr:@denotest/add 0.2.0 -> 1.0.0 + - jsr:@denotest/multiple-exports 0.2.0 -> 0.5.0 + - npm:@denotest/bin 0.6.0 -> 1.0.0 diff --git a/tests/specs/update/deno_json/outdated.out b/tests/specs/update/deno_json/outdated.out new file mode 100644 index 0000000000..07ff9f3416 --- /dev/null +++ b/tests/specs/update/deno_json/outdated.out @@ -0,0 +1,15 @@ +┌────────────────────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/multiple-exports │ 0.2.0 │ 0.2.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/subtract │ 0.2.0 │ 0.2.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/add │ 0.2.0 │ 0.2.1 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.1 │ 0.2.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/bin │ 0.6.0 │ 0.6.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/breaking-change-between-versions │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└────────────────────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/deno_json/outdated_compatible.out b/tests/specs/update/deno_json/outdated_compatible.out new file mode 100644 index 0000000000..54511a537f --- /dev/null +++ b/tests/specs/update/deno_json/outdated_compatible.out @@ -0,0 +1,7 @@ +┌──────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├──────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/add │ 0.2.0 │ 0.2.1 │ 1.0.0 │ +├──────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.1 │ 0.2.0 │ +└──────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/deno_json/print_file.ts b/tests/specs/update/deno_json/print_file.ts new file mode 100644 index 0000000000..c57b6c3a4e --- /dev/null +++ b/tests/specs/update/deno_json/print_file.ts @@ -0,0 +1,2 @@ +const file = Deno.args[0]; +console.log(Deno.readTextFileSync(file).trim()); diff --git a/tests/specs/update/deno_json/update_compatible/deno.json.out b/tests/specs/update/deno_json/update_compatible/deno.json.out new file mode 100644 index 0000000000..e453eff3c9 --- /dev/null +++ b/tests/specs/update/deno_json/update_compatible/deno.json.out @@ -0,0 +1,19 @@ +{ + "imports": { + "@denotest/add": "jsr:@denotest/add@^0.2.1", + "@denotest/add/": "jsr:/@denotest/add@^0.2.1/", + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.2.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@1.0.0", + "@denotest/bin": "npm:@denotest/bin@0.6.0", + "@denotest/has-patch-versions": "npm:@denotest/has-patch-versions@^0.1.1" + }, + "scopes": { + "/foo/": { + "@denotest/add": "jsr:@denotest/add@^0.2.1", + "@denotest/add/": "jsr:/@denotest/add@^0.2.1/", + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.2.0/data-json" + } + } +} diff --git a/tests/specs/update/deno_json/update_compatible/deno.lock.out b/tests/specs/update/deno_json/update_compatible/deno.lock.out new file mode 100644 index 0000000000..994d6f7ac9 --- /dev/null +++ b/tests/specs/update/deno_json/update_compatible/deno.lock.out @@ -0,0 +1,43 @@ +{ + "version": "4", + "specifiers": { + "jsr:@denotest/add@~0.2.1": "0.2.1", + "jsr:@denotest/multiple-exports@0.2.0": "0.2.0", + "jsr:@denotest/subtract@0.2": "0.2.0", + "npm:@denotest/bin@0.6.0": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@~0.1.1": "0.1.1" + }, + "jsr": { + "@denotest/add@0.2.1": { + "integrity": "a9076d30ecb42b2fc6dd95e7055fbf4e6358b53f550741bd7f60089d19f68848" + }, + "@denotest/multiple-exports@0.2.0": { + "integrity": "efe9748a0c0939c7ac245fee04acc0c42bd7a61874ff71a360c4543e4f5f6b36" + }, + "@denotest/subtract@0.2.0": { + "integrity": "c9650fc559ab2430effc0c7fb1540e3aa89888fbdd926335ccfdeac57eb3a64d" + } + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.1": { + "integrity": "sha512-slUqYhu6DrPiSdNzmW5aMdW2/osIYnDP0yY3CwgLzAiyK0/cwb0epSpTSyZEmTKXA3rezxxC7ASSsnD34uH1/w==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@denotest/add@~0.2.1", + "jsr:@denotest/multiple-exports@0.2.0", + "jsr:@denotest/subtract@0.2", + "npm:@denotest/bin@0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0", + "npm:@denotest/has-patch-versions@~0.1.1" + ] + } +} diff --git a/tests/specs/update/deno_json/update_compatible/update.out b/tests/specs/update/deno_json/update_compatible/update.out new file mode 100644 index 0000000000..17c5358fe6 --- /dev/null +++ b/tests/specs/update/deno_json/update_compatible/update.out @@ -0,0 +1,7 @@ +[UNORDERED_START] +Download http://127.0.0.1:4250/@denotest/add/0.2.1/mod.ts +Download http://localhost:4260/@denotest/has-patch-versions/0.1.1.tgz +[UNORDERED_END] +Updated 2 dependencies: + - jsr:@denotest/add 0.2.0 -> 0.2.1 + - npm:@denotest/has-patch-versions 0.1.0 -> 0.1.1 diff --git a/tests/specs/update/deno_json/update_latest/deno.json.out b/tests/specs/update/deno_json/update_latest/deno.json.out new file mode 100644 index 0000000000..5e4e99bd66 --- /dev/null +++ b/tests/specs/update/deno_json/update_latest/deno.json.out @@ -0,0 +1,19 @@ +{ + "imports": { + "@denotest/add": "jsr:@denotest/add@^1.0.0", + "@denotest/add/": "jsr:/@denotest/add@^1.0.0/", + "@denotest/subtract": "jsr:@denotest/subtract@^1.0.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@^1.0.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@^2.0.0", + "@denotest/bin": "npm:@denotest/bin@^1.0.0", + "@denotest/has-patch-versions": "npm:@denotest/has-patch-versions@^0.2.0" + }, + "scopes": { + "/foo/": { + "@denotest/add": "jsr:@denotest/add@^1.0.0", + "@denotest/add/": "jsr:/@denotest/add@^1.0.0/", + "@denotest/subtract": "jsr:@denotest/subtract@^1.0.0", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@^1.0.0/data-json" + } + } +} diff --git a/tests/specs/update/deno_json/update_latest/deno.lock.out b/tests/specs/update/deno_json/update_latest/deno.lock.out new file mode 100644 index 0000000000..ad83546ab1 --- /dev/null +++ b/tests/specs/update/deno_json/update_latest/deno.lock.out @@ -0,0 +1,43 @@ +{ + "version": "4", + "specifiers": { + "jsr:@denotest/add@1": "1.0.0", + "jsr:@denotest/multiple-exports@1": "1.0.0", + "jsr:@denotest/subtract@1": "1.0.0", + "npm:@denotest/bin@1": "1.0.0", + "npm:@denotest/breaking-change-between-versions@2": "2.0.0", + "npm:@denotest/has-patch-versions@0.2": "0.2.0" + }, + "jsr": { + "@denotest/add@1.0.0": { + "integrity": "3b2e675c1ad7fba2a45bc251992e01aff08a3c974ac09079b11e6a5b95d4bfcb" + }, + "@denotest/multiple-exports@1.0.0": { + "integrity": "efe9748a0c0939c7ac245fee04acc0c42bd7a61874ff71a360c4543e4f5f6b36" + }, + "@denotest/subtract@1.0.0": { + "integrity": "e178a7101c073e93d9efa6833d5cbf83bc1bc8d509b7c2a5ecbf74265e917597" + } + }, + "npm": { + "@denotest/bin@1.0.0": { + "integrity": "sha512-ZtrWnYYPIzw4a9H1uNeZRZRWuLCpHZZU/SllIyFLqcTLH/3zdRI8UH4Z1Kf+8N++bWGO3fg8Ev4vvS1LoLlidg==" + }, + "@denotest/breaking-change-between-versions@2.0.0": { + "integrity": "sha512-3eQpPhhJYbSHaAmpgyVT8IM3+MkxcAQl90Uw8zmuTiFs64Wt3HGzSz74cwPlvfqqesRktm8fBZMmrtxVo3ENzw==" + }, + "@denotest/has-patch-versions@0.2.0": { + "integrity": "sha512-7XIVGrBMyqnts5/wcc7dn7rVl4IWrCiGUT2GLDSLWnogOMIZBapJJLW9n8Leq1bDTJ3U6aDTEFKz9fVSOwZfLQ==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@denotest/add@1", + "jsr:@denotest/multiple-exports@1", + "jsr:@denotest/subtract@1", + "npm:@denotest/bin@1", + "npm:@denotest/breaking-change-between-versions@2", + "npm:@denotest/has-patch-versions@0.2" + ] + } +} diff --git a/tests/specs/update/deno_json/update_latest/update.out b/tests/specs/update/deno_json/update_latest/update.out new file mode 100644 index 0000000000..35e1ef9fe5 --- /dev/null +++ b/tests/specs/update/deno_json/update_latest/update.out @@ -0,0 +1,16 @@ +[UNORDERED_START] +Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts +Download http://127.0.0.1:4250/@denotest/subtract/1.0.0/mod.ts +Download http://127.0.0.1:4250/@denotest/multiple-exports/1.0.0_meta.json +Download http://127.0.0.1:4250/@denotest/multiple-exports/1.0.0/data.json +Download http://localhost:4260/@denotest/has-patch-versions/0.2.0.tgz +Download http://localhost:4260/@denotest/bin/1.0.0.tgz +Download http://localhost:4260/@denotest/breaking-change-between-versions/2.0.0.tgz +[UNORDERED_END] +Updated 6 dependencies: + - jsr:@denotest/add 0.2.0 -> 1.0.0 + - jsr:@denotest/multiple-exports 0.2.0 -> 1.0.0 + - jsr:@denotest/subtract 0.2.0 -> 1.0.0 + - npm:@denotest/bin 0.6.0 -> 1.0.0 + - npm:@denotest/breaking-change-between-versions 1.0.0 -> 2.0.0 + - npm:@denotest/has-patch-versions 0.1.0 -> 0.2.0 diff --git a/tests/specs/update/mixed_workspace/__test__.jsonc b/tests/specs/update/mixed_workspace/__test__.jsonc new file mode 100644 index 0000000000..8c846467d4 --- /dev/null +++ b/tests/specs/update/mixed_workspace/__test__.jsonc @@ -0,0 +1,153 @@ +{ + "tempDir": true, + "tests": { + // just to make sure install doesn't change the lockfile + "sanity_lockfile_up_to_date": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": [ + "eval", + "const now = Deno.readTextFileSync('./deno.lock'); console.log(now.trim());" + ], + "output": "deno.lock.orig.out" + } + ] + }, + "print_outdated_root": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated", + "output": "print_outdated/root.out" + } + ] + }, + "print_outdated_recursive": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --recursive", + "output": "print_outdated/recursive.out" + } + ] + }, + "print_outdated_subdir": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "cwd": "member-a", + "args": "outdated", + "output": "print_outdated/member_a.out" + }, + { + "cwd": "member-b", + "args": "outdated", + "output": "print_outdated/member_b.out" + } + ] + }, + "update_latest_root": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest", + "output": "update_latest/root/update.out" + }, + { + "args": "-A print_file.ts ./deno.json", + "output": "./update_latest/root/deno.json.out" + } + ] + }, + "update_latest_subdir": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "cwd": "member-a", + "args": "outdated --update --latest", + "output": "update_latest/subdir/member_a.out" + }, + { + "args": "-A print_file.ts ./member-a/deno.json", + "output": "update_latest/subdir/member_a_deno.json.out" + }, + { + "cwd": "member-b", + "args": "outdated --update --latest", + "output": "update_latest/subdir/member_b.out" + }, + { + "args": "-A print_file.ts ./member-b/package.json", + "output": "update_latest/subdir/member_b_package.json.out" + } + ] + }, + "update_latest_recursive": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest --recursive", + "output": "update_latest/recursive/update.out" + }, + { + "args": "-A print_file.ts ./deno.json", + "output": "update_latest/root/deno.json.out" + }, + { + "args": "-A print_file.ts ./member-a/deno.json", + "output": "update_latest/subdir/member_a_deno.json.out" + }, + { + "args": "-A print_file.ts ./member-b/package.json", + "output": "update_latest/subdir/member_b_package.json.out" + } + ] + }, + "update_filtered": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest --recursive @denotest/add @denotest/sub* !@denotest/breaking* aliased @denotest/with-subpath@0.5.0", + "output": "filtered/update.out" + }, + { + "args": "-A print_file.ts ./deno.json", + "output": "./update_latest/root/deno.json.out" + }, + { + "args": "-A print_file.ts ./member-a/deno.json", + "output": "./filtered/member_a_deno.json.out" + }, + { + "args": "-A print_file.ts ./member-b/package.json", + "output": "./filtered/member_b_package.json.out" + } + ] + } + } +} diff --git a/tests/specs/update/mixed_workspace/deno.json b/tests/specs/update/mixed_workspace/deno.json new file mode 100644 index 0000000000..e4034d3a31 --- /dev/null +++ b/tests/specs/update/mixed_workspace/deno.json @@ -0,0 +1,6 @@ +{ + "workspace": ["./member-a", "./member-b"], + "imports": { + "@denotest/subtract": "jsr:@denotest/subtract@^0.2.0" + } +} diff --git a/tests/specs/update/mixed_workspace/deno.lock b/tests/specs/update/mixed_workspace/deno.lock new file mode 100644 index 0000000000..23613ce58d --- /dev/null +++ b/tests/specs/update/mixed_workspace/deno.lock @@ -0,0 +1,55 @@ +{ + "version": "4", + "specifiers": { + "jsr:@denotest/add@0.2": "0.2.1", + "jsr:@denotest/multiple-exports@0.2.0": "0.2.0", + "jsr:@denotest/subtract@0.2": "0.2.0", + "npm:@denotest/bin@0.6.0": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@0.1.0": "0.1.0" + }, + "jsr": { + "@denotest/add@0.2.1": { + "integrity": "a9076d30ecb42b2fc6dd95e7055fbf4e6358b53f550741bd7f60089d19f68848" + }, + "@denotest/multiple-exports@0.2.0": { + "integrity": "efe9748a0c0939c7ac245fee04acc0c42bd7a61874ff71a360c4543e4f5f6b36" + }, + "@denotest/subtract@0.2.0": { + "integrity": "c9650fc559ab2430effc0c7fb1540e3aa89888fbdd926335ccfdeac57eb3a64d" + } + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.0": { + "integrity": "sha512-H/MBo0jKDdMsX4AAGEGQbZj70nfNe3oUNZXbohYHhqf9EfpLnXp/7FC29ZdfV4+p6VjEcOGdCtXc6rilE6iYpg==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@denotest/subtract@0.2" + ], + "members": { + "member-a": { + "dependencies": [ + "jsr:@denotest/add@0.2", + "jsr:@denotest/multiple-exports@0.2.0", + "npm:@denotest/breaking-change-between-versions@1.0.0" + ] + }, + "member-b": { + "packageJson": { + "dependencies": [ + "npm:@denotest/bin@0.6.0", + "npm:@denotest/has-patch-versions@0.1.0" + ] + } + } + } + } +} diff --git a/tests/specs/update/mixed_workspace/deno.lock.orig.out b/tests/specs/update/mixed_workspace/deno.lock.orig.out new file mode 100644 index 0000000000..23613ce58d --- /dev/null +++ b/tests/specs/update/mixed_workspace/deno.lock.orig.out @@ -0,0 +1,55 @@ +{ + "version": "4", + "specifiers": { + "jsr:@denotest/add@0.2": "0.2.1", + "jsr:@denotest/multiple-exports@0.2.0": "0.2.0", + "jsr:@denotest/subtract@0.2": "0.2.0", + "npm:@denotest/bin@0.6.0": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@0.1.0": "0.1.0" + }, + "jsr": { + "@denotest/add@0.2.1": { + "integrity": "a9076d30ecb42b2fc6dd95e7055fbf4e6358b53f550741bd7f60089d19f68848" + }, + "@denotest/multiple-exports@0.2.0": { + "integrity": "efe9748a0c0939c7ac245fee04acc0c42bd7a61874ff71a360c4543e4f5f6b36" + }, + "@denotest/subtract@0.2.0": { + "integrity": "c9650fc559ab2430effc0c7fb1540e3aa89888fbdd926335ccfdeac57eb3a64d" + } + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.0": { + "integrity": "sha512-H/MBo0jKDdMsX4AAGEGQbZj70nfNe3oUNZXbohYHhqf9EfpLnXp/7FC29ZdfV4+p6VjEcOGdCtXc6rilE6iYpg==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@denotest/subtract@0.2" + ], + "members": { + "member-a": { + "dependencies": [ + "jsr:@denotest/add@0.2", + "jsr:@denotest/multiple-exports@0.2.0", + "npm:@denotest/breaking-change-between-versions@1.0.0" + ] + }, + "member-b": { + "packageJson": { + "dependencies": [ + "npm:@denotest/bin@0.6.0", + "npm:@denotest/has-patch-versions@0.1.0" + ] + } + } + } + } +} diff --git a/tests/specs/update/mixed_workspace/filtered/member_a_deno.json.out b/tests/specs/update/mixed_workspace/filtered/member_a_deno.json.out new file mode 100644 index 0000000000..2b622efee5 --- /dev/null +++ b/tests/specs/update/mixed_workspace/filtered/member_a_deno.json.out @@ -0,0 +1,10 @@ +{ + "name": "@denotest/member-a", + "exports": "./mod.ts", + "imports": { + "@denotest/add": "jsr:@denotest/add@^1.0.0", + "@denotest/add/": "jsr:/@denotest/add@^1.0.0/", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.5.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@1.0.0" + } +} diff --git a/tests/specs/update/mixed_workspace/filtered/member_b_package.json.out b/tests/specs/update/mixed_workspace/filtered/member_b_package.json.out new file mode 100644 index 0000000000..7e582feeab --- /dev/null +++ b/tests/specs/update/mixed_workspace/filtered/member_b_package.json.out @@ -0,0 +1,8 @@ +{ + "name": "@denotest/member-b", + "version": "0.1.0", + "dependencies": { + "@denotest/has-patch-versions": "0.1.0", + "aliased": "npm:@denotest/bin@^1.0.0" + } +} diff --git a/tests/specs/update/mixed_workspace/filtered/update.out b/tests/specs/update/mixed_workspace/filtered/update.out new file mode 100644 index 0000000000..26543e5167 --- /dev/null +++ b/tests/specs/update/mixed_workspace/filtered/update.out @@ -0,0 +1,12 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/bin/1.0.0.tgz +Download http://127.0.0.1:4250/@denotest/multiple-exports/0.5.0_meta.json +Download http://127.0.0.1:4250/@denotest/multiple-exports/0.5.0/data.json +Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts +Download http://127.0.0.1:4250/@denotest/subtract/1.0.0/mod.ts +[UNORDERED_END] +Updated 4 dependencies: + - jsr:@denotest/add 0.2.1 -> 1.0.0 + - jsr:@denotest/multiple-exports 0.2.0 -> 0.5.0 + - jsr:@denotest/subtract 0.2.0 -> 1.0.0 + - npm:@denotest/bin 0.6.0 -> 1.0.0 diff --git a/tests/specs/update/mixed_workspace/member-a/deno.json b/tests/specs/update/mixed_workspace/member-a/deno.json new file mode 100644 index 0000000000..0340d3bb92 --- /dev/null +++ b/tests/specs/update/mixed_workspace/member-a/deno.json @@ -0,0 +1,10 @@ +{ + "name": "@denotest/member-a", + "exports": "./mod.ts", + "imports": { + "@denotest/add": "jsr:@denotest/add@^0.2.0", + "@denotest/add/": "jsr:/@denotest/add@^0.2.0/", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@0.2.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@1.0.0" + } +} diff --git a/tests/specs/update/mixed_workspace/member-a/mod.ts b/tests/specs/update/mixed_workspace/member-a/mod.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/specs/update/mixed_workspace/member-b/package.json b/tests/specs/update/mixed_workspace/member-b/package.json new file mode 100644 index 0000000000..0f9929a478 --- /dev/null +++ b/tests/specs/update/mixed_workspace/member-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "@denotest/member-b", + "version": "0.1.0", + "dependencies": { + "@denotest/has-patch-versions": "0.1.0", + "aliased": "npm:@denotest/bin@0.6.0" + } +} diff --git a/tests/specs/update/mixed_workspace/print_file.ts b/tests/specs/update/mixed_workspace/print_file.ts new file mode 100644 index 0000000000..c57b6c3a4e --- /dev/null +++ b/tests/specs/update/mixed_workspace/print_file.ts @@ -0,0 +1,2 @@ +const file = Deno.args[0]; +console.log(Deno.readTextFileSync(file).trim()); diff --git a/tests/specs/update/mixed_workspace/print_outdated/member_a.out b/tests/specs/update/mixed_workspace/print_outdated/member_a.out new file mode 100644 index 0000000000..8699aac2bf --- /dev/null +++ b/tests/specs/update/mixed_workspace/print_outdated/member_a.out @@ -0,0 +1,9 @@ +┌────────────────────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/multiple-exports │ 0.2.0 │ 0.2.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/add │ 0.2.1 │ 0.2.1 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/breaking-change-between-versions │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└────────────────────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/mixed_workspace/print_outdated/member_b.out b/tests/specs/update/mixed_workspace/print_outdated/member_b.out new file mode 100644 index 0000000000..fc8ef320a8 --- /dev/null +++ b/tests/specs/update/mixed_workspace/print_outdated/member_b.out @@ -0,0 +1,7 @@ +┌──────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├──────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.0 │ 0.2.0 │ +├──────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/bin │ 0.6.0 │ 0.6.0 │ 1.0.0 │ +└──────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/mixed_workspace/print_outdated/recursive.out b/tests/specs/update/mixed_workspace/print_outdated/recursive.out new file mode 100644 index 0000000000..ca03776a9c --- /dev/null +++ b/tests/specs/update/mixed_workspace/print_outdated/recursive.out @@ -0,0 +1,15 @@ +┌────────────────────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/multiple-exports │ 0.2.0 │ 0.2.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/subtract │ 0.2.0 │ 0.2.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/add │ 0.2.1 │ 0.2.1 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.0 │ 0.2.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/bin │ 0.6.0 │ 0.6.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/breaking-change-between-versions │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└────────────────────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/mixed_workspace/print_outdated/root.out b/tests/specs/update/mixed_workspace/print_outdated/root.out new file mode 100644 index 0000000000..c7934edc79 --- /dev/null +++ b/tests/specs/update/mixed_workspace/print_outdated/root.out @@ -0,0 +1,5 @@ +┌────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────────────┼─────────┼────────┼────────┤ +│ jsr:@denotest/subtract │ 0.2.0 │ 0.2.0 │ 1.0.0 │ +└────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/mixed_workspace/update_latest/recursive/update.out b/tests/specs/update/mixed_workspace/update_latest/recursive/update.out new file mode 100644 index 0000000000..ef6e36ded5 --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/recursive/update.out @@ -0,0 +1,16 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/breaking-change-between-versions/2.0.0.tgz +Download http://localhost:4260/@denotest/has-patch-versions/0.2.0.tgz +Download http://localhost:4260/@denotest/bin/1.0.0.tgz +Download http://127.0.0.1:4250/@denotest/multiple-exports/1.0.0_meta.json +Download http://127.0.0.1:4250/@denotest/multiple-exports/1.0.0/data.json +Download http://127.0.0.1:4250/@denotest/subtract/1.0.0/mod.ts +Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts +[UNORDERED_END] +Updated 6 dependencies: + - jsr:@denotest/add 0.2.1 -> 1.0.0 + - jsr:@denotest/multiple-exports 0.2.0 -> 1.0.0 + - jsr:@denotest/subtract 0.2.0 -> 1.0.0 + - npm:@denotest/bin 0.6.0 -> 1.0.0 + - npm:@denotest/breaking-change-between-versions 1.0.0 -> 2.0.0 + - npm:@denotest/has-patch-versions 0.1.0 -> 0.2.0 diff --git a/tests/specs/update/mixed_workspace/update_latest/root/deno.json.out b/tests/specs/update/mixed_workspace/update_latest/root/deno.json.out new file mode 100644 index 0000000000..0317e6c39f --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/root/deno.json.out @@ -0,0 +1,6 @@ +{ + "workspace": ["./member-a", "./member-b"], + "imports": { + "@denotest/subtract": "jsr:@denotest/subtract@^1.0.0" + } +} diff --git a/tests/specs/update/mixed_workspace/update_latest/root/update.out b/tests/specs/update/mixed_workspace/update_latest/root/update.out new file mode 100644 index 0000000000..15d21621e8 --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/root/update.out @@ -0,0 +1,3 @@ +Download http://127.0.0.1:4250/@denotest/subtract/1.0.0/mod.ts +Updated 1 dependency: + - jsr:@denotest/subtract 0.2.0 -> 1.0.0 diff --git a/tests/specs/update/mixed_workspace/update_latest/subdir/member_a.out b/tests/specs/update/mixed_workspace/update_latest/subdir/member_a.out new file mode 100644 index 0000000000..f16e0b7f2a --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/subdir/member_a.out @@ -0,0 +1,10 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/breaking-change-between-versions/2.0.0.tgz +Download http://127.0.0.1:4250/@denotest/multiple-exports/1.0.0_meta.json +Download http://127.0.0.1:4250/@denotest/multiple-exports/1.0.0/data.json +Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts +[UNORDERED_END] +Updated 3 dependencies: + - jsr:@denotest/add 0.2.1 -> 1.0.0 + - jsr:@denotest/multiple-exports 0.2.0 -> 1.0.0 + - npm:@denotest/breaking-change-between-versions 1.0.0 -> 2.0.0 diff --git a/tests/specs/update/mixed_workspace/update_latest/subdir/member_a_deno.json.out b/tests/specs/update/mixed_workspace/update_latest/subdir/member_a_deno.json.out new file mode 100644 index 0000000000..9210123b82 --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/subdir/member_a_deno.json.out @@ -0,0 +1,10 @@ +{ + "name": "@denotest/member-a", + "exports": "./mod.ts", + "imports": { + "@denotest/add": "jsr:@denotest/add@^1.0.0", + "@denotest/add/": "jsr:/@denotest/add@^1.0.0/", + "@denotest/with-subpath": "jsr:@denotest/multiple-exports@^1.0.0/data-json", + "@denotest/breaking-change-between-versions": "npm:@denotest/breaking-change-between-versions@^2.0.0" + } +} diff --git a/tests/specs/update/mixed_workspace/update_latest/subdir/member_b.out b/tests/specs/update/mixed_workspace/update_latest/subdir/member_b.out new file mode 100644 index 0000000000..5ca3297e20 --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/subdir/member_b.out @@ -0,0 +1,7 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/bin/1.0.0.tgz +Download http://localhost:4260/@denotest/has-patch-versions/0.2.0.tgz +[UNORDERED_END] +Updated 2 dependencies: + - npm:@denotest/bin 0.6.0 -> 1.0.0 + - npm:@denotest/has-patch-versions 0.1.0 -> 0.2.0 diff --git a/tests/specs/update/mixed_workspace/update_latest/subdir/member_b_package.json.out b/tests/specs/update/mixed_workspace/update_latest/subdir/member_b_package.json.out new file mode 100644 index 0000000000..1426fcd7f8 --- /dev/null +++ b/tests/specs/update/mixed_workspace/update_latest/subdir/member_b_package.json.out @@ -0,0 +1,8 @@ +{ + "name": "@denotest/member-b", + "version": "0.1.0", + "dependencies": { + "@denotest/has-patch-versions": "^0.2.0", + "aliased": "npm:@denotest/bin@^1.0.0" + } +} diff --git a/tests/specs/update/package_json/__test__.jsonc b/tests/specs/update/package_json/__test__.jsonc new file mode 100644 index 0000000000..19d576dfc0 --- /dev/null +++ b/tests/specs/update/package_json/__test__.jsonc @@ -0,0 +1,100 @@ +{ + "tempDir": true, + "tests": { + "sanity_lockfile_up_to_date": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": [ + "eval", + "const now = Deno.readTextFileSync('./deno.lock'); console.log(now.trim());" + ], + "output": "deno.lock.orig.out" + } + ] + }, + "print_outdated": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated", + "output": "outdated.out" + } + ] + }, + "print_outdated_compatible": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --compatible", + "output": "outdated_compatible.out" + } + ] + }, + "update_compatible": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update", + "output": "./update_compatible/update.out" + }, + { + "args": "-A ./print_file.ts ./package.json", + "output": "./update_compatible/package.json.out" + }, + { + "args": "-A ./print_file.ts ./deno.lock", + "output": "./update_compatible/deno.lock.out" + } + ] + }, + "update_latest": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest", + "output": "update_latest/update.out" + }, + { + "args": "-A ./print_file.ts ./package.json", + "output": "update_latest/package.json.out" + }, + { + "args": "-A ./print_file.ts ./deno.lock", + "output": "update_latest/deno.lock.out" + } + ] + }, + "update_filtered": { + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + "args": "outdated --update --latest @denotest/has-patch* aliased@0.7.0", + "output": "filtered/update.out" + }, + { + "args": "-A print_file.ts ./package.json", + "output": "filtered/package.json.out" + } + ] + } + } +} diff --git a/tests/specs/update/package_json/deno.lock b/tests/specs/update/package_json/deno.lock new file mode 100644 index 0000000000..05253094db --- /dev/null +++ b/tests/specs/update/package_json/deno.lock @@ -0,0 +1,28 @@ +{ + "version": "4", + "specifiers": { + "npm:@denotest/bin@0.6": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@0.1": "0.1.0" + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.0": { + "integrity": "sha512-H/MBo0jKDdMsX4AAGEGQbZj70nfNe3oUNZXbohYHhqf9EfpLnXp/7FC29ZdfV4+p6VjEcOGdCtXc6rilE6iYpg==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@denotest/bin@0.6", + "npm:@denotest/breaking-change-between-versions@1.0.0", + "npm:@denotest/has-patch-versions@0.1" + ] + } + } +} diff --git a/tests/specs/update/package_json/deno.lock.orig.out b/tests/specs/update/package_json/deno.lock.orig.out new file mode 100644 index 0000000000..05253094db --- /dev/null +++ b/tests/specs/update/package_json/deno.lock.orig.out @@ -0,0 +1,28 @@ +{ + "version": "4", + "specifiers": { + "npm:@denotest/bin@0.6": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@0.1": "0.1.0" + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.0": { + "integrity": "sha512-H/MBo0jKDdMsX4AAGEGQbZj70nfNe3oUNZXbohYHhqf9EfpLnXp/7FC29ZdfV4+p6VjEcOGdCtXc6rilE6iYpg==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@denotest/bin@0.6", + "npm:@denotest/breaking-change-between-versions@1.0.0", + "npm:@denotest/has-patch-versions@0.1" + ] + } + } +} diff --git a/tests/specs/update/package_json/filtered/package.json.out b/tests/specs/update/package_json/filtered/package.json.out new file mode 100644 index 0000000000..39faff22d6 --- /dev/null +++ b/tests/specs/update/package_json/filtered/package.json.out @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@denotest/has-patch-versions": "^0.2.0", + "@denotest/breaking-change-between-versions": "1.0.0" + }, + "devDependencies": { + "aliased": "npm:@denotest/bin@0.7.0" + } +} diff --git a/tests/specs/update/package_json/filtered/update.out b/tests/specs/update/package_json/filtered/update.out new file mode 100644 index 0000000000..3093281f10 --- /dev/null +++ b/tests/specs/update/package_json/filtered/update.out @@ -0,0 +1,9 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/bin/0.7.0.tgz +Download http://localhost:4260/@denotest/has-patch-versions/0.2.0.tgz +Initialize @denotest/has-patch-versions@0.2.0 +Initialize @denotest/bin@0.7.0 +[UNORDERED_END] +Updated 2 dependencies: + - npm:@denotest/bin 0.6.0 -> 0.7.0 + - npm:@denotest/has-patch-versions 0.1.0 -> 0.2.0 diff --git a/tests/specs/update/package_json/outdated.out b/tests/specs/update/package_json/outdated.out new file mode 100644 index 0000000000..d672aace7f --- /dev/null +++ b/tests/specs/update/package_json/outdated.out @@ -0,0 +1,9 @@ +┌────────────────────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.1 │ 0.2.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/bin │ 0.6.0 │ 0.6.0 │ 1.0.0 │ +├────────────────────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/breaking-change-between-versions │ 1.0.0 │ 1.0.0 │ 2.0.0 │ +└────────────────────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/package_json/outdated_compatible.out b/tests/specs/update/package_json/outdated_compatible.out new file mode 100644 index 0000000000..8a682c461c --- /dev/null +++ b/tests/specs/update/package_json/outdated_compatible.out @@ -0,0 +1,5 @@ +┌──────────────────────────────────┬─────────┬────────┬────────┐ +│ Package │ Current │ Update │ Latest │ +├──────────────────────────────────┼─────────┼────────┼────────┤ +│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.1 │ 0.2.0 │ +└──────────────────────────────────┴─────────┴────────┴────────┘ diff --git a/tests/specs/update/package_json/package.json b/tests/specs/update/package_json/package.json new file mode 100644 index 0000000000..9cd8cf59dc --- /dev/null +++ b/tests/specs/update/package_json/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@denotest/has-patch-versions": "^0.1.0", + "@denotest/breaking-change-between-versions": "1.0.0" + }, + "devDependencies": { + "aliased": "npm:@denotest/bin@^0.6.0" + } +} diff --git a/tests/specs/update/package_json/print_file.ts b/tests/specs/update/package_json/print_file.ts new file mode 100644 index 0000000000..c57b6c3a4e --- /dev/null +++ b/tests/specs/update/package_json/print_file.ts @@ -0,0 +1,2 @@ +const file = Deno.args[0]; +console.log(Deno.readTextFileSync(file).trim()); diff --git a/tests/specs/update/package_json/update_compatible/deno.lock.out b/tests/specs/update/package_json/update_compatible/deno.lock.out new file mode 100644 index 0000000000..f82a21ee46 --- /dev/null +++ b/tests/specs/update/package_json/update_compatible/deno.lock.out @@ -0,0 +1,28 @@ +{ + "version": "4", + "specifiers": { + "npm:@denotest/bin@0.6": "0.6.0", + "npm:@denotest/breaking-change-between-versions@1.0.0": "1.0.0", + "npm:@denotest/has-patch-versions@~0.1.1": "0.1.1" + }, + "npm": { + "@denotest/bin@0.6.0": { + "integrity": "sha512-vCNpxFgQN4fw4ZOp63nbTX1ilcDqNpvXCvYyC8nmfxQQAezsEt095I/YXwMIoMGzWtjCvlMf9kVEYfLuT5oEGQ==" + }, + "@denotest/breaking-change-between-versions@1.0.0": { + "integrity": "sha512-bzMGYx+DxxPlI74n/VsDAN7Db1BY7Sz2XqxXruMo9dEznsBZu7Ez3i8YQ8n0leTxAiiMk1RCG4zQHPG1aj3xRw==" + }, + "@denotest/has-patch-versions@0.1.1": { + "integrity": "sha512-slUqYhu6DrPiSdNzmW5aMdW2/osIYnDP0yY3CwgLzAiyK0/cwb0epSpTSyZEmTKXA3rezxxC7ASSsnD34uH1/w==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@denotest/bin@0.6", + "npm:@denotest/breaking-change-between-versions@1.0.0", + "npm:@denotest/has-patch-versions@~0.1.1" + ] + } + } +} diff --git a/tests/specs/update/package_json/update_compatible/package.json.out b/tests/specs/update/package_json/update_compatible/package.json.out new file mode 100644 index 0000000000..6f822ee5d6 --- /dev/null +++ b/tests/specs/update/package_json/update_compatible/package.json.out @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@denotest/has-patch-versions": "^0.1.1", + "@denotest/breaking-change-between-versions": "1.0.0" + }, + "devDependencies": { + "aliased": "npm:@denotest/bin@^0.6.0" + } +} diff --git a/tests/specs/update/package_json/update_compatible/update.out b/tests/specs/update/package_json/update_compatible/update.out new file mode 100644 index 0000000000..4c3c740791 --- /dev/null +++ b/tests/specs/update/package_json/update_compatible/update.out @@ -0,0 +1,4 @@ +Download http://localhost:4260/@denotest/has-patch-versions/0.1.1.tgz +Initialize @denotest/has-patch-versions@0.1.1 +Updated 1 dependency: + - npm:@denotest/has-patch-versions 0.1.0 -> 0.1.1 diff --git a/tests/specs/update/package_json/update_latest/deno.lock.out b/tests/specs/update/package_json/update_latest/deno.lock.out new file mode 100644 index 0000000000..9a9b1bad5e --- /dev/null +++ b/tests/specs/update/package_json/update_latest/deno.lock.out @@ -0,0 +1,28 @@ +{ + "version": "4", + "specifiers": { + "npm:@denotest/bin@1": "1.0.0", + "npm:@denotest/breaking-change-between-versions@2": "2.0.0", + "npm:@denotest/has-patch-versions@0.2": "0.2.0" + }, + "npm": { + "@denotest/bin@1.0.0": { + "integrity": "sha512-ZtrWnYYPIzw4a9H1uNeZRZRWuLCpHZZU/SllIyFLqcTLH/3zdRI8UH4Z1Kf+8N++bWGO3fg8Ev4vvS1LoLlidg==" + }, + "@denotest/breaking-change-between-versions@2.0.0": { + "integrity": "sha512-3eQpPhhJYbSHaAmpgyVT8IM3+MkxcAQl90Uw8zmuTiFs64Wt3HGzSz74cwPlvfqqesRktm8fBZMmrtxVo3ENzw==" + }, + "@denotest/has-patch-versions@0.2.0": { + "integrity": "sha512-7XIVGrBMyqnts5/wcc7dn7rVl4IWrCiGUT2GLDSLWnogOMIZBapJJLW9n8Leq1bDTJ3U6aDTEFKz9fVSOwZfLQ==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@denotest/bin@1", + "npm:@denotest/breaking-change-between-versions@2", + "npm:@denotest/has-patch-versions@0.2" + ] + } + } +} diff --git a/tests/specs/update/package_json/update_latest/package.json.out b/tests/specs/update/package_json/update_latest/package.json.out new file mode 100644 index 0000000000..fb483d78bd --- /dev/null +++ b/tests/specs/update/package_json/update_latest/package.json.out @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@denotest/has-patch-versions": "^0.2.0", + "@denotest/breaking-change-between-versions": "^2.0.0" + }, + "devDependencies": { + "aliased": "npm:@denotest/bin@^1.0.0" + } +} diff --git a/tests/specs/update/package_json/update_latest/update.out b/tests/specs/update/package_json/update_latest/update.out new file mode 100644 index 0000000000..a24ae1e32b --- /dev/null +++ b/tests/specs/update/package_json/update_latest/update.out @@ -0,0 +1,14 @@ +[UNORDERED_START] +Download http://localhost:4260/@denotest/bin/1.0.0.tgz +Download http://localhost:4260/@denotest/has-patch-versions/0.2.0.tgz +Download http://localhost:4260/@denotest/breaking-change-between-versions/2.0.0.tgz +Initialize @denotest/has-patch-versions@0.2.0 +Initialize @denotest/breaking-change-between-versions@2.0.0 +Initialize @denotest/bin@1.0.0 +[UNORDERED_END] +Updated 3 dependencies: +[UNORDERED_START] + - npm:@denotest/bin 0.6.0 -> 1.0.0 + - npm:@denotest/breaking-change-between-versions 1.0.0 -> 2.0.0 + - npm:@denotest/has-patch-versions 0.1.0 -> 0.2.0 +[UNORDERED_END] diff --git a/tests/util/server/src/npm.rs b/tests/util/server/src/npm.rs index 31686fa854..081989ddb5 100644 --- a/tests/util/server/src/npm.rs +++ b/tests/util/server/src/npm.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fs; +use std::path::Path; use anyhow::Context; use anyhow::Result; @@ -189,6 +190,60 @@ impl TestNpmRegistry { } } +// NOTE: extracted out partially from the `tar` crate, all credits to the original authors +fn append_dir_all( + builder: &mut tar::Builder, + path: &Path, + src_path: &Path, +) -> Result<()> { + builder.follow_symlinks(true); + let mode = tar::HeaderMode::Deterministic; + builder.mode(mode); + let mut stack = vec![(src_path.to_path_buf(), true, false)]; + let mut entries = Vec::new(); + while let Some((src, is_dir, is_symlink)) = stack.pop() { + let dest = path.join(src.strip_prefix(src_path).unwrap()); + // In case of a symlink pointing to a directory, is_dir is false, but src.is_dir() will return true + if is_dir || (is_symlink && src.is_dir()) { + for entry in fs::read_dir(&src)? { + let entry = entry?; + let file_type = entry.file_type()?; + stack.push((entry.path(), file_type.is_dir(), file_type.is_symlink())); + } + if dest != Path::new("") { + entries.push((src, dest)); + } + } else { + entries.push((src, dest)); + } + } + entries.sort_by(|(_, a), (_, b)| a.cmp(b)); + for (src, dest) in entries { + let mut header = tar::Header::new_gnu(); + let metadata = src.metadata().with_context(|| { + format!("trying to get metadata for {}", src.display()) + })?; + header.set_metadata_in_mode(&metadata, mode); + // this is what `tar` sets the mtime to on unix in deterministic mode, on windows it uses a different + // value, which causes the tarball to have a different hash on windows. force it to be the same + // to ensure the same output on all platforms + header.set_mtime(1153704088); + + let data = if src.is_file() { + Box::new( + fs::File::open(&src) + .with_context(|| format!("trying to open file {}", src.display()))?, + ) as Box + } else { + Box::new(std::io::empty()) as Box + }; + builder + .append_data(&mut header, dest, data) + .with_context(|| "appending data")?; + } + Ok(()) +} + fn get_npm_package( registry_hostname: &str, local_path: &str, @@ -228,11 +283,14 @@ fn get_npm_package( GzEncoder::new(&mut tarball_bytes, Compression::default()); { let mut builder = Builder::new(&mut encoder); - builder - .append_dir_all("package", &version_folder) - .with_context(|| { - format!("Error adding tarball for directory: {}", version_folder) - })?; + append_dir_all( + &mut builder, + Path::new("package"), + version_folder.as_path(), + ) + .with_context(|| { + format!("Error adding tarball for directory {}", version_folder,) + })?; builder.finish()?; } encoder.finish()?;