tsconfig_includes/
exact.rs

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
104/// Invoke the TypeScript compiler with the [listFilesOnly] flag to enumerate
105/// the files included in the compilation process.
106fn 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            // Drop the empty newline at the end of stdout
139            .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
256/// Enumerate source code files used by the TypeScript compiler during
257/// compilation. The return value is a list of alphabetically-sorted relative
258/// paths from the monorepo root, grouped by scoped package name.
259///
260/// - `monorepo_root` may be an absolute path
261/// - `tsconfig_files` should be relative paths from the monorepo root
262pub 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    // As relative path from monorepo root
278    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                // Enumerate internal dependencies (exclusive)
296                package_manifest
297                    .transitive_internal_dependency_package_names_exclusive(
298                        &package_manifests_by_package_name,
299                    )
300                    // Make this list inclusive of the target package
301                    .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        // REFACTOR: avoid intermediate allocations
321        .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                // This relies on the assumption that tsconfig.json is always the name of the tsconfig file
336                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}