node_maintainer/
lockfile.rs

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/// A representation of a resolved lockfile.
14#[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                    // TODO: add a miette span here
79                    .map_err(|_| NodeMaintainerError::InvalidLockfileVersion)?
80                    .unwrap_or(1),
81                root: kdl
82                    .get("root")
83                    // TODO: add a miette span here
84                    .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                    // TODO: add a miette span here
117                    .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                // Nothing we can do here, we don't have enough information to resolve the package.
172                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                // TODO: add a miette span here
243                .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                    // TODO: add a miette span here
257                    .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}