Skip to content

Commit 46ebb04

Browse files
committed
fix: optimize vite tailwind v4 hmr candidates
1 parent a327734 commit 46ebb04

30 files changed

Lines changed: 3348 additions & 272 deletions
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+
优化 Vite watch 模式下 Tailwind v4 热更新性能:缓存 source candidates 扫描结果,优先按 `@source`/CSS 入口缩小扫描范围,并复用 Tailwind v4 generator 的增量结果,避免 demo 热更新时反复全量扫描源码和重复生成 CSS。
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
@import "tailwindcss";
2-
@config "../tailwind.config.js";
2+
@config "./tailwind.config.js";
33
@source "./**/*.{html,js,ts,wxml,ttml}";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
@import "tailwindcss" source(none);
2-
@config "../../../tailwind.config.sub-independent.js";
2+
@config "./tailwind.config.sub-independent.js";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
@import "tailwindcss" source(none);
2-
@config "../../../tailwind.config.sub-normal.js";
2+
@config "./tailwind.config.sub-normal.js";

packages/weapp-tailwindcss/src/bundlers/shared/generator-css.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types'
2+
import type { GeneratorResolvedSource } from './generator-css/source-resolver'
3+
import type { TailwindSourceEntry } from '@/tailwindcss/source-scan'
24
import type { InternalUserDefinedOptions } from '@/types'
35
import postcss from 'postcss'
46
import {
@@ -23,6 +25,8 @@ import {
2325
stripTailwindBanner,
2426
} from './generator-css/markers'
2527
import {
28+
29+
resolveGeneratorSourceEntries,
2630
resolveGeneratorSources,
2731
} from './generator-css/source-resolver'
2832

@@ -65,6 +69,7 @@ export interface GenerateCssByGeneratorOptions {
6569
file: string
6670
cssHandlerOptions: IStyleHandlerOptions
6771
cssUserHandlerOptions: IStyleHandlerOptions
72+
getSourceCandidatesForEntries?: ((entries: TailwindSourceEntry[] | undefined) => Set<string>) | undefined
6873
styleHandler: InternalUserDefinedOptions['styleHandler']
6974
debug: (format: string, ...args: unknown[]) => void
7075
}
@@ -83,6 +88,27 @@ function finalizeMiniProgramGeneratorCss(css: string, target: string) {
8388
return finalizeMiniProgramCss(css)
8489
}
8590

91+
function mergeScopedRuntimeWithCurrentRuntime(
92+
scopedRuntime: Set<string>,
93+
runtime: Set<string>,
94+
options: {
95+
cssHandlerOptions: IStyleHandlerOptions
96+
isolateCssSource: boolean
97+
},
98+
) {
99+
if (runtime.size === 0 || !options.cssHandlerOptions.isMainChunk || options.isolateCssSource) {
100+
return scopedRuntime
101+
}
102+
return new Set([
103+
...scopedRuntime,
104+
...runtime,
105+
])
106+
}
107+
108+
function shouldIsolateMatchedCssSource(source: GeneratorResolvedSource, sourceEntries: TailwindSourceEntry[] | undefined) {
109+
return Boolean(source.__weappTailwindcssMeta?.matchedCssSourceFile && sourceEntries?.length)
110+
}
111+
86112
function resolveGeneratorStyleOptions(
87113
opts: InternalUserDefinedOptions,
88114
cssHandlerOptions: IStyleHandlerOptions,
@@ -207,6 +233,7 @@ export async function generateCssByGenerator(
207233
file,
208234
cssHandlerOptions,
209235
cssUserHandlerOptions,
236+
getSourceCandidatesForEntries,
210237
styleHandler,
211238
debug,
212239
} = options
@@ -269,10 +296,23 @@ export async function generateCssByGenerator(
269296
const configuredContainerCompat = hasConfiguredContainerCompatSources(sources)
270297
const generatedResults = await Promise.all(sources.map(async (source) => {
271298
const generator = createWeappTailwindcssGenerator(source)
299+
const sourceEntries = getSourceCandidatesForEntries && majorVersion === 4
300+
? await resolveGeneratorSourceEntries(source, runtimeState)
301+
: undefined
302+
const scopedRuntime = sourceEntries
303+
? getSourceCandidatesForEntries?.(sourceEntries)
304+
: undefined
305+
const isolateCssSource = shouldIsolateMatchedCssSource(source, sourceEntries)
306+
const sourceRuntime = scopedRuntime && (scopedRuntime.size > 0 || isolateCssSource)
307+
? mergeScopedRuntimeWithCurrentRuntime(scopedRuntime, runtime, {
308+
cssHandlerOptions,
309+
isolateCssSource,
310+
})
311+
: runtime
272312
return generator.generate({
273-
candidates: runtime,
313+
candidates: sourceRuntime,
274314
incrementalCache: majorVersion === 3 || majorVersion === 4,
275-
scanSources: majorVersion === 4 && runtime.size === 0,
315+
scanSources: majorVersion === 4 && sourceRuntime.size === 0 && !isolateCssSource,
276316
styleOptions: generatorStyleOptions,
277317
tailwindcssV3Compatibility: generatorOptions.tailwindcssV3Compatibility,
278318
target: generatorOptions.target,
@@ -370,6 +410,14 @@ export async function generateCssByGenerator(
370410
if (generated.target === 'weapp') {
371411
css = inheritLegacyUnitConvertedDeclarations(css, effectiveRawSource)
372412
}
413+
if (sources.some(source => (source as GeneratorResolvedSource).__weappTailwindcssMeta?.matchedCssSourceFile)) {
414+
return {
415+
css: finalizeMiniProgramGeneratorCss(css, generated.target),
416+
target: generated.target,
417+
source: 'generator',
418+
dependencies: generated.dependencies,
419+
}
420+
}
373421
css = await appendLegacyCompatCss(
374422
css,
375423
effectiveRawSource,

packages/weapp-tailwindcss/src/bundlers/shared/generator-css/source-resolver.ts

Lines changed: 201 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { InternalUserDefinedOptions } from '@/types'
44
import { existsSync, readFileSync } from 'node:fs'
55
import path from 'node:path'
66
import process from 'node:process'
7+
import { resolveTailwindV4EntriesFromCss } from '@/bundlers/vite/source-scan'
78
import {
89
resolveTailwindV3Source,
910
resolveTailwindV3SourceFromPatcher,
@@ -33,11 +34,27 @@ interface GeneratorSourceRuntimeState {
3334
twPatcher: InternalUserDefinedOptions['twPatcher']
3435
}
3536

37+
export interface GeneratorSourceMetadata {
38+
matchedCssSourceFile?: string | undefined
39+
}
40+
41+
export type GeneratorResolvedSource = TailwindResolvedSource & {
42+
__weappTailwindcssMeta?: GeneratorSourceMetadata | undefined
43+
}
44+
3645
function resolvePostcssFromOption(cssHandlerOptions: IStyleHandlerOptions) {
3746
const from = cssHandlerOptions.postcssOptions?.options?.from
3847
return typeof from === 'string' && from.length > 0 ? from : undefined
3948
}
4049

50+
function resolvePostcssSourceFile(cssHandlerOptions: IStyleHandlerOptions) {
51+
const from = resolvePostcssFromOption(cssHandlerOptions)
52+
if (!from || !path.isAbsolute(from)) {
53+
return undefined
54+
}
55+
return from.replace(/[?#].*$/, '')
56+
}
57+
4158
export function resolveCssSourceBase(file: string, cssHandlerOptions: IStyleHandlerOptions) {
4259
const from = resolvePostcssFromOption(cssHandlerOptions)
4360
const baseFile = from ?? file
@@ -107,6 +124,41 @@ function getOutputFileStem(file: string) {
107124
return path.basename(normalized, path.extname(normalized))
108125
}
109126

127+
function getOutputFileWithoutExtension(file: string) {
128+
const normalized = file.replace(/[?#].*$/, '')
129+
const ext = path.extname(normalized)
130+
return ext ? normalized.slice(0, -ext.length) : normalized
131+
}
132+
133+
function normalizeMatchPath(file: string) {
134+
return file.split(path.sep).join('/')
135+
}
136+
137+
function stripKnownBuildRootPrefix(file: string) {
138+
const segments = normalizeMatchPath(file).split('/')
139+
const knownRoots = new Set(['dist', 'src'])
140+
for (let index = segments.length - 1; index >= 0; index--) {
141+
if (knownRoots.has(segments[index]!)) {
142+
return segments.slice(index + 1).join('/')
143+
}
144+
}
145+
return segments.join('/')
146+
}
147+
148+
function isMatchingTailwindV4CssSourceFile(file: string, cssSourceFile: string) {
149+
const outputBase = normalizeMatchPath(getOutputFileWithoutExtension(path.resolve(file)))
150+
const sourceBase = normalizeMatchPath(getOutputFileWithoutExtension(path.resolve(cssSourceFile)))
151+
const outputRelativeBase = stripKnownBuildRootPrefix(outputBase)
152+
const sourceRelativeBase = stripKnownBuildRootPrefix(sourceBase)
153+
return outputBase === sourceBase
154+
|| outputBase.endsWith(`/${sourceBase}`)
155+
|| sourceBase.endsWith(`/${outputBase}`)
156+
|| (
157+
outputRelativeBase.length > 0
158+
&& outputRelativeBase === sourceRelativeBase
159+
)
160+
}
161+
110162
function resolveMatchingTailwindV4CssEntry(
111163
rawSource: string,
112164
file: string,
@@ -143,6 +195,43 @@ function resolveMatchingTailwindV4CssEntry(
143195
})
144196
}
145197

198+
async function resolveMatchingTailwindV4CssSource(
199+
rawSource: string,
200+
file: string,
201+
cssHandlerOptions: IStyleHandlerOptions,
202+
sourceOptions: ReturnType<typeof resolveTailwindV4SourceOptionsFromPatcher>,
203+
) {
204+
const cssSources = sourceOptions.cssSources
205+
if (!cssSources?.length) {
206+
return undefined
207+
}
208+
209+
const normalizedRawSource = normalizeCssSourceForCompare(rawSource)
210+
const sourceFile = resolvePostcssSourceFile(cssHandlerOptions)
211+
const matchingSource = cssSources.find((cssSource) => {
212+
if (typeof cssSource.css !== 'string' || cssSource.css.length === 0) {
213+
return false
214+
}
215+
if (sourceFile && typeof cssSource.file === 'string' && path.resolve(sourceFile) === path.resolve(cssSource.file)) {
216+
return true
217+
}
218+
if (typeof cssSource.file === 'string' && isMatchingTailwindV4CssSourceFile(file, cssSource.file)) {
219+
return true
220+
}
221+
return normalizeCssSourceForCompare(cssSource.css) === normalizedRawSource
222+
})
223+
if (!matchingSource) {
224+
return undefined
225+
}
226+
const source = await resolveTailwindV4Source({
227+
...omitUndefined(sourceOptions),
228+
cssSources: [matchingSource],
229+
})
230+
return withGeneratorSourceMetadata(source, {
231+
matchedCssSourceFile: typeof matchingSource.file === 'string' ? matchingSource.file : undefined,
232+
})
233+
}
234+
146235
function tryResolveTailwindV4SourceOptions(
147236
runtimeState: GeneratorSourceRuntimeState,
148237
) {
@@ -161,6 +250,32 @@ function hasConfiguredTailwindV4CssSource(
161250
|| Boolean(sourceOptions?.cssSources?.length)
162251
}
163252

253+
function createTailwindV4CssSourceResolver(
254+
sourceOptions: ReturnType<typeof resolveTailwindV4SourceOptionsFromPatcher>,
255+
generatorOptions: NormalizedWeappTailwindcssGeneratorOptions | undefined,
256+
) {
257+
return (cssSource: NonNullable<typeof sourceOptions.cssSources>[number]) =>
258+
resolveTailwindV4Source({
259+
...omitUndefined(sourceOptions),
260+
cssSources: [cssSource],
261+
}).then(source => generatorOptions?.config
262+
? {
263+
...source,
264+
css: prependConfigDirective(source.css, generatorOptions.config),
265+
}
266+
: source)
267+
}
268+
269+
function withGeneratorSourceMetadata(
270+
source: TailwindResolvedSource,
271+
metadata: GeneratorSourceMetadata,
272+
): GeneratorResolvedSource {
273+
return {
274+
...source,
275+
__weappTailwindcssMeta: metadata,
276+
}
277+
}
278+
164279
function createTailwindV4ApplyReferenceSource(css: string, sourceOptions: { packageName?: string }) {
165280
if (!hasTailwindApplyDirective(css) || hasTailwindRootDirectives(css)) {
166281
return css
@@ -211,10 +326,13 @@ export async function resolveGeneratorSource(
211326
}
212327

213328
const sourceOptions = tryResolveTailwindV4SourceOptions(runtimeState)
329+
const matchedCssSource = sourceOptions
330+
? await resolveMatchingTailwindV4CssSource(rawSource, file, cssHandlerOptions, sourceOptions)
331+
: undefined
214332
const configuredCssSource = sourceOptions
215333
&& hasConfiguredTailwindV4CssSource(sourceOptions)
216334
&& hasTailwindGeneratedCssMarkers(rawSource)
217-
? await resolveTailwindV4Source(sourceOptions)
335+
? matchedCssSource ?? await resolveTailwindV4Source(sourceOptions)
218336
: undefined
219337
if (configuredCssSource) {
220338
return generatorOptions?.config
@@ -240,7 +358,7 @@ export async function resolveGeneratorSource(
240358
cssEntries: [sourceOptions.cssEntries[0]!],
241359
})
242360
: undefined
243-
const preferredCssEntrySource = matchedCssEntrySource ?? mainCssEntrySource
361+
const preferredCssEntrySource = matchedCssEntrySource ?? matchedCssSource ?? mainCssEntrySource
244362
if (preferredCssEntrySource) {
245363
return generatorOptions?.config
246364
? {
@@ -310,13 +428,32 @@ export async function resolveGeneratorSources(
310428
]
311429
}
312430

431+
const matchedCssEntrySource = cssEntrySource
432+
? await resolveMatchingTailwindV4CssEntry(rawSource, file, sourceOptions)
433+
: undefined
434+
const matchedCssSource = await resolveMatchingTailwindV4CssSource(rawSource, file, cssHandlerOptions, sourceOptions)
435+
const preferredCssEntrySource = matchedCssEntrySource ?? matchedCssSource
436+
if (preferredCssEntrySource) {
437+
return [
438+
generatorOptions?.config
439+
? {
440+
...preferredCssEntrySource,
441+
css: prependConfigDirective(preferredCssEntrySource.css, generatorOptions.config),
442+
}
443+
: preferredCssEntrySource,
444+
]
445+
}
446+
313447
if (!sourceOptions.cssEntries || sourceOptions.cssEntries.length <= 1) {
448+
if (sourceOptions.cssSources?.length) {
449+
return Promise.all(sourceOptions.cssSources.map(createTailwindV4CssSourceResolver(sourceOptions, generatorOptions)))
450+
}
314451
return [
315452
await resolveGeneratorSource(majorVersion, runtimeState, rawSource, file, cssHandlerOptions, generatorOptions),
316453
]
317454
}
318455

319-
const sources = await Promise.all(sourceOptions.cssEntries.map(cssEntry =>
456+
const cssEntrySources = await Promise.all(sourceOptions.cssEntries.map(cssEntry =>
320457
resolveTailwindV4Source({
321458
...omitUndefined(sourceOptions),
322459
cssEntries: [cssEntry],
@@ -327,5 +464,65 @@ export async function resolveGeneratorSources(
327464
}
328465
: source),
329466
))
330-
return sources
467+
const cssSources = sourceOptions.cssSources?.length
468+
? await Promise.all(sourceOptions.cssSources.map(createTailwindV4CssSourceResolver(sourceOptions, generatorOptions)))
469+
: []
470+
return [
471+
...cssEntrySources,
472+
...cssSources,
473+
]
474+
}
475+
476+
export async function resolveGeneratorSourceEntries(source: TailwindResolvedSource, runtimeState?: GeneratorSourceRuntimeState) {
477+
if (!('css' in source) || !('base' in source) || !('baseFallbacks' in source)) {
478+
return undefined
479+
}
480+
const sourceMetadata = (source as GeneratorResolvedSource).__weappTailwindcssMeta
481+
const resolved = await resolveTailwindV4EntriesFromCss(source.css, source.base)
482+
if (resolved?.entries.length || (!resolved?.explicit && !sourceMetadata?.matchedCssSourceFile) || !runtimeState) {
483+
return resolved?.entries
484+
}
485+
const sourceOptions = tryResolveTailwindV4SourceOptions(runtimeState)
486+
const matchingCssSource = sourceOptions?.cssSources?.find((cssSource) => {
487+
if (
488+
sourceMetadata?.matchedCssSourceFile
489+
&& typeof cssSource.file === 'string'
490+
&& path.resolve(cssSource.file) === path.resolve(sourceMetadata.matchedCssSourceFile)
491+
) {
492+
return true
493+
}
494+
return cssSource.css === source.css
495+
})
496+
if (!matchingCssSource) {
497+
return resolved?.entries
498+
}
499+
const sourceResolved = await resolveTailwindV4EntriesFromCss(
500+
matchingCssSource.css,
501+
typeof matchingCssSource.base === 'string' && matchingCssSource.base.length > 0
502+
? matchingCssSource.base
503+
: typeof matchingCssSource.file === 'string' && matchingCssSource.file.length > 0
504+
? path.dirname(matchingCssSource.file)
505+
: source.base,
506+
)
507+
if (sourceResolved?.entries.length) {
508+
return sourceResolved.entries
509+
}
510+
for (const dependency of matchingCssSource.dependencies ?? []) {
511+
if (!existsSync(dependency)) {
512+
continue
513+
}
514+
try {
515+
const dependencyResolved = await resolveTailwindV4EntriesFromCss(
516+
readFileSync(dependency, 'utf8'),
517+
path.dirname(dependency),
518+
)
519+
if (dependencyResolved?.entries.length) {
520+
return dependencyResolved.entries
521+
}
522+
}
523+
catch {
524+
// 依赖内容只用于裁剪候选,读取失败时回退到 Tailwind 自身生成逻辑。
525+
}
526+
}
527+
return resolved.entries
331528
}

0 commit comments

Comments
 (0)