Compare commits
16 commits
chore/impr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa81d697fe | ||
|
|
9798a8ab5b | ||
|
|
722acdfebf | ||
|
|
86f3daf912 | ||
|
|
41d9cae8e5 | ||
|
|
f723e834ad | ||
|
|
9f451eead8 | ||
|
|
90f8ff41c8 | ||
|
|
7a7f386648 | ||
|
|
021b7fe6f2 | ||
|
|
f7f58f0806 | ||
|
|
de9e71c599 | ||
|
|
00ef99df3d | ||
|
|
b9286a7346 | ||
|
|
84bea42b6c | ||
|
|
606438c6c3 |
41 changed files with 5164 additions and 1250 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -23,9 +23,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383))
|
||||
- Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w<flex>`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387))
|
||||
- Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392))
|
||||
- Don't scan gitignored directories (e.g. `node_modules` and `.git`) when the project uses a safelist-style `.gitignore` (e.g. `/*` followed by `!/…` negations) ([#20397](https://github.com/tailwindlabs/tailwindcss/discussions/20397))
|
||||
- Don't scan gitignored directories (e.g. `node_modules` and `.git`) when the project uses a safelist-style `.gitignore` (e.g. `/*` followed by `!/…` negations) ([#20397](https://github.com/tailwindlabs/tailwindcss/pull/20397))
|
||||
- Ensure root `theme('…')` namespace lookups in JavaScript plugins and config files return the full namespace object instead of the value of its `DEFAULT` key ([#20399](https://github.com/tailwindlabs/tailwindcss/pull/20399))
|
||||
- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
|
||||
- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
|
||||
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
|
||||
- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))
|
||||
- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419))
|
||||
- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
|
||||
- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
|
||||
- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466))
|
||||
- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
|
||||
- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508))
|
||||
- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
|
|
|
|||
71
Cargo.lock
generated
71
Cargo.lock
generated
|
|
@ -40,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-automata 0.4.18",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
@ -59,6 +59,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
|
|
@ -126,6 +137,12 @@ version = "1.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
|
|
@ -241,14 +258,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.17"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305"
|
||||
checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
|
|
@ -273,7 +290,7 @@ dependencies = [
|
|||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-automata 0.4.18",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
|
|
@ -281,7 +298,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.24"
|
||||
version = "0.4.33"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crossbeam-channel",
|
||||
|
|
@ -290,12 +307,24 @@ dependencies = [
|
|||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-automata 0.4.18",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
|
||||
dependencies = [
|
||||
"console",
|
||||
"once_cell",
|
||||
"similar",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
|
|
@ -445,9 +474,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
|
@ -517,7 +546,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
|
|
@ -532,9 +561,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.8"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -602,6 +631,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
|
|
@ -646,7 +681,8 @@ dependencies = [
|
|||
"dunce",
|
||||
"fast-glob",
|
||||
"globwalk",
|
||||
"ignore 0.4.24",
|
||||
"ignore 0.4.33",
|
||||
"insta",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
|
|
@ -832,6 +868,15 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ignore"
|
||||
version = "0.4.24" #:version
|
||||
version = "0.4.33" #:version
|
||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||
description = """
|
||||
A fast library for efficiently matching ignore files such as `.gitignore`
|
||||
|
|
@ -12,7 +12,10 @@ repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
|
|||
readme = "README.md"
|
||||
keywords = ["glob", "ignore", "gitignore", "pattern", "file"]
|
||||
license = "Unlicense OR MIT"
|
||||
# CHANGED: Use an explicit edition instead of `edition.workspace = true` since this crate is
|
||||
# vendored into the Tailwind CSS workspace.
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
|
||||
[lib]
|
||||
name = "ignore"
|
||||
|
|
@ -20,15 +23,17 @@ bench = false
|
|||
|
||||
[dependencies]
|
||||
crossbeam-deque = "0.8.3"
|
||||
globset = "0.4.17"
|
||||
# CHANGED: Use the published globset crate instead of a path dependency.
|
||||
globset = "0.4.20"
|
||||
log = "0.4.20"
|
||||
memchr = "2.6.3"
|
||||
same-file = "1.0.6"
|
||||
walkdir = "2.4.0"
|
||||
# CHANGED: Added `dunce` to canonicalize paths without UNC prefixes on Windows.
|
||||
dunce = "1.0.5"
|
||||
|
||||
[dependencies.regex-automata]
|
||||
version = "0.4.0"
|
||||
version = "0.4.18"
|
||||
default-features = false
|
||||
features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ fn main() {
|
|||
let stdout_thread = std::thread::spawn(move || {
|
||||
let mut stdout = std::io::BufWriter::new(std::io::stdout());
|
||||
for dent in rx {
|
||||
stdout
|
||||
.write_all(&Vec::from_path_lossy(dent.path()))
|
||||
.unwrap();
|
||||
stdout.write_all(&Vec::from_path_lossy(dent.path())).unwrap();
|
||||
stdout.write_all(b"\n").unwrap();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["cml"], &["*.cml"]),
|
||||
(&["coffeescript"], &["*.coffee"]),
|
||||
(&["config"], &["*.cfg", "*.conf", "*.config", "*.ini"]),
|
||||
(&["container"], &["*Containerfile*", "*Dockerfile*"]),
|
||||
(&["coq"], &["*.v"]),
|
||||
(&["cpp"], &[
|
||||
"*.[ChH]", "*.cc", "*.[ch]pp", "*.[ch]xx", "*.hh", "*.inl",
|
||||
|
|
@ -109,6 +110,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["hbs"], &["*.hbs"]),
|
||||
(&["hs"], &["*.hs", "*.lhs"]),
|
||||
(&["html"], &["*.htm", "*.html", "*.ejs"]),
|
||||
(&["hurl"], &["*.hurl"]),
|
||||
(&["hy"], &["*.hy"]),
|
||||
(&["idris"], &["*.idr", "*.lidr"]),
|
||||
(&["janet"], &["*.janet"]),
|
||||
|
|
@ -185,6 +187,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["mint"], &["*.mint"]),
|
||||
(&["mk"], &["mkfile"]),
|
||||
(&["ml"], &["*.ml"]),
|
||||
(&["mojo"], &["*.mojo"]),
|
||||
(&["motoko"], &["*.mo"]),
|
||||
(&["msbuild"], &[
|
||||
"*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets",
|
||||
|
|
@ -206,11 +209,12 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
"*.php", "*.php3", "*.php4", "*.php5", "*.php7", "*.php8",
|
||||
"*.pht", "*.phtml"
|
||||
]),
|
||||
(&["pkgbuild"], &["PKGBUILD"]),
|
||||
(&["po"], &["*.po"]),
|
||||
(&["pod"], &["*.pod"]),
|
||||
(&["postscript"], &["*.eps", "*.ps"]),
|
||||
(&["prolog"], &["*.pl", "*.pro", "*.prolog", "*.P"]),
|
||||
(&["protobuf"], &["*.proto"]),
|
||||
(&["proto", "protobuf"], &["*.proto"]),
|
||||
(&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]),
|
||||
(&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]),
|
||||
(&["purs"], &["*.purs"]),
|
||||
|
|
@ -231,6 +235,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["red"], &["*.r", "*.red", "*.reds"]),
|
||||
(&["rescript"], &["*.res", "*.resi"]),
|
||||
(&["robot"], &["*.robot"]),
|
||||
(&["rocq"], &["*.v"]),
|
||||
(&["rst"], &["*.rst"]),
|
||||
(&["ruby"], &[
|
||||
// Idiomatic files
|
||||
|
|
@ -274,6 +279,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["spark"], &["*.spark"]),
|
||||
(&["spec"], &["*.spec"]),
|
||||
(&["sql"], &["*.sql", "*.psql"]),
|
||||
(&["ssa"], &["*.ssa"]),
|
||||
(&["stylus"], &["*.styl"]),
|
||||
(&["sv"], &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]),
|
||||
(&["svelte"], &["*.svelte", "*.svelte.ts"]),
|
||||
|
|
@ -359,4 +365,14 @@ mod tests {
|
|||
previous_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_types_aliases_are_sorted() {
|
||||
for (aliases, _) in DEFAULT_TYPES.iter() {
|
||||
assert!(
|
||||
aliases.is_sorted(),
|
||||
"this alias list is not sorted: {aliases:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -102,7 +102,9 @@ impl Gitignore {
|
|||
///
|
||||
/// Note that I/O errors are ignored. For more granular control over
|
||||
/// errors, use `GitignoreBuilder`.
|
||||
pub fn new<P: AsRef<Path>>(gitignore_path: P) -> (Gitignore, Option<Error>) {
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
gitignore_path: P,
|
||||
) -> (Gitignore, Option<Error>) {
|
||||
let path = gitignore_path.as_ref();
|
||||
let parent = path.parent().unwrap_or(Path::new("/"));
|
||||
let mut builder = GitignoreBuilder::new(parent);
|
||||
|
|
@ -123,6 +125,17 @@ impl Gitignore {
|
|||
/// The global config file path is specified by git's `core.excludesFile`
|
||||
/// config option.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// This routine does its best to discover any global git exclude files.
|
||||
/// This will try to parse out the `excludesFile` config option in your
|
||||
/// global git configuration, if necessary.
|
||||
///
|
||||
/// The specific things this routine tries (which are subject to change
|
||||
/// based on how git behaves) are:
|
||||
///
|
||||
///
|
||||
///
|
||||
/// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
|
||||
/// does not exist or does not specify `core.excludesFile`, then
|
||||
/// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
|
||||
|
|
@ -145,7 +158,8 @@ impl Gitignore {
|
|||
num_ignores: 0,
|
||||
num_whitelists: 0,
|
||||
matches: None,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +204,11 @@ impl Gitignore {
|
|||
/// determined by a common suffix of the directory containing this
|
||||
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
|
||||
/// then `path` is assumed to be relative to this matcher.
|
||||
pub fn matched<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match<&Glob> {
|
||||
pub fn matched<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -243,11 +261,16 @@ impl Gitignore {
|
|||
}
|
||||
|
||||
/// Like matched, but takes a path that has already been stripped.
|
||||
fn matched_stripped<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match<&Glob> {
|
||||
fn matched_stripped<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
// CHANGED: Rules marked as only_on_files can not match against directories.
|
||||
// CHANGED: Rules marked as only_on_files can not match against
|
||||
// directories.
|
||||
if self.only_on_files && is_dir {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -270,7 +293,10 @@ impl Gitignore {
|
|||
|
||||
/// Strips the given path such that it's suitable for matching with this
|
||||
/// gitignore matcher.
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(&'a self, path: &'a P) -> &'a Path {
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(
|
||||
&'a self,
|
||||
path: &'a P,
|
||||
) -> &'a Path {
|
||||
let mut path = path.as_ref();
|
||||
// A leading ./ is completely superfluous. We also strip it from
|
||||
// our gitignore root path, so we need to strip it from our candidate
|
||||
|
|
@ -326,7 +352,8 @@ impl GitignoreBuilder {
|
|||
globs: vec![],
|
||||
case_insensitive: false,
|
||||
allow_unclosed_class: true,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -337,18 +364,21 @@ impl GitignoreBuilder {
|
|||
pub fn build(&self) -> Result<Gitignore, Error> {
|
||||
let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
|
||||
let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
|
||||
let set = self.builder.build().map_err(|err| Error::Glob {
|
||||
glob: None,
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
let set = self
|
||||
.builder
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
Ok(Gitignore {
|
||||
set,
|
||||
root: self.root.clone(),
|
||||
globs: self.globs.clone(),
|
||||
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.
|
||||
matches: Some(Arc::new(
|
||||
Pool::with_available_parallelism_capacity(|| vec![]),
|
||||
)),
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: self.only_on_files,
|
||||
})
|
||||
}
|
||||
|
|
@ -411,11 +441,8 @@ impl GitignoreBuilder {
|
|||
|
||||
// Match Git's handling of .gitignore files that begin with the Unicode BOM
|
||||
const UTF8_BOM: &str = "\u{feff}";
|
||||
let line = if i == 0 {
|
||||
line.trim_start_matches(UTF8_BOM)
|
||||
} else {
|
||||
&line
|
||||
};
|
||||
let line =
|
||||
if i == 0 { line.trim_start_matches(UTF8_BOM) } else { &line };
|
||||
|
||||
if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
|
||||
errs.push(err.tagged(path, lineno));
|
||||
|
|
@ -537,7 +564,10 @@ impl GitignoreBuilder {
|
|||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut GitignoreBuilder, Error> {
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.case_insensitive = yes;
|
||||
|
|
@ -556,7 +586,10 @@ impl GitignoreBuilder {
|
|||
/// modes since the glob parser becomes more permissive. You might want to
|
||||
/// enable this when compatibility (e.g., with POSIX glob implementations)
|
||||
/// is more important than good error messages.
|
||||
pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut GitignoreBuilder {
|
||||
pub fn allow_unclosed_class(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> &mut GitignoreBuilder {
|
||||
self.allow_unclosed_class = yes;
|
||||
self
|
||||
}
|
||||
|
|
@ -576,32 +609,56 @@ impl GitignoreBuilder {
|
|||
///
|
||||
/// Note that the file path returned may not exist.
|
||||
pub fn gitconfig_excludes_path() -> Option<PathBuf> {
|
||||
// git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably,
|
||||
// both can be active at the same time, where $HOME/.gitconfig takes
|
||||
// precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then
|
||||
// we're done.
|
||||
match gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) {
|
||||
Some(path) => return Some(path),
|
||||
None => {}
|
||||
// When GIT_CONFIG_GLOBAL is set, it replaces both $HOME/.gitconfig and
|
||||
// $XDG_CONFIG_HOME/git/config (per git 2.32+). Otherwise, git supports
|
||||
// $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config simultaneously, where
|
||||
// $HOME/.gitconfig takes precedent.
|
||||
gitconfig_global_env_contents()
|
||||
.and_then(|x| parse_excludes_file(&x))
|
||||
.or_else(|| {
|
||||
gitconfig_home_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(|| {
|
||||
gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
// System-level config has the lowest priority for core.excludesFile.
|
||||
// GIT_CONFIG_SYSTEM overrides the default /etc/gitconfig path.
|
||||
.or_else(|| {
|
||||
gitconfig_system_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(excludes_file_default)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file from the path
|
||||
/// specified by the `GIT_CONFIG_GLOBAL` environment variable.
|
||||
fn gitconfig_global_env_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_GLOBAL").map(PathBuf::from)?;
|
||||
if path.as_os_str().is_empty() {
|
||||
return None;
|
||||
}
|
||||
match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) {
|
||||
Some(path) => return Some(path),
|
||||
None => {}
|
||||
}
|
||||
excludes_file_default()
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's system-level config file.
|
||||
///
|
||||
/// Checks `GIT_CONFIG_SYSTEM` first, then falls back to `/etc/gitconfig`.
|
||||
fn gitconfig_system_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_SYSTEM")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| PathBuf::from("/etc/gitconfig"));
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file, if one exists, in
|
||||
/// the user's home directory.
|
||||
fn gitconfig_home_contents() -> Option<Vec<u8>> {
|
||||
let home = match home_dir() {
|
||||
None => return None,
|
||||
Some(home) => home,
|
||||
};
|
||||
let mut file = match File::open(home.join(".gitconfig")) {
|
||||
Err(_) => return None,
|
||||
Ok(file) => BufReader::new(file),
|
||||
};
|
||||
let home = home_dir()?;
|
||||
let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
|
@ -610,19 +667,11 @@ fn gitconfig_home_contents() -> Option<Vec<u8>> {
|
|||
/// the user's XDG_CONFIG_HOME directory.
|
||||
fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.and_then(|x| {
|
||||
if x.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(x))
|
||||
}
|
||||
})
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/config"));
|
||||
let mut file = match path.and_then(|p| File::open(p).ok()) {
|
||||
None => return None,
|
||||
Some(file) => BufReader::new(file),
|
||||
};
|
||||
.map(|x| x.join("git/config"))?;
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
|
@ -632,13 +681,8 @@ fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
|||
/// Specifically, this respects XDG_CONFIG_HOME.
|
||||
fn excludes_file_default() -> Option<PathBuf> {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.and_then(|x| {
|
||||
if x.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(x))
|
||||
}
|
||||
})
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/ignore"))
|
||||
}
|
||||
|
|
@ -667,9 +711,7 @@ fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
|
|||
re.captures(data, &mut caps);
|
||||
let span = caps.get_group(1)?;
|
||||
let candidate = &data[span];
|
||||
std::str::from_utf8(candidate)
|
||||
.ok()
|
||||
.map(|s| PathBuf::from(expand_tilde(s)))
|
||||
std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s)))
|
||||
}
|
||||
|
||||
/// Expands ~ in file paths to the value of $HOME.
|
||||
|
|
@ -831,7 +873,10 @@ mod tests {
|
|||
fn parse_excludes_file4() {
|
||||
let data = bytes("[core]\nexcludesFile = \"~/foo/bar\"");
|
||||
let got = super::parse_excludes_file(&data);
|
||||
assert_eq!(path_string(got.unwrap()), super::expand_tilde("~/foo/bar"));
|
||||
assert_eq!(
|
||||
path_string(got.unwrap()),
|
||||
super::expand_tilde("~/foo/bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
1286
crates/ignore/src/incremental.rs
Normal file
1286
crates/ignore/src/incremental.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -48,13 +48,16 @@ See the documentation for `WalkBuilder` for many other options.
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use crate::incremental::{IncrementalIgnore, IncrementalMatch};
|
||||
pub use crate::walk::{
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder, WalkParallel, WalkState,
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
|
||||
WalkParallel, WalkState,
|
||||
};
|
||||
|
||||
mod default_types;
|
||||
mod dir;
|
||||
pub mod gitignore;
|
||||
mod incremental;
|
||||
pub mod overrides;
|
||||
mod pathutil;
|
||||
pub mod types;
|
||||
|
|
@ -120,34 +123,31 @@ impl Clone for Error {
|
|||
fn clone(&self) -> Error {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => Error::Partial(errs.clone()),
|
||||
Error::WithLineNumber { line, ref err } => Error::WithLineNumber {
|
||||
line,
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::WithPath { ref path, ref err } => Error::WithPath {
|
||||
path: path.clone(),
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::WithDepth { depth, ref err } => Error::WithDepth {
|
||||
depth,
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::Loop {
|
||||
ref ancestor,
|
||||
ref child,
|
||||
} => Error::Loop {
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
Error::WithLineNumber { line, err: err.clone() }
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
Error::WithPath { path: path.clone(), err: err.clone() }
|
||||
}
|
||||
Error::WithDepth { depth, ref err } => {
|
||||
Error::WithDepth { depth, err: err.clone() }
|
||||
}
|
||||
Error::Loop { ref ancestor, ref child } => Error::Loop {
|
||||
ancestor: ancestor.clone(),
|
||||
child: child.clone(),
|
||||
},
|
||||
Error::Io(ref err) => match err.raw_os_error() {
|
||||
Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
|
||||
None => Error::Io(std::io::Error::new(err.kind(), err.to_string())),
|
||||
None => {
|
||||
Error::Io(std::io::Error::new(err.kind(), err.to_string()))
|
||||
}
|
||||
},
|
||||
Error::Glob { ref glob, ref err } => Error::Glob {
|
||||
glob: glob.clone(),
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::UnrecognizedFileType(ref err) => Error::UnrecognizedFileType(err.clone()),
|
||||
Error::Glob { ref glob, ref err } => {
|
||||
Error::Glob { glob: glob.clone(), err: err.clone() }
|
||||
}
|
||||
Error::UnrecognizedFileType(ref err) => {
|
||||
Error::UnrecognizedFileType(err.clone())
|
||||
}
|
||||
Error::InvalidDefinition => Error::InvalidDefinition,
|
||||
}
|
||||
}
|
||||
|
|
@ -269,19 +269,14 @@ impl Error {
|
|||
|
||||
/// Turn an error into a tagged error with the given depth.
|
||||
fn with_depth(self, depth: usize) -> Error {
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(self),
|
||||
}
|
||||
Error::WithDepth { depth, err: Box::new(self) }
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path and line
|
||||
/// number. If path is empty, then it is omitted from the error.
|
||||
fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
|
||||
let errline = Error::WithLineNumber {
|
||||
line: lineno,
|
||||
err: Box::new(self),
|
||||
};
|
||||
let errline =
|
||||
Error::WithLineNumber { line: lineno, err: Box::new(self) };
|
||||
if path.as_ref().as_os_str().is_empty() {
|
||||
return errline;
|
||||
}
|
||||
|
|
@ -301,12 +296,12 @@ impl Error {
|
|||
};
|
||||
}
|
||||
let path = err.path().map(|p| p.to_path_buf());
|
||||
let mut ig_err = Error::Io(std::io::Error::from(err));
|
||||
let mut ig_err = Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Io(std::io::Error::from(err))),
|
||||
};
|
||||
if let Some(path) = path {
|
||||
ig_err = Error::WithPath {
|
||||
path,
|
||||
err: Box::new(ig_err),
|
||||
};
|
||||
ig_err = Error::WithPath { path, err: Box::new(ig_err) };
|
||||
}
|
||||
ig_err
|
||||
}
|
||||
|
|
@ -333,7 +328,8 @@ impl std::fmt::Display for Error {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
let msgs: Vec<String> = errs.iter().map(|err| err.to_string()).collect();
|
||||
let msgs: Vec<String> =
|
||||
errs.iter().map(|err| err.to_string()).collect();
|
||||
write!(f, "{}", msgs.join("\n"))
|
||||
}
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
|
|
@ -343,10 +339,7 @@ impl std::fmt::Display for Error {
|
|||
write!(f, "{}: {}", path.display(), err)
|
||||
}
|
||||
Error::WithDepth { ref err, .. } => err.fmt(f),
|
||||
Error::Loop {
|
||||
ref ancestor,
|
||||
ref child,
|
||||
} => write!(
|
||||
Error::Loop { ref ancestor, ref child } => write!(
|
||||
f,
|
||||
"File system loop found: \
|
||||
{} points to an ancestor {}",
|
||||
|
|
@ -354,14 +347,8 @@ impl std::fmt::Display for Error {
|
|||
ancestor.display()
|
||||
),
|
||||
Error::Io(ref err) => err.fmt(f),
|
||||
Error::Glob {
|
||||
glob: None,
|
||||
ref err,
|
||||
} => write!(f, "{}", err),
|
||||
Error::Glob {
|
||||
glob: Some(ref glob),
|
||||
ref err,
|
||||
} => {
|
||||
Error::Glob { glob: None, ref err } => write!(f, "{}", err),
|
||||
Error::Glob { glob: Some(ref glob), ref err } => {
|
||||
write!(f, "error parsing glob '{}': {}", glob, err)
|
||||
}
|
||||
Error::UnrecognizedFileType(ref ty) => {
|
||||
|
|
@ -507,7 +494,8 @@ mod tests {
|
|||
};
|
||||
|
||||
/// A convenient result type alias.
|
||||
pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
pub(crate) type Result<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {
|
||||
|
|
@ -545,8 +533,9 @@ mod tests {
|
|||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&path)
|
||||
.map_err(|e| err!("failed to create {}: {}", path.display(), e))?;
|
||||
fs::create_dir_all(&path).map_err(|e| {
|
||||
err!("failed to create {}: {}", path.display(), e)
|
||||
})?;
|
||||
return Ok(TempDir(path));
|
||||
}
|
||||
Err(err!("failed to create temp dir after {} tries", TRIES))
|
||||
|
|
|
|||
|
|
@ -94,7 +94,11 @@ impl Override {
|
|||
/// given) is stripped. If there is no common suffix/prefix overlap, then
|
||||
/// `path` is assumed to reside in the same directory as the root path for
|
||||
/// this set of overrides.
|
||||
pub fn matched<'a, P: AsRef<Path>>(&'a self, path: P, is_dir: bool) -> Match<Glob<'a>> {
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -146,7 +150,10 @@ impl OverrideBuilder {
|
|||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut OverrideBuilder, Error> {
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut OverrideBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.builder.case_insensitive(yes)?;
|
||||
|
|
@ -276,11 +283,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn default_case_sensitive() {
|
||||
let ov = OverrideBuilder::new(ROOT)
|
||||
.add("*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let ov =
|
||||
OverrideBuilder::new(ROOT).add("*.html").unwrap().build().unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_ignore());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
|
|
|
|||
|
|
@ -2,55 +2,89 @@ use std::{ffi::OsStr, path::Path};
|
|||
|
||||
use crate::walk::DirEntry;
|
||||
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
/// Returns true if and only if this path is considered to be hidden.
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// On Unix, this implements a more optimized check.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.as_bytes().get(0) == Some(&b'.')
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
/// ## Windows
|
||||
///
|
||||
/// On Windows, this returns true if one of the following is true:
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)
|
||||
pub(crate) fn is_hidden_path(dent: &Path) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(path: &Path) -> bool {
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(path: &Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
if let Ok(md) = path.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this directory entry is considered to be
|
||||
/// hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_entry(dent: &DirEntry) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden from only
|
||||
/// the path itself.
|
||||
///
|
||||
/// This has the same behavior on all platforms.
|
||||
fn is_hidden_path_only(path: &Path) -> bool {
|
||||
if let Some(name) = file_name(path) {
|
||||
name.as_encoded_bytes().starts_with(b".")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
@ -59,83 +93,79 @@ pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
|||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
#[cfg(unix)]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let prefix = prefix.as_ref().as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
let prefix = prefix.as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
#[cfg(not(unix))]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
imp(prefix.as_ref(), path)
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memchr;
|
||||
|
||||
let path = path.as_ref().as_os_str().as_bytes();
|
||||
memchr(b'/', path).is_none()
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||
/// file_name will return None.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Option<&'a OsStr> {
|
||||
use memchr::memrchr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let path = path.as_ref().as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
memchr::memchr(b'/', path.as_ref().as_os_str().as_encoded_bytes())
|
||||
.is_none()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||
/// file_name will return None.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Option<&'a OsStr> {
|
||||
path.as_ref().file_name()
|
||||
/// If the path terminates in `.`, `..`, or consists solely of a root of
|
||||
/// prefix, this will return `None`.
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||
path: &'a P,
|
||||
) -> Option<&'a OsStr> {
|
||||
#[cfg(unix)]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memrchr;
|
||||
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
path.file_name()
|
||||
}
|
||||
|
||||
imp(path.as_ref())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,8 +204,12 @@ impl<T> Selection<T> {
|
|||
|
||||
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Selection<U> {
|
||||
match self {
|
||||
Selection::Select(name, inner) => Selection::Select(name, f(inner)),
|
||||
Selection::Negate(name, inner) => Selection::Negate(name, f(inner)),
|
||||
Selection::Select(name, inner) => {
|
||||
Selection::Select(name, f(inner))
|
||||
}
|
||||
Selection::Negate(name, inner) => {
|
||||
Selection::Negate(name, f(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +231,9 @@ impl Types {
|
|||
has_selected: false,
|
||||
glob_to_selection: vec![],
|
||||
set: GlobSetBuilder::new().build().unwrap(),
|
||||
matches: Arc::new(Pool::new(|| vec![])),
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +260,11 @@ impl Types {
|
|||
/// The path is considered ignored if it matches a negated file type.
|
||||
/// If at least one file type is selected and `path` doesn't match, then
|
||||
/// the path is also considered ignored.
|
||||
pub fn matched<'a, P: AsRef<Path>>(&'a self, path: P, is_dir: bool) -> Match<Glob<'a>> {
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
// File types don't apply to directories, and we can't do anything
|
||||
// if our glob set is empty.
|
||||
if is_dir || self.set.is_empty() {
|
||||
|
|
@ -306,10 +316,7 @@ impl TypesBuilder {
|
|||
/// of default type definitions can be added with `add_defaults`, and
|
||||
/// additional type definitions can be added with `select` and `negate`.
|
||||
pub fn new() -> TypesBuilder {
|
||||
TypesBuilder {
|
||||
types: HashMap::new(),
|
||||
selections: vec![],
|
||||
}
|
||||
TypesBuilder { types: HashMap::new(), selections: vec![] }
|
||||
}
|
||||
|
||||
/// Build the current set of file type definitions *and* selections into
|
||||
|
|
@ -343,17 +350,18 @@ impl TypesBuilder {
|
|||
}
|
||||
selections.push(selection.clone().map(move |_| def));
|
||||
}
|
||||
let set = build_set.build().map_err(|err| Error::Glob {
|
||||
glob: None,
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
let set = build_set
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
Ok(Types {
|
||||
defs,
|
||||
selections,
|
||||
has_selected,
|
||||
glob_to_selection,
|
||||
set,
|
||||
matches: Arc::new(Pool::new(|| vec![])),
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -377,12 +385,10 @@ impl TypesBuilder {
|
|||
pub fn select(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections
|
||||
.push(Selection::Select(name.to_string(), ()));
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections
|
||||
.push(Selection::Select(name.to_string(), ()));
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -393,12 +399,10 @@ impl TypesBuilder {
|
|||
pub fn negate(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections
|
||||
.push(Selection::Negate(name.to_string(), ()));
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections
|
||||
.push(Selection::Negate(name.to_string(), ()));
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -453,7 +457,10 @@ impl TypesBuilder {
|
|||
3 => {
|
||||
let name = parts[0];
|
||||
let types_string = parts[2];
|
||||
if name.is_empty() || parts[1] != "include" || types_string.is_empty() {
|
||||
if name.is_empty()
|
||||
|| parts[1] != "include"
|
||||
|| types_string.is_empty()
|
||||
{
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let types = types_string.split(',');
|
||||
|
|
@ -463,7 +470,8 @@ impl TypesBuilder {
|
|||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
for type_name in types {
|
||||
let globs = self.types.get(type_name).unwrap().globs.clone();
|
||||
let globs =
|
||||
self.types.get(type_name).unwrap().globs.clone();
|
||||
for glob in globs {
|
||||
self.add(name, &glob)?;
|
||||
}
|
||||
|
|
@ -549,30 +557,9 @@ mod tests {
|
|||
|
||||
matched!(not, matchnot1, types(), vec!["rust"], vec![], "index.html");
|
||||
matched!(not, matchnot2, types(), vec![], vec!["rust"], "main.rs");
|
||||
matched!(
|
||||
not,
|
||||
matchnot3,
|
||||
types(),
|
||||
vec!["foo"],
|
||||
vec!["rust"],
|
||||
"main.rs"
|
||||
);
|
||||
matched!(
|
||||
not,
|
||||
matchnot4,
|
||||
types(),
|
||||
vec!["rust"],
|
||||
vec!["foo"],
|
||||
"main.rs"
|
||||
);
|
||||
matched!(
|
||||
not,
|
||||
matchnot5,
|
||||
types(),
|
||||
vec!["rust"],
|
||||
vec!["foo"],
|
||||
"main.foo"
|
||||
);
|
||||
matched!(not, matchnot3, types(), vec!["foo"], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot4, types(), vec!["rust"], vec!["foo"], "main.rs");
|
||||
matched!(not, matchnot5, types(), vec!["rust"], vec!["foo"], "main.foo");
|
||||
matched!(not, matchnot6, types(), vec!["combo"], vec![], "leftpad.js");
|
||||
matched!(not, matchnot7, types(), vec!["py"], vec![], "index.html");
|
||||
matched!(not, matchnot8, types(), vec!["python"], vec![], "doc.md");
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ use {
|
|||
use crate::{
|
||||
Error, PartialErrorBuilder,
|
||||
dir::{Ignore, IgnoreBuilder},
|
||||
// CHANGED: Also import `Gitignore` for `WalkBuilder::add_gitignore`.
|
||||
gitignore::{Gitignore, GitignoreBuilder},
|
||||
incremental::{IncrementalIgnore, IncrementalIgnoreOptions},
|
||||
overrides::Override,
|
||||
types::Types,
|
||||
};
|
||||
|
|
@ -104,24 +106,15 @@ impl DirEntry {
|
|||
}
|
||||
|
||||
fn new_stdin() -> DirEntry {
|
||||
DirEntry {
|
||||
dent: DirEntryInner::Stdin,
|
||||
err: None,
|
||||
}
|
||||
DirEntry { dent: DirEntryInner::Stdin, err: None }
|
||||
}
|
||||
|
||||
fn new_walkdir(dent: walkdir::DirEntry, err: Option<Error>) -> DirEntry {
|
||||
DirEntry {
|
||||
dent: DirEntryInner::Walkdir(dent),
|
||||
err,
|
||||
}
|
||||
DirEntry { dent: DirEntryInner::Walkdir(dent), err }
|
||||
}
|
||||
|
||||
fn new_raw(dent: DirEntryRaw, err: Option<Error>) -> DirEntry {
|
||||
DirEntry {
|
||||
dent: DirEntryInner::Raw(dent),
|
||||
err,
|
||||
}
|
||||
DirEntry { dent: DirEntryInner::Raw(dent), err }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,9 +180,11 @@ impl DirEntryInner {
|
|||
));
|
||||
Err(err.with_path("<stdin>"))
|
||||
}
|
||||
Walkdir(ref x) => x
|
||||
.metadata()
|
||||
.map_err(|err| Error::Io(io::Error::from(err)).with_path(x.path())),
|
||||
Walkdir(ref x) => x.metadata().map_err(|err| {
|
||||
Error::Io(io::Error::from(err))
|
||||
.with_depth(x.depth())
|
||||
.with_path(x.path())
|
||||
}),
|
||||
Raw(ref x) => x.metadata(),
|
||||
}
|
||||
}
|
||||
|
|
@ -308,7 +303,9 @@ impl DirEntryRaw {
|
|||
} else {
|
||||
fs::symlink_metadata(&self.path)
|
||||
}
|
||||
.map_err(|err| Error::Io(io::Error::from(err)).with_path(&self.path))
|
||||
.map_err(|err| {
|
||||
Error::Io(err).with_depth(self.depth).with_path(&self.path)
|
||||
})
|
||||
}
|
||||
|
||||
fn file_type(&self) -> FileType {
|
||||
|
|
@ -316,9 +313,7 @@ impl DirEntryRaw {
|
|||
}
|
||||
|
||||
fn file_name(&self) -> &OsStr {
|
||||
self.path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| self.path.as_os_str())
|
||||
self.path.file_name().unwrap_or_else(|| self.path.as_os_str())
|
||||
}
|
||||
|
||||
fn depth(&self) -> usize {
|
||||
|
|
@ -330,13 +325,13 @@ impl DirEntryRaw {
|
|||
self.ino
|
||||
}
|
||||
|
||||
fn from_entry(depth: usize, ent: &fs::DirEntry) -> Result<DirEntryRaw, Error> {
|
||||
fn from_entry(
|
||||
depth: usize,
|
||||
ent: &fs::DirEntry,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
let ty = ent.file_type().map_err(|err| {
|
||||
let err = Error::Io(io::Error::from(err)).with_path(ent.path());
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(err),
|
||||
}
|
||||
let err = Error::Io(err).with_depth(depth).with_path(ent.path());
|
||||
Error::WithDepth { depth, err: Box::new(err) }
|
||||
})?;
|
||||
DirEntryRaw::from_entry_os(depth, ent, ty)
|
||||
}
|
||||
|
|
@ -348,11 +343,8 @@ impl DirEntryRaw {
|
|||
ty: fs::FileType,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
let md = ent.metadata().map_err(|err| {
|
||||
let err = Error::Io(io::Error::from(err)).with_path(ent.path());
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(err),
|
||||
}
|
||||
let err = Error::Io(err).with_depth(depth).with_path(ent.path());
|
||||
Error::WithDepth { depth, err: Box::new(err) }
|
||||
})?;
|
||||
Ok(DirEntryRaw {
|
||||
path: ent.path(),
|
||||
|
|
@ -395,8 +387,13 @@ impl DirEntryRaw {
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntryRaw, Error> {
|
||||
let md = fs::metadata(&pb).map_err(|err| Error::Io(err).with_path(&pb))?;
|
||||
fn from_path(
|
||||
depth: usize,
|
||||
pb: PathBuf,
|
||||
link: bool,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
let md = fs::metadata(&pb)
|
||||
.map_err(|err| Error::Io(err).with_depth(depth).with_path(&pb))?;
|
||||
Ok(DirEntryRaw {
|
||||
path: pb,
|
||||
ty: md.file_type(),
|
||||
|
|
@ -407,10 +404,15 @@ impl DirEntryRaw {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntryRaw, Error> {
|
||||
fn from_path(
|
||||
depth: usize,
|
||||
pb: PathBuf,
|
||||
link: bool,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let md = fs::metadata(&pb).map_err(|err| Error::Io(err).with_path(&pb))?;
|
||||
let md = fs::metadata(&pb)
|
||||
.map_err(|err| Error::Io(err).with_depth(depth).with_path(&pb))?;
|
||||
Ok(DirEntryRaw {
|
||||
path: pb,
|
||||
ty: md.file_type(),
|
||||
|
|
@ -423,7 +425,11 @@ impl DirEntryRaw {
|
|||
// Placeholder implementation to allow compiling on non-standard platforms
|
||||
// (e.g. wasm32).
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntryRaw, Error> {
|
||||
fn from_path(
|
||||
depth: usize,
|
||||
pb: PathBuf,
|
||||
link: bool,
|
||||
) -> Result<DirEntryRaw, Error> {
|
||||
Err(Error::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unsupported platform",
|
||||
|
|
@ -502,7 +508,8 @@ pub struct WalkBuilder {
|
|||
///
|
||||
/// When `None`, the CWD is fetched from `std::env::current_dir()`. If
|
||||
/// that fails, then global gitignores are ignored (an error is logged).
|
||||
global_gitignores_relative_to: OnceLock<Result<PathBuf, Arc<std::io::Error>>>,
|
||||
global_gitignores_relative_to:
|
||||
OnceLock<Result<PathBuf, Arc<std::io::Error>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -544,8 +551,16 @@ impl WalkBuilder {
|
|||
/// is better to call `add` on this builder than to create multiple
|
||||
/// `Walk` values.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> WalkBuilder {
|
||||
WalkBuilder::from_iter([path])
|
||||
}
|
||||
|
||||
/// Create an empty builder to which paths can be added.
|
||||
///
|
||||
/// Note that if you call `build` on this instance before calling `add`
|
||||
/// on it, it will return exactly zero items during iteration.
|
||||
pub fn empty() -> WalkBuilder {
|
||||
WalkBuilder {
|
||||
paths: vec![path.as_ref().to_path_buf()],
|
||||
paths: vec![],
|
||||
ig_builder: IgnoreBuilder::new(),
|
||||
max_depth: None,
|
||||
min_depth: None,
|
||||
|
|
@ -560,6 +575,21 @@ impl WalkBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new builder for a recursive directory iterator from the
|
||||
/// sequence of paths.
|
||||
///
|
||||
/// Note that if the iterator is empty, this is the same as
|
||||
/// `WalkBuilder::empty`.
|
||||
pub fn from_iter<P: AsRef<Path>, I: IntoIterator<Item = P>>(
|
||||
paths: I,
|
||||
) -> WalkBuilder {
|
||||
let mut builder = WalkBuilder::empty();
|
||||
for path in paths.into_iter() {
|
||||
builder.add(path);
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
/// Build a new `Walk` iterator.
|
||||
pub fn build(&self) -> Walk {
|
||||
let follow_links = self.follow_links;
|
||||
|
|
@ -585,10 +615,14 @@ impl WalkBuilder {
|
|||
if let Some(ref sorter) = sorter {
|
||||
match sorter.clone() {
|
||||
Sorter::ByName(cmp) => {
|
||||
wd = wd.sort_by(move |a, b| cmp(a.file_name(), b.file_name()));
|
||||
wd = wd.sort_by(move |a, b| {
|
||||
cmp(a.file_name(), b.file_name())
|
||||
});
|
||||
}
|
||||
Sorter::ByPath(cmp) => {
|
||||
wd = wd.sort_by(move |a, b| cmp(a.path(), b.path()));
|
||||
wd = wd.sort_by(move |a, b| {
|
||||
cmp(a.path(), b.path())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -597,31 +631,76 @@ impl WalkBuilder {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter();
|
||||
let ig_root = self
|
||||
.get_or_set_current_dir()
|
||||
.map(|cwd| self.ig_builder.build_with_cwd(Some(cwd.to_path_buf())))
|
||||
.unwrap_or_else(|| self.ig_builder.build());
|
||||
let ig_root = self.build_ignore();
|
||||
Walk {
|
||||
its,
|
||||
it: None,
|
||||
ig_root: ig_root.clone(),
|
||||
ig: ig_root.clone(),
|
||||
max_depth: self.max_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
skip: self.skip.clone(),
|
||||
filter: self.filter.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build matchers for checking paths against ignore files without
|
||||
/// recursively walking the configured roots.
|
||||
///
|
||||
/// The returned matchers use the path-based filtering configuration
|
||||
/// on this builder, including glob overrides, file type selections,
|
||||
/// parent ignore files, `.ignore`, `.gitignore`, global Git
|
||||
/// ignore files, explicitly added ignore files and custom ignore
|
||||
/// file names. For example, ripgrep configures `.rgignore` via
|
||||
/// [`WalkBuilder::add_custom_ignore_filename`]. Minimum and maximum depth
|
||||
/// limits, maximum file size and hidden-file filtering are also applied.
|
||||
/// Other options that only control traversal or require a directory entry,
|
||||
/// such as custom entry predicates, are not applied.
|
||||
///
|
||||
/// One matcher is returned for each configured path, in the same order as
|
||||
/// the paths were added to this builder. Each matcher accepts paths
|
||||
/// relative to its own [`IncrementalIgnore::root`]. The matcher for the
|
||||
/// special `-` path representing standard input always returns a non-match
|
||||
/// for all inputs.
|
||||
///
|
||||
/// Ignore matchers are loaded lazily and cached by directory.
|
||||
/// Thus, the first query may read ignore files from the root and
|
||||
/// its parents, while later queries reuse the compiled matchers.
|
||||
/// Errors encountered while loading ignore files are returned by
|
||||
/// [`IncrementalIgnore::matched_with_errors`]. Once an ignore file has
|
||||
/// been loaded, changes to it are not observed. Build new matchers to
|
||||
/// reload changed ignore files.
|
||||
///
|
||||
/// Matchers built together share the builder's base ignore configuration
|
||||
/// and compiled parent matchers.
|
||||
pub fn build_matchers(&self) -> Vec<IncrementalIgnore> {
|
||||
let ignore = self.build_ignore();
|
||||
let options = IncrementalIgnoreOptions {
|
||||
min_depth: self.min_depth,
|
||||
max_depth: self.max_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
hidden: self.ig_builder.is_hidden(),
|
||||
follow_links: self.follow_links,
|
||||
};
|
||||
self.paths
|
||||
.iter()
|
||||
.map(move |path| {
|
||||
IncrementalIgnore::new(
|
||||
path.clone(),
|
||||
ignore.clone(),
|
||||
options.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build a new `WalkParallel` iterator.
|
||||
///
|
||||
/// Note that this *doesn't* return something that implements `Iterator`.
|
||||
/// Instead, the returned value must be run with a closure. e.g.,
|
||||
/// `builder.build_parallel().run(|| |path| { println!("{path:?}"); WalkState::Continue })`.
|
||||
pub fn build_parallel(&self) -> WalkParallel {
|
||||
let ig_root = self
|
||||
.get_or_set_current_dir()
|
||||
.map(|cwd| self.ig_builder.build_with_cwd(Some(cwd.to_path_buf())))
|
||||
.unwrap_or_else(|| self.ig_builder.build());
|
||||
let ig_root = self.build_ignore();
|
||||
WalkParallel {
|
||||
paths: self.paths.clone().into_iter(),
|
||||
ig_root,
|
||||
|
|
@ -651,7 +730,10 @@ impl WalkBuilder {
|
|||
/// The default, `None`, imposes no depth restriction.
|
||||
pub fn max_depth(&mut self, depth: Option<usize>) -> &mut WalkBuilder {
|
||||
self.max_depth = depth;
|
||||
if self.min_depth.is_some() && self.max_depth.is_some() && self.max_depth < self.min_depth {
|
||||
if self.min_depth.is_some()
|
||||
&& self.max_depth.is_some()
|
||||
&& self.max_depth < self.min_depth
|
||||
{
|
||||
self.max_depth = self.min_depth;
|
||||
}
|
||||
self
|
||||
|
|
@ -662,7 +744,10 @@ impl WalkBuilder {
|
|||
/// The default, `None`, imposes no minimum depth restriction.
|
||||
pub fn min_depth(&mut self, depth: Option<usize>) -> &mut WalkBuilder {
|
||||
self.min_depth = depth;
|
||||
if self.max_depth.is_some() && self.min_depth.is_some() && self.min_depth > self.max_depth {
|
||||
if self.max_depth.is_some()
|
||||
&& self.min_depth.is_some()
|
||||
&& self.min_depth > self.max_depth
|
||||
{
|
||||
self.min_depth = self.max_depth;
|
||||
}
|
||||
self
|
||||
|
|
@ -705,7 +790,12 @@ impl WalkBuilder {
|
|||
/// An error will also occur if this walker could not get the current
|
||||
/// working directory (and `WalkBuilder::current_dir` isn't set).
|
||||
pub fn add_ignore<P: AsRef<Path>>(&mut self, path: P) -> Option<Error> {
|
||||
// CHANGED: Dropped this code
|
||||
// CHANGED: Root the ignore file at `""` instead of the current working
|
||||
// directory. Explicit ignores are scoped to the directory of the
|
||||
// ignore file (see `matched_ignore`), and a root of `""` makes the
|
||||
// rules apply to every walked path regardless of the walk root. This
|
||||
// also avoids depending on the current working directory entirely.
|
||||
//
|
||||
// let path = path.as_ref();
|
||||
// let Some(cwd) = self.get_or_set_current_dir() else {
|
||||
// let err = std::io::Error::other(format!(
|
||||
|
|
@ -729,7 +819,11 @@ impl WalkBuilder {
|
|||
errs.into_error_option()
|
||||
}
|
||||
|
||||
/// CHANGED: Add a Gitignore to the builder.
|
||||
/// CHANGED: Add a prebuilt Gitignore to the builder.
|
||||
///
|
||||
/// Like the ignore file added via `add_ignore`, these rules are matched
|
||||
/// against the full path of each walked entry, scoped to the `Gitignore`'s
|
||||
/// root path.
|
||||
pub fn add_gitignore(&mut self, gi: Gitignore) {
|
||||
self.ig_builder.add_ignore(gi);
|
||||
}
|
||||
|
|
@ -982,7 +1076,10 @@ impl WalkBuilder {
|
|||
///
|
||||
/// Global gitignore files come from things like a user's git configuration
|
||||
/// or from gitignore files added via [`WalkBuilder::add_ignore`].
|
||||
pub fn current_dir(&mut self, cwd: impl Into<PathBuf>) -> &mut WalkBuilder {
|
||||
pub fn current_dir(
|
||||
&mut self,
|
||||
cwd: impl Into<PathBuf>,
|
||||
) -> &mut WalkBuilder {
|
||||
let cwd = cwd.into();
|
||||
self.ig_builder.current_dir(cwd.clone());
|
||||
if let Err(cwd) = self.global_gitignores_relative_to.set(Ok(cwd)) {
|
||||
|
|
@ -1002,7 +1099,10 @@ impl WalkBuilder {
|
|||
let result = std::env::current_dir().map_err(Arc::new);
|
||||
match result {
|
||||
Ok(ref path) => {
|
||||
log::trace!("automatically discovered CWD: {}", path.display());
|
||||
log::trace!(
|
||||
"automatically discovered CWD: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Err(ref err) => {
|
||||
log::debug!(
|
||||
|
|
@ -1016,6 +1116,13 @@ impl WalkBuilder {
|
|||
});
|
||||
result.as_ref().ok().map(|path| &**path)
|
||||
}
|
||||
|
||||
/// Build the root ignore matcher shared by all consumers of this builder.
|
||||
fn build_ignore(&self) -> Ignore {
|
||||
self.get_or_set_current_dir()
|
||||
.map(|cwd| self.ig_builder.build_with_cwd(Some(cwd.to_path_buf())))
|
||||
.unwrap_or_else(|| self.ig_builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk is a recursive directory iterator over file paths in one or more
|
||||
|
|
@ -1029,6 +1136,7 @@ pub struct Walk {
|
|||
it: Option<WalkEventIter>,
|
||||
ig_root: Ignore,
|
||||
ig: Ignore,
|
||||
max_depth: Option<usize>,
|
||||
max_filesize: Option<u64>,
|
||||
skip: Option<Arc<Handle>>,
|
||||
filter: Option<Filter>,
|
||||
|
|
@ -1044,6 +1152,17 @@ impl Walk {
|
|||
WalkBuilder::new(path).build()
|
||||
}
|
||||
|
||||
/// Create a new recursive directory iterator from the sequence of paths
|
||||
/// given.
|
||||
///
|
||||
/// Note that if the provided iterator is empty, then `Walk` is guaranteed
|
||||
/// to yield zero entries.
|
||||
pub fn from_iter<P: AsRef<Path>, I: IntoIterator<Item = P>>(
|
||||
paths: I,
|
||||
) -> Walk {
|
||||
WalkBuilder::from_iter(paths).build()
|
||||
}
|
||||
|
||||
fn skip_entry(&self, ent: &DirEntry) -> Result<bool, Error> {
|
||||
if ent.depth() == 0 {
|
||||
return Ok(false);
|
||||
|
|
@ -1128,12 +1247,17 @@ impl Iterator for Walk {
|
|||
self.it.as_mut().unwrap().it.skip_current_dir();
|
||||
// Still need to push this on the stack because
|
||||
// we'll get a WalkEvent::Exit event for this dir.
|
||||
// We don't care if it errors though.
|
||||
let (igtmp, _) = self.ig.add_child(ent.path());
|
||||
// Its ignore files cannot apply to any visited entry.
|
||||
let (igtmp, _) =
|
||||
self.ig.add_child_with_entries(ent.path(), &[]);
|
||||
self.ig = igtmp;
|
||||
continue;
|
||||
}
|
||||
let (igtmp, err) = self.ig.add_child(ent.path());
|
||||
let (igtmp, err) = if self.max_depth == Some(ent.depth()) {
|
||||
self.ig.add_child_with_entries(ent.path(), &[])
|
||||
} else {
|
||||
self.ig.add_child(ent.path())
|
||||
};
|
||||
self.ig = igtmp;
|
||||
ent.err = err;
|
||||
return Some(Ok(ent));
|
||||
|
|
@ -1175,11 +1299,7 @@ enum WalkEvent {
|
|||
|
||||
impl From<WalkDir> for WalkEventIter {
|
||||
fn from(it: WalkDir) -> WalkEventIter {
|
||||
WalkEventIter {
|
||||
depth: 0,
|
||||
it: it.into_iter(),
|
||||
next: None,
|
||||
}
|
||||
WalkEventIter { depth: 0, it: it.into_iter(), next: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1252,7 +1372,9 @@ pub trait ParallelVisitorBuilder<'s> {
|
|||
fn build(&mut self) -> Box<dyn ParallelVisitor + 's>;
|
||||
}
|
||||
|
||||
impl<'a, 's, P: ParallelVisitorBuilder<'s>> ParallelVisitorBuilder<'s> for &'a mut P {
|
||||
impl<'a, 's, P: ParallelVisitorBuilder<'s>> ParallelVisitorBuilder<'s>
|
||||
for &'a mut P
|
||||
{
|
||||
fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
|
||||
(**self).build()
|
||||
}
|
||||
|
|
@ -1273,14 +1395,17 @@ struct FnBuilder<F> {
|
|||
builder: F,
|
||||
}
|
||||
|
||||
impl<'s, F: FnMut() -> FnVisitor<'s>> ParallelVisitorBuilder<'s> for FnBuilder<F> {
|
||||
impl<'s, F: FnMut() -> FnVisitor<'s>> ParallelVisitorBuilder<'s>
|
||||
for FnBuilder<F>
|
||||
{
|
||||
fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
|
||||
let visitor = (self.builder)();
|
||||
Box::new(FnVisitorImp { visitor })
|
||||
}
|
||||
}
|
||||
|
||||
type FnVisitor<'s> = Box<dyn FnMut(Result<DirEntry, Error>) -> WalkState + Send + 's>;
|
||||
type FnVisitor<'s> =
|
||||
Box<dyn FnMut(Result<DirEntry, Error>) -> WalkState + Send + 's>;
|
||||
|
||||
struct FnVisitorImp<'s> {
|
||||
visitor: FnVisitor<'s>,
|
||||
|
|
@ -1370,7 +1495,9 @@ impl WalkParallel {
|
|||
}
|
||||
};
|
||||
match DirEntryRaw::from_path(0, path, false) {
|
||||
Ok(dent) => (DirEntry::new_raw(dent, None), root_device),
|
||||
Ok(dent) => {
|
||||
(DirEntry::new_raw(dent, None), root_device)
|
||||
}
|
||||
Err(err) => {
|
||||
if visitor.visit(Err(err)).is_quit() {
|
||||
return;
|
||||
|
|
@ -1394,21 +1521,28 @@ impl WalkParallel {
|
|||
let quit_now = Arc::new(AtomicBool::new(false));
|
||||
let active_workers = Arc::new(AtomicUsize::new(threads));
|
||||
let stacks = Stack::new_for_each_thread(threads, stack);
|
||||
// Collect all of the workers first. In the case that
|
||||
// `builder.build()` panics, we want that to happen and
|
||||
// propagate before we actually start to run any of the
|
||||
// workers.
|
||||
let workers: Vec<_> = stacks
|
||||
.into_iter()
|
||||
.map(|stack| Worker {
|
||||
visitor: builder.build(),
|
||||
stack,
|
||||
quit_now: quit_now.clone(),
|
||||
active_workers: active_workers.clone(),
|
||||
max_depth: self.max_depth,
|
||||
min_depth: self.min_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
follow_links: self.follow_links,
|
||||
skip: self.skip.clone(),
|
||||
filter: self.filter.clone(),
|
||||
})
|
||||
.collect();
|
||||
std::thread::scope(|s| {
|
||||
let handles: Vec<_> = stacks
|
||||
let handles: Vec<_> = workers
|
||||
.into_iter()
|
||||
.map(|stack| Worker {
|
||||
visitor: builder.build(),
|
||||
stack,
|
||||
quit_now: quit_now.clone(),
|
||||
active_workers: active_workers.clone(),
|
||||
max_depth: self.max_depth,
|
||||
min_depth: self.min_depth,
|
||||
max_filesize: self.max_filesize,
|
||||
follow_links: self.follow_links,
|
||||
skip: self.skip.clone(),
|
||||
filter: self.filter.clone(),
|
||||
})
|
||||
.map(|worker| s.spawn(|| worker.run()))
|
||||
.collect();
|
||||
for handle in handles {
|
||||
|
|
@ -1419,9 +1553,7 @@ impl WalkParallel {
|
|||
|
||||
fn threads(&self) -> usize {
|
||||
if self.threads == 0 {
|
||||
std::thread::available_parallelism()
|
||||
.map_or(1, |n| n.get())
|
||||
.min(12)
|
||||
std::thread::available_parallelism().map_or(1, |n| n.get()).min(12)
|
||||
} else {
|
||||
self.threads
|
||||
}
|
||||
|
|
@ -1452,6 +1584,12 @@ struct Work {
|
|||
root_device: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReadDirResult {
|
||||
entries: Vec<fs::DirEntry>,
|
||||
errors: Vec<Error>,
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// Returns true if and only if this work item is a directory.
|
||||
fn is_dir(&self) -> bool {
|
||||
|
|
@ -1478,6 +1616,13 @@ impl Work {
|
|||
err
|
||||
}
|
||||
|
||||
/// Adds ignore rules for this directory without reading its contents.
|
||||
fn add_ignore(&mut self) {
|
||||
let (ig, err) = self.ignore.add_child(self.dent.path());
|
||||
self.ignore = ig;
|
||||
self.dent.err = err;
|
||||
}
|
||||
|
||||
/// Reads the directory contents of this work item and adds ignore
|
||||
/// rules for this directory.
|
||||
///
|
||||
|
|
@ -1485,7 +1630,7 @@ impl Work {
|
|||
/// an error is returned. If there was a problem reading the ignore
|
||||
/// rules for this directory, then the error is attached to this
|
||||
/// work item's directory entry.
|
||||
fn read_dir(&mut self) -> Result<fs::ReadDir, Error> {
|
||||
fn read_dir(&mut self) -> Result<ReadDirResult, Error> {
|
||||
let readdir = match fs::read_dir(self.dent.path()) {
|
||||
Ok(readdir) => readdir,
|
||||
Err(err) => {
|
||||
|
|
@ -1495,10 +1640,24 @@ impl Work {
|
|||
return Err(err);
|
||||
}
|
||||
};
|
||||
let (ig, err) = self.ignore.add_child(self.dent.path());
|
||||
// Actually descend into the directory and read its contents
|
||||
let mut result = ReadDirResult::default();
|
||||
for entry in readdir {
|
||||
match entry {
|
||||
Ok(entry) => result.entries.push(entry),
|
||||
Err(err) => result.errors.push(
|
||||
Error::from(err)
|
||||
.with_path(self.dent.path())
|
||||
.with_depth(self.dent.depth() + 1),
|
||||
),
|
||||
}
|
||||
}
|
||||
let (ig, err) = self
|
||||
.ignore
|
||||
.add_child_with_entries(self.dent.path(), &result.entries);
|
||||
self.ignore = ig;
|
||||
self.dent.err = err;
|
||||
Ok(readdir)
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1522,11 +1681,11 @@ impl Stack {
|
|||
// breadth-first. We do depth-first because a breadth first traversal
|
||||
// on wide directories with a lot of gitignores is disastrous (for
|
||||
// example, searching a directory tree containing all of crates.io).
|
||||
let deques: Vec<Deque<Message>> = std::iter::repeat_with(Deque::new_lifo)
|
||||
.take(threads)
|
||||
.collect();
|
||||
let stealers =
|
||||
Arc::<[Stealer<Message>]>::from(deques.iter().map(Deque::stealer).collect::<Vec<_>>());
|
||||
let deques: Vec<Deque<Message>> =
|
||||
std::iter::repeat_with(Deque::new_lifo).take(threads).collect();
|
||||
let stealers = Arc::<[Stealer<Message>]>::from(
|
||||
deques.iter().map(Deque::stealer).collect::<Vec<_>>(),
|
||||
);
|
||||
let stacks: Vec<Stack> = deques
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
|
|
@ -1668,8 +1827,13 @@ impl<'s> Worker<'s> {
|
|||
// have sufficient read permissions to list the directory.
|
||||
// In that case we still want to provide the closure with a valid
|
||||
// entry before passing the error value.
|
||||
let readdir = work.read_dir();
|
||||
let depth = work.dent.depth();
|
||||
let readdir = if descend && self.max_depth.is_none_or(|m| depth < m) {
|
||||
Some(work.read_dir())
|
||||
} else {
|
||||
work.add_ignore();
|
||||
None
|
||||
};
|
||||
if should_visit {
|
||||
let state = self.visitor.visit(Ok(work.dent));
|
||||
if !state.is_continue() {
|
||||
|
|
@ -1680,6 +1844,10 @@ impl<'s> Worker<'s> {
|
|||
return WalkState::Skip;
|
||||
}
|
||||
|
||||
let readdir = match readdir {
|
||||
Some(readdir) => readdir,
|
||||
None => return WalkState::Skip,
|
||||
};
|
||||
let readdir = match readdir {
|
||||
Ok(readdir) => readdir,
|
||||
Err(err) => {
|
||||
|
|
@ -1687,11 +1855,19 @@ impl<'s> Worker<'s> {
|
|||
}
|
||||
};
|
||||
|
||||
if self.max_depth.map_or(false, |max| depth >= max) {
|
||||
return WalkState::Skip;
|
||||
for result in readdir.entries {
|
||||
let state = self.generate_work(
|
||||
&work.ignore,
|
||||
depth + 1,
|
||||
work.root_device,
|
||||
result,
|
||||
);
|
||||
if state.is_quit() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
for result in readdir {
|
||||
let state = self.generate_work(&work.ignore, depth + 1, work.root_device, result);
|
||||
for err in readdir.errors {
|
||||
let state = self.visitor.visit(Err(err));
|
||||
if state.is_quit() {
|
||||
return state;
|
||||
}
|
||||
|
|
@ -1717,14 +1893,8 @@ impl<'s> Worker<'s> {
|
|||
ig: &Ignore,
|
||||
depth: usize,
|
||||
root_device: Option<u64>,
|
||||
result: Result<fs::DirEntry, io::Error>,
|
||||
fs_dent: fs::DirEntry,
|
||||
) -> WalkState {
|
||||
let fs_dent = match result {
|
||||
Ok(fs_dent) => fs_dent,
|
||||
Err(err) => {
|
||||
return self.visitor.visit(Err(Error::from(err).with_depth(depth)));
|
||||
}
|
||||
};
|
||||
let mut dent = match DirEntryRaw::from_entry(depth, &fs_dent) {
|
||||
Ok(dent) => DirEntry::new_raw(dent, None),
|
||||
Err(err) => {
|
||||
|
|
@ -1760,26 +1930,24 @@ impl<'s> Worker<'s> {
|
|||
return WalkState::Continue;
|
||||
}
|
||||
}
|
||||
let should_skip_filesize = if self.max_filesize.is_some() && !dent.is_dir() {
|
||||
skip_filesize(
|
||||
self.max_filesize.unwrap(),
|
||||
dent.path(),
|
||||
&dent.metadata().ok(),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let should_skip_filtered = if let Some(Filter(predicate)) = &self.filter {
|
||||
!predicate(&dent)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let should_skip_filesize =
|
||||
if self.max_filesize.is_some() && !dent.is_dir() {
|
||||
skip_filesize(
|
||||
self.max_filesize.unwrap(),
|
||||
dent.path(),
|
||||
&dent.metadata().ok(),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let should_skip_filtered =
|
||||
if let Some(Filter(predicate)) = &self.filter {
|
||||
!predicate(&dent)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !should_skip_filesize && !should_skip_filtered {
|
||||
self.send(Work {
|
||||
dent,
|
||||
ignore: ig.clone(),
|
||||
root_device,
|
||||
});
|
||||
self.send(Work { dent, ignore: ig.clone(), root_device });
|
||||
}
|
||||
WalkState::Continue
|
||||
}
|
||||
|
|
@ -1820,6 +1988,9 @@ impl<'s> Worker<'s> {
|
|||
}
|
||||
// Wait for next `Work` or `Quit` message.
|
||||
loop {
|
||||
if self.is_quit_now() {
|
||||
return None;
|
||||
}
|
||||
if let Some(v) = self.recv() {
|
||||
self.activate_worker();
|
||||
value = Some(v);
|
||||
|
|
@ -1873,24 +2044,25 @@ impl<'s> Worker<'s> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'s> Drop for Worker<'s> {
|
||||
fn drop(&mut self) {
|
||||
if std::thread::panicking() {
|
||||
self.quit_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_symlink_loop(
|
||||
ig_parent: &Ignore,
|
||||
child_path: &Path,
|
||||
child_depth: usize,
|
||||
) -> Result<(), Error> {
|
||||
let hchild = Handle::from_path(child_path).map_err(|err| {
|
||||
Error::from(err)
|
||||
.with_path(child_path)
|
||||
.with_depth(child_depth)
|
||||
Error::from(err).with_path(child_path).with_depth(child_depth)
|
||||
})?;
|
||||
for ig in ig_parent
|
||||
.parents()
|
||||
.take_while(|ig| !ig.is_absolute_parent())
|
||||
{
|
||||
for ig in ig_parent.parents().take_while(|ig| !ig.is_absolute_parent()) {
|
||||
let h = Handle::from_path(ig.path()).map_err(|err| {
|
||||
Error::from(err)
|
||||
.with_path(child_path)
|
||||
.with_depth(child_depth)
|
||||
Error::from(err).with_path(child_path).with_depth(child_depth)
|
||||
})?;
|
||||
if hchild == h {
|
||||
return Err(Error::Loop {
|
||||
|
|
@ -1905,7 +2077,11 @@ fn check_symlink_loop(
|
|||
|
||||
// Before calling this function, make sure that you ensure that is really
|
||||
// necessary as the arguments imply a file stat.
|
||||
fn skip_filesize(max_filesize: u64, path: &Path, ent: &Option<Metadata>) -> bool {
|
||||
fn skip_filesize(
|
||||
max_filesize: u64,
|
||||
path: &Path,
|
||||
ent: &Option<Metadata>,
|
||||
) -> bool {
|
||||
let filesize = match *ent {
|
||||
Some(ref md) => Some(md.len()),
|
||||
None => None,
|
||||
|
|
@ -1977,9 +2153,9 @@ fn path_equals(dent: &DirEntry, handle: &Handle) -> Result<bool, Error> {
|
|||
if dent.is_stdin() || never_equal(dent, handle) {
|
||||
return Ok(false);
|
||||
}
|
||||
Handle::from_path(dent.path())
|
||||
.map(|h| &h == handle)
|
||||
.map_err(|err| Error::Io(err).with_path(dent.path()))
|
||||
Handle::from_path(dent.path()).map(|h| &h == handle).map_err(|err| {
|
||||
Error::Io(err).with_depth(dent.depth()).with_path(dent.path())
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if the given walkdir entry corresponds to a directory.
|
||||
|
|
@ -1997,16 +2173,14 @@ fn walkdir_is_dir(dent: &walkdir::DirEntry) -> bool {
|
|||
if !dent.file_type().is_symlink() || dent.depth() > 0 {
|
||||
return false;
|
||||
}
|
||||
dent.path()
|
||||
.metadata()
|
||||
.ok()
|
||||
.map_or(false, |md| md.file_type().is_dir())
|
||||
dent.path().metadata().ok().map_or(false, |md| md.file_type().is_dir())
|
||||
}
|
||||
|
||||
/// Returns true if and only if the given path is on the same device as the
|
||||
/// given root device.
|
||||
fn is_same_file_system(root_device: u64, path: &Path) -> Result<bool, Error> {
|
||||
let dent_device = device_num(path).map_err(|err| Error::Io(err).with_path(path))?;
|
||||
let dent_device =
|
||||
device_num(path).map_err(|err| Error::Io(err).with_path(path))?;
|
||||
Ok(root_device == dent_device)
|
||||
}
|
||||
|
||||
|
|
@ -2065,11 +2239,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn normal_path(unix: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
unix.replace("\\", "/")
|
||||
} else {
|
||||
unix.to_string()
|
||||
}
|
||||
if cfg!(windows) { unix.replace("\\", "/") } else { unix.to_string() }
|
||||
}
|
||||
|
||||
fn walk_collect(prefix: &Path, builder: &WalkBuilder) -> Vec<String> {
|
||||
|
|
@ -2089,7 +2259,10 @@ mod tests {
|
|||
paths
|
||||
}
|
||||
|
||||
fn walk_collect_parallel(prefix: &Path, builder: &WalkBuilder) -> Vec<String> {
|
||||
fn walk_collect_parallel(
|
||||
prefix: &Path,
|
||||
builder: &WalkBuilder,
|
||||
) -> Vec<String> {
|
||||
let mut paths = vec![];
|
||||
for dent in walk_collect_entries_parallel(builder) {
|
||||
let path = dent.path().strip_prefix(prefix).unwrap();
|
||||
|
|
@ -2278,6 +2451,27 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_depth_does_not_load_unreachable_ignore_files() {
|
||||
let td = tmpdir();
|
||||
let leaf = td.path().join("leaf");
|
||||
mkdirp(&leaf);
|
||||
wfile(leaf.join(".ignore"), "{invalid\n");
|
||||
|
||||
let mut builder = WalkBuilder::new(td.path());
|
||||
builder.max_depth(Some(1));
|
||||
let entry = builder
|
||||
.build()
|
||||
.find_map(|result| {
|
||||
let entry = result.unwrap();
|
||||
(entry.path() == leaf).then_some(entry)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(entry.error().is_none());
|
||||
assert_paths(td.path(), &builder, &["leaf"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_depth() {
|
||||
let td = tmpdir();
|
||||
|
|
@ -2388,7 +2582,9 @@ mod tests {
|
|||
assert_eq!(1, dents.len());
|
||||
assert!(!dents[0].path_is_symlink());
|
||||
|
||||
let dents = walk_collect_entries_parallel(&WalkBuilder::new(td.path().join("foo")));
|
||||
let dents = walk_collect_entries_parallel(&WalkBuilder::new(
|
||||
td.path().join("foo"),
|
||||
));
|
||||
assert_eq!(1, dents.len());
|
||||
assert!(!dents[0].path_is_symlink());
|
||||
}
|
||||
|
|
@ -2474,8 +2670,88 @@ mod tests {
|
|||
|
||||
assert_paths(
|
||||
td.path(),
|
||||
&WalkBuilder::new(td.path()).filter_entry(|entry| entry.file_name() != OsStr::new("a")),
|
||||
&WalkBuilder::new(td.path())
|
||||
.filter_entry(|entry| entry.file_name() != OsStr::new("a")),
|
||||
&["x", "x/y", "x/y/foo"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let td = tmpdir();
|
||||
assert_paths(td.path(), &WalkBuilder::empty(), &[]);
|
||||
|
||||
let empty_paths: Vec<&OsStr> = Vec::new();
|
||||
assert_paths(td.path(), &WalkBuilder::from_iter(empty_paths), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_iter() {
|
||||
let td = tmpdir();
|
||||
mkdirp(td.path().join("a/b/c"));
|
||||
mkdirp(td.path().join("d/e/f"));
|
||||
mkdirp(td.path().join("x/y"));
|
||||
wfile(td.path().join("a/b/foo"), "");
|
||||
wfile(td.path().join("d/e/f/foo"), "");
|
||||
wfile(td.path().join("x/y/foo"), "");
|
||||
|
||||
let paths = vec![
|
||||
td.path().join("a"),
|
||||
td.path().join("d"),
|
||||
td.path().join("x"),
|
||||
];
|
||||
|
||||
assert_paths(
|
||||
td.path(),
|
||||
&WalkBuilder::from_iter(paths),
|
||||
&[
|
||||
"x",
|
||||
"x/y",
|
||||
"x/y/foo",
|
||||
"d",
|
||||
"d/e",
|
||||
"d/e/f",
|
||||
"d/e/f/foo",
|
||||
"a",
|
||||
"a/b",
|
||||
"a/b/foo",
|
||||
"a/b/c",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// This should always panic and never hang.
|
||||
//
|
||||
// Ref: https://github.com/BurntSushi/ripgrep/issues/3009
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panic_in_parallel() {
|
||||
let td = tmpdir();
|
||||
wfile(td.path().join("foo.txt"), "");
|
||||
|
||||
WalkBuilder::new(td.path())
|
||||
.threads(40)
|
||||
.build_parallel()
|
||||
.run(|| Box::new(|_| panic!("oops!")));
|
||||
}
|
||||
|
||||
// This should always panic and never hang. The first call to the visitor
|
||||
// builder is used while processing the root paths. Previously, a panic on
|
||||
// the third call occurred after the first worker had already been spawned,
|
||||
// leaving it waiting indefinitely for workers that were never created.
|
||||
#[test]
|
||||
#[should_panic(expected = "builder panic")]
|
||||
fn panic_in_parallel_builder() {
|
||||
let td = tmpdir();
|
||||
wfile(td.path().join("foo.txt"), "");
|
||||
|
||||
let mut builds = 0;
|
||||
WalkBuilder::new(td.path()).threads(2).build_parallel().run(|| {
|
||||
builds += 1;
|
||||
if builds == 3 {
|
||||
panic!("builder panic");
|
||||
}
|
||||
Box::new(|_| WalkState::Continue)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use std::path::Path;
|
|||
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
|
||||
const IGNORE_FILE: &'static str = "tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
const IGNORE_FILE: &'static str =
|
||||
"tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
|
||||
fn get_gitignore() -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
|
|
@ -23,7 +24,9 @@ fn test_path_should_be_under_root() {
|
|||
#[test]
|
||||
fn test_files_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/file_root_00").is_ignore());
|
||||
|
|
@ -53,7 +56,9 @@ fn test_files_in_root() {
|
|||
#[test]
|
||||
fn test_files_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/parent_dir/file_deep_00").is_ignore());
|
||||
|
|
@ -83,8 +88,9 @@ fn test_files_in_deep() {
|
|||
#[test]
|
||||
fn test_dirs_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m =
|
||||
|path: &str, is_dir: bool| gitignore.matched_path_or_any_parents(Path::new(path), is_dir);
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/dir_root_00", true).is_ignore());
|
||||
|
|
@ -186,20 +192,25 @@ fn test_dirs_in_root() {
|
|||
#[test]
|
||||
fn test_dirs_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m =
|
||||
|path: &str, is_dir: bool| gitignore.matched_path_or_any_parents(Path::new(path), is_dir);
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none());
|
||||
|
|
@ -241,51 +252,67 @@ fn test_dirs_in_deep() {
|
|||
assert!(m("ROOT/parent_dir/dir_deep_20", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 22
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 23
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 32
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 33
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ ignore = { path = "../ignore" }
|
|||
regex = "1.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.48.0"
|
||||
tempfile = "3.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
unicode-width = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -37,20 +37,52 @@ pub fn init_tracing() {
|
|||
return;
|
||||
}
|
||||
|
||||
let file_path = format!("tailwindcss-{}.log", std::process::id());
|
||||
let file = OpenOptions::new()
|
||||
let root = Path::new(".tailwindcss");
|
||||
let logs_dir = root.join("logs");
|
||||
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&logs_dir.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure everything inside `.tailwindcss/` is ignored by git. The file is only created if it
|
||||
// doesn't exist yet, an existing `.gitignore` is left untouched.
|
||||
if let Ok(mut file) = std::fs::File::create_new(root.join(".gitignore")) {
|
||||
_ = file.write_all(b"*\n");
|
||||
}
|
||||
|
||||
let file_path = logs_dir.join(format!(
|
||||
"scanner-{}-{}.log",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0),
|
||||
std::process::id()
|
||||
));
|
||||
let file = match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to open {file_path}"));
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&file_path.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let file_path = Path::new(&file_path);
|
||||
let absolute_file_path = dunce::canonicalize(file_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
|
||||
let absolute_file_path = dunce::canonicalize(&file_path).unwrap_or_else(|_| file_path.clone());
|
||||
eprintln!(
|
||||
"{} Writing debug info to: {}\n",
|
||||
dim("[DEBUG]"),
|
||||
highlight(absolute_file_path.as_path().to_str().unwrap())
|
||||
highlight(&absolute_file_path.display().to_string())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
|
|
|||
|
|
@ -356,15 +356,14 @@ impl Scanner {
|
|||
let i = s.as_ptr() as usize - offset;
|
||||
let original = &original_content[i..i + s.len()];
|
||||
if original.contains_str("-[]") {
|
||||
return Some(unsafe {
|
||||
(String::from_utf8_unchecked(original.to_vec()), i)
|
||||
});
|
||||
return String::from_utf8(original.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i));
|
||||
}
|
||||
|
||||
// SAFETY: When we parsed the candidates, we already guaranteed that the byte
|
||||
// slices are valid, therefore we don't have to re-check here when we want to
|
||||
// convert it back to a string.
|
||||
Some(unsafe { (String::from_utf8_unchecked(s.to_vec()), i) })
|
||||
String::from_utf8(s.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i))
|
||||
}
|
||||
|
||||
_ => None,
|
||||
|
|
@ -617,7 +616,7 @@ where
|
|||
a
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
|
||||
.filter_map(|s| String::from_utf8(s.to_vec()).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -580,11 +580,11 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
describe.sequential.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])(
|
||||
describe.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])(
|
||||
'Using Vite %s',
|
||||
(version) => {
|
||||
test(
|
||||
'external source file changes trigger a full reload',
|
||||
'external source file changes update the CSS',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
|
|
@ -661,26 +661,33 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
expect(styles).toContain(candidate`content-['project-b/src/index.php']`)
|
||||
})
|
||||
|
||||
// Flush all messages so that we can be sure the next messages are from
|
||||
// the file changes we're about to make
|
||||
// Flush all messages so that we can be sure the next messages are
|
||||
// from the file changes we're about to make
|
||||
process.flush()
|
||||
|
||||
// Changing an external .php file should trigger a full reload
|
||||
// Changing an external .php file hot-updates the generated CSS
|
||||
{
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
txt`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
// Ensure the page reloaded
|
||||
// On Vite < 7.1, Vite itself hard-invalidates watched files that
|
||||
// aren't part of the module graph and reloads the page.
|
||||
//
|
||||
// On newer versions nothing reloads the page: the CSS hot-updates
|
||||
// through the regular pipeline because the changed file is a
|
||||
// watch dependency of the CSS root.
|
||||
//
|
||||
// Reloading the page for external template changes is the
|
||||
// responsibility of the backend integration (e.g. `laravel-vite-plugin`'s `refresh` option, or `vite-plugin-full-reload`).
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
if (version === '^6' || version === '7.0.8') {
|
||||
await process.onStdout((m) => m.includes('page reload') && m.includes('index.php'))
|
||||
} else {
|
||||
await process.onStderr(
|
||||
(m) => m.includes('vite:hmr (client)') && m.includes('index.php'),
|
||||
)
|
||||
await process.onStdout((m) => m.includes('hmr update') && m.includes('index.css'))
|
||||
}
|
||||
await process.onStderr((m) => m.includes('vite:hmr (ssr)') && m.includes('index.php'))
|
||||
|
||||
// Ensure the styles were regenerated with the new content
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
|
|
@ -853,7 +860,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/lazy.tsx']`)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// The same holds for a custom file type as long as some file of the
|
||||
|
|
@ -868,7 +874,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/comp-b.custom']`)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing a scanned stylesheet that is not part of the module graph
|
||||
|
|
@ -893,23 +898,26 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing an external file (e.g. a PHP template) should still trigger
|
||||
// a full reload. This must work even though `snippet.php` is part of
|
||||
// the module graph via the `?raw` import: a query import only pulls
|
||||
// the file's contents into the graph (and creates an untransformed
|
||||
// module node for the underlying file), it is not evidence that Vite
|
||||
// processes `.php` files as modules.
|
||||
// Changing an external file (e.g. a PHP template) hot-updates the
|
||||
// generated CSS but does not trigger a full reload either. Reloading the
|
||||
// page for external template changes is the responsibility of the backend
|
||||
// integration (e.g. `laravel-vite-plugin`'s `refresh` option, or
|
||||
// `vite-plugin-full-reload`).
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
{
|
||||
let updates = (await fs.read('project-a/hmr.log')).split('"type":"update"').length
|
||||
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
html`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
expect(await fs.read('project-a/hmr.log')).toContain('full-reload')
|
||||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
|
|
|
|||
165
integrations/vite/preact.test.ts
Normal file
165
integrations/vite/preact.test.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts, txt } from '../utils'
|
||||
|
||||
test(
|
||||
'dev mode',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"preact": "^10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import preact from '@preact/preset-vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
preact(),
|
||||
{
|
||||
// Log all HMR payloads to a file so the test can assert on them
|
||||
name: 'hmr-wiretap',
|
||||
configureServer(server) {
|
||||
let logFile = path.resolve('hmr.log')
|
||||
fs.writeFileSync(logFile, '')
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
let send = environment.hot.send.bind(environment.hot)
|
||||
environment.hot.send = (payload) => {
|
||||
fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n')
|
||||
return send(payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/main.tsx': ts`
|
||||
import { render } from 'preact'
|
||||
import { App } from './app'
|
||||
|
||||
render(<App />, document.getElementById('app')!)
|
||||
`,
|
||||
'src/app.tsx': ts`
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
<button className="underline" onClick={() => setCount((c) => c + 1)}>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss';`,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn, expect }) => {
|
||||
let process = await spawn('pnpm vite dev')
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
})
|
||||
|
||||
// Load the component modules, like a browser visiting the page would
|
||||
await fetch(`${url}/src/main.tsx`)
|
||||
await fetch(`${url}/src/app.tsx`)
|
||||
|
||||
// Editing a component keeps HMR intact: new classes are delivered through
|
||||
// a regular update, not a full page reload (which would lose all state)
|
||||
{
|
||||
await fs.write(
|
||||
'src/app.tsx',
|
||||
ts`
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
<button className="underline flex" onClick={() => setCount((c) => c + 1)}>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`underline`)
|
||||
expect(styles).toContain(candidate`flex`)
|
||||
})
|
||||
expect(await fs.read('hmr.log')).toContain('"type":"update"')
|
||||
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing a scanned file that is not part of the module graph (e.g.
|
||||
// `package.json`, which package managers and other tooling write to while
|
||||
// the dev server is running) should not trigger a full reload either —
|
||||
// that would destroy client state. New candidates should still be picked
|
||||
// up because the file is a watch dependency of the CSS root, so the CSS
|
||||
// hot-updates through Vite's regular pipeline.
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
{
|
||||
await fs.write(
|
||||
'package.json',
|
||||
txt`
|
||||
{
|
||||
"type": "module",
|
||||
"description": "content-['package.json']",
|
||||
"dependencies": {
|
||||
"preact": "^10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url)
|
||||
expect(styles).toContain(candidate`content-['package.json']`)
|
||||
})
|
||||
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -376,7 +376,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// Create a new compiler, given the new `input`
|
||||
;[compiler, scanner] = await createCompiler(input, I)
|
||||
|
||||
// Succesfully created a new compiler, so the `fullRebuildPaths`
|
||||
// Successfully created a new compiler, so the `fullRebuildPaths`
|
||||
// will be updated. If other errors occur, we should be able to
|
||||
// restore the paths unconditionally.
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
|
|
|
|||
|
|
@ -61,10 +61,14 @@ export function optimize(
|
|||
map = result.map?.toString()
|
||||
|
||||
result.warnings = result.warnings.filter((warning) => {
|
||||
// Ignore warnings about unknown pseudo-classes as they are likely caused
|
||||
// by the use of `:deep()`, `:slotted()`, and `:global()` which are not
|
||||
// standard CSS but are commonly used in frameworks like Vue.
|
||||
if (/'(deep|slotted|global)' is not recognized as a valid pseudo-/.test(warning.message)) {
|
||||
// Ignore warnings about unknown pseudo-classes that are used in frameworks
|
||||
// such as Vue or Angular and are handled by their own compilers, before
|
||||
// reaching the browser.
|
||||
if (
|
||||
/'(deep|slotted|global|ng-deep|host-context)' is not recognized as a valid pseudo-/.test(
|
||||
warning.message,
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import tailwindcss from './index'
|
||||
|
||||
// Vite's experimental `bundledDev` mode calls `hotUpdate` without a `server`,
|
||||
// so the handler must not dereference it.
|
||||
//
|
||||
// - https://github.com/vitejs/vite/discussions/22746
|
||||
// - https://github.com/tailwindlabs/tailwindcss/issues/20378
|
||||
// - https://vite.dev/blog/announcing-vite8-1#experimental-bundled-dev-mode
|
||||
test('hotUpdate does not crash when Vite omits the server (bundledDev)', () => {
|
||||
let plugin = tailwindcss().find((plugin) => plugin.name === '@tailwindcss/vite:generate:serve')!
|
||||
|
||||
let hotUpdate = plugin.hotUpdate as unknown as (options: {
|
||||
file: string
|
||||
modules: unknown[]
|
||||
timestamp: number
|
||||
server: undefined
|
||||
}) => unknown
|
||||
|
||||
expect(() =>
|
||||
hotUpdate.call(plugin, {
|
||||
file: '/app/template.html',
|
||||
modules: [{ type: 'asset', id: undefined }],
|
||||
timestamp: Date.now(),
|
||||
server: undefined,
|
||||
}),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
|
@ -9,23 +9,15 @@ import {
|
|||
} from '@tailwindcss/node'
|
||||
import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
||||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type {
|
||||
Environment,
|
||||
InternalResolveOptions,
|
||||
Plugin,
|
||||
ResolvedConfig,
|
||||
ViteDevServer,
|
||||
} from 'vite'
|
||||
import type { Environment, InternalResolveOptions, Plugin, ResolvedConfig } from 'vite'
|
||||
import * as vite from 'vite'
|
||||
|
||||
const DEBUG = env.DEBUG
|
||||
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/
|
||||
const COMMON_JS_PROXY_RE = /\?commonjs-proxy/
|
||||
const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/
|
||||
const JS_EXTENSIONS_RE = /^\.[cm]?[jt]sx?$/
|
||||
|
||||
export type PluginOptions = {
|
||||
/**
|
||||
|
|
@ -73,17 +65,9 @@ function createCustomResolver(
|
|||
}
|
||||
|
||||
export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
||||
let servers: ViteDevServer[] = []
|
||||
let config: ResolvedConfig | null = null
|
||||
let rootsByEnv = new DefaultMap<string, Map<string, Root>>((env: string) => new Map())
|
||||
|
||||
// File extensions that Vite (or one of its plugins) has been seen to process
|
||||
// as a module. Plugins don't get added or removed while the dev server is
|
||||
// running (changing the Vite config restarts the server), so once we've seen
|
||||
// evidence for a file type we don't need to scan the module graphs for it
|
||||
// again.
|
||||
let viteProcessedExtensions = new Set<string>()
|
||||
|
||||
let isSSR = false
|
||||
let shouldOptimize = true
|
||||
let minify = true
|
||||
|
|
@ -196,10 +180,6 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
name: '@tailwindcss/vite:scan',
|
||||
enforce: 'pre',
|
||||
|
||||
configureServer(server) {
|
||||
servers.push(server)
|
||||
},
|
||||
|
||||
async configResolved(_config) {
|
||||
config = _config
|
||||
isSSR = config.build.ssr !== false && config.build.ssr !== undefined
|
||||
|
|
@ -256,151 +236,6 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
return result
|
||||
},
|
||||
},
|
||||
|
||||
hotUpdate({ file, modules, timestamp, server }) {
|
||||
// Vite's experimental `bundledDev` mode invokes `hotUpdate` without a
|
||||
// `server`, so there are no sibling environments to inspect and no
|
||||
// server-level `hot`/`ws` channel to reload through. Bail out early
|
||||
// rather than dereferencing `undefined`.
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20378
|
||||
if (!server) return
|
||||
|
||||
// Ensure full-reloads are triggered for files that are being watched by
|
||||
// Tailwind but aren't part of the module graph (like PHP or HTML
|
||||
// files). If we don't do this, then changes to those files won't
|
||||
// trigger a reload at all since Vite doesn't know about them.
|
||||
{
|
||||
// It's a little bit confusing, because due to the `addWatchFile`
|
||||
// calls, it _is_ part of the module graph but nothing is really
|
||||
// handling those files. These modules typically have an id of
|
||||
// undefined and/or have a type of 'asset'.
|
||||
//
|
||||
// If we call `addWatchFile` on a file that is part of the actual
|
||||
// module graph, then we will see a module for it with a type of `js`
|
||||
// and a type of `asset`. We are only interested if _all_ of them are
|
||||
// missing an id and/or have a type of 'asset', which is a strong
|
||||
// signal that the changed file is not being handled by Vite or any of
|
||||
// the plugins.
|
||||
//
|
||||
// Note: in Vite v7.0.6 the modules here will have a type of `js`, not
|
||||
// 'asset'. But it will also have a `HARD_INVALIDATED` state and will
|
||||
// do a full page reload already.
|
||||
//
|
||||
// Empty modules can be skipped since it means it's not
|
||||
// `addWatchFile`d and thus irrelevant to Tailwind.
|
||||
let isExternalFile =
|
||||
modules.length > 0 &&
|
||||
modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
|
||||
if (!isExternalFile) return
|
||||
|
||||
// Skip files that Vite (or one of its plugins) processes as a
|
||||
// module — in this environment (e.g. a lazily-loaded route that
|
||||
// hasn't been visited yet) or in another one (e.g. an SSR-only
|
||||
// module). Such a file can only affect the page through Vite's own
|
||||
// pipeline, so a full reload would only destroy client state. Any
|
||||
// changes to the generated CSS still go through the regular
|
||||
// `css-update` flow because the file is registered via
|
||||
// `addWatchFile`.
|
||||
//
|
||||
// If the file exists as a real module in another environment, then
|
||||
// that environment is responsible for it. E.g. an SSR framework
|
||||
// has its own server side hmr/reload mechanism when handling
|
||||
// server only modules. See https://v6.vite.dev/guide/migration.html
|
||||
// > Updates to an SSR-only module no longer triggers a full page reload in the client. ...
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
if (environment.name === this.environment.name) continue
|
||||
|
||||
let modules = environment.moduleGraph.getModulesByFile(file)
|
||||
if (modules) {
|
||||
for (let mod of modules) {
|
||||
if (mod.type !== 'asset') {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise the file is not loaded as a module anywhere, so
|
||||
// determine whether its file _type_ would be processed by Vite
|
||||
// when requested by the browser (in which case the file just isn't
|
||||
// loaded yet, e.g. a lazily-loaded route that hasn't been visited).
|
||||
// Vite has no API to answer this without actually running the
|
||||
// plugin pipeline, so instead:
|
||||
//
|
||||
// Files Vite handles natively (the JS/TS and CSS families) are always
|
||||
// processed by Vite. This includes stylesheets that never show up as
|
||||
// their own module because a framework plugin compiles them into a
|
||||
// component (e.g. Angular), in which case that plugin owns their HMR.
|
||||
let extension = path.extname(file)
|
||||
if (JS_EXTENSIONS_RE.test(extension) || vite.isCSSRequest(file)) return
|
||||
|
||||
// For any other file type (e.g. `.vue`, `.svelte`, or `.md` with an
|
||||
// SSG plugin), if a file with the same extension exists as a real
|
||||
// module in any environment's module graph, then a plugin evidently
|
||||
// handles this file type and the changed file just isn't loaded
|
||||
// (yet).
|
||||
if (extension !== '') {
|
||||
if (viteProcessedExtensions.has(extension)) return
|
||||
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
for (let mod of environment.moduleGraph.idToModuleMap.values()) {
|
||||
if (!mod.file?.endsWith(extension)) continue
|
||||
if (mod.type === 'asset') continue
|
||||
|
||||
// Only count modules that the plugin pipeline actually
|
||||
// transformed. Vite also creates untransformed placeholder
|
||||
// nodes (e.g. for the file underlying a `?raw` import) that
|
||||
// are not evidence that a plugin handles this file type.
|
||||
if (mod.transformResult == null) continue
|
||||
|
||||
// Similarly, ignore query imports (e.g. `./template.html?raw`,
|
||||
// or the `?html-proxy` modules Vite creates for inline
|
||||
// scripts): they pull a file's _contents_ into the graph
|
||||
// without a plugin processing the file type. A scanned
|
||||
// `.html` template must still trigger a full reload even if
|
||||
// some other `.html` file is imported with `?raw`.
|
||||
if (!mod.id || mod.id.includes('?')) continue
|
||||
|
||||
viteProcessedExtensions.add(extension)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of new Set([this.environment.name, 'client'])) {
|
||||
let roots = rootsByEnv.get(env)
|
||||
if (roots.size === 0) continue
|
||||
|
||||
// If the file is not being watched by any of the roots, then we can
|
||||
// skip the reload since it's not relevant to Tailwind CSS.
|
||||
if (!isScannedFile(file, modules, roots)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// https://vite.dev/changes/hotupdate-hook#migration-guide
|
||||
let invalidatedModules = new Set<vite.EnvironmentModuleNode>()
|
||||
for (let mod of modules) {
|
||||
this.environment.moduleGraph.invalidateModule(
|
||||
mod,
|
||||
invalidatedModules,
|
||||
timestamp,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if (env === this.environment.name) {
|
||||
this.environment.hot.send({ type: 'full-reload' })
|
||||
} else if (server.hot.send) {
|
||||
server.hot.send({ type: 'full-reload' })
|
||||
} else if (server.ws.send) {
|
||||
server.ws.send({ type: 'full-reload' })
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -521,10 +356,6 @@ class Root {
|
|||
private customJsResolver: (id: string, base: string) => Promise<string | false | undefined>,
|
||||
) {}
|
||||
|
||||
get scannedFiles() {
|
||||
return this.scanner?.files ?? []
|
||||
}
|
||||
|
||||
// Generate the CSS for the root file. This can return false if the file is
|
||||
// not considered a Tailwind root. When this happened, the root can be GCed.
|
||||
public async generate(
|
||||
|
|
@ -710,54 +541,3 @@ class Root {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isScannedFile(
|
||||
file: string,
|
||||
modules: vite.EnvironmentModuleNode[],
|
||||
roots: Map<string, Root>,
|
||||
) {
|
||||
let seen = new Set()
|
||||
let q = [...modules]
|
||||
let checks = {
|
||||
file,
|
||||
get realpath() {
|
||||
try {
|
||||
let realpath = realpathSync(file)
|
||||
Object.defineProperty(checks, 'realpath', { value: realpath })
|
||||
return realpath
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
while (q.length > 0) {
|
||||
let module = q.shift()!
|
||||
if (seen.has(module)) continue
|
||||
seen.add(module)
|
||||
|
||||
if (module.id) {
|
||||
let root = roots.get(module.id)
|
||||
|
||||
if (root) {
|
||||
// If the file is part of the scanned files for this root, then we know
|
||||
// for sure that it's being watched by any of the Tailwind CSS roots. It
|
||||
// doesn't matter which root it is since it's only used to know whether
|
||||
// we should trigger a full reload or not.
|
||||
if (
|
||||
root.scannedFiles.includes(checks.file) ||
|
||||
(checks.realpath && root.scannedFiles.includes(checks.realpath))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep walking up the tree until we find a root.
|
||||
for (let importer of module.importers) {
|
||||
q.push(importer)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -516,6 +516,8 @@ it('should not parse functional utilities with multiple modifiers', () => {
|
|||
utilities.functional('bg', () => [])
|
||||
|
||||
expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`[]`)
|
||||
expect(run('bg-red-500/50/', { utilities })).toMatchInlineSnapshot(`[]`)
|
||||
expect(run('bg-red-500/50//foo', { utilities })).toMatchInlineSnapshot(`[]`)
|
||||
})
|
||||
|
||||
it('should parse a utility with an arbitrary value', () => {
|
||||
|
|
@ -1416,6 +1418,19 @@ it('should parse a functional variant with a modifier', () => {
|
|||
`)
|
||||
})
|
||||
|
||||
it('should not parse variants with multiple modifiers', () => {
|
||||
let utilities = new Utilities()
|
||||
utilities.static('flex', () => [])
|
||||
|
||||
let variants = new Variants()
|
||||
variants.static('hover', () => {})
|
||||
variants.compound('group', Compounds.StyleRules, () => {})
|
||||
|
||||
expect(run('group-hover/foo:flex', { utilities, variants })).toHaveLength(1)
|
||||
expect(run('group-hover/foo/:flex', { utilities, variants })).toEqual([])
|
||||
expect(run('group-hover/foo//bar:flex', { utilities, variants })).toEqual([])
|
||||
})
|
||||
|
||||
it('should parse a functional variant starting with @', () => {
|
||||
let utilities = new Utilities()
|
||||
utilities.static('flex', () => [])
|
||||
|
|
|
|||
|
|
@ -372,6 +372,14 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
|
|||
}
|
||||
}
|
||||
|
||||
// If there's more than one modifier, the utility is invalid.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `bg-red-500/50/50`
|
||||
let parts = segment(base, '/')
|
||||
if (parts.length > 2) return
|
||||
|
||||
// Figure out the new base and the modifier segment if present.
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -381,14 +389,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
|
|||
// ^^^^^^^^^^ -> Base without modifier
|
||||
// ^^ -> Modifier segment
|
||||
// ```
|
||||
let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/')
|
||||
|
||||
// If there's more than one modifier, the utility is invalid.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `bg-red-500/50/50`
|
||||
if (additionalModifier) return
|
||||
let [baseWithoutModifier, modifierSegment = null] = parts
|
||||
|
||||
let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment)
|
||||
|
||||
|
|
@ -707,17 +708,18 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
|
|||
|
||||
// Static, functional and compound variants
|
||||
{
|
||||
// group-hover/group-name
|
||||
// ^^^^^^^^^^^ -> Variant without modifier
|
||||
// ^^^^^^^^^^ -> Modifier
|
||||
let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/')
|
||||
|
||||
// If there's more than one modifier, the variant is invalid.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `group-hover/foo/bar`
|
||||
if (additionalModifier) return null
|
||||
let parts = segment(variant, '/')
|
||||
if (parts.length > 2) return null
|
||||
|
||||
// group-hover/group-name
|
||||
// ^^^^^^^^^^^ -> Variant without modifier
|
||||
// ^^^^^^^^^^ -> Modifier
|
||||
let [variantWithoutModifier, modifier = null] = parts
|
||||
|
||||
let roots = findRoots(variantWithoutModifier, (root) => {
|
||||
return designSystem.variants.has(root)
|
||||
|
|
@ -1096,7 +1098,7 @@ const printArbitraryValueCache = new DefaultMap<string, string>((input) => {
|
|||
node.value = ','
|
||||
}
|
||||
|
||||
// Wrap custom functions starting with `--`, in parentheses if preceeded by
|
||||
// Wrap custom functions starting with `--`, in parentheses if preceded by
|
||||
// a symbol. E.g.: `calc(100%---spacing(2))` → `calc(100%-(--spacing(2)))`
|
||||
else if (node.kind === 'function' && node.value.startsWith('--')) {
|
||||
let idx = ctx.index
|
||||
|
|
|
|||
|
|
@ -1638,4 +1638,26 @@ describe('regressions', () => {
|
|||
'lg:flex',
|
||||
])
|
||||
})
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1610
|
||||
test('does not merge utilities whose theme variables resolve to CSS-wide keywords', async () => {
|
||||
let designSystem = await __unstable__loadDesignSystem(
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@theme {
|
||||
--foreground: unset;
|
||||
--default: unset;
|
||||
}
|
||||
@theme inline {
|
||||
--color-foreground: var(--foreground);
|
||||
--color-default-soft-hover: color-mix(in oklab, var(--default) 60%, transparent);
|
||||
}
|
||||
`,
|
||||
{ base: __dirname },
|
||||
)
|
||||
|
||||
expect(
|
||||
designSystem.canonicalizeCandidates(['text-foreground/60', 'text-default-soft-hover']),
|
||||
).toEqual(['text-foreground/60', 'text-default-soft-hover'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2563,6 +2563,25 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
return ast
|
||||
}
|
||||
|
||||
// Variables whose theme value is a CSS-wide keyword (e.g.: `unset`) are never
|
||||
// inlined. These are typically registered as a placeholder to be re-assigned at
|
||||
// runtime, so two variables that share such a value are not interchangeable.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// @theme {
|
||||
// --foreground: unset;
|
||||
// --background: unset;
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Inlining would make `text-(--foreground)` and `text-(--background)` produce
|
||||
// the same signature `color: unset`, even though they are different at runtime.
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/Data_types#css-wide_keywords
|
||||
const CSS_WIDE_KEYWORDS = ['initial', 'inherit', 'revert', 'revert-layer', 'revert-rule', 'unset']
|
||||
|
||||
// Resolve theme values to their inlined value.
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -2578,8 +2597,8 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
// }
|
||||
// ```
|
||||
//
|
||||
// Which conveniently will be equivalent to: `text-red-500` when we inline
|
||||
// the value.
|
||||
// Which conveniently will be equivalent to: `text-red-500` when we inline the
|
||||
// value.
|
||||
//
|
||||
// Without inlining:
|
||||
// ```css
|
||||
|
|
@ -2595,13 +2614,13 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
// }
|
||||
// ```
|
||||
//
|
||||
// Recently we made sure that utilities like `text-red-500` also generate
|
||||
// the fallback value for usage in `@reference` mode.
|
||||
// Recently we made sure that utilities like `text-red-500` also generate the
|
||||
// fallback value for usage in `@reference` mode.
|
||||
//
|
||||
// The second assumption is that if you use `var(--key, fallback)` that
|
||||
// happens to match a known variable _and_ its inlined value. Then we can
|
||||
// replace it with the inlined variable. This allows us to handle custom
|
||||
// `@theme` and `@theme inline` definitions.
|
||||
// The second assumption is that if you use `var(--key, fallback)` that happens
|
||||
// to match a known variable _and_ its inlined value. Then we can replace it
|
||||
// with the inlined variable. This allows us to handle custom `@theme` and
|
||||
// `@theme inline` definitions.
|
||||
function resolveVariablesInValue(value: string, designSystem: DesignSystem): string {
|
||||
let changed = false
|
||||
let valueAst = ValueParser.parse(value)
|
||||
|
|
@ -2630,6 +2649,9 @@ function resolveVariablesInValue(value: string, designSystem: DesignSystem): str
|
|||
seen.add(variable)
|
||||
if (variableValue === undefined) return // Couldn't resolve the variable
|
||||
|
||||
// CSS-wide keywords are never inlined
|
||||
if (CSS_WIDE_KEYWORDS.includes(variableValue.toLowerCase())) return
|
||||
|
||||
// Inject variable fallbacks when no fallback is present yet.
|
||||
//
|
||||
// A fallback could consist of multiple values.
|
||||
|
|
|
|||
|
|
@ -1630,7 +1630,7 @@ describe('addVariant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) {
|
||||
:is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -1744,7 +1744,7 @@ describe('addVariant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -1784,7 +1784,7 @@ describe('addVariant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -1827,12 +1827,12 @@ describe('addVariant', () => {
|
|||
"
|
||||
@layer utilities {
|
||||
@media (hover: hover) {
|
||||
.group-hocus\\:flex:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-hocus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -1964,7 +1964,7 @@ describe('addVariant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-optional\\:flex:is(:where(.group):optional *), .group-optional\\/foo\\:flex:is(:where(.group\\/foo):optional *), .peer-optional\\:flex:is(:where(.peer):optional ~ *), .optional\\:flex:optional {
|
||||
:is(:where(.group):optional .group-optional\\:flex), :is(:where(.group\\/foo):optional .group-optional\\/foo\\:flex), :is(:where(.peer):optional ~ .peer-optional\\:flex), .optional\\:flex:optional {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -2903,7 +2903,7 @@ describe('matchVariant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-optional-\\[test\\]\\:flex:is(:where(.group):optional:has(test) :where(.group) *), .group-optional-\\[test\\]\\/foo\\:flex:is(:where(.group\\/foo):optional:has(test) :where(.group\\/foo) *), .peer-optional-\\[test\\]\\:flex:is(:where(.peer):optional:has(test) :where(.peer) ~ *), .optional-\\[test\\]\\:flex:optional:has(test) .optional-\\[test\\]\\:flex {
|
||||
:is(:where(.group):optional:has(test) :where(.group) .group-optional-\\[test\\]\\:flex), :is(:where(.group\\/foo):optional:has(test) :where(.group\\/foo) .group-optional-\\[test\\]\\/foo\\:flex), :is(:where(.peer):optional:has(test) :where(.peer) ~ .peer-optional-\\[test\\]\\:flex), .optional-\\[test\\]\\:flex:optional:has(test) .optional-\\[test\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,23 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
|||
).toEqual([])
|
||||
})
|
||||
|
||||
it('should end a comment at `*/` even when it is preceded by a `\\`', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
/* C:\temp\*/
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a comment inside of a selector and ignore it', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
|
|
@ -448,6 +465,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
|||
])
|
||||
})
|
||||
|
||||
it('should end a comment in a custom property at `*/` even when it is preceded by a `\\`', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: /* C:\temp\*/ bar;
|
||||
--bar: /* baz */ qux;
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: '/* C:\\temp\\*/ bar',
|
||||
important: false,
|
||||
},
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--bar',
|
||||
value: '/* baz */ qux',
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse empty custom properties', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
|
|
|
|||
|
|
@ -128,19 +128,24 @@ export function parse(input: string, opts?: ParseOptions) {
|
|||
// ^^^^^^^^^^^^^
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// The escape character `\` in comments are ignored, this means that a
|
||||
// the end of a comment preceded by `\` does _not_ mean that the end of the
|
||||
// comment is escaped and therefore we have to keep parsing.
|
||||
//
|
||||
// This is a valid comment:
|
||||
// ```
|
||||
// /*C:\*/
|
||||
// ```
|
||||
// See: https://www.w3.org/TR/css-syntax-3/#consume-comment
|
||||
else if (currentChar === SLASH && input.charCodeAt(i + 1) === ASTERISK) {
|
||||
let start = i
|
||||
|
||||
for (let j = i + 2; j < input.length; j++) {
|
||||
peekChar = input.charCodeAt(j)
|
||||
|
||||
// Current character is a `\` therefore the next character is escaped.
|
||||
if (peekChar === BACKSLASH) {
|
||||
j += 1
|
||||
}
|
||||
|
||||
// End of the comment
|
||||
else if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) {
|
||||
// End of the comment.
|
||||
if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) {
|
||||
i = j + 1
|
||||
break
|
||||
}
|
||||
|
|
@ -224,13 +229,9 @@ export function parse(input: string, opts?: ParseOptions) {
|
|||
else if (peekChar === SLASH && input.charCodeAt(j + 1) === ASTERISK) {
|
||||
for (let k = j + 2; k < input.length; k++) {
|
||||
peekChar = input.charCodeAt(k)
|
||||
// Current character is a `\` therefore the next character is escaped.
|
||||
if (peekChar === BACKSLASH) {
|
||||
k += 1
|
||||
}
|
||||
|
||||
// End of the comment
|
||||
else if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) {
|
||||
// End of the comment.
|
||||
if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) {
|
||||
j = k + 1
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ export function buildDesignSystem(
|
|||
let variants = createVariants(theme)
|
||||
|
||||
let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem))
|
||||
let cachedVariantOrder: Map<Variant, number> | null = null
|
||||
let cachedVariantOrderSize = -1
|
||||
let parsedCandidates = new DefaultMap((candidate) =>
|
||||
Array.from(parseCandidate(candidate, designSystem)),
|
||||
)
|
||||
|
|
@ -188,6 +190,11 @@ export function buildDesignSystem(
|
|||
},
|
||||
|
||||
getVariantOrder() {
|
||||
// parsedVariants only grows, so the cache should be reset only when the size changes
|
||||
if (cachedVariantOrder !== null && cachedVariantOrderSize === parsedVariants.size) {
|
||||
return cachedVariantOrder
|
||||
}
|
||||
|
||||
let variants = Array.from(parsedVariants.values())
|
||||
variants.sort((a, z) => this.variants.compare(a, z))
|
||||
|
||||
|
|
@ -209,6 +216,9 @@ export function buildDesignSystem(
|
|||
prevVariant = variant
|
||||
}
|
||||
|
||||
cachedVariantOrder = order
|
||||
cachedVariantOrderSize = parsedVariants.size
|
||||
|
||||
return order
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1444,42 +1444,42 @@ describe('sorting', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:flex:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-focus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-focus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.peer-hover\\:flex:is(:where(.peer):hover ~ *) {
|
||||
:is(:where(.peer):hover ~ .peer-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:peer-hover\\:flex:is(:where(.group):hover *):is(:where(.peer):hover ~ *), .peer-hover\\:group-hover\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):hover *) {
|
||||
:is(:where(.peer):hover ~ :is(:where(.group):hover .group-hover\\:peer-hover\\:flex)), :is(:where(.group):hover :is(:where(.peer):hover ~ .peer-hover\\:group-hover\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-focus\\:peer-hover\\:flex:is(:where(.group):focus *):is(:where(.peer):hover ~ *), .peer-hover\\:group-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):focus *) {
|
||||
:is(:where(.peer):hover ~ :is(:where(.group):focus .group-focus\\:peer-hover\\:flex)), :is(:where(.group):focus :is(:where(.peer):hover ~ .peer-hover\\:group-focus\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.peer-focus\\:flex:is(:where(.peer):focus ~ *) {
|
||||
:is(:where(.peer):focus ~ .peer-focus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:peer-focus\\:flex:is(:where(.group):hover *):is(:where(.peer):focus ~ *), .peer-focus\\:group-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):hover *) {
|
||||
:is(:where(.peer):focus ~ :is(:where(.group):hover .group-hover\\:peer-focus\\:flex)), :is(:where(.group):hover :is(:where(.peer):focus ~ .peer-focus\\:group-hover\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-focus\\:peer-focus\\:flex:is(:where(.group):focus *):is(:where(.peer):focus ~ *), .peer-focus\\:group-focus\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):focus *) {
|
||||
:is(:where(.peer):focus ~ :is(:where(.group):focus .group-focus\\:peer-focus\\:flex)), :is(:where(.group):focus :is(:where(.peer):focus ~ .peer-focus\\:group-focus\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -3601,7 +3601,7 @@ describe('plugins', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) {
|
||||
:is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -3636,7 +3636,7 @@ describe('plugins', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -3674,7 +3674,7 @@ describe('plugins', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):hover .group-hocus\\:flex), :is(:where(.group):focus .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -3715,12 +3715,12 @@ describe('plugins', () => {
|
|||
"
|
||||
@layer utilities {
|
||||
@media (hover: hover) {
|
||||
.group-hocus\\:flex:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-hocus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -4199,7 +4199,7 @@ describe('@custom-variant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) {
|
||||
:is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -4294,7 +4294,7 @@ describe('@custom-variant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-selected\\:underline:is(:where(.group)[data-selected] *), .selected\\:underline[data-selected] {
|
||||
:is(:where(.group)[data-selected] .group-selected\\:underline), .selected\\:underline[data-selected] {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
|
@ -4322,7 +4322,7 @@ describe('@custom-variant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:underline:is(:is(:where(.group):hover, :where(.group):focus) *), .hocus\\:underline:hover, .hocus\\:underline:focus {
|
||||
:is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:underline), .hocus\\:underline:hover, .hocus\\:underline:focus {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
|
@ -4353,7 +4353,7 @@ describe('@custom-variant', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.group-hocus\\:underline:is(:where(.group):hover *), .group-hocus\\:underline:is(:where(.group):focus *), .hocus\\:underline:hover, .hocus\\:underline:focus {
|
||||
:is(:where(.group):hover .group-hocus\\:underline), :is(:where(.group):focus .group-hocus\\:underline), .hocus\\:underline:hover, .hocus\\:underline:focus {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
|
@ -4458,12 +4458,12 @@ describe('@custom-variant', () => {
|
|||
"
|
||||
@layer utilities {
|
||||
@media (hover: hover) {
|
||||
.group-hocus\\:underline:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hocus\\:underline) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.group-hocus\\:underline:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-hocus\\:underline) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
|
|
@ -4503,7 +4503,7 @@ describe('@custom-variant', () => {
|
|||
"
|
||||
@layer utilities {
|
||||
@media (hover: hover) {
|
||||
.group-hocus\\:underline:is(:where(.group):hover *), .hocus\\:underline:hover {
|
||||
:is(:where(.group):hover .group-hocus\\:underline), .hocus\\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,9 +97,9 @@ test('getVariants compound', async () => {
|
|||
]
|
||||
|
||||
expect(list).toEqual([
|
||||
['@media (hover: hover) { &:is(:where(.group):hover *) }'],
|
||||
['@media (hover: hover) { &:is(:where(.group\\/sidebar):hover *) }'],
|
||||
['@media (hover: hover) { &:is(:where(.group):is(:where(.group):hover *) *) }'],
|
||||
['@media (hover: hover) { :is(:where(.group):hover &) }'],
|
||||
['@media (hover: hover) { :is(:where(.group\\/sidebar):hover &) }'],
|
||||
['@media (hover: hover) { :is(:is(:where(.group):hover :where(.group)) &) }'],
|
||||
[],
|
||||
[],
|
||||
])
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ test('utilities must be prefixed', async () => {
|
|||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.tw\\:group-hover\\:flex:is(:where(.tw\\:group):hover *), .tw\\:peer-hover\\:flex:is(:where(.tw\\:peer):hover ~ *) {
|
||||
:is(:where(.tw\\:group):hover .tw\\:group-hover\\:flex), :is(:where(.tw\\:peer):hover ~ .tw\\:peer-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12154,18 +12154,28 @@ test('rounded', async () => {
|
|||
"
|
||||
`)
|
||||
expect(
|
||||
await run([
|
||||
'-rounded',
|
||||
'-rounded-full',
|
||||
'-rounded-none',
|
||||
'-rounded-sm',
|
||||
'-rounded-[4px]',
|
||||
'rounded/foo',
|
||||
'rounded-full/foo',
|
||||
'rounded-none/foo',
|
||||
'rounded-sm/foo',
|
||||
'rounded-[4px]/foo',
|
||||
]),
|
||||
await run(
|
||||
[
|
||||
'-rounded',
|
||||
'-rounded-full',
|
||||
'-rounded-none',
|
||||
'-rounded-sm',
|
||||
'-rounded-[4px]',
|
||||
'rounded/foo',
|
||||
'rounded-full/foo',
|
||||
'rounded-none/foo',
|
||||
'rounded-sm/foo',
|
||||
'rounded-[4px]/foo',
|
||||
'rounded-sm/[5]',
|
||||
'rounded-sm/(--foo)',
|
||||
],
|
||||
css`
|
||||
@theme {
|
||||
--radius-sm: 0.125rem;
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
|
|
@ -22373,6 +22383,11 @@ test('stroke', async () => {
|
|||
// Width
|
||||
'-stroke-0',
|
||||
'stroke--1',
|
||||
'stroke-0/foo',
|
||||
'stroke-1/50',
|
||||
'stroke-2/[0.5]',
|
||||
'stroke-2/(--my-opacity)',
|
||||
'stroke-[12px]/foo',
|
||||
]),
|
||||
).toEqual('')
|
||||
})
|
||||
|
|
@ -24398,82 +24413,90 @@ test('filter', async () => {
|
|||
"
|
||||
`)
|
||||
expect(
|
||||
await run([
|
||||
'-filter',
|
||||
'-filter-none',
|
||||
'-filter-[var(--value)]',
|
||||
'-blur-xl',
|
||||
'-blur-[4px]',
|
||||
'brightness--50',
|
||||
'-brightness-50',
|
||||
'-brightness-[1.23]',
|
||||
'brightness-unknown',
|
||||
'contrast--50',
|
||||
'-contrast-50',
|
||||
'-contrast-[1.23]',
|
||||
'contrast-unknown',
|
||||
'-grayscale',
|
||||
'-grayscale-0',
|
||||
'grayscale--1',
|
||||
'-grayscale-[var(--value)]',
|
||||
'grayscale-unknown',
|
||||
'hue-rotate--5',
|
||||
'hue-rotate-unknown',
|
||||
'-invert',
|
||||
'invert--5',
|
||||
'-invert-0',
|
||||
'-invert-[var(--value)]',
|
||||
'invert-unknown',
|
||||
'-drop-shadow-xl',
|
||||
'-drop-shadow-[0_0_red]',
|
||||
await run(
|
||||
[
|
||||
'-filter',
|
||||
'-filter-none',
|
||||
'-filter-[var(--value)]',
|
||||
'-blur-xl',
|
||||
'-blur-[4px]',
|
||||
'brightness--50',
|
||||
'-brightness-50',
|
||||
'-brightness-[1.23]',
|
||||
'brightness-unknown',
|
||||
'contrast--50',
|
||||
'-contrast-50',
|
||||
'-contrast-[1.23]',
|
||||
'contrast-unknown',
|
||||
'-grayscale',
|
||||
'-grayscale-0',
|
||||
'grayscale--1',
|
||||
'-grayscale-[var(--value)]',
|
||||
'grayscale-unknown',
|
||||
'hue-rotate--5',
|
||||
'hue-rotate-unknown',
|
||||
'-invert',
|
||||
'invert--5',
|
||||
'-invert-0',
|
||||
'-invert-[var(--value)]',
|
||||
'invert-unknown',
|
||||
'-drop-shadow-xl',
|
||||
'-drop-shadow-[0_0_red]',
|
||||
|
||||
'drop-shadow/foo',
|
||||
'-drop-shadow/foo',
|
||||
'-drop-shadow/25',
|
||||
'-drop-shadow-red-500',
|
||||
'drop-shadow-red-500/foo',
|
||||
'-drop-shadow-red-500/foo',
|
||||
'-drop-shadow-red-500/50',
|
||||
'drop-shadow/foo',
|
||||
'-drop-shadow/foo',
|
||||
'-drop-shadow/25',
|
||||
'-drop-shadow-red-500',
|
||||
'drop-shadow-red-500/foo',
|
||||
'-drop-shadow-red-500/foo',
|
||||
'-drop-shadow-red-500/50',
|
||||
|
||||
'-saturate-0',
|
||||
'saturate--5',
|
||||
'-saturate-[1.75]',
|
||||
'-saturate-[var(--value)]',
|
||||
'saturate-saturate',
|
||||
'-sepia',
|
||||
'sepia--50',
|
||||
'-sepia-0',
|
||||
'-sepia-[50%]',
|
||||
'-sepia-[var(--value)]',
|
||||
'sepia-unknown',
|
||||
'filter/foo',
|
||||
'filter-none/foo',
|
||||
'filter-[var(--value)]/foo',
|
||||
'blur-xl/foo',
|
||||
'blur-none/foo',
|
||||
'blur-[4px]/foo',
|
||||
'brightness-50/foo',
|
||||
'brightness-[1.23]/foo',
|
||||
'contrast-50/foo',
|
||||
'contrast-[1.23]/foo',
|
||||
'grayscale/foo',
|
||||
'grayscale-0/foo',
|
||||
'grayscale-[var(--value)]/foo',
|
||||
'hue-rotate-15/foo',
|
||||
'hue-rotate-[45deg]/foo',
|
||||
'invert/foo',
|
||||
'invert-0/foo',
|
||||
'invert-[var(--value)]/foo',
|
||||
'drop-shadow-xl/foo',
|
||||
'drop-shadow-[0_0_red]/foo',
|
||||
'saturate-0/foo',
|
||||
'saturate-[1.75]/foo',
|
||||
'saturate-[var(--value)]/foo',
|
||||
'sepia/foo',
|
||||
'sepia-0/foo',
|
||||
'sepia-[50%]/foo',
|
||||
'sepia-[var(--value)]/foo',
|
||||
]),
|
||||
'-saturate-0',
|
||||
'saturate--5',
|
||||
'-saturate-[1.75]',
|
||||
'-saturate-[var(--value)]',
|
||||
'saturate-saturate',
|
||||
'-sepia',
|
||||
'sepia--50',
|
||||
'-sepia-0',
|
||||
'-sepia-[50%]',
|
||||
'-sepia-[var(--value)]',
|
||||
'sepia-unknown',
|
||||
'filter/foo',
|
||||
'filter-none/foo',
|
||||
'filter-[var(--value)]/foo',
|
||||
'blur-xl/foo',
|
||||
'blur-none/foo',
|
||||
'blur-[4px]/foo',
|
||||
'brightness-50/foo',
|
||||
'brightness-[1.23]/foo',
|
||||
'contrast-50/foo',
|
||||
'contrast-[1.23]/foo',
|
||||
'grayscale/foo',
|
||||
'grayscale-0/foo',
|
||||
'grayscale-[var(--value)]/foo',
|
||||
'hue-rotate-15/foo',
|
||||
'hue-rotate-[45deg]/foo',
|
||||
'invert/foo',
|
||||
'invert-0/foo',
|
||||
'invert-[var(--value)]/foo',
|
||||
'drop-shadow-xl/foo',
|
||||
'drop-shadow-[0_0_red]/foo',
|
||||
'saturate-0/foo',
|
||||
'saturate-[1.75]/foo',
|
||||
'saturate-[var(--value)]/foo',
|
||||
'sepia/foo',
|
||||
'sepia-0/foo',
|
||||
'sepia-[50%]/foo',
|
||||
'sepia-[var(--value)]/foo',
|
||||
],
|
||||
css`
|
||||
@theme {
|
||||
--drop-shadow: 0 1px 1px rgb(0 0 0 / 0.05);
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
),
|
||||
).toEqual('')
|
||||
|
||||
expect(
|
||||
|
|
@ -27243,25 +27266,36 @@ test('text-shadow', async () => {
|
|||
"
|
||||
`)
|
||||
expect(
|
||||
await run([
|
||||
'-shadow-xl',
|
||||
'-shadow-none',
|
||||
'-shadow-red-500',
|
||||
'-shadow-red-500/50',
|
||||
'-shadow-red-500/[0.5]',
|
||||
'-shadow-red-500/[50%]',
|
||||
'-shadow-current',
|
||||
'-shadow-current/50',
|
||||
'-shadow-current/[0.5]',
|
||||
'-shadow-current/[50%]',
|
||||
'-shadow-inherit',
|
||||
'-shadow-transparent',
|
||||
'-shadow-[#0088cc]',
|
||||
'-shadow-[#0088cc]/50',
|
||||
'-shadow-[#0088cc]/[0.5]',
|
||||
'-shadow-[#0088cc]/[50%]',
|
||||
'-shadow-[var(--value)]',
|
||||
]),
|
||||
await run(
|
||||
[
|
||||
'-text-shadow-sm',
|
||||
'-text-shadow-none',
|
||||
'-text-shadow-red-500',
|
||||
'-text-shadow-red-500/50',
|
||||
'-text-shadow-red-500/[0.5]',
|
||||
'-text-shadow-red-500/[50%]',
|
||||
'-text-shadow-current',
|
||||
'-text-shadow-current/50',
|
||||
'-text-shadow-current/[0.5]',
|
||||
'-text-shadow-current/[50%]',
|
||||
'-text-shadow-inherit',
|
||||
'-text-shadow-transparent',
|
||||
'-text-shadow-[#0088cc]',
|
||||
'-text-shadow-[#0088cc]/50',
|
||||
'-text-shadow-[#0088cc]/[0.5]',
|
||||
'-text-shadow-[#0088cc]/[50%]',
|
||||
'-text-shadow-[var(--value)]',
|
||||
'text-shadow/foo',
|
||||
'text-shadow-sm/foo',
|
||||
],
|
||||
css`
|
||||
@theme {
|
||||
--text-shadow: 0 1px 1px rgb(0 0 0 / 0.05);
|
||||
--text-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
|
|
@ -27725,25 +27759,38 @@ test('shadow', async () => {
|
|||
"
|
||||
`)
|
||||
expect(
|
||||
await run([
|
||||
'-shadow-xl',
|
||||
'-shadow-none',
|
||||
'-shadow-red-500',
|
||||
'-shadow-red-500/50',
|
||||
'-shadow-red-500/[0.5]',
|
||||
'-shadow-red-500/[50%]',
|
||||
'-shadow-current',
|
||||
'-shadow-current/50',
|
||||
'-shadow-current/[0.5]',
|
||||
'-shadow-current/[50%]',
|
||||
'-shadow-inherit',
|
||||
'-shadow-transparent',
|
||||
'-shadow-[#0088cc]',
|
||||
'-shadow-[#0088cc]/50',
|
||||
'-shadow-[#0088cc]/[0.5]',
|
||||
'-shadow-[#0088cc]/[50%]',
|
||||
'-shadow-[var(--value)]',
|
||||
]),
|
||||
await run(
|
||||
[
|
||||
'-shadow-sm',
|
||||
'-shadow-none',
|
||||
'-shadow-red-500',
|
||||
'-shadow-red-500/50',
|
||||
'-shadow-red-500/[0.5]',
|
||||
'-shadow-red-500/[50%]',
|
||||
'-shadow-current',
|
||||
'-shadow-current/50',
|
||||
'-shadow-current/[0.5]',
|
||||
'-shadow-current/[50%]',
|
||||
'-shadow-inherit',
|
||||
'-shadow-transparent',
|
||||
'-shadow-[#0088cc]',
|
||||
'-shadow-[#0088cc]/50',
|
||||
'-shadow-[#0088cc]/[0.5]',
|
||||
'-shadow-[#0088cc]/[50%]',
|
||||
'-shadow-[var(--value)]',
|
||||
'shadow/foo',
|
||||
'shadow-sm/foo',
|
||||
'shadow-[10px_10px]/foo',
|
||||
'shadow-[#0088cc]/foo',
|
||||
],
|
||||
css`
|
||||
@theme {
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
|
|
@ -28216,25 +28263,37 @@ test('inset-shadow', async () => {
|
|||
"
|
||||
`)
|
||||
expect(
|
||||
await run([
|
||||
'-inset-shadow-sm',
|
||||
'-inset-shadow-none',
|
||||
'-inset-shadow-red-500',
|
||||
'-inset-shadow-red-500/50',
|
||||
'-inset-shadow-red-500/[0.5]',
|
||||
'-inset-shadow-red-500/[50%]',
|
||||
'-inset-shadow-current',
|
||||
'-inset-shadow-current/50',
|
||||
'-inset-shadow-current/[0.5]',
|
||||
'-inset-shadow-current/[50%]',
|
||||
'-inset-shadow-inherit',
|
||||
'-inset-shadow-transparent',
|
||||
'-inset-shadow-[#0088cc]',
|
||||
'-inset-shadow-[#0088cc]/50',
|
||||
'-inset-shadow-[#0088cc]/[0.5]',
|
||||
'-inset-shadow-[#0088cc]/[50%]',
|
||||
'-inset-shadow-[var(--value)]',
|
||||
]),
|
||||
await run(
|
||||
[
|
||||
'-inset-shadow-sm',
|
||||
'-inset-shadow-none',
|
||||
'-inset-shadow-red-500',
|
||||
'-inset-shadow-red-500/50',
|
||||
'-inset-shadow-red-500/[0.5]',
|
||||
'-inset-shadow-red-500/[50%]',
|
||||
'-inset-shadow-current',
|
||||
'-inset-shadow-current/50',
|
||||
'-inset-shadow-current/[0.5]',
|
||||
'-inset-shadow-current/[50%]',
|
||||
'-inset-shadow-inherit',
|
||||
'-inset-shadow-transparent',
|
||||
'-inset-shadow-[#0088cc]',
|
||||
'-inset-shadow-[#0088cc]/50',
|
||||
'-inset-shadow-[#0088cc]/[0.5]',
|
||||
'-inset-shadow-[#0088cc]/[50%]',
|
||||
'-inset-shadow-[var(--value)]',
|
||||
'inset-shadow/foo',
|
||||
'inset-shadow-sm/foo',
|
||||
'inset-shadow-[10px_10px]/foo',
|
||||
],
|
||||
css`
|
||||
@theme {
|
||||
--inset-shadow: inset 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
--inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05);
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -421,6 +421,13 @@ export function createUtilities(theme: Theme) {
|
|||
desc.themeKeys ?? [],
|
||||
)
|
||||
|
||||
// If the theme value resolved without consuming the modifier, then
|
||||
// the modifier would be silently ignored and `w-4` and `w-4/foo`
|
||||
// would generate the same CSS.
|
||||
//
|
||||
// The modifier would be consumed if it's a fraction, e.g.: `w-1/2`
|
||||
if (value !== null && candidate.modifier && !candidate.value.fraction) return
|
||||
|
||||
// Automatically handle things like `w-1/2` without requiring `1/2` to
|
||||
// exist as a theme value.
|
||||
if (value === null && desc.supportsFractions && candidate.value.fraction) {
|
||||
|
|
@ -3888,6 +3895,10 @@ export function createUtilities(theme: Theme) {
|
|||
}
|
||||
|
||||
{
|
||||
// A modifier is only valid as the opacity of a stroke color. The value
|
||||
// did not resolve to a color, so a modifier would be silently ignored.
|
||||
if (candidate.modifier) return
|
||||
|
||||
let value = theme.resolve(candidate.value.value, ['--stroke-width'])
|
||||
if (value) {
|
||||
return [decl('stroke-width', value)]
|
||||
|
|
@ -4628,6 +4639,7 @@ export function createUtilities(theme: Theme) {
|
|||
let value = theme.get(['--drop-shadow'])
|
||||
let resolved = theme.resolve(null, ['--drop-shadow'])
|
||||
if (value === null || resolved === null) return
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
filterProperties(),
|
||||
|
|
@ -5436,6 +5448,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (!candidate.value) {
|
||||
let value = theme.get(['--text-shadow'])
|
||||
if (value === null) return
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
textShadowProperties(),
|
||||
|
|
@ -5463,6 +5476,8 @@ export function createUtilities(theme: Theme) {
|
|||
]
|
||||
}
|
||||
default: {
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
textShadowProperties(),
|
||||
decl('--tw-text-shadow-alpha', alpha),
|
||||
|
|
@ -5491,6 +5506,8 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
let value = theme.get([`--text-shadow-${candidate.value.value}`])
|
||||
if (value) {
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
textShadowProperties(),
|
||||
decl('--tw-text-shadow-alpha', alpha),
|
||||
|
|
@ -5582,6 +5599,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (!candidate.value) {
|
||||
let value = theme.get(['--shadow'])
|
||||
if (value === null) return
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
|
|
@ -5611,6 +5629,8 @@ export function createUtilities(theme: Theme) {
|
|||
]
|
||||
}
|
||||
default: {
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
decl('--tw-shadow-alpha', alpha),
|
||||
|
|
@ -5644,6 +5664,8 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
let value = theme.get([`--shadow-${candidate.value.value}`])
|
||||
if (value) {
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
decl('--tw-shadow-alpha', alpha),
|
||||
|
|
@ -5708,6 +5730,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (!candidate.value) {
|
||||
let value = theme.get(['--inset-shadow'])
|
||||
if (value === null) return
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
|
|
@ -5737,6 +5760,8 @@ export function createUtilities(theme: Theme) {
|
|||
]
|
||||
}
|
||||
default: {
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
decl('--tw-inset-shadow-alpha', alpha),
|
||||
|
|
@ -5772,6 +5797,8 @@ export function createUtilities(theme: Theme) {
|
|||
let value = theme.get([`--inset-shadow-${candidate.value.value}`])
|
||||
|
||||
if (value) {
|
||||
if (candidate.modifier && !alpha) return
|
||||
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
decl('--tw-inset-shadow-alpha', alpha),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function compareBreakpoints(a: string, z: string, direction: 'asc' | 'des
|
|||
// Compare by bucket name
|
||||
(aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) ||
|
||||
// If bucket names are the same, compare by value
|
||||
(direction === 'asc' ? parseInt(a) - parseInt(z) : parseInt(z) - parseInt(a))
|
||||
(direction === 'asc' ? parseFloat(a) - parseFloat(z) : parseFloat(z) - parseFloat(a))
|
||||
|
||||
// If the groups are the same, and the contents are not numbers, the
|
||||
// `order` will result in `NaN`. In this case, we want to make sorting
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ test('after', async () => {
|
|||
test('first', async () => {
|
||||
expect(await run(['first:flex', 'group-first:flex', 'peer-first:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-first\\:flex:is(:where(.group):first-child *), .peer-first\\:flex:is(:where(.peer):first-child ~ *), .first\\:flex:first-child {
|
||||
:is(:where(.group):first-child .group-first\\:flex), :is(:where(.peer):first-child ~ .peer-first\\:flex), .first\\:flex:first-child {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -197,7 +197,7 @@ test('first', async () => {
|
|||
test('last', async () => {
|
||||
expect(await run(['last:flex', 'group-last:flex', 'peer-last:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-last\\:flex:is(:where(.group):last-child *), .peer-last\\:flex:is(:where(.peer):last-child ~ *), .last\\:flex:last-child {
|
||||
:is(:where(.group):last-child .group-last\\:flex), :is(:where(.peer):last-child ~ .peer-last\\:flex), .last\\:flex:last-child {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -208,7 +208,7 @@ test('last', async () => {
|
|||
test('only', async () => {
|
||||
expect(await run(['only:flex', 'group-only:flex', 'peer-only:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-only\\:flex:is(:where(.group):only-child *), .peer-only\\:flex:is(:where(.peer):only-child ~ *), .only\\:flex:only-child {
|
||||
:is(:where(.group):only-child .group-only\\:flex), :is(:where(.peer):only-child ~ .peer-only\\:flex), .only\\:flex:only-child {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -219,7 +219,7 @@ test('only', async () => {
|
|||
test('odd', async () => {
|
||||
expect(await run(['odd:flex', 'group-odd:flex', 'peer-odd:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-odd\\:flex:is(:where(.group):nth-child(odd) *), .peer-odd\\:flex:is(:where(.peer):nth-child(odd) ~ *), .odd\\:flex:nth-child(odd) {
|
||||
:is(:where(.group):nth-child(odd) .group-odd\\:flex), :is(:where(.peer):nth-child(odd) ~ .peer-odd\\:flex), .odd\\:flex:nth-child(odd) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -230,7 +230,7 @@ test('odd', async () => {
|
|||
test('even', async () => {
|
||||
expect(await run(['even:flex', 'group-even:flex', 'peer-even:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-even\\:flex:is(:where(.group):nth-child(2n) *), .peer-even\\:flex:is(:where(.peer):nth-child(2n) ~ *), .even\\:flex:nth-child(2n) {
|
||||
:is(:where(.group):nth-child(2n) .group-even\\:flex), :is(:where(.peer):nth-child(2n) ~ .peer-even\\:flex), .even\\:flex:nth-child(2n) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -242,7 +242,7 @@ test('first-of-type', async () => {
|
|||
expect(await run(['first-of-type:flex', 'group-first-of-type:flex', 'peer-first-of-type:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-first-of-type\\:flex:is(:where(.group):first-of-type *), .peer-first-of-type\\:flex:is(:where(.peer):first-of-type ~ *), .first-of-type\\:flex:first-of-type {
|
||||
:is(:where(.group):first-of-type .group-first-of-type\\:flex), :is(:where(.peer):first-of-type ~ .peer-first-of-type\\:flex), .first-of-type\\:flex:first-of-type {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -254,7 +254,7 @@ test('last-of-type', async () => {
|
|||
expect(await run(['last-of-type:flex', 'group-last-of-type:flex', 'peer-last-of-type:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-last-of-type\\:flex:is(:where(.group):last-of-type *), .peer-last-of-type\\:flex:is(:where(.peer):last-of-type ~ *), .last-of-type\\:flex:last-of-type {
|
||||
:is(:where(.group):last-of-type .group-last-of-type\\:flex), :is(:where(.peer):last-of-type ~ .peer-last-of-type\\:flex), .last-of-type\\:flex:last-of-type {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -266,7 +266,7 @@ test('only-of-type', async () => {
|
|||
expect(await run(['only-of-type:flex', 'group-only-of-type:flex', 'peer-only-of-type:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-only-of-type\\:flex:is(:where(.group):only-of-type *), .peer-only-of-type\\:flex:is(:where(.peer):only-of-type ~ *), .only-of-type\\:flex:only-of-type {
|
||||
:is(:where(.group):only-of-type .group-only-of-type\\:flex), :is(:where(.peer):only-of-type ~ .peer-only-of-type\\:flex), .only-of-type\\:flex:only-of-type {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -278,7 +278,7 @@ test('visited', async () => {
|
|||
expect(await run(['visited:flex', 'group-visited:flex', 'peer-visited:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-visited\\:flex:is(:where(.group):visited *), .peer-visited\\:flex:is(:where(.peer):visited ~ *), .visited\\:flex:visited {
|
||||
:is(:where(.group):visited .group-visited\\:flex), :is(:where(.peer):visited ~ .peer-visited\\:flex), .visited\\:flex:visited {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -290,7 +290,7 @@ test('target', async () => {
|
|||
expect(await run(['target:flex', 'group-target:flex', 'peer-target:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-target\\:flex:is(:where(.group):target *), .peer-target\\:flex:is(:where(.peer):target ~ *), .target\\:flex:target {
|
||||
:is(:where(.group):target .group-target\\:flex), :is(:where(.peer):target ~ .peer-target\\:flex), .target\\:flex:target {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -302,7 +302,7 @@ test('open', async () => {
|
|||
expect(await run(['open:flex', 'group-open:flex', 'peer-open:flex', 'not-open:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.not-open\\:flex:not(:is([open], :popover-open, :open)), .group-open\\:flex:is(:where(.group):is([open], :popover-open, :open) *), .peer-open\\:flex:is(:where(.peer):is([open], :popover-open, :open) ~ *), .open\\:flex:is([open], :popover-open, :open) {
|
||||
.not-open\\:flex:not(:is([open], :popover-open, :open)), :is(:where(.group):is([open], :popover-open, :open) .group-open\\:flex), :is(:where(.peer):is([open], :popover-open, :open) ~ .peer-open\\:flex), .open\\:flex:is([open], :popover-open, :open) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -314,7 +314,7 @@ test('default', async () => {
|
|||
expect(await run(['default:flex', 'group-default:flex', 'peer-default:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-default\\:flex:is(:where(.group):default *), .peer-default\\:flex:is(:where(.peer):default ~ *), .default\\:flex:default {
|
||||
:is(:where(.group):default .group-default\\:flex), :is(:where(.peer):default ~ .peer-default\\:flex), .default\\:flex:default {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -326,7 +326,7 @@ test('checked', async () => {
|
|||
expect(await run(['checked:flex', 'group-checked:flex', 'peer-checked:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-checked\\:flex:is(:where(.group):checked *), .peer-checked\\:flex:is(:where(.peer):checked ~ *), .checked\\:flex:checked {
|
||||
:is(:where(.group):checked .group-checked\\:flex), :is(:where(.peer):checked ~ .peer-checked\\:flex), .checked\\:flex:checked {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -338,7 +338,7 @@ test('indeterminate', async () => {
|
|||
expect(await run(['indeterminate:flex', 'group-indeterminate:flex', 'peer-indeterminate:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-indeterminate\\:flex:is(:where(.group):indeterminate *), .peer-indeterminate\\:flex:is(:where(.peer):indeterminate ~ *), .indeterminate\\:flex:indeterminate {
|
||||
:is(:where(.group):indeterminate .group-indeterminate\\:flex), :is(:where(.peer):indeterminate ~ .peer-indeterminate\\:flex), .indeterminate\\:flex:indeterminate {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -355,7 +355,7 @@ test('placeholder-shown', async () => {
|
|||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-placeholder-shown\\:flex:is(:where(.group):placeholder-shown *), .peer-placeholder-shown\\:flex:is(:where(.peer):placeholder-shown ~ *), .placeholder-shown\\:flex:placeholder-shown {
|
||||
:is(:where(.group):placeholder-shown .group-placeholder-shown\\:flex), :is(:where(.peer):placeholder-shown ~ .peer-placeholder-shown\\:flex), .placeholder-shown\\:flex:placeholder-shown {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -367,7 +367,7 @@ test('autofill', async () => {
|
|||
expect(await run(['autofill:flex', 'group-autofill:flex', 'peer-autofill:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-autofill\\:flex:is(:where(.group):autofill *), .peer-autofill\\:flex:is(:where(.peer):autofill ~ *) {
|
||||
:is(:where(.group):autofill .group-autofill\\:flex), :is(:where(.peer):autofill ~ .peer-autofill\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -383,7 +383,7 @@ test('optional', async () => {
|
|||
expect(await run(['optional:flex', 'group-optional:flex', 'peer-optional:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-optional\\:flex:is(:where(.group):optional *), .peer-optional\\:flex:is(:where(.peer):optional ~ *), .optional\\:flex:optional {
|
||||
:is(:where(.group):optional .group-optional\\:flex), :is(:where(.peer):optional ~ .peer-optional\\:flex), .optional\\:flex:optional {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -395,7 +395,7 @@ test('required', async () => {
|
|||
expect(await run(['required:flex', 'group-required:flex', 'peer-required:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-required\\:flex:is(:where(.group):required *), .peer-required\\:flex:is(:where(.peer):required ~ *), .required\\:flex:required {
|
||||
:is(:where(.group):required .group-required\\:flex), :is(:where(.peer):required ~ .peer-required\\:flex), .required\\:flex:required {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -406,7 +406,7 @@ test('required', async () => {
|
|||
test('valid', async () => {
|
||||
expect(await run(['valid:flex', 'group-valid:flex', 'peer-valid:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-valid\\:flex:is(:where(.group):valid *), .peer-valid\\:flex:is(:where(.peer):valid ~ *), .valid\\:flex:valid {
|
||||
:is(:where(.group):valid .group-valid\\:flex), :is(:where(.peer):valid ~ .peer-valid\\:flex), .valid\\:flex:valid {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -418,7 +418,7 @@ test('invalid', async () => {
|
|||
expect(await run(['invalid:flex', 'group-invalid:flex', 'peer-invalid:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-invalid\\:flex:is(:where(.group):invalid *), .peer-invalid\\:flex:is(:where(.peer):invalid ~ *), .invalid\\:flex:invalid {
|
||||
:is(:where(.group):invalid .group-invalid\\:flex), :is(:where(.peer):invalid ~ .peer-invalid\\:flex), .invalid\\:flex:invalid {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -430,7 +430,7 @@ test('user-valid', async () => {
|
|||
expect(await run(['user-valid:flex', 'group-user-valid:flex', 'peer-user-valid:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-user-valid\\:flex:is(:where(.group):user-valid *), .peer-user-valid\\:flex:is(:where(.peer):user-valid ~ *) {
|
||||
:is(:where(.group):user-valid .group-user-valid\\:flex), :is(:where(.peer):user-valid ~ .peer-user-valid\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +446,7 @@ test('user-invalid', async () => {
|
|||
expect(await run(['user-invalid:flex', 'group-user-invalid:flex', 'peer-user-invalid:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-user-invalid\\:flex:is(:where(.group):user-invalid *), .peer-user-invalid\\:flex:is(:where(.peer):user-invalid ~ *) {
|
||||
:is(:where(.group):user-invalid .group-user-invalid\\:flex), :is(:where(.peer):user-invalid ~ .peer-user-invalid\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -462,7 +462,7 @@ test('in-range', async () => {
|
|||
expect(await run(['in-range:flex', 'group-in-range:flex', 'peer-in-range:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-in-range\\:flex:is(:where(.group):in-range *), .peer-in-range\\:flex:is(:where(.peer):in-range ~ *), .in-range\\:flex:in-range {
|
||||
:is(:where(.group):in-range .group-in-range\\:flex), :is(:where(.peer):in-range ~ .peer-in-range\\:flex), .in-range\\:flex:in-range {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -474,7 +474,7 @@ test('out-of-range', async () => {
|
|||
expect(await run(['out-of-range:flex', 'group-out-of-range:flex', 'peer-out-of-range:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-out-of-range\\:flex:is(:where(.group):out-of-range *), .peer-out-of-range\\:flex:is(:where(.peer):out-of-range ~ *), .out-of-range\\:flex:out-of-range {
|
||||
:is(:where(.group):out-of-range .group-out-of-range\\:flex), :is(:where(.peer):out-of-range ~ .peer-out-of-range\\:flex), .out-of-range\\:flex:out-of-range {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -486,7 +486,7 @@ test('read-only', async () => {
|
|||
expect(await run(['read-only:flex', 'group-read-only:flex', 'peer-read-only:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-read-only\\:flex:is(:where(.group):read-only *), .peer-read-only\\:flex:is(:where(.peer):read-only ~ *), .read-only\\:flex:read-only {
|
||||
:is(:where(.group):read-only .group-read-only\\:flex), :is(:where(.peer):read-only ~ .peer-read-only\\:flex), .read-only\\:flex:read-only {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -497,7 +497,7 @@ test('read-only', async () => {
|
|||
test('empty', async () => {
|
||||
expect(await run(['empty:flex', 'group-empty:flex', 'peer-empty:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-empty\\:flex:is(:where(.group):empty *), .peer-empty\\:flex:is(:where(.peer):empty ~ *), .empty\\:flex:empty {
|
||||
:is(:where(.group):empty .group-empty\\:flex), :is(:where(.peer):empty ~ .peer-empty\\:flex), .empty\\:flex:empty {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -509,7 +509,7 @@ test('focus-within', async () => {
|
|||
expect(await run(['focus-within:flex', 'group-focus-within:flex', 'peer-focus-within:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-focus-within\\:flex:is(:where(.group):focus-within *), .peer-focus-within\\:flex:is(:where(.peer):focus-within ~ *), .focus-within\\:flex:focus-within {
|
||||
:is(:where(.group):focus-within .group-focus-within\\:flex), :is(:where(.peer):focus-within ~ .peer-focus-within\\:flex), .focus-within\\:flex:focus-within {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -521,7 +521,7 @@ test('hover', async () => {
|
|||
expect(await run(['hover:flex', 'group-hover:flex', 'peer-hover:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:flex:is(:where(.group):hover *), .peer-hover\\:flex:is(:where(.peer):hover ~ *), .hover\\:flex:hover {
|
||||
:is(:where(.group):hover .group-hover\\:flex), :is(:where(.peer):hover ~ .peer-hover\\:flex), .hover\\:flex:hover {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -533,7 +533,7 @@ test('hover', async () => {
|
|||
test('focus', async () => {
|
||||
expect(await run(['focus:flex', 'group-focus:flex', 'peer-focus:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-focus\\:flex:is(:where(.group):focus *), .peer-focus\\:flex:is(:where(.peer):focus ~ *), .focus\\:flex:focus {
|
||||
:is(:where(.group):focus .group-focus\\:flex), :is(:where(.peer):focus ~ .peer-focus\\:flex), .focus\\:flex:focus {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -545,12 +545,12 @@ test('group-hover group-focus sorting', async () => {
|
|||
expect(await run(['group-hover:flex', 'group-focus:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:flex:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-focus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-focus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -558,23 +558,36 @@ test('group-hover group-focus sorting', async () => {
|
|||
expect(await run(['group-focus:flex', 'group-hover:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:flex:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-focus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-focus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('stacking group and peer variants does not duplicate the target selector', async () => {
|
||||
let candidate = Array.from(
|
||||
{ length: 12 },
|
||||
(_, i) => `${i % 2 === 0 ? 'group' : 'peer'}-data-active/name${i}`,
|
||||
).join(':')
|
||||
|
||||
let output = await run([`${candidate}:flex`])
|
||||
expect(output).toContain('display: flex')
|
||||
// Repeating `&` in each variant makes the generated selector grow
|
||||
// exponentially. This many variants should still produce a small rule.
|
||||
expect(output.length).toBeLessThan(2_000)
|
||||
})
|
||||
|
||||
test('focus-visible', async () => {
|
||||
expect(await run(['focus-visible:flex', 'group-focus-visible:flex', 'peer-focus-visible:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-focus-visible\\:flex:is(:where(.group):focus-visible *), .peer-focus-visible\\:flex:is(:where(.peer):focus-visible ~ *), .focus-visible\\:flex:focus-visible {
|
||||
:is(:where(.group):focus-visible .group-focus-visible\\:flex), :is(:where(.peer):focus-visible ~ .peer-focus-visible\\:flex), .focus-visible\\:flex:focus-visible {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -586,7 +599,7 @@ test('active', async () => {
|
|||
expect(await run(['active:flex', 'group-active:flex', 'peer-active:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-active\\:flex:is(:where(.group):active *), .peer-active\\:flex:is(:where(.peer):active ~ *), .active\\:flex:active {
|
||||
:is(:where(.group):active .group-active\\:flex), :is(:where(.peer):active ~ .peer-active\\:flex), .active\\:flex:active {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -598,7 +611,7 @@ test('enabled', async () => {
|
|||
expect(await run(['enabled:flex', 'group-enabled:flex', 'peer-enabled:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-enabled\\:flex:is(:where(.group):enabled *), .peer-enabled\\:flex:is(:where(.peer):enabled ~ *), .enabled\\:flex:enabled {
|
||||
:is(:where(.group):enabled .group-enabled\\:flex), :is(:where(.peer):enabled ~ .peer-enabled\\:flex), .enabled\\:flex:enabled {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -610,7 +623,7 @@ test('disabled', async () => {
|
|||
expect(await run(['disabled:flex', 'group-disabled:flex', 'peer-disabled:flex']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-disabled\\:flex:is(:where(.group):disabled *), .peer-disabled\\:flex:is(:where(.peer):disabled ~ *), .disabled\\:flex:disabled {
|
||||
:is(:where(.group):disabled .group-disabled\\:flex), :is(:where(.peer):disabled ~ .peer-disabled\\:flex), .disabled\\:flex:disabled {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -621,7 +634,7 @@ test('disabled', async () => {
|
|||
test('inert', async () => {
|
||||
expect(await run(['inert:flex', 'group-inert:flex', 'peer-inert:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-inert\\:flex:is(:where(.group):is([inert], [inert] *) *), .peer-inert\\:flex:is(:where(.peer):is([inert], [inert] *) ~ *), .inert\\:flex:is([inert], [inert] *) {
|
||||
:is(:where(.group):is([inert], [inert] *) .group-inert\\:flex), :is(:where(.peer):is([inert], [inert] *) ~ .peer-inert\\:flex), .inert\\:flex:is([inert], [inert] *) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -640,17 +653,17 @@ test('group-[...]', async () => {
|
|||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-\\[\\&_p\\]\\:flex:is(:where(.group) p *), .group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex:is(:where(.group):hover *):is(:where(.group) p *) {
|
||||
:is(:where(.group) p .group-\\[\\&_p\\]\\:flex), :is(:where(.group) p :is(:where(.group):hover .group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group) p *):hover, .hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group) p *) {
|
||||
:is(:where(.group) p .group-\\[\\&_p\\]\\:hover\\:flex):hover, :is(:where(.group) p .hover\\:group-\\[\\&_p\\]\\:flex:hover) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group) p *):hover {
|
||||
:is(:where(.group) p .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover):hover {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -692,22 +705,22 @@ test('group-*', async () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:flex:is(:where(.group):hover *) {
|
||||
:is(:where(.group):hover .group-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-focus\\:flex:is(:where(.group):focus *) {
|
||||
:is(:where(.group):focus .group-focus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.group-focus\\:group-hover\\:flex:is(:where(.group):focus *):is(:where(.group):hover *), .group-hover\\:group-focus\\:flex:is(:where(.group):hover *):is(:where(.group):focus *) {
|
||||
:is(:where(.group):hover :is(:where(.group):focus .group-focus\\:group-hover\\:flex)), :is(:where(.group):focus :is(:where(.group):hover .group-hover\\:group-focus\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) {
|
||||
:is(:is(:where(.group):hover, :where(.group):focus) .group-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -742,12 +755,12 @@ test('peer-[...]', async () => {
|
|||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.peer-\\[\\&_p\\]\\:flex:is(:where(.peer) p ~ *), .peer-\\[\\&\\:hover\\]\\:peer-\\[\\&_p\\]\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer) p ~ *) {
|
||||
:is(:where(.peer) p ~ .peer-\\[\\&_p\\]\\:flex), :is(:where(.peer) p ~ :is(:where(.peer):hover ~ .peer-\\[\\&\\:hover\\]\\:peer-\\[\\&_p\\]\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.hover\\:peer-\\[\\&_p\\]\\:flex:hover:is(:where(.peer) p ~ *), .peer-\\[\\&_p\\]\\:hover\\:flex:is(:where(.peer) p ~ *):hover, .hover\\:peer-\\[\\&_p\\]\\:focus\\:flex:hover:is(:where(.peer) p ~ *):focus {
|
||||
:is(:where(.peer) p ~ .hover\\:peer-\\[\\&_p\\]\\:flex:hover), :is(:where(.peer) p ~ .peer-\\[\\&_p\\]\\:hover\\:flex):hover, :is(:where(.peer) p ~ .hover\\:peer-\\[\\&_p\\]\\:focus\\:flex:hover):focus {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -782,22 +795,22 @@ test('peer-*', async () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media (hover: hover) {
|
||||
.peer-hover\\:flex:is(:where(.peer):hover ~ *) {
|
||||
:is(:where(.peer):hover ~ .peer-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.peer-focus\\:flex:is(:where(.peer):focus ~ *) {
|
||||
:is(:where(.peer):focus ~ .peer-focus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.peer-focus\\:peer-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.peer):hover ~ *), .peer-hover\\:peer-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer):focus ~ *) {
|
||||
:is(:where(.peer):hover ~ :is(:where(.peer):focus ~ .peer-focus\\:peer-hover\\:flex)), :is(:where(.peer):focus ~ :is(:where(.peer):hover ~ .peer-hover\\:peer-focus\\:flex)) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.peer-hocus\\:flex:is(:is(:where(.peer):hover, :where(.peer):focus) ~ *) {
|
||||
:is(:is(:where(.peer):hover, :where(.peer):focus) ~ .peer-hocus\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -1424,6 +1437,80 @@ test('sorting `min` and `max` should sort by unit, then by value, then alphabeti
|
|||
`)
|
||||
})
|
||||
|
||||
test('sorting `min` and `max` should compare decimal values numerically', async () => {
|
||||
expect(
|
||||
await run([
|
||||
'min-[40.5rem]:flex',
|
||||
'min-[40.25rem]:flex',
|
||||
'max-[40.25rem]:flex',
|
||||
'max-[40.5rem]:flex',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@media not all and (min-width: 40.5rem) {
|
||||
.max-\\[40\\.5rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media not all and (min-width: 40.25rem) {
|
||||
.max-\\[40\\.25rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 40.25rem) {
|
||||
.min-\\[40\\.25rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 40.5rem) {
|
||||
.min-\\[40\\.5rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('sorting `@min` and `@max` should compare decimal values numerically', async () => {
|
||||
expect(
|
||||
await run([
|
||||
'@min-[40.5rem]:flex',
|
||||
'@min-[40.25rem]:flex',
|
||||
'@max-[40.25rem]:flex',
|
||||
'@max-[40.5rem]:flex',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@container not (min-width: 40.5rem) {
|
||||
.\\@max-\\[40\\.5rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@container not (min-width: 40.25rem) {
|
||||
.\\@max-\\[40\\.25rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 40.25rem) {
|
||||
.\\@min-\\[40\\.25rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 40.5rem) {
|
||||
.\\@min-\\[40\\.5rem\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('supports', async () => {
|
||||
expect(
|
||||
await run([
|
||||
|
|
@ -1435,6 +1522,17 @@ test('supports', async () => {
|
|||
'supports-[font-tech(color-COLRv1)]:flex',
|
||||
'supports-[var(--test)]:flex',
|
||||
'supports-[--test]:flex',
|
||||
|
||||
// Only top-level and/or/not should have spaces around them. We should
|
||||
// ignore and/or/not inside of `selector(…)`
|
||||
'supports-[selector(a:not(.foo))]:flex',
|
||||
'supports-[selector(a:is(.and,.or))]:flex',
|
||||
'supports-[(display:grid)or(display:flex)]:grid',
|
||||
'supports-[not(display:grid)]:flex',
|
||||
'supports-[foo-not(display:grid)]:flex',
|
||||
'supports-[selector([data-foo="("])or(display:grid)]:flex',
|
||||
'supports-[selector(.foo\\(bar)or(display:grid)]:flex',
|
||||
'supports-[((display:grid)or(display:flex))]:grid',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1444,12 +1542,24 @@ test('supports', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
@supports (display: grid) or (display: flex) {
|
||||
.supports-\\[\\(\\(display\\:grid\\)or\\(display\\:flex\\)\\)\\]\\:grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (display: grid) and font-format(opentype) {
|
||||
.supports-\\[\\(display\\:grid\\)_and_font-format\\(opentype\\)\\]\\:grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (display: grid) or (display: flex) {
|
||||
.supports-\\[\\(display\\:grid\\)or\\(display\\:flex\\)\\]\\:grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (--test: var(--tw)) {
|
||||
.supports-\\[--test\\]\\:flex {
|
||||
display: flex;
|
||||
|
|
@ -1474,12 +1584,48 @@ test('supports', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
@supports foo-not(display:grid) {
|
||||
.supports-\\[foo-not\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (display: grid) {
|
||||
.supports-\\[not\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(.foo\\(bar) or (display: grid) {
|
||||
.supports-\\[selector\\(\\.foo\\\\\\(bar\\)or\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(A > B) {
|
||||
.supports-\\[selector\\(A_\\>_B\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector([data-foo="("]) or (display: grid) {
|
||||
.supports-\\[selector\\(\\[data-foo\\=\\"\\(\\"\\]\\)or\\(display\\:grid\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(a:is(.and,.or)) {
|
||||
.supports-\\[selector\\(a\\:is\\(\\.and\\,\\.or\\)\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(a:not(.foo)) {
|
||||
.supports-\\[selector\\(a\\:not\\(\\.foo\\)\\)\\]\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@supports var(--test) {
|
||||
.supports-\\[var\\(--test\\)\\]\\:flex {
|
||||
display: flex;
|
||||
|
|
@ -1764,7 +1910,7 @@ test('not', async () => {
|
|||
}
|
||||
}
|
||||
|
||||
.group-not-checked\\:flex:is(:where(.group):not(:checked) *), .group-not-hocus\\:flex:is(:where(.group):not(:hover, :focus) *), .group-not-hocus\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:hover, :focus) *), .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *), .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *), .peer-not-checked\\:flex:is(:where(.peer):not(:checked) ~ *), .peer-not-hocus\\:flex:is(:where(.peer):not(:hover, :focus) ~ *), .peer-not-hocus\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):not(:hover, :focus) ~ *), .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *), .peer-not-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):not(:checked) ~ *) {
|
||||
:is(:where(.group):not(:checked) .group-not-checked\\:flex), :is(:where(.group):not(:hover, :focus) .group-not-hocus\\:flex), :is(:where(.group\\/parent-name):not(:hover, :focus) .group-not-hocus\\/parent-name\\:flex), :is(:where(.group):not(:checked) .group-not-\\[\\:checked\\]\\:flex), :is(:where(.group\\/parent-name):not(:checked) .group-not-\\[\\:checked\\]\\/parent-name\\:flex), :is(:where(.peer):not(:checked) ~ .peer-not-checked\\:flex), :is(:where(.peer):not(:hover, :focus) ~ .peer-not-hocus\\:flex), :is(:where(.peer\\/sibling-name):not(:hover, :focus) ~ .peer-not-hocus\\/sibling-name\\:flex), :is(:where(.peer):not(:checked) ~ .peer-not-\\[\\:checked\\]\\:flex), :is(:where(.peer\\/sibling-name):not(:checked) ~ .peer-not-\\[\\:checked\\]\\/sibling-name\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -1993,7 +2139,7 @@ test('has', async () => {
|
|||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-has-checked\\:flex:is(:where(.group):has(:checked) *), .group-has-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *), .group-has-hocus\\:flex:is(:where(.group):has(:hover, :focus) *), .group-has-hocus\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:hover, :focus) *), .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *), .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *), .group-has-\\[\\&\\>img\\]\\:flex:is(:where(.group):has(* > img) *), .group-has-\\[\\&\\>img\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(* > img) *), .group-has-\\[\\+img\\]\\:flex:is(:where(.group):has( + img) *), .group-has-\\[\\>img\\]\\:flex:is(:where(.group):has( > img) *), .group-has-\\[\\>img\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has( > img) *), .group-has-\\[\\~img\\]\\:flex:is(:where(.group):has( ~ img) *), .peer-has-checked\\:flex:is(:where(.peer):has(:checked) ~ *), .peer-has-checked\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *), .peer-has-hocus\\:flex:is(:where(.peer):has(:hover, :focus) ~ *), .peer-has-hocus\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:hover, :focus) ~ *), .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *), .peer-has-\\[\\:checked\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(:checked) ~ *), .peer-has-\\[\\&\\>img\\]\\:flex:is(:where(.peer):has(* > img) ~ *), .peer-has-\\[\\&\\>img\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has(* > img) ~ *), .peer-has-\\[\\+img\\]\\:flex:is(:where(.peer):has( + img) ~ *), .peer-has-\\[\\>img\\]\\:flex:is(:where(.peer):has( > img) ~ *), .peer-has-\\[\\>img\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name):has( > img) ~ *), .peer-has-\\[\\~img\\]\\:flex:is(:where(.peer):has( ~ img) ~ *), .has-checked\\:flex:has(:checked), .has-hocus\\:flex:has(:hover, :focus), .has-\\[\\:checked\\]\\:flex:has(:checked), .has-\\[\\&\\>img\\]\\:flex:has(* > img), .has-\\[\\+img\\]\\:flex:has( + img), .has-\\[\\>img\\]\\:flex:has( > img), .has-\\[\\~img\\]\\:flex:has( ~ img) {
|
||||
:is(:where(.group):has(:checked) .group-has-checked\\:flex), :is(:where(.group\\/parent-name):has(:checked) .group-has-checked\\/parent-name\\:flex), :is(:where(.group):has(:hover, :focus) .group-has-hocus\\:flex), :is(:where(.group\\/parent-name):has(:hover, :focus) .group-has-hocus\\/parent-name\\:flex), :is(:where(.group):has(:checked) .group-has-\\[\\:checked\\]\\:flex), :is(:where(.group\\/parent-name):has(:checked) .group-has-\\[\\:checked\\]\\/parent-name\\:flex), :is(:where(.group):has(* > img) .group-has-\\[\\&\\>img\\]\\:flex), :is(:where(.group\\/parent-name):has(* > img) .group-has-\\[\\&\\>img\\]\\/parent-name\\:flex), :is(:where(.group):has( + img) .group-has-\\[\\+img\\]\\:flex), :is(:where(.group):has( > img) .group-has-\\[\\>img\\]\\:flex), :is(:where(.group\\/parent-name):has( > img) .group-has-\\[\\>img\\]\\/parent-name\\:flex), :is(:where(.group):has( ~ img) .group-has-\\[\\~img\\]\\:flex), :is(:where(.peer):has(:checked) ~ .peer-has-checked\\:flex), :is(:where(.peer\\/sibling-name):has(:checked) ~ .peer-has-checked\\/sibling-name\\:flex), :is(:where(.peer):has(:hover, :focus) ~ .peer-has-hocus\\:flex), :is(:where(.peer\\/sibling-name):has(:hover, :focus) ~ .peer-has-hocus\\/sibling-name\\:flex), :is(:where(.peer):has(:checked) ~ .peer-has-\\[\\:checked\\]\\:flex), :is(:where(.peer\\/sibling-name):has(:checked) ~ .peer-has-\\[\\:checked\\]\\/sibling-name\\:flex), :is(:where(.peer):has(* > img) ~ .peer-has-\\[\\&\\>img\\]\\:flex), :is(:where(.peer\\/sibling-name):has(* > img) ~ .peer-has-\\[\\&\\>img\\]\\/sibling-name\\:flex), :is(:where(.peer):has( + img) ~ .peer-has-\\[\\+img\\]\\:flex), :is(:where(.peer):has( > img) ~ .peer-has-\\[\\>img\\]\\:flex), :is(:where(.peer\\/sibling-name):has( > img) ~ .peer-has-\\[\\>img\\]\\/sibling-name\\:flex), :is(:where(.peer):has( ~ img) ~ .peer-has-\\[\\~img\\]\\:flex), .has-checked\\:flex:has(:checked), .has-hocus\\:flex:has(:hover, :focus), .has-\\[\\:checked\\]\\:flex:has(:checked), .has-\\[\\&\\>img\\]\\:flex:has(* > img), .has-\\[\\+img\\]\\:flex:has( + img), .has-\\[\\>img\\]\\:flex:has( > img), .has-\\[\\~img\\]\\:flex:has( ~ img) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -2046,7 +2192,7 @@ test('aria', async () => {
|
|||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-aria-checked\\:flex:is(:where(.group)[aria-checked="true"] *), .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *), .group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *), .group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *), .group-aria-\\[valuenow\\=1\\]\\:flex:is(:where(.group)[aria-valuenow="1"] *), .group-aria-\\[valuenow\\=1\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-valuenow="1"] *), .peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *), .peer-aria-checked\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[aria-checked="true"] ~ *), .peer-aria-\\[modal\\]\\:flex:is(:where(.peer)[aria-modal] ~ *), .peer-aria-\\[modal\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[aria-modal] ~ *), .peer-aria-\\[valuenow\\=1\\]\\:flex:is(:where(.peer)[aria-valuenow="1"] ~ *), .peer-aria-\\[valuenow\\=1\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[aria-valuenow="1"] ~ *), .aria-checked\\:flex[aria-checked="true"], .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"], .aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"], .aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] {
|
||||
:is(:where(.group)[aria-checked="true"] .group-aria-checked\\:flex), :is(:where(.group\\/parent-name)[aria-checked="true"] .group-aria-checked\\/parent-name\\:flex), :is(:where(.group)[aria-modal] .group-aria-\\[modal\\]\\:flex), :is(:where(.group\\/parent-name)[aria-modal] .group-aria-\\[modal\\]\\/parent-name\\:flex), :is(:where(.group)[aria-valuenow="1"] .group-aria-\\[valuenow\\=1\\]\\:flex), :is(:where(.group\\/parent-name)[aria-valuenow="1"] .group-aria-\\[valuenow\\=1\\]\\/parent-name\\:flex), :is(:where(.peer)[aria-checked="true"] ~ .peer-aria-checked\\:flex), :is(:where(.peer\\/sibling-name)[aria-checked="true"] ~ .peer-aria-checked\\/sibling-name\\:flex), :is(:where(.peer)[aria-modal] ~ .peer-aria-\\[modal\\]\\:flex), :is(:where(.peer\\/sibling-name)[aria-modal] ~ .peer-aria-\\[modal\\]\\/sibling-name\\:flex), :is(:where(.peer)[aria-valuenow="1"] ~ .peer-aria-\\[valuenow\\=1\\]\\:flex), :is(:where(.peer\\/sibling-name)[aria-valuenow="1"] ~ .peer-aria-\\[valuenow\\=1\\]\\/sibling-name\\:flex), .aria-checked\\:flex[aria-checked="true"], .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"], .aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"], .aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -2085,7 +2231,7 @@ test('data', async () => {
|
|||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *), .group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *), .group-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo$="bar" i] *), .group-data-\\[foo\\$\\=bar_baz_i\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo$="bar baz" i] *), .group-data-\\[foo\\=1\\]\\:flex:is(:where(.group)[data-foo="1"] *), .group-data-\\[foo\\=1\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo="1"] *), .group-data-\\[foo\\=bar\\ baz\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-foo="bar baz"] *), .peer-data-\\[disabled\\]\\:flex:is(:where(.peer)[data-disabled] ~ *), .peer-data-\\[disabled\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-disabled] ~ *), .peer-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo$="bar" i] ~ *), .peer-data-\\[foo\\$\\=bar_baz_i\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo$="bar baz" i] ~ *), .peer-data-\\[foo\\=1\\]\\:flex:is(:where(.peer)[data-foo="1"] ~ *), .peer-data-\\[foo\\=1\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo="1"] ~ *), .peer-data-\\[foo\\=bar\\ baz\\]\\/sibling-name\\:flex:is(:where(.peer\\/sibling-name)[data-foo="bar baz"] ~ *), .data-disabled\\:flex[data-disabled], .data-\\[foo\\$\\=\\'bar\\'_i\\]\\:flex[data-foo$="bar" i], .data-\\[foo\\$\\=bar_baz_i\\]\\:flex[data-foo$="bar baz" i], .data-\\[foo\\=1\\]\\:flex[data-foo="1"], .data-\\[foo\\=bar_baz\\]\\:flex[data-foo="bar baz"], .data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"], .data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"], .data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="], .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] {
|
||||
:is(:where(.group)[data-disabled] .group-data-\\[disabled\\]\\:flex), :is(:where(.group\\/parent-name)[data-disabled] .group-data-\\[disabled\\]\\/parent-name\\:flex), :is(:where(.group\\/parent-name)[data-foo$="bar" i] .group-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/parent-name\\:flex), :is(:where(.group\\/parent-name)[data-foo$="bar baz" i] .group-data-\\[foo\\$\\=bar_baz_i\\]\\/parent-name\\:flex), :is(:where(.group)[data-foo="1"] .group-data-\\[foo\\=1\\]\\:flex), :is(:where(.group\\/parent-name)[data-foo="1"] .group-data-\\[foo\\=1\\]\\/parent-name\\:flex), :is(:where(.group\\/parent-name)[data-foo="bar baz"] .group-data-\\[foo\\=bar\\ baz\\]\\/parent-name\\:flex), :is(:where(.peer)[data-disabled] ~ .peer-data-\\[disabled\\]\\:flex), :is(:where(.peer\\/sibling-name)[data-disabled] ~ .peer-data-\\[disabled\\]\\/sibling-name\\:flex), :is(:where(.peer\\/sibling-name)[data-foo$="bar" i] ~ .peer-data-\\[foo\\$\\=\\'bar\\'_i\\]\\/sibling-name\\:flex), :is(:where(.peer\\/sibling-name)[data-foo$="bar baz" i] ~ .peer-data-\\[foo\\$\\=bar_baz_i\\]\\/sibling-name\\:flex), :is(:where(.peer)[data-foo="1"] ~ .peer-data-\\[foo\\=1\\]\\:flex), :is(:where(.peer\\/sibling-name)[data-foo="1"] ~ .peer-data-\\[foo\\=1\\]\\/sibling-name\\:flex), :is(:where(.peer\\/sibling-name)[data-foo="bar baz"] ~ .peer-data-\\[foo\\=bar\\ baz\\]\\/sibling-name\\:flex), .data-disabled\\:flex[data-disabled], .data-\\[foo\\$\\=\\'bar\\'_i\\]\\:flex[data-foo$="bar" i], .data-\\[foo\\$\\=bar_baz_i\\]\\:flex[data-foo$="bar baz" i], .data-\\[foo\\=1\\]\\:flex[data-foo="1"], .data-\\[foo\\=bar_baz\\]\\:flex[data-foo="bar baz"], .data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"], .data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"], .data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="], .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
@ -2584,7 +2730,7 @@ test('variant order', async () => {
|
|||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.group-hover\\:flex:is(:where(.group):hover *), .peer-hover\\:flex:is(:where(.peer):hover ~ *) {
|
||||
:is(:where(.group):hover .group-hover\\:flex), :is(:where(.peer):hover ~ .peer-hover\\:flex) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -2857,7 +3003,7 @@ test('move modifier of compound variant to sub-variant if its also a compound va
|
|||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.not-group-focus\\/name\\:flex:not(:is(:where(.group\\/name):focus *)), .group-peer-focus\\/name\\:flex:is(:where(.group\\/name):is(:where(.peer):focus ~ *) *), :where(:is(:where(.group\\/name):focus *)) .in-group-focus\\/name\\:flex, .has-group-focus\\/name\\:flex:has(:is(:where(.group\\/name):focus *)) {
|
||||
.not-group-focus\\/name\\:flex:not(:is(:where(.group\\/name):focus *)), :is(:is(:where(.peer):focus ~ :where(.group\\/name)) .group-peer-focus\\/name\\:flex), :where(:is(:where(.group\\/name):focus *)) .in-group-focus\\/name\\:flex, .has-group-focus\\/name\\:flex:has(:is(:where(.group\\/name):focus *)) {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
|
|
|
|||
|
|
@ -610,7 +610,13 @@ export function createVariants(theme: Theme): Variants {
|
|||
selector = `:is(${selector})`
|
||||
}
|
||||
|
||||
node.selector = `&:is(${selector} *)`
|
||||
// Put the target inside `:is` so browsers can limit style invalidation to
|
||||
// it when the group changes. Keep `&` instead of the candidate's class so
|
||||
// this also works with `@apply` and variants that change the target.
|
||||
//
|
||||
// The `:is` wrapper also keeps `has-group-*` from anchoring the group
|
||||
// itself to the element carrying the utility.
|
||||
node.selector = `:is(${selector} &)`
|
||||
|
||||
// Track that the variant was actually applied
|
||||
didApply = true
|
||||
|
|
@ -651,7 +657,7 @@ export function createVariants(theme: Theme): Variants {
|
|||
|
||||
// For most variants we rely entirely on CSS nesting to build-up the final
|
||||
// selector, but there is no way to use CSS nesting to make `&` refer to
|
||||
// just the `.group` class the way we'd need to for these variants, so we
|
||||
// just the `.peer` class the way we'd need to for these variants, so we
|
||||
// need to replace it in the selector ourselves.
|
||||
let selector = node.selector.replaceAll('&', variantSelector)
|
||||
|
||||
|
|
@ -662,7 +668,8 @@ export function createVariants(theme: Theme): Variants {
|
|||
selector = `:is(${selector})`
|
||||
}
|
||||
|
||||
node.selector = `&:is(${selector} ~ *)`
|
||||
// As with `group`, constrain the target without repeating its selector.
|
||||
node.selector = `:is(${selector} ~ &)`
|
||||
|
||||
// Track that the variant was actually applied
|
||||
didApply = true
|
||||
|
|
@ -938,7 +945,22 @@ export function createVariants(theme: Theme): Variants {
|
|||
if (/^[\w-]*\s*\(/.test(value)) {
|
||||
// Chrome has a bug where `(condition1)or(condition2)` is not valid, but
|
||||
// `(condition1) or (condition2)` is supported.
|
||||
let query = value.replace(/\b(and|or|not)\b/g, ' $1 ')
|
||||
let changed = false
|
||||
let ast = ValueParser.parse(value)
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'function') return
|
||||
|
||||
// Leave selectors as-is, they could contain `selector(a:not(b))`, and
|
||||
// in this case we don't want the space around the `not`.
|
||||
if (node.value === 'selector') return WalkAction.Skip
|
||||
|
||||
if (node.value === 'and' || node.value === 'or' || node.value === 'not') {
|
||||
changed = true
|
||||
node.value = ` ${node.value} `
|
||||
}
|
||||
})
|
||||
|
||||
let query = changed ? ValueParser.toCss(ast) : value
|
||||
|
||||
ruleNode.nodes = [atRule('@supports', query, ruleNode.nodes)]
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2387,6 +2387,120 @@ test('shadow DOM has access to variables', async ({ page }) => {
|
|||
}
|
||||
}
|
||||
|
||||
for (let kind of ['group', 'peer']) {
|
||||
test(`${kind}-* variants`, async ({ page }) => {
|
||||
let targets = html`
|
||||
<div id="target" class="${kind}-focus:z-10"></div>
|
||||
<div id="applied" class="applied"></div>
|
||||
`
|
||||
|
||||
let { getPropertyValue } = await render(
|
||||
page,
|
||||
kind === 'group'
|
||||
? html`<div id="trigger" tabindex="0" class="group owner">${targets}</div>`
|
||||
: html`<div class="owner">
|
||||
<div id="trigger" tabindex="0" class="peer"></div>
|
||||
${targets}
|
||||
</div>`,
|
||||
css`
|
||||
.owner > .applied {
|
||||
@apply ${kind}-focus:z-20;
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('auto')
|
||||
expect(await getPropertyValue('#applied', 'z-index')).toEqual('auto')
|
||||
|
||||
await page.locator('#trigger').focus()
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('10')
|
||||
expect(await getPropertyValue('#applied', 'z-index')).toEqual('20')
|
||||
|
||||
await page.locator('#trigger').blur()
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('auto')
|
||||
expect(await getPropertyValue('#applied', 'z-index')).toEqual('auto')
|
||||
})
|
||||
}
|
||||
|
||||
test('group-* variants do not change specificity', async ({ page }) => {
|
||||
let { getPropertyValue } = await render(
|
||||
page,
|
||||
html`
|
||||
<div id="trigger" tabindex="0" class="group">
|
||||
<div id="target" class="target group-focus:z-10"></div>
|
||||
</div>
|
||||
`,
|
||||
css`
|
||||
@layer utilities {
|
||||
.target {
|
||||
z-index: 20;
|
||||
}
|
||||
.target.equal {
|
||||
z-index: 30;
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await page.locator('#trigger').focus()
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('10')
|
||||
|
||||
await page.locator('#target').evaluate((element) => element.classList.add('equal'))
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('30')
|
||||
})
|
||||
|
||||
test('stacked group-* variants match regardless of the order of the groups', async ({ page }) => {
|
||||
for (let [outer, inner] of [
|
||||
['a', 'b'],
|
||||
['b', 'a'],
|
||||
]) {
|
||||
let { getPropertyValue } = await render(
|
||||
page,
|
||||
html`
|
||||
<div class="group/${outer}" data-active>
|
||||
<div id="inner" class="group/${inner}" data-active>
|
||||
<div id="target" class="group-data-active/a:group-data-active/b:z-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('10')
|
||||
|
||||
await page.locator('#inner').evaluate((element) => element.removeAttribute('data-active'))
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('auto')
|
||||
}
|
||||
})
|
||||
|
||||
test('compound group-* and peer-* variants', async ({ page }) => {
|
||||
let { getPropertyValue } = await render(
|
||||
page,
|
||||
html`
|
||||
<div id="outer" tabindex="0" class="group">
|
||||
<div id="peer" tabindex="0" class="peer"></div>
|
||||
<div id="middle" class="group peer-group-focus:z-20">
|
||||
<div id="target" class="group-peer-focus:z-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
|
||||
await page.locator('#peer').focus()
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('10')
|
||||
expect(await getPropertyValue('#middle', 'z-index')).toEqual('auto')
|
||||
|
||||
await page.locator('#outer').focus()
|
||||
|
||||
expect(await getPropertyValue('#target', 'z-index')).toEqual('auto')
|
||||
expect(await getPropertyValue('#middle', 'z-index')).toEqual('20')
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue