chaste_pnpm/
lib.rs

1// SPDX-FileCopyrightText: 2024 The Chaste Authors
2// SPDX-License-Identifier: Apache-2.0 OR BSD-2-Clause
3
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8use chaste_types::{
9    package_name_part, Chastefile, ChastefileBuilder, Checksums, DependencyBuilder, DependencyKind,
10    InstallationBuilder, Integrity, ModulePath, PackageBuilder, PackageName, PackageSource,
11    SourceVersionSpecifier, PACKAGE_JSON_FILENAME,
12};
13use nom::bytes::complete::tag;
14use nom::combinator::{opt, recognize, rest, verify};
15use nom::sequence::{preceded, terminated};
16use nom::{IResult, Parser};
17
18pub use crate::error::Error;
19use crate::error::Result;
20
21mod error;
22#[cfg(test)]
23mod tests;
24mod types;
25
26pub static LOCKFILE_NAME: &str = "pnpm-lock.yaml";
27
28fn package_name(input: &str) -> IResult<&str, &str, nom::error::Error<&str>> {
29    recognize((
30        opt(preceded(tag("@"), terminated(package_name_part, tag("/")))),
31        verify(package_name_part, |part: &str| {
32            part != "node_modules" && part != "favicon.ico"
33        }),
34    ))
35    .parse(input)
36}
37
38pub fn parse<P>(root_dir: P) -> Result<Chastefile>
39where
40    P: AsRef<Path>,
41{
42    let root_dir = root_dir.as_ref();
43
44    let lockfile_contents = fs::read_to_string(root_dir.join(LOCKFILE_NAME))?;
45    let lockfile: types::Lockfile = serde_norway::from_str(&lockfile_contents)?;
46
47    if lockfile.lockfile_version != "9.0" {
48        return Err(Error::UnknownLockfileVersion(
49            lockfile.lockfile_version.to_string(),
50        ));
51    }
52
53    let mut chastefile = ChastefileBuilder::new();
54
55    let mut importer_to_pid = HashMap::with_capacity(lockfile.importers.len());
56    for importer_path in lockfile.importers.keys() {
57        let package_json_contents = fs::read_to_string(if *importer_path == "." {
58            root_dir.join(PACKAGE_JSON_FILENAME)
59        } else {
60            root_dir.join(importer_path).join(PACKAGE_JSON_FILENAME)
61        })?;
62        let package_json: types::PackageJson = serde_json::from_str(&package_json_contents)?;
63        let importer_pkg = PackageBuilder::new(
64            package_json
65                .name
66                .map(|n| PackageName::new(n.to_string()))
67                .transpose()?,
68            package_json.version.map(|v| v.to_string()),
69        );
70        let importer_pid = chastefile.add_package(importer_pkg.build()?)?;
71        if *importer_path == "." {
72            chastefile.set_root_package_id(importer_pid)?;
73        } else {
74            chastefile.set_as_workspace_member(importer_pid)?;
75        }
76        importer_to_pid.insert(importer_path, importer_pid);
77        let installation = InstallationBuilder::new(
78            importer_pid,
79            ModulePath::new(if *importer_path == "." {
80                "".to_string()
81            } else {
82                importer_path.to_string()
83            })?,
84        )
85        .build()?;
86        chastefile.add_package_installation(installation);
87    }
88
89    let mut desc_pid = HashMap::with_capacity(lockfile.packages.len());
90    for (pkg_desc, pkg) in &lockfile.packages {
91        let (_, (package_name, _, package_svd)) =
92            (package_name, tag("@"), rest)
93                .parse(&pkg_desc)
94                .map_err(|_| Error::InvalidPackageDescriptor(pkg_desc.to_string()))?;
95        let version = pkg
96            .version
97            .as_deref()
98            .or(Some(package_svd))
99            .map(|v| v.to_string());
100        let mut package =
101            PackageBuilder::new(Some(PackageName::new(package_name.to_string())?), version);
102        if let Some(integrity) = pkg.resolution.integrity {
103            let inte: Integrity = integrity.parse()?;
104            if !inte.hashes.is_empty() {
105                package.checksums(Checksums::Tarball(inte));
106            }
107        }
108        if let Some(tarball_url) = &pkg.resolution.tarball {
109            // If there is a checksum, it's a custom registry.
110            if pkg.resolution.integrity.is_some() {
111                package.source(PackageSource::Npm);
112            } else {
113                package.source(PackageSource::TarballURL {
114                    url: tarball_url.to_string(),
115                });
116            }
117        } else if let Some(git_url) = package_svd.strip_prefix("git+") {
118            package.source(PackageSource::Git {
119                url: git_url.to_string(),
120            });
121        } else if SourceVersionSpecifier::new(package_svd.to_string()).is_ok_and(|svs| svs.is_npm())
122        {
123            package.source(PackageSource::Npm);
124        }
125        let pkg_pid = chastefile.add_package(package.build()?)?;
126        desc_pid.insert(
127            (package_name, package_svd),
128            (pkg_pid, &pkg.peer_dependencies, &pkg.peer_dependencies_meta),
129        );
130    }
131
132    for (importer_path, importer) in &lockfile.importers {
133        let importer_pid = *importer_to_pid.get(importer_path).unwrap();
134        for (dependencies, kind) in [
135            (&importer.dependencies, DependencyKind::Dependency),
136            (&importer.dev_dependencies, DependencyKind::DevDependency),
137            (&importer.peer_dependencies, DependencyKind::PeerDependency),
138            (
139                &importer.optional_dependencies,
140                DependencyKind::OptionalDependency,
141            ),
142        ] {
143            for (dep_name, d) in dependencies {
144                if d.version.starts_with("link:") {
145                    // TODO:
146                    continue;
147                }
148                let mut is_aliased = false;
149                let dep_pid = {
150                    if let Some((dep_pid, _, _)) = desc_pid.get(&(&dep_name, &d.version)) {
151                        *dep_pid
152                    } else if let Ok((aliased_dep_svd, aliased_dep_name)) =
153                        terminated(package_name, tag("@")).parse(&d.version)
154                    {
155                        if let Some((dep_pid, _, _)) =
156                            desc_pid.get(&(aliased_dep_name, aliased_dep_svd))
157                        {
158                            is_aliased = true;
159                            *dep_pid
160                        } else {
161                            return Err(Error::DependencyPackageNotFound(d.version.to_string()));
162                        }
163                    } else {
164                        return Err(Error::DependencyPackageNotFound(format!(
165                            "{dep_name}@{}",
166                            d.version
167                        )));
168                    }
169                };
170                let mut dep = DependencyBuilder::new(kind, importer_pid, dep_pid);
171                if is_aliased {
172                    dep.alias_name(PackageName::new(dep_name.to_string())?);
173                }
174                dep.svs(SourceVersionSpecifier::new(d.specifier.to_string())?);
175                chastefile.add_dependency(dep.build());
176            }
177        }
178    }
179
180    for (pkg_desc, snap) in lockfile.snapshots {
181        let Some((pkg_svd, pkg_name)) = terminated(package_name, tag("@")).parse(&pkg_desc).ok()
182        else {
183            unreachable!();
184        };
185        // TODO: handle peer dependencies properly
186        // https://codeberg.org/selfisekai/chaste/issues/46
187        let pkg_svd = pkg_svd.split_once("(").map(|(s, _)| s).unwrap_or(pkg_svd);
188        let (pkg_pid, _pkg_peers, _pkg_peers_meta) = *desc_pid
189            .get(&(pkg_name, pkg_svd))
190            .ok_or_else(|| Error::SnapshotNotFound(pkg_desc.to_string()))?;
191        for (dependencies, kind) in [
192            (&snap.dependencies, DependencyKind::Dependency),
193            (&snap.dev_dependencies, DependencyKind::DevDependency),
194            (
195                &snap.optional_dependencies,
196                DependencyKind::OptionalDependency,
197            ),
198        ] {
199            for (dep_name, dep_svd) in dependencies {
200                let dep_svd = dep_svd.split_once("(").map(|(s, _)| s).unwrap_or(&dep_svd);
201                let dep = DependencyBuilder::new(kind, pkg_pid, {
202                    if let Some((dep_pid, _, _)) = desc_pid.get(&(&dep_name, dep_svd)) {
203                        *dep_pid
204                    } else if let Ok((aliased_dep_svd, aliased_dep_name)) =
205                        terminated(package_name, tag("@")).parse(dep_svd)
206                    {
207                        if let Some((dep_pid, _, _)) =
208                            desc_pid.get(&(aliased_dep_name, aliased_dep_svd))
209                        {
210                            *dep_pid
211                        } else {
212                            return Err(Error::DependencyPackageNotFound(dep_svd.to_string()));
213                        }
214                    } else {
215                        return Err(Error::DependencyPackageNotFound(format!(
216                            "{dep_name}@{dep_svd}"
217                        )));
218                    }
219                });
220                chastefile.add_dependency(dep.build());
221            }
222        }
223    }
224
225    Ok(chastefile.build()?)
226}