Skip to content

Commit 395e22f

Browse files
committed
fix(tailwindcss-patch): refresh v3 runtime state
1 parent 596ca34 commit 395e22f

4 files changed

Lines changed: 164 additions & 2 deletions

File tree

.changeset/few-rice-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tailwindcss-patch": patch
3+
---
4+
5+
Fix Tailwind CSS v3 runtime context refresh so removed classes are dropped correctly across repeated patcher recreations, including HMR-style update flows that add and then remove content classes in the same process.

packages/tailwindcss-patch/src/api/tailwindcss-patcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ export class TailwindcssPatcher {
102102
}
103103

104104
private readonly cacheStore: CacheStore
105-
private patchMemo?: PatchMemo
106-
private inFlightBuild?: Promise<void>
105+
private patchMemo: PatchMemo | undefined
106+
private inFlightBuild: Promise<void> | undefined
107107

108108
constructor(options: TailwindcssPatcherInitOptions = {}) {
109109
const resolvedOptions: TailwindcssPatchOptions

packages/tailwindcss-patch/src/runtime/process-tailwindcss.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createRequire } from 'node:module'
2+
import fs from 'fs-extra'
23
import path from 'pathe'
34
import postcss from 'postcss'
45
import { loadConfig } from 'tailwindcss-config'
@@ -12,6 +13,68 @@ export interface TailwindBuildOptions {
1213
postcssPlugin?: string
1314
}
1415

16+
function resolveModuleEntry(id: string) {
17+
return path.isAbsolute(id) ? id : require.resolve(id)
18+
}
19+
20+
function resolvePackageRootFromEntry(entry: string) {
21+
let current = path.dirname(entry)
22+
while (current && current !== path.dirname(current)) {
23+
const packageJsonPath = path.join(current, 'package.json')
24+
if (fs.pathExistsSync(packageJsonPath)) {
25+
return current
26+
}
27+
current = path.dirname(current)
28+
}
29+
return undefined
30+
}
31+
32+
function clearTailwindV3RuntimeState(pluginName: string) {
33+
try {
34+
const entry = resolveModuleEntry(pluginName)
35+
const root = resolvePackageRootFromEntry(entry)
36+
if (!root) {
37+
return
38+
}
39+
40+
const sharedStatePath = path.join(root, 'lib/lib/sharedState.js')
41+
if (!fs.pathExistsSync(sharedStatePath)) {
42+
return
43+
}
44+
45+
const sharedState = require.cache[sharedStatePath]?.exports as
46+
| {
47+
contextMap?: Map<unknown, unknown>
48+
configContextMap?: Map<unknown, unknown>
49+
contextSourcesMap?: Map<unknown, unknown>
50+
sourceHashMap?: Map<unknown, unknown>
51+
}
52+
| undefined
53+
sharedState?.contextMap?.clear()
54+
sharedState?.configContextMap?.clear()
55+
sharedState?.contextSourcesMap?.clear()
56+
sharedState?.sourceHashMap?.clear()
57+
58+
for (const candidate of ['lib/plugin.js', 'lib/index.js']) {
59+
const runtimeEntry = path.join(root, candidate)
60+
if (!fs.pathExistsSync(runtimeEntry)) {
61+
continue
62+
}
63+
64+
const runtimeModule = require.cache[runtimeEntry]?.exports as
65+
| {
66+
contextRef?: { value?: unknown[] }
67+
}
68+
| undefined
69+
runtimeModule?.contextRef?.value?.splice(0, runtimeModule.contextRef.value.length)
70+
break
71+
}
72+
}
73+
catch {
74+
// best-effort cleanup for Tailwind v3 runtime state
75+
}
76+
}
77+
1578
async function resolveConfigPath(options: TailwindBuildOptions) {
1679
if (options.config && path.isAbsolute(options.config)) {
1780
return options.config
@@ -28,6 +91,10 @@ export async function runTailwindBuild(options: TailwindBuildOptions) {
2891
const configPath = await resolveConfigPath(options)
2992
const pluginName = options.postcssPlugin ?? (options.majorVersion === 4 ? '@tailwindcss/postcss' : 'tailwindcss')
3093

94+
if (options.majorVersion === 3) {
95+
clearTailwindV3RuntimeState(pluginName)
96+
}
97+
3198
if (options.majorVersion === 4) {
3299
return postcss([
33100
require(pluginName)({

packages/tailwindcss-patch/test/api.tailwindcss-patcher.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createRequire } from 'node:module'
12
import os from 'node:os'
23
import fs from 'fs-extra'
34
import path from 'pathe'
@@ -11,6 +12,7 @@ import * as contextRegistry from '@/runtime/context-registry'
1112
import * as runtimeBuild from '@/runtime/process-tailwindcss'
1213

1314
const fixturesRoot = path.resolve(__dirname, 'fixtures/v4')
15+
const require = createRequire(import.meta.url)
1416
let tempDir: string
1517

1618
beforeEach(async () => {
@@ -276,6 +278,9 @@ describe('TailwindcssPatcher', () => {
276278
dir: tempDir,
277279
file: 'cache.json',
278280
},
281+
tailwind: {
282+
versionHint: 3,
283+
},
279284
})
280285

281286
const classCache = new Map<string, any>([
@@ -312,6 +317,9 @@ describe('TailwindcssPatcher', () => {
312317
file: 'cache.json',
313318
strategy: 'overwrite',
314319
},
320+
tailwind: {
321+
versionHint: 3,
322+
},
315323
})
316324

317325
vi.spyOn(patcher, 'getContexts').mockReturnValue([
@@ -333,6 +341,9 @@ describe('TailwindcssPatcher', () => {
333341
const patcher = new TailwindcssPatcher({
334342
overwrite: false,
335343
cache: false,
344+
tailwind: {
345+
versionHint: 3,
346+
},
336347
})
337348

338349
vi.spyOn(patcher, 'getContexts').mockImplementation(() => contexts)
@@ -658,6 +669,9 @@ describe('TailwindcssPatcher', () => {
658669
dir: tempDir,
659670
file: 'cache.json',
660671
},
672+
tailwind: {
673+
versionHint: 3,
674+
},
661675
})
662676

663677
vi.spyOn(patcher, 'getContexts').mockReturnValue([
@@ -691,6 +705,9 @@ describe('TailwindcssPatcher', () => {
691705
file: 'cache.json',
692706
strategy: 'overwrite',
693707
},
708+
tailwind: {
709+
versionHint: 3,
710+
},
694711
})
695712

696713
vi.spyOn(patcher, 'getContexts').mockReturnValue([
@@ -714,6 +731,9 @@ describe('TailwindcssPatcher', () => {
714731
dir: tempDir,
715732
file: 'cache.json',
716733
},
734+
tailwind: {
735+
versionHint: 3,
736+
},
717737
})
718738

719739
const getContextsSpy = vi.spyOn(patcher, 'getContexts')
@@ -738,4 +758,74 @@ describe('TailwindcssPatcher', () => {
738758
expect(second?.has('second-pass')).toBe(true)
739759
expect(second?.has('first-pass')).toBe(false)
740760
})
761+
762+
it('drops removed v3 content classes after recreating the patcher', async () => {
763+
const projectRoot = await fs.mkdtemp(path.join(tempDir, 'v3-refresh-'))
764+
const tailwindRoot = path.dirname(require.resolve('tailwindcss-3/package.json'))
765+
const postcssPlugin = require.resolve('tailwindcss-3')
766+
const wxmlFile = path.join(projectRoot, 'src/index.wxml')
767+
const marker = 'text-red-500'
768+
769+
try {
770+
await fs.ensureDir(path.join(projectRoot, 'src'))
771+
await fs.writeFile(
772+
path.join(projectRoot, 'tailwind.config.js'),
773+
[
774+
'const path = require(\'path\')',
775+
'module.exports = {',
776+
' content: [path.resolve(__dirname, "./src/*.{js,wxml}")],',
777+
' theme: { extend: {} },',
778+
' plugins: [],',
779+
' corePlugins: { preflight: false },',
780+
'}',
781+
].join('\n'),
782+
'utf8',
783+
)
784+
const original = '<view class="font-bold">baseline</view>\n'
785+
await fs.writeFile(wxmlFile, original, 'utf8')
786+
787+
const createPatcher = () => new TailwindcssPatcher({
788+
projectRoot,
789+
cache: false,
790+
output: {
791+
enabled: false,
792+
},
793+
tailwind: {
794+
packageName: 'tailwindcss-3',
795+
version: 3,
796+
resolve: {
797+
paths: [path.dirname(tailwindRoot)],
798+
},
799+
cwd: projectRoot,
800+
config: path.join(projectRoot, 'tailwind.config.js'),
801+
postcssPlugin,
802+
v3: {
803+
cwd: projectRoot,
804+
config: path.join(projectRoot, 'tailwind.config.js'),
805+
postcssPlugin,
806+
},
807+
},
808+
})
809+
810+
const baselinePatcher = createPatcher()
811+
await baselinePatcher.patch()
812+
const baseline = await baselinePatcher.extract({ write: false })
813+
expect(baseline.classSet.has(marker)).toBe(false)
814+
815+
await fs.writeFile(wxmlFile, `${original}<view class="${marker}">hmr</view>\n`, 'utf8')
816+
const updatedPatcher = createPatcher()
817+
await updatedPatcher.patch()
818+
const updated = await updatedPatcher.extract({ write: false })
819+
expect(updated.classSet.has(marker)).toBe(true)
820+
821+
await fs.writeFile(wxmlFile, original, 'utf8')
822+
const restoredPatcher = createPatcher()
823+
await restoredPatcher.patch()
824+
const restored = await restoredPatcher.extract({ write: false })
825+
expect(restored.classSet.has(marker)).toBe(false)
826+
}
827+
finally {
828+
await fs.remove(projectRoot)
829+
}
830+
})
741831
})

0 commit comments

Comments
 (0)