Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Prevent `theme('colors.foo')` in JS plugins from returning an internal disambiguation object when a CSS theme key shares a prefix with a sibling key like `--color-foo-bar` ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
- Fix parsing selectors like `[data-foo]div` as one selector instead of two ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303))
- Fix stale `@tailwindcss/postcss` output when changed input CSS is passed from a preprocessor like Sass without updating the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310))

## [4.3.2] - 2026-06-26

Expand Down
63 changes: 50 additions & 13 deletions packages/@tailwindcss-postcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ function inputCssFilePath() {

const css = dedent

async function run(plugin: any, from: string, input: string): Promise<string> {
let ast = postcss.parse(input)
for (let runner of plugin.plugins) {
if (runner.Once) {
await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } })
}
}
return ast.toString()
}

test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
Expand Down Expand Up @@ -422,17 +432,7 @@ describe('concurrent builds', () => {

let plugin = tailwindcss({ optimize: { minify: false } })

async function run(input: string): Promise<string> {
let ast = postcss.parse(input)
for (let runner of (plugin as any).plugins) {
if (runner.Once) {
await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } })
}
}
return ast.toString()
}

let result = await run(input)
let result = await run(plugin, from, input)

expect(result).toContain('.underline')

Expand All @@ -448,14 +448,51 @@ describe('concurrent builds', () => {
`,
)

let promise1 = run(input)
let promise2 = run(input)
let promise1 = run(plugin, from, input)
let promise2 = run(plugin, from, input)

expect(await promise1).toContain('.red')
expect(await promise2).toContain('.red')
})
})

test('rebuilds when the input CSS changes even if the `from` file on disk did not', async () => {
let dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss'))
// The `from` file exists on disk with a stable mtime. The input CSS is handed
// to the plugin directly (as a preprocessor like Sass would produce it), so it
// can change without `from`'s mtime ever changing.
await writeFile(path.join(dir, 'index.css'), '')

let from = path.join(dir, 'index.css')
let plugin = tailwindcss({ base: dir, optimize: { minify: false } })

let first = await run(
plugin,
from,
css`
@tailwind utilities;
.first {
@apply underline;
}
`,
)
let second = await run(
plugin,
from,
css`
@tailwind utilities;
.second {
@apply line-through;
}
`,
)

expect(first).toContain('.first')
expect(second).toContain('.second')

await rm(dir, { recursive: true, force: true })
})

test('does not register the input file as a dependency, even if it is passed in as relative path', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
Expand Down
12 changes: 12 additions & 0 deletions packages/@tailwindcss-postcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const DEBUG = env.DEBUG

interface CacheEntry {
mtimes: Map<string, number>
inputCss: string
compiler: null | ReturnType<typeof compileAst>
scanner: null | Scanner
tailwindCssAst: AstNode[]
Expand All @@ -34,6 +35,7 @@ function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOp
if (cache.has(key)) return cache.get(key)!
let entry = {
mtimes: new Map<string, number>(),
inputCss: '',
compiler: null,
scanner: null,

Expand Down Expand Up @@ -194,6 +196,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
rebuildStrategy = 'full'
context.mtimes.set(file, changedTime)
}

// The mtimes above only track files on disk. When the input CSS is
// generated by another tool (e.g. Sass) and passed to `process()`,
// it can change without any tracked file's mtime changing, so also
// rebuild when the input CSS itself differs from the last build.
let inputCss = root.source?.input.css ?? root.toString()
if (context.inputCss !== inputCss) {
rebuildStrategy = 'full'
context.inputCss = inputCss
}
}
DEBUG && I.end('Register full rebuild paths')

Expand Down