From 3dd46c2b417d8fd520d4f295afd34332327103de Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 7 Apr 2025 16:13:34 +0200 Subject: [PATCH 1/5] add failing test --- crates/oxide/tests/scanner.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index 681d63620f22..08ed9e9ff66c 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -311,6 +311,21 @@ 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(&[("some.pages/index.html", "content-['some.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 { From c47384ef349da459d328cb624af0ea5a5d673e4d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 7 Apr 2025 16:13:45 +0200 Subject: [PATCH 2/5] ignore binary extensions, but not folders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We generate a glob to ignore binary extensions that looks 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 will instead generate the following globs: ``` *.{mp4,pages,exe,…} !*.{mp4,pages,exe,…}/ ``` This way, the `/` indicates that we are dealing with a folder and that these should _not_ be ignored. --- .../src/scanner/auto_source_detection.rs | 34 +++++++++++++------ crates/oxide/src/scanner/mod.rs | 7 ++-- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/crates/oxide/src/scanner/auto_source_detection.rs b/crates/oxide/src/scanner/auto_source_detection.rs index e9b7f64aacbc..5a568b13416a 100644 --- a/crates/oxide/src/scanner/auto_source_detection.rs +++ b/crates/oxide/src/scanner/auto_source_detection.rs @@ -15,7 +15,9 @@ pub static RULES: sync::LazyLock = sync::LazyLock::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(); + for glob in BINARY_EXTENSIONS_GLOB.clone() { + builder.add_line(None, &glob).unwrap(); + } builder.add_line(None, &IGNORED_FILES_GLOB).unwrap(); builder.build().unwrap() @@ -42,15 +44,27 @@ static IGNORED_EXTENSIONS_GLOB: sync::LazyLock = sync::LazyLock::new(|| ) }); -pub static BINARY_EXTENSIONS_GLOB: sync::LazyLock = sync::LazyLock::new(|| { - format!( - "*.{{{}}}", - include_str!("fixtures/binary-extensions.txt") - .trim() - .lines() - .collect::>() - .join(",") - ) +pub static BINARY_EXTENSIONS_GLOB: sync::LazyLock> = sync::LazyLock::new(|| { + vec![ + // Ignore the extensions + format!( + "*.{{{}}}", + include_str!("fixtures/binary-extensions.txt") + .trim() + .lines() + .collect::>() + .join(","), + ), + // Do not ignore folders that happen to end with a binary extension + format!( + "!*.{{{}}}/", + include_str!("fixtures/binary-extensions.txt") + .trim() + .lines() + .collect::>() + .join(","), + ), + ] }); static IGNORED_FILES_GLOB: sync::LazyLock = sync::LazyLock::new(|| { diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index fad6fa712da4..126c6cbf72fd 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -564,10 +564,9 @@ fn create_walker(sources: Sources) -> Option { .insert(format!("!{}", "/**/*")); // External sources should still disallow binary extensions: - ignores - .entry(base) - .or_default() - .insert(BINARY_EXTENSIONS_GLOB.clone()); + for glob in BINARY_EXTENSIONS_GLOB.clone() { + ignores.entry(base).or_default().insert(glob); + } } } } From e094c30398247bc643518b9643a422bbadef6bd2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 9 Apr 2025 10:56:31 +0200 Subject: [PATCH 3/5] WIP: potential failing test The implementation we chose right now means that there is a situation where an explicit ignore in your `.gitignore` file could now be included due to the fix we implemented. We can always add an additional `@source not` on top of everything, but ideally this is not needed at all. --- crates/oxide/tests/scanner.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index 08ed9e9ff66c..4003ff253048 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -319,7 +319,17 @@ mod scanner { globs, normalized_sources, .. - } = scan(&[("some.pages/index.html", "content-['some.pages/index.html']")]); + } = 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}"]); From c9329be1788675f2b8328da55b82e1c740bc2365 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Apr 2025 17:19:27 +0200 Subject: [PATCH 4/5] Proposed fix --- crates/ignore/src/gitignore.rs | 24 ++++++++++ .../src/scanner/auto_source_detection.rs | 44 ++++++++----------- crates/oxide/src/scanner/mod.rs | 11 +++-- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/crates/ignore/src/gitignore.rs b/crates/ignore/src/gitignore.rs index 30f1ccef2d7c..7da86153cf79 100644 --- a/crates/ignore/src/gitignore.rs +++ b/crates/ignore/src/gitignore.rs @@ -85,6 +85,8 @@ pub struct Gitignore { num_ignores: u64, num_whitelists: u64, matches: Option>>>, + // 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, 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. diff --git a/crates/oxide/src/scanner/auto_source_detection.rs b/crates/oxide/src/scanner/auto_source_detection.rs index 5a568b13416a..0d723a1ab1f5 100644 --- a/crates/oxide/src/scanner/auto_source_detection.rs +++ b/crates/oxide/src/scanner/auto_source_detection.rs @@ -10,17 +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 = sync::LazyLock::new(|| { +pub static RULES: sync::LazyLock> = 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(); - for glob in BINARY_EXTENSIONS_GLOB.clone() { - builder.add_line(None, &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> = sync::LazyLock::new(|| { @@ -44,27 +48,15 @@ static IGNORED_EXTENSIONS_GLOB: sync::LazyLock = sync::LazyLock::new(|| ) }); -pub static BINARY_EXTENSIONS_GLOB: sync::LazyLock> = sync::LazyLock::new(|| { - vec![ - // Ignore the extensions - format!( - "*.{{{}}}", - include_str!("fixtures/binary-extensions.txt") - .trim() - .lines() - .collect::>() - .join(","), - ), - // Do not ignore folders that happen to end with a binary extension - format!( - "!*.{{{}}}/", - include_str!("fixtures/binary-extensions.txt") - .trim() - .lines() - .collect::>() - .join(","), - ), - ] +pub static BINARY_EXTENSIONS_GLOB: sync::LazyLock = sync::LazyLock::new(|| { + format!( + "*.{{{}}}", + include_str!("fixtures/binary-extensions.txt") + .trim() + .lines() + .collect::>() + .join(",") + ) }); static IGNORED_FILES_GLOB: sync::LazyLock = sync::LazyLock::new(|| { diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index 126c6cbf72fd..5512ad000354 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -564,9 +564,10 @@ fn create_walker(sources: Sources) -> Option { .insert(format!("!{}", "/**/*")); // External sources should still disallow binary extensions: - for glob in BINARY_EXTENSIONS_GLOB.clone() { - ignores.entry(base).or_default().insert(glob); - } + ignores + .entry(base) + .or_default() + .insert(BINARY_EXTENSIONS_GLOB.clone()); } } } @@ -629,7 +630,9 @@ fn create_walker(sources: Sources) -> Option { } // 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 { From 44d5d89365cf4db411049e8f1bb6ddb6b10245af Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Apr 2025 18:37:13 +0200 Subject: [PATCH 5/5] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351d4e4837a5..914177cfbb0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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