Skip to content
This repository was archived by the owner on Sep 17, 2023. It is now read-only.

Commit 0b759cc

Browse files
committed
fix: improve error messages presented to the user
This commit improves error messages presented to the user when encountering errors parsing JSON. This required a bit of refactoring, since we were performing too much work up-front, and swallowing the helpful errors with a fallback (that stops looking for a lerna manifest and starts looking for an npm 7 workspace) that happened too late. I took this opportunity to improve the representation of package.json manifests, which makes interaction with serde more ergonomic.
1 parent f2f6dc8 commit 0b759cc

File tree

11 files changed

+156
-123
lines changed

11 files changed

+156
-123
lines changed

Cargo.lock

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ anyhow = "1.0.57"
1212
askama = "0.11.1"
1313
clap = { version = "3.2.4", features = ["cargo", "derive"] }
1414
globwalk = "0.8.1"
15+
indoc = "1.0.7"
1516
pariter = "0.5.1"
1617
pathdiff = "0.2.1"
1718
serde = { version = "1.0.137", features = ["derive"] }

src/configuration_file.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ pub trait ConfigurationFile<T> {
99

1010
/// Create an instance of this configuration file by reading
1111
/// the specified file from this directory on disk.
12-
fn from_directory<P>(monorepo_root: P, directory: P) -> Result<T>
13-
where
14-
P: AsRef<Path>;
12+
fn from_directory(monorepo_root: &Path, directory: &Path) -> Result<T>;
1513

1614
/// Relative path to directory containing this configuration file,
1715
/// from monorepo root.

src/io.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
use anyhow::Context;
12
use std::fs::File;
2-
use std::io::{BufWriter, Write};
3+
use std::io::{BufWriter, Read, Write};
34
use std::path::Path;
45

56
use anyhow::Result;
67

78
use serde::{Deserialize, Serialize};
89

9-
#[derive(Serialize, Deserialize, Debug, PartialEq)]
10+
// REFACTOR: this belongs in a different file
11+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
1012
pub struct TypescriptProjectReference {
1113
pub path: String,
1214
}
1315

14-
#[derive(Serialize, PartialEq)]
16+
// REFACTOR: this belongs in a different file
17+
#[derive(Serialize, PartialEq, Eq)]
1518
pub struct TypescriptParentProjectReference {
1619
pub files: Vec<String>,
1720
pub references: Vec<TypescriptProjectReference>,
@@ -28,3 +31,16 @@ pub fn write_project_references<P: AsRef<Path>>(
2831
writer.flush()?;
2932
Ok(())
3033
}
34+
35+
pub(crate) fn read_json_from_file<T>(filename: &Path) -> Result<T>
36+
where
37+
for<'de> T: Deserialize<'de>,
38+
{
39+
// Reading a file into a string before invoking Serde is faster than
40+
// invoking Serde from a BufReader, see
41+
// https://github.com/serde-rs/json/issues/160
42+
let mut string = String::new();
43+
File::open(&filename)?.read_to_string(&mut string)?;
44+
serde_json::from_str(&string)
45+
.with_context(|| format!("Unable to parse JSON from file '{:?}'", filename))
46+
}

src/link.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fn link_children_packages(opts: &opts::Link, lerna_manifest: &MonorepoManifest)
6161
let mut is_exit_success = true;
6262

6363
lerna_manifest
64-
.internal_package_manifests
64+
.internal_package_manifests()?
6565
.iter()
6666
.fold(HashMap::new(), key_children_by_parent)
6767
.iter()
@@ -102,9 +102,7 @@ fn link_children_packages(opts: &opts::Link, lerna_manifest: &MonorepoManifest)
102102
}
103103

104104
fn link_package_dependencies(opts: &opts::Link, lerna_manifest: &MonorepoManifest) -> Result<bool> {
105-
let package_manifest_by_package_name = lerna_manifest
106-
.package_manifests_by_package_name()
107-
.expect("Unable to read all package manifests");
105+
let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
108106

109107
let tsconfig_diffs: Vec<Option<TypescriptConfig>> = package_manifest_by_package_name
110108
.iter()
@@ -118,6 +116,7 @@ fn link_package_dependencies(opts: &opts::Link, lerna_manifest: &MonorepoManifes
118116

119117
let desired_project_references: Vec<TypescriptProjectReference> = {
120118
let mut typescript_project_references: Vec<String> = internal_dependencies
119+
.into_iter()
121120
.map(|dependency| {
122121
diff_paths(dependency.directory(), package_manifest.directory())
123122
.expect(

src/lint.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ fn most_common_dependency_version(
3030
fn lint_dependency_version(opts: &opts::DependencyVersion) -> Result<()> {
3131
let opts::DependencyVersion { root, dependencies } = opts;
3232

33-
let lerna_manifest = MonorepoManifest::from_directory(&root)?;
33+
let lerna_manifest = MonorepoManifest::from_directory(root)?;
3434
let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
3535

3636
let mut is_exit_success = true;

src/monorepo_manifest.rs

Lines changed: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,48 @@
11
use std::collections::HashMap;
2-
use std::fs::File;
3-
use std::io::BufReader;
4-
use std::marker::Sync;
5-
use std::path::Path;
2+
use std::path::{Path, PathBuf};
63

7-
use anyhow::Result;
4+
use anyhow::{Context, Result};
85

96
use globwalk::{FileType, GlobWalkerBuilder};
107

8+
use indoc::formatdoc;
119
use serde::{Deserialize, Serialize};
1210

1311
use pariter::IteratorExt as _;
1412

1513
use crate::configuration_file::ConfigurationFile;
14+
use crate::io::read_json_from_file;
1615
use crate::package_manifest::PackageManifest;
1716

17+
#[derive(Serialize, Deserialize, Debug)]
18+
struct PackageManifestGlob(String);
19+
20+
// REFACTOR: drop the File suffix in this identifier
1821
#[derive(Serialize, Deserialize, Debug)]
1922
struct LernaManifestFile {
20-
packages: Vec<String>,
23+
packages: Vec<PackageManifestGlob>,
2124
}
2225

26+
// REFACTOR: drop the File suffix in this identifier
2327
#[derive(Serialize, Deserialize, Debug)]
2428
struct PackageManifestFile {
25-
workspaces: Vec<String>,
29+
workspaces: Vec<PackageManifestGlob>,
2630
}
2731

2832
#[derive(Debug)]
2933
pub struct MonorepoManifest {
30-
pub internal_package_manifests: Vec<PackageManifest>,
34+
root: PathBuf,
35+
globs: Vec<PackageManifestGlob>,
3136
}
3237

33-
fn get_internal_package_manifests<'a, P, I>(
34-
directory: P,
35-
package_globs: I,
36-
) -> Result<Vec<PackageManifest>>
37-
where
38-
P: AsRef<Path> + Sync,
39-
I: Iterator<Item = &'a String>,
40-
{
38+
fn get_internal_package_manifests(
39+
monorepo_root: &Path,
40+
package_globs: &[PackageManifestGlob],
41+
) -> Result<Vec<PackageManifest>> {
4142
let mut package_manifests: Vec<String> = package_globs
43+
.iter()
4244
.map(|package_manifest_glob| {
43-
Path::new(package_manifest_glob)
45+
Path::new(&package_manifest_glob.0)
4446
.join("package.json")
4547
.to_str()
4648
.expect("Path not valid UTF-8")
@@ -51,9 +53,10 @@ where
5153
// ignore paths to speed up file-system walk
5254
package_manifests.push(String::from("!node_modules/"));
5355

54-
let monorepo_root = directory.as_ref().to_owned();
56+
// Take ownership so we can move this value into the parallel_map
57+
let monorepo_root = monorepo_root.to_owned();
5558

56-
GlobWalkerBuilder::from_patterns(&directory, &package_manifests)
59+
GlobWalkerBuilder::from_patterns(&monorepo_root, &package_manifests)
5760
.file_type(FileType::FILE)
5861
.min_depth(1)
5962
.build()
@@ -64,14 +67,13 @@ where
6467
|options| options.threads(32),
6568
move |dir_entry| {
6669
PackageManifest::from_directory(
67-
monorepo_root.clone(),
70+
&monorepo_root,
6871
dir_entry
6972
.path()
7073
.parent()
7174
.expect("Unexpected package in monorepo root")
72-
.strip_prefix(monorepo_root.clone())
73-
.expect("Unexpected package in monorepo root")
74-
.to_owned(),
75+
.strip_prefix(&monorepo_root)
76+
.expect("Unexpected package in monorepo root"),
7577
)
7678
},
7779
)
@@ -82,57 +84,63 @@ impl MonorepoManifest {
8284
const LERNA_MANIFEST_FILENAME: &'static str = "lerna.json";
8385
const PACKAGE_MANIFEST_FILENAME: &'static str = "package.json";
8486

85-
fn from_lerna_manifest<P>(root: P) -> Result<MonorepoManifest>
86-
where
87-
P: AsRef<Path> + Sync,
88-
{
89-
let file = File::open(root.as_ref().join(Self::LERNA_MANIFEST_FILENAME))?;
90-
let reader = BufReader::new(file);
91-
let lerna_manifest_contents: LernaManifestFile = serde_json::from_reader(reader)?;
87+
fn from_lerna_manifest(root: &Path) -> Result<MonorepoManifest> {
88+
let filename = root.join(Self::LERNA_MANIFEST_FILENAME);
89+
let lerna_manifest: LernaManifestFile =
90+
read_json_from_file(&filename).with_context(|| {
91+
formatdoc!(
92+
"
93+
Unexpected contents in {:?}
94+
95+
I'm trying to parse the following properties and values out
96+
of this lerna.json file:
97+
98+
- packages: string[]
99+
",
100+
filename
101+
)
102+
})?;
92103
Ok(MonorepoManifest {
93-
internal_package_manifests: get_internal_package_manifests(
94-
&root,
95-
lerna_manifest_contents.packages.iter(),
96-
)?,
104+
root: root.to_owned(),
105+
globs: lerna_manifest.packages,
97106
})
98107
}
99108

100-
fn from_package_manifest<P>(root: P) -> Result<MonorepoManifest>
101-
where
102-
P: AsRef<Path> + Sync,
103-
{
104-
let file = File::open(root.as_ref().join(Self::PACKAGE_MANIFEST_FILENAME))?;
105-
let reader = BufReader::new(file);
106-
let package_manifest_contents: PackageManifestFile = serde_json::from_reader(reader)?;
109+
fn from_package_manifest(root: &Path) -> Result<MonorepoManifest> {
110+
let filename = root.join(Self::PACKAGE_MANIFEST_FILENAME);
111+
let package_manifest: PackageManifestFile =
112+
read_json_from_file(&filename).with_context(|| {
113+
formatdoc!(
114+
"
115+
Unexpected contents in {:?}
116+
117+
I'm trying to parse the following properties and values out
118+
of this package.json file:
119+
120+
- workspaces: string[]
121+
",
122+
filename
123+
)
124+
})?;
107125
Ok(MonorepoManifest {
108-
internal_package_manifests: get_internal_package_manifests(
109-
&root,
110-
package_manifest_contents.workspaces.iter(),
111-
)?,
126+
root: root.to_owned(),
127+
globs: package_manifest.workspaces,
112128
})
113129
}
114130

115-
pub fn from_directory<P>(root: P) -> Result<MonorepoManifest>
116-
where
117-
P: AsRef<Path> + Sync,
118-
{
119-
MonorepoManifest::from_lerna_manifest(&root)
120-
.or_else(|_| MonorepoManifest::from_package_manifest(&root))
131+
pub fn from_directory(root: &Path) -> Result<MonorepoManifest> {
132+
MonorepoManifest::from_lerna_manifest(root)
133+
.or_else(|_| MonorepoManifest::from_package_manifest(root))
121134
}
122135

123-
pub fn package_manifests_by_package_name(&self) -> Result<HashMap<String, &PackageManifest>> {
124-
self.internal_package_manifests
125-
.iter()
126-
.map(|manifest| Ok((manifest.contents.name.to_owned(), manifest)))
127-
.collect()
136+
pub fn package_manifests_by_package_name(&self) -> Result<HashMap<String, PackageManifest>> {
137+
Ok(get_internal_package_manifests(&self.root, &self.globs)?
138+
.into_iter()
139+
.map(|manifest| (manifest.contents.name.to_owned(), manifest))
140+
.collect())
128141
}
129142

130-
pub fn into_package_manifests_by_package_name(
131-
self,
132-
) -> Result<HashMap<String, PackageManifest>> {
133-
self.internal_package_manifests
134-
.into_iter()
135-
.map(|manifest| Ok((manifest.contents.name.clone(), manifest)))
136-
.collect()
143+
pub fn internal_package_manifests(&self) -> Result<Vec<PackageManifest>> {
144+
get_internal_package_manifests(&self.root, &self.globs)
137145
}
138146
}

0 commit comments

Comments
 (0)