1use std::borrow::Borrow;
2use std::collections::HashMap;
3use std::ffi::OsString;
4use std::fmt::Display;
5use std::path::{Path, PathBuf};
6
7use pathdiff::diff_paths;
8
9use crate::configuration_file::{ConfigurationFile, WriteError};
10use crate::io::FromFileError;
11use crate::monorepo_manifest::{EnumeratePackageManifestsError, MonorepoManifest};
12use crate::out_of_date_project_references::{
13 AllOutOfDateTypescriptConfig, OutOfDatePackageProjectReferences,
14 OutOfDateParentProjectReferences, OutOfDateTypescriptConfig,
15};
16use crate::package_manifest::PackageManifest;
17use crate::types::{Directory, PackageName};
18use crate::typescript_config::{
19 TypescriptConfig, TypescriptParentProjectReference, TypescriptProjectReference,
20};
21
22#[derive(Debug)]
23#[non_exhaustive]
24pub struct LinkError {
25 pub kind: LinkErrorKind,
26}
27
28impl Display for LinkError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "error linking TypeScript project references")
31 }
32}
33
34impl std::error::Error for LinkError {
35 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36 match &self.kind {
37 LinkErrorKind::EnumeratePackageManifests(err) => Some(err),
38 LinkErrorKind::FromFile(err) => Some(err),
39 LinkErrorKind::Write(err) => Some(err),
40 LinkErrorKind::InvalidUtf8(err) => Some(err),
41 }
42 }
43}
44
45impl From<EnumeratePackageManifestsError> for LinkError {
46 fn from(err: EnumeratePackageManifestsError) -> Self {
47 Self {
48 kind: LinkErrorKind::EnumeratePackageManifests(err),
49 }
50 }
51}
52
53impl From<FromFileError> for LinkError {
54 fn from(err: FromFileError) -> Self {
55 Self {
56 kind: LinkErrorKind::FromFile(err),
57 }
58 }
59}
60
61impl From<WriteError> for LinkError {
62 fn from(err: WriteError) -> Self {
63 Self {
64 kind: LinkErrorKind::Write(err),
65 }
66 }
67}
68
69impl From<InvalidUtf8Error> for LinkError {
70 fn from(err: InvalidUtf8Error) -> Self {
71 Self {
72 kind: LinkErrorKind::InvalidUtf8(err),
73 }
74 }
75}
76
77#[derive(Debug)]
78pub enum LinkErrorKind {
79 #[non_exhaustive]
80 EnumeratePackageManifests(EnumeratePackageManifestsError),
81 #[non_exhaustive]
82 FromFile(FromFileError),
83 #[non_exhaustive]
84 InvalidUtf8(InvalidUtf8Error),
85 #[non_exhaustive]
86 Write(WriteError),
87}
88
89#[derive(Debug)]
90pub struct InvalidUtf8Error(OsString);
91
92impl Display for InvalidUtf8Error {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "path cannot be expressed as UTF-8: {:?}", self.0)
95 }
96}
97
98impl std::error::Error for InvalidUtf8Error {
99 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
100 None
101 }
102}
103
104fn key_children_by_parent<M>(
105 mut accumulator: HashMap<Directory, Vec<String>>,
106 package_manifest: M,
107) -> Result<HashMap<Directory, Vec<String>>, InvalidUtf8Error>
108where
109 M: Borrow<PackageManifest>,
110{
111 let mut path_so_far = PathBuf::new();
112 for component in package_manifest.borrow().directory().iter() {
113 let children = accumulator
114 .entry(Directory::unchecked_from_path(path_so_far.clone()))
115 .or_default();
116
117 let new_child = component
118 .to_str()
119 .map(ToOwned::to_owned)
120 .ok_or_else(|| InvalidUtf8Error(component.to_owned()))?;
121 if !children.contains(&new_child) {
123 children.push(new_child);
124 }
125
126 path_so_far.push(component);
127 }
128 Ok(accumulator)
129}
130
131fn create_project_references(mut children: Vec<String>) -> Vec<TypescriptProjectReference> {
132 children.sort_unstable();
135 children
136 .into_iter()
137 .map(|path| TypescriptProjectReference { path })
138 .collect()
139}
140
141fn link_children_packages(
144 root: &Directory,
145 package_manifests_by_package_name: &HashMap<PackageName, PackageManifest>,
146) -> Result<(), LinkError> {
147 out_of_date_parent_project_references(root, package_manifests_by_package_name)?.try_for_each(
148 |maybe_parent_project_references| -> Result<(), LinkError> {
149 let OutOfDateParentProjectReferences {
150 mut tsconfig,
151 desired_references,
152 } = maybe_parent_project_references?;
153 tsconfig.contents.references = desired_references;
154 Ok(TypescriptParentProjectReference::write(root, tsconfig)?)
155 },
156 )
157}
158
159fn link_package_dependencies(
160 root: &Directory,
161 package_manifests_by_package_name: &HashMap<PackageName, PackageManifest>,
162) -> Result<(), LinkError> {
163 out_of_date_package_project_references(root, package_manifests_by_package_name)?
164 .map(
165 |maybe_package_project_references| -> Result<Option<_>, FromFileError> {
166 let OutOfDatePackageProjectReferences {
167 mut tsconfig,
168 desired_references,
169 } = maybe_package_project_references?;
170 let current_project_references = &tsconfig
172 .contents
173 .get("references")
174 .map(|value| {
175 serde_json::from_value::<Vec<TypescriptProjectReference>>(value.clone())
176 .expect("value starting as JSON should be deserializable")
177 })
178 .unwrap_or_default();
179
180 let needs_update = !current_project_references.eq(&desired_references);
181 if !needs_update {
182 return Ok(None);
183 }
184
185 tsconfig.contents.insert(
187 String::from("references"),
188 serde_json::to_value(desired_references).expect(
189 "should be able to express desired TypeScript project references as JSON",
190 ),
191 );
192
193 Ok(Some(tsconfig))
194 },
195 )
196 .filter_map(Result::transpose)
197 .map(|maybe_tsconfig| -> Result<(), LinkError> {
198 let tsconfig = maybe_tsconfig?;
199 Ok(TypescriptConfig::write(root, tsconfig)?)
200 })
201 .collect::<Result<Vec<_>, _>>()?;
202 Ok(())
203}
204
205pub fn modify<P>(root: P) -> Result<(), LinkError>
206where
207 P: AsRef<Path>,
208{
209 fn inner(root: &Path) -> Result<(), LinkError> {
210 let lerna_manifest = MonorepoManifest::from_directory(root)?;
211 let package_manifests_by_package_name =
212 lerna_manifest.package_manifests_by_package_name()?;
213 link_children_packages(&lerna_manifest.root, &package_manifests_by_package_name)?;
214 link_package_dependencies(&lerna_manifest.root, &package_manifests_by_package_name)?;
215 Ok(())
217 }
218 inner(root.as_ref())
219}
220
221#[derive(Debug)]
222#[non_exhaustive]
223pub struct LinkLintError {
224 pub kind: LinkLintErrorKind,
225}
226
227impl Display for LinkLintError {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 match &self.kind {
230 LinkLintErrorKind::ProjectReferencesOutOfDate(out_of_date_references) => {
231 writeln!(f, "TypeScript project references are not up-to-date")?;
232 writeln!(f, "{}", out_of_date_references)
233 }
234 _ => write!(f, "error linking TypeScript project references"),
235 }
236 }
237}
238
239impl std::error::Error for LinkLintError {
240 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
241 match &self.kind {
242 LinkLintErrorKind::EnumeratePackageManifests(err) => Some(err),
243 LinkLintErrorKind::FromFile(err) => Some(err),
244 LinkLintErrorKind::ProjectReferencesOutOfDate(_) => None,
245 LinkLintErrorKind::InvalidUtf8(err) => Some(err),
246 }
247 }
248}
249
250impl From<EnumeratePackageManifestsError> for LinkLintError {
251 fn from(err: EnumeratePackageManifestsError) -> Self {
252 Self {
253 kind: LinkLintErrorKind::EnumeratePackageManifests(err),
254 }
255 }
256}
257
258impl From<FromFileError> for LinkLintError {
259 fn from(err: FromFileError) -> Self {
260 Self {
261 kind: LinkLintErrorKind::FromFile(err),
262 }
263 }
264}
265
266impl From<InvalidUtf8Error> for LinkLintError {
267 fn from(err: InvalidUtf8Error) -> Self {
268 Self {
269 kind: LinkLintErrorKind::InvalidUtf8(err),
270 }
271 }
272}
273
274impl From<AllOutOfDateTypescriptConfig> for LinkLintError {
275 fn from(err: AllOutOfDateTypescriptConfig) -> Self {
276 Self {
277 kind: LinkLintErrorKind::ProjectReferencesOutOfDate(err),
278 }
279 }
280}
281
282#[derive(Debug)]
283pub enum LinkLintErrorKind {
284 #[non_exhaustive]
285 EnumeratePackageManifests(EnumeratePackageManifestsError),
286 #[non_exhaustive]
287 FromFile(FromFileError),
288 #[non_exhaustive]
289 InvalidUtf8(InvalidUtf8Error),
290 #[non_exhaustive]
292 ProjectReferencesOutOfDate(AllOutOfDateTypescriptConfig),
293}
294
295fn out_of_date_parent_project_references<'a>(
296 root: &'a Directory,
297 package_manifests_by_package_name: &'a HashMap<PackageName, PackageManifest>,
298) -> Result<
299 impl Iterator<Item = Result<OutOfDateParentProjectReferences, FromFileError>> + 'a,
300 InvalidUtf8Error,
301> {
302 let iter = package_manifests_by_package_name
303 .values()
304 .try_fold(HashMap::default(), key_children_by_parent)?
305 .into_iter()
306 .map(move |(directory, children)| {
307 let desired_references = create_project_references(children);
308 let tsconfig = TypescriptParentProjectReference::from_directory(&root, directory)?;
309 let current_project_references = &tsconfig.contents.references;
310 let needs_update = !current_project_references.eq(&desired_references);
311 Ok(match needs_update {
312 true => Some(OutOfDateParentProjectReferences {
313 tsconfig,
314 desired_references,
315 }),
316 false => None,
317 })
318 })
319 .filter_map(Result::transpose);
320 Ok(iter)
321}
322
323fn out_of_date_package_project_references<'a>(
324 root: &'a Directory,
325 package_manifests_by_package_name: &'a HashMap<PackageName, PackageManifest>,
326) -> Result<
327 impl Iterator<Item = Result<OutOfDatePackageProjectReferences, FromFileError>> + 'a,
328 InvalidUtf8Error,
329> {
330 let iter = package_manifests_by_package_name
331 .values()
332 .map(move |package_manifest| {
333 let package_directory = package_manifest.directory();
334 let tsconfig = TypescriptConfig::from_directory(&root, package_directory.to_owned())?;
335 let internal_dependencies =
336 package_manifest.internal_dependencies_iter(&package_manifests_by_package_name);
337
338 let desired_references: Vec<TypescriptProjectReference> = {
339 let mut typescript_project_references: Vec<String> = internal_dependencies
340 .into_iter()
341 .map(|dependency| {
342 diff_paths(dependency.directory(), package_manifest.directory())
343 .expect(
344 "Unable to calculate a relative path to dependency from package",
345 )
346 .to_str()
347 .expect("Path not valid UTF-8 encoded")
348 .to_string()
349 })
350 .collect::<Vec<_>>();
351 typescript_project_references.sort_unstable();
353
354 typescript_project_references
355 .into_iter()
356 .map(|path| TypescriptProjectReference { path })
357 .collect()
358 };
359
360 let current_project_references = &tsconfig
362 .contents
363 .get("references")
364 .map(|value| {
365 serde_json::from_value::<Vec<TypescriptProjectReference>>(value.clone())
366 .expect("value starting as JSON should be serializable")
367 })
368 .unwrap_or_default();
369
370 let needs_update = !current_project_references.eq(&desired_references);
371 Ok(match needs_update {
372 true => Some(OutOfDatePackageProjectReferences {
373 tsconfig,
374 desired_references,
375 }),
376 false => None,
377 })
378 })
379 .filter_map(Result::transpose);
380
381 Ok(iter)
382}
383
384pub fn lint<P>(root: P) -> Result<(), LinkLintError>
385where
386 P: AsRef<Path>,
387{
388 fn inner(root: &Path) -> Result<(), LinkLintError> {
389 let monorepo_manifest = MonorepoManifest::from_directory(root)?;
390 let package_manifests_by_package_name =
391 monorepo_manifest.package_manifests_by_package_name()?;
392
393 let is_children_link_success = out_of_date_parent_project_references(
394 &monorepo_manifest.root,
395 &package_manifests_by_package_name,
396 )?
397 .map(
398 |result| -> Result<OutOfDateTypescriptConfig, FromFileError> { result.map(Into::into) },
399 );
400
401 let is_dependencies_link_success = out_of_date_package_project_references(
402 &monorepo_manifest.root,
403 &package_manifests_by_package_name,
404 )?
405 .map(
406 |result| -> Result<OutOfDateTypescriptConfig, FromFileError> { result.map(Into::into) },
407 );
408
409 let lint_issues: AllOutOfDateTypescriptConfig = is_children_link_success
410 .chain(is_dependencies_link_success)
411 .collect::<Result<_, _>>()?;
412
413 match lint_issues.is_empty() {
414 true => Ok(()),
415 false => Err(lint_issues)?,
416 }
417 }
418 inner(root.as_ref())
419}