Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: subcommand to view and update outdated dependencies #26942

Merged
merged 26 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions cli/args/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,13 +463,27 @@ pub enum DenoSubcommand {
Serve(ServeFlags),
Task(TaskFlags),
Test(TestFlags),
Outdated(OutdatedFlags),
Types,
Upgrade(UpgradeFlags),
Vendor,
Publish(PublishFlags),
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<String>,
pub recursive: bool,
pub kind: OutdatedKind,
}

impl DenoSubcommand {
pub fn is_run(&self) -> bool {
matches!(self, Self::Run(_))
Expand Down Expand Up @@ -1200,6 +1214,7 @@ static DENO_HELP: &str = cstr!(
<p(245)>deno add jsr:@std/assert | deno add npm:express</>
<g>install</> Installs dependencies either in the local project or globally to a bin directory
<g>uninstall</> Uninstalls a dependency or an executable script in the installation root's bin directory
<g>outdated</> Find and update outdated dependencies
<g>remove</> Remove dependencies from the configuration file

<y>Tooling:</>
Expand Down Expand Up @@ -1382,6 +1397,7 @@ pub fn flags_from_vec(args: Vec<OsString>) -> clap::error::Result<Flags> {
"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)?,
Expand Down Expand Up @@ -1624,6 +1640,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())
Expand Down Expand Up @@ -2614,6 +2631,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:
<p(245)>deno outdated</>
<p(245)>deno outdated --compatible</>

Update dependencies:
<p(245)>deno outdated --update</>
<p(245)>deno outdated --update --latest</>
<p(245)>deno outdated --update</>

Filters can be used to select which packages to act on. Filters can include wildcards (*) to match multiple packages.
<p(245)>deno outdated --update --latest \"@std/*\"</>
<p(245)>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.
<p(245)>deno outdated --update --latest foobar</>
Filters can be combined, and negative filters can be used to exclude results:
<p(245)>deno outdated --update --latest \"@std/*\" \"!@std/fmt*\"</>

Specific version requirements to update to can be specified:
<p(245)>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)
nathanwhit marked this conversation as resolved.
Show resolved Hide resolved
.help("include all workspace members"),
)
})
}

fn uninstall_subcommand() -> Command {
command(
"uninstall",
Expand Down Expand Up @@ -4329,6 +4423,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::<String>("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(())
}

nathanwhit marked this conversation as resolved.
Show resolved Hide resolved
fn bench_parse(
flags: &mut Flags,
matches: &mut ArgMatches,
Expand Down Expand Up @@ -11227,4 +11346,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
);
}
}
}
1 change: 1 addition & 0 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,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;
Expand Down
5 changes: 5 additions & 0 deletions cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
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 })
}
Expand Down
2 changes: 1 addition & 1 deletion cli/npm/managed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NpmPackageId, PackageReqNotFoundError> {
Expand Down
1 change: 1 addition & 0 deletions cli/tools/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 29 additions & 3 deletions cli/tools/registry/pm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,10 +33,13 @@ use crate::jsr::JsrFetchResolver;
use crate::npm::NpmFetchResolver;

mod cache_deps;
pub(crate) mod deps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, there's not much point to pub(crate) in the CLI crate. I remove it whenever I see it.

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,
Expand Down Expand Up @@ -86,6 +90,28 @@ impl ConfigUpdater {
self.cst.to_string()
}

fn get_property_for_mutation(
&mut self,
key_path: &KeyPath,
) -> Option<CstObjectProp> {
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
Expand Down Expand Up @@ -824,7 +850,7 @@ async fn npm_install_after_modification(
flags: Arc<Flags>,
// explicitly provided to prevent redownloading
jsr_resolver: Option<Arc<crate::jsr::JsrFetchResolver>>,
) -> Result<(), AnyError> {
) -> Result<CliFactory, AnyError> {
// clear the previously cached package.json from memory before reloading it
node_resolver::PackageJsonThreadLocalCache::clear();

Expand All @@ -842,7 +868,7 @@ async fn npm_install_after_modification(
lockfile.write_if_changed()?;
}

Ok(())
Ok(cli_factory)
}

#[cfg(test)]
Expand Down
17 changes: 11 additions & 6 deletions cli/tools/registry/pm/cache_deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
Loading