-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: store package.json and deno.json JSR and npm deps in lockfile for tracking removable dependencies #13
Changes from 36 commits
d77733b
ca6a30d
1b0deff
8f2ac7d
5aea0b2
586dbc0
52b5635
a094544
c9afd52
5043098
8d84292
87e4c3d
cba7168
8433aaa
2bf8107
03f3fac
e4a4210
c5bcd36
37e4809
61ca625
3f770c2
f753893
f835ae6
51b3d11
dc22667
6e7b112
e447dc4
3f8a869
8ff8f66
2b59da4
23310f1
ebd1aef
3edd70b
ba57e9b
360d1b5
b68069d
13fc231
f57512f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. | ||
|
||
use std::collections::BTreeMap; | ||
use std::collections::BTreeSet; | ||
use std::collections::HashMap; | ||
use std::collections::HashSet; | ||
use std::collections::VecDeque; | ||
|
||
use crate::NpmPackageInfo; | ||
use crate::PackagesContent; | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
enum LockfilePkgId { | ||
Npm(LockfileNpmPackageId), | ||
Jsr(LockfileJsrPkgNv), | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
struct LockfileJsrPkgNv(String); | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
struct LockfileNpmPackageId(String); | ||
|
||
impl LockfileNpmPackageId { | ||
pub fn parts(&self) -> impl Iterator<Item = &str> { | ||
let package_id = &self.0; | ||
let package_id = package_id.strip_prefix("npm:").unwrap_or(package_id); | ||
package_id.split('_').filter(|s| !s.is_empty()) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
struct LockfilePkgReq(String); | ||
|
||
enum LockfileGraphPackage { | ||
Jsr(LockfileJsrGraphPackage), | ||
Npm(LockfileNpmGraphPackage), | ||
} | ||
|
||
struct LockfileNpmGraphPackage { | ||
/// Root ids that transitively reference this package. | ||
root_ids: HashSet<LockfilePkgId>, | ||
integrity: String, | ||
dependencies: BTreeMap<String, LockfileNpmPackageId>, | ||
} | ||
|
||
#[derive(Default)] | ||
struct LockfileJsrGraphPackage { | ||
/// Root ids that transitively reference this package. | ||
root_ids: HashSet<LockfilePkgId>, | ||
dependencies: BTreeSet<LockfilePkgReq>, | ||
} | ||
|
||
/// Graph used to analyze a lockfile to determine which packages | ||
/// and remotes can be removed based on config file changes. | ||
pub struct LockfilePackageGraph<FNvToJsrUrl: Fn(&str) -> Option<String>> { | ||
root_packages: HashMap<LockfilePkgReq, LockfilePkgId>, | ||
packages: HashMap<LockfilePkgId, LockfileGraphPackage>, | ||
remotes: BTreeMap<String, String>, | ||
nv_to_jsr_url: FNvToJsrUrl, | ||
} | ||
|
||
impl<FNvToJsrUrl: Fn(&str) -> Option<String>> | ||
LockfilePackageGraph<FNvToJsrUrl> | ||
{ | ||
pub fn from_lockfile<'a>( | ||
content: PackagesContent, | ||
remotes: BTreeMap<String, String>, | ||
old_config_file_packages: impl Iterator<Item = &'a str>, | ||
nv_to_jsr_url: FNvToJsrUrl, | ||
) -> Self { | ||
let mut root_packages = | ||
HashMap::<LockfilePkgReq, LockfilePkgId>::with_capacity( | ||
content.specifiers.len(), | ||
); | ||
// collect the specifiers to version mappings | ||
let mut packages = HashMap::new(); | ||
dsherret marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (key, value) in content.specifiers { | ||
if let Some(value) = value.strip_prefix("npm:") { | ||
root_packages.insert( | ||
LockfilePkgReq(key.to_string()), | ||
LockfilePkgId::Npm(LockfileNpmPackageId(value.to_string())), | ||
); | ||
} else if let Some(value) = value.strip_prefix("jsr:") { | ||
let nv = LockfilePkgId::Jsr(LockfileJsrPkgNv(value.to_string())); | ||
root_packages.insert(LockfilePkgReq(key), nv.clone()); | ||
packages.insert( | ||
nv, | ||
LockfileGraphPackage::Jsr(LockfileJsrGraphPackage::default()), | ||
); | ||
} | ||
} | ||
|
||
for (nv, content_package) in content.jsr { | ||
let new_deps = &content_package.dependencies; | ||
let package = packages | ||
.entry(LockfilePkgId::Jsr(LockfileJsrPkgNv(nv.clone()))) | ||
.or_insert_with(|| { | ||
LockfileGraphPackage::Jsr(LockfileJsrGraphPackage::default()) | ||
}); | ||
match package { | ||
LockfileGraphPackage::Jsr(package) => { | ||
package.dependencies = new_deps | ||
.iter() | ||
.map(|req| LockfilePkgReq(req.clone())) | ||
.collect(); | ||
} | ||
LockfileGraphPackage::Npm(_) => unreachable!(), | ||
} | ||
} | ||
for (id, package) in content.npm { | ||
packages.insert( | ||
LockfilePkgId::Npm(LockfileNpmPackageId(id.clone())), | ||
LockfileGraphPackage::Npm(LockfileNpmGraphPackage { | ||
root_ids: Default::default(), | ||
integrity: package.integrity.clone(), | ||
dependencies: package | ||
.dependencies | ||
.iter() | ||
.map(|(key, dep_id)| { | ||
(key.clone(), LockfileNpmPackageId(dep_id.clone())) | ||
}) | ||
.collect(), | ||
}), | ||
); | ||
} | ||
|
||
let mut root_ids = old_config_file_packages | ||
.filter_map(|value| { | ||
root_packages | ||
.get(&LockfilePkgReq(value.to_string())) | ||
.cloned() | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
// trace every root identifier through the graph finding all corresponding packages | ||
while let Some(root_id) = root_ids.pop() { | ||
let mut pending = VecDeque::from([root_id.clone()]); | ||
dsherret marked this conversation as resolved.
Show resolved
Hide resolved
|
||
while let Some(id) = pending.pop_back() { | ||
if let Some(package) = packages.get_mut(&id) { | ||
Comment on lines
+142
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might occur if someone has been modifying the lockfile themselves. It's not a big deal in that case because then the package is already gone. |
||
match package { | ||
LockfileGraphPackage::Jsr(package) => { | ||
if package.root_ids.insert(root_id.clone()) { | ||
for req in &package.dependencies { | ||
if let Some(nv) = root_packages.get(req) { | ||
pending.push_back(nv.clone()); | ||
} | ||
} | ||
} | ||
} | ||
LockfileGraphPackage::Npm(package) => { | ||
if package.root_ids.insert(root_id.clone()) { | ||
for dep_id in package.dependencies.values() { | ||
pending.push_back(LockfilePkgId::Npm(dep_id.clone())); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
Self { | ||
root_packages, | ||
packages, | ||
remotes, | ||
nv_to_jsr_url, | ||
} | ||
} | ||
|
||
pub fn remove_root_packages( | ||
&mut self, | ||
package_reqs: impl Iterator<Item = String>, | ||
) { | ||
let mut root_ids = Vec::new(); | ||
|
||
// collect the root ids being removed | ||
{ | ||
let mut pending_reqs = | ||
package_reqs.map(LockfilePkgReq).collect::<VecDeque<_>>(); | ||
let mut visited_root_packages = | ||
HashSet::with_capacity(self.root_packages.len()); | ||
visited_root_packages.extend(pending_reqs.iter().cloned()); | ||
while let Some(pending_req) = pending_reqs.pop_front() { | ||
if let Some(id) = self.root_packages.get(&pending_req) { | ||
if let LockfilePkgId::Npm(id) = id { | ||
if let Some(first_part) = id.parts().next() { | ||
for (req, id) in &self.root_packages { | ||
if let LockfilePkgId::Npm(id) = &id { | ||
// be a bit aggressive and remove any npm packages that | ||
// have this package as a peer dependency | ||
if id.parts().skip(1).any(|part| part == first_part) { | ||
let has_visited = visited_root_packages.insert(req.clone()); | ||
if has_visited { | ||
pending_reqs.push_back(req.clone()); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
root_ids.push(id.clone()); | ||
} | ||
} | ||
} | ||
|
||
// Go through the graph and mark the packages that no | ||
// longer use this root id. If the package goes to having | ||
// no root ids, then remove it from the graph. | ||
while let Some(root_id) = root_ids.pop() { | ||
let mut pending = VecDeque::from([root_id.clone()]); | ||
while let Some(id) = pending.pop_back() { | ||
if let Some(package) = self.packages.get_mut(&id) { | ||
match package { | ||
LockfileGraphPackage::Jsr(package) => { | ||
if package.root_ids.remove(&root_id) { | ||
for req in &package.dependencies { | ||
if let Some(id) = self.root_packages.get(req) { | ||
pending.push_back(id.clone()); | ||
} | ||
} | ||
if package.root_ids.is_empty() { | ||
self.remove_package(id); | ||
} | ||
} | ||
} | ||
LockfileGraphPackage::Npm(package) => { | ||
if package.root_ids.remove(&root_id) { | ||
for dep_id in package.dependencies.values() { | ||
pending.push_back(LockfilePkgId::Npm(dep_id.clone())); | ||
} | ||
if package.root_ids.is_empty() { | ||
self.remove_package(id); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn remove_package(&mut self, id: LockfilePkgId) { | ||
self.packages.remove(&id); | ||
self.root_packages.retain(|_, pkg_id| *pkg_id != id); | ||
if let LockfilePkgId::Jsr(nv) = id { | ||
if let Some(url) = (self.nv_to_jsr_url)(&nv.0) { | ||
debug_assert!( | ||
url.ends_with('/'), | ||
"JSR URL should end with slash: {}", | ||
url | ||
); | ||
self.remotes.retain(|k, _| !k.starts_with(&url)); | ||
} | ||
} | ||
} | ||
|
||
pub fn populate_packages( | ||
self, | ||
packages: &mut PackagesContent, | ||
remotes: &mut BTreeMap<String, String>, | ||
) { | ||
*remotes = self.remotes; | ||
for (req, id) in self.root_packages { | ||
packages.specifiers.insert( | ||
req.0, | ||
match id { | ||
LockfilePkgId::Npm(id) => format!("npm:{}", id.0), | ||
LockfilePkgId::Jsr(nv) => format!("jsr:{}", nv.0), | ||
}, | ||
); | ||
} | ||
|
||
for (id, package) in self.packages { | ||
match package { | ||
LockfileGraphPackage::Jsr(package) => { | ||
if !package.dependencies.is_empty() { | ||
packages.jsr.insert( | ||
match id { | ||
LockfilePkgId::Jsr(nv) => nv.0, | ||
LockfilePkgId::Npm(_) => unreachable!(), | ||
}, | ||
crate::JsrPackageInfo { | ||
dependencies: package | ||
.dependencies | ||
.into_iter() | ||
.map(|req| req.0) | ||
.collect(), | ||
}, | ||
); | ||
} | ||
} | ||
LockfileGraphPackage::Npm(package) => { | ||
packages.npm.insert( | ||
match id { | ||
LockfilePkgId::Jsr(_) => unreachable!(), | ||
LockfilePkgId::Npm(id) => id.0, | ||
}, | ||
NpmPackageInfo { | ||
integrity: package.integrity.clone(), | ||
dependencies: package | ||
.dependencies | ||
.into_iter() | ||
.map(|(name, id)| (name, id.0)) | ||
.collect(), | ||
}, | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, I didn't know you could do that (or never seen that in the wild)