typescript_tools/
package_manifest.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::configuration_file::ConfigurationFile;
7use crate::io::{read_json_from_file, FromFileError};
8use crate::types::{Directory, PackageName};
9
10#[derive(Serialize, Deserialize, Clone, Debug)]
11#[serde(rename_all = "camelCase")]
12pub struct PackageManifestFile {
13    pub name: PackageName,
14    pub version: String,
15    #[serde(flatten)]
16    pub extra_fields: serde_json::Map<String, serde_json::Value>,
17}
18
19#[derive(Clone, Debug)]
20pub struct PackageManifest {
21    relative_directory: Directory,
22    pub contents: PackageManifestFile,
23}
24
25#[derive(Debug)]
26pub(crate) struct DependencyGroup;
27
28impl DependencyGroup {
29    pub(crate) const VALUES: [&str; 4] = [
30        "dependencies",
31        "devDependencies",
32        "optionalDependencies",
33        "peerDependencies",
34    ];
35}
36
37impl ConfigurationFile for PackageManifest {
38    type Contents = PackageManifestFile;
39
40    const FILENAME: &'static str = "package.json";
41
42    fn from_directory(
43        monorepo_root: &Directory,
44        relative_directory: Directory,
45    ) -> Result<Self, FromFileError> {
46        let filename = monorepo_root.join(&relative_directory).join(Self::FILENAME);
47        let manifest_contents: PackageManifestFile = read_json_from_file(&filename)?;
48        Ok(PackageManifest {
49            relative_directory,
50            contents: manifest_contents,
51        })
52    }
53
54    fn directory(&self) -> &Directory {
55        &self.relative_directory
56    }
57
58    fn path(&self) -> PathBuf {
59        self.relative_directory.join(Self::FILENAME)
60    }
61
62    fn contents(&self) -> &PackageManifestFile {
63        &self.contents
64    }
65}
66
67impl AsRef<PackageManifest> for PackageManifest {
68    fn as_ref(&self) -> &Self {
69        self
70    }
71}
72
73impl PackageManifest {
74    // REFACTOR: for nearness
75    // Get the dependency
76    pub(crate) fn get_dependency_version<S>(&self, dependency: S) -> Option<String>
77    where
78        S: AsRef<str>,
79    {
80        DependencyGroup::VALUES
81            .iter()
82            // only iterate over the objects corresponding to each dependency group
83            .filter_map(|dependency_group| {
84                self.contents
85                    .extra_fields
86                    .get(*dependency_group)?
87                    .as_object()
88            })
89            // get the target dependency version, if exists
90            .filter_map(|dependency_group_value| {
91                dependency_group_value
92                    .get(dependency.as_ref())
93                    .and_then(|version_value| version_value.as_str().map(|a| a.to_owned()))
94            })
95            .take(1)
96            .next()
97    }
98
99    pub(crate) fn dependencies_iter(
100        &self,
101    ) -> impl Iterator<Item = (PackageName, &serde_json::Value)> {
102        DependencyGroup::VALUES
103            .iter()
104            .filter_map(|dependency_group| {
105                self.contents
106                    .extra_fields
107                    .get(*dependency_group)?
108                    .as_object()
109            })
110            .flat_map(|object| object.iter())
111            .map(|(package_name, package_version)| {
112                (PackageName::from(package_name), package_version)
113            })
114    }
115
116    pub(crate) fn internal_dependencies_iter<'a>(
117        &'a self,
118        package_manifests_by_package_name: &'a HashMap<PackageName, PackageManifest>,
119    ) -> impl Iterator<Item = &'a PackageManifest> {
120        DependencyGroup::VALUES
121            .iter()
122            // only iterate over the objects corresponding to each dependency group
123            .filter_map(|dependency_group| {
124                self.contents
125                    .extra_fields
126                    .get(*dependency_group)?
127                    .as_object()
128            })
129            // get all dependency names from all groups
130            .flat_map(|dependency_group_value| dependency_group_value.keys())
131            // filter out external packages
132            .filter_map(|package_name| {
133                package_manifests_by_package_name.get(&PackageName::from(package_name))
134            })
135    }
136
137    pub(crate) fn transitive_internal_dependency_package_names_exclusive<'a>(
138        &'a self,
139        package_manifest_by_package_name: &'a HashMap<PackageName, PackageManifest>,
140    ) -> impl Iterator<Item = &'a PackageManifest> {
141        // Depth-first search all transitive internal dependencies of package
142        let mut seen_package_names = HashSet::new();
143        let mut internal_dependencies = HashSet::new();
144        let mut to_visit_package_manifests = VecDeque::new();
145
146        to_visit_package_manifests.push_back(self);
147
148        while let Some(current_manifest) = to_visit_package_manifests.pop_front() {
149            seen_package_names.insert(&current_manifest.contents.name);
150
151            for dependency in
152                current_manifest.internal_dependencies_iter(package_manifest_by_package_name)
153            {
154                internal_dependencies.insert(&dependency.contents.name);
155                if !seen_package_names.contains(&dependency.contents.name) {
156                    to_visit_package_manifests.push_back(dependency);
157                }
158            }
159        }
160
161        internal_dependencies
162            .into_iter()
163            .map(|dependency_package_name| {
164                package_manifest_by_package_name
165                    .get(dependency_package_name)
166                    .unwrap()
167            })
168    }
169
170    // REFACTOR: for nearness
171    // Name of the archive generated by `npm pack`, for example "myscope-a-cool-package-1.0.0.tgz"
172    pub(crate) fn npm_pack_file_basename(&self) -> String {
173        format!(
174            "{}-{}.tgz",
175            self.contents
176                .name
177                .as_str()
178                .trim_start_matches('@')
179                .replace('/', "-"),
180            &self.contents.version,
181        )
182    }
183
184    pub(crate) fn unscoped_package_name(&self) -> &str {
185        match &self.contents.name.as_str().rsplit_once('/') {
186            Some((_scope, name)) => name,
187            None => &self.contents.name.as_str(),
188        }
189    }
190}