Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
- cli
- postcss
- workers
- webpack

# Exclude windows and macos from being built on feature branches
run-all:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ 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))
- Ensure the `--theme(…)` function still resolves to the CSS variables even when legacy JS plugins are enabled
- Extract used CSS variables from `.css` files ([#17433](https://github.com/tailwindlabs/tailwindcss/pull/17433), [#17467](https://github.com/tailwindlabs/tailwindcss/pull/17467))

### Changed

Expand Down
1 change: 0 additions & 1 deletion crates/oxide/src/extractor/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::cursor;
use crate::extractor::machine::Span;
use bstr::ByteSlice;
use candidate_machine::CandidateMachine;
use css_variable_machine::CssVariableMachine;
use machine::{Machine, MachineState};
Expand Down
70 changes: 25 additions & 45 deletions crates/oxide/src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ use fxhash::{FxHashMap, FxHashSet};
use ignore::{gitignore::GitignoreBuilder, WalkBuilder};
use rayon::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::{self, Arc, Mutex};
use std::time::SystemTime;
use tracing::event;
Expand Down Expand Up @@ -265,18 +264,21 @@ 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;
match extension {
// Special handing for CSS files, we don't want to extract candidates from
// these files, but we do want to extract used CSS variables.
"css" => {
self.css_files.push(path.clone());
}
_ => {
self.changed_content.push(ChangedContent::File(
path.to_path_buf(),
extension.to_owned(),
));
}
}

self.extensions.insert(extension.to_owned());
self.changed_content.push(ChangedContent::File(
path.to_path_buf(),
extension.to_owned(),
));

self.files.push(path);
}
}
Expand Down Expand Up @@ -427,43 +429,21 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {

#[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
extract(blobs, |mut extractor| {
extractor.extract_variables_from_css()
})
}

#[tracing::instrument(skip_all)]
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
extract(blobs, |mut extractor| extractor.extract())
}

#[tracing::instrument(skip_all)]
fn extract<H>(blobs: Vec<Vec<u8>>, handle: H) -> Vec<String>
where
H: Fn(Extractor) -> Vec<Extracted> + std::marker::Sync,
{
let mut result: Vec<_> = blobs
.par_iter()
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
Expand All @@ -472,7 +452,7 @@ fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
return None;
}

let extracted = crate::extractor::Extractor::new(blob).extract();
let extracted = handle(crate::extractor::Extractor::new(blob));
if extracted.is_empty() {
return None;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/oxide/tests/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ mod scanner {
("c.less", ""),
]);

assert_eq!(files, vec!["index.html"]);
assert_eq!(files, vec!["a.css", "index.html"]);
assert_eq!(globs, vec!["*"]);
assert_eq!(normalized_sources, vec!["**/*"]);
}
Expand Down
74 changes: 74 additions & 0 deletions integrations/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,80 @@ test(
},
)

test(
'changes to CSS files should pick up new CSS variables (if any)',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'unrelated.module.css': css`
.module {
color: var(--color-blue-500);
}
`,
'index.css': css`
@import 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
'index.html': html`<div class="flex"></div>`,
},
},
async ({ spawn, exec, fs, expect }) => {
// Generate the initial build so output CSS files exist on disk
await exec('pnpm tailwindcss --input ./index.css --output ./dist/out.css')

// NOTE: We are writing to an output CSS file which is not being ignored by
// `.gitignore` nor marked with `@source not`. This should not result in an
// infinite loop.
let process = await spawn(
'pnpm tailwindcss --input ./index.css --output ./dist/out.css --watch',
)
await process.onStderr((m) => m.includes('Done in'))

expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
"
--- ./dist/out.css ---
:root, :host {
--color-blue-500: oklch(0.623 0.214 259.815);
}
.flex {
display: flex;
}
"
`)

await fs.write(
'unrelated.module.css',
css`
.module {
color: var(--color-blue-500);
background-color: var(--color-red-500);
}
`,
)
await process.onStderr((m) => m.includes('Done in'))

expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
"
--- ./dist/out.css ---
:root, :host {
--color-red-500: oklch(0.637 0.237 25.331);
--color-blue-500: oklch(0.623 0.214 259.815);
}
.flex {
display: flex;
}
"
`)
},
)

function withBOM(text: string): string {
return '\uFEFF' + text
}
97 changes: 97 additions & 0 deletions integrations/postcss/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,100 @@ test(
])
},
)

test(
'changes to CSS files should pick up new CSS variables (if any)',
{
fs: {
'package.json': json`
{
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "^14"
},
"devDependencies": {
"@tailwindcss/postcss": "workspace:^",
"tailwindcss": "workspace:^"
}
}
`,
'postcss.config.mjs': js`
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
`,
'next.config.mjs': js`export default {}`,
'app/layout.js': js`
import './globals.css'

export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
`,
'app/page.js': js`
export default function Page() {
return <div className="flex"></div>
}
`,
'unrelated.module.css': css`
.module {
color: var(--color-blue-500);
}
`,
'app/globals.css': css`
@import 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
},
},
async ({ spawn, exec, fs, expect }) => {
// Generate the initial build so output CSS files exist on disk
await exec('pnpm next build')

// NOTE: We are writing to an output CSS file which is not being ignored by
// `.gitignore` nor marked with `@source not`. This should not result in an
// infinite loop.
let process = await spawn(`pnpm next dev`)

let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)/.exec(m)
if (match) url = match[1]
return Boolean(url)
})

await process.onStdout((m) => m.includes('Ready in'))

await retryAssertion(async () => {
let css = await fetchStyles(url)
expect(css).toContain(candidate`flex`)
expect(css).toContain('--color-blue-500:')
expect(css).not.toContain('--color-red-500:')
})

await fs.write(
'unrelated.module.css',
css`
.module {
color: var(--color-blue-500);
background-color: var(--color-red-500);
}
`,
)
await process.onStdout((m) => m.includes('Compiled in'))

await retryAssertion(async () => {
let css = await fetchStyles(url)
expect(css).toContain(candidate`flex`)
expect(css).toContain('--color-blue-500:')
expect(css).toContain('--color-red-500:')
})
},
)
2 changes: 1 addition & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export function test(
return [
file,
// Drop license comment
content.replace(/[\s\n]*\/\*! tailwindcss .*? \*\/[\s\n]*/g, ''),
content.replace(/[\s\n]*\/\*![\s\S]*?\*\/[\s\n]*/g, ''),
]
}),
)
Expand Down
Loading