Skip to content

Commit

Permalink
Add .prettierignore support (#21297)
Browse files Browse the repository at this point in the history
Closes #11115

**Context**:

Consider a monorepo setup like this: the root has Prettier installed,
but the individual monorepos do not. In this case, only one Prettier
instance is used, with its installation located at the root. The
monorepos also use this same instance for formatting.

However, monorepo can have its own `.prettierignore` file, which will
take precedence over the `.prettierignore` file at the root level (if
one exists) for files in that monorepo.

<img
src="https://github.com/user-attachments/assets/742f16ac-11ad-4d2f-a5a2-696e47a617b9"
alt="prettier" width="200px" />

**Implementation**:

From the context above, we should keep ignore dir decoupled from the
Prettier instance. This means that even if the project has only one
Prettier installation (and thus a single Prettier instance), there can
still be multiple `.prettierignore` in play.

This approach also allows us to respect `.prettierignore` even when the
project does not have Prettier installed locally and instead relies on
the editor’s Prettier instance.

**Tests**:

1. No Prettier in project, using editor Prettier: Ensures
`.prettierignore` is respected even without a local Prettier
installation.
2. Monorepo with root Prettier and child `.prettierignore`: Confirms
that the child project’s ignore file is correctly used.
3. Monorepo with root and child `.prettierignore` files: Verifies the
child ignore file takes precedence over the root’s.

Release Notes:

- Added `.prettierignore` support to the Prettier integration.
  • Loading branch information
0xtimsb authored Dec 12, 2024
1 parent 8dd1c23 commit 6a37307
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 4 deletions.
253 changes: 252 additions & 1 deletion crates/prettier/src/prettier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl Prettier {
"prettier.config.js",
"prettier.config.cjs",
".editorconfig",
".prettierignore",
];

pub async fn locate_prettier_installation(
Expand Down Expand Up @@ -134,6 +135,101 @@ impl Prettier {
}
}

pub async fn locate_prettier_ignore(
fs: &dyn Fs,
prettier_ignores: &HashSet<PathBuf>,
locate_from: &Path,
) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
let mut path_to_check = locate_from
.components()
.take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
.collect::<PathBuf>();
if path_to_check != locate_from {
log::debug!(
"Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules"
);
return Ok(ControlFlow::Break(()));
}

let path_to_check_metadata = fs
.metadata(&path_to_check)
.await
.with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
.with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
if !path_to_check_metadata.is_dir {
path_to_check.pop();
}

let mut closest_package_json_path = None;
loop {
if prettier_ignores.contains(&path_to_check) {
log::debug!("Found prettier ignore at {path_to_check:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else if let Some(package_json_contents) =
read_package_json(fs, &path_to_check).await?
{
let ignore_path = path_to_check.join(".prettierignore");
if let Some(metadata) = fs
.metadata(&ignore_path)
.await
.with_context(|| format!("fetching metadata for {ignore_path:?}"))?
{
if !metadata.is_dir && !metadata.is_symlink {
log::info!("Found prettier ignore at {ignore_path:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
}
}
match &closest_package_json_path {
None => closest_package_json_path = Some(path_to_check.clone()),
Some(closest_package_json_path) => {
if let Some(serde_json::Value::Array(workspaces)) =
package_json_contents.get("workspaces")
{
let subproject_path = closest_package_json_path
.strip_prefix(&path_to_check)
.expect("traversing path parents, should be able to strip prefix");

if workspaces
.iter()
.filter_map(|value| {
if let serde_json::Value::String(s) = value {
Some(s.clone())
} else {
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)
})
})
{
let workspace_ignore = path_to_check.join(".prettierignore");
if let Some(metadata) = fs.metadata(&workspace_ignore).await? {
if !metadata.is_dir {
log::info!("Found prettier ignore at workspace root {workspace_ignore:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
}
}
}
}
}
}
}

if !path_to_check.pop() {
log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
}
}
}

#[cfg(any(test, feature = "test-support"))]
pub async fn start(
_: LanguageServerId,
Expand Down Expand Up @@ -201,6 +297,7 @@ impl Prettier {
&self,
buffer: &Model<Buffer>,
buffer_path: Option<PathBuf>,
ignore_dir: Option<PathBuf>,
cx: &mut AsyncAppContext,
) -> anyhow::Result<Diff> {
match self {
Expand Down Expand Up @@ -315,11 +412,17 @@ impl Prettier {

}

let ignore_path = ignore_dir.and_then(|dir| {
let ignore_file = dir.join(".prettierignore");
ignore_file.is_file().then_some(ignore_file)
});

log::debug!(
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
buffer.file().map(|f| f.full_path(cx)),
plugins,
prettier_options,
ignore_path,
);

anyhow::Ok(FormatParams {
Expand All @@ -329,6 +432,7 @@ impl Prettier {
plugins,
path: buffer_path,
prettier_options,
ignore_path,
},
})
})?
Expand Down Expand Up @@ -449,6 +553,7 @@ struct FormatOptions {
#[serde(rename = "filepath")]
path: Option<PathBuf>,
prettier_options: Option<HashMap<String, serde_json::Value>>,
ignore_path: Option<PathBuf>,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -840,4 +945,150 @@ mod tests {
},
};
}

#[gpui::test]
async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"project": {
"src": {
"index.js": "// index.js file contents",
"ignored.js": "// this file should be ignored",
},
".prettierignore": "ignored.js",
"package.json": r#"{
"name": "test-project"
}"#
}
}),
)
.await;

assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/project/src/index.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
"Should find prettierignore in project root"
);
}

#[gpui::test]
async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
cx: &mut gpui::TestAppContext,
) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"monorepo": {
"node_modules": {
"prettier": {
"index.js": "// Dummy prettier package file",
}
},
"packages": {
"web": {
"src": {
"index.js": "// index.js contents",
"ignored.js": "// this should be ignored",
},
".prettierignore": "ignored.js",
"package.json": r#"{
"name": "web-package"
}"#
}
},
"package.json": r#"{
"workspaces": ["packages/*"],
"devDependencies": {
"prettier": "^2.0.0"
}
}"#
}
}),
)
.await;

assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/monorepo/packages/web/src/index.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
"Should find prettierignore in child package"
);
}

#[gpui::test]
async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
cx: &mut gpui::TestAppContext,
) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"monorepo": {
"node_modules": {
"prettier": {
"index.js": "// Dummy prettier package file",
}
},
".prettierignore": "main.js",
"packages": {
"web": {
"src": {
"main.js": "// this should not be ignored",
"ignored.js": "// this should be ignored",
},
".prettierignore": "ignored.js",
"package.json": r#"{
"name": "web-package"
}"#
}
},
"package.json": r#"{
"workspaces": ["packages/*"],
"devDependencies": {
"prettier": "^2.0.0"
}
}"#
}
}),
)
.await;

assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/monorepo/packages/web/src/main.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
"Should find child package prettierignore first"
);

assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/monorepo/packages/web/src/ignored.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
"Should find child package prettierignore first"
);
}
}
24 changes: 22 additions & 2 deletions crates/prettier/src/prettier_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ class Prettier {
process.exit(1);
}
process.stderr.write(
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`,
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(
config,
)}\n`,
);
process.stdin.resume();
handleBuffer(new Prettier(prettierPath, prettier, config));
Expand All @@ -68,7 +70,9 @@ async function handleBuffer(prettier) {
sendResponse({
id: message.id,
...makeError(
`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`,
`error during message '${JSON.stringify(
errorMessage,
)}' handling: ${e}`,
),
});
});
Expand Down Expand Up @@ -189,6 +193,22 @@ async function handleMessage(message, prettier) {
if (params.options.filepath) {
resolvedConfig =
(await prettier.prettier.resolveConfig(params.options.filepath)) || {};

if (params.options.ignorePath) {
const fileInfo = await prettier.prettier.getFileInfo(
params.options.filepath,
{
ignorePath: params.options.ignorePath,
},
);
if (fileInfo.ignored) {
process.stderr.write(
`Ignoring file '${params.options.filepath}' based on rules in '${params.options.ignorePath}'\n`,
);
sendResponse({ id, result: { text: params.text } });
return;
}
}
}

// Marking the params.options.filepath as undefined makes
Expand Down
Loading

0 comments on commit 6a37307

Please sign in to comment.