1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

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/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 <biwanczuk@gmail.com>
This commit is contained in:
Nathan Whitaker 2024-11-20 15:22:15 -08:00 committed by GitHub
parent 0670206a2c
commit 56f31628f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 3109 additions and 18 deletions

View file

@ -465,6 +465,7 @@ pub enum DenoSubcommand {
Serve(ServeFlags), Serve(ServeFlags),
Task(TaskFlags), Task(TaskFlags),
Test(TestFlags), Test(TestFlags),
Outdated(OutdatedFlags),
Types, Types,
Upgrade(UpgradeFlags), Upgrade(UpgradeFlags),
Vendor, Vendor,
@ -472,6 +473,19 @@ pub enum DenoSubcommand {
Help(HelpFlags), 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 { impl DenoSubcommand {
pub fn is_run(&self) -> bool { pub fn is_run(&self) -> bool {
matches!(self, Self::Run(_)) matches!(self, Self::Run(_))
@ -1203,6 +1217,7 @@ static DENO_HELP: &str = cstr!(
<p(245)>deno add jsr:@std/assert | deno add npm:express</> <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>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>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 <g>remove</> Remove dependencies from the configuration file
<y>Tooling:</> <y>Tooling:</>
@ -1385,6 +1400,7 @@ pub fn flags_from_vec(args: Vec<OsString>) -> clap::error::Result<Flags> {
"jupyter" => jupyter_parse(&mut flags, &mut m), "jupyter" => jupyter_parse(&mut flags, &mut m),
"lint" => lint_parse(&mut flags, &mut m)?, "lint" => lint_parse(&mut flags, &mut m)?,
"lsp" => lsp_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)?, "repl" => repl_parse(&mut flags, &mut m)?,
"run" => run_parse(&mut flags, &mut m, app, false)?, "run" => run_parse(&mut flags, &mut m, app, false)?,
"serve" => serve_parse(&mut flags, &mut m, app)?, "serve" => serve_parse(&mut flags, &mut m, app)?,
@ -1627,6 +1643,7 @@ pub fn clap_root() -> Command {
.subcommand(json_reference_subcommand()) .subcommand(json_reference_subcommand())
.subcommand(jupyter_subcommand()) .subcommand(jupyter_subcommand())
.subcommand(uninstall_subcommand()) .subcommand(uninstall_subcommand())
.subcommand(outdated_subcommand())
.subcommand(lsp_subcommand()) .subcommand(lsp_subcommand())
.subcommand(lint_subcommand()) .subcommand(lint_subcommand())
.subcommand(publish_subcommand()) .subcommand(publish_subcommand())
@ -2617,6 +2634,83 @@ fn jupyter_subcommand() -> Command {
.conflicts_with("install")) .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)
.help("include all workspace members"),
)
})
}
fn uninstall_subcommand() -> Command { fn uninstall_subcommand() -> Command {
command( command(
"uninstall", "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::<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(())
}
fn bench_parse( fn bench_parse(
flags: &mut Flags, flags: &mut Flags,
matches: &mut ArgMatches, matches: &mut ArgMatches,
@ -11299,4 +11418,77 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
assert!(r.is_err()); 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
);
}
}
} }

View file

@ -1628,6 +1628,7 @@ impl CliOptions {
DenoSubcommand::Install(_) DenoSubcommand::Install(_)
| DenoSubcommand::Add(_) | DenoSubcommand::Add(_)
| DenoSubcommand::Remove(_) | DenoSubcommand::Remove(_)
| DenoSubcommand::Outdated(_)
) { ) {
// For `deno install/add/remove` we want to force the managed resolver so it can set up `node_modules/` directory. // For `deno install/add/remove` we want to force the managed resolver so it can set up `node_modules/` directory.
return false; return false;

View file

@ -188,6 +188,11 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
tools::lint::lint(flags, lint_flags).await 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) => { DenoSubcommand::Repl(repl_flags) => {
spawn_subcommand(async move { tools::repl::run(flags, repl_flags).await }) spawn_subcommand(async move { tools::repl::run(flags, repl_flags).await })
} }

View file

@ -500,7 +500,7 @@ impl ManagedCliNpmResolver {
self.resolve_pkg_folder_from_pkg_id(&pkg_id) 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, &self,
req: &PackageReq, req: &PackageReq,
) -> Result<NpmPackageId, PackageReqNotFoundError> { ) -> Result<NpmPackageId, PackageReqNotFoundError> {

View file

@ -68,6 +68,7 @@ use auth::get_auth_method;
use auth::AuthMethod; use auth::AuthMethod;
pub use pm::add; pub use pm::add;
pub use pm::cache_top_level_deps; pub use pm::cache_top_level_deps;
pub use pm::outdated;
pub use pm::remove; pub use pm::remove;
pub use pm::AddCommandName; pub use pm::AddCommandName;
pub use pm::AddRmPackageReq; pub use pm::AddRmPackageReq;

View file

@ -16,6 +16,7 @@ use deno_semver::package::PackageNv;
use deno_semver::package::PackageReq; use deno_semver::package::PackageReq;
use deno_semver::Version; use deno_semver::Version;
use deno_semver::VersionReq; use deno_semver::VersionReq;
use deps::KeyPath;
use jsonc_parser::cst::CstObject; use jsonc_parser::cst::CstObject;
use jsonc_parser::cst::CstObjectProp; use jsonc_parser::cst::CstObjectProp;
use jsonc_parser::cst::CstRootNode; use jsonc_parser::cst::CstRootNode;
@ -32,10 +33,13 @@ use crate::jsr::JsrFetchResolver;
use crate::npm::NpmFetchResolver; use crate::npm::NpmFetchResolver;
mod cache_deps; mod cache_deps;
pub(crate) mod deps;
mod outdated;
pub use cache_deps::cache_top_level_deps; pub use cache_deps::cache_top_level_deps;
pub use outdated::outdated;
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone, Hash)]
enum ConfigKind { enum ConfigKind {
DenoJson, DenoJson,
PackageJson, PackageJson,
@ -86,6 +90,28 @@ impl ConfigUpdater {
self.cst.to_string() 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 add(&mut self, selected: SelectedPackage, dev: bool) {
fn insert_index(object: &CstObject, searching_name: &str) -> usize { fn insert_index(object: &CstObject, searching_name: &str) -> usize {
object object
@ -824,7 +850,7 @@ async fn npm_install_after_modification(
flags: Arc<Flags>, flags: Arc<Flags>,
// explicitly provided to prevent redownloading // explicitly provided to prevent redownloading
jsr_resolver: Option<Arc<crate::jsr::JsrFetchResolver>>, jsr_resolver: Option<Arc<crate::jsr::JsrFetchResolver>>,
) -> Result<(), AnyError> { ) -> Result<CliFactory, AnyError> {
// clear the previously cached package.json from memory before reloading it // clear the previously cached package.json from memory before reloading it
node_resolver::PackageJsonThreadLocalCache::clear(); node_resolver::PackageJsonThreadLocalCache::clear();
@ -842,7 +868,7 @@ async fn npm_install_after_modification(
lockfile.write_if_changed()?; lockfile.write_if_changed()?;
} }
Ok(()) Ok(cli_factory)
} }
#[cfg(test)] #[cfg(test)]

View file

@ -8,7 +8,7 @@ use crate::graph_container::ModuleGraphUpdatePermit;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::futures::stream::FuturesUnordered; use deno_core::futures::stream::FuturesUnordered;
use deno_core::futures::StreamExt; use deno_core::futures::StreamExt;
use deno_semver::package::PackageReq; use deno_semver::jsr::JsrPackageReqReference;
pub async fn cache_top_level_deps( pub async fn cache_top_level_deps(
// todo(dsherret): don't pass the factory into this function. Instead use ctor 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() { match specifier.scheme() {
"jsr" => { "jsr" => {
let specifier_str = specifier.as_str(); let specifier_str = specifier.as_str();
let specifier_str = if let Ok(req) = JsrPackageReqReference::from_str(specifier_str) {
specifier_str.strip_prefix("jsr:").unwrap_or(specifier_str); if let Some(sub_path) = req.sub_path() {
if let Ok(req) = PackageReq::from_str(specifier_str) { if sub_path.ends_with('/') {
if !seen_reqs.insert(req.clone()) { continue;
}
roots.push(specifier.clone());
continue;
}
if !seen_reqs.insert(req.req().clone()) {
continue; continue;
} }
let jsr_resolver = jsr_resolver.clone(); let jsr_resolver = jsr_resolver.clone();
info_futures.push(async move { 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 if let Some(info) = jsr_resolver.package_version_info(&nv).await
{ {
return Some((specifier.clone(), info)); return Some((specifier.clone(), info));

View file

@ -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<std::path::Path> {
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>(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<String> for KeyPart {
fn from(value: String) -> Self {
KeyPart::String(value)
}
}
impl From<PackageJsonDepKind> 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<KeyPart>,
}
impl KeyPath {
fn from_parts(parts: impl IntoIterator<Item = KeyPart>) -> 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<String>,
}
fn import_map_entries(
import_map: &ImportMap,
) -> impl Iterator<Item = (KeyPath, SpecifierMapEntry<'_>)> {
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<Option<(ImportMapWithDiagnostics, ImportMapKind)>, 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<PackageJsonDepValue, PackageJsonDepValueParseError> {
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<String, String>>,
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<ConfigFile>,
mut filter: impl DepFilter,
deps: &mut Vec<Dep>,
) {
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<Dep>,
) {
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<Workspace>,
dep_filter: impl DepFilter,
) -> Result<Vec<Dep>, 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<T> 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<PackageNv>,
pub latest: Option<PackageNv>,
}
pub struct DepManager {
deps: Vec<Dep>,
resolved_versions: Vec<Option<PackageNv>>,
latest_versions: Vec<PackageLatestVersion>,
pending_changes: Vec<Change>,
dependencies_resolved: AtomicBool,
module_load_preparer: Arc<ModuleLoadPreparer>,
// TODO(nathanwhit): probably shouldn't be pub
pub(crate) jsr_fetch_resolver: Arc<JsrFetchResolver>,
pub(crate) npm_fetch_resolver: Arc<NpmFetchResolver>,
npm_resolver: Arc<dyn CliNpmResolver>,
permissions_container: PermissionsContainer,
main_module_graph_container: Arc<MainModuleGraphContainer>,
lockfile: Option<Arc<CliLockfile>>,
}
pub struct DepManagerArgs {
pub module_load_preparer: Arc<ModuleLoadPreparer>,
pub jsr_fetch_resolver: Arc<JsrFetchResolver>,
pub npm_fetch_resolver: Arc<NpmFetchResolver>,
pub npm_resolver: Arc<dyn CliNpmResolver>,
pub permissions_container: PermissionsContainer,
pub main_module_graph_container: Arc<MainModuleGraphContainer>,
pub lockfile: Option<Arc<CliLockfile>>,
}
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<Dep>, 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<WorkspaceDirectory>,
dep_filter: impl DepFilter,
args: DepManagerArgs,
) -> Result<Self, AnyError> {
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<Workspace>,
dep_filter: impl DepFilter,
args: DepManagerArgs,
) -> Result<Self, AnyError> {
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<Vec<PackageLatestVersion>, 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<Item = (DepId, Option<PackageNv>, 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<std::path::PathBuf, ConfigUpdater>,
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<String> {
// 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(),
})
}

View file

@ -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!(
"│ {:<package_column_width$} ",
format!("{}:{}", package.kind.scheme(), package.name),
package_column_width = package_column_width - 2
);
print!(
"│ {:<current_column_width$} ",
package.current,
current_column_width = current_column_width - 2
);
print!(
"│ {:<update_column_width$} ",
package.semver_compatible,
update_column_width = update_column_width - 2
);
println!(
"│ {:<latest_column_width$} │",
package.latest,
latest_column_width = latest_column_width - 2
);
}
println!("{package_fill}{current_fill}{update_fill}{latest_fill}",);
}
fn print_outdated(
deps: &mut DepManager,
compatible: bool,
) -> 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<Flags>,
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<VersionReq> {
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<Flags>,
) -> Result<(), AnyError> {
let mut updated = Vec::new();
for (dep_id, resolved, latest_versions) in deps
.deps_with_resolved_latest_versions()
.into_iter()
.collect::<Vec<_>>()
{
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::<Vec<_>>();
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(&current_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<NpmFetchResolver>,
jsr_fetch_resolver: Arc<JsrFetchResolver>,
) -> Result<DepManagerArgs, AnyError> {
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<VersionReq>,
}
fn pattern_to_regex(pattern: &str) -> Result<regex::Regex, AnyError> {
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<Self, AnyError> {
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<Filter>,
has_exclude: bool,
has_include: bool,
}
impl FilterSet {
pub fn from_filter_strings<'a>(
filter_strings: impl IntoIterator<Item = &'a str>,
) -> Result<Self, AnyError> {
let filters = filter_strings
.into_iter()
.map(Filter::from_str)
.collect::<Result<Vec<_>, _>>()?;
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<Item = &'a str>,
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"))
);
}
}
}

View file

@ -0,0 +1,4 @@
// This is renamed to `add()` in 1.0.0.
export function sum(a: number, b: number): number {
return a + b;
}

View file

@ -0,0 +1,8 @@
{
"exports": {
".": "./mod.ts"
},
"moduleGraph1": {
"/mod.ts": {}
}
}

View file

@ -4,6 +4,7 @@
"yanked": true "yanked": true
}, },
"1.0.0": {}, "1.0.0": {},
"0.2.0": {} "0.2.0": {},
"0.2.1": {}
} }
} }

View file

@ -0,0 +1 @@
export * from "jsr:@denotest/add@1";

View file

@ -0,0 +1,3 @@
{
"a": 1
}

View file

@ -0,0 +1 @@
export * from "jsr:@denotest/subtract@1";

View file

@ -0,0 +1,7 @@
{
"exports": {
"./add": "./add.ts",
"./subtract": "./subtract.ts",
"./data-json": "./data.json"
}
}

View file

@ -0,0 +1 @@
export * from "jsr:@denotest/add@1";

View file

@ -0,0 +1,3 @@
{
"a": 1
}

View file

@ -0,0 +1 @@
export * from "jsr:@denotest/subtract@1";

View file

@ -0,0 +1,7 @@
{
"exports": {
"./add": "./add.ts",
"./subtract": "./subtract.ts",
"./data-json": "./data.json"
}
}

View file

@ -1,5 +1,7 @@
{ {
"versions": { "versions": {
"1.0.0": {} "1.0.0": {},
"0.5.0": {},
"0.2.0": {}
} }
} }

View file

@ -0,0 +1,3 @@
export function sub(a: number, b: number): number {
return a - b;
}

View file

@ -0,0 +1,8 @@
{
"exports": {
".": "./mod.ts"
},
"moduleGraph1": {
"/mod.ts": {}
}
}

View file

@ -1,5 +1,7 @@
{ {
"latest": "1.0.0",
"versions": { "versions": {
"1.0.0": {} "1.0.0": {},
"0.2.0": {}
} }
} }

View file

@ -0,0 +1,5 @@
{
"name": "@denotest/has-patch-versions",
"version": "0.1.0"
}

View file

@ -0,0 +1,5 @@
{
"name": "@denotest/has-patch-versions",
"version": "0.1.1"
}

View file

@ -0,0 +1,4 @@
{
"name": "@denotest/has-patch-versions",
"version": "0.2.0"
}

View file

@ -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"
}
]
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
]
}
}

View file

@ -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"
]
}
}

View file

@ -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"
}
}
}

View file

@ -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

View file

@ -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 │
└────────────────────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -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 │
└──────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -0,0 +1,2 @@
const file = Deno.args[0];
console.log(Deno.readTextFileSync(file).trim());

View file

@ -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"
}
}
}

View file

@ -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"
]
}
}

View file

@ -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

View file

@ -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"
}
}
}

View file

@ -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"
]
}
}

View file

@ -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

View file

@ -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"
}
]
}
}
}

View file

@ -0,0 +1,6 @@
{
"workspace": ["./member-a", "./member-b"],
"imports": {
"@denotest/subtract": "jsr:@denotest/subtract@^0.2.0"
}
}

View file

@ -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"
]
}
}
}
}
}

View file

@ -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"
]
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,2 @@
const file = Deno.args[0];
console.log(Deno.readTextFileSync(file).trim());

View file

@ -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 │
└────────────────────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -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 │
└──────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -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 │
└────────────────────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -0,0 +1,5 @@
┌────────────────────────┬─────────┬────────┬────────┐
│ Package │ Current │ Update │ Latest │
├────────────────────────┼─────────┼────────┼────────┤
│ jsr:@denotest/subtract │ 0.2.0 │ 0.2.0 │ 1.0.0 │
└────────────────────────┴─────────┴────────┴────────┘

View file

@ -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

View file

@ -0,0 +1,6 @@
{
"workspace": ["./member-a", "./member-b"],
"imports": {
"@denotest/subtract": "jsr:@denotest/subtract@^1.0.0"
}
}

View file

@ -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

View file

@ -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

View file

@ -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"
}
}

View file

@ -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

View file

@ -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"
}
}

View file

@ -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"
}
]
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -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"
}
}

View file

@ -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

View file

@ -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 │
└────────────────────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -0,0 +1,5 @@
┌──────────────────────────────────┬─────────┬────────┬────────┐
│ Package │ Current │ Update │ Latest │
├──────────────────────────────────┼─────────┼────────┼────────┤
│ npm:@denotest/has-patch-versions │ 0.1.0 │ 0.1.1 │ 0.2.0 │
└──────────────────────────────────┴─────────┴────────┴────────┘

View file

@ -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"
}
}

View file

@ -0,0 +1,2 @@
const file = Deno.args[0];
console.log(Deno.readTextFileSync(file).trim());

View file

@ -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"
]
}
}
}

View file

@ -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"
}
}

View file

@ -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

View file

@ -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"
]
}
}
}

View file

@ -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"
}
}

View file

@ -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]

View file

@ -2,6 +2,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::Path;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; 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<W: std::io::Write>(
builder: &mut tar::Builder<W>,
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<dyn std::io::Read>
} else {
Box::new(std::io::empty()) as Box<dyn std::io::Read>
};
builder
.append_data(&mut header, dest, data)
.with_context(|| "appending data")?;
}
Ok(())
}
fn get_npm_package( fn get_npm_package(
registry_hostname: &str, registry_hostname: &str,
local_path: &str, local_path: &str,
@ -228,10 +283,13 @@ fn get_npm_package(
GzEncoder::new(&mut tarball_bytes, Compression::default()); GzEncoder::new(&mut tarball_bytes, Compression::default());
{ {
let mut builder = Builder::new(&mut encoder); let mut builder = Builder::new(&mut encoder);
builder append_dir_all(
.append_dir_all("package", &version_folder) &mut builder,
Path::new("package"),
version_folder.as_path(),
)
.with_context(|| { .with_context(|| {
format!("Error adding tarball for directory: {}", version_folder) format!("Error adding tarball for directory {}", version_folder,)
})?; })?;
builder.finish()?; builder.finish()?;
} }