Do not retain the finished build's plugin context in @tailwindcss/vite

This commit is contained in:
lazerg 2026-09-21 08:41:58 +05:00
parent 41d9cae8e5
commit 0dd6923d5e
No known key found for this signature in database
2 changed files with 87 additions and 0 deletions

View file

@ -0,0 +1,78 @@
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
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)
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('pluginContextAlive=' + (ctxRef?.deref() !== undefined))
`,
},
},
async ({ exec, expect }) => {
let output = await exec('node --expose-gc probe.mjs')
expect(output).toContain('pluginContextAlive=false')
},
)

View file

@ -282,6 +282,15 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
return result
},
},
closeBundle() {
// 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[]
}