Lookup prettier more leniently (#14403)

Do not require the `prettier` dependency name to be in package.json's
[dev]Dependencies, instead just checking the `node_modules` contents.

Release Notes:

- Improved `prettier` detection to pick up its installation from
transitive dependencies
([12731](https://github.com/zed-industries/zed/issues/12731)
This commit is contained in:
Kirill Bulatov 2024-07-13 21:59:14 +03:00 committed by GitHub
parent e5dc6beace
commit 88c5eb550e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -84,7 +84,7 @@ impl Prettier {
path_to_check.pop(); path_to_check.pop();
} }
let mut project_path_with_prettier_dependency = None; let mut closest_package_json_path = None;
loop { loop {
if installed_prettiers.contains(&path_to_check) { if installed_prettiers.contains(&path_to_check) {
log::debug!("Found prettier path {path_to_check:?} in installed prettiers"); log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
@ -92,61 +92,44 @@ impl Prettier {
} else if let Some(package_json_contents) = } else if let Some(package_json_contents) =
read_package_json(fs, &path_to_check).await? read_package_json(fs, &path_to_check).await?
{ {
if has_prettier_in_package_json(&package_json_contents) { if has_prettier_in_node_modules(fs, &path_to_check).await? {
if has_prettier_in_node_modules(fs, &path_to_check).await? { log::debug!("Found prettier path {path_to_check:?} in the node_modules");
log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules"); return Ok(ControlFlow::Continue(Some(path_to_check)));
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else if project_path_with_prettier_dependency.is_none() {
project_path_with_prettier_dependency = Some(path_to_check.clone());
}
} else { } else {
match package_json_contents.get("workspaces") { match &closest_package_json_path {
Some(serde_json::Value::Array(workspaces)) => { None => closest_package_json_path = Some(path_to_check.clone()),
match &project_path_with_prettier_dependency { Some(closest_package_json_path) => {
Some(project_path_with_prettier_dependency) => { match package_json_contents.get("workspaces") {
let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix"); Some(serde_json::Value::Array(workspaces)) => {
if workspaces.iter().filter_map(|value| { let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
if let serde_json::Value::String(s) = value { if workspaces.iter().filter_map(|value| {
Some(s.clone()) if let serde_json::Value::String(s) = value {
} else { Some(s.clone())
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
None
}
}).any(|workspace_definition| {
if let Some(path_matcher) = PathMatcher::new(&[workspace_definition.clone()]).ok() {
path_matcher.is_match(subproject_path)
} else {
workspace_definition == subproject_path.to_string_lossy()
}
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else { } else {
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}"); log::warn!("Skipping non-string 'workspaces' value: {value:?}");
None
} }
}).any(|workspace_definition| {
workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path))
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else {
log::warn!("Skipping path {path_to_check:?} workspace root with workspaces {workspaces:?} that have no prettier installed");
} }
None => { },
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json"); Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
} None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
} }
},
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
} }
}
} }
} }
if !path_to_check.pop() { if !path_to_check.pop() {
match project_path_with_prettier_dependency { log::debug!("Found no prettier in ancestors of {locate_from:?}");
Some(closest_prettier_discovered) => { return Ok(ControlFlow::Continue(None));
anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}")
}
None => {
log::debug!("Found no prettier in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
}
}
} }
} }
} }
@ -448,22 +431,6 @@ async fn read_package_json(
Ok(None) Ok(None)
} }
fn has_prettier_in_package_json(
package_json_contents: &HashMap<String, serde_json::Value>,
) -> bool {
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return true;
}
}
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return true;
}
}
false
}
enum Format {} enum Format {}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@ -548,40 +515,36 @@ mod tests {
) )
.await; .await;
assert!( assert_eq!(
matches!( Prettier::locate_prettier_installation(
Prettier::locate_prettier_installation( fs.as_ref(),
fs.as_ref(), &HashSet::default(),
&HashSet::default(), Path::new("/root/.config/zed/settings.json"),
Path::new("/root/.config/zed/settings.json"), )
) .await
.await, .unwrap(),
Ok(ControlFlow::Continue(None)) ControlFlow::Continue(None),
), "Should find no prettier for path hierarchy without it"
"Should successfully find no prettier for path hierarchy without it"
); );
assert!( assert_eq!(
matches!( Prettier::locate_prettier_installation(
Prettier::locate_prettier_installation( fs.as_ref(),
fs.as_ref(), &HashSet::default(),
&HashSet::default(), Path::new("/root/work/project/src/index.js")
Path::new("/root/work/project/src/index.js") )
) .await.unwrap(),
.await, ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
Ok(ControlFlow::Continue(None)) "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
),
"Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
); );
assert!( assert_eq!(
matches!( Prettier::locate_prettier_installation(
Prettier::locate_prettier_installation( fs.as_ref(),
fs.as_ref(), &HashSet::default(),
&HashSet::default(), Path::new("/root/work/project/node_modules/expect/build/print.js")
Path::new("/root/work/project/node_modules/expect/build/print.js") )
) .await
.await, .unwrap(),
Ok(ControlFlow::Break(())) ControlFlow::Break(()),
),
"Should not format files inside node_modules/" "Should not format files inside node_modules/"
); );
} }
@ -691,18 +654,17 @@ mod tests {
) )
.await; .await;
match Prettier::locate_prettier_installation( assert_eq!(
fs.as_ref(), Prettier::locate_prettier_installation(
&HashSet::default(), fs.as_ref(),
Path::new("/root/work/web_blog/pages/[slug].tsx") &HashSet::default(),
) Path::new("/root/work/web_blog/pages/[slug].tsx")
.await { )
Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), .await
Err(e) => { .unwrap(),
let message = e.to_string(); ControlFlow::Continue(None),
assert!(message.contains("/root/work/web_blog"), "Error message should mention which project had prettier defined"); "Should find no prettier when node_modules don't have it"
}, );
};
assert_eq!( assert_eq!(
Prettier::locate_prettier_installation( Prettier::locate_prettier_installation(