mirror of
https://github.com/hanzoai/tailwindcss.git
synced 2026-09-24 17:03:14 +00:00
Ensure @tailwindcss/postcss rebuilds when the input CSS changes but its mtime is unchanged (#20310)
## Summary `@tailwindcss/postcss` chooses between an incremental and a full rebuild by comparing the mtimes of the entry file and its resolved `@import`/`@config`/`@plugin` graph. It never looks at the input CSS itself, so when that CSS is produced by an upstream tool (e.g. Sass) and passed to the plugin via `process()`, it can change while the `from` file's mtime stays the same. The plugin then re-emits its previously cached output and silently drops the change. This stores the input CSS per cache entry and takes the existing full rebuild path when it differs from the previous compile, mirroring the fix the CLI watcher already has for changed input files. Fixes #20307 ## Test plan - Added a regression test in `packages/@tailwindcss-postcss/src/index.test.ts` that compiles two different inputs for the same on-disk `from` file (unchanged mtime) and asserts the second compile reflects the new CSS. - Confirmed it fails on `main` (the second compile returns the stale first output) and passes with the fix. - Ran the `@tailwindcss/postcss` package tests (all green) and checked formatting with Prettier. --------- Co-authored-by: Robin Malfait <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Robin Malfait
parent
532f1b22ae
commit
a860e45a8c
@@ -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
|
||||
|
||||
|
||||
@@ -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 } }),
|
||||
@@ -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')
|
||||
|
||||
@@ -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 } }),
|
||||
|
||||
@@ -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[]
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user