From dd38bbe2693df27cc042b3900ea18bd2df2e4195 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 2 Apr 2025 18:57:05 +0200 Subject: [PATCH 1/6] add failing integration test --- integrations/postcss/next.test.ts | 91 +++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts index dbc31ffe19a1..fbc83a78bf9e 100644 --- a/integrations/postcss/next.test.ts +++ b/integrations/postcss/next.test.ts @@ -356,3 +356,94 @@ test( }) }, ) + +test( + 'changes to CSS file should immediately reflect with HMR using `--turbopack`', + { + fs: { + 'package.json': json` + { + "dependencies": { + "react": "^18", + "react-dom": "^18", + "next": "^15" + }, + "devDependencies": { + "@tailwindcss/postcss": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + '.gitignore': ` + node_modules + .next/ + `, + '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 ( + + {children} + + ) + } + `, + 'app/page.js': js` + export default function Page() { + return
+ } + `, + 'app/globals.css': css` + @import 'tailwindcss/utilities' source(none); + + body { + --value: 'before change'; + } + `, + }, + }, + async ({ spawn, fs, expect }) => { + let process = await spawn(`pnpm next dev --turbopack`) + + 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('before change') + }) + + await fs.write( + 'app/globals.css', + css` + @import 'tailwindcss/utilities' source(none); + + body { + --value: 'after change'; + } + `, + ) + + await process.onStdout((m) => m.includes('Compiled / in')) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).not.toContain('before change') + expect(css).toContain('after change') + }) + }, +) From 2df0e59770903842c7ce0940e9731c80456dcce2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 2 Apr 2025 19:13:41 +0200 Subject: [PATCH 2/6] 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 6235e881b267008742b3f0e7268b03d2e66bc73e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 2 Apr 2025 18:47:05 +0200 Subject: [PATCH 3/6] re-use compiler promise --- packages/@tailwindcss-postcss/src/index.ts | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 5a76aba13acb..759cbbd07991 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -20,7 +20,7 @@ const DEBUG = env.DEBUG interface CacheEntry { mtimes: Map - compiler: null | Awaited> + compiler: null | ReturnType scanner: null | Scanner tailwindCssAst: AstNode[] cachedPostCssAst: postcss.Root @@ -138,9 +138,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // Setup the compiler if it doesn't exist yet. This way we can // guarantee a `build()` function is available. - context.compiler ??= await createCompiler() + context.compiler ??= createCompiler() - if (context.compiler.features === Features.None) { + if ((await context.compiler).features === Features.None) { return } @@ -188,25 +188,26 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // initial build. If it wasn't, we need to create a new one. !isInitialBuild ) { - context.compiler = await createCompiler() + context.compiler = createCompiler() } if (context.scanner === null || rebuildStrategy === 'full') { DEBUG && I.start('Setup scanner') + let compiler = await context.compiler let sources = (() => { // Disable auto source detection - if (context.compiler.root === 'none') { + if (compiler.root === 'none') { return [] } // No root specified, use the base directory - if (context.compiler.root === null) { + if (compiler.root === null) { return [{ base, pattern: '**/*', negated: false }] } // Use the specified root - return [{ ...context.compiler.root, negated: false }] - })().concat(context.compiler.sources) + return [{ ...compiler.root, negated: false }] + })().concat(compiler.sources) // Look for candidates used to generate the CSS context.scanner = new Scanner({ sources }) @@ -215,10 +216,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { DEBUG && I.start('Scan for candidates') let candidates = - context.compiler.features & Features.Utilities ? context.scanner.scan() : [] + (await context.compiler).features & Features.Utilities ? context.scanner.scan() : [] DEBUG && I.end('Scan for candidates') - if (context.compiler.features & Features.Utilities) { + if ((await context.compiler).features & Features.Utilities) { DEBUG && I.start('Register dependency messages') // Add all found files as direct dependencies for (let file of context.scanner.files) { @@ -267,7 +268,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } DEBUG && I.start('Build utilities') - let tailwindCssAst = context.compiler.build(candidates) + let tailwindCssAst = (await context.compiler).build(candidates) DEBUG && I.end('Build utilities') if (context.tailwindCssAst !== tailwindCssAst) { From 342645de46e32283eb7a89d30e84f56ae3989ac2 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 3 Apr 2025 14:43:12 +0200 Subject: [PATCH 4/6] Add a working regression test --- integrations/postcss/next.test.ts | 91 ------------------- .../@tailwindcss-postcss/src/index.test.ts | 63 ++++++++++++- packages/@tailwindcss-postcss/src/index.ts | 3 +- 3 files changed, 64 insertions(+), 93 deletions(-) diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts index fbc83a78bf9e..dbc31ffe19a1 100644 --- a/integrations/postcss/next.test.ts +++ b/integrations/postcss/next.test.ts @@ -356,94 +356,3 @@ test( }) }, ) - -test( - 'changes to CSS file should immediately reflect with HMR using `--turbopack`', - { - fs: { - 'package.json': json` - { - "dependencies": { - "react": "^18", - "react-dom": "^18", - "next": "^15" - }, - "devDependencies": { - "@tailwindcss/postcss": "workspace:^", - "tailwindcss": "workspace:^" - } - } - `, - '.gitignore': ` - node_modules - .next/ - `, - '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 ( - - {children} - - ) - } - `, - 'app/page.js': js` - export default function Page() { - return
- } - `, - 'app/globals.css': css` - @import 'tailwindcss/utilities' source(none); - - body { - --value: 'before change'; - } - `, - }, - }, - async ({ spawn, fs, expect }) => { - let process = await spawn(`pnpm next dev --turbopack`) - - 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('before change') - }) - - await fs.write( - 'app/globals.css', - css` - @import 'tailwindcss/utilities' source(none); - - body { - --value: 'after change'; - } - `, - ) - - await process.onStdout((m) => m.includes('Compiled / in')) - - await retryAssertion(async () => { - let css = await fetchStyles(url) - expect(css).not.toContain('before change') - expect(css).toContain('after change') - }) - }, -) diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 11d8e34967b4..bf7c0f97a13b 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -1,5 +1,5 @@ import dedent from 'dedent' -import { mkdir, mkdtemp, unlink, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'path' import postcss from 'postcss' @@ -357,3 +357,64 @@ test('runs `Once` plugins in the right order', async () => { }" `) }) + +describe('concurrent builds', () => { + let dir: string + beforeEach(async () => { + dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) + await writeFile(path.join(dir, 'index.html'), `
`) + await writeFile( + path.join(dir, 'index.css'), + css` + @import './dependency.css'; + `, + ) + await writeFile( + path.join(dir, 'dependency.css'), + css` + @tailwind utilities; + `, + ) + }) + afterEach(() => rm(dir, { recursive: true, force: true })) + + test('the current working directory is used by default', async () => { + const spy = vi.spyOn(process, 'cwd') + spy.mockReturnValue(dir) + + let from = path.join(dir, 'index.css') + let input = (await readFile(path.join(dir, 'index.css'))).toString() + + let plugin = tailwindcss({ optimize: { minify: false } }) + + async function run(input: string): Promise { + let ast = postcss.parse(input) + for (let runner of (plugin as any).plugins) { + if (runner.Once) { + await runner.Once(ast, { result: { opts: { from }, messages: [] } }) + } + } + return ast.toString() + } + + let result = await run(input) + + expect(result).toContain('.underline') + + await writeFile( + path.join(dir, 'dependency.css'), + css` + @tailwind utilities; + .red { + color: red; + } + `, + ) + + let promise1 = run(input) + let promise2 = run(input) + + expect(await promise1).toContain('.red') + expect(await promise2).toContain('.red') + }) +}) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 759cbbd07991..599ea635cc52 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -89,7 +89,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { node.name === 'variant' || node.name === 'config' || node.name === 'plugin' || - node.name === 'apply' + node.name === 'apply' || + node.name === 'tailwind' ) { canBail = false return false From 6cd6f83c2df5ba041afb56bc853519496c7a4165 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 3 Apr 2025 14:46:25 +0200 Subject: [PATCH 5/6] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff31af18925d..87585fd51e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ 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)) +- PostCSS: Fix race condition when two changes are queued concurrently ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514)) +- PostCSS: Ensure we process files containing an `@tailwind utilities;` directive ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514)) ## [4.1.1] - 2025-04-02 From 10b0c766ea657fafe191a35ed5dfc2b092356440 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 3 Apr 2025 16:39:08 +0200 Subject: [PATCH 6/6] Put compiler into a var --- packages/@tailwindcss-postcss/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 599ea635cc52..e728c2f9506e 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -192,9 +192,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { context.compiler = createCompiler() } + let compiler = await context.compiler + if (context.scanner === null || rebuildStrategy === 'full') { DEBUG && I.start('Setup scanner') - let compiler = await context.compiler let sources = (() => { // Disable auto source detection if (compiler.root === 'none') { @@ -216,11 +217,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } DEBUG && I.start('Scan for candidates') - let candidates = - (await context.compiler).features & Features.Utilities ? context.scanner.scan() : [] + let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : [] DEBUG && I.end('Scan for candidates') - if ((await context.compiler).features & Features.Utilities) { + if (compiler.features & Features.Utilities) { DEBUG && I.start('Register dependency messages') // Add all found files as direct dependencies for (let file of context.scanner.files) { @@ -269,7 +269,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } DEBUG && I.start('Build utilities') - let tailwindCssAst = (await context.compiler).build(candidates) + let tailwindCssAst = compiler.build(candidates) DEBUG && I.end('Build utilities') if (context.tailwindCssAst !== tailwindCssAst) {