Skip to content

Commit 81a676f

Browse files
Fix race condition in Next.js with --turbopack (#17514)
This PR fixes an issue where if you use Next.js with `--turbopack` a race condition happens because the `@tailwindcss/postcss` plugin is called twice in rapid succession. The first call sees an update and does a partial update with the new classes. Next some internal `mtimes` are updated. The second call therefore doesn't see any changes anymore because the `mtimes` are the same, therefore it's serving its stale data. Fixes: #17508 ## Test plan - Tested with the repro provided in #17508 - Added a new unit test that calls into the PostCSS plugin directly for the same change from the same JavaScript run-loop. --------- Co-authored-by: Philipp Spiess <[email protected]>
1 parent e45302b commit 81a676f

File tree

4 files changed

+82
-15
lines changed

4 files changed

+82
-15
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Fix multi-value inset shadow ([#17523](https://github.com/tailwindlabs/tailwindcss/pull/17523))
1414
- Fix `drop-shadow` utility ([#17515](https://github.com/tailwindlabs/tailwindcss/pull/17515))
1515
- Fix `drop-shadow-*` utilities that use multiple shadows in `@theme inline` ([#17515](https://github.com/tailwindlabs/tailwindcss/pull/17515))
16+
- PostCSS: Fix race condition when two changes are queued concurrently ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
17+
- PostCSS: Ensure we process files containing an `@tailwind utilities;` directive ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
1618

1719
## [4.1.1] - 2025-04-02
1820

integrations/utils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,8 @@ export async function fetchStyles(base: string, path = '/'): Promise<string> {
589589
async function gracefullyRemove(dir: string) {
590590
// Skip removing the directory in CI because it can stall on Windows
591591
if (!process.env.CI) {
592-
await fs.rm(dir, { recursive: true, force: true })
592+
await fs.rm(dir, { recursive: true, force: true }).catch((error) => {
593+
console.log(`Failed to remove ${dir}`, error)
594+
})
593595
}
594596
}

packages/@tailwindcss-postcss/src/index.test.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import dedent from 'dedent'
2-
import { mkdir, mkdtemp, unlink, writeFile } from 'node:fs/promises'
2+
import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises'
33
import { tmpdir } from 'node:os'
44
import path from 'path'
55
import postcss from 'postcss'
@@ -357,3 +357,64 @@ test('runs `Once` plugins in the right order', async () => {
357357
}"
358358
`)
359359
})
360+
361+
describe('concurrent builds', () => {
362+
let dir: string
363+
beforeEach(async () => {
364+
dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss'))
365+
await writeFile(path.join(dir, 'index.html'), `<div class="underline"></div>`)
366+
await writeFile(
367+
path.join(dir, 'index.css'),
368+
css`
369+
@import './dependency.css';
370+
`,
371+
)
372+
await writeFile(
373+
path.join(dir, 'dependency.css'),
374+
css`
375+
@tailwind utilities;
376+
`,
377+
)
378+
})
379+
afterEach(() => rm(dir, { recursive: true, force: true }))
380+
381+
test('the current working directory is used by default', async () => {
382+
const spy = vi.spyOn(process, 'cwd')
383+
spy.mockReturnValue(dir)
384+
385+
let from = path.join(dir, 'index.css')
386+
let input = (await readFile(path.join(dir, 'index.css'))).toString()
387+
388+
let plugin = tailwindcss({ optimize: { minify: false } })
389+
390+
async function run(input: string): Promise<string> {
391+
let ast = postcss.parse(input)
392+
for (let runner of (plugin as any).plugins) {
393+
if (runner.Once) {
394+
await runner.Once(ast, { result: { opts: { from }, messages: [] } })
395+
}
396+
}
397+
return ast.toString()
398+
}
399+
400+
let result = await run(input)
401+
402+
expect(result).toContain('.underline')
403+
404+
await writeFile(
405+
path.join(dir, 'dependency.css'),
406+
css`
407+
@tailwind utilities;
408+
.red {
409+
color: red;
410+
}
411+
`,
412+
)
413+
414+
let promise1 = run(input)
415+
let promise2 = run(input)
416+
417+
expect(await promise1).toContain('.red')
418+
expect(await promise2).toContain('.red')
419+
})
420+
})

packages/@tailwindcss-postcss/src/index.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const DEBUG = env.DEBUG
2020

2121
interface CacheEntry {
2222
mtimes: Map<string, number>
23-
compiler: null | Awaited<ReturnType<typeof compileAst>>
23+
compiler: null | ReturnType<typeof compileAst>
2424
scanner: null | Scanner
2525
tailwindCssAst: AstNode[]
2626
cachedPostCssAst: postcss.Root
@@ -89,7 +89,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
8989
node.name === 'variant' ||
9090
node.name === 'config' ||
9191
node.name === 'plugin' ||
92-
node.name === 'apply'
92+
node.name === 'apply' ||
93+
node.name === 'tailwind'
9394
) {
9495
canBail = false
9596
return false
@@ -138,9 +139,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
138139

139140
// Setup the compiler if it doesn't exist yet. This way we can
140141
// guarantee a `build()` function is available.
141-
context.compiler ??= await createCompiler()
142+
context.compiler ??= createCompiler()
142143

143-
if (context.compiler.features === Features.None) {
144+
if ((await context.compiler).features === Features.None) {
144145
return
145146
}
146147

@@ -188,37 +189,38 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
188189
// initial build. If it wasn't, we need to create a new one.
189190
!isInitialBuild
190191
) {
191-
context.compiler = await createCompiler()
192+
context.compiler = createCompiler()
192193
}
193194

195+
let compiler = await context.compiler
196+
194197
if (context.scanner === null || rebuildStrategy === 'full') {
195198
DEBUG && I.start('Setup scanner')
196199
let sources = (() => {
197200
// Disable auto source detection
198-
if (context.compiler.root === 'none') {
201+
if (compiler.root === 'none') {
199202
return []
200203
}
201204

202205
// No root specified, use the base directory
203-
if (context.compiler.root === null) {
206+
if (compiler.root === null) {
204207
return [{ base, pattern: '**/*', negated: false }]
205208
}
206209

207210
// Use the specified root
208-
return [{ ...context.compiler.root, negated: false }]
209-
})().concat(context.compiler.sources)
211+
return [{ ...compiler.root, negated: false }]
212+
})().concat(compiler.sources)
210213

211214
// Look for candidates used to generate the CSS
212215
context.scanner = new Scanner({ sources })
213216
DEBUG && I.end('Setup scanner')
214217
}
215218

216219
DEBUG && I.start('Scan for candidates')
217-
let candidates =
218-
context.compiler.features & Features.Utilities ? context.scanner.scan() : []
220+
let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : []
219221
DEBUG && I.end('Scan for candidates')
220222

221-
if (context.compiler.features & Features.Utilities) {
223+
if (compiler.features & Features.Utilities) {
222224
DEBUG && I.start('Register dependency messages')
223225
// Add all found files as direct dependencies
224226
for (let file of context.scanner.files) {
@@ -267,7 +269,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
267269
}
268270

269271
DEBUG && I.start('Build utilities')
270-
let tailwindCssAst = context.compiler.build(candidates)
272+
let tailwindCssAst = compiler.build(candidates)
271273
DEBUG && I.end('Build utilities')
272274

273275
if (context.tailwindCssAst !== tailwindCssAst) {

0 commit comments

Comments
 (0)