Ignore binary extensions, except in folder names (#17595)

We generate a glob to ignore binary extensions that looks something like
this:
```
*.{mp4,pages,exe,…}
```

However, if you have a folder that happens to end in `.pages` for
example, then this folder will be ignored in its entirety.

To solve this, we added a new flag to the `Gitignore` struct so we can
register a bunch of ignore rules that _only_ apply to paths and not
folders.

Fixes: #17569

## Test plan

- Added a unit test

---------

Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
Robin Malfait 2025-04-12 13:30:17 +02:00 committed by GitHub
parent d801d8dc54
commit bbd916aaa0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 62 additions and 4 deletions

View File

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure container query variant names can contain hyphens ([#17628](https://github.com/tailwindlabs/tailwindcss/pull/17628))
- Ensure `shadow-inherit`, `inset-shadow-inherit`, `drop-shadow-inherit`, and `text-shadow-inherit` inherits the shadow color ([#17647](https://github.com/tailwindlabs/tailwindcss/pull/17647))
- Ensure compatibility with array tuples used in `fontSize` JS theme keys ([#17630](https://github.com/tailwindlabs/tailwindcss/pull/17630))
- Ensure folders with binary file extensions in its name are scanned for utilities ([#17595](https://github.com/tailwindlabs/tailwindcss/pull/17595))
- Upgrade: Convert `fontSize` array tuple syntax to CSS theme variables ([#17630](https://github.com/tailwindlabs/tailwindcss/pull/17630))
## [4.1.3] - 2025-04-04

View File

@ -85,6 +85,8 @@ pub struct Gitignore {
num_ignores: u64,
num_whitelists: u64,
matches: Option<Arc<Pool<Vec<usize>>>>,
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
only_on_files: bool,
}
impl Gitignore {
@ -140,6 +142,8 @@ impl Gitignore {
num_ignores: 0,
num_whitelists: 0,
matches: None,
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
only_on_files: false,
}
}
@ -240,6 +244,10 @@ impl Gitignore {
if self.is_empty() {
return Match::None;
}
// CHANGED: Rules marked as only_on_files can not match against directories.
if self.only_on_files && is_dir {
return Match::None;
}
let path = path.as_ref();
let mut matches = self.matches.as_ref().unwrap().get();
let candidate = Candidate::new(path);
@ -295,6 +303,8 @@ pub struct GitignoreBuilder {
root: PathBuf,
globs: Vec<Glob>,
case_insensitive: bool,
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
only_on_files: bool,
}
impl GitignoreBuilder {
@ -311,6 +321,8 @@ impl GitignoreBuilder {
root: strip_prefix("./", root).unwrap_or(root).to_path_buf(),
globs: vec![],
case_insensitive: false,
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
only_on_files: false,
}
}
@ -331,6 +343,8 @@ impl GitignoreBuilder {
num_ignores: nignore as u64,
num_whitelists: nwhite as u64,
matches: Some(Arc::new(Pool::new(|| vec![]))),
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
only_on_files: self.only_on_files,
})
}
@ -514,6 +528,16 @@ impl GitignoreBuilder {
self.case_insensitive = yes;
Ok(self)
}
/// CHANGED: Add a flag to have Gitignore rules that apply only to files.
///
/// If this is set, then the globs will only be matched against file paths.
/// This will ensure that ignore rules like `*.pages` will _only_ ignore
/// files ending in `.pages` and not folders ending in `.pages`.
pub fn only_on_files(&mut self, yes: bool) -> &mut GitignoreBuilder {
self.only_on_files = yes;
self
}
}
/// Return the file path of the current environment's global gitignore file.

View File

@ -10,15 +10,21 @@ use std::sync;
/// - Ignoring common binary file extensions like `.png` and `.jpg`
/// - Ignoring common files like `yarn.lock` and `package-lock.json`
///
pub static RULES: sync::LazyLock<Gitignore> = sync::LazyLock::new(|| {
pub static RULES: sync::LazyLock<Vec<Gitignore>> = sync::LazyLock::new(|| {
let mut builder = GitignoreBuilder::new("");
builder.add_line(None, &IGNORED_CONTENT_DIRS_GLOB).unwrap();
builder.add_line(None, &IGNORED_EXTENSIONS_GLOB).unwrap();
builder.add_line(None, &BINARY_EXTENSIONS_GLOB).unwrap();
builder.add_line(None, &IGNORED_FILES_GLOB).unwrap();
builder.build().unwrap()
// Ensure these rules do not match on folder names
let mut file_only_builder = GitignoreBuilder::new("");
file_only_builder
.only_on_files(true)
.add_line(None, &BINARY_EXTENSIONS_GLOB)
.unwrap();
vec![builder.build().unwrap(), file_only_builder.build().unwrap()]
});
pub static IGNORED_CONTENT_DIRS: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(|| {

View File

@ -630,7 +630,9 @@ fn create_walker(sources: Sources) -> Option<WalkBuilder> {
}
// Setup auto source detection rules
builder.add_gitignore(auto_source_detection::RULES.clone());
for ignore in auto_source_detection::RULES.iter() {
builder.add_gitignore(ignore.clone());
}
// Setup ignores based on `@source` definitions
for (base, patterns) in ignores {

View File

@ -311,6 +311,31 @@ mod scanner {
assert_eq!(normalized_sources, vec!["**/*"]);
}
// https://github.com/tailwindlabs/tailwindcss/issues/17569
#[test]
fn it_should_not_ignore_folders_that_end_with_a_binary_extension() {
let ScanResult {
files,
globs,
normalized_sources,
..
} = scan(&[
// Looks like `.pages` binary extension, but it's a folder
("some.pages/index.html", "content-['some.pages/index.html']"),
// Ignore a specific folder. This is to ensure that this still "wins" from the internal
// solution of dealing with binary extensions for files only.
(".gitignore", "other.pages"),
(
"other.pages/index.html",
"content-['other.pages/index.html']",
),
]);
assert_eq!(files, vec!["some.pages/index.html"]);
assert_eq!(globs, vec!["*", "some.pages/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}"]);
assert_eq!(normalized_sources, vec!["**/*"]);
}
#[test]
fn it_should_ignore_known_extensions() {
let ScanResult {