Skip to content

Commit 9182022

Browse files
committed
[WIP] feat(update): support updating @jsr/** dependencies
1 parent 5febac2 commit 9182022

File tree

14 files changed

+336
-63
lines changed

14 files changed

+336
-63
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
],
3737
"dependencies": {
3838
"cosmiconfig": "^9.0.0",
39-
"typescript": "^5.8.3"
39+
"typescript": "^5.8.3",
40+
"@jsr/luca__cases": "0.9.9",
41+
"@std/path": "npm:@jsr/std__path@^0.1.2",
42+
"@std/fs": "npm:@jsr/std__fs@^0.2.3"
4043
},
4144
"devDependencies": {
4245
"@biomejs/biome": "^1.9.4",

src/context.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use {
22
crate::{
33
config::Config,
4+
dependency::UpdateUrl,
45
instance::Instance,
56
packages::Packages,
67
registry_client::{LiveRegistryClient, PackageMeta, RegistryClient, RegistryError},
@@ -118,20 +119,20 @@ impl Context {
118119
let mut all_updates_by_internal_name: HashMap<String, Vec<Specifier>> = HashMap::new();
119120
let mut failed_updates: Vec<String> = vec![];
120121

121-
for internal_name in self.get_unique_internal_names_of_updateable_dependencies() {
122+
for update_url in self.get_unique_update_urls() {
122123
let permit = Arc::clone(&semaphore).acquire_owned().await;
123124
let client = Arc::clone(&client);
124125
let progress_bars = Arc::clone(&progress_bars);
125126

126127
handles.push((
127-
internal_name.clone(),
128+
update_url.internal_name.clone(),
128129
spawn(async move {
129130
let _permit = permit;
130131
let progress_bar = progress_bars.add(ProgressBar::new_spinner());
131132
progress_bar.enable_steady_tick(Duration::from_millis(100));
132133
progress_bar.set_style(ProgressStyle::default_spinner());
133-
progress_bar.set_message(internal_name.clone());
134-
let package_meta = client.fetch(&internal_name).await;
134+
progress_bar.set_message(update_url.internal_name.clone());
135+
let package_meta = client.fetch(&update_url).await;
135136
progress_bar.finish_and_clear();
136137
progress_bars.remove(&progress_bar);
137138
package_meta
@@ -143,7 +144,7 @@ impl Context {
143144
match handle.await {
144145
Ok(result) => match result {
145146
Ok(package_meta) => {
146-
let all_updates = all_updates_by_internal_name.entry(package_meta.name.clone()).or_default();
147+
let all_updates = all_updates_by_internal_name.entry(internal_name.clone()).or_default();
147148
for (version, _timestamp) in package_meta.time.iter() {
148149
if !version.contains("created") && !version.contains("modified") {
149150
all_updates.push(Specifier::new(version, None));
@@ -169,18 +170,18 @@ impl Context {
169170
/// Return a list of every dependency we should query the registry for
170171
/// updates. We use internal names in order to support dependency groups,
171172
/// where many dependencies can be aliased as one.
172-
fn get_unique_internal_names_of_updateable_dependencies(&self) -> HashSet<String> {
173+
fn get_unique_update_urls(&self) -> HashSet<UpdateUrl> {
173174
self
174175
.version_groups
175176
.iter()
176177
.filter(|group| group.matches_cli_filter)
177-
.fold(HashSet::new(), |mut unique_internal_names, group| {
178-
group.get_internal_names_of_updateable_dependencies().inspect(|internal_names| {
179-
internal_names.iter().for_each(|internal_name| {
180-
unique_internal_names.insert(internal_name.clone());
178+
.fold(HashSet::new(), |mut unique_update_urls, group| {
179+
group.get_update_urls().inspect(|update_urls| {
180+
update_urls.iter().for_each(|url| {
181+
unique_update_urls.insert(url.clone());
181182
});
182183
});
183-
unique_internal_names
184+
unique_update_urls
184185
})
185186
}
186187
}

src/dependency.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ use {
1515
#[path = "dependency_test.rs"]
1616
mod dependency_test;
1717

18+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
19+
pub struct UpdateUrl {
20+
/// The name of the dependency
21+
pub internal_name: String,
22+
/// @example https://registry.npmjs.org/syncpack
23+
/// @example https://npm.jsr.io/@jsr/std__path
24+
pub url: String,
25+
}
26+
1827
#[derive(Debug)]
1928
pub struct Dependency {
2029
/// The expected version specifier which all instances of this dependency
@@ -58,8 +67,12 @@ impl Dependency {
5867
}
5968
}
6069

61-
pub fn is_updateable(&self) -> bool {
62-
self.matches_cli_filter && self.internal_name_is_supported() && !self.has_local_instance() && !self.contains_alias_specifier()
70+
pub fn get_update_url(&self) -> Option<UpdateUrl> {
71+
if self.matches_cli_filter && self.internal_name_is_supported() {
72+
self.instances.iter().find_map(|instance| instance.get_update_url())
73+
} else {
74+
None
75+
}
6376
}
6477

6578
pub fn add_instance(&mut self, instance: Rc<Instance>) {

src/instance.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
#[cfg(test)]
2+
#[path = "instance_test.rs"]
3+
mod instance_test;
4+
15
use {
26
crate::{
7+
dependency::UpdateUrl,
38
dependency_type::{DependencyType, Strategy},
49
instance_state::{
510
FixableInstance, InstanceState, InvalidInstance, SemverGroupAndVersionConflict, SuspectInstance, UnfixableInstance, ValidInstance,
@@ -205,6 +210,52 @@ impl Instance {
205210
.map(|preferred_semver_range| self.descriptor.specifier.clone().with_range(preferred_semver_range))
206211
}
207212

213+
pub fn get_update_url(&self) -> Option<UpdateUrl> {
214+
if self.descriptor.matches_cli_filter && !self.is_local {
215+
let internal_name = &self.descriptor.internal_name;
216+
let actual_name = &self.descriptor.name;
217+
let raw = self.descriptor.specifier.get_raw();
218+
match &self.descriptor.specifier {
219+
Specifier::Alias(alias) => {
220+
if let Some(aliased_name) = alias.extract_package_name() {
221+
if aliased_name.starts_with("@jsr/") {
222+
Some(UpdateUrl {
223+
internal_name: internal_name.clone(),
224+
url: format!("https://npm.jsr.io/{}", aliased_name),
225+
})
226+
} else if &aliased_name == actual_name {
227+
Some(UpdateUrl {
228+
internal_name: internal_name.clone(),
229+
url: format!("https://registry.npmjs.org/{}", actual_name),
230+
})
231+
} else {
232+
debug!("'{aliased_name}' in '{raw}' does not equal the instance name '{actual_name}', skipping update as this might create mismatches");
233+
None
234+
}
235+
} else {
236+
None
237+
}
238+
}
239+
Specifier::BasicSemver(_) => {
240+
if actual_name.starts_with("@jsr/") {
241+
Some(UpdateUrl {
242+
internal_name: internal_name.clone(),
243+
url: format!("https://npm.jsr.io/{}", actual_name),
244+
})
245+
} else {
246+
Some(UpdateUrl {
247+
internal_name: internal_name.clone(),
248+
url: format!("https://registry.npmjs.org/{}", actual_name),
249+
})
250+
}
251+
}
252+
_ => None,
253+
}
254+
} else {
255+
None
256+
}
257+
}
258+
208259
/// Does this instance's specifier match the specifier of every one of the
209260
/// given instances?
210261
pub fn already_satisfies_all(&self, instances: &[Rc<Instance>]) -> bool {

src/instance_test.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use {
2+
crate::{
3+
dependency::UpdateUrl,
4+
test::{self},
5+
Context,
6+
},
7+
serde_json::json,
8+
};
9+
10+
#[test]
11+
fn returns_correct_registry_update_url() {
12+
let config = test::mock::config_from_mock(json!({}));
13+
let packages = test::mock::packages_from_mocks(vec![json!({
14+
"name": "local-package",
15+
"version": "0.0.0",
16+
"dependencies": {
17+
"@jsr/luca__cases": "1",
18+
"@lit-labs/ssr": "npm:@lit-labs/ssr@3.3.0",
19+
"@luca/cases": "npm:@jsr/luca__cases@1",
20+
"@std/fmt": "npm:@jsr/std__fmt@^1.0.3",
21+
"@std/yaml": "npm:@jsr/std__yaml",
22+
"lit": "npm:lit@3.2.1",
23+
}
24+
})]);
25+
let registry_client = None;
26+
let ctx = Context::create(config, packages, registry_client);
27+
28+
let get_update_url_by_name = |name: &str| {
29+
ctx
30+
.instances
31+
.iter()
32+
.find(|instance| instance.descriptor.internal_name == name)
33+
.unwrap()
34+
.get_update_url()
35+
};
36+
37+
assert_eq!(get_update_url_by_name("local-package"), None);
38+
assert_eq!(
39+
get_update_url_by_name("@jsr/luca__cases"),
40+
Some(UpdateUrl {
41+
internal_name: "@jsr/luca__cases".to_string(),
42+
url: "https://npm.jsr.io/@jsr/luca__cases".to_string()
43+
})
44+
);
45+
assert_eq!(
46+
get_update_url_by_name("@lit-labs/ssr"),
47+
Some(UpdateUrl {
48+
internal_name: "@lit-labs/ssr".to_string(),
49+
url: "https://registry.npmjs.org/@lit-labs/ssr".to_string()
50+
})
51+
);
52+
assert_eq!(
53+
get_update_url_by_name("@luca/cases"),
54+
Some(UpdateUrl {
55+
internal_name: "@luca/cases".to_string(),
56+
url: "https://npm.jsr.io/@jsr/luca__cases".to_string()
57+
})
58+
);
59+
assert_eq!(
60+
get_update_url_by_name("@std/fmt"),
61+
Some(UpdateUrl {
62+
internal_name: "@std/fmt".to_string(),
63+
url: "https://npm.jsr.io/@jsr/std__fmt".to_string()
64+
})
65+
);
66+
assert_eq!(
67+
get_update_url_by_name("@std/yaml"),
68+
Some(UpdateUrl {
69+
internal_name: "@std/yaml".to_string(),
70+
url: "https://npm.jsr.io/@jsr/std__yaml".to_string()
71+
})
72+
);
73+
assert_eq!(
74+
get_update_url_by_name("lit"),
75+
Some(UpdateUrl {
76+
internal_name: "lit".to_string(),
77+
url: "https://registry.npmjs.org/lit".to_string()
78+
})
79+
);
80+
}

src/registry_client.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use {
2+
crate::dependency::UpdateUrl,
23
log::{debug, error},
34
reqwest::{header::ACCEPT, Client, StatusCode},
45
serde::{Deserialize, Serialize},
@@ -8,15 +9,15 @@ use {
89

910
#[derive(Error, Debug)]
1011
pub enum RegistryError {
11-
#[error("Failed to fetch package '{name}': {source}")]
12+
#[error("Failed to fetch package '{url}': {source}")]
1213
FetchError {
13-
name: String,
14+
url: String,
1415
#[source]
1516
source: Box<dyn std::error::Error + Send + Sync>,
1617
},
1718

18-
#[error("HTTP error for package '{name}': {status}")]
19-
HttpError { name: String, status: StatusCode },
19+
#[error("HTTP error for package '{url}': {status}")]
20+
HttpError { url: String, status: StatusCode },
2021
}
2122

2223
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -29,7 +30,7 @@ pub struct PackageMeta {
2930
#[async_trait::async_trait]
3031
pub trait RegistryClient: std::fmt::Debug + Send + Sync {
3132
/// Fetch latest version of a given dep
32-
async fn fetch(&self, dependency_name: &str) -> Result<PackageMeta, RegistryError>;
33+
async fn fetch(&self, update_url: &UpdateUrl) -> Result<PackageMeta, RegistryError>;
3334
}
3435

3536
/// The real implementation of RegistryClientTrait which makes actual network
@@ -41,26 +42,25 @@ pub struct LiveRegistryClient {
4142

4243
#[async_trait::async_trait]
4344
impl RegistryClient for LiveRegistryClient {
44-
async fn fetch(&self, dependency_name: &str) -> Result<PackageMeta, RegistryError> {
45-
let url = format!("https://registry.npmjs.org/{}", dependency_name);
46-
let req = self.client.get(&url).header(ACCEPT, "application/json");
47-
debug!("GET {url}");
45+
async fn fetch(&self, update_url: &UpdateUrl) -> Result<PackageMeta, RegistryError> {
46+
let req = self.client.get(&update_url.url).header(ACCEPT, "application/json");
47+
debug!("GET {update_url:?}");
4848
match req.send().await {
4949
Ok(res) => match res.status() {
5050
StatusCode::OK => match res.json::<PackageMeta>().await {
5151
Ok(package_meta) => Ok(package_meta),
5252
Err(err) => Err(RegistryError::FetchError {
53-
name: dependency_name.to_string(),
53+
url: update_url.url.to_string(),
5454
source: Box::new(err),
5555
}),
5656
},
5757
status => Err(RegistryError::HttpError {
58-
name: dependency_name.to_string(),
58+
url: update_url.url.to_string(),
5959
status,
6060
}),
6161
},
6262
Err(err) => Err(RegistryError::FetchError {
63-
name: dependency_name.to_string(),
63+
url: update_url.url.to_string(),
6464
source: Box::new(err),
6565
}),
6666
}

src/specifier.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,20 @@ impl Specifier {
148148

149149
/// Create a new instance from an npm alias specifier
150150
fn from_alias(value: &str, raw: String) -> Self {
151-
let aliased_version = {
152-
let start = value.rfind('@').unwrap() + 1;
153-
value[start..].to_string()
154-
};
155-
let aliased_name = {
151+
let (aliased_name, aliased_version) = {
156152
let start = value.find(':').unwrap() + 1;
157-
let end = value.rfind('@').unwrap();
158-
value[start..end].to_string()
153+
if let Some(at_pos) = value.rfind('@') {
154+
if at_pos > start {
155+
// There's a version specifier
156+
(value[start..at_pos].to_string(), value[at_pos + 1..].to_string())
157+
} else {
158+
// The @ is part of a scoped package name, no version
159+
(value[start..].to_string(), String::new())
160+
}
161+
} else {
162+
// No @ at all, unscoped package without version
163+
(value[start..].to_string(), String::new())
164+
}
159165
};
160166
if aliased_name.is_empty() {
161167
Self::Unsupported(raw::Raw { raw })
@@ -384,8 +390,8 @@ impl Specifier {
384390

385391
/// Are both specifiers on eg. "-alpha", or neither have a release channel?
386392
pub fn has_same_release_channel_as(&self, other: &Specifier) -> bool {
387-
if let (Specifier::BasicSemver(a), Specifier::BasicSemver(b)) = (self, other) {
388-
a.node_version.pre_release.first() == b.node_version.pre_release.first()
393+
if let (Some(a), Some(b)) = (self.get_node_version(), other.get_node_version()) {
394+
a.pre_release.first() == b.pre_release.first()
389395
} else {
390396
false
391397
}
@@ -402,7 +408,7 @@ impl Specifier {
402408
}
403409

404410
pub fn is_older_than(&self, other: &Specifier) -> bool {
405-
if let (Specifier::BasicSemver(_), Specifier::BasicSemver(_)) = (self, other) {
411+
if self.get_node_version().is_some() && other.get_node_version().is_some() {
406412
other > self
407413
} else {
408414
false
@@ -411,8 +417,8 @@ impl Specifier {
411417

412418
/// Is this specifier on the same major version, but otherwise older?
413419
pub fn is_older_than_by_minor(&self, other: &Specifier) -> bool {
414-
if let (Specifier::BasicSemver(a), Specifier::BasicSemver(b)) = (self, other) {
415-
b.node_version.major == a.node_version.major && other > self
420+
if let (Some(a), Some(b)) = (self.get_node_version(), other.get_node_version()) {
421+
b.major == a.major && other > self
416422
} else {
417423
false
418424
}
@@ -421,8 +427,8 @@ impl Specifier {
421427
/// Is this specifier on the same major and minor version, but otherwise
422428
/// older?
423429
pub fn is_older_than_by_patch(&self, other: &Specifier) -> bool {
424-
if let (Specifier::BasicSemver(a), Specifier::BasicSemver(b)) = (self, other) {
425-
b.node_version.major == a.node_version.major && b.node_version.minor == a.node_version.minor && other > self
430+
if let (Some(a), Some(b)) = (self.get_node_version(), other.get_node_version()) {
431+
b.major == a.major && b.minor == a.minor && other > self
426432
} else {
427433
false
428434
}

0 commit comments

Comments
 (0)