1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Display,
4 iter,
5 path::{Path, PathBuf},
6 process::Command,
7 string,
8};
9
10use log::{debug, trace};
11use rayon::prelude::*;
12use typescript_tools::{configuration_file::ConfigurationFile, monorepo_manifest};
13
14use crate::{
15 path::{
16 self, is_child_of_node_modules, is_monorepo_file,
17 remove_relative_path_prefix_from_absolute_path,
18 },
19 typescript_package::{
20 FromTypescriptConfigFileError, PackageInMonorepoRootError, PackageManifest,
21 PackageManifestFile, TypescriptConfigFile, TypescriptPackage,
22 },
23};
24
25#[derive(Debug)]
26#[non_exhaustive]
27pub struct EnumerateError {
28 kind: EnumerateErrorKind,
29}
30
31impl Display for EnumerateError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match &self.kind {
34 EnumerateErrorKind::Command(_) => write!(f, "unable to spawn child process"),
35 EnumerateErrorKind::TypescriptCompiler { command, error } => {
36 writeln!(
37 f,
38 "tsc exited with non-zero status code for command {:?}:",
39 command
40 )?;
41 write!(f, "{:?}", error)
42 }
43 EnumerateErrorKind::InvalidUtf8(_) => {
44 write!(f, "command output included invalid UTF-8")
45 }
46 EnumerateErrorKind::StripPrefix(_) => write!(f, "unable to manipulate file path"),
47 EnumerateErrorKind::PackageInMonorepoRoot(path) => {
48 write!(f, "unexpected package in monorepo root: {:?}", path)
49 }
50 EnumerateErrorKind::Canonicalize { path, inner: _ } => {
51 write!(f, "unable to canonicalize path {:?}", path)
52 }
53 }
54 }
55}
56
57impl std::error::Error for EnumerateError {
58 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59 match &self.kind {
60 EnumerateErrorKind::Command(err) => Some(err),
61 EnumerateErrorKind::TypescriptCompiler {
62 command: _,
63 error: _,
64 } => None,
65 EnumerateErrorKind::InvalidUtf8(err) => Some(err),
66 EnumerateErrorKind::StripPrefix(err) => Some(err),
67 EnumerateErrorKind::PackageInMonorepoRoot(_) => None,
68 EnumerateErrorKind::Canonicalize { path: _, inner } => Some(inner),
69 }
70 }
71}
72
73#[derive(Debug)]
74pub enum EnumerateErrorKind {
75 #[non_exhaustive]
76 Command(std::io::Error),
77 #[non_exhaustive]
78 TypescriptCompiler { command: String, error: Vec<u8> },
79 #[non_exhaustive]
80 InvalidUtf8(string::FromUtf8Error),
81 #[non_exhaustive]
82 StripPrefix(path::StripPrefixError),
83 #[non_exhaustive]
84 PackageInMonorepoRoot(PathBuf),
85 #[non_exhaustive]
86 Canonicalize {
87 path: PathBuf,
88 inner: std::io::Error,
89 },
90}
91
92impl From<string::FromUtf8Error> for EnumerateErrorKind {
93 fn from(err: string::FromUtf8Error) -> Self {
94 Self::InvalidUtf8(err)
95 }
96}
97
98impl From<path::StripPrefixError> for EnumerateErrorKind {
99 fn from(err: path::StripPrefixError) -> Self {
100 Self::StripPrefix(err)
101 }
102}
103
104fn tsconfig_includes_exact(
107 monorepo_root: &Path,
108 tsconfig: &TypescriptConfigFile,
109) -> Result<Vec<PathBuf>, EnumerateError> {
110 (|| {
111 let monorepo_root = std::fs::canonicalize(monorepo_root).map_err(|inner| {
112 EnumerateErrorKind::Canonicalize {
113 path: monorepo_root.to_path_buf(),
114 inner,
115 }
116 })?;
117
118 let child = Command::new("tsc")
119 .arg("--listFilesOnly")
120 .arg("--project")
121 .arg(
122 tsconfig
123 .package_directory(&monorepo_root)
124 .map_err(|err| EnumerateErrorKind::PackageInMonorepoRoot(err.0))?,
125 )
126 .output()
127 .map_err(EnumerateErrorKind::Command)?;
128 if child.status.code() != Some(0) {
129 return Err(EnumerateErrorKind::TypescriptCompiler {
130 command: format!("tsc --listFilesOnly --project {:?}", tsconfig),
131 error: child.stderr,
132 });
133 }
134 let stdout = String::from_utf8(child.stdout)?;
135
136 let included_files: Vec<PathBuf> = stdout
137 .lines()
138 .filter(|s| !s.is_empty())
140 .map(PathBuf::from)
141 .filter(|path| is_monorepo_file(&monorepo_root, path))
142 .filter(|path| !is_child_of_node_modules(path))
143 .map(|source_file| {
144 remove_relative_path_prefix_from_absolute_path(&monorepo_root, &source_file)
145 })
146 .collect::<Result<_, _>>()?;
147
148 Ok(included_files)
149 })()
150 .map_err(|kind| EnumerateError { kind })
151}
152
153#[derive(Debug)]
154#[non_exhaustive]
155pub struct Error {
156 kind: ErrorKind,
157}
158
159impl Display for Error {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 match &self.kind {
162 ErrorKind::PackageInMonorepoRoot(path) => {
163 write!(f, "unexpected package in monorepo root: {:?}", path)
164 }
165 _ => write!(f, "unable to enumerate exact tsconfig includes"),
166 }
167 }
168}
169
170impl std::error::Error for Error {
171 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172 match &self.kind {
173 ErrorKind::MonorepoManifest(err) => Some(err),
174 ErrorKind::EnumeratePackageManifestsError(err) => Some(err),
175 ErrorKind::PackageInMonorepoRoot(_) => None,
176 ErrorKind::FromFile(err) => Some(err),
177 ErrorKind::Enumerate(err) => Some(err),
178 }
179 }
180}
181
182impl From<ErrorKind> for Error {
183 fn from(kind: ErrorKind) -> Self {
184 Self { kind }
185 }
186}
187
188impl From<typescript_tools::io::FromFileError> for Error {
189 fn from(err: typescript_tools::io::FromFileError) -> Self {
190 Self {
191 kind: ErrorKind::MonorepoManifest(err),
192 }
193 }
194}
195
196impl From<typescript_tools::monorepo_manifest::EnumeratePackageManifestsError> for Error {
197 fn from(err: typescript_tools::monorepo_manifest::EnumeratePackageManifestsError) -> Self {
198 Self {
199 kind: ErrorKind::EnumeratePackageManifestsError(err),
200 }
201 }
202}
203
204impl From<crate::io::FromFileError> for Error {
205 fn from(err: crate::io::FromFileError) -> Self {
206 Self {
207 kind: ErrorKind::FromFile(err),
208 }
209 }
210}
211
212impl From<EnumerateError> for Error {
213 fn from(err: EnumerateError) -> Self {
214 Self {
215 kind: ErrorKind::Enumerate(err),
216 }
217 }
218}
219
220impl From<FromTypescriptConfigFileError> for Error {
221 fn from(err: FromTypescriptConfigFileError) -> Self {
222 let kind = match err {
223 FromTypescriptConfigFileError::PackageInMonorepoRoot(path) => {
224 ErrorKind::PackageInMonorepoRoot(path)
225 }
226 FromTypescriptConfigFileError::FromFile(err) => ErrorKind::FromFile(err),
227 };
228 Self { kind }
229 }
230}
231
232impl From<PackageInMonorepoRootError> for Error {
233 fn from(err: PackageInMonorepoRootError) -> Self {
234 Self {
235 kind: ErrorKind::PackageInMonorepoRoot(err.0),
236 }
237 }
238}
239
240#[derive(Debug)]
241pub enum ErrorKind {
242 #[non_exhaustive]
243 MonorepoManifest(typescript_tools::io::FromFileError),
244 #[non_exhaustive]
245 EnumeratePackageManifestsError(
246 typescript_tools::monorepo_manifest::EnumeratePackageManifestsError,
247 ),
248 #[non_exhaustive]
249 PackageInMonorepoRoot(PathBuf),
250 #[non_exhaustive]
251 FromFile(crate::io::FromFileError),
252 #[non_exhaustive]
253 Enumerate(EnumerateError),
254}
255
256pub fn tsconfig_includes_by_package_name<P, Q>(
263 monorepo_root: P,
264 tsconfig_files: Q,
265) -> Result<HashMap<String, Vec<PathBuf>>, Error>
266where
267 P: AsRef<Path> + Sync,
268 Q: IntoIterator,
269 Q::Item: AsRef<Path>,
270{
271 let lerna_manifest =
272 monorepo_manifest::MonorepoManifest::from_directory(monorepo_root.as_ref())
273 .map_err(|thing| thing)?;
274 let package_manifests_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
275 trace!("{:?}", lerna_manifest);
276
277 let transitive_internal_dependency_tsconfigs_inclusive_to_enumerate: HashSet<
279 TypescriptPackage,
280 > = tsconfig_files
281 .into_iter()
282 .map(|tsconfig_file| -> Result<Vec<TypescriptPackage>, Error> {
283 let tsconfig_file: TypescriptConfigFile =
284 monorepo_root.as_ref().join(tsconfig_file.as_ref()).into();
285 let package_manifest: PackageManifest = (&tsconfig_file).try_into()?;
286
287 let package_manifest = package_manifests_by_package_name
288 .get(&package_manifest.name)
289 .expect(&format!(
290 "tsconfig {:?} should belong to a package in the lerna monorepo",
291 tsconfig_file
292 ));
293
294 let transitive_internal_dependencies_inclusive = {
295 package_manifest
297 .transitive_internal_dependency_package_names_exclusive(
298 &package_manifests_by_package_name,
299 )
300 .chain(iter::once(package_manifest))
302 };
303
304 Ok(transitive_internal_dependencies_inclusive
305 .map(
306 |package_manifest| -> Result<_, PackageInMonorepoRootError> {
307 let package_manifest_file =
308 PackageManifestFile::from(package_manifest.path());
309 let tsconfig_file: TypescriptConfigFile =
310 package_manifest_file.try_into()?;
311 let typescript_package = TypescriptPackage {
312 scoped_package_name: package_manifest.contents.name.clone(),
313 tsconfig_file,
314 };
315 Ok(typescript_package)
316 },
317 )
318 .collect::<Result<_, _>>()?)
319 })
320 .collect::<Result<Vec<_>, _>>()?
322 .into_iter()
323 .flatten()
324 .collect();
325
326 debug!(
327 "transitive_internal_dependency_tsconfigs_inclusive_to_enumerate: {:?}",
328 transitive_internal_dependency_tsconfigs_inclusive_to_enumerate
329 );
330
331 let included_files: HashMap<String, Vec<PathBuf>> =
332 transitive_internal_dependency_tsconfigs_inclusive_to_enumerate
333 .into_par_iter()
334 .map(|typescript_package| -> Result<(_, _), Error> {
335 let tsconfig = &typescript_package.tsconfig_file;
337 let mut included_files = tsconfig_includes_exact(monorepo_root.as_ref(), tsconfig)?;
338 included_files.sort_unstable();
339 Ok((typescript_package.scoped_package_name, included_files))
340 })
341 .collect::<Result<HashMap<_, _>, _>>()?;
342
343 debug!("tsconfig_includes: {:?}", included_files);
344 Ok(included_files)
345}