mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Fix crash when using @source containing .. (#14831)
This PR fixes an issue where a `@source` crashes when the path eventually resolves to a path ending in `..`. We have to make sure that we canonicalize the path to make sure that we are working with the real directory. --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
eb54dcdbfc
commit
92007a5b23
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Detect classes in new files when using `@tailwindcss/postcss` ([#14829](https://github.com/tailwindlabs/tailwindcss/pull/14829))
|
||||
- Fix crash when using `@source` containing `..` ([#14831](https://github.com/tailwindlabs/tailwindcss/pull/14831))
|
||||
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
|
||||
|
||||
## [4.0.0-alpha.31] - 2024-10-29
|
||||
|
||||
@ -42,7 +42,10 @@ pub fn hoist_static_glob_parts(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
|
||||
// folders.
|
||||
if pattern.is_empty() && base.is_file() {
|
||||
result.push(GlobEntry {
|
||||
// SAFETY: `parent()` will be available because we verify `base` is a file, thus a
|
||||
// parent folder exists.
|
||||
base: base.parent().unwrap().to_string_lossy().to_string(),
|
||||
// SAFETY: `file_name()` will be available because we verify `base` is a file.
|
||||
pattern: base.file_name().unwrap().to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
@ -100,6 +103,7 @@ pub fn optimize_patterns(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
|
||||
GlobEntry {
|
||||
base,
|
||||
pattern: match size {
|
||||
// SAFETY: we can unwrap here because we know that the size is 1.
|
||||
1 => patterns.next().unwrap(),
|
||||
_ => {
|
||||
let mut patterns = patterns.collect::<Vec<_>>();
|
||||
|
||||
@ -322,17 +322,27 @@ impl Scanner {
|
||||
|
||||
fn join_paths(a: &str, b: &str) -> PathBuf {
|
||||
let mut tmp = a.to_owned();
|
||||
let b = b.trim_end_matches("**/*").trim_end_matches('/');
|
||||
|
||||
if b.starts_with('/') {
|
||||
return PathBuf::from(b);
|
||||
}
|
||||
|
||||
// On Windows a path like C:/foo.txt is absolute but C:foo.txt is not
|
||||
// (the 2nd is relative to the CWD)
|
||||
if b.chars().nth(1) == Some(':') && b.chars().nth(2) == Some('/') {
|
||||
return PathBuf::from(b);
|
||||
}
|
||||
|
||||
tmp += "/";
|
||||
tmp += b.trim_end_matches("**/*").trim_end_matches('/');
|
||||
tmp += b;
|
||||
|
||||
PathBuf::from(&tmp)
|
||||
}
|
||||
|
||||
for path in auto_sources
|
||||
.iter()
|
||||
.map(|source| join_paths(&source.base, &source.pattern))
|
||||
{
|
||||
for path in auto_sources.iter().filter_map(|source| {
|
||||
dunce::canonicalize(join_paths(&source.base, &source.pattern)).ok()
|
||||
}) {
|
||||
// Insert a glob for the base path, so we can see new files/folders in the directory itself.
|
||||
self.globs.push(GlobEntry {
|
||||
base: path.to_string_lossy().into(),
|
||||
|
||||
@ -362,6 +362,8 @@ impl<'a> Extractor<'a> {
|
||||
}
|
||||
|
||||
// The ':` must be preceded by a-Z0-9 because it represents a property name.
|
||||
// SAFETY: the Self::validate_arbitrary_property function from above validates that the
|
||||
// `:` exists.
|
||||
let colon = utility.find(":").unwrap();
|
||||
|
||||
if !utility
|
||||
|
||||
@ -25,13 +25,13 @@ static IGNORED_FILES: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(||
|
||||
static IGNORED_CONTENT_DIRS: sync::LazyLock<Vec<&'static str>> =
|
||||
sync::LazyLock::new(|| vec![".git"]);
|
||||
|
||||
#[tracing::instrument(skip(root))]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn resolve_allowed_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
// Read the directory recursively with no depth limit
|
||||
read_dir(root, None)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(root))]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn resolve_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
@ -40,7 +40,7 @@ pub fn resolve_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
.filter_map(Result::ok)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(root))]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn read_dir(root: &Path, depth: Option<usize>) -> impl Iterator<Item = DirEntry> {
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
|
||||
@ -8,7 +8,7 @@ mod scanner {
|
||||
use tailwindcss_oxide::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn create_files_in(dir: &path::PathBuf, paths: &[(&str, &str)]) {
|
||||
fn create_files_in(dir: &path::Path, paths: &[(&str, &str)]) {
|
||||
// Create the necessary files
|
||||
for (path, contents) in paths {
|
||||
// Ensure we use the right path separator for the current platform
|
||||
@ -334,6 +334,53 @@ mod scanner {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_be_possible_to_scan_in_the_parent_directory() {
|
||||
let candidates = scan_with_globs(
|
||||
&[("foo/bar/baz/foo.html", "content-['foo.html']")],
|
||||
vec!["./foo/bar/baz/.."],
|
||||
)
|
||||
.1;
|
||||
|
||||
assert_eq!(candidates, vec!["content-['foo.html']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_files_without_extensions() {
|
||||
// These look like folders, but they are files
|
||||
let candidates =
|
||||
scan_with_globs(&[("my-file", "content-['my-file']")], vec!["./my-file"]).1;
|
||||
|
||||
assert_eq!(candidates, vec!["content-['my-file']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_folders_with_extensions() {
|
||||
// These look like files, but they are folders
|
||||
let candidates = scan_with_globs(
|
||||
&[
|
||||
(
|
||||
"my-folder.templates/foo.html",
|
||||
"content-['my-folder.templates/foo.html']",
|
||||
),
|
||||
(
|
||||
"my-folder.bin/foo.html",
|
||||
"content-['my-folder.bin/foo.html']",
|
||||
),
|
||||
],
|
||||
vec!["./my-folder.templates", "./my-folder.bin"],
|
||||
)
|
||||
.1;
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['my-folder.bin/foo.html']",
|
||||
"content-['my-folder.templates/foo.html']",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_content_paths() {
|
||||
let candidates = scan_with_globs(
|
||||
@ -349,6 +396,44 @@ mod scanner {
|
||||
assert_eq!(candidates, vec!["content-['foo.styl']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_absolute_paths() {
|
||||
// Create a temporary working directory
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
|
||||
// Initialize this directory as a git repository
|
||||
let _ = Command::new("git").arg("init").current_dir(&dir).output();
|
||||
|
||||
// Create files
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
("project-a/index.html", "content-['project-a/index.html']"),
|
||||
("project-b/index.html", "content-['project-b/index.html']"),
|
||||
],
|
||||
);
|
||||
|
||||
// Get POSIX-style absolute path
|
||||
let full_path = format!("{}", dir.display()).replace('\\', "/");
|
||||
|
||||
let sources = vec![GlobEntry {
|
||||
base: full_path.clone(),
|
||||
pattern: full_path.clone(),
|
||||
}];
|
||||
|
||||
let mut scanner = Scanner::new(Some(sources));
|
||||
let candidates = scanner.scan();
|
||||
|
||||
// We've done the initial scan and found the files
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['project-a/index.html']".to_owned(),
|
||||
"content-['project-b/index.html']".to_owned(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_content_paths_even_when_they_are_git_ignored() {
|
||||
let candidates = scan_with_globs(
|
||||
|
||||
@ -431,6 +431,9 @@ test(
|
||||
|
||||
/* bar.html is git ignored, but explicitly listed here to scan */
|
||||
@source '../../project-d/src/bar.html';
|
||||
|
||||
/* Project E's source ends with '..' */
|
||||
@source '../../project-e/nested/..';
|
||||
`,
|
||||
|
||||
// Project A is the current folder, but we explicitly configured
|
||||
@ -553,6 +556,13 @@ test(
|
||||
class="content-['project-d/my-binary-file.bin']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project E's `@source "project-e/nested/.."` ends with `..`, which
|
||||
// should look for files in `project-e` itself.
|
||||
'project-e/index.html': html`<div class="content-['project-e/index.html']"></div>`,
|
||||
'project-e/nested/index.html': html`<div
|
||||
class="content-['project-e/nested/index.html']"
|
||||
></div>`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, spawn, root }) => {
|
||||
@ -599,6 +609,14 @@ test(
|
||||
--tw-content: 'project-d/src/index.html';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'project-e\\/index\\.html\\'\\] {
|
||||
--tw-content: 'project-e/index.html';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'project-e\\/nested\\/index\\.html\\'\\] {
|
||||
--tw-content: 'project-e/nested/index.html';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@supports (-moz-orient: inline) {
|
||||
@layer base {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
|
||||
@ -679,7 +679,7 @@ for (let transformer of ['postcss', 'lightningcss']) {
|
||||
},
|
||||
async ({ root, fs, exec }) => {
|
||||
await expect(() =>
|
||||
exec('pnpm vite build', { cwd: path.join(root, 'project-a') }),
|
||||
exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }),
|
||||
).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist')
|
||||
|
||||
let files = await fs.glob('project-a/dist/**/*.css')
|
||||
|
||||
@ -59,7 +59,7 @@ function extractV3Base(
|
||||
// ^^^^^^^^^ -> Base
|
||||
let rawVariants = segment(rawCandidate, ':')
|
||||
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// SAFETY: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because even if the `input` was an empty string, splitting an
|
||||
// empty string by `:` will always result in an array with at least one
|
||||
// element.
|
||||
|
||||
@ -97,7 +97,7 @@ export function compileCandidates(
|
||||
}
|
||||
|
||||
astNodes.sort((a, z) => {
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// SAFETY: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because if the ast nodes didn't exist, we introduced a bug
|
||||
// above, but there is no need to re-check just to be sure. If this relied
|
||||
// on pure user input, then we would need to check for its existence.
|
||||
@ -194,7 +194,7 @@ export function applyVariant(
|
||||
return
|
||||
}
|
||||
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// SAFETY: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because if the `candidate.root` didn't exist, `parseCandidate`
|
||||
// would have returned `null` and we would have returned early resulting in
|
||||
// not hitting this code path.
|
||||
@ -322,7 +322,7 @@ function getPropertySort(nodes: AstNode[]) {
|
||||
let q: AstNode[] = nodes.slice()
|
||||
|
||||
while (q.length > 0) {
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// SAFETY: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because we guarded against `q.length > 0` above.
|
||||
let node = q.shift()!
|
||||
if (node.kind === 'declaration') {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user