/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/pm/deps.rs
1 277 строк
39 KB
Nathan Whitaker
fix(bundle): respect runtime file permissions (#36107)
05 авг 2026, 00:04
Не верифицирован
05 авг 2026, 00:04
3a10142
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::path::PathBuf; 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::Context; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; use deno_core::futures::future::try_join; use deno_core::futures::stream::FuturesOrdered; use deno_core::futures::stream::FuturesUnordered; use deno_core::serde_json; use deno_core::url::Url; use deno_graph::JsrPackageReqNotFoundError; use deno_graph::packages::JsrPackageVersionInfo; use deno_npm::resolution::NpmVersionResolver; use deno_package_json::PackageJsonDepsMap; use deno_package_json::PackageJsonRc; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::StackString; use deno_semver::Version; use deno_semver::VersionReq; use deno_semver::jsr::JsrDepPackageReq; 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 import_map::ImportMap; use import_map::ImportMapWithDiagnostics; use import_map::SpecifierMapEntry; use tokio::sync::Semaphore; use super::ConfigUpdater; 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::CliNpmInstaller; use crate::npm::CliNpmResolver; use crate::npm::NpmFetchResolver; use crate::npm::PackageInfoLoadError; use crate::util::progress_bar::ProgressBar; use crate::util::sync::AtomicFlag; #[derive(Clone, Debug, PartialEq, Eq)] pub enum ImportMapKind { Inline, Outline(PathBuf), } #[derive(Clone)] pub enum DepLocation { DenoJson(ConfigFileRc, KeyPath, ImportMapKind), PackageJson(PackageJsonRc, KeyPath), /// A `catalog`/`catalogs` entry defined in the workspace root config /// (deno.json or package.json). Unlike the other locations the value is a /// bare version requirement (e.g. `^1.0.0`) rather than a full specifier. /// `key_paths` lists candidate paths to the entry; the first one that /// resolves is updated (package.json may declare catalogs at the top level /// or nested under `workspaces`). Catalog { path: PathBuf, kind: super::ConfigKind, key_paths: Vec<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, _, kind) => match kind { ImportMapKind::Inline => { Cow::Owned(arc.specifier.to_file_path().unwrap()) } ImportMapKind::Outline(path) => Cow::Borrowed(path.as_path()), }, DepLocation::PackageJson(arc, _) => Cow::Borrowed(arc.path.as_ref()), DepLocation::Catalog { path, .. } => Cow::Borrowed(path.as_path()), } } fn config_kind(&self) -> super::ConfigKind { match self { DepLocation::DenoJson(_, _, _) => super::ConfigKind::DenoJson, DepLocation::PackageJson(_, _) => super::ConfigKind::PackageJson, DepLocation::Catalog { kind, .. } => *kind, } } } struct DebugAdapter<T>(T); impl std::fmt::Debug for DebugAdapter<&ConfigFileRc> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ConfigFile") .field("specifier", &self.0.specifier) .finish() } } impl std::fmt::Debug for DebugAdapter<&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() } DepLocation::Catalog { path, kind, key_paths, } => f .debug_struct("Catalog") .field("path", path) .field("kind", kind) .field("key_paths", key_paths) .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(StackString), } 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.as_str(), } } } #[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, reason = "not used yet")] pub id: DepId, #[allow(dead_code, reason = "not used yet")] pub alias: Option<String>, } impl Dep { pub fn alias_or_name(&self) -> &str { self.alias.as_deref().unwrap_or_else(|| &self.req.name) } /// Whether this dependency is a development dependency. Only `package.json` /// `devDependencies` are considered dev; `deno.json` imports and `catalog` /// entries have no such distinction and are always treated as regular /// dependencies. pub fn is_dev(&self) -> bool { match &self.location { DepLocation::PackageJson(_, key_path) => { matches!(key_path.parts.first(), Some(KeyPart::DevDependencies)) } DepLocation::DenoJson(..) | DepLocation::Catalog { .. } => false, } } } 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, KeyPart::String(scope.raw_key.into()), ]); scope.imports.entries().map(move |entry| { let mut full_path = path.clone(); full_path.push(KeyPart::String(entry.raw_key.into())); (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 err_context = || { format!( "loading import map at '{}' (from \"importMap\" field in '{}')", path.display(), deno_json.specifier ) }; let text = std::fs::read_to_string(&path).with_context(err_context)?; let value = serde_json::from_str(&text).with_context(err_context)?; (value, ImportMapKind::Outline(path)) } 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, } 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, 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.clone(), ), kind, req, id, alias, }); } } fn add_deps_from_package_json( package_json: &PackageJsonRc, filter: impl DepFilter, deps: &mut Vec<Dep>, ) { let package_json_deps = package_json.resolve_local_package_json_deps(); fn iterate( package_json: &PackageJsonRc, mut filter: impl DepFilter, package_dep_kind: PackageJsonDepKind, package_json_deps: &PackageJsonDepsMap, deps: &mut Vec<Dep>, ) { for (k, v) in package_json_deps { let v = match v { Ok(v) => v, Err(e) => { log::warn!("bad package json dep value: {e}"); continue; } }; match v { deno_package_json::PackageJsonDepValue::File(_) => { // ignore } 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(), KeyPart::String(k.clone()), ]), ), req: req.clone(), alias, }) } deno_package_json::PackageJsonDepValue::Workspace { .. } | deno_package_json::PackageJsonDepValue::Catalog(_) => continue, } } } iterate( package_json, filter, PackageJsonDepKind::Normal, &package_json_deps.dependencies, deps, ); iterate( package_json, filter, PackageJsonDepKind::Dev, &package_json_deps.dev_dependencies, deps, ); } /// Collects dependencies declared in the workspace root `catalog`/`catalogs` /// (referenced from members via the `catalog:` protocol). These live in a /// single place in the root config, so they are gathered once here rather than /// per member reference. fn add_deps_from_catalogs( workspace: &Workspace, mut filter: impl DepFilter, deps: &mut Vec<Dep>, ) { // Mirror `Workspace`'s own resolution: a root package.json catalog takes // precedence over deno.json, and if it's present deno.json catalogs are // ignored entirely. let pkg_json = workspace .root_pkg_json() .filter(|pj| pj.catalog.is_some() || pj.catalogs.is_some()); let (path, kind, catalog, catalogs, nested) = if let Some(pj) = pkg_json { ( pj.path.clone(), super::ConfigKind::PackageJson, pj.catalog.as_ref(), pj.catalogs.as_ref(), // package.json may declare catalogs nested under `workspaces`. true, ) } else if let Some(dj) = workspace.root_deno_json() { let Ok(path) = dj.specifier.to_file_path() else { return; }; ( path, super::ConfigKind::DenoJson, dj.catalog(), dj.catalogs(), false, ) } else { return; }; let mut add_entry = |name: &str, version_req_str: &str, key_paths: Vec<KeyPath>| { let version_req = match VersionReq::parse_from_npm(version_req_str) { Ok(version_req) => version_req, Err(err) => { log::warn!( "failed to parse catalog version requirement \"{version_req_str}\" for \"{name}\": {err}" ); return; } }; let req = PackageReq { name: name.into(), version_req, }; // Catalog entries are keyed by the package name itself, so there is no // separate alias. if !filter.should_include(None, &req, DepKind::Npm) { return; } let id = DepId(deps.len()); deps.push(Dep { id, kind: DepKind::Npm, location: DepLocation::Catalog { path: path.clone(), kind, key_paths, }, req, alias: None, }); }; if let Some(catalog) = catalog { for (name, version_req) in catalog { let mut key_paths = vec![KeyPath::from_parts([ KeyPart::String("catalog".into()), KeyPart::String(name.as_str().into()), ])]; if nested { key_paths.push(KeyPath::from_parts([ KeyPart::String("workspaces".into()), KeyPart::String("catalog".into()), KeyPart::String(name.as_str().into()), ])); } add_entry(name, version_req, key_paths); } } if let Some(catalogs) = catalogs { for (catalog_name, entries) in catalogs { for (name, version_req) in entries { let mut key_paths = vec![KeyPath::from_parts([ KeyPart::String("catalogs".into()), KeyPart::String(catalog_name.as_str().into()), KeyPart::String(name.as_str().into()), ])]; if nested { key_paths.push(KeyPath::from_parts([ KeyPart::String("workspaces".into()), KeyPart::String("catalogs".into()), KeyPart::String(catalog_name.as_str().into()), KeyPart::String(name.as_str().into()), ])); } add_entry(name, version_req, key_paths); } } } } 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); } add_deps_from_catalogs(workspace, 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>, /// Set when fetching the package's metadata failed (e.g. an unreachable or /// unauthorized private registry). The package is dropped from the outdated /// table, so this is used to warn the user instead of skipping silently. pub fetch_error: Option<Arc<PackageInfoLoadError>>, } pub struct DepManager { deps: Vec<Dep>, resolved_versions: Vec<Option<PackageNv>>, latest_versions: Vec<PackageLatestVersion>, pending_changes: Vec<Change>, dependencies_resolved: AtomicFlag, 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: CliNpmResolver, npm_version_resolver: Arc<NpmVersionResolver>, npm_installer: Arc<CliNpmInstaller>, permissions_container: PermissionsContainer, progress_bar: ProgressBar, 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_installer: Arc<CliNpmInstaller>, pub npm_resolver: CliNpmResolver, pub npm_version_resolver: Arc<NpmVersionResolver>, pub permissions_container: PermissionsContainer, pub progress_bar: ProgressBar, 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_installer, npm_resolver, npm_version_resolver, progress_bar, permissions_container, main_module_graph_container, lockfile, } = args; Self { deps, resolved_versions: Vec::new(), latest_versions: Vec::new(), jsr_fetch_resolver, dependencies_resolved: AtomicFlag::lowered(), module_load_preparer, npm_fetch_resolver, npm_installer, npm_resolver, npm_version_resolver, progress_bar, 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.member_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.member_pkg_json() { add_deps_from_package_json(package_json, dep_filter, &mut deps); } // Catalogs are a workspace-root concept, so only include them when running // in the root directory (the recursive path handles them via the workspace). if workspace_dir.dir_path() == workspace_dir.workspace.root_dir_path() { add_deps_from_catalogs(&workspace_dir.workspace, 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.is_raised() { return Ok(()); } let _clear_guard = self.progress_bar.deferred_keep_initialize_alive(); 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 { lockfile.fill_graph(graph); } let npm_resolver = self.npm_resolver.as_managed().unwrap(); if self.deps.iter().all(|dep| match dep.kind { DepKind::Npm => npm_resolver .resolution() .resolve_pkg_id_from_pkg_req(&dep.req) .is_ok(), DepKind::Jsr => graph.packages.mappings().contains_key(&dep.req), }) { self.dependencies_resolved.raise(); graph_permit.commit(); return Ok(()); } self .npm_installer .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 => { // Note: a tagged req (e.g. `@latest`) never appears in the graph // mappings because the graph build rejects jsr version tags, so // the VersionReq::matches call below (which panics on tags) // cannot be reached with one. let resolved_nv = graph.packages.mappings().get(&dep.req); let resolved_nv = resolved_nv .and_then(|nv| { let versions = graph.packages.versions_by_name(&dep.req.name)?; let mut best = nv; for version in versions { if version.version > best.version && dep.req.version_req.matches(&version.version) { best = version; } } Some(best) }) .cloned(); info_futures.push(async { let nv = if let Some(nv) = resolved_nv { nv } else if let Some(nv) = self.jsr_fetch_resolver.req_to_nv(&dep.req).await? { nv } else { return Result::< Option<(Url, Arc<JsrPackageVersionInfo>)>, JsrPackageReqNotFoundError, >::Ok(None); }; if let Some(info) = self.jsr_fetch_resolver.package_version_info(&nv).await { let specifier = ModuleSpecifier::parse(&format!("jsr:/{}/", &dep.req)) .unwrap(); return Ok(Some((specifier, info))); } Ok(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, crate::module_loader::PrepareModuleLoadOptions { is_dynamic: false, lib: deno_config::workspace::TsTypeLib::DenoWindow, permissions: self.permissions_container.clone(), file_permission_api_name: None, ext_overwrite: None, allow_unknown_media_types: true, allow_sloppy_imports_hints_for_unreferenced_roots: true, skip_graph_roots_validation: false, file_content_overrides: Default::default(), file_header_overrides: Default::default(), }, ) .await?; self.dependencies_resolved.raise(); 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() .resolution() .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 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 _permit = npm_sema.acquire().await; let mut semver_compatible = self .npm_fetch_resolver .req_to_nv(semver_req) .await .ok() .flatten(); let info_result = self .npm_fetch_resolver .package_info_with_reason(&semver_req.name) .await; let fetch_error = info_result.as_ref().err().cloned(); let latest = info_result .ok() .and_then(|info| { let version_resolver = self.npm_version_resolver.get_for_package(&info); let latest_tag = info.dist_tags.get("latest")?; let can_use_latest = version_resolver .version_req_satisfies_and_matches_newest_dependency_date( &semver_req.version_req, latest_tag, ) .ok()?; if can_use_latest { semver_compatible = Some(PackageNv { name: semver_req.name.clone(), version: latest_tag.clone(), }); return Some(latest_tag.clone()); } // Use the `latest` dist-tag directly, matching npm/pnpm/bun // behavior. Computing the max across all versions is incorrect // when packages publish pre-release versions with commit hashes // (e.g., 1.0.0-beta.9-commit.d91dfb5) because semver considers // alphanumeric pre-release identifiers higher than numeric ones, // so beta.9-commit.hash > beta.32. // Ref: https://github.com/denoland/deno/issues/29647 if version_resolver.matches_newest_dependency_date(latest_tag) { Some(latest_tag.clone()) } else { None } }) .map(|version| PackageNv { name: semver_req.name.clone(), version, }); PackageLatestVersion { latest, semver_compatible, fetch_error, } } .boxed_local(), ), DepKind::Jsr => futs.push_back( async { let semver_req = &dep.req; let _permit = jsr_sema.acquire().await; let semver_compatible = self .jsr_fetch_resolver .req_to_nv(semver_req) .await .ok() .flatten(); let info_result = self .jsr_fetch_resolver .package_info_with_reason(&semver_req.name) .await; let fetch_error = info_result.as_ref().err().cloned(); let latest = info_result .ok() .and_then(|info| { let version_resolver = self .jsr_fetch_resolver .version_resolver_for_package(&semver_req.name, &info); let lower_bound = &semver_compatible.as_ref()?.version; // Only consider pre-release versions when the currently // resolved version is itself a pre-release. Otherwise // `deno outdated --latest` reports a pre-release (e.g. // 2.0.3-alpha.0) as the latest version even though JSR // designates the newest stable release as latest. This mirrors // npm packages, which resolve latest from the `latest` // dist-tag. (#36320) // // JSR's `meta.json` also has a `latest` field (exposed as // `info.latest`), but it can't replace the scan below on its // own: it never points at a pre-release, so a project already // tracking a pre-release line would stop being offered newer // pre-releases, and it may fall outside the // `minimumDependencyAge` window, in which case we still want // the newest version that is old enough. That last fallback is // also where the two registries deliberately differ: the npm // path above reports no latest version at all when its `latest` // dist-tag is too new, rather than walking back to an older // version. let allow_pre_release = !lower_bound.pre.is_empty(); { let mut best: Option<&Version> = Some(lower_bound); for version in info.versions.iter().filter_map( |(version, version_info)| { if !version_info.yanked && (allow_pre_release || version.pre.is_empty()) && version_resolver .matches_newest_dependency_date(version_info) { Some(version) } else { None } }, ) { if best.is_none_or(|b| version > b) { best = Some(version); } } best.cloned() } }) .map(|version| PackageNv { name: semver_req.name.clone(), version, }); PackageLatestVersion { latest, semver_compatible, fetch_error, } } .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 deps_with_ids(&self) -> impl Iterator<Item = (DepId, &Dep)> { self.deps.iter().enumerate().map(|(i, dep)| (DepId(i), dep)) } 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, _) => { 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)); } DepLocation::Catalog { path, key_paths, .. } => { let updater = get_or_create_updater(&mut config_updaters, &dep.location)?; if !updater .update_catalog_entry(key_paths, &version_req.to_string()) { log::warn!( "failed to find catalog entry for \"{}\" in {}", dep.req.name, path.display() ); } } } } } } for (_, updater) in config_updaters { updater.commit()?; } Ok(()) } /// Removes the lockfile specifier entries for the given dependencies and /// writes the lockfile to disk. The next install will re-resolve the /// missing entries to the latest version that matches each dependency's /// existing version requirement, bumping the lockfile within ranges /// without modifying `deno.json`/`package.json`. pub fn invalidate_lockfile_for_update( &self, dep_ids: impl IntoIterator<Item = DepId>, ) -> Result<(), AnyError> { let Some(lockfile_lock) = &self.lockfile else { return Ok(()); }; { let mut lockfile = lockfile_lock.lock(); for dep_id in dep_ids { let dep = &self.deps[dep_id.0]; let req = match dep.kind { DepKind::Npm => JsrDepPackageReq::npm(dep.req.clone()), DepKind::Jsr => JsrDepPackageReq::jsr(dep.req.clone()), }; if lockfile.content.packages.specifiers.remove(&req).is_some() { lockfile.has_content_changed = true; } } } lockfile_lock.write_if_changed()?; 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(), }) }