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 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) + }) } } 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 5a76aba13acb..e728c2f9506e 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 @@ -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 @@ -138,9 +139,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 +189,27 @@ 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() } + let compiler = await context.compiler + if (context.scanner === null || rebuildStrategy === 'full') { DEBUG && I.start('Setup scanner') 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 }) @@ -214,11 +217,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } DEBUG && I.start('Scan for candidates') - let candidates = - context.compiler.features & Features.Utilities ? context.scanner.scan() : [] + let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : [] DEBUG && I.end('Scan for candidates') - if (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) { @@ -267,7 +269,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } DEBUG && I.start('Build utilities') - let tailwindCssAst = context.compiler.build(candidates) + let tailwindCssAst = compiler.build(candidates) DEBUG && I.end('Build utilities') if (context.tailwindCssAst !== tailwindCssAst) {