1use indexmap::IndexMap;
2use kdl::{KdlDocument, KdlNode};
3use nassun::{client::Nassun, package::Package, PackageResolution};
4use node_semver::Version;
5use oro_common::CorgiManifest;
6use oro_package_spec::PackageSpec;
7use serde::{Deserialize, Serialize};
8use ssri::Integrity;
9use unicase::UniCase;
10
11use crate::{error::NodeMaintainerError, graph::DepType, IntoKdl};
12
13#[derive(Default, Debug, Clone, PartialEq, Eq)]
15pub struct Lockfile {
16 pub(crate) version: u64,
17 pub(crate) root: LockfileNode,
18 pub(crate) packages: IndexMap<UniCase<String>, LockfileNode>,
19}
20
21impl Lockfile {
22 pub fn version(&self) -> u64 {
23 self.version
24 }
25
26 pub fn root(&self) -> &LockfileNode {
27 &self.root
28 }
29
30 pub fn packages(&self) -> &IndexMap<UniCase<String>, LockfileNode> {
31 &self.packages
32 }
33
34 pub fn to_kdl(&self) -> KdlDocument {
35 let mut doc = KdlDocument::new();
36 doc.set_leading(
37 "// This file is automatically generated and not intended for manual editing.",
38 );
39 let mut version_node = KdlNode::new("lockfile-version");
40 version_node.push(self.version as i64);
41 doc.nodes_mut().push(version_node);
42 doc.nodes_mut().push(self.root.to_kdl());
43 let mut packages = self.packages.iter().collect::<Vec<_>>();
44 packages.sort_by(|(a, _), (b, _)| a.cmp(b));
45 for (_, pkg) in packages {
46 doc.nodes_mut().push(pkg.to_kdl());
47 }
48 doc.fmt();
49 doc
50 }
51
52 pub fn from_kdl(kdl: impl IntoKdl) -> Result<Self, NodeMaintainerError> {
53 let kdl: KdlDocument = kdl.into_kdl()?;
54 fn inner(kdl: KdlDocument) -> Result<Lockfile, NodeMaintainerError> {
55 let packages = kdl
56 .nodes()
57 .iter()
58 .filter(|node| node.name().to_string() == "pkg")
59 .map(|node| LockfileNode::from_kdl(node, false))
60 .map(|node| {
61 let node = node?;
62 let path_str = node
63 .path
64 .iter()
65 .map(|x| x.to_string())
66 .collect::<Vec<_>>()
67 .join("/node_modules/");
68 Ok((UniCase::from(path_str), node))
69 })
70 .collect::<Result<IndexMap<UniCase<String>, LockfileNode>, NodeMaintainerError>>(
71 )?;
72 Ok(Lockfile {
73 version: kdl
74 .get_arg("lockfile-version")
75 .and_then(|v| v.as_i64())
76 .map(|v| v.try_into())
77 .transpose()
78 .map_err(|_| NodeMaintainerError::InvalidLockfileVersion)?
80 .unwrap_or(1),
81 root: kdl
82 .get("root")
83 .ok_or_else(|| NodeMaintainerError::KdlLockMissingRoot(kdl.clone()))
85 .and_then(|node| LockfileNode::from_kdl(node, true))?,
86 packages,
87 })
88 }
89 inner(kdl)
90 }
91
92 pub fn from_npm(npm: impl AsRef<str>) -> Result<Self, NodeMaintainerError> {
93 let pkglock: NpmPackageLock = serde_json::from_str(npm.as_ref())?;
94 fn inner(npm: NpmPackageLock) -> Result<Lockfile, NodeMaintainerError> {
95 let packages = npm
96 .packages
97 .iter()
98 .map(|(path, entry)| LockfileNode::from_npm(path, entry))
99 .map(|node| {
100 let node = node?;
101 let path_str = node
102 .path
103 .iter()
104 .map(|x| x.to_string())
105 .collect::<Vec<_>>()
106 .join("/node_modules/");
107 Ok((UniCase::from(path_str), node))
108 })
109 .collect::<Result<IndexMap<UniCase<String>, LockfileNode>, NodeMaintainerError>>(
110 )?;
111 Ok(Lockfile {
112 version: npm
113 .lockfile_version
114 .map(|v| v.try_into())
115 .transpose()
116 .map_err(|_| NodeMaintainerError::InvalidLockfileVersion)?
118 .unwrap_or(3),
119 root: npm
120 .packages
121 .get("")
122 .ok_or_else(|| NodeMaintainerError::NpmLockMissingRoot(npm.clone()))
123 .and_then(|node| LockfileNode::from_npm("", node))?,
124 packages,
125 })
126 }
127 inner(pkglock)
128 }
129}
130
131#[derive(Default, Debug, Clone, PartialEq, Eq)]
132pub struct LockfileNode {
133 pub name: UniCase<String>,
134 pub is_root: bool,
135 pub path: Vec<UniCase<String>>,
136 pub resolved: Option<String>,
137 pub version: Option<Version>,
138 pub integrity: Option<Integrity>,
139 pub dependencies: IndexMap<String, String>,
140 pub dev_dependencies: IndexMap<String, String>,
141 pub peer_dependencies: IndexMap<String, String>,
142 pub optional_dependencies: IndexMap<String, String>,
143}
144
145impl From<LockfileNode> for CorgiManifest {
146 fn from(value: LockfileNode) -> Self {
147 CorgiManifest {
148 name: Some(value.name.to_string()),
149 version: value.version,
150 dependencies: value.dependencies,
151 dev_dependencies: value.dev_dependencies,
152 peer_dependencies: value.peer_dependencies,
153 optional_dependencies: value.optional_dependencies,
154 bundled_dependencies: None,
155 }
156 }
157}
158
159impl LockfileNode {
160 pub(crate) async fn to_package(
161 &self,
162 nassun: &Nassun,
163 ) -> Result<Option<Package>, NodeMaintainerError> {
164 let spec = match (self.resolved.as_ref(), self.version.as_ref()) {
165 (Some(resolved), Some(version)) if resolved.starts_with("http") => {
166 format!("{}@{version}", self.name)
167 }
168 (Some(resolved), _) => format!("{}@{resolved}", self.name),
169 (_, Some(version)) => format!("{}@{version}", self.name),
170 _ => {
171 return Ok(None);
173 }
174 };
175 let spec: PackageSpec = spec.parse()?;
176 let package = match &spec.target() {
177 PackageSpec::Dir { path } => {
178 let resolution = PackageResolution::Dir {
179 name: self.name.to_string(),
180 path: path.clone(),
181 };
182 nassun.resolve_from(self.name.to_string(), spec, resolution)
183 }
184 PackageSpec::Npm { name, .. } => {
185 let version = if let Some(ref version) = self.version {
186 version
187 } else {
188 return Err(NodeMaintainerError::MissingVersion);
189 };
190 if let Some(ref url) = self.resolved {
191 let resolution = PackageResolution::Npm {
192 name: name.clone(),
193 version: version.clone(),
194 tarball: url
195 .parse()
196 .map_err(|e| NodeMaintainerError::UrlParseError(url.clone(), e))?,
197 integrity: self.integrity.clone(),
198 };
199 nassun.resolve_from(self.name.to_string(), spec, resolution)
200 } else {
201 nassun.resolve(spec.to_string()).await?
202 }
203 }
204 PackageSpec::Git(info) => {
205 if info.committish().is_some() {
206 let resolution = PackageResolution::Git {
207 name: self.name.to_string(),
208 info: info.clone(),
209 };
210 nassun.resolve_from(self.name.to_string(), spec, resolution)
211 } else {
212 nassun.resolve(spec.to_string()).await?
213 }
214 }
215 PackageSpec::Alias { .. } => {
216 unreachable!("Alias should have already been resolved by the .target() call above.")
217 }
218 };
219 Ok(Some(package))
220 }
221
222 fn from_kdl(node: &KdlNode, is_root: bool) -> Result<Self, NodeMaintainerError> {
223 let children = node.children().cloned().unwrap_or_else(KdlDocument::new);
224 let path = node
225 .entries()
226 .iter()
227 .filter(|e| e.value().is_string() && e.name().is_none())
228 .map(|e| {
229 UniCase::new(
230 e.value()
231 .as_string()
232 .expect("We already checked that it's a string, above.")
233 .into(),
234 )
235 })
236 .collect::<Vec<_>>();
237 let name = if is_root {
238 UniCase::new("".into())
239 } else {
240 path.last()
241 .cloned()
242 .ok_or_else(|| NodeMaintainerError::KdlLockMissingName(node.clone()))?
244 };
245 let integrity = children
246 .get_arg("integrity")
247 .and_then(|i| i.as_string())
248 .map(|i| i.parse())
249 .transpose()
250 .map_err(|e| NodeMaintainerError::KdlLockfileIntegrityParseError(node.clone(), e))?;
251 let version = children
252 .get_arg("version")
253 .and_then(|val| val.as_string())
254 .map(|val| {
255 val.parse()
256 .map_err(NodeMaintainerError::SemverParseError)
258 })
259 .transpose()?;
260 let resolved = children
261 .get_arg("resolved")
262 .and_then(|resolved| resolved.as_string())
263 .map(|resolved| resolved.to_string());
264 Ok(Self {
265 name,
266 is_root,
267 path,
268 integrity,
269 resolved,
270 version,
271 dependencies: Self::from_kdl_deps(&children, &DepType::Prod)?,
272 dev_dependencies: Self::from_kdl_deps(&children, &DepType::Dev)?,
273 optional_dependencies: Self::from_kdl_deps(&children, &DepType::Opt)?,
274 peer_dependencies: Self::from_kdl_deps(&children, &DepType::Peer)?,
275 })
276 }
277
278 fn from_kdl_deps(
279 children: &KdlDocument,
280 dep_type: &DepType,
281 ) -> Result<IndexMap<String, String>, NodeMaintainerError> {
282 use DepType::*;
283 let type_name = match dep_type {
284 Prod => "dependencies",
285 Dev => "dev-dependencies",
286 Peer => "peer-dependencies",
287 Opt => "optional-dependencies",
288 };
289 let mut deps = IndexMap::new();
290 if let Some(node) = children.get(type_name) {
291 if let Some(children) = node.children() {
292 for dep in children.nodes() {
293 let name = dep.name().value().to_string();
294 let spec = dep.get(0).and_then(|spec| spec.as_string()).unwrap_or("*");
295 deps.insert(name.clone(), spec.into());
296 }
297 }
298 }
299 Ok(deps)
300 }
301
302 fn to_kdl(&self) -> KdlNode {
303 let mut kdl_node = if self.is_root {
304 KdlNode::new("root")
305 } else {
306 KdlNode::new("pkg")
307 };
308 for name in &self.path {
309 kdl_node.push(name.as_ref());
310 }
311 if let Some(ref version) = self.version {
312 let mut vnode = KdlNode::new("version");
313 vnode.push(version.to_string());
314 kdl_node.ensure_children().nodes_mut().push(vnode);
315 }
316 if let Some(resolved) = &self.resolved {
317 if !self.is_root {
318 let mut rnode = KdlNode::new("resolved");
319 rnode.push(resolved.to_string());
320 kdl_node.ensure_children().nodes_mut().push(rnode);
321
322 if let Some(integrity) = &self.integrity {
323 let mut inode = KdlNode::new("integrity");
324 inode.push(integrity.to_string());
325 kdl_node.ensure_children().nodes_mut().push(inode);
326 }
327 }
328 }
329 if !self.dependencies.is_empty() {
330 kdl_node
331 .ensure_children()
332 .nodes_mut()
333 .push(self.to_kdl_deps(&DepType::Prod, &self.dependencies));
334 }
335 if !self.dev_dependencies.is_empty() {
336 kdl_node
337 .ensure_children()
338 .nodes_mut()
339 .push(self.to_kdl_deps(&DepType::Dev, &self.dev_dependencies));
340 }
341 if !self.peer_dependencies.is_empty() {
342 kdl_node
343 .ensure_children()
344 .nodes_mut()
345 .push(self.to_kdl_deps(&DepType::Peer, &self.peer_dependencies));
346 }
347 if !self.optional_dependencies.is_empty() {
348 kdl_node
349 .ensure_children()
350 .nodes_mut()
351 .push(self.to_kdl_deps(&DepType::Opt, &self.optional_dependencies));
352 }
353 kdl_node
354 }
355
356 fn to_kdl_deps(&self, dep_type: &DepType, deps: &IndexMap<String, String>) -> KdlNode {
357 use DepType::*;
358 let type_name = match dep_type {
359 Prod => "dependencies",
360 Dev => "dev-dependencies",
361 Peer => "peer-dependencies",
362 Opt => "optional-dependencies",
363 };
364 let mut deps_node = KdlNode::new(type_name);
365 for (name, requested) in deps {
366 let children = deps_node.ensure_children();
367 let mut ddnode = KdlNode::new(name.clone());
368 ddnode.push(requested.clone());
369 children.nodes_mut().push(ddnode);
370 }
371 deps_node
372 .ensure_children()
373 .nodes_mut()
374 .sort_by_key(|n| n.name().value().to_string());
375 deps_node
376 }
377
378 fn from_npm(path_str: &str, npm: &NpmPackageLockEntry) -> Result<Self, NodeMaintainerError> {
379 let mut path = "/".to_string();
380 path.push_str(path_str);
381 let path = path
382 .split("/node_modules/")
383 .skip(1)
384 .map(|s| s.into())
385 .collect::<Vec<_>>();
386 let name = if path_str.is_empty() {
387 UniCase::new("".into())
388 } else {
389 npm.name
390 .clone()
391 .map(UniCase::new)
392 .or_else(|| path.last().cloned())
393 .ok_or_else(|| NodeMaintainerError::NpmLockMissingName(Box::new(npm.clone())))?
394 };
395 let integrity = npm
396 .integrity
397 .as_ref()
398 .map(|i| i.parse())
399 .transpose()
400 .map_err(|e| {
401 NodeMaintainerError::NpmLockfileIntegrityParseError(Box::new(npm.clone()), e)
402 })?;
403 let version = npm
404 .version
405 .as_ref()
406 .map(|val| val.parse().map_err(NodeMaintainerError::SemverParseError))
407 .transpose()?;
408 Ok(Self {
409 name,
410 is_root: path.is_empty(),
411 path,
412 integrity,
413 resolved: npm.resolved.clone(),
414 version,
415 dependencies: npm.dependencies.clone(),
416 dev_dependencies: npm.dev_dependencies.clone(),
417 optional_dependencies: npm.optional_dependencies.clone(),
418 peer_dependencies: npm.peer_dependencies.clone(),
419 })
420 }
421}
422
423#[derive(Debug, Clone, Deserialize, Serialize)]
424#[serde(rename_all = "camelCase")]
425pub struct NpmPackageLock {
426 #[serde(default)]
427 pub lockfile_version: Option<usize>,
428 #[serde(default)]
429 pub requires: bool,
430 #[serde(default)]
431 pub packages: IndexMap<String, NpmPackageLockEntry>,
432}
433
434#[derive(Debug, Clone, Deserialize, Serialize)]
435#[serde(rename_all = "camelCase")]
436pub struct NpmPackageLockEntry {
437 #[serde(default)]
438 pub name: Option<String>,
439 #[serde(default)]
440 pub version: Option<String>,
441 #[serde(default)]
442 pub resolved: Option<String>,
443 #[serde(default)]
444 pub integrity: Option<String>,
445 #[serde(default)]
446 pub dependencies: IndexMap<String, String>,
447 #[serde(default)]
448 pub dev_dependencies: IndexMap<String, String>,
449 #[serde(default)]
450 pub optional_dependencies: IndexMap<String, String>,
451 #[serde(default)]
452 pub peer_dependencies: IndexMap<String, String>,
453}