Skip to content

Commit 801e802

Browse files
authored
feat: support patch packages (#43)
1 parent 5938b14 commit 801e802

8 files changed

+889
-28
lines changed

src/graphs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ enum LockfileGraphPackage {
7474
struct LockfileNpmGraphPackage {
7575
/// Root ids that transitively reference this package.
7676
root_ids: HashSet<LockfilePkgId>,
77-
integrity: String,
77+
integrity: Option<String>,
7878
dependencies: BTreeMap<StackString, LockfileNpmPackageId>,
7979
}
8080

src/lib.rs

+61-8
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub struct SetWorkspaceConfigOptions {
5151
pub struct WorkspaceConfig {
5252
pub root: WorkspaceMemberConfig,
5353
pub members: HashMap<String, WorkspaceMemberConfig>,
54+
pub patches: HashMap<String, LockfilePatchContent>,
5455
}
5556

5657
#[derive(Default, Debug, Clone, PartialEq, Eq)]
@@ -62,7 +63,8 @@ pub struct WorkspaceMemberConfig {
6263
#[derive(Debug, Clone, PartialEq, Eq)]
6364
pub struct NpmPackageLockfileInfo {
6465
pub serialized_id: StackString,
65-
pub integrity: String,
66+
/// Will be `None` for patch packages.
67+
pub integrity: Option<String>,
6668
pub dependencies: Vec<NpmPackageDependencyLockfileInfo>,
6769
}
6870

@@ -74,7 +76,8 @@ pub struct NpmPackageDependencyLockfileInfo {
7476

7577
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
7678
pub struct NpmPackageInfo {
77-
pub integrity: String,
79+
/// Will be `None` for patch packages.
80+
pub integrity: Option<String>,
7881
#[serde(default)]
7982
pub dependencies: BTreeMap<StackString, StackString>,
8083
}
@@ -162,18 +165,34 @@ impl WorkspaceMemberConfigContent {
162165
}
163166
}
164167

168+
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
169+
#[serde(rename_all = "camelCase")]
170+
pub struct LockfilePatchContent {
171+
#[serde(default)]
172+
#[serde(skip_serializing_if = "Vec::is_empty")]
173+
pub dependencies: HashSet<JsrDepPackageReq>,
174+
#[serde(default)]
175+
#[serde(skip_serializing_if = "Vec::is_empty")]
176+
pub peer_dependencies: HashSet<JsrDepPackageReq>,
177+
#[serde(default)]
178+
#[serde(skip_serializing_if = "HashMap::is_empty")]
179+
pub peer_dependencies_meta: HashMap<String, serde_json::Value>,
180+
}
181+
165182
#[derive(Debug, Default, Clone, Deserialize)]
166183
#[serde(rename_all = "camelCase")]
167184
pub(crate) struct WorkspaceConfigContent {
168185
#[serde(default, flatten)]
169186
pub root: WorkspaceMemberConfigContent,
170187
#[serde(default)]
171188
pub members: HashMap<String, WorkspaceMemberConfigContent>,
189+
#[serde(default)]
190+
pub patches: HashMap<String, LockfilePatchContent>,
172191
}
173192

174193
impl WorkspaceConfigContent {
175194
pub fn is_empty(&self) -> bool {
176-
self.root.is_empty() && self.members.is_empty()
195+
self.root.is_empty() && self.members.is_empty() && self.patches.is_empty()
177196
}
178197

179198
fn get_all_dep_reqs(&self) -> impl Iterator<Item = &JsrDepPackageReq> {
@@ -209,7 +228,7 @@ impl LockfileContent {
209228

210229
#[derive(Debug, Deserialize)]
211230
struct RawNpmPackageInfo {
212-
pub integrity: String,
231+
pub integrity: Option<String>,
213232
#[serde(default)]
214233
pub dependencies: Vec<StackString>,
215234
}
@@ -577,6 +596,40 @@ impl Lockfile {
577596
// to !self.has_content_changed after populating it with this information
578597
let allow_content_changed =
579598
self.has_content_changed || !self.content.is_empty();
599+
600+
let has_any_patch_changed = options.config.patches.len()
601+
!= self.content.workspace.patches.len()
602+
|| !options.config.patches.is_empty()
603+
&& options.config.patches.iter().all(|(patch, new)| {
604+
let Some(existing) = self.content.workspace.patches.get_mut(patch)
605+
else {
606+
return true;
607+
};
608+
new != existing
609+
});
610+
611+
// if a patch changes, it's quite complicated to figure out how to get it to redo
612+
// npm resolution just for that part, so for now, clear out all the npm dependencies
613+
// if any patch changes
614+
if has_any_patch_changed {
615+
self.has_content_changed = true;
616+
self.content.packages.npm.clear();
617+
self
618+
.content
619+
.packages
620+
.specifiers
621+
.retain(|k, _| match k.kind {
622+
deno_semver::package::PackageKind::Jsr => true,
623+
deno_semver::package::PackageKind::Npm => false,
624+
});
625+
self.content.workspace.patches.clear();
626+
self
627+
.content
628+
.workspace
629+
.patches
630+
.extend(options.config.patches);
631+
}
632+
580633
let old_deps = self
581634
.content
582635
.workspace
@@ -1015,7 +1068,7 @@ mod tests {
10151068
// already in lockfile
10161069
let npm_package = NpmPackageLockfileInfo {
10171070
serialized_id: "[email protected]".into(),
1018-
integrity: "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==".to_string(),
1071+
integrity: Some("sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==".to_string()),
10191072
dependencies: vec![],
10201073
};
10211074
lockfile.insert_npm_package(npm_package);
@@ -1024,7 +1077,7 @@ mod tests {
10241077
// insert package that exists already, but has slightly different properties
10251078
let npm_package = NpmPackageLockfileInfo {
10261079
serialized_id: "[email protected]".into(),
1027-
integrity: "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==".to_string(),
1080+
integrity: Some("sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==".to_string()),
10281081
dependencies: vec![],
10291082
};
10301083
lockfile.insert_npm_package(npm_package);
@@ -1033,7 +1086,7 @@ mod tests {
10331086
lockfile.has_content_changed = false;
10341087
let npm_package = NpmPackageLockfileInfo {
10351088
serialized_id: "[email protected]".into(),
1036-
integrity: "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==".to_string(),
1089+
integrity: Some("sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==".to_string()),
10371090
dependencies: vec![],
10381091
};
10391092
// Not present in lockfile yet, should be inserted
@@ -1047,7 +1100,7 @@ mod tests {
10471100

10481101
let npm_package = NpmPackageLockfileInfo {
10491102
serialized_id: "[email protected]".into(),
1050-
integrity: "sha512-foobar".to_string(),
1103+
integrity: Some("sha512-foobar".to_string()),
10511104
dependencies: vec![],
10521105
};
10531106
// Now present in lockfile, should be changed due to different integrity

src/printer.rs

+58-19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::borrow::Cow;
44
use std::collections::BTreeMap;
55
use std::collections::HashMap;
6+
use std::collections::HashSet;
67

78
use deno_semver::jsr::JsrDepPackageReq;
89
use deno_semver::package::PackageNv;
@@ -13,6 +14,7 @@ use serde::Serialize;
1314
use crate::JsrPackageInfo;
1415
use crate::LockfileContent;
1516
use crate::LockfilePackageJsonContent;
17+
use crate::LockfilePatchContent;
1618
use crate::NpmPackageInfo;
1719
use crate::WorkspaceConfigContent;
1820
use crate::WorkspaceMemberConfigContent;
@@ -26,7 +28,9 @@ struct SerializedJsrPkg<'a> {
2628

2729
#[derive(Serialize)]
2830
struct SerializedNpmPkg<'a> {
29-
integrity: &'a str,
31+
/// Will be `None` for patch packages.
32+
#[serde(skip_serializing_if = "Option::is_none")]
33+
integrity: Option<&'a str>,
3034
#[serde(skip_serializing_if = "Vec::is_empty")]
3135
dependencies: Vec<Cow<'a, str>>,
3236
}
@@ -58,6 +62,20 @@ impl SerializedLockfilePackageJsonContent {
5862
}
5963
}
6064

65+
#[derive(Debug, Default, Serialize)]
66+
#[serde(rename_all = "camelCase")]
67+
struct SerializedLockfilePatchContent {
68+
#[serde(default)]
69+
#[serde(skip_serializing_if = "Vec::is_empty")]
70+
pub dependencies: Vec<SerializedJsrDepPackageReq>,
71+
#[serde(default)]
72+
#[serde(skip_serializing_if = "Vec::is_empty")]
73+
pub peer_dependencies: Vec<SerializedJsrDepPackageReq>,
74+
#[serde(default)]
75+
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
76+
pub peer_dependencies_meta: BTreeMap<String, serde_json::Value>,
77+
}
78+
6179
#[derive(Debug, Default, Serialize)]
6280
#[serde(rename_all = "camelCase")]
6381
struct SerializedWorkspaceMemberConfigContent {
@@ -85,11 +103,14 @@ struct SerializedWorkspaceConfigContent<'a> {
85103
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
86104
#[serde(default)]
87105
pub members: BTreeMap<&'a str, SerializedWorkspaceMemberConfigContent>,
106+
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
107+
#[serde(default)]
108+
pub patches: BTreeMap<&'a str, SerializedLockfilePatchContent>,
88109
}
89110

90111
impl SerializedWorkspaceConfigContent<'_> {
91112
pub fn is_empty(&self) -> bool {
92-
self.root.is_empty() && self.members.is_empty()
113+
self.root.is_empty() && self.members.is_empty() && self.patches.is_empty()
93114
}
94115
}
95116

@@ -201,7 +222,7 @@ pub fn print_v4_content(content: &LockfileContent) -> String {
201222
(
202223
key.as_str(),
203224
SerializedNpmPkg {
204-
integrity: &value.integrity,
225+
integrity: value.integrity.as_deref(),
205226
dependencies: value
206227
.dependencies
207228
.iter()
@@ -231,32 +252,45 @@ pub fn print_v4_content(content: &LockfileContent) -> String {
231252
fn handle_pkg_json_content(
232253
content: &LockfilePackageJsonContent,
233254
) -> SerializedLockfilePackageJsonContent {
234-
let mut dependencies = content
235-
.dependencies
236-
.iter()
237-
.map(SerializedJsrDepPackageReq::new)
238-
.collect::<Vec<_>>();
239-
dependencies.sort();
240-
SerializedLockfilePackageJsonContent { dependencies }
255+
SerializedLockfilePackageJsonContent {
256+
dependencies: sort_deps(&content.dependencies),
257+
}
241258
}
242259

243260
fn handle_workspace_member(
244261
member: &WorkspaceMemberConfigContent,
245262
) -> SerializedWorkspaceMemberConfigContent {
246263
SerializedWorkspaceMemberConfigContent {
247-
dependencies: {
248-
let mut member = member
249-
.dependencies
250-
.iter()
251-
.map(SerializedJsrDepPackageReq::new)
252-
.collect::<Vec<_>>();
253-
member.sort();
254-
member
255-
},
264+
dependencies: sort_deps(&member.dependencies),
256265
package_json: handle_pkg_json_content(&member.package_json),
257266
}
258267
}
259268

269+
fn handle_patch_content(
270+
content: &LockfilePatchContent,
271+
) -> SerializedLockfilePatchContent {
272+
SerializedLockfilePatchContent {
273+
dependencies: sort_deps(&content.dependencies),
274+
peer_dependencies: sort_deps(&content.peer_dependencies),
275+
peer_dependencies_meta: content
276+
.peer_dependencies_meta
277+
.iter()
278+
.map(|(k, v)| (k.clone(), v.clone()))
279+
.collect(),
280+
}
281+
}
282+
283+
fn sort_deps(
284+
deps: &HashSet<JsrDepPackageReq>,
285+
) -> Vec<SerializedJsrDepPackageReq> {
286+
let mut dependencies = deps
287+
.iter()
288+
.map(SerializedJsrDepPackageReq::new)
289+
.collect::<Vec<_>>();
290+
dependencies.sort();
291+
dependencies
292+
}
293+
260294
fn handle_workspace(
261295
content: &WorkspaceConfigContent,
262296
) -> SerializedWorkspaceConfigContent {
@@ -267,6 +301,11 @@ pub fn print_v4_content(content: &LockfileContent) -> String {
267301
.iter()
268302
.map(|(key, value)| (key.as_str(), handle_workspace_member(value)))
269303
.collect(),
304+
patches: content
305+
.patches
306+
.iter()
307+
.map(|(key, value)| (key.as_str(), handle_patch_content(value)))
308+
.collect(),
270309
}
271310
}
272311

tests/integration_test.rs

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn adding_workspace_does_not_cause_content_changes() {
2929
package_json_deps: Default::default(),
3030
},
3131
members: Default::default(),
32+
patches: Default::default(),
3233
},
3334
});
3435
assert!(!lockfile.has_content_changed); // should not have changed
@@ -50,6 +51,7 @@ fn adding_workspace_does_not_cause_content_changes() {
5051
package_json_deps: Default::default(),
5152
},
5253
members: Default::default(),
54+
patches: Default::default(),
5355
},
5456
});
5557
assert!(lockfile.has_content_changed);
@@ -76,6 +78,7 @@ fn adding_workspace_does_not_cause_content_changes() {
7678
package_json_deps: Default::default(),
7779
},
7880
members: Default::default(),
81+
patches: Default::default(),
7982
},
8083
});
8184
assert!(lockfile.has_content_changed); // should have changed since lockfile was not empty

tests/spec_test.rs

+39
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::BTreeSet;
55
use std::panic::AssertUnwindSafe;
66

77
use deno_lockfile::Lockfile;
8+
use deno_lockfile::LockfilePatchContent;
89
use deno_lockfile::NewLockfileOptions;
910
use deno_lockfile::PackagesContent;
1011
use deno_lockfile::SetWorkspaceConfigOptions;
@@ -69,6 +70,23 @@ fn config_changes_test(test: &CollectedTest) {
6970
package_json: LockfilePackageJsonContent,
7071
}
7172

73+
#[derive(Debug, Default, Clone, Deserialize, Hash)]
74+
#[serde(rename_all = "camelCase")]
75+
struct PatchConfigContent {
76+
#[serde(default)]
77+
dependencies: BTreeSet<JsrDepPackageReq>,
78+
#[serde(default)]
79+
peer_dependencies: BTreeSet<JsrDepPackageReq>,
80+
#[serde(default)]
81+
peer_dependencies_meta: BTreeMap<String, PeerDependenciesMetaValue>,
82+
}
83+
84+
#[derive(Debug, Default, Clone, Serialize, Deserialize, Hash)]
85+
#[serde(rename_all = "camelCase")]
86+
struct PeerDependenciesMetaValue {
87+
optional: bool,
88+
}
89+
7290
#[derive(Debug, Default, Clone, Deserialize, Hash)]
7391
#[serde(rename_all = "camelCase")]
7492
struct WorkspaceConfigContent {
@@ -77,6 +95,9 @@ fn config_changes_test(test: &CollectedTest) {
7795
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
7896
#[serde(default)]
7997
members: BTreeMap<String, WorkspaceMemberConfigContent>,
98+
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
99+
#[serde(default)]
100+
patches: BTreeMap<String, PatchConfigContent>,
80101
}
81102

82103
impl WorkspaceConfigContent {
@@ -108,6 +129,24 @@ fn config_changes_test(test: &CollectedTest) {
108129
)
109130
})
110131
.collect(),
132+
patches: self
133+
.patches
134+
.into_iter()
135+
.map(|(k, v)| {
136+
(
137+
k,
138+
LockfilePatchContent {
139+
dependencies: v.dependencies.into_iter().collect(),
140+
peer_dependencies: v.peer_dependencies.into_iter().collect(),
141+
peer_dependencies_meta: v
142+
.peer_dependencies_meta
143+
.into_iter()
144+
.map(|(k, v)| (k, serde_json::to_value(v).unwrap()))
145+
.collect(),
146+
},
147+
)
148+
})
149+
.collect(),
111150
}
112151
}
113152
}

0 commit comments

Comments
 (0)