From 4b892ac9bc7c53dfd3143c8324784db1f97b8ebc Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Mar 2025 12:48:15 +0100 Subject: [PATCH 1/6] add tests for symlinks --- crates/oxide/tests/scanner.rs | 142 +++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index 03daee0ebafb..a1b4e2efdace 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod scanner { - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use std::process::Command; use std::thread::sleep; use std::time::Duration; @@ -9,6 +9,16 @@ mod scanner { use tailwindcss_oxide::*; use tempfile::tempdir; + fn symlink, Q: AsRef>(original: P, link: Q) -> std::io::Result<()> { + #[cfg(not(windows))] + let result = std::os::unix::fs::symlink(original, link); + + #[cfg(windows)] + let result = std::os::windows::fs::symlink_dir(original, link); + + result + } + fn public_source_entry_from_pattern(dir: PathBuf, pattern: &str) -> PublicSourceEntry { let mut parts = pattern.split_whitespace(); let _ = parts.next().unwrap_or_default(); @@ -1611,4 +1621,134 @@ mod scanner { assert_eq!(globs, vec!["*", "src/*/*.{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 test_glob_with_symlinks() { + // Create a temporary working directory + let dir = tempdir().unwrap().into_path(); + + // Create files + create_files_in( + &dir, + &[ + (".gitignore", "node_modules\ndist"), + ( + "node_modules/.pnpm/@org+my-ui-library/dist/index.ts", + "content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']", + ), + // Make sure the `@org` does exist + ("node_modules/@org/.gitkeep", ""), + ], + ); + + // Symlink folder + let _ = symlink( + dir.join("node_modules/.pnpm/@org+my-ui-library"), + dir.join("node_modules/@org/my-ui-library"), + ); + + // Should work with just node_modules (because we skip symlinks, and go straight into the + // `.pnpm` folder) + let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( + dir.clone(), + "@source 'node_modules'", + )]); + let candidates = scanner.scan(); + + assert_eq!( + candidates, + vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] + ); + + // Should work with the full folder name + let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( + dir.clone(), + "@source 'node_modules/@org/my-ui-library'", + )]); + let candidates = scanner.scan(); + + assert_eq!( + candidates, + vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] + ); + + // This should work, but isn't + let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( + dir.clone(), + "@source 'node_modules/@org'", + )]); + let candidates = scanner.scan(); + + assert_eq!( + candidates, + vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] + ); + } + + #[test] + fn test_globs_with_recursive_symlinks() { + // Create a temporary working directory + let dir = tempdir().unwrap().into_path(); + + // Create files + create_files_in( + &dir, + &[ + ("b/index.html", "content-['b/index.html']"), + ("z/index.html", "content-['z/index.html']"), + ], + ); + + // Create recursive symlinks + let _ = symlink(dir.join("a"), dir.join("b")); + let _ = symlink(dir.join("b/c"), dir.join("c")); + let _ = symlink(dir.join("b/root"), &dir); + let _ = symlink(dir.join("c"), dir.join("a")); + + let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( + dir.clone(), + "@source '.'", + )]); + let candidates = scanner.scan(); + + assert_eq!( + candidates, + vec!["content-['b/index.html']", "content-['z/index.html']"] + ); + } + + #[test] + fn test_partial_globs_with_symlinks() { + // Create a temporary working directory + let dir = tempdir().unwrap().into_path(); + + // Create files + create_files_in(&dir, &[("abcd/xyz.html", "content-['abcd/xyz.html']")]); + + // Symlink folder + let _ = symlink(dir.join("abcd"), dir.join("efgh")); + + // No sources should find nothing + let mut scanner = Scanner::new(vec![]); + let candidates = scanner.scan(); + assert!(candidates.is_empty()); + + // Full symlinked folder name, should find the file + let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( + dir.clone(), + "@source 'efgh/*.html'", + )]); + let candidates = scanner.scan(); + + assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]); + + // Partially referencing the symlinked folder with a glob, should find the file + let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( + dir.clone(), + "@source 'ef*/*.html'", + )]); + let candidates = scanner.scan(); + + assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]); + } } From 6212add8b4c66938667ae30101acbeda6ee51730 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Mar 2025 12:48:35 +0100 Subject: [PATCH 2/6] cleanup: drop unnecessary `.to_owned()` --- crates/oxide/tests/scanner.rs | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index a1b4e2efdace..f6ac209e0556 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -656,8 +656,8 @@ mod scanner { assert_eq!( candidates, vec![ - "content-['project-a/index.html']".to_owned(), - "content-['project-b/index.html']".to_owned(), + "content-['project-a/index.html']", + "content-['project-b/index.html']" ] ); } @@ -712,8 +712,8 @@ mod scanner { assert_eq!( candidates, vec![ - "content-['project-a/index.html']".to_owned(), - "content-['project-b/index.html']".to_owned(), + "content-['project-a/index.html']", + "content-['project-b/index.html']" ] ); @@ -736,10 +736,10 @@ mod scanner { assert_eq!( candidates, vec![ - "content-['project-a/index.html']".to_owned(), - "content-['project-a/new.html']".to_owned(), - "content-['project-b/index.html']".to_owned(), - "content-['project-b/new.html']".to_owned(), + "content-['project-a/index.html']", + "content-['project-a/new.html']", + "content-['project-b/index.html']", + "content-['project-b/new.html']" ] ); @@ -768,12 +768,12 @@ mod scanner { assert_eq!( candidates, vec![ - "content-['project-a/index.html']".to_owned(), - "content-['project-a/new.html']".to_owned(), - "content-['project-a/sub1/sub2/index.html']".to_owned(), - "content-['project-b/index.html']".to_owned(), - "content-['project-b/new.html']".to_owned(), - "content-['project-b/sub1/sub2/index.html']".to_owned(), + "content-['project-a/index.html']", + "content-['project-a/new.html']", + "content-['project-a/sub1/sub2/index.html']", + "content-['project-b/index.html']", + "content-['project-b/new.html']", + "content-['project-b/sub1/sub2/index.html']" ] ); @@ -802,14 +802,14 @@ mod scanner { assert_eq!( candidates, vec![ - "content-['project-a/index.html']".to_owned(), - "content-['project-a/new.html']".to_owned(), - "content-['project-a/sub1/sub2/index.html']".to_owned(), - "content-['project-a/sub1/sub2/new.html']".to_owned(), - "content-['project-b/index.html']".to_owned(), - "content-['project-b/new.html']".to_owned(), - "content-['project-b/sub1/sub2/index.html']".to_owned(), - "content-['project-b/sub1/sub2/new.html']".to_owned(), + "content-['project-a/index.html']", + "content-['project-a/new.html']", + "content-['project-a/sub1/sub2/index.html']", + "content-['project-a/sub1/sub2/new.html']", + "content-['project-b/index.html']", + "content-['project-b/new.html']", + "content-['project-b/sub1/sub2/index.html']", + "content-['project-b/sub1/sub2/new.html']" ] ); } From 04ed169d70977dd1f6acad7d5237a335634e1e57 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Mar 2025 12:48:51 +0100 Subject: [PATCH 3/6] ensure we properly follow symlinks --- crates/oxide/src/scanner/mod.rs | 3 +++ crates/oxide/src/scanner/sources.rs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index 445dfc98372b..416191c27ce0 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -523,6 +523,9 @@ fn create_walker(sources: Sources) -> Option { let mut builder = WalkBuilder::new(first_root?); + // We have to follow symlinks + builder.follow_links(true); + // Scan hidden files / directories builder.hidden(false); diff --git a/crates/oxide/src/scanner/sources.rs b/crates/oxide/src/scanner/sources.rs index 450fcb7c2873..ea6c115a4fc3 100644 --- a/crates/oxide/src/scanner/sources.rs +++ b/crates/oxide/src/scanner/sources.rs @@ -243,7 +243,9 @@ impl From for SourceEntry { std::path::MAIN_SEPARATOR, dir, std::path::MAIN_SEPARATOR - )) + )) || value + .base + .ends_with(&format!("{}{}", std::path::MAIN_SEPARATOR, dir,)) }); match (value.negated, auto, inside_ignored_content_dir) { From a00fadd69f3e0da1f13ba07cda77dbb529363ed4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Mar 2025 13:03:41 +0100 Subject: [PATCH 4/6] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2c8117a14f..065c88e5fbbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix an issue causing the CLI to hang when processing Ruby files ([#17383](https://github.com/tailwindlabs/tailwindcss/pull/17383)) +- Fix symlink issues when resolving `@source` directives ([#17391](https://github.com/tailwindlabs/tailwindcss/pull/17391)) ## [4.0.16] - 2025-03-25 From d097617d44e8a15918b9bff7c879bb12e42a0a41 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Mar 2025 13:04:39 +0100 Subject: [PATCH 5/6] fix changelog --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 065c88e5fbbb..4071daaab0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix an issue causing the CLI to hang when processing Ruby files ([#17383](https://github.com/tailwindlabs/tailwindcss/pull/17383)) - Fix symlink issues when resolving `@source` directives ([#17391](https://github.com/tailwindlabs/tailwindcss/pull/17391)) +## [4.0.17] - 2025-03-26 + +### Fixed + +- Fix an issue causing the CLI to hang when processing Ruby files ([#17383](https://github.com/tailwindlabs/tailwindcss/pull/17383)) + ## [4.0.16] - 2025-03-25 ### Added @@ -3583,7 +3588,8 @@ No release notes - Everything! -[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.16...HEAD +[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.17...HEAD +[4.0.17]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.16...v4.0.17 [4.0.16]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.15...v4.0.16 [4.0.15]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.14...v4.0.15 [4.0.14]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.13...v4.0.14 From c3e53e78456489862fc6286daf70eaec89d7544d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Mar 2025 13:06:40 +0100 Subject: [PATCH 6/6] cleanup comments --- crates/oxide/tests/scanner.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index f6ac209e0556..c5ece5d75090 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -1624,10 +1624,7 @@ mod scanner { #[test] fn test_glob_with_symlinks() { - // Create a temporary working directory let dir = tempdir().unwrap().into_path(); - - // Create files create_files_in( &dir, &[ @@ -1640,15 +1637,11 @@ mod scanner { ("node_modules/@org/.gitkeep", ""), ], ); - - // Symlink folder let _ = symlink( dir.join("node_modules/.pnpm/@org+my-ui-library"), dir.join("node_modules/@org/my-ui-library"), ); - // Should work with just node_modules (because we skip symlinks, and go straight into the - // `.pnpm` folder) let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( dir.clone(), "@source 'node_modules'", @@ -1660,7 +1653,6 @@ mod scanner { vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] ); - // Should work with the full folder name let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( dir.clone(), "@source 'node_modules/@org/my-ui-library'", @@ -1672,7 +1664,6 @@ mod scanner { vec!["content-['node_modules/.pnpm/@org+my-ui-library/dist/index.ts']"] ); - // This should work, but isn't let mut scanner = Scanner::new(vec![public_source_entry_from_pattern( dir.clone(), "@source 'node_modules/@org'", @@ -1687,10 +1678,7 @@ mod scanner { #[test] fn test_globs_with_recursive_symlinks() { - // Create a temporary working directory let dir = tempdir().unwrap().into_path(); - - // Create files create_files_in( &dir, &[ @@ -1719,13 +1707,8 @@ mod scanner { #[test] fn test_partial_globs_with_symlinks() { - // Create a temporary working directory let dir = tempdir().unwrap().into_path(); - - // Create files create_files_in(&dir, &[("abcd/xyz.html", "content-['abcd/xyz.html']")]); - - // Symlink folder let _ = symlink(dir.join("abcd"), dir.join("efgh")); // No sources should find nothing