Skip to content

Commit 1db6ba6

Browse files
committed
fix: stabilize source scanning watch hmr
1 parent 51aa2de commit 1db6ba6

17 files changed

Lines changed: 325 additions & 62 deletions

File tree

demo/taro-webpack-vue3-tailwindcss-v3/src/pages/index/index.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,21 @@ const dynamicClass = computed(() => 'bg-[#123456] text-[#ffffff] p-[20px]')
2626
2727
useLoad(() => {})
2828
</script>
29+
30+
<style lang="scss">
31+
.test {
32+
@apply flex items-center justify-center h-[100px] w-[100px] rounded-[40px] bg-[#123456] bg-opacity-[0.54] text-[#ffffff] #{!important};
33+
}
34+
35+
.aspect-w-16 > * {
36+
color: red;
37+
}
38+
39+
.a {
40+
color: green;
41+
}
42+
43+
.b {
44+
color: yellow;
45+
}
46+
</style>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@reference "tailwindcss";
2+
13
.tw-page-style-watch-anchor {
24
color: inherit;
35
}

demo/taro-webpack-vue3-tailwindcss-v4/src/pages/index/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<script setup lang="ts">
1515
import { useLoad } from '@tarojs/taro'
16+
import './index.css'
1617
1718
useLoad(() => {})
1819
</script>

packages/weapp-tailwindcss/src/bundlers/vite/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { resolveImplicitAppTypeFromViteRoot } from './resolve-app-type'
2424
import { createRewriteCssImportsPlugins } from './rewrite-css-imports'
2525
import { createViteRuntimeClassSet } from './runtime-class-set'
2626
import { createSourceCandidateCollector, isSourceCandidateRequest } from './source-candidates'
27-
import { createViteSourceScanMatcher, resolveViteSourceScanEntries } from './source-scan'
27+
import { createViteSourceScanMatcher, resolveViteSourceScanEntries, resolveViteTailwindV4CssDependencies } from './source-scan'
2828
import { resolveImplicitTailwindcssBasedirFromViteRoot } from './tailwind-basedir'
2929
import { cleanUrl, slash } from './utils'
3030

@@ -130,9 +130,11 @@ export function WeappTailwindcss(options: UserDefinedOptions = {}): Plugin[] | u
130130
return
131131
}
132132
autoCssSourceContent.set(sourceFile, css)
133+
const dependencies = await resolveViteTailwindV4CssDependencies(css, path.dirname(sourceFile))
133134
upsertTailwindV4CssSource(opts, {
134135
file: sourceFile,
135136
css,
137+
dependencies,
136138
})
137139
debug('detected tailwindcss v4 css source from vite css module: %s', sourceFile)
138140
autoCssSourcesRefresh = (autoCssSourcesRefresh ?? Promise.resolve()).then(async () => {

packages/weapp-tailwindcss/src/bundlers/vite/source-scan.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import path from 'node:path'
55
import process from 'node:process'
66
import micromatch from 'micromatch'
77
import postcss from 'postcss'
8+
import { loadConfig } from 'tailwindcss-config'
89
import {
910
collectCssInlineSourceCandidates,
1011
createSourceScanPattern,
1112
normalizeLegacyContentEntries,
13+
parseConfigParam,
1214
resolveCssSourceEntries,
1315
} from '@/tailwindcss/source-scan'
1416
import { resolveTailwindV3SourceFromPatcher } from '@/tailwindcss/v3-engine'
@@ -72,17 +74,51 @@ function resolveSourceBase(base: string, sourcePath: string) {
7274
return path.isAbsolute(sourcePath) ? sourcePath : path.resolve(base, sourcePath)
7375
}
7476

77+
function resolveConfigPath(base: string, configPath: string) {
78+
return path.isAbsolute(configPath) ? path.resolve(configPath) : path.resolve(base, configPath)
79+
}
80+
7581
interface ResolvedTailwindV4CssEntries {
7682
entries: TailwindSourceEntry[]
7783
explicit: boolean
7884
inlineCandidates: TailwindInlineSourceCandidates
85+
dependencies: string[]
7986
}
8087

8188
export interface ResolvedViteSourceScan {
8289
entries?: TailwindSourceEntry[]
8390
inlineCandidates?: TailwindInlineSourceCandidates
8491
}
8592

93+
async function resolveConfigContentEntries(root: postcss.Root, base: string) {
94+
const configPaths = new Set<string>()
95+
root.walkAtRules('config', (rule) => {
96+
const configPath = parseConfigParam(rule.params)
97+
if (configPath) {
98+
configPaths.add(resolveConfigPath(base, configPath))
99+
}
100+
})
101+
102+
const entries: TailwindSourceEntry[] = []
103+
for (const configPath of configPaths) {
104+
try {
105+
const loaded = await loadConfig({
106+
config: configPath,
107+
cwd: path.dirname(configPath),
108+
})
109+
entries.push(...normalizeLegacyContentEntries(loaded?.config.content, path.dirname(configPath)))
110+
}
111+
catch {
112+
// 依赖收集只负责补充 watch 签名,配置有效性由 Tailwind 生成阶段校验。
113+
}
114+
}
115+
116+
return {
117+
dependencies: [...configPaths],
118+
entries,
119+
}
120+
}
121+
86122
async function resolveTailwindV4EntriesFromCss(css: string, base: string): Promise<ResolvedTailwindV4CssEntries | undefined> {
87123
let root: postcss.Root
88124
try {
@@ -94,7 +130,14 @@ async function resolveTailwindV4EntriesFromCss(css: string, base: string): Promi
94130

95131
let importSourceBase: string | undefined
96132
let hasSourceNone = false
97-
const entries = await resolveCssSourceEntries(root, base, VITE_SOURCE_CANDIDATE_PATTERN)
133+
const [sourceEntries, configEntries] = await Promise.all([
134+
resolveCssSourceEntries(root, base, VITE_SOURCE_CANDIDATE_PATTERN),
135+
resolveConfigContentEntries(root, base),
136+
])
137+
const entries = [
138+
...configEntries.entries,
139+
...sourceEntries,
140+
]
98141
const inlineCandidates = collectCssInlineSourceCandidates(root)
99142

100143
root.walkAtRules('import', (rule) => {
@@ -122,6 +165,7 @@ async function resolveTailwindV4EntriesFromCss(css: string, base: string): Promi
122165
],
123166
explicit: true,
124167
inlineCandidates,
168+
dependencies: configEntries.dependencies,
125169
}
126170
}
127171

@@ -130,6 +174,7 @@ async function resolveTailwindV4EntriesFromCss(css: string, base: string): Promi
130174
entries,
131175
explicit: true,
132176
inlineCandidates,
177+
dependencies: configEntries.dependencies,
133178
}
134179
}
135180

@@ -138,16 +183,23 @@ async function resolveTailwindV4EntriesFromCss(css: string, base: string): Promi
138183
entries,
139184
explicit: true,
140185
inlineCandidates,
186+
dependencies: configEntries.dependencies,
141187
}
142188
: inlineCandidates.included.size > 0 || inlineCandidates.excluded.size > 0
143189
? {
144190
entries: [],
145191
explicit: true,
146192
inlineCandidates,
193+
dependencies: configEntries.dependencies,
147194
}
148195
: undefined
149196
}
150197

198+
export async function resolveViteTailwindV4CssDependencies(css: string, base: string) {
199+
const resolved = await resolveTailwindV4EntriesFromCss(css, base)
200+
return resolved?.dependencies ?? []
201+
}
202+
151203
function collectExistingCssEntries(options: UserDefinedOptions) {
152204
return [
153205
...(options.cssEntries ?? []),

packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { OutputAsset, OutputChunk } from 'rollup'
22
import type { Plugin, ResolvedConfig } from 'vite'
33
import type { CreateJsHandlerOptions } from '@/types'
4-
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
55
import os from 'node:os'
66
import path from 'node:path'
77
import { MappingChars2String } from '@weapp-core/escape'
@@ -20,6 +20,12 @@ import {
2020

2121
const TEST_TIMEOUT_MS = 30000
2222
const SPLIT_WHITESPACE_RE = /\s+/
23+
const MINIMAL_TAILWIND_V4_CSS = `
24+
@theme default {
25+
--spacing: 0.25rem;
26+
}
27+
@tailwind utilities;
28+
`
2329
const createdDirs: string[] = []
2430

2531
async function loadUnifiedVitePlugin() {
@@ -255,8 +261,13 @@ describe('bundlers/vite WeappTailwindcss bundle', () => {
255261
}, TEST_TIMEOUT_MS)
256262

257263
it('detects tailwindcss v4 css sources from vite css transforms when omitted', async () => {
258-
const entry = path.join(os.tmpdir(), 'weapp-tw-vite-auto-entry.css')
259-
const css = '@import "tailwindcss";\n@plugin "./plugin.js";'
264+
const root = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-vite-auto-entry-'))
265+
createdDirs.push(root)
266+
const entry = path.join(root, 'subpackage', 'entry.css')
267+
const configFile = path.join(root, 'subpackage', 'tailwind.config.js')
268+
const css = '@import "tailwindcss" source(none);\n@config "./tailwind.config.js";'
269+
await mkdir(path.dirname(entry), { recursive: true })
270+
await writeFile(configFile, 'module.exports = { content: ["./**/*.wxml"] }\n', 'utf8')
260271
const refreshTailwindcssPatcher = vi.fn()
261272
const context = createContext({
262273
twPatcher: {
@@ -287,12 +298,63 @@ describe('bundlers/vite WeappTailwindcss bundle', () => {
287298
{
288299
file: entry,
289300
css,
301+
dependencies: [configFile],
290302
},
291303
])
292304
expect(refreshTailwindcssPatcher).toHaveBeenCalledTimes(1)
293305
expect(String((result as any)?.code)).toContain('generator-placeholder.css')
294306
})
295307

308+
it('scans Tailwind v4 @config content for vite source candidates', async () => {
309+
const root = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-vite-config-content-'))
310+
createdDirs.push(root)
311+
const cssEntry = path.join(root, 'sub-independent', 'pages', 'index.css')
312+
const configFile = path.join(root, 'sub-independent', 'tailwind.config.cjs')
313+
const pageFile = path.join(root, 'sub-independent', 'pages', 'index.wxml')
314+
await mkdir(path.dirname(pageFile), { recursive: true })
315+
await writeFile(configFile, 'module.exports = { content: ["./pages/**/*.{wxml,ts}"] }\n', 'utf8')
316+
await writeFile(pageFile, '<view class="bg-[#010721] text-[35px] h-[29px]">ok</view>\n', 'utf8')
317+
318+
const context = createContext({
319+
twPatcher: {
320+
patch: vi.fn(),
321+
getClassSet: vi.fn(async () => new Set()),
322+
getClassSetSync: vi.fn(() => new Set()),
323+
majorVersion: 4,
324+
extract: vi.fn(async () => ({ classSet: new Set() })),
325+
},
326+
})
327+
setCurrentContext(context)
328+
329+
const WeappTailwindcss = await loadUnifiedVitePlugin()
330+
const plugins = WeappTailwindcss({
331+
cssEntries: [cssEntry],
332+
})
333+
const sourcePlugin = plugins?.find(plugin => plugin.name === 'weapp-tailwindcss:adaptor:source-candidates') as Plugin
334+
const postPlugin = plugins?.find(plugin => plugin.name === 'weapp-tailwindcss:adaptor:post') as Plugin
335+
await writeFile(cssEntry, '@import "tailwindcss" source(none);\n@config "./tailwind.config.cjs";\n', 'utf8')
336+
await (postPlugin.configResolved as any)?.call(postPlugin, {
337+
command: 'build',
338+
root,
339+
css: { postcss: { plugins: [] } },
340+
build: { outDir: 'dist' },
341+
} as ResolvedConfig)
342+
await (sourcePlugin.buildStart as any)?.call(sourcePlugin)
343+
344+
const bundle = {
345+
'app.css': {
346+
...createRollupAsset(MINIMAL_TAILWIND_V4_CSS),
347+
fileName: 'app.css',
348+
},
349+
}
350+
const generateBundle = getGenerateBundleHandler(postPlugin)
351+
await generateBundle?.call({ addWatchFile: vi.fn() }, {}, bundle, false)
352+
353+
expect(String(bundle['app.css'].source)).toContain('.bg-_b_h010721_B')
354+
expect(String(bundle['app.css'].source)).toContain('.text-_b35px_B')
355+
expect(String(bundle['app.css'].source)).toContain('.h-_b29px_B')
356+
})
357+
296358
it('updates auto tailwindcss v4 css source content on repeated vite css transforms', async () => {
297359
const entry = path.join(os.tmpdir(), 'weapp-tw-vite-auto-entry-update.css')
298360
const refreshTailwindcssPatcher = vi.fn()
@@ -321,6 +383,7 @@ describe('bundlers/vite WeappTailwindcss bundle', () => {
321383
{
322384
file: entry,
323385
css: '@import "tailwindcss";\n@source inline("h-4");',
386+
dependencies: [],
324387
},
325388
])
326389
expect(refreshTailwindcssPatcher).toHaveBeenCalledTimes(2)

packages/weapp-tailwindcss/test/watch-hmr-coverage-matrix.unit.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ const styleApplyUnsupportedCases = new Set([
2222

2323
const styleFunctionUnsupportedCases = new Set([
2424
'mpx-tailwindcss-v4',
25+
'taro-vite-react-tailwindcss-v4',
26+
'taro-webpack-react-tailwindcss-v4',
2527
])
2628

2729
const styleReferenceRequiredCases = new Set([
30+
'gulp-tailwindcss-v4',
2831
'mpx-tailwindcss-v4',
2932
'uni-app-vite-tailwindcss-v4',
3033
'taro-vite-react-tailwindcss-v4',
@@ -91,7 +94,8 @@ describe('watch-hmr coverage matrix', () => {
9194
expect(subPackageMutation.templateMutation.verifyEscapedIn.length + (subPackageMutation.templateMutation.verifyClassLiteralIn?.length ?? 0)).toBeGreaterThan(0)
9295
}
9396
if (watchCase.contentMutation) {
94-
expect(watchCase.contentMutation.verifyClassLiteralIn).toContain('js')
97+
const expectedCarrier = watchCase.contentMutation.sourceFile.endsWith('.wxml') ? 'wxml' : 'js'
98+
expect(watchCase.contentMutation.verifyClassLiteralIn).toContain(expectedCarrier)
9599
}
96100
}
97101
})
@@ -113,8 +117,9 @@ describe('watch-hmr coverage matrix', () => {
113117
}
114118

115119
if (watchCase.contentMutation) {
120+
const expectedCarrier = watchCase.contentMutation.sourceFile.endsWith('.wxml') ? 'wxml' : 'js'
116121
expectDemoSourceFile(watchCase.contentMutation.sourceFile, `${watchCase.project} content mutation`)
117-
expect(watchCase.contentMutation.verifyClassLiteralIn, `${watchCase.project} content mutation should verify JS-visible literals`).toContain('js')
122+
expect(watchCase.contentMutation.verifyClassLiteralIn, `${watchCase.project} content mutation should verify source-visible literals`).toContain(expectedCarrier)
118123
}
119124
}
120125
})
@@ -171,6 +176,14 @@ describe('watch-hmr coverage matrix', () => {
171176
expect(mpxCase?.minGlobalStyleEscapedClasses).toBeGreaterThanOrEqual(1)
172177
})
173178

179+
it('keeps Taro Webpack Vue3 v4 style HMR wired through the page css import', () => {
180+
const watchCase = automatedWatchCases.find(item => item.project === 'demo/taro-webpack-vue3-tailwindcss-v4')
181+
182+
expect(watchCase).toBeDefined()
183+
expect(watchCase?.styleMutation?.sourceFile).toBe('/repo/demo/taro-webpack-vue3-tailwindcss-v4/src/pages/index/index.css')
184+
expect(watchCase?.outputStyleCandidates).toContain('/repo/demo/taro-webpack-vue3-tailwindcss-v4/dist/app.wxss')
185+
})
186+
174187
it('keeps the automated watch matrix explicit', () => {
175188
expect([...watchCoveredProjects].sort()).toEqual([...matrixProjects].sort())
176189
})

0 commit comments

Comments
 (0)