Skip to content

Commit 56f3162

Browse files
feat: subcommand to view and update outdated dependencies (#26942)
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/[email protected] @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 <[email protected]>
1 parent 0670206 commit 56f3162

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+3109
-18
lines changed

cli/args/flags.rs

+192
Original file line numberDiff line numberDiff line change
@@ -465,13 +465,27 @@ pub enum DenoSubcommand {
465465
Serve(ServeFlags),
466466
Task(TaskFlags),
467467
Test(TestFlags),
468+
Outdated(OutdatedFlags),
468469
Types,
469470
Upgrade(UpgradeFlags),
470471
Vendor,
471472
Publish(PublishFlags),
472473
Help(HelpFlags),
473474
}
474475

476+
#[derive(Clone, Debug, PartialEq, Eq)]
477+
pub enum OutdatedKind {
478+
Update { latest: bool },
479+
PrintOutdated { compatible: bool },
480+
}
481+
482+
#[derive(Clone, Debug, PartialEq, Eq)]
483+
pub struct OutdatedFlags {
484+
pub filters: Vec<String>,
485+
pub recursive: bool,
486+
pub kind: OutdatedKind,
487+
}
488+
475489
impl DenoSubcommand {
476490
pub fn is_run(&self) -> bool {
477491
matches!(self, Self::Run(_))
@@ -1203,6 +1217,7 @@ static DENO_HELP: &str = cstr!(
12031217
<p(245)>deno add jsr:@std/assert | deno add npm:express</>
12041218
<g>install</> Installs dependencies either in the local project or globally to a bin directory
12051219
<g>uninstall</> Uninstalls a dependency or an executable script in the installation root's bin directory
1220+
<g>outdated</> Find and update outdated dependencies
12061221
<g>remove</> Remove dependencies from the configuration file
12071222
12081223
<y>Tooling:</>
@@ -1385,6 +1400,7 @@ pub fn flags_from_vec(args: Vec<OsString>) -> clap::error::Result<Flags> {
13851400
"jupyter" => jupyter_parse(&mut flags, &mut m),
13861401
"lint" => lint_parse(&mut flags, &mut m)?,
13871402
"lsp" => lsp_parse(&mut flags, &mut m),
1403+
"outdated" => outdated_parse(&mut flags, &mut m)?,
13881404
"repl" => repl_parse(&mut flags, &mut m)?,
13891405
"run" => run_parse(&mut flags, &mut m, app, false)?,
13901406
"serve" => serve_parse(&mut flags, &mut m, app)?,
@@ -1627,6 +1643,7 @@ pub fn clap_root() -> Command {
16271643
.subcommand(json_reference_subcommand())
16281644
.subcommand(jupyter_subcommand())
16291645
.subcommand(uninstall_subcommand())
1646+
.subcommand(outdated_subcommand())
16301647
.subcommand(lsp_subcommand())
16311648
.subcommand(lint_subcommand())
16321649
.subcommand(publish_subcommand())
@@ -2617,6 +2634,83 @@ fn jupyter_subcommand() -> Command {
26172634
.conflicts_with("install"))
26182635
}
26192636

2637+
fn outdated_subcommand() -> Command {
2638+
command(
2639+
"outdated",
2640+
cstr!("Find and update outdated dependencies.
2641+
By default, outdated dependencies are only displayed.
2642+
2643+
Display outdated dependencies:
2644+
<p(245)>deno outdated</>
2645+
<p(245)>deno outdated --compatible</>
2646+
2647+
Update dependencies:
2648+
<p(245)>deno outdated --update</>
2649+
<p(245)>deno outdated --update --latest</>
2650+
<p(245)>deno outdated --update</>
2651+
2652+
Filters can be used to select which packages to act on. Filters can include wildcards (*) to match multiple packages.
2653+
<p(245)>deno outdated --update --latest \"@std/*\"</>
2654+
<p(245)>deno outdated --update --latest \"react*\"</>
2655+
Note that filters act on their aliases configured in deno.json / package.json, not the actual package names:
2656+
Given \"foobar\": \"npm:[email protected]\" in deno.json or package.json, the filter \"foobar\" would update npm:react to
2657+
the latest version.
2658+
<p(245)>deno outdated --update --latest foobar</>
2659+
Filters can be combined, and negative filters can be used to exclude results:
2660+
<p(245)>deno outdated --update --latest \"@std/*\" \"!@std/fmt*\"</>
2661+
2662+
Specific version requirements to update to can be specified:
2663+
<p(245)>deno outdated --update @std/fmt@^1.0.2</>
2664+
"),
2665+
UnstableArgsConfig::None,
2666+
)
2667+
.defer(|cmd| {
2668+
cmd
2669+
.arg(
2670+
Arg::new("filters")
2671+
.num_args(0..)
2672+
.action(ArgAction::Append)
2673+
.help(concat!("Filters selecting which packages to act on. Can include wildcards (*) to match multiple packages. ",
2674+
"If a version requirement is specified, the matching packages will be updated to the given requirement."),
2675+
)
2676+
)
2677+
.arg(no_lock_arg())
2678+
.arg(lock_arg())
2679+
.arg(
2680+
Arg::new("latest")
2681+
.long("latest")
2682+
.action(ArgAction::SetTrue)
2683+
.help(
2684+
"Update to the latest version, regardless of semver constraints",
2685+
)
2686+
.requires("update")
2687+
.conflicts_with("compatible"),
2688+
)
2689+
.arg(
2690+
Arg::new("update")
2691+
.long("update")
2692+
.short('u')
2693+
.action(ArgAction::SetTrue)
2694+
.conflicts_with("compatible")
2695+
.help("Update dependency versions"),
2696+
)
2697+
.arg(
2698+
Arg::new("compatible")
2699+
.long("compatible")
2700+
.action(ArgAction::SetTrue)
2701+
.help("Only output versions that satisfy semver requirements")
2702+
.conflicts_with("update"),
2703+
)
2704+
.arg(
2705+
Arg::new("recursive")
2706+
.long("recursive")
2707+
.short('r')
2708+
.action(ArgAction::SetTrue)
2709+
.help("include all workspace members"),
2710+
)
2711+
})
2712+
}
2713+
26202714
fn uninstall_subcommand() -> Command {
26212715
command(
26222716
"uninstall",
@@ -4353,6 +4447,31 @@ fn remove_parse(flags: &mut Flags, matches: &mut ArgMatches) {
43534447
});
43544448
}
43554449

4450+
fn outdated_parse(
4451+
flags: &mut Flags,
4452+
matches: &mut ArgMatches,
4453+
) -> clap::error::Result<()> {
4454+
let filters = match matches.remove_many::<String>("filters") {
4455+
Some(f) => f.collect(),
4456+
None => vec![],
4457+
};
4458+
let recursive = matches.get_flag("recursive");
4459+
let update = matches.get_flag("update");
4460+
let kind = if update {
4461+
let latest = matches.get_flag("latest");
4462+
OutdatedKind::Update { latest }
4463+
} else {
4464+
let compatible = matches.get_flag("compatible");
4465+
OutdatedKind::PrintOutdated { compatible }
4466+
};
4467+
flags.subcommand = DenoSubcommand::Outdated(OutdatedFlags {
4468+
filters,
4469+
recursive,
4470+
kind,
4471+
});
4472+
Ok(())
4473+
}
4474+
43564475
fn bench_parse(
43574476
flags: &mut Flags,
43584477
matches: &mut ArgMatches,
@@ -11299,4 +11418,77 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
1129911418
assert!(r.is_err());
1130011419
}
1130111420
}
11421+
11422+
#[test]
11423+
fn outdated_subcommand() {
11424+
let cases = [
11425+
(
11426+
svec![],
11427+
OutdatedFlags {
11428+
filters: vec![],
11429+
kind: OutdatedKind::PrintOutdated { compatible: false },
11430+
recursive: false,
11431+
},
11432+
),
11433+
(
11434+
svec!["--recursive"],
11435+
OutdatedFlags {
11436+
filters: vec![],
11437+
kind: OutdatedKind::PrintOutdated { compatible: false },
11438+
recursive: true,
11439+
},
11440+
),
11441+
(
11442+
svec!["--recursive", "--compatible"],
11443+
OutdatedFlags {
11444+
filters: vec![],
11445+
kind: OutdatedKind::PrintOutdated { compatible: true },
11446+
recursive: true,
11447+
},
11448+
),
11449+
(
11450+
svec!["--update"],
11451+
OutdatedFlags {
11452+
filters: vec![],
11453+
kind: OutdatedKind::Update { latest: false },
11454+
recursive: false,
11455+
},
11456+
),
11457+
(
11458+
svec!["--update", "--latest"],
11459+
OutdatedFlags {
11460+
filters: vec![],
11461+
kind: OutdatedKind::Update { latest: true },
11462+
recursive: false,
11463+
},
11464+
),
11465+
(
11466+
svec!["--update", "--recursive"],
11467+
OutdatedFlags {
11468+
filters: vec![],
11469+
kind: OutdatedKind::Update { latest: false },
11470+
recursive: true,
11471+
},
11472+
),
11473+
(
11474+
svec!["--update", "@foo/bar"],
11475+
OutdatedFlags {
11476+
filters: svec!["@foo/bar"],
11477+
kind: OutdatedKind::Update { latest: false },
11478+
recursive: false,
11479+
},
11480+
),
11481+
];
11482+
for (input, expected) in cases {
11483+
let mut args = svec!["deno", "outdated"];
11484+
args.extend(input);
11485+
let r = flags_from_vec(args.clone()).unwrap();
11486+
assert_eq!(
11487+
r.subcommand,
11488+
DenoSubcommand::Outdated(expected),
11489+
"incorrect result for args: {:?}",
11490+
args
11491+
);
11492+
}
11493+
}
1130211494
}

cli/args/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,7 @@ impl CliOptions {
16281628
DenoSubcommand::Install(_)
16291629
| DenoSubcommand::Add(_)
16301630
| DenoSubcommand::Remove(_)
1631+
| DenoSubcommand::Outdated(_)
16311632
) {
16321633
// For `deno install/add/remove` we want to force the managed resolver so it can set up `node_modules/` directory.
16331634
return false;

cli/main.rs

+5
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
188188
tools::lint::lint(flags, lint_flags).await
189189
}
190190
}),
191+
DenoSubcommand::Outdated(update_flags) => {
192+
spawn_subcommand(async move {
193+
tools::registry::outdated(flags, update_flags).await
194+
})
195+
}
191196
DenoSubcommand::Repl(repl_flags) => {
192197
spawn_subcommand(async move { tools::repl::run(flags, repl_flags).await })
193198
}

cli/npm/managed/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ impl ManagedCliNpmResolver {
500500
self.resolve_pkg_folder_from_pkg_id(&pkg_id)
501501
}
502502

503-
fn resolve_pkg_id_from_pkg_req(
503+
pub fn resolve_pkg_id_from_pkg_req(
504504
&self,
505505
req: &PackageReq,
506506
) -> Result<NpmPackageId, PackageReqNotFoundError> {

cli/tools/registry/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ use auth::get_auth_method;
6868
use auth::AuthMethod;
6969
pub use pm::add;
7070
pub use pm::cache_top_level_deps;
71+
pub use pm::outdated;
7172
pub use pm::remove;
7273
pub use pm::AddCommandName;
7374
pub use pm::AddRmPackageReq;

cli/tools/registry/pm.rs

+29-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use deno_semver::package::PackageNv;
1616
use deno_semver::package::PackageReq;
1717
use deno_semver::Version;
1818
use deno_semver::VersionReq;
19+
use deps::KeyPath;
1920
use jsonc_parser::cst::CstObject;
2021
use jsonc_parser::cst::CstObjectProp;
2122
use jsonc_parser::cst::CstRootNode;
@@ -32,10 +33,13 @@ use crate::jsr::JsrFetchResolver;
3233
use crate::npm::NpmFetchResolver;
3334

3435
mod cache_deps;
36+
pub(crate) mod deps;
37+
mod outdated;
3538

3639
pub use cache_deps::cache_top_level_deps;
40+
pub use outdated::outdated;
3741

38-
#[derive(Debug, Copy, Clone)]
42+
#[derive(Debug, Copy, Clone, Hash)]
3943
enum ConfigKind {
4044
DenoJson,
4145
PackageJson,
@@ -86,6 +90,28 @@ impl ConfigUpdater {
8690
self.cst.to_string()
8791
}
8892

93+
fn get_property_for_mutation(
94+
&mut self,
95+
key_path: &KeyPath,
96+
) -> Option<CstObjectProp> {
97+
let mut current_node = self.root_object.clone();
98+
99+
self.modified = true;
100+
101+
for (i, part) in key_path.parts.iter().enumerate() {
102+
let s = part.as_str();
103+
if i < key_path.parts.len().saturating_sub(1) {
104+
let object = current_node.object_value(s)?;
105+
current_node = object;
106+
} else {
107+
// last part
108+
return current_node.get(s);
109+
}
110+
}
111+
112+
None
113+
}
114+
89115
fn add(&mut self, selected: SelectedPackage, dev: bool) {
90116
fn insert_index(object: &CstObject, searching_name: &str) -> usize {
91117
object
@@ -824,7 +850,7 @@ async fn npm_install_after_modification(
824850
flags: Arc<Flags>,
825851
// explicitly provided to prevent redownloading
826852
jsr_resolver: Option<Arc<crate::jsr::JsrFetchResolver>>,
827-
) -> Result<(), AnyError> {
853+
) -> Result<CliFactory, AnyError> {
828854
// clear the previously cached package.json from memory before reloading it
829855
node_resolver::PackageJsonThreadLocalCache::clear();
830856

@@ -842,7 +868,7 @@ async fn npm_install_after_modification(
842868
lockfile.write_if_changed()?;
843869
}
844870

845-
Ok(())
871+
Ok(cli_factory)
846872
}
847873

848874
#[cfg(test)]

cli/tools/registry/pm/cache_deps.rs

+11-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::graph_container::ModuleGraphUpdatePermit;
88
use deno_core::error::AnyError;
99
use deno_core::futures::stream::FuturesUnordered;
1010
use deno_core::futures::StreamExt;
11-
use deno_semver::package::PackageReq;
11+
use deno_semver::jsr::JsrPackageReqReference;
1212

1313
pub async fn cache_top_level_deps(
1414
// 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(
5656
match specifier.scheme() {
5757
"jsr" => {
5858
let specifier_str = specifier.as_str();
59-
let specifier_str =
60-
specifier_str.strip_prefix("jsr:").unwrap_or(specifier_str);
61-
if let Ok(req) = PackageReq::from_str(specifier_str) {
62-
if !seen_reqs.insert(req.clone()) {
59+
if let Ok(req) = JsrPackageReqReference::from_str(specifier_str) {
60+
if let Some(sub_path) = req.sub_path() {
61+
if sub_path.ends_with('/') {
62+
continue;
63+
}
64+
roots.push(specifier.clone());
65+
continue;
66+
}
67+
if !seen_reqs.insert(req.req().clone()) {
6368
continue;
6469
}
6570
let jsr_resolver = jsr_resolver.clone();
6671
info_futures.push(async move {
67-
if let Some(nv) = jsr_resolver.req_to_nv(&req).await {
72+
if let Some(nv) = jsr_resolver.req_to_nv(req.req()).await {
6873
if let Some(info) = jsr_resolver.package_version_info(&nv).await
6974
{
7075
return Some((specifier.clone(), info));

0 commit comments

Comments
 (0)