npmgen_core/project/
workspace.rs1use std::collections::BTreeMap;
2use std::path::Path;
3
4use cargo_metadata::{Metadata, MetadataCommand, Package};
5
6use super::{METADATA_KEY, Overrides, Project, ProjectError};
7use crate::config::Config;
8
9pub struct Workspace {
12 metadata: Metadata,
13}
14
15impl Workspace {
16 pub fn load(manifest_path: &Path) -> Result<Self, ProjectError> {
18 let metadata = MetadataCommand::new()
19 .manifest_path(manifest_path)
20 .exec()
21 .map_err(|source| ProjectError::Metadata {
22 source: Box::new(source),
23 })?;
24 Ok(Self { metadata })
25 }
26
27 pub fn projects(&self, overrides: &Overrides) -> Result<Vec<Project>, ProjectError> {
38 let workspace_root = self.metadata.workspace_root.as_std_path();
39 let target_directory = self.metadata.target_directory.as_std_path();
40 let workspace_config = self.workspace_config()?;
41
42 let mut projects = Vec::new();
43 for package in self.selected_packages(overrides)? {
44 let bins = Self::selected_bins(package, overrides);
45 if bins.is_empty() {
46 continue;
47 }
48 let config = Self::package_config(package)?.inherit(&workspace_config);
49 for bin in bins {
50 projects.push(Project::from_package_bin(
51 package,
52 bin,
53 &config,
54 overrides,
55 workspace_root,
56 target_directory,
57 )?);
58 }
59 }
60
61 self.reject_unmatched_bins(overrides, &projects)?;
62 Self::reject_duplicate_names(&projects)?;
63 Ok(projects)
64 }
65
66 fn selected_packages(&self, overrides: &Overrides) -> Result<Vec<&Package>, ProjectError> {
68 let mut packages = if !overrides.packages.is_empty() {
69 let mut picked = Vec::new();
72 for name in &overrides.packages {
73 picked.push(self.package_named(name)?);
74 }
75 picked
76 } else {
77 let base = if overrides.workspace {
78 self.metadata.workspace_packages()
79 } else {
80 self.default_packages()
81 };
82 base.into_iter().filter(|p| Self::publishable(p)).collect()
83 };
84
85 if !overrides.exclude.is_empty() {
86 packages.retain(|package| {
87 !overrides
88 .exclude
89 .iter()
90 .any(|name| name == package.name.as_str())
91 });
92 }
93 Ok(packages)
94 }
95
96 fn default_packages(&self) -> Vec<&Package> {
99 if self.metadata.workspace_default_members.is_available() {
100 self.metadata.workspace_default_packages()
101 } else {
102 self.metadata.workspace_packages()
103 }
104 }
105
106 fn package_named(&self, name: &str) -> Result<&Package, ProjectError> {
107 self.metadata
108 .workspace_packages()
109 .into_iter()
110 .find(|package| package.name.as_str() == name)
111 .ok_or_else(|| ProjectError::PackageNotFound {
112 name: name.to_owned(),
113 })
114 }
115
116 fn publishable(package: &Package) -> bool {
118 !package
119 .publish
120 .as_ref()
121 .is_some_and(|registries| registries.is_empty())
122 }
123
124 fn selected_bins<'a>(package: &'a Package, overrides: &Overrides) -> Vec<&'a str> {
126 Self::bin_names(package)
127 .filter(|name| {
128 overrides.bins.is_empty() || overrides.bins.iter().any(|bin| bin == name)
129 })
130 .collect()
131 }
132
133 fn workspace_config(&self) -> Result<Config, ProjectError> {
134 match self.metadata.workspace_metadata.get(METADATA_KEY) {
135 Some(value) => Ok(Config::from_metadata(value)?),
136 None => Ok(Config::default()),
137 }
138 }
139
140 fn package_config(package: &Package) -> Result<Config, ProjectError> {
141 match package.metadata.get(METADATA_KEY) {
142 Some(value) => Ok(Config::from_metadata(value)?),
143 None => Ok(Config::default()),
144 }
145 }
146
147 fn reject_unmatched_bins(
149 &self,
150 overrides: &Overrides,
151 projects: &[Project],
152 ) -> Result<(), ProjectError> {
153 for wanted in &overrides.bins {
154 if !projects.iter().any(|project| &project.bin == wanted) {
155 return Err(if overrides.packages.is_empty() {
156 ProjectError::BinNotInWorkspace {
157 bin: wanted.clone(),
158 }
159 } else {
160 ProjectError::UnknownBin {
161 package: overrides.packages.join(", "),
162 bin: wanted.clone(),
163 }
164 });
165 }
166 }
167 Ok(())
168 }
169
170 fn reject_duplicate_names(projects: &[Project]) -> Result<(), ProjectError> {
174 let mut by_name: BTreeMap<&str, &str> = BTreeMap::new();
175 for project in projects {
176 let package = project.package.as_deref().unwrap_or_default();
177 if let Some(other) = by_name.insert(&project.identity.name, package) {
178 return Err(ProjectError::AmbiguousBin {
179 bin: project.identity.name.clone(),
180 packages: vec![other.to_owned(), package.to_owned()],
181 });
182 }
183 }
184 Ok(())
185 }
186
187 fn bin_names(package: &Package) -> impl Iterator<Item = &str> {
188 package
189 .targets
190 .iter()
191 .filter(|target| target.is_bin())
192 .map(|target| target.name.as_str())
193 }
194}