1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-27 16:10:57 -05:00
denoland-deno/cli/tools/registry/pm/deps.rs
Nathan Whitaker 56f31628f7
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>
2024-11-20 15:22:15 -08:00

964 lines
28 KiB
Rust

// 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(),
})
}