From d996004763b137ac9f82c5036acad60de0a3d7fb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 15:47:18 +0200 Subject: [PATCH 1/8] fix typo --- integrations/webpack/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/webpack/index.test.ts b/integrations/webpack/index.test.ts index 8a4c871a78e1..2a91411d2494 100644 --- a/integrations/webpack/index.test.ts +++ b/integrations/webpack/index.test.ts @@ -1,7 +1,7 @@ import { css, html, js, json, test } from '../utils' test( - 'Webpack + PostCSS (watch)', + 'webpack + PostCSS (watch)', { fs: { 'package.json': json` From 74ce2a064781c293f4126705181214cdf90f26ee Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 2 Apr 2025 19:13:41 +0200 Subject: [PATCH 2/8] catch errors in case `fs.rm` doesn't work --- integrations/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integrations/utils.ts b/integrations/utils.ts index f4218b176958..82f88c690bf0 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -589,6 +589,8 @@ export async function fetchStyles(base: string, path = '/'): Promise { 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) + }) } } From 0a0f80d34b4ec5078ac14412c72d4c6bd9f5b30d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 15:47:45 +0200 Subject: [PATCH 3/8] add failing integration test --- integrations/postcss/next.test.ts | 130 +++++++++++++++++++++++++++++- integrations/utils.ts | 9 +++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts index dbc31ffe19a1..969254a75c8c 100644 --- a/integrations/postcss/next.test.ts +++ b/integrations/postcss/next.test.ts @@ -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', @@ -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 ( + + {children} + + ) + } + `, + 'app/page.js': js` + export default function Page() { + return
+ } + `, + '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
+ } + `, + ) + 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') + }, +) diff --git a/integrations/utils.ts b/integrations/utils.ts index 82f88c690bf0..a1165b1da6f7 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -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 onStderr: (predicate: (message: string) => boolean) => Promise } @@ -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((resolve) => { stdoutActors.push({ predicate, resolve }) @@ -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 From a63b1fa115e0b22aa2f4e06e87519c7f19ebda25 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 15:51:25 +0200 Subject: [PATCH 4/8] do not use mutable self --- crates/oxide/src/scanner/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index 612f5a8648e3..ad5f8edcb451 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -321,7 +321,7 @@ impl Scanner { } #[tracing::instrument(skip_all)] - pub fn get_normalized_sources(&mut self) -> Vec { + pub fn get_normalized_sources(&self) -> Vec { self.sources .iter() .filter_map(|source| match source { From c80631d2314cc8a05246d87f282146b37091e940 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 15:51:47 +0200 Subject: [PATCH 5/8] use cached `self.globs` --- crates/oxide/src/scanner/mod.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index ad5f8edcb451..fa40b90a422b 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -93,7 +93,7 @@ pub struct Scanner { dirs: Vec, /// All generated globs, used for setting up watchers - globs: Vec, + globs: Option>, /// Track unique set of candidates candidates: FxHashSet, @@ -296,16 +296,24 @@ impl Scanner { #[tracing::instrument(skip_all)] pub fn get_globs(&mut self) -> Vec { + 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(), }); @@ -315,9 +323,12 @@ 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)] From 9ab12c406ca06db050656cf1ec449bdfe9f15106 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 15:52:00 +0200 Subject: [PATCH 6/8] skip scanning ignored content dirs --- crates/oxide/src/scanner/detect_sources.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/oxide/src/scanner/detect_sources.rs b/crates/oxide/src/scanner/detect_sources.rs index 3a32038ddedd..9dc340fbaf47 100644 --- a/crates/oxide/src/scanner/detect_sources.rs +++ b/crates/oxide/src/scanner/detect_sources.rs @@ -1,3 +1,4 @@ +use crate::scanner::auto_source_detection::IGNORED_CONTENT_DIRS; use crate::GlobEntry; use fxhash::FxHashSet; use globwalk::DirEntry; @@ -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; } From 0622765bb1e9d39011add017ff3f88cd8e0bc845 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 16:55:00 +0200 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff31af18925d..cea17b0b232b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 223b24676313d4bbb59f747b1eaaee7ab226f48c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Apr 2025 17:06:48 +0200 Subject: [PATCH 8/8] drop the parens --- crates/oxide/src/scanner/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index fa40b90a422b..fad6fa712da4 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -307,7 +307,7 @@ impl Scanner { match source { SourceEntry::Auto { base } | SourceEntry::External { base } => { globs.extend(resolve_globs( - (base).to_path_buf(), + base.to_path_buf(), &self.dirs, &self.extensions, ));