mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2026-02-01 17:26:34 +00:00
Extract used CSS variables from .css files (#17433)
This PR fixes an issue where CSS variables could be used in CSS modules, but where never emitted in your final CSS. Some backstory, when Tailwind CSS v4 came out, we _always_ emitted all CSS variables whether they were used or not. Later, we added an optimization where we only emit the CSS variables that were actually used. The definition of "used" in this case is: 1. Used in your CSS file(s) — (we check the final CSS AST for this) 2. Used _somewhere_ in any of your source files (e.g.: a JavaScript file accessing a variable) The issue this PR tries to solve is with the very first point. If you are using CSS modules, then every CSS file is processed separately. This is not a choice Tailwind CSS made, but how other build tooling works (like Vite for example). To prevent emitting all of Tailwind's Preflight reset and all utilities per CSS file, you can use the `@reference` directive instead of repeating `@import "tailwindcss";`. This is explained here: https://tailwindcss.com/docs/compatibility#explicit-context-sharing But now we are just _referencing_ them, not emitting them. And since the CSS module is not connected in any way to the main `index.css` file that contains the `@import "tailwindcss";` directive, we don't even see the CSS variables while processing the `index.css` file. (or wherever your main CSS file is) This is where point 2 from above comes in. This is a situation where we rely on the extractor to find the used CSS variables so we can internally mark them as used. To finally get to the point of this PR, the extractor only scans `.html`, `.js`, ... files but not `.css` files. So all the CSS variables used inside of CSS modules will not be generated. This PR changes that behavior to also scan `.css` files. But _only_ for CSS variables (not any other type of class candidate). This is important, otherwise all your custom `@utility foo {}` definitions would always mark `foo` as a used class and include it in the CSS which is not always the case. On top extracting CSS variables, we will also make sure that the CSS variables we find are in usage positions (e.g.: `var(--color-red-500)`) and not in definition positions (e.g.: `--color-red-500: #ff0000;`). This is important because we only want to emit the variables that are actually used in the final CSS output. One future improvement not implemented here, is that technically we will also extract CSS variables that might not be used if defined in a `@utility`. ```css @utility never-used { color: var(--color-red-500); /* --color-red-500 will be emitted, even if it might not be used */ } ``` Fixes: #16904 Fixes: #17429 # Test plan 1. Added a test where CSS variables are defined in `.css` files (and ignored) 2. Added a test where CSS variables are used in `.css` files (and included) Testing on the reproduction defined in #16904, the `.module.css` file contains a reference to `var(--color-hot-pink)`, but generating a build shows that the variable definition is not available: <img width="1630" alt="image" src="https://github.com/user-attachments/assets/a0d5c37e-6813-4cd5-a677-6c356b5a73d4" /> When you run the build again with the changes from this PR, then we _do_ see the definition of the `--color-hot-pink` in the root CSS file: <img width="2876" alt="image" src="https://github.com/user-attachments/assets/beab7c11-a31b-4ea4-8235-4849a8e92859" />
This commit is contained in:
parent
3412a9623d
commit
2af7c57983
@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix negated `content` rules in legacy JavaScript configuration ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
|
||||
- Extract special `@("@")md:…` syntax in Razor files ([#17427](https://github.com/tailwindlabs/tailwindcss/pull/17427))
|
||||
- Disallow arbitrary values with top-level braces and semicolons as well as unbalanced parentheses and brackets ([#17361](https://github.com/tailwindlabs/tailwindcss/pull/17361))
|
||||
- Extract used CSS variables from `.css` files ([#17433](https://github.com/tailwindlabs/tailwindcss/pull/17433))
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use crate::cursor;
|
||||
use crate::extractor::machine::Span;
|
||||
use bstr::ByteSlice;
|
||||
use candidate_machine::CandidateMachine;
|
||||
use css_variable_machine::CssVariableMachine;
|
||||
use machine::{Machine, MachineState};
|
||||
@ -139,6 +140,41 @@ impl<'a> Extractor<'a> {
|
||||
|
||||
extracted
|
||||
}
|
||||
|
||||
pub fn extract_variables_from_css(&mut self) -> Vec<Extracted<'a>> {
|
||||
let mut extracted = Vec::with_capacity(100);
|
||||
|
||||
let len = self.cursor.input.len();
|
||||
|
||||
let cursor = &mut self.cursor.clone();
|
||||
while cursor.pos < len {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
if let MachineState::Done(span) = self.css_variable_machine.next(cursor) {
|
||||
// We are only interested in variables that are used, not defined. Therefore we
|
||||
// need to ensure that the variable is prefixed with `var(`.
|
||||
if span.start < 4 {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
let slice_before = Span::new(span.start - 4, span.start - 1);
|
||||
if !slice_before.slice(self.cursor.input).starts_with(b"var(") {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
extracted.push(Extracted::CssVariable(span.slice(self.cursor.input)));
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
extracted
|
||||
}
|
||||
}
|
||||
|
||||
// Extract sub-candidates from a given range.
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
css
|
||||
less
|
||||
lock
|
||||
sass
|
||||
|
||||
@ -84,6 +84,9 @@ pub struct Scanner {
|
||||
/// All found extensions
|
||||
extensions: FxHashSet<String>,
|
||||
|
||||
/// All CSS files we want to scan for CSS variable usage
|
||||
css_files: Vec<PathBuf>,
|
||||
|
||||
/// All files that we have to scan
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
@ -212,11 +215,25 @@ impl Scanner {
|
||||
fn extract_candidates(&mut self) -> Vec<String> {
|
||||
let changed_content = self.changed_content.drain(..).collect::<Vec<_>>();
|
||||
|
||||
let candidates = parse_all_blobs(read_all_files(changed_content));
|
||||
// Extract all candidates from the changed content
|
||||
let mut new_candidates = parse_all_blobs(read_all_files(changed_content));
|
||||
|
||||
// Extract all CSS variables from the CSS files
|
||||
let css_files = self.css_files.drain(..).collect::<Vec<_>>();
|
||||
if !css_files.is_empty() {
|
||||
let css_variables = extract_css_variables(read_all_files(
|
||||
css_files
|
||||
.into_iter()
|
||||
.map(|file| ChangedContent::File(file, "css".into()))
|
||||
.collect(),
|
||||
));
|
||||
|
||||
new_candidates.extend(css_variables);
|
||||
}
|
||||
|
||||
// Only compute the new candidates and ignore the ones we already have. This is for
|
||||
// subsequent calls to prevent serializing the entire set of candidates every time.
|
||||
let mut new_candidates = candidates
|
||||
let mut new_candidates = new_candidates
|
||||
.into_par_iter()
|
||||
.filter(|candidate| !self.candidates.contains(candidate))
|
||||
.collect::<Vec<_>>();
|
||||
@ -248,6 +265,12 @@ impl Scanner {
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default(); // In case the file has no extension
|
||||
|
||||
// Special handing for CSS files to extract CSS variables
|
||||
if extension == "css" {
|
||||
self.css_files.push(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
self.extensions.insert(extension.to_owned());
|
||||
self.changed_content.push(ChangedContent::File(
|
||||
path.to_path_buf(),
|
||||
@ -402,6 +425,43 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
let mut result: Vec<_> = blobs
|
||||
.par_iter()
|
||||
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
|
||||
.filter_map(|blob| {
|
||||
if blob.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let extracted = crate::extractor::Extractor::new(blob).extract_variables_from_css();
|
||||
if extracted.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(FxHashSet::from_iter(extracted.into_iter().map(
|
||||
|x| match x {
|
||||
Extracted::CssVariable(bytes) => bytes,
|
||||
_ => &[],
|
||||
},
|
||||
)))
|
||||
})
|
||||
.reduce(Default::default, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
|
||||
.collect();
|
||||
|
||||
// SAFETY: Unstable sort is faster and in this scenario it's also safe because we are
|
||||
// guaranteed to have unique candidates.
|
||||
result.par_sort_unstable();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
let mut result: Vec<_> = blobs
|
||||
|
||||
@ -1735,4 +1735,39 @@ mod scanner {
|
||||
|
||||
assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_used_css_variables_from_css() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
(
|
||||
"src/index.css",
|
||||
r#"
|
||||
@theme {
|
||||
--color-red: #ff0000; /* Not used, so don't extract */
|
||||
--color-green: #00ff00; /* Not used, so don't extract */
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--color-red); /* Used, so extract */
|
||||
}
|
||||
"#,
|
||||
),
|
||||
("src/used-at-start.css", "var(--color-used-at-start)"),
|
||||
// Here to verify that we don't crash when trying to find `var(` in front of the
|
||||
// variable.
|
||||
("src/defined-at-start.css", "--color-defined-at-start: red;"),
|
||||
],
|
||||
);
|
||||
|
||||
let mut scanner = Scanner::new(vec![public_source_entry_from_pattern(
|
||||
dir.clone(),
|
||||
"@source './'",
|
||||
)]);
|
||||
let candidates = scanner.scan();
|
||||
|
||||
assert_eq!(candidates, vec!["--color-red", "--color-used-at-start"]);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user