typescript_tools/
monorepo_manifest.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::{Path, PathBuf};
4
5use globwalk::{FileType, GlobWalkerBuilder};
6use pariter::IteratorExt;
7use serde::Deserialize;
8
9use crate::configuration_file::ConfigurationFile;
10use crate::io::{read_json_from_file, FromFileError};
11use crate::package_manifest::PackageManifest;
12use crate::types::{Directory, PackageName};
13
14#[derive(Debug, Deserialize)]
15struct PackageManifestGlob(String);
16
17#[derive(Debug, Clone, Copy)]
18pub enum MonorepoKind {
19    Lerna,
20    Workspace,
21}
22
23impl Into<&'static str> for MonorepoKind {
24    fn into(self) -> &'static str {
25        match self {
26            MonorepoKind::Lerna => "lerna",
27            MonorepoKind::Workspace => "workspace",
28        }
29    }
30}
31
32#[derive(Debug, Deserialize)]
33struct LernaManifest {
34    packages: Vec<PackageManifestGlob>,
35}
36
37#[derive(Debug, Deserialize)]
38struct WorkspaceManifest {
39    workspaces: Vec<PackageManifestGlob>,
40}
41
42#[derive(Debug)]
43pub struct MonorepoManifest {
44    pub kind: MonorepoKind,
45    pub root: Directory,
46    globs: Vec<PackageManifestGlob>,
47}
48
49#[derive(Debug)]
50#[non_exhaustive]
51pub struct GlobError {
52    pub kind: GlobErrorKind,
53}
54
55impl Display for GlobError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "unable to enumerate monorepo packages")
58    }
59}
60
61impl std::error::Error for GlobError {
62    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63        Some(&self.kind)
64    }
65}
66
67impl From<GlobErrorKind> for GlobError {
68    fn from(kind: GlobErrorKind) -> Self {
69        Self { kind }
70    }
71}
72
73#[derive(Debug)]
74pub enum GlobErrorKind {
75    #[non_exhaustive]
76    GlobNotValidUtf8(PathBuf),
77    #[non_exhaustive]
78    GlobWalkBuilderError(globwalk::GlobError),
79}
80
81impl Display for GlobErrorKind {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            GlobErrorKind::GlobNotValidUtf8(glob) => {
85                write!(f, "glob cannot be expressed in UTF-8: {:?}", glob)
86            }
87            GlobErrorKind::GlobWalkBuilderError(_) => {
88                write!(f, "unable to build glob walker")
89            }
90        }
91    }
92}
93
94impl std::error::Error for GlobErrorKind {
95    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
96        match &self {
97            GlobErrorKind::GlobNotValidUtf8(_) => None,
98            GlobErrorKind::GlobWalkBuilderError(err) => Some(err),
99        }
100    }
101}
102
103#[derive(Debug)]
104#[non_exhaustive]
105pub struct WalkError {
106    pub kind: WalkErrorKind,
107}
108
109impl Display for WalkError {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(f, "unable to enumerate monorepo packages")
112    }
113}
114
115impl std::error::Error for WalkError {
116    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117        Some(&self.kind)
118    }
119}
120
121impl From<globwalk::WalkError> for WalkError {
122    fn from(err: globwalk::WalkError) -> Self {
123        Self {
124            kind: WalkErrorKind::GlobWalkError(err),
125        }
126    }
127}
128
129impl From<WalkErrorKind> for WalkError {
130    fn from(kind: WalkErrorKind) -> Self {
131        Self { kind }
132    }
133}
134
135#[derive(Debug)]
136pub enum WalkErrorKind {
137    #[non_exhaustive]
138    GlobWalkError(globwalk::WalkError),
139    #[non_exhaustive]
140    FromFile(FromFileError),
141    #[non_exhaustive]
142    PackageInMonorepoRoot(PathBuf),
143}
144
145impl Display for WalkErrorKind {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match self {
148            WalkErrorKind::FromFile(_) => {
149                write!(f, "unable to reading file")
150            }
151            WalkErrorKind::GlobWalkError(_) => {
152                write!(f, "error walking directory tree")
153            }
154            WalkErrorKind::PackageInMonorepoRoot(path) => {
155                write!(f, "package in monorepo root: {:?}", path)
156            }
157        }
158    }
159}
160
161impl std::error::Error for WalkErrorKind {
162    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
163        match &self {
164            WalkErrorKind::FromFile(err) => err.source(),
165            WalkErrorKind::GlobWalkError(err) => Some(err),
166            WalkErrorKind::PackageInMonorepoRoot(_) => None,
167        }
168    }
169}
170
171#[derive(Debug)]
172pub enum EnumeratePackageManifestsError {
173    #[non_exhaustive]
174    GlobError(GlobError),
175    #[non_exhaustive]
176    WalkError(WalkError),
177}
178
179impl Display for EnumeratePackageManifestsError {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        write!(f, "unable to enumerate monorepo packages")
182    }
183}
184
185impl std::error::Error for EnumeratePackageManifestsError {
186    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
187        match &self {
188            EnumeratePackageManifestsError::GlobError(err) => Some(err),
189            EnumeratePackageManifestsError::WalkError(err) => Some(err),
190        }
191    }
192}
193
194impl From<GlobError> for EnumeratePackageManifestsError {
195    fn from(err: GlobError) -> Self {
196        Self::GlobError(err)
197    }
198}
199
200impl From<WalkError> for EnumeratePackageManifestsError {
201    fn from(err: WalkError) -> Self {
202        Self::WalkError(err)
203    }
204}
205
206fn get_internal_package_manifests(
207    monorepo_root: &Directory,
208    package_globs: &[PackageManifestGlob],
209) -> Result<impl Iterator<Item = Result<PackageManifest, WalkError>>, GlobError> {
210    let mut package_manifests: Vec<String> = package_globs
211        .iter()
212        .map(|package_manifest_glob| {
213            let glob = Path::new(&package_manifest_glob.0).join("package.json");
214            glob.to_str().map(ToOwned::to_owned).ok_or(GlobError {
215                kind: GlobErrorKind::GlobNotValidUtf8(glob),
216            })
217        })
218        .collect::<Result<_, _>>()?;
219
220    // ignore paths to speed up file-system walk
221    package_manifests.push(String::from("!node_modules/"));
222
223    // Take ownership so we can move this value into the parallel_map
224    let monorepo_root = monorepo_root.to_owned();
225
226    // DISCUSS: can we enforce _here_ that there are no files in the monorepo root,
227    // and never have to check that again anywhere else?
228    let package_manifests_iter =
229        GlobWalkerBuilder::from_patterns(&monorepo_root, &package_manifests)
230            .file_type(FileType::FILE)
231            .min_depth(1)
232            .build()
233            .map_err(|err| GlobError {
234                kind: GlobErrorKind::GlobWalkBuilderError(err),
235            })?
236            .parallel_map_custom(
237                |options| options.threads(32),
238                move |dir_entry| -> Result<PackageManifest, WalkError> {
239                    let dir_entry = dir_entry?;
240                    let path = dir_entry.path();
241                    let manifest = PackageManifest::from_directory(
242                        &monorepo_root,
243                        Directory::unchecked_from_path(
244                            path.parent()
245                                .ok_or_else(|| {
246                                    WalkErrorKind::PackageInMonorepoRoot(path.to_owned())
247                                })?
248                                .strip_prefix(&monorepo_root)
249                                .expect("expected all files to be children of monorepo root"),
250                        ),
251                    )
252                    .map_err(WalkErrorKind::FromFile)?;
253                    Ok(manifest)
254                },
255            );
256
257    Ok(package_manifests_iter)
258}
259
260impl MonorepoManifest {
261    const LERNA_MANIFEST_FILENAME: &'static str = "lerna.json";
262    const PACKAGE_MANIFEST_FILENAME: &'static str = "package.json";
263
264    fn from_lerna_manifest(root: &Path) -> Result<MonorepoManifest, FromFileError> {
265        let filename = root.join(Self::LERNA_MANIFEST_FILENAME);
266        let lerna_manifest: LernaManifest = read_json_from_file(&filename)?;
267        Ok(MonorepoManifest {
268            kind: MonorepoKind::Lerna,
269            root: Directory::unchecked_from_path(root),
270            globs: lerna_manifest.packages,
271        })
272    }
273
274    fn from_package_manifest(root: &Path) -> Result<MonorepoManifest, FromFileError> {
275        let filename = root.join(Self::PACKAGE_MANIFEST_FILENAME);
276        let package_manifest: WorkspaceManifest = read_json_from_file(&filename)?;
277        Ok(MonorepoManifest {
278            kind: MonorepoKind::Workspace,
279            root: Directory::unchecked_from_path(root),
280            globs: package_manifest.workspaces,
281        })
282    }
283
284    pub fn from_directory<P>(root: P) -> Result<MonorepoManifest, FromFileError>
285    where
286        P: AsRef<Path>,
287    {
288        MonorepoManifest::from_lerna_manifest(root.as_ref())
289            .or_else(|_| MonorepoManifest::from_package_manifest(root.as_ref()))
290    }
291
292    pub fn package_manifests_by_package_name(
293        &self,
294    ) -> Result<HashMap<PackageName, PackageManifest>, EnumeratePackageManifestsError> {
295        let map = get_internal_package_manifests(&self.root, &self.globs)?
296            .map(|maybe_manifest| -> Result<_, WalkError> {
297                let manifest = maybe_manifest?;
298                Ok((manifest.contents.name.to_owned(), manifest))
299            })
300            .collect::<Result<_, _>>()?;
301        Ok(map)
302    }
303
304    pub fn internal_package_manifests(
305        &self,
306    ) -> Result<impl Iterator<Item = Result<PackageManifest, WalkError>>, GlobError> {
307        get_internal_package_manifests(&self.root, &self.globs)
308    }
309}