From ff8a6f67e878960d90953e481d49de83465fc9a4 Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 21 Sep 2026 08:58:57 +0500 Subject: [PATCH] Don't clear the build plugin's Root cache during watch rebuilds --- integrations/vite/plugin-context-leak.test.ts | 4 ++++ packages/@tailwindcss-vite/src/index.ts | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/integrations/vite/plugin-context-leak.test.ts b/integrations/vite/plugin-context-leak.test.ts index 56eb05957..64d36e0d5 100644 --- a/integrations/vite/plugin-context-leak.test.ts +++ b/integrations/vite/plugin-context-leak.test.ts @@ -33,6 +33,7 @@ test( const NAME = '@tailwindcss/vite:generate:build' let ctxRef = null + let probeRan = false function instrument(plugins) { return plugins.map((plugin) => { @@ -44,6 +45,7 @@ test( ...plugin.transform, handler(...args) { ctxRef ??= new WeakRef(this) + probeRan = true return original.apply(this, args) }, }, @@ -67,12 +69,14 @@ test( 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') }, ) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 96d184033..a05e58a51 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -284,6 +284,13 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { }, 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