1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Display,
4 iter,
5 path::{Path, PathBuf},
6};
7
8use globwalk::{FileType, GlobWalkerBuilder};
9use log::{debug, trace};
10use rayon::prelude::*;
11use serde::Deserialize;
12use typescript_tools::{configuration_file::ConfigurationFile, monorepo_manifest};
13
14use crate::{
15 io::read_json_from_file,
16 path::{self, *},
17 typescript_package::{
18 FromTypescriptConfigFileError, PackageInMonorepoRootError, PackageManifest,
19 PackageManifestFile, TypescriptConfigFile, TypescriptPackage,
20 },
21};
22
23#[derive(Debug, Default, Deserialize)]
24#[serde(rename_all = "camelCase")]
25struct CompilerOptions {
26 #[serde(default)]
27 allow_js: bool,
28 #[serde(default)]
29 resolve_json_module: bool,
30}
31
32#[derive(Debug, Deserialize)]
33#[serde(rename_all = "camelCase")]
34struct TypescriptConfig {
35 #[serde(default)]
36 compiler_options: CompilerOptions,
37 include: Vec<String>,
39}
40
41impl TypescriptConfig {
42 fn whitelisted_file_extensions(&self) -> HashSet<String> {
50 let mut whitelist: Vec<String> = vec![
51 String::from(".ts"),
52 String::from(".tsx"),
53 String::from(".d.ts"),
54 ];
55 if self.compiler_options.allow_js {
56 whitelist.append(&mut vec![String::from(".js"), String::from(".jsx")]);
57 }
58
59 let mut glob_extensions: Vec<String> = self
61 .include
62 .iter()
63 .filter(|pattern| is_glob(pattern))
64 .filter_map(|glob| glob_file_extension(glob))
65 .collect();
66
67 whitelist.append(&mut glob_extensions);
69 whitelist
70 .into_iter()
71 .filter(|extension| {
72 if !extension.ends_with(".json") {
73 return true;
74 }
75 self.compiler_options.resolve_json_module
78 })
79 .collect()
80 }
81}
82
83#[derive(Debug)]
84#[non_exhaustive]
85pub struct BuildWalkerError {
86 kind: BuildWalkerErrorKind,
87}
88
89impl Display for BuildWalkerError {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match &self.kind {
92 BuildWalkerErrorKind::PackageInMonorepoRoot(path) => {
93 write!(f, "unexpected package in monorepo root: {:?}", path)
94 }
95 BuildWalkerErrorKind::IO(_) => write!(f, "unable to estimate tsconfig includes"),
96 }
97 }
98}
99
100impl std::error::Error for BuildWalkerError {
101 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102 match &self.kind {
103 BuildWalkerErrorKind::IO(err) => Some(err),
104 BuildWalkerErrorKind::PackageInMonorepoRoot(_) => None,
105 }
106 }
107}
108
109#[derive(Debug)]
110pub enum BuildWalkerErrorKind {
111 #[non_exhaustive]
112 IO(crate::io::FromFileError),
113 #[non_exhaustive]
114 PackageInMonorepoRoot(PathBuf),
115}
116
117impl From<crate::io::FromFileError> for BuildWalkerErrorKind {
118 fn from(err: crate::io::FromFileError) -> Self {
119 Self::IO(err)
120 }
121}
122
123#[derive(Debug)]
124#[non_exhaustive]
125pub struct WalkError {
126 kind: WalkErrorKind,
127}
128
129impl Display for WalkError {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match &self.kind {
132 WalkErrorKind::WalkError(_) => write!(f, "unable to walk directory tree"),
133 WalkErrorKind::Path(_) => write!(f, "unable to strip path prefix"),
136 }
137 }
138}
139
140impl std::error::Error for WalkError {
141 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
142 match &self.kind {
143 WalkErrorKind::Path(err) => Some(err),
144 WalkErrorKind::WalkError(err) => Some(err),
145 }
146 }
147}
148
149impl From<globwalk::WalkError> for WalkError {
150 fn from(err: globwalk::WalkError) -> Self {
151 Self {
152 kind: WalkErrorKind::WalkError(err),
153 }
154 }
155}
156
157#[derive(Debug)]
158pub enum WalkErrorKind {
159 #[non_exhaustive]
160 Path(path::StripPrefixError),
161 #[non_exhaustive]
162 WalkError(globwalk::WalkError),
163}
164
165fn tsconfig_includes_estimate<'a, 'b>(
168 monorepo_root: &'a Path,
169 tsconfig_file: &'b TypescriptConfigFile,
170) -> Result<impl Iterator<Item = Result<PathBuf, WalkError>>, BuildWalkerError> {
171 let monorepo_root = monorepo_root.to_owned();
172 let package_directory = tsconfig_file
173 .package_directory(&monorepo_root)
174 .map_err(|kind| BuildWalkerError {
175 kind: BuildWalkerErrorKind::PackageInMonorepoRoot(kind.0),
176 })?;
177 let tsconfig: TypescriptConfig =
178 read_json_from_file(monorepo_root.join(tsconfig_file.as_path())).map_err(|err| {
179 BuildWalkerError {
180 kind: BuildWalkerErrorKind::IO(err),
181 }
182 })?;
183
184 let whitelisted_file_extensions = tsconfig.whitelisted_file_extensions();
185
186 let is_whitelisted_file_extension = move |path: &Path| -> bool {
187 whitelisted_file_extensions.iter().any(|extension| {
190 path.to_str()
191 .expect("Path should contain only valid UTF-8")
192 .ends_with(extension)
193 })
194 };
195
196 let monorepo_root_two = monorepo_root.clone();
197 let included_files = GlobWalkerBuilder::from_patterns(package_directory, &tsconfig.include)
198 .file_type(FileType::FILE)
199 .min_depth(0)
200 .build()
201 .expect("should be able to create glob walker")
202 .filter(move |maybe_dir_entry| match maybe_dir_entry {
203 Ok(dir_entry) => {
204 is_monorepo_file(&monorepo_root_two, dir_entry.path())
205 && is_whitelisted_file_extension(dir_entry.path())
206 }
207 Err(_) => true,
208 })
209 .map(move |maybe_dir_entry| -> Result<PathBuf, WalkError> {
210 let dir_entry = maybe_dir_entry?;
211 let path = dir_entry
212 .path()
213 .strip_prefix(&monorepo_root)
214 .map(ToOwned::to_owned)
215 .expect(&format!(
216 "Should be able to strip monorepo-root prefix from path in monorepo: {:?}",
217 dir_entry.path()
218 ));
219 Ok(path)
220 });
221
222 Ok(included_files)
223}
224
225#[derive(Debug)]
226#[non_exhaustive]
227pub struct Error {
228 kind: ErrorKind,
229}
230
231impl Display for Error {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match &self.kind {
234 ErrorKind::PackageInMonorepoRoot(path) => {
235 write!(f, "unexpected package in monorepo root: {:?}", path)
236 }
237 _ => write!(f, "unable to estimate tsconfig includes"),
238 }
239 }
240}
241
242impl std::error::Error for Error {
243 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
244 match &self.kind {
245 ErrorKind::MonorepoManifest(err) => Some(err),
246 ErrorKind::EnumeratePackageManifestsError(err) => Some(err),
247 ErrorKind::PackageInMonorepoRoot(_) => None,
248 ErrorKind::FromFile(err) => Some(err),
249 ErrorKind::BuildWalker(err) => Some(err),
250 ErrorKind::Walk(err) => Some(err),
251 }
252 }
253}
254
255impl From<ErrorKind> for Error {
256 fn from(kind: ErrorKind) -> Self {
257 Self { kind }
258 }
259}
260
261impl From<typescript_tools::io::FromFileError> for Error {
262 fn from(err: typescript_tools::io::FromFileError) -> Self {
263 Self {
264 kind: ErrorKind::MonorepoManifest(err),
265 }
266 }
267}
268
269impl From<typescript_tools::monorepo_manifest::EnumeratePackageManifestsError> for Error {
270 fn from(err: typescript_tools::monorepo_manifest::EnumeratePackageManifestsError) -> Self {
271 Self {
272 kind: ErrorKind::EnumeratePackageManifestsError(err),
273 }
274 }
275}
276
277impl From<crate::io::FromFileError> for Error {
278 fn from(err: crate::io::FromFileError) -> Self {
279 Self {
280 kind: ErrorKind::FromFile(err),
281 }
282 }
283}
284
285impl From<BuildWalkerError> for Error {
286 fn from(err: BuildWalkerError) -> Self {
287 match err.kind {
288 BuildWalkerErrorKind::PackageInMonorepoRoot(path) => Self {
290 kind: ErrorKind::PackageInMonorepoRoot(path),
291 },
292 _ => Self {
293 kind: ErrorKind::BuildWalker(err),
294 },
295 }
296 }
297}
298
299impl From<WalkError> for Error {
300 fn from(err: WalkError) -> Self {
301 Self {
302 kind: ErrorKind::Walk(err),
303 }
304 }
305}
306
307impl From<FromTypescriptConfigFileError> for Error {
308 fn from(err: FromTypescriptConfigFileError) -> Self {
309 let kind = match err {
310 FromTypescriptConfigFileError::PackageInMonorepoRoot(path) => {
311 ErrorKind::PackageInMonorepoRoot(path)
312 }
313 FromTypescriptConfigFileError::FromFile(err) => ErrorKind::FromFile(err),
314 };
315 Self { kind }
316 }
317}
318
319impl From<PackageInMonorepoRootError> for Error {
320 fn from(err: PackageInMonorepoRootError) -> Self {
321 Self {
322 kind: ErrorKind::PackageInMonorepoRoot(err.0),
323 }
324 }
325}
326
327#[derive(Debug)]
328pub enum ErrorKind {
329 #[non_exhaustive]
330 MonorepoManifest(typescript_tools::io::FromFileError),
331 #[non_exhaustive]
332 EnumeratePackageManifestsError(
333 typescript_tools::monorepo_manifest::EnumeratePackageManifestsError,
334 ),
335 #[non_exhaustive]
336 PackageInMonorepoRoot(PathBuf),
337 #[non_exhaustive]
338 FromFile(crate::io::FromFileError),
339 #[non_exhaustive]
340 BuildWalker(BuildWalkerError),
341 #[non_exhaustive]
342 Walk(WalkError),
343}
344
345pub fn tsconfig_includes_by_package_name<P, T>(
352 monorepo_root: P,
353 tsconfig_files: T,
354) -> Result<HashMap<String, Vec<PathBuf>>, Error>
355where
356 P: AsRef<Path> + Sync,
357 T: IntoIterator,
358 T::Item: AsRef<Path>,
359{
360 let lerna_manifest =
362 monorepo_manifest::MonorepoManifest::from_directory(monorepo_root.as_ref())?;
363 let package_manifests_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
364 trace!("{:?}", lerna_manifest);
365
366 let transitive_internal_dependency_tsconfigs_inclusive_to_enumerate: HashSet<
368 TypescriptPackage,
369 > = tsconfig_files
370 .into_iter()
371 .map(|tsconfig_file| -> Result<Vec<TypescriptPackage>, Error> {
372 let tsconfig_file: TypescriptConfigFile =
373 monorepo_root.as_ref().join(tsconfig_file.as_ref()).into();
374 let package_manifest: PackageManifest = (&tsconfig_file).try_into()?;
375 let package_manifest = package_manifests_by_package_name
376 .get(&package_manifest.name)
377 .expect(&format!(
378 "tsconfig {:?} should belong to a package in the lerna monorepo",
379 tsconfig_file
380 ));
381
382 let transitive_internal_dependencies_inclusive = {
385 package_manifest
387 .transitive_internal_dependency_package_names_exclusive(
388 &package_manifests_by_package_name,
389 )
390 .chain(iter::once(package_manifest))
392 };
393
394 Ok(transitive_internal_dependencies_inclusive
395 .map(
396 |package_manifest| -> Result<_, PackageInMonorepoRootError> {
397 let package_manifest_file =
398 PackageManifestFile::from(package_manifest.path());
399 let tsconfig_file: TypescriptConfigFile =
400 package_manifest_file.try_into()?;
401 let typescript_package = TypescriptPackage {
402 scoped_package_name: package_manifest.contents.name.clone(),
403 tsconfig_file,
404 };
405 Ok(typescript_package)
406 },
407 )
408 .collect::<Result<_, _>>()?)
409 })
410 .collect::<Result<Vec<_>, _>>()?
412 .into_iter()
413 .flatten()
414 .collect();
415
416 debug!(
417 "transitive_internal_dependency_tsconfigs_inclusive_to_enumerate: {:?}",
418 transitive_internal_dependency_tsconfigs_inclusive_to_enumerate
419 );
420
421 let included_files: HashMap<String, Vec<PathBuf>> =
422 transitive_internal_dependency_tsconfigs_inclusive_to_enumerate
423 .into_par_iter()
424 .map(|typescript_package| -> Result<(_, _), Error> {
425 let tsconfig_file = &typescript_package.tsconfig_file;
427 let mut included_files: Vec<_> =
428 tsconfig_includes_estimate(monorepo_root.as_ref(), tsconfig_file)?
429 .collect::<Result<_, _>>()?;
430 included_files.sort_unstable();
431 Ok((typescript_package.scoped_package_name, included_files))
432 })
433 .collect::<Result<HashMap<_, _>, _>>()?;
434
435 debug!("tsconfig_includes: {:?}", included_files);
436 Ok(included_files)
437}