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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix multi-value inset shadow ([#17523](https://github.com/tailwindlabs/tailwindcss/pull/17523))
- Fix `drop-shadow` utility ([#17515](https://github.com/tailwindlabs/tailwindcss/pull/17515))
- Fix `drop-shadow-*` utilities that use multiple shadows in `@theme inline` ([#17515](https://github.com/tailwindlabs/tailwindcss/pull/17515))
- Fix slow incremental builds with `@tailwindcss/vite` and `@tailwindcss/postscss` (especially on Windows) ([#17511](https://github.com/tailwindlabs/tailwindcss/pull/17511))

## [4.1.1] - 2025-04-02

Expand Down
12 changes: 12 additions & 0 deletions crates/oxide/src/scanner/detect_sources.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::scanner::auto_source_detection::IGNORED_CONTENT_DIRS;
use crate::GlobEntry;
use fxhash::FxHashSet;
use globwalk::DirEntry;
Expand Down Expand Up @@ -101,6 +102,17 @@ pub fn resolve_globs(
continue;
}

if IGNORED_CONTENT_DIRS
.iter()
.any(|dir| match path.file_name() {
Some(name) => name == *dir,
None => false,
})
{
it.skip_current_dir();
continue;
}

if !allowed_paths.contains(path) {
continue;
}
Expand Down
25 changes: 18 additions & 7 deletions crates/oxide/src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub struct Scanner {
dirs: Vec<PathBuf>,

/// All generated globs, used for setting up watchers
globs: Vec<GlobEntry>,
globs: Option<Vec<GlobEntry>>,

/// Track unique set of candidates
candidates: FxHashSet<String>,
Expand Down Expand Up @@ -296,16 +296,24 @@ impl Scanner {

#[tracing::instrument(skip_all)]
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
if let Some(globs) = &self.globs {
return globs.clone();
}

self.scan_sources();

let mut globs = vec![];
for source in self.sources.iter() {
match source {
SourceEntry::Auto { base } | SourceEntry::External { base } => {
let globs = resolve_globs((base).to_path_buf(), &self.dirs, &self.extensions);
self.globs.extend(globs);
globs.extend(resolve_globs(
base.to_path_buf(),
&self.dirs,
&self.extensions,
));
}
SourceEntry::Pattern { base, pattern } => {
self.globs.push(GlobEntry {
globs.push(GlobEntry {
base: base.to_string_lossy().to_string(),
pattern: pattern.to_string(),
});
Expand All @@ -315,13 +323,16 @@ impl Scanner {
}

// Re-optimize the globs to reduce the number of patterns we have to scan.
self.globs = optimize_patterns(&self.globs);
globs = optimize_patterns(&globs);

// Track the globs for subsequent calls
self.globs = Some(globs.clone());

self.globs.clone()
globs
}

#[tracing::instrument(skip_all)]
pub fn get_normalized_sources(&mut self) -> Vec<GlobEntry> {
pub fn get_normalized_sources(&self) -> Vec<GlobEntry> {
self.sources
.iter()
.filter_map(|source| match source {
Expand Down
130 changes: 129 additions & 1 deletion integrations/postcss/next.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe } from 'vitest'
import { candidate, css, fetchStyles, js, json, retryAssertion, test } from '../utils'
import { candidate, css, fetchStyles, js, json, jsx, retryAssertion, test, txt } from '../utils'

test(
'production build',
Expand Down Expand Up @@ -356,3 +356,131 @@ test(
})
},
)

test(
'changes to `public/` should not trigger an infinite loop',
{
fs: {
'package.json': json`
{
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "^15",
"@ducanh2912/next-pwa": "^10.2.9"
},
"devDependencies": {
"@tailwindcss/postcss": "workspace:^",
"tailwindcss": "workspace:^"
}
}
`,
'.gitignore': txt`
.next/
public/workbox-*.js
public/sw.js
`,
'postcss.config.mjs': js`
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
`,
'next.config.mjs': js`
import withPWA from '@ducanh2912/next-pwa'

const pwaConfig = {
dest: 'public',
register: true,
skipWaiting: true,
reloadOnOnline: false,
cleanupOutdatedCaches: true,
clientsClaim: true,
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024,
}

const nextConfig = {}

const configWithPWA = withPWA(pwaConfig)(nextConfig)

export default configWithPWA
`,
'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>
}
`,
'app/globals.css': css`
@import 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
},
},
async ({ spawn, fs, expect }) => {
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).not.toContain(candidate`underline`)
})

await fs.write(
'app/page.js',
jsx`
export default function Page() {
return <div className="flex underline"></div>
}
`,
)
await process.onStdout((m) => m.includes('Compiled in'))

await retryAssertion(async () => {
let css = await fetchStyles(url)
expect(css).toContain(candidate`flex`)
expect(css).toContain(candidate`underline`)
})
// Flush all existing messages in the queue
process.flush()

// Fetch the styles one more time, to ensure we see the latest version of
// the CSS
await fetchStyles(url)

// At this point, no changes should triger a compile step. If we see any
// changes, there is an infinite loop because we (the user) didn't write any
// files to disk.
//
// Ensure there are no more changes in stdout (signaling no infinite loop)
let result = await Promise.race([
// If this succeeds, it means that it saw another change which indicates
// an infinite loop.
process.onStdout((m) => m.includes('Compiled in')).then(() => 'infinite loop detected'),

// There should be no changes in stdout
new Promise((resolve) => setTimeout(() => resolve('no infinite loop detected'), 2_000)),
])
expect(result).toBe('no infinite loop detected')
},
)
13 changes: 12 additions & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((na

interface SpawnedProcess {
dispose: () => void
flush: () => void
onStdout: (predicate: (message: string) => boolean) => Promise<void>
onStderr: (predicate: (message: string) => boolean) => Promise<void>
}
Expand Down Expand Up @@ -253,6 +254,13 @@ export function test(

return {
dispose,
flush() {
stdoutActors.splice(0)
stderrActors.splice(0)

stdoutMessages.splice(0)
stderrMessages.splice(0)
},
onStdout(predicate: (message: string) => boolean) {
return new Promise<void>((resolve) => {
stdoutActors.push({ predicate, resolve })
Expand Down Expand Up @@ -504,6 +512,7 @@ export let css = dedent
export let html = dedent
export let ts = dedent
export let js = dedent
export let jsx = dedent
export let json = dedent
export let yaml = dedent
export let txt = dedent
Expand Down Expand Up @@ -589,6 +598,8 @@ export async function fetchStyles(base: string, path = '/'): Promise<string> {
async function gracefullyRemove(dir: string) {
// Skip removing the directory in CI because it can stall on Windows
if (!process.env.CI) {
await fs.rm(dir, { recursive: true, force: true })
await fs.rm(dir, { recursive: true, force: true }).catch((error) => {
console.log(`Failed to remove ${dir}`, error)
})
}
}
2 changes: 1 addition & 1 deletion integrations/webpack/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css, html, js, json, test } from '../utils'

test(
'Webpack + PostCSS (watch)',
'webpack + PostCSS (watch)',
{
fs: {
'package.json': json`
Expand Down