This commit is contained in:
Lazizbek Ergashev 2026-09-21 04:04:04 +00:00 • committed by GitHub
commit 5b5b5310aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 0 deletions

View file

@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466))
- `@tailwindcss/vite` no longer keeps a finished build's plugin context alive, which could retain multiple GB of memory in long-lived build processes (e.g. Astro static builds) ([#20503](https://github.com/tailwindlabs/tailwindcss/pull/20503))
## [4.3.3] - 2026-07-16

View file

@ -0,0 +1,82 @@
import { css, js, json, test } from '../utils'
// The build plugin used to cache a `Root` (and its compiler) for the
// lifetime of the plugin instance without ever releasing it, which kept the
// `PluginContext` of the `transform` call that created it (and everything
// reachable through it, e.g. a finished Rolldown bundle) alive long after
// the build finished. Only observable with Vite 8's Rolldown bundler, not
// with Rollup.
//
// https://github.com/tailwindlabs/tailwindcss/issues/20501
test(
'does not keep the transform PluginContext alive after a build finishes',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'src/app.css': css`@import 'tailwindcss';`,
'src/main.js': js`import './app.css'`,
'probe.mjs': js`
import { build } from 'vite'
import tailwindcss from '@tailwindcss/vite'
const NAME = '@tailwindcss/vite:generate:build'
let ctxRef = null
let probeRan = false
function instrument(plugins) {
return plugins.map((plugin) => {
if (plugin.name !== NAME) return plugin
let original = plugin.transform.handler
return {
...plugin,
transform: {
...plugin.transform,
handler(...args) {
ctxRef ??= new WeakRef(this)
probeRan = true
return original.apply(this, args)
},
},
}
})
}
await build({
root: import.meta.dirname,
logLevel: 'error',
configFile: false,
build: {
write: false,
lib: { entry: 'src/main.js', formats: ['es'], fileName: 'out' },
},
plugins: [instrument(tailwindcss())],
})
for (let i = 0; i < 5; i++) {
global.gc()
await new Promise((resolve) => setImmediate(resolve))
}
console.log('probeRan=' + probeRan)
console.log('pluginContextAlive=' + (ctxRef?.deref() !== undefined))
`,
},
},
async ({ exec, expect }) => {
let output = await exec('node --expose-gc probe.mjs')
expect(output).toContain('probeRan=true')
expect(output).toContain('pluginContextAlive=false')
},
)

View file

@ -282,6 +282,22 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
return result
},
},
closeBundle() {
// In `--watch` mode, `closeBundle` fires after every rebuild, not
// just once at the end — clearing the roots here would throw away
// the mtime-based rebuild cache `Root` relies on and force a full
// recompile on every change. Only clear once the build itself is
// done for good.
if (config?.build.watch) return
// Roots hold on to the compiler (and, through it, the `PluginContext`
// of whichever transform call created it) for the lifetime of the
// plugin instance. Drop them once the bundle is done so a long-lived
// process (e.g. Astro's SSR build followed by static route
// generation) doesn't keep the finished build's context alive.
rootsByEnv.get(this.environment?.name ?? 'default').clear()
},
},
] satisfies Plugin[]
}