Merge branch 'tailwindlabs:main' into riscv64
This commit is contained in:
commit
307ee67114
13 changed files with 863 additions and 229 deletions
|
|
@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198))
|
||||
- Don't crash in the Ruby or Vue preprocessors when scanning files containing invalid UTF-8 bytes ([#19588](https://github.com/tailwindlabs/tailwindcss/pull/19588))
|
||||
- Allow `@variant` to be used inside `addBase` ([#19480](https://github.com/tailwindlabs/tailwindcss/pull/19480))
|
||||
- Ensure `@source` globs with symlinks are preserved ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Ensure later `@source` rules can re-include files excluded by earlier `@source not` rules ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Upgrade: don't migrate empty class rules to invalid `@utility` rules ([#20205](https://github.com/tailwindlabs/tailwindcss/pull/20205))
|
||||
- Ensure transitions between `inset-shadow-none` and other inset shadows work correctly ([#20208](https://github.com/tailwindlabs/tailwindcss/pull/20208))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ use fxhash::{FxHashMap, FxHashSet};
|
|||
use ignore::{gitignore::GitignoreBuilder, WalkBuilder};
|
||||
use init_tracing::{init_tracing, SHOULD_TRACE};
|
||||
use rayon::prelude::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
|
|
@ -637,7 +636,16 @@ fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
|||
fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
||||
let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default();
|
||||
let mut first_root: Option<&PathBuf> = None;
|
||||
let mut ignores: BTreeMap<&PathBuf, BTreeSet<String>> = Default::default();
|
||||
|
||||
let mut ignores: Vec<(&PathBuf, Vec<String>)> = Default::default();
|
||||
let mut emit = |base, pattern| match ignores.last_mut() {
|
||||
Some((prev_base, patterns)) if *prev_base == base => {
|
||||
patterns.push(pattern);
|
||||
}
|
||||
_ => {
|
||||
ignores.push((base, vec![pattern]));
|
||||
}
|
||||
};
|
||||
|
||||
for source in sources.iter() {
|
||||
match source {
|
||||
|
|
@ -649,7 +657,7 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
}
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let mut pattern = pattern.to_string();
|
||||
let pattern = pattern.to_owned();
|
||||
|
||||
if first_root.is_none() {
|
||||
first_root = Some(base);
|
||||
|
|
@ -658,35 +666,19 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
|
||||
if !pattern.contains("**") {
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
|
||||
// Specific patterns should take precedence even over git-ignored files:
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!{}", pattern));
|
||||
emit(base, format!("!{}", pattern));
|
||||
} else {
|
||||
// Assumption: the pattern we receive will already be brace expanded. So
|
||||
// `*.{html,jsx}` will result in two separate patterns: `*.html` and `*.jsx`.
|
||||
if let Some(extension) = Path::new(&pattern).extension() {
|
||||
// Extend auto source detection to include the extension
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!*.{}", extension.to_string_lossy()));
|
||||
emit(base, format!("!*.{}", extension.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
SourceEntry::Ignored { base, pattern } => {
|
||||
let mut pattern = pattern.to_string();
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
ignores.entry(base).or_default().insert(pattern);
|
||||
emit(base, pattern.to_owned());
|
||||
}
|
||||
SourceEntry::External { base } => {
|
||||
if first_root.is_none() {
|
||||
|
|
@ -696,16 +688,10 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
|
||||
// External sources should take precedence even over git-ignored files:
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!{}", "/**/*"));
|
||||
emit(base, format!("!{}", "/**/*"));
|
||||
|
||||
// External sources should still disallow binary extensions:
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(BINARY_EXTENSIONS_GLOB.clone());
|
||||
emit(base, BINARY_EXTENSIONS_GLOB.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::glob::split_pattern;
|
||||
use crate::GlobEntry;
|
||||
use bexpand::Expression;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use tracing::{event, Level};
|
||||
|
||||
use super::auto_source_detection::IGNORED_CONTENT_DIRS;
|
||||
|
|
@ -102,72 +101,215 @@ impl PublicSourceEntry {
|
|||
/// resolved path.
|
||||
pub fn optimize(&mut self) {
|
||||
// Resolve base path immediately
|
||||
let Ok(base) = dunce::canonicalize(&self.base) else {
|
||||
let Ok(mut base) = dunce::canonicalize(&self.base) else {
|
||||
event!(Level::ERROR, "Failed to resolve base: {:?}", self.base);
|
||||
return;
|
||||
};
|
||||
self.base = base.to_string_lossy().to_string();
|
||||
|
||||
// No dynamic part, figure out if we are dealing with a file or a directory.
|
||||
if !self.pattern.contains('*') {
|
||||
let combined_path = if self.pattern.starts_with("/") {
|
||||
PathBuf::from(&self.pattern)
|
||||
} else {
|
||||
PathBuf::from(&self.base).join(&self.pattern)
|
||||
};
|
||||
|
||||
match dunce::canonicalize(combined_path) {
|
||||
Ok(resolved_path) if resolved_path.is_dir() => {
|
||||
self.base = resolved_path.to_string_lossy().to_string();
|
||||
self.pattern = "**/*".to_owned();
|
||||
}
|
||||
Ok(resolved_path) if resolved_path.is_file() => {
|
||||
self.base = resolved_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
// Ensure leading slash, otherwise it will match against all files in all folders/
|
||||
self.pattern =
|
||||
format!("/{}", resolved_path.file_name().unwrap().to_string_lossy());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
let mut new_pattern = PathBuf::new();
|
||||
enum ComponentStage {
|
||||
Base,
|
||||
Pattern,
|
||||
}
|
||||
let mut stage = ComponentStage::Base;
|
||||
|
||||
// Contains dynamic part
|
||||
let (static_part, dynamic_part) = split_pattern(&self.pattern);
|
||||
let mut components = Path::new(&self.pattern).components().peekable();
|
||||
while let Some(component) = components.next() {
|
||||
match stage {
|
||||
ComponentStage::Base => {
|
||||
match component {
|
||||
// Ignore the current dir, e.g. `.`
|
||||
Component::CurDir => {}
|
||||
|
||||
let base: PathBuf = self.base.clone().into();
|
||||
let base = match static_part {
|
||||
Some(static_part) => {
|
||||
// TODO: If the base does not exist on disk, try removing the last slash and try
|
||||
// again.
|
||||
match dunce::canonicalize(base.join(static_part)) {
|
||||
Ok(base) => base,
|
||||
Err(err) => {
|
||||
event!(tracing::Level::ERROR, "Failed to resolve glob: {:?}", err);
|
||||
return;
|
||||
// Go up a directory, e.g. `..`
|
||||
Component::ParentDir => {
|
||||
base.pop();
|
||||
}
|
||||
|
||||
// Once we hit a component that contains a wildcard character, then we
|
||||
// can't change the base anymore and we must move to the pattern part.
|
||||
Component::Normal(part) if part.to_string_lossy().contains("*") => {
|
||||
new_pattern.push(component);
|
||||
stage = ComponentStage::Pattern;
|
||||
}
|
||||
|
||||
// File or folder, but not the last component
|
||||
Component::Normal(part) if components.peek().is_some() => {
|
||||
base.push(part);
|
||||
}
|
||||
|
||||
// Last file or folder. If it's a folder, we move it to the base,
|
||||
// otherwise we move it to the pattern.
|
||||
Component::Normal(part) => {
|
||||
let full_path = base.join(part);
|
||||
if full_path.is_dir() {
|
||||
base.push(part);
|
||||
} else {
|
||||
new_pattern.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// When we're dealing with an absolute path, then we have to bypass the
|
||||
// `base` entirely.
|
||||
Component::Prefix(_) => {
|
||||
base.clear();
|
||||
base.push(component);
|
||||
}
|
||||
Component::RootDir => {
|
||||
#[cfg(not(windows))]
|
||||
base.clear();
|
||||
base.push(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => base,
|
||||
};
|
||||
|
||||
let pattern = match dynamic_part {
|
||||
Some(dynamic_part) => dynamic_part,
|
||||
None => {
|
||||
if base.is_dir() {
|
||||
"**/*".to_owned()
|
||||
} else {
|
||||
"".to_owned()
|
||||
ComponentStage::Pattern => {
|
||||
new_pattern.push(component);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
self.base = base.to_string_lossy().to_string();
|
||||
self.pattern = pattern;
|
||||
self.pattern = path_to_posix_string(&new_pattern);
|
||||
|
||||
// Ensure we have `**/*` when the base is a folder and we don't have a pattern at all
|
||||
if self.pattern == "" {
|
||||
self.pattern = "/**/*".to_owned();
|
||||
}
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
else if !self.pattern.starts_with("/") {
|
||||
self.pattern = format!("/{}", self.pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_posix_string(path: &Path) -> String {
|
||||
let mut parts = Vec::new();
|
||||
let mut is_rooted = false;
|
||||
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(prefix) => {
|
||||
parts.push(prefix.as_os_str().to_string_lossy().to_string());
|
||||
}
|
||||
Component::RootDir => {
|
||||
is_rooted = true;
|
||||
if parts.is_empty() {
|
||||
parts.push(String::new());
|
||||
}
|
||||
}
|
||||
Component::CurDir => {
|
||||
parts.push(".".to_string());
|
||||
}
|
||||
Component::ParentDir => {
|
||||
parts.push("..".to_string());
|
||||
}
|
||||
Component::Normal(part) => {
|
||||
parts.push(part.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = parts.join("/");
|
||||
if result.is_empty() && is_rooted {
|
||||
"/".to_string()
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_relative_paths() {
|
||||
let path = PathBuf::from("src").join("**").join("*.html");
|
||||
|
||||
assert_eq!(path_to_posix_string(&path), "src/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_rooted_paths() {
|
||||
let path = PathBuf::from(std::path::MAIN_SEPARATOR.to_string())
|
||||
.join("src")
|
||||
.join("**")
|
||||
.join("*.html");
|
||||
|
||||
assert_eq!(path_to_posix_string(&path), "/src/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_empty_paths() {
|
||||
assert_eq!(path_to_posix_string(&PathBuf::new()), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_hoists_static_directories_and_keeps_files_in_the_pattern() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src").join("examples")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/examples/index.html".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src").join("examples"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/index.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_hoists_folder_patterns() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src").join("examples")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/examples".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src").join("examples"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/**/*");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_keeps_wildcards_in_the_pattern() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/**/*.html".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/**/*.html");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ mod scanner {
|
|||
result
|
||||
}
|
||||
|
||||
fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> std::io::Result<()> {
|
||||
#[cfg(not(windows))]
|
||||
let result = std::os::unix::fs::symlink(original, link);
|
||||
|
||||
#[cfg(windows)]
|
||||
let result = std::os::windows::fs::symlink_file(original, link);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn public_source_entry_from_pattern(dir: PathBuf, pattern: &str) -> PublicSourceEntry {
|
||||
let mut parts = pattern.split_whitespace();
|
||||
let _ = parts.next().unwrap_or_default();
|
||||
|
|
@ -546,6 +556,36 @@ mod scanner {
|
|||
assert_eq!(normalized_sources, vec!["**/*", "foo/bar/**/*"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_preserve_source_order_when_referencing_a_sibling_project() {
|
||||
// Create a temporary working directory
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
|
||||
// Create files
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
("project-a/src/index.css", ""),
|
||||
("project-b/keep/keep.html", "content-['GOOD-1']"),
|
||||
("project-b/ignored/ignored.html", "content-['BAD-1']"),
|
||||
("project-b/ignored/except.html", "content-['GOOD-2']"),
|
||||
],
|
||||
);
|
||||
|
||||
let base = dir.join("project-a/src");
|
||||
let mut scanner = Scanner::new(vec![
|
||||
public_source_entry_from_pattern(base.clone(), "@source '../../project-b'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../../project-b/ignored'"),
|
||||
public_source_entry_from_pattern(
|
||||
base.clone(),
|
||||
"@source '../../project-b/ignored/except.html'",
|
||||
),
|
||||
]);
|
||||
let candidates = scanner.scan();
|
||||
|
||||
assert_eq!(candidates, vec!["content-['GOOD-1']", "content-['GOOD-2']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_files_without_extensions() {
|
||||
// These look like folders, but they are files
|
||||
|
|
@ -1761,6 +1801,88 @@ mod scanner {
|
|||
assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symlinked_sources_within_the_scanned_tree_can_be_ignored() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
("src/keep.html", "content-['src/keep.html']"),
|
||||
(
|
||||
"actual-dir/ignore.html",
|
||||
"content-['actual-dir/ignore.html']",
|
||||
),
|
||||
("actual-file.html", "content-['actual-file.html']"),
|
||||
],
|
||||
);
|
||||
|
||||
let _ = symlink(dir.join("actual-dir"), dir.join("linked-dir"));
|
||||
let _ = symlink_file(dir.join("actual-file.html"), dir.join("linked-file.html"));
|
||||
|
||||
let base = dir.join("src");
|
||||
let mut scanner = Scanner::new(vec![
|
||||
public_source_entry_from_pattern(base.clone(), "@source '../**/*'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../actual-dir'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../linked-dir'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../actual-file.html'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../linked-file.html'"),
|
||||
]);
|
||||
let candidates = scanner.scan();
|
||||
|
||||
assert_eq!(candidates, vec!["content-['src/keep.html']"]);
|
||||
|
||||
let mut scanner = Scanner::new(vec![
|
||||
public_source_entry_from_pattern(base.clone(), "@source '../**/*'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../actual-dir/**/*.html'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../linked-dir/**/*.html'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../actual-file.html'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not '../linked-file.html'"),
|
||||
]);
|
||||
let candidates = scanner.scan();
|
||||
|
||||
assert_eq!(candidates, vec!["content-['src/keep.html']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symlinked_sources_outside_the_scanned_tree_can_be_ignored() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
(
|
||||
"actual-dir/ignore.html",
|
||||
"content-['actual-dir/ignore.html']",
|
||||
),
|
||||
("project/keep.html", "content-['project/keep.html']"),
|
||||
],
|
||||
);
|
||||
|
||||
let _ = symlink(dir.join("actual-dir"), dir.join("project/linked-dir"));
|
||||
|
||||
let base = dir.join("project");
|
||||
let mut scanner = Scanner::new(vec![public_source_entry_from_pattern(
|
||||
base.clone(),
|
||||
"@source '**/*'",
|
||||
)]);
|
||||
let candidates = scanner.scan();
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['actual-dir/ignore.html']",
|
||||
"content-['project/keep.html']",
|
||||
]
|
||||
);
|
||||
|
||||
let mut scanner = Scanner::new(vec![
|
||||
public_source_entry_from_pattern(base.clone(), "@source '**/*'"),
|
||||
public_source_entry_from_pattern(base.clone(), "@source not 'linked-dir'"),
|
||||
]);
|
||||
let candidates = scanner.scan();
|
||||
|
||||
assert_eq!(candidates, vec!["content-['project/keep.html']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_used_css_variables_from_css() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
|
|
|
|||
|
|
@ -1938,7 +1938,7 @@ test(
|
|||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, spawn, root, expect }) => {
|
||||
async ({ fs, exec, root, expect }) => {
|
||||
await exec('pnpm tailwindcss --input ./index.css --output dist/out.css', { cwd: root })
|
||||
|
||||
let content = await fs.dumpFiles('./dist/*.css')
|
||||
|
|
@ -1949,6 +1949,201 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source order is important (referencing sibling project)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
@source '../../project-b';
|
||||
@source not '../../project-b/ignored';
|
||||
@source '../../project-b/ignored/except.html';
|
||||
`,
|
||||
|
||||
'project-b/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'project-b/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'project-b/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
},
|
||||
},
|
||||
async ({ fs, root, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./project-a/dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source works with symlinks (referencing sibling project)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
/* Same as the previous test, but using symlinks instead */
|
||||
@source '../../project-b';
|
||||
@source not '../../project-b/ignored';
|
||||
@source '../../project-b/ignored/except.html';
|
||||
`,
|
||||
|
||||
'project-b/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'project-c/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'project-c/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
|
||||
// Symlink the ignored folder to another project
|
||||
'project-b/ignored': 'symlink:../../project-c/ignored/',
|
||||
},
|
||||
},
|
||||
async ({ fs, root, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./project-a/dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'@source works with symlinks (referencing folder in current folder)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities' source(none);
|
||||
|
||||
/* Same as the previous test, but using symlinks instead */
|
||||
@source '../lib';
|
||||
@source not '../lib/ignored';
|
||||
@source '../lib/ignored/except.html';
|
||||
`,
|
||||
|
||||
'lib/keep/keep.html': html`<div class="content-['GOOD-1']"></div>`,
|
||||
'vendor/ignored/ignored.html': html`<div class="content-['BAD-1']"></div>`,
|
||||
'vendor/ignored/except.html': html`<div class="content-['GOOD-2']"></div>`,
|
||||
|
||||
// Symlink the ignored folder to another folder
|
||||
'lib/ignored': 'symlink:../../vendor/ignored/',
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./dist/out.css ---
|
||||
@layer properties;
|
||||
.content-\\[\\'GOOD-1\\'\\] {
|
||||
--tw-content: 'GOOD-1';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
.content-\\[\\'GOOD-2\\'\\] {
|
||||
--tw-content: 'GOOD-2';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'auto source detection disabled',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ interface TestContext {
|
|||
parseSourceMap(opts: string | SourceMapOptions): SourceMap
|
||||
fs: {
|
||||
write(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>
|
||||
symlink(dst: string, src: string): Promise<void>
|
||||
create(filePaths: string[]): Promise<void>
|
||||
read(filePath: string): Promise<string>
|
||||
delete(filePath: string): Promise<void>
|
||||
|
|
@ -294,11 +295,7 @@ export function test(
|
|||
}
|
||||
},
|
||||
fs: {
|
||||
async write(
|
||||
filename: string,
|
||||
content: string | Uint8Array,
|
||||
encoding: BufferEncoding = 'utf8',
|
||||
): Promise<void> {
|
||||
async write(filename, content, encoding = 'utf8') {
|
||||
let full = path.join(root, filename)
|
||||
let dir = path.dirname(full)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
|
|
@ -319,7 +316,19 @@ export function test(
|
|||
await fs.writeFile(full, content, encoding)
|
||||
},
|
||||
|
||||
async create(filenames: string[]): Promise<void> {
|
||||
async symlink(target, src) {
|
||||
let targetAbsolute = path.join(root, target)
|
||||
let targetParent = path.dirname(targetAbsolute)
|
||||
await fs.mkdir(targetParent, { recursive: true })
|
||||
|
||||
let srcAbsolute = path.join(root, src)
|
||||
let srcParent = path.dirname(srcAbsolute)
|
||||
await fs.mkdir(srcParent, { recursive: true })
|
||||
|
||||
await fs.symlink(targetAbsolute, srcAbsolute)
|
||||
},
|
||||
|
||||
async create(filenames) {
|
||||
for (let filename of filenames) {
|
||||
let full = path.join(root, filename)
|
||||
|
||||
|
|
@ -329,11 +338,11 @@ export function test(
|
|||
}
|
||||
},
|
||||
|
||||
async delete(filename: string): Promise<void> {
|
||||
async delete(filename) {
|
||||
await fs.unlink(path.join(root, filename))
|
||||
},
|
||||
|
||||
async read(filePath: string) {
|
||||
async read(filePath) {
|
||||
let content = await fs.readFile(path.resolve(root, filePath), 'utf8')
|
||||
|
||||
// Ensure that files read on Windows have \r\n line endings removed
|
||||
|
|
@ -343,7 +352,7 @@ export function test(
|
|||
|
||||
return content
|
||||
},
|
||||
async glob(pattern: string) {
|
||||
async glob(pattern) {
|
||||
let files = await fastGlob(pattern, { cwd: root })
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
|
|
@ -356,7 +365,7 @@ export function test(
|
|||
}),
|
||||
)
|
||||
},
|
||||
async dumpFiles(pattern: string) {
|
||||
async dumpFiles(pattern) {
|
||||
let files = await context.fs.glob(pattern)
|
||||
return `\n${files
|
||||
.slice()
|
||||
|
|
@ -416,7 +425,17 @@ export function test(
|
|||
`
|
||||
|
||||
for (let [filename, content] of Object.entries(config.fs)) {
|
||||
await context.fs.write(filename, content)
|
||||
if (content.toString().startsWith('symlink:')) {
|
||||
// The symlink path is relative to the target destination's path
|
||||
let target = path.join(
|
||||
filename,
|
||||
content.toString().slice('symlink:'.length), // Relative path
|
||||
)
|
||||
|
||||
await context.fs.symlink(target, filename)
|
||||
} else {
|
||||
await context.fs.write(filename, content)
|
||||
}
|
||||
}
|
||||
|
||||
let shouldInstallDependencies = config.installDependencies ?? true
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ async function migrate(
|
|||
.use(sortBuckets())
|
||||
.use(formatNodes())
|
||||
.process(stylesheet.root!, { from: expect.getState().testPath })
|
||||
.then((result) => result.css)
|
||||
.then((result) => `\n${result.css.trim()}\n`)
|
||||
}
|
||||
|
||||
it('should migrate simple `@layer utilities` to `@utility`', async () => {
|
||||
|
|
@ -52,9 +52,11 @@ it('should migrate simple `@layer utilities` to `@utility`', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -69,13 +71,15 @@ it('should split multiple selectors in separate utilities', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@utility bar {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -99,14 +103,16 @@ it('should merge `@utility` with the same name', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bar {
|
||||
color: blue;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -149,7 +155,8 @@ it('should leave non-class utilities alone', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* 2. */
|
||||
/* 2.1. */
|
||||
color: red;
|
||||
|
|
@ -182,7 +189,58 @@ it('should leave non-class utilities alone', async () => {
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should not convert empty classes into `@utility` since that would make them invalid', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@layer utilities {
|
||||
.good {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bad-empty {
|
||||
}
|
||||
|
||||
/* Comment above */
|
||||
.bad-with-comment-above {
|
||||
}
|
||||
|
||||
.bad-with-comment-inside {
|
||||
/* Comment inside */
|
||||
}
|
||||
|
||||
.bad-with-comment-below {
|
||||
}
|
||||
/* Comment below */
|
||||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@utility good {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bad-empty {
|
||||
}
|
||||
|
||||
/* Comment above */
|
||||
.bad-with-comment-above {
|
||||
}
|
||||
|
||||
.bad-with-comment-inside {
|
||||
/* Comment inside */
|
||||
}
|
||||
|
||||
.bad-with-comment-below {
|
||||
}
|
||||
/* Comment below */
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -204,7 +262,8 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -214,7 +273,8 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async
|
|||
&:focus {
|
||||
color: green;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -232,13 +292,15 @@ it('should migrate multiple simple `@layer utilities` to `@utility`', async () =
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@utility bar {
|
||||
color: blue;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -260,7 +322,8 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +333,8 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => {
|
|||
.baz {
|
||||
color: green;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -286,11 +350,13 @@ it('should invert at-rules to make them migrate-able', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (min-width: 640px) {
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -314,7 +380,8 @@ it('should migrate at-rules with multiple utilities and invert them', async () =
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (min-width: 640px) {
|
||||
color: red;
|
||||
}
|
||||
|
|
@ -324,7 +391,8 @@ it('should migrate at-rules with multiple utilities and invert them', async () =
|
|||
@media (min-width: 640px) {
|
||||
color: blue;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -356,7 +424,8 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (min-width: 640px) {
|
||||
color: red;
|
||||
}
|
||||
|
|
@ -384,7 +453,8 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the
|
|||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -398,11 +468,13 @@ it('should migrate classes with pseudo elements', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -416,11 +488,13 @@ it('should migrate classes with attribute selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&[data-checked=''] {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -434,11 +508,13 @@ it('should migrate classes with element selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
& main {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -452,11 +528,13 @@ it('should migrate classes attached to an element selector', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&main {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -470,11 +548,13 @@ it('should migrate classes with id selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&#main {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -488,7 +568,8 @@ it('should migrate classes with another attached class', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&.main {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -498,7 +579,8 @@ it('should migrate classes with another attached class', async () => {
|
|||
&.no-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -512,7 +594,8 @@ it('should migrate a selector with multiple classes to multiple @utility definit
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
& .bar:hover .baz:focus {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -528,7 +611,8 @@ it('should migrate a selector with multiple classes to multiple @utility definit
|
|||
.foo .bar:hover &:focus {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -548,7 +632,8 @@ it('should merge `@utility` definitions with the same name', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility step {
|
||||
"
|
||||
@utility step {
|
||||
counter-increment: step;
|
||||
|
||||
&:before {
|
||||
|
|
@ -556,7 +641,8 @@ it('should merge `@utility` definitions with the same name', async () => {
|
|||
@apply ml-[-41px];
|
||||
content: counter(step);
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -570,7 +656,8 @@ it('should not migrate nested classes inside a `:not(…)`', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
& .bar:not(.qux):has(.baz) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -586,7 +673,8 @@ it('should not migrate nested classes inside a `:not(…)`', async () => {
|
|||
.foo .bar:not(.qux):has(&) {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -620,7 +708,8 @@ it('should migrate advanced combinations', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (width >= 100px) {
|
||||
@supports (display: none) {
|
||||
& .bar:not(.qux):has(.baz) {
|
||||
|
|
@ -659,7 +748,8 @@ it('should migrate advanced combinations', async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -677,12 +767,14 @@ describe('comments', () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Start of utilities: */
|
||||
"
|
||||
/* Start of utilities: */
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -699,7 +791,8 @@ describe('comments', () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Start of utilities: */
|
||||
"
|
||||
/* Start of utilities: */
|
||||
@utility foo {
|
||||
/* Foo & Bar */
|
||||
& .bar {
|
||||
|
|
@ -713,7 +806,8 @@ describe('comments', () => {
|
|||
/* Declarations: */
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -733,7 +827,8 @@ describe('comments', () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Start of utilities: */
|
||||
"
|
||||
/* Start of utilities: */
|
||||
@utility foo {
|
||||
/* Mobile only */
|
||||
@media (width <= 640px) {
|
||||
|
|
@ -741,7 +836,8 @@ describe('comments', () => {
|
|||
/* Declarations: */
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -780,7 +876,8 @@ describe('comments', () => {
|
|||
/* After */
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Tailwind Utilities: */
|
||||
"
|
||||
/* Tailwind Utilities: */
|
||||
@utility no-scrollbar {
|
||||
/* Chrome, Safari and Opera */
|
||||
/* Second comment */
|
||||
|
|
@ -807,7 +904,8 @@ describe('comments', () => {
|
|||
.after {
|
||||
/* Inside */
|
||||
}
|
||||
/* After */"
|
||||
/* After */
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -832,7 +930,8 @@ it('should not lose attribute selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@layer components {
|
||||
"
|
||||
@layer components {
|
||||
#TableOfContents {
|
||||
.toc a {
|
||||
@apply block max-w-full truncate py-1 pl-2 hover:font-medium hover:no-underline;
|
||||
|
|
@ -845,7 +944,8 @@ it('should not lose attribute selectors', async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -863,11 +963,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -884,11 +986,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['components'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -905,11 +1009,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['foo'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Utility #1 */
|
||||
"
|
||||
/* Utility #1 */
|
||||
.foo {
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -929,7 +1035,8 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
|
|
@ -937,7 +1044,8 @@ describe('layered stylesheets', () => {
|
|||
|
||||
#main {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -978,7 +1086,8 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@layer utilities {
|
||||
@layer utilities {
|
||||
/* Utility #1 */
|
||||
|
|
@ -1011,7 +1120,8 @@ describe('layered stylesheets', () => {
|
|||
|
||||
#secondary {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -1028,11 +1138,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@import 'thing';
|
||||
"
|
||||
@import 'thing';
|
||||
|
||||
@utility foo {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -1049,11 +1161,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@charset "utf-8";
|
||||
"
|
||||
@charset "utf-8";
|
||||
|
||||
@utility foo {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss'
|
||||
import { type AtRule, type Comment, type Container, type Plugin, type Rule } from 'postcss'
|
||||
import SelectorParser from 'postcss-selector-parser'
|
||||
import { segment } from '../../../../tailwindcss/src/utils/segment'
|
||||
import { Stylesheet } from '../../stylesheet'
|
||||
|
|
@ -43,6 +43,7 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
|
||||
walk(atRule, (node) => {
|
||||
if (node.type !== 'rule') return
|
||||
if (isEmpty(node)) return
|
||||
|
||||
// Find all the classes in the selector
|
||||
SelectorParser((selectors) => {
|
||||
|
|
@ -184,6 +185,18 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
toRemove.push(node.parent?.nodes.at(i) as Comment)
|
||||
}
|
||||
}
|
||||
|
||||
let commentsAfter: Comment[] = []
|
||||
for (let i = idx + 1; i < (node.parent?.nodes.length ?? 0); i++) {
|
||||
if (node.parent?.nodes.at(i)?.type === 'rule') {
|
||||
commentsAfter = []
|
||||
break
|
||||
}
|
||||
if (node.parent?.nodes.at(i)?.type === 'comment') {
|
||||
commentsAfter.push(node.parent?.nodes.at(i) as Comment)
|
||||
}
|
||||
}
|
||||
toRemove.push(...commentsAfter)
|
||||
}
|
||||
for (let node of toRemove) {
|
||||
node.remove()
|
||||
|
|
@ -231,8 +244,12 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
// Remove empty rules
|
||||
if ((node.type === 'rule' || node.type === 'atrule') && node.nodes?.length === 0) {
|
||||
// Remove empty rules from `@utility` clones and empty wrapper at-rules
|
||||
// from the default `@layer` clone.
|
||||
if (
|
||||
(node.type === 'atrule' && node.nodes?.length === 0) ||
|
||||
(clone !== defaultsAtRule && node.type === 'rule' && node.nodes?.length === 0)
|
||||
) {
|
||||
node.remove()
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +278,11 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
// Remove empty clones entirely
|
||||
if (clone.nodes?.length === 0) {
|
||||
clones.splice(idx, 1)
|
||||
} else if (clone === defaultsAtRule) {
|
||||
let first = clone.nodes?.[0]
|
||||
if (first) {
|
||||
first.raws.before = first.raws.before?.replace(/\n\s*\n/g, '\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,3 +338,17 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(node: Container): boolean {
|
||||
// We have no body
|
||||
if (node.nodes === undefined) return false
|
||||
|
||||
// We have a body, but no nodes
|
||||
if (node.nodes.length === 0) return true
|
||||
|
||||
// We have nodes, but they are only comments which we consider empty
|
||||
if (node.nodes.every((child) => child.type === 'comment')) return true
|
||||
|
||||
// Not empty
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27850,7 +27850,7 @@ test('inset-shadow', async () => {
|
|||
}
|
||||
|
||||
.inset-shadow-none {
|
||||
--tw-inset-shadow: 0 0 #0000;
|
||||
--tw-inset-shadow: inset 0 0 #0000;
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5734,7 +5734,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier) return
|
||||
return [
|
||||
boxShadowProperties(),
|
||||
decl('--tw-inset-shadow', nullShadow),
|
||||
decl('--tw-inset-shadow', `inset ${nullShadow}`),
|
||||
decl('box-shadow', cssBoxShadowValue),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/postcss": "workspace:^",
|
||||
"fast-glob": "^3.3.3",
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
"upgrade": "node scripts/upgrade.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"tailwindcss": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
128
pnpm-lock.yaml
generated
128
pnpm-lock.yaml
generated
|
|
@ -516,14 +516,14 @@ importers:
|
|||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
next:
|
||||
specifier: 16.2.6
|
||||
version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
specifier: 16.2.7
|
||||
version: 16.2.7(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
react:
|
||||
specifier: 19.2.6
|
||||
version: 19.2.6
|
||||
specifier: 19.2.7
|
||||
version: 19.2.7
|
||||
react-dom:
|
||||
specifier: 19.2.6
|
||||
version: 19.2.6(react@19.2.6)
|
||||
specifier: 19.2.7
|
||||
version: 19.2.7(react@19.2.7)
|
||||
tailwindcss:
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/tailwindcss
|
||||
|
|
@ -544,14 +544,14 @@ importers:
|
|||
playgrounds/v3:
|
||||
dependencies:
|
||||
next:
|
||||
specifier: 16.2.6
|
||||
version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
specifier: 16.2.7
|
||||
version: 16.2.7(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
react:
|
||||
specifier: 19.2.6
|
||||
version: 19.2.6
|
||||
specifier: 19.2.7
|
||||
version: 19.2.7
|
||||
react-dom:
|
||||
specifier: 19.2.6
|
||||
version: 19.2.6(react@19.2.6)
|
||||
specifier: 19.2.7
|
||||
version: 19.2.7(react@19.2.7)
|
||||
tailwindcss:
|
||||
specifier: ^3
|
||||
version: 3.4.19
|
||||
|
|
@ -1400,53 +1400,53 @@ packages:
|
|||
resolution: {integrity: sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@next/env@16.2.6':
|
||||
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
|
||||
'@next/env@16.2.7':
|
||||
resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.2.6':
|
||||
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
|
||||
'@next/swc-darwin-arm64@16.2.7':
|
||||
resolution: {integrity: sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.2.6':
|
||||
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
|
||||
'@next/swc-darwin-x64@16.2.7':
|
||||
resolution: {integrity: sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.2.6':
|
||||
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
|
||||
'@next/swc-linux-arm64-gnu@16.2.7':
|
||||
resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.2.6':
|
||||
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
|
||||
'@next/swc-linux-arm64-musl@16.2.7':
|
||||
resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.2.6':
|
||||
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
|
||||
'@next/swc-linux-x64-gnu@16.2.7':
|
||||
resolution: {integrity: sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.2.6':
|
||||
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
|
||||
'@next/swc-linux-x64-musl@16.2.7':
|
||||
resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.2.6':
|
||||
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
|
||||
'@next/swc-win32-arm64-msvc@16.2.7':
|
||||
resolution: {integrity: sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
|
||||
'@next/swc-win32-x64-msvc@16.2.7':
|
||||
resolution: {integrity: sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
|
@ -2834,8 +2834,8 @@ packages:
|
|||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
next@16.2.6:
|
||||
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
|
||||
next@16.2.7:
|
||||
resolution: {integrity: sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
|
|
@ -3031,10 +3031,19 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^19.2.6
|
||||
|
||||
react-dom@19.2.7:
|
||||
resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
|
||||
peerDependencies:
|
||||
react: ^19.2.7
|
||||
|
||||
react@19.2.6:
|
||||
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react@19.2.7:
|
||||
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
|
|
@ -4061,30 +4070,30 @@ snapshots:
|
|||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
'@next/env@16.2.6': {}
|
||||
'@next/env@16.2.7': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.2.6':
|
||||
'@next/swc-darwin-arm64@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.2.6':
|
||||
'@next/swc-darwin-x64@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.2.6':
|
||||
'@next/swc-linux-arm64-gnu@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.2.6':
|
||||
'@next/swc-linux-arm64-musl@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.2.6':
|
||||
'@next/swc-linux-x64-gnu@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.2.6':
|
||||
'@next/swc-linux-x64-musl@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.2.6':
|
||||
'@next/swc-win32-arm64-msvc@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.2.6':
|
||||
'@next/swc-win32-x64-msvc@16.2.7':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
|
|
@ -5240,25 +5249,25 @@ snapshots:
|
|||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
next@16.2.7(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@next/env': 16.2.6
|
||||
'@next/env': 16.2.7
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.10.31
|
||||
caniuse-lite: 1.0.30001793
|
||||
postcss: 8.4.31
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
styled-jsx: 5.1.6(react@19.2.6)
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
styled-jsx: 5.1.6(react@19.2.7)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.2.6
|
||||
'@next/swc-darwin-x64': 16.2.6
|
||||
'@next/swc-linux-arm64-gnu': 16.2.6
|
||||
'@next/swc-linux-arm64-musl': 16.2.6
|
||||
'@next/swc-linux-x64-gnu': 16.2.6
|
||||
'@next/swc-linux-x64-musl': 16.2.6
|
||||
'@next/swc-win32-arm64-msvc': 16.2.6
|
||||
'@next/swc-win32-x64-msvc': 16.2.6
|
||||
'@next/swc-darwin-arm64': 16.2.7
|
||||
'@next/swc-darwin-x64': 16.2.7
|
||||
'@next/swc-linux-arm64-gnu': 16.2.7
|
||||
'@next/swc-linux-arm64-musl': 16.2.7
|
||||
'@next/swc-linux-x64-gnu': 16.2.7
|
||||
'@next/swc-linux-x64-musl': 16.2.7
|
||||
'@next/swc-win32-arm64-msvc': 16.2.7
|
||||
'@next/swc-win32-x64-msvc': 16.2.7
|
||||
'@playwright/test': 1.60.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -5413,8 +5422,15 @@ snapshots:
|
|||
react: 19.2.6
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-dom@19.2.7(react@19.2.7):
|
||||
dependencies:
|
||||
react: 19.2.7
|
||||
scheduler: 0.27.0
|
||||
|
||||
react@19.2.6: {}
|
||||
|
||||
react@19.2.7: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
|
|
@ -5560,10 +5576,10 @@ snapshots:
|
|||
|
||||
std-env@4.1.0: {}
|
||||
|
||||
styled-jsx@5.1.6(react@19.2.6):
|
||||
styled-jsx@5.1.6(react@19.2.7):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.6
|
||||
react: 19.2.7
|
||||
|
||||
sucrase@3.35.1:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue