Fix symlink issues when resolving @source directives (#17391)
This PR fixes some issues related to symlinks when using them in the `@source` directive. Fixes: #16765 Fixes: #16038 ## Test plan 1. Added tests to prove this works - Added a recursive symlink test as well to make sure we don't hang 2. Existing tests still pass [ci-all]
This commit is contained in:
parent
18365fff6c
commit
b0c48c3de9
4 changed files with 158 additions and 24 deletions
|
|
@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Fixed
|
||||
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -523,6 +523,9 @@ fn create_walker(sources: Sources) -> Option<WalkBuilder> {
|
|||
|
||||
let mut builder = WalkBuilder::new(first_root?);
|
||||
|
||||
// We have to follow symlinks
|
||||
builder.follow_links(true);
|
||||
|
||||
// Scan hidden files / directories
|
||||
builder.hidden(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,9 @@ impl From<PublicSourceEntry> 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) {
|
||||
|
|
|
|||
|
|
@ -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<P: AsRef<Path>, Q: AsRef<Path>>(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();
|
||||
|
|
@ -646,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']"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -702,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']"
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -726,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']"
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -758,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']"
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -792,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']"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -1611,4 +1621,117 @@ 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() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
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", ""),
|
||||
],
|
||||
);
|
||||
let _ = symlink(
|
||||
dir.join("node_modules/.pnpm/@org+my-ui-library"),
|
||||
dir.join("node_modules/@org/my-ui-library"),
|
||||
);
|
||||
|
||||
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']"]
|
||||
);
|
||||
|
||||
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']"]
|
||||
);
|
||||
|
||||
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() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
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() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
create_files_in(&dir, &[("abcd/xyz.html", "content-['abcd/xyz.html']")]);
|
||||
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']"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue