Merge ff8a6f67e8 into 41d9cae8e5
This commit is contained in:
commit
5b5b5310aa
3 changed files with 99 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
82
integrations/vite/plugin-context-leak.test.ts
Normal file
82
integrations/vite/plugin-context-leak.test.ts
Normal 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')
|
||||
},
|
||||
)
|
||||
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue