Skip to content

Commit b0300a4

Browse files
committed
fix: 修复当 cssEntries 指向子目录文件时强制重写 Tailwind v4 base 的问题
1 parent bd16aaa commit b0300a4

File tree

10 files changed

+222
-66
lines changed

10 files changed

+222
-66
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'weapp-tailwindcss': patch
3+
---
4+
5+
修复当 `cssEntries` 指向子目录文件时强制重写 Tailwind v4 `base` 的问题,优先沿用工作区/用户指定根目录并在多包场景下智能分组;补充整合测试确保通过 `getCompilerContext` 仍能识别子目录样式并正确重写 `bg-[#00aa55]` 这类动态类名。

benchmark/data/2025-11-27.json

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
592.9260840000006,
66
962.3252920000014,
77
589.087125,
8-
870.4599589999998
8+
870.4599589999998,
9+
180.84000000000015
910
]
1011
},
1112
"rax": {
@@ -20,7 +21,12 @@
2021
663.5587500000001,
2122
1847.5564999999988,
2223
772.8657909999993,
23-
1215.4829580000005
24+
1215.4829580000005,
25+
130.59170899999936,
26+
135.9237919999996,
27+
139.932417,
28+
222.61924999999974,
29+
248.11099999999988
2430
]
2531
},
2632
"taro-react": {
@@ -39,7 +45,10 @@
3945
294.9744999999966,
4046
2141.2788329999967,
4147
2651.306875000002,
42-
474.655709000006
48+
474.655709000006,
49+
450.9234579999975,
50+
153.07587500000227,
51+
187.0468329999967
4352
]
4453
},
4554
"uni-app-webpack-vue2": {
@@ -48,7 +57,8 @@
4857
2489.4247499999983,
4958
496.32066699999996,
5059
1445.2460830000018,
51-
1538.2430000000022
60+
1538.2430000000022,
61+
736.6286670000009
5262
]
5363
},
5464
"native-webpack": {
@@ -57,7 +67,8 @@
5767
187.3241250000001,
5868
494.0784999999996,
5969
296.815208,
60-
451.606541000001
70+
451.606541000001,
71+
62.77299999999991
6172
]
6273
},
6374
"taro-vue3": {
@@ -66,7 +77,8 @@
6677
866.9951660000006,
6778
293.1339169999992,
6879
590.4976250000036,
69-
564.633249999999
80+
564.633249999999,
81+
170.9773750000004
7082
]
7183
},
7284
"mpx": {
@@ -87,7 +99,14 @@
8799
459.89008399999875,
88100
3610.6299580000014,
89101
2283.188292,
90-
2122.0997499999994
102+
2122.0997499999994,
103+
53.40770899999916,
104+
362.8924590000006,
105+
182.1295829999981,
106+
183.09991699999955,
107+
144.08699999999953,
108+
30.053584000001138,
109+
328.2539170000018
91110
]
92111
}
93112
}

packages/weapp-tailwindcss/src/context/tailwindcss.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,44 @@ function normalizeCssEntries(entries: string[] | undefined, anchor: string): str
258258
return normalized.size > 0 ? [...normalized] : undefined
259259
}
260260

261-
function groupCssEntriesByBase(entries: string[]) {
261+
interface GroupCssEntriesOptions {
262+
preferredBaseDir?: string
263+
workspaceRoot?: string
264+
}
265+
266+
function isSubPath(parent: string | undefined, child: string | undefined) {
267+
if (!parent || !child) {
268+
return false
269+
}
270+
const relative = path.relative(parent, child)
271+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
272+
}
273+
274+
function resolveCssEntryBase(entryDir: string, options: GroupCssEntriesOptions): string {
275+
const normalizedDir = path.normalize(entryDir)
276+
const { preferredBaseDir, workspaceRoot } = options
277+
if (preferredBaseDir && isSubPath(preferredBaseDir, normalizedDir)) {
278+
return preferredBaseDir
279+
}
280+
if (workspaceRoot && isSubPath(workspaceRoot, normalizedDir)) {
281+
return workspaceRoot
282+
}
283+
const packageRoot = findNearestPackageRoot(normalizedDir)
284+
if (packageRoot) {
285+
return path.normalize(packageRoot)
286+
}
287+
return normalizedDir
288+
}
289+
290+
function groupCssEntriesByBase(entries: string[], options: GroupCssEntriesOptions = {}) {
291+
const normalizedOptions: GroupCssEntriesOptions = {
292+
preferredBaseDir: options.preferredBaseDir ? path.normalize(options.preferredBaseDir) : undefined,
293+
workspaceRoot: options.workspaceRoot ? path.normalize(options.workspaceRoot) : undefined,
294+
}
262295
const groups = new Map<string, string[]>()
263296
for (const entry of entries) {
264-
const baseDir = path.normalize(path.dirname(entry))
297+
const entryDir = path.dirname(entry)
298+
const baseDir = resolveCssEntryBase(entryDir, normalizedOptions)
265299
const bucket = groups.get(baseDir)
266300
if (bucket) {
267301
bucket.push(entry)
@@ -537,8 +571,14 @@ export function createTailwindcssPatcherFromContext(ctx: InternalUserDefinedOpti
537571
appType,
538572
}
539573

574+
const workspaceRoot = findWorkspaceRoot(resolvedTailwindcssBasedir)
575+
?? (absoluteCssEntryBasedir ? findWorkspaceRoot(absoluteCssEntryBasedir) : undefined)
576+
540577
const groupedCssEntries = normalizedCssEntries
541-
? groupCssEntriesByBase(normalizedCssEntries)
578+
? groupCssEntriesByBase(normalizedCssEntries, {
579+
preferredBaseDir: resolvedTailwindcssBasedir,
580+
workspaceRoot,
581+
})
542582
: undefined
543583

544584
const multiPatcher = groupedCssEntries
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import path from 'node:path'
2+
import { escape } from '@weapp-core/escape'
3+
import { describe, expect, it } from 'vitest'
4+
import { getCompilerContext } from '@/context'
5+
import { collectRuntimeClassSet } from '@/tailwindcss/runtime'
6+
7+
describe('cssEntries integration', () => {
8+
it('collects class names from nested css entries and rewrites arbitrary classes', async () => {
9+
const projectRoot = path.resolve(__dirname, '..', 'fixtures', 'tailwind-v4-app')
10+
const cssEntry = path.join(projectRoot, 'src', 'main.css')
11+
12+
const ctx = getCompilerContext({
13+
tailwindcssBasedir: projectRoot,
14+
cssEntries: [cssEntry],
15+
})
16+
17+
await ctx.twPatcher.patch()
18+
const runtimeSet = await collectRuntimeClassSet(ctx.twPatcher, { force: true, skipRefresh: true })
19+
expect(runtimeSet.size).toBeGreaterThan(0)
20+
expect(runtimeSet.has('bg-[#00aa55]')).toBe(true)
21+
const source = 'const cls = \'bg-[#00aa55]\''
22+
const result = ctx.jsHandler(source, runtimeSet)
23+
const expectedClass = escape('bg-[#00aa55]')
24+
25+
expect(result.code).toContain(expectedClass)
26+
expect(result.code).not.toContain('bg-[#00aa55]')
27+
})
28+
})

packages/weapp-tailwindcss/test/context/tailwindcss.test.ts

Lines changed: 90 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { CreateTailwindcssPatcherOptions } from '@/tailwindcss/patcher'
22
import type { InternalUserDefinedOptions, TailwindcssPatcherLike } from '@/types'
3+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
4+
import os from 'node:os'
35
import path from 'node:path'
46
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
57

@@ -63,7 +65,7 @@ describe('createTailwindcssPatcherFromContext', () => {
6365
vi.resetModules()
6466
})
6567

66-
it('creates multiple patchers when css entries resolve to different directories', async () => {
68+
it('creates multiple patchers when css entries belong to different package roots', async () => {
6769
const calls: CreateTailwindcssPatcherOptions[] = []
6870
const createdPatchers: TailwindcssPatcherLike[] = []
6971
const classSets = [['foo'], ['bar']]
@@ -77,7 +79,6 @@ describe('createTailwindcssPatcherFromContext', () => {
7779
const stub: TailwindcssPatcherLike = {
7880
packageInfo: { version: '4.1.0' } as any,
7981
majorVersion: 4,
80-
// 测试仅校验结构传递,避免在此处施加过严的类型约束
8182
options: options as any,
8283
patch: vi.fn(async () => ({})),
8384
getClassSet: vi.fn(async () => new Set(classes)),
@@ -93,39 +94,57 @@ describe('createTailwindcssPatcherFromContext', () => {
9394
})
9495

9596
const { createTailwindcssPatcherFromContext } = await import('@/context/tailwindcss')
96-
const workspace = path.resolve('/workspace/project')
97+
98+
const workspaceTemp = mkdtempSync(path.join(os.tmpdir(), 'weapp-tw-workspace-'))
99+
const workspace = path.join(workspaceTemp, 'project')
100+
mkdirSync(path.join(workspace, 'apps', 'alpha', 'src'), { recursive: true })
101+
writeFileSync(path.join(workspace, 'package.json'), JSON.stringify({ name: 'workspace-root' }))
102+
writeFileSync(path.join(workspace, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n')
103+
writeFileSync(path.join(workspace, 'apps', 'alpha', 'package.json'), JSON.stringify({ name: 'alpha' }))
97104
const entryA = path.join(workspace, 'apps', 'alpha', 'src', 'app.css')
98-
const entryB = path.join(workspace, 'apps', 'beta', 'src', 'app.css')
99-
const ctx = {
100-
tailwindcssBasedir: workspace,
101-
supportCustomLengthUnitsPatch: undefined,
102-
tailwindcss: undefined,
103-
tailwindcssPatcherOptions: undefined,
104-
cssEntries: [entryA, entryB],
105-
appType: 'taro',
106-
} as unknown as InternalUserDefinedOptions
107-
108-
const patcher = createTailwindcssPatcherFromContext(ctx)
109-
110-
expect(calls).toHaveLength(2)
111-
expect(calls.map(call => call.basedir)).toEqual([
112-
path.dirname(entryA),
113-
path.dirname(entryB),
114-
])
115-
116-
await patcher.patch()
117-
expect(createdPatchers[0].patch).toHaveBeenCalledTimes(1)
118-
expect(createdPatchers[1].patch).toHaveBeenCalledTimes(1)
119-
120-
const classSet = await patcher.getClassSet()
121-
expect(Array.from(classSet)).toEqual(['foo', 'bar'])
122-
123-
const extracted = await patcher.extract({})
124-
expect(Array.from(extracted.classSet)).toEqual(['foo', 'bar'])
125-
expect(extracted.classList).toEqual(['foo', 'bar'])
105+
106+
const externalTemp = mkdtempSync(path.join(os.tmpdir(), 'weapp-tw-external-'))
107+
const externalRoot = path.join(externalTemp, 'external')
108+
mkdirSync(path.join(externalRoot, 'src'), { recursive: true })
109+
writeFileSync(path.join(externalRoot, 'package.json'), JSON.stringify({ name: 'external-app' }))
110+
const entryB = path.join(externalRoot, 'src', 'app.css')
111+
112+
try {
113+
const ctx = {
114+
tailwindcssBasedir: workspace,
115+
supportCustomLengthUnitsPatch: undefined,
116+
tailwindcss: undefined,
117+
tailwindcssPatcherOptions: undefined,
118+
cssEntries: [entryA, entryB],
119+
appType: 'taro',
120+
} as unknown as InternalUserDefinedOptions
121+
122+
const patcher = createTailwindcssPatcherFromContext(ctx)
123+
124+
expect(calls).toHaveLength(2)
125+
expect(calls.map(call => call.basedir)).toEqual([
126+
workspace,
127+
externalRoot,
128+
])
129+
130+
await patcher.patch()
131+
expect(createdPatchers[0].patch).toHaveBeenCalledTimes(1)
132+
expect(createdPatchers[1].patch).toHaveBeenCalledTimes(1)
133+
134+
const classSet = await patcher.getClassSet()
135+
expect(Array.from(classSet)).toEqual(['foo', 'bar'])
136+
137+
const extracted = await patcher.extract({})
138+
expect(Array.from(extracted.classSet)).toEqual(['foo', 'bar'])
139+
expect(extracted.classList).toEqual(['foo', 'bar'])
140+
}
141+
finally {
142+
rmSync(workspaceTemp, { recursive: true, force: true })
143+
rmSync(externalTemp, { recursive: true, force: true })
144+
}
126145
})
127146

128-
it('returns a single patcher when css entries share the same base directory', async () => {
147+
it('returns a single patcher when css entries share the same workspace base', async () => {
129148
const createdPatchers: TailwindcssPatcherLike[] = []
130149
const calls: CreateTailwindcssPatcherOptions[] = []
131150
const createTailwindcssPatcher = vi.fn((options: CreateTailwindcssPatcherOptions) => {
@@ -150,27 +169,44 @@ describe('createTailwindcssPatcherFromContext', () => {
150169
vi.doMock('@/tailwindcss', () => ({ createTailwindcssPatcher }))
151170

152171
const { createTailwindcssPatcherFromContext } = await import('@/context/tailwindcss')
153-
const workspace = path.resolve('/workspace/project')
154-
const baseDir = path.join(workspace, 'apps', 'alpha', 'src')
155-
const ctx = {
156-
tailwindcssBasedir: undefined,
157-
supportCustomLengthUnitsPatch: undefined,
158-
tailwindcss: undefined,
159-
tailwindcssPatcherOptions: undefined,
160-
cssEntries: [
161-
path.join(baseDir, 'app.css'),
162-
path.join(baseDir, 'other.css'),
163-
],
164-
appType: 'taro',
165-
} as unknown as InternalUserDefinedOptions
166-
167-
const patcher = createTailwindcssPatcherFromContext(ctx)
168-
169-
expect(createTailwindcssPatcher).toHaveBeenCalledTimes(1)
170-
expect(patcher).toBe(createdPatchers[0])
171-
expect(calls[0].tailwindcss?.v4?.cssEntries).toEqual([
172-
path.join(baseDir, 'app.css'),
173-
path.join(baseDir, 'other.css'),
174-
])
172+
const workspaceTemp = mkdtempSync(path.join(os.tmpdir(), 'weapp-tw-single-'))
173+
const workspace = path.join(workspaceTemp, 'project')
174+
mkdirSync(path.join(workspace, 'apps', 'alpha', 'src'), { recursive: true })
175+
mkdirSync(path.join(workspace, 'apps', 'beta', 'src'), { recursive: true })
176+
writeFileSync(path.join(workspace, 'package.json'), JSON.stringify({ name: 'workspace-root' }))
177+
writeFileSync(path.join(workspace, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n')
178+
writeFileSync(path.join(workspace, 'apps', 'alpha', 'package.json'), JSON.stringify({ name: 'alpha' }))
179+
writeFileSync(path.join(workspace, 'apps', 'beta', 'package.json'), JSON.stringify({ name: 'beta' }))
180+
const entryA = path.join(workspace, 'apps', 'alpha', 'src', 'app.css')
181+
const entryB = path.join(workspace, 'apps', 'beta', 'src', 'other.css')
182+
183+
try {
184+
const ctx = {
185+
tailwindcssBasedir: workspace,
186+
supportCustomLengthUnitsPatch: undefined,
187+
tailwindcss: undefined,
188+
tailwindcssPatcherOptions: undefined,
189+
cssEntries: [entryA, entryB],
190+
appType: 'taro',
191+
} as unknown as InternalUserDefinedOptions
192+
193+
const patcher = createTailwindcssPatcherFromContext(ctx)
194+
195+
expect(createTailwindcssPatcher).toHaveBeenCalledTimes(1)
196+
expect(patcher).toBe(createdPatchers[0])
197+
expect(calls[0].basedir).toBe(workspace)
198+
expect(calls[0].tailwindcss?.v4?.base).toBe(workspace)
199+
expect(calls[0].tailwindcss?.v4?.cssEntries).toEqual([
200+
entryA,
201+
entryB,
202+
])
203+
expect(ctx.cssEntries).toEqual([
204+
entryA,
205+
entryB,
206+
])
207+
}
208+
finally {
209+
rmSync(workspaceTemp, { recursive: true, force: true })
210+
}
175211
})
176212
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "tailwind-v4-fixture"
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<html>
3+
<body>
4+
<div class="bg-[#00aa55]">
5+
fixture
6+
</div>
7+
</body>
8+
</html>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/** @type {import('tailwindcss').Config} */
2+
module.exports = {
3+
content: [
4+
'./src/index.html',
5+
],
6+
theme: {
7+
extend: {},
8+
},
9+
corePlugins: {
10+
preflight: false,
11+
container: false,
12+
},
13+
plugins: [],
14+
}

0 commit comments

Comments
 (0)