From c3cddc527e19444d4bf0f692969245080b259c18 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Thu, 20 Aug 2026 15:43:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(vite):=20=E4=BF=AE=E5=A4=8D=20uni-app=20x?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E5=9B=BE=20variant=20=E5=80=99=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #1091 --- .changeset/slick-bears-sleep.md | 5 ++ .../shared/source-candidates/store.ts | 72 ++++++++++++++----- .../source-candidates/types-and-cache.ts | 3 + .../vite/frameworks/uni-app-x/index.ts | 1 + .../create-framework-plugins-runtime.ts | 6 +- .../vite/shared/create-framework-plugins.ts | 1 + .../shared/framework-module-candidates.ts | 38 ++++++++++ .../weapp-tailwindcss/src/uni-app-x/vite.ts | 13 +++- .../src/uni-app-x/vite/plugin-options.ts | 1 + .../vite-plugin.uni-app-x.unit.test.ts | 72 +++++++++++++++++++ .../vite-source-candidates.unit.test.ts | 45 ++++++++++++ 11 files changed, 235 insertions(+), 22 deletions(-) create mode 100644 .changeset/slick-bears-sleep.md create mode 100644 packages/weapp-tailwindcss/src/bundlers/vite/shared/framework-module-candidates.ts diff --git a/.changeset/slick-bears-sleep.md b/.changeset/slick-bears-sleep.md new file mode 100644 index 000000000..ff055b670 --- /dev/null +++ b/.changeset/slick-bears-sleep.md @@ -0,0 +1,5 @@ +--- +"weapp-tailwindcss": patch +--- + +修复 uni-app x H5 中已进入构建图但未命中显式 Tailwind 正向源码范围的页面,暗色等 variant 任意值类无法生成和同步转译的问题。 diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/store.ts b/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/store.ts index d7985f646..1c6447c2c 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/store.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/store.ts @@ -1,7 +1,7 @@ import type { ScanSourceCandidateRootOptions, SourceCandidateCollectorOptions, SourceCandidateCollectorSnapshot, SourceCandidateFilterOptions, SourceCandidateStore } from './types-and-cache' import type { TailwindInlineSourceCandidates, TailwindSourceEntry } from '@/tailwindcss/source-scan' import { readFile } from 'node:fs/promises' -import { isFileMatchedByTailwindSourceEntries, resolveSourceScanPath } from '@/tailwindcss/source-scan' +import { isFileExcludedByTailwindSourceEntries, isFileMatchedByTailwindSourceEntries, resolveSourceScanPath } from '@/tailwindcss/source-scan' import { resolveSourceCandidateScanFiles } from './scan-root' import { addCandidateSet, cleanUrl, createSourceCandidateContentCacheKey, diffCandidateSets, extractCandidates, isSourceCandidateRequest, removeCandidateSet, resolveSourceCandidateExtension, sourceCandidateContentCache } from './types-and-cache' @@ -10,9 +10,11 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti const scanCandidatesById = new Map>() const transformCandidatesById = new Map>() const cssCandidatesById = new Map>() + const moduleCandidatesById = new Map>() const scanSourceById = new Map() const transformSourceById = new Map() const cssSourceById = new Map() + const moduleSourceById = new Map() const candidateCount = new Map() let inlineIncludedCandidates = new Set() let inlineExcludedCandidates = new Set() @@ -66,6 +68,15 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti replaceCssLayer(normalizedId, await resolveCandidates(source, 'css')) } + async function syncModuleSource(id: string, source: string) { + const normalizedId = cleanUrl(id) + moduleSourceById.set(normalizedId, source) + const extension = resolveSourceCandidateExtension(normalizedId) + const candidates = await resolveCandidates(source, extension) + replaceModuleLayer(normalizedId, candidates) + return new Set(candidates) + } + async function merge(id: string, source: string) { const normalizedId = cleanUrl(id) transformSourceById.set(normalizedId, source) @@ -104,8 +115,10 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti if (extension !== 'css') { transformCandidatesById.delete(normalizedId) cssCandidatesById.delete(normalizedId) + moduleCandidatesById.delete(normalizedId) transformSourceById.delete(normalizedId) cssSourceById.delete(normalizedId) + moduleSourceById.delete(normalizedId) } scanSourceById.set(normalizedId, source) replaceScanLayer(normalizedId, nextCandidates) @@ -196,12 +209,24 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti recompute(normalizedId) } + function replaceModuleLayer(id: string, nextCandidates: Set) { + const normalizedId = cleanUrl(id) + if (nextCandidates.size === 0) { + moduleCandidatesById.delete(normalizedId) + } + else { + moduleCandidatesById.set(normalizedId, nextCandidates) + } + recompute(normalizedId) + } + function recompute(id: string) { const normalizedId = cleanUrl(id) const nextCandidates = new Set([ ...(scanCandidatesById.get(normalizedId) ?? []), ...(transformCandidatesById.get(normalizedId) ?? []), ...(cssCandidatesById.get(normalizedId) ?? []), + ...(moduleCandidatesById.get(normalizedId) ?? []), ]) replaceFinal(normalizedId, nextCandidates) } @@ -218,9 +243,11 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti scanCandidatesById.delete(normalizedId) transformCandidatesById.delete(normalizedId) cssCandidatesById.delete(normalizedId) + moduleCandidatesById.delete(normalizedId) scanSourceById.delete(normalizedId) transformSourceById.delete(normalizedId) cssSourceById.delete(normalizedId) + moduleSourceById.delete(normalizedId) const previousCandidates = candidatesById.get(normalizedId) if (!previousCandidates) { return diffCandidateSets(previousVisibleCandidates, new Set()) @@ -238,6 +265,7 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti return scanSourceById.get(normalizedId) ?? cssSourceById.get(normalizedId) ?? transformSourceById.get(normalizedId) + ?? moduleSourceById.get(normalizedId) } function sources() { @@ -261,12 +289,12 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti return values() } } - if (entries?.length === 0) { - return new Set(inlineIncludedCandidates) - } const filtered = new Set() for (const [id, candidates] of candidatesById) { - if (entries !== undefined && !isFileMatchedByTailwindSourceEntries(id, entries)) { + const isActiveModule = moduleCandidatesById.has(id) + if (entries !== undefined && (isActiveModule + ? isFileExcludedByTailwindSourceEntries(id, entries) + : entries.length === 0 || !isFileMatchedByTailwindSourceEntries(id, entries))) { continue } if (options.excludeEntries?.length && isFileMatchedByTailwindSourceEntries(id, options.excludeEntries)) { @@ -298,18 +326,11 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti } } - if (entries?.length === 0) { - for (const candidate of inlineIncludedCandidates) { - addCandidateSource(candidate, undefined) - } - for (const candidate of inlineExcludedCandidates) { - sources.delete(candidate) - } - return sources - } - for (const [id, candidates] of candidatesById) { - if (entries !== undefined && !isFileMatchedByTailwindSourceEntries(id, entries)) { + const isActiveModule = moduleCandidatesById.has(id) + if (entries !== undefined && (isActiveModule + ? isFileExcludedByTailwindSourceEntries(id, entries) + : entries.length === 0 || !isFileMatchedByTailwindSourceEntries(id, entries))) { continue } if (options.excludeEntries?.length && isFileMatchedByTailwindSourceEntries(id, options.excludeEntries)) { @@ -333,9 +354,11 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti scanCandidatesById.clear() transformCandidatesById.clear() cssCandidatesById.clear() + moduleCandidatesById.clear() scanSourceById.clear() transformSourceById.clear() cssSourceById.clear() + moduleSourceById.clear() candidateCount.clear() inlineIncludedCandidates.clear() inlineExcludedCandidates.clear() @@ -360,6 +383,8 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti candidatesById: [...candidatesById.entries()].map(([id, candidates]) => [id, [...candidates]]), cssCandidatesById: [...cssCandidatesById.entries()].map(([id, candidates]) => [id, [...candidates]]), cssSourceById: [...cssSourceById.entries()], + moduleCandidatesById: [...moduleCandidatesById.entries()].map(([id, candidates]) => [id, [...candidates]]), + moduleSourceById: [...moduleSourceById.entries()], scanCandidatesById: [...scanCandidatesById.entries()].map(([id, candidates]) => [id, [...candidates]]), scanSourceById: [...scanSourceById.entries()], sourceById: [...mergeSourcesByPriority().entries()], @@ -396,6 +421,13 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti } cssCandidatesById.set(id, candidateSet) } + for (const [id, candidates] of snapshot.moduleCandidatesById ?? []) { + const candidateSet = new Set(candidates) + if (candidateSet.size === 0) { + continue + } + moduleCandidatesById.set(id, candidateSet) + } for (const [id, source] of snapshot.scanSourceById ?? snapshot.sourceById ?? []) { scanSourceById.set(id, source) } @@ -405,10 +437,14 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti for (const [id, source] of snapshot.cssSourceById ?? []) { cssSourceById.set(id, source) } + for (const [id, source] of snapshot.moduleSourceById ?? []) { + moduleSourceById.set(id, source) + } const ids = new Set([ ...scanCandidatesById.keys(), ...transformCandidatesById.keys(), ...cssCandidatesById.keys(), + ...moduleCandidatesById.keys(), ]) for (const id of ids) { recompute(id) @@ -419,6 +455,7 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti syncSource: sync, sync, syncCss, + syncModuleSource, merge, syncFile, syncCurrentSource, @@ -441,6 +478,9 @@ export function createSourceCandidateStore(options: SourceCandidateCollectorOpti function mergeSourcesByPriority() { const sources = new Map() + for (const [id, source] of moduleSourceById) { + sources.set(id, source) + } for (const [id, source] of transformSourceById) { sources.set(id, source) } diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/types-and-cache.ts b/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/types-and-cache.ts index 5b29157f1..a15d2fb66 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/types-and-cache.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/source-candidates/types-and-cache.ts @@ -10,6 +10,7 @@ export interface SourceCandidateStore { syncSource: (id: string, source: string) => Promise sync: (id: string, source: string) => Promise syncCss: (id: string, source: string) => Promise + syncModuleSource: (id: string, source: string) => Promise> merge: (id: string, source: string) => Promise syncFile: (id: string) => Promise syncCurrentSource: (id: string, source: string) => Promise @@ -41,6 +42,8 @@ export interface SourceCandidateCollectorSnapshot { candidatesById: Array<[string, string[]]> cssCandidatesById?: Array<[string, string[]]> | undefined cssSourceById?: Array<[string, string]> | undefined + moduleCandidatesById?: Array<[string, string[]]> | undefined + moduleSourceById?: Array<[string, string]> | undefined scanCandidatesById?: Array<[string, string[]]> | undefined scanSourceById?: Array<[string, string]> | undefined sourceById?: Array<[string, string]> | undefined diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/frameworks/uni-app-x/index.ts b/packages/weapp-tailwindcss/src/bundlers/vite/frameworks/uni-app-x/index.ts index 8516e2a34..23b53f42f 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/frameworks/uni-app-x/index.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/frameworks/uni-app-x/index.ts @@ -96,6 +96,7 @@ export function createUniAppXVitePlugins(options: UserDefinedOptions | InternalU isWebGeneratorTarget: context.isWebGeneratorTarget, jsHandler: context.jsHandler, mainCssChunkMatcher: context.mainCssChunkMatcher, + registerModuleGraphCandidates: context.registerModuleGraphCandidates, runtimeState: context.runtimeState, styleHandler: context.styleHandler, syncSourceCandidatesForHotUpdate: context.syncSourceCandidatesForHotUpdate, diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins-runtime.ts b/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins-runtime.ts index 32d675943..e99a908e5 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins-runtime.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins-runtime.ts @@ -49,6 +49,7 @@ import { shouldAdaptFrameworkWatchCssBeforeCache, wrapViteCssPostTransform } fro import { resolveWeappViteSourceRoot } from '../weapp-vite-config' import { resolveViteWebCssCompatOptions, shouldApplyViteWebCssCompat } from '../web-css-compat' import { createViteHmrCandidateState } from './framework-hmr-candidate-state' +import { createFrameworkModuleCandidateRegistrar } from './framework-module-candidates' import { orderFrameworkSourceCandidatePlugins } from './framework-plugin-order' import { createFrameworkPostPlugin } from './framework-post-plugin' import { createFrameworkProcessedCssRegistry } from './framework-processed-css-registry' @@ -416,14 +417,13 @@ ${tracedCss}` const shouldSplitGenerateBundlePhases = () => opts.appType === 'weapp-vite' && getResolvedConfig()?.mode !== 'production'; const preGenerateBundleHook = createGenerateBundleHook({ ...generateBundleContext, processMarkupAndScripts: false, shouldProcessBundle: shouldSplitGenerateBundlePhases }) const generateBundleHook = createGenerateBundleHook({ ...generateBundleContext, ...createGenericWebProductionBundleHooks({ frameworkName: frameworkBranch.frameworkName, getHasProcessedCss: () => processedCssRegistry.getStats().viteProcessedCssAssetResults > 0, getIsWebGeneratorTarget: () => resolveCurrentGeneratorBranch().isWeb, getResolvedConfig, onEnd: opts.onEnd, onStart: opts.onStart }), shouldProcessStyles: () => !shouldSplitGenerateBundlePhases() }) const cssFinalizerOutputPlugin = createViteCssFinalizerOutputPlugin({ opts, runtimeState, ensureRuntimeClassSet, cssPipelineStrategy: frameworkCssPipelineStrategy, debug, frameworkName: frameworkBranch.frameworkName, getResolvedConfig, hmrTimingRecorder, markCssAssetProcessed, isCssAssetProcessed, isViteProcessedCssAsset, resolveCssAssetIdentity, recordCssAssetResult, recordViteProcessedCssAssetResult, getViteProcessedCssAssetResults, getRecordedGeneratorCandidates, getSourceCandidates, getSourceCandidatesForEntries, getSourceCandidateSourcesForEntries, waitForSourceCandidateSyncs: sourceScanSession.waitForPendingSyncs, frameworkRootImportShellTargetByFile, rememberMainCssSource: (file, rawSource) => cssMemory.rememberCssSource({ outputFile: file, rawSource, sourceFile: file }), getRememberedMainCssSource: cssMemory.getRememberedCssSourceEntry }) - const extraPluginPlatform = frameworkBranch.getExtraPluginPlatform?.() ?? {} - const syncSourceCandidatesForHotUpdate = ctx => syncFrameworkSourceCandidatesForHotUpdate(sourceScanSession, ctx) + const extraPluginPlatform = frameworkBranch.getExtraPluginPlatform?.() ?? {}; const syncSourceCandidatesForHotUpdate = ctx => syncFrameworkSourceCandidatesForHotUpdate(sourceScanSession, ctx); const registerModuleGraphCandidates = createFrameworkModuleCandidateRegistrar({ cacheCurrent: sourceScanSession.cacheCurrent, debug, getCssHandlerOptions: transformCssHandlerOptions.getCssHandlerOptions, getGeneratorPlatform: resolveGeneratorPlatform, invalidateRecordedGeneratorCandidates, opts, runtimeState, sourceCandidateCollector, styleHandler }) const prepareTailwindGeneration = async () => { if (sourceScanSession.shouldDiscoverAutoCssSources(autoCssSourcesDiscovered)) { await discoverAndRegisterAutoCssSources() } await sourceScanSession.sync() } - const extraPlugins = frameworkBranch.createExtraPlugins?.({ customAttributesEntities, disabledDefaultTemplateHandler, ensureRuntimeClassSet, generateCss: generateTailwindCssForVitePipeline, getResolvedConfig, isEnabled: shouldEnableFrameworkExtraPlugins, isIosPlatform: extraPluginPlatform.isIosPlatform === true, isNativeAppStyleTarget: () => frameworkCssPipelineStrategy?.isNativeAppStyleTarget?.(createCssPipelineContext()) === true, isWebGeneratorTarget: () => resolveCurrentGeneratorBranch().isWeb, jsHandler, mainCssChunkMatcher, runtimeState, styleHandler, syncSourceCandidatesForHotUpdate, tailwindRootCssModuleIds, uniAppX, viteProcessedCssSourceFiles: processedCssRegistry.sourceFiles }) ?? [] + const extraPlugins = frameworkBranch.createExtraPlugins?.({ customAttributesEntities, disabledDefaultTemplateHandler, ensureRuntimeClassSet, generateCss: generateTailwindCssForVitePipeline, getResolvedConfig, isEnabled: shouldEnableFrameworkExtraPlugins, isIosPlatform: extraPluginPlatform.isIosPlatform === true, isNativeAppStyleTarget: () => frameworkCssPipelineStrategy?.isNativeAppStyleTarget?.(createCssPipelineContext()) === true, isWebGeneratorTarget: () => resolveCurrentGeneratorBranch().isWeb, jsHandler, mainCssChunkMatcher, registerModuleGraphCandidates, runtimeState, styleHandler, syncSourceCandidatesForHotUpdate, tailwindRootCssModuleIds, uniAppX, viteProcessedCssSourceFiles: processedCssRegistry.sourceFiles }) ?? [] const installFrameworkWatchCssCacheAdapter = async (config) => { if (!shouldAdaptFrameworkWatchCss()) { return diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins.ts b/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins.ts index 62ea8731c..a8cea1eea 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/shared/create-framework-plugins.ts @@ -40,6 +40,7 @@ export interface ViteFrameworkExtraPluginContext { isWebGeneratorTarget: () => boolean jsHandler: ReturnType['jsHandler'] mainCssChunkMatcher: ReturnType['mainCssChunkMatcher'] + registerModuleGraphCandidates: (id: string, source: string) => Promise> runtimeState: ReturnType['runtimeState'] styleHandler: ReturnType['styleHandler'] syncSourceCandidatesForHotUpdate: (ctx: HmrContext) => Promise diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/shared/framework-module-candidates.ts b/packages/weapp-tailwindcss/src/bundlers/vite/shared/framework-module-candidates.ts new file mode 100644 index 000000000..3c4225abd --- /dev/null +++ b/packages/weapp-tailwindcss/src/bundlers/vite/shared/framework-module-candidates.ts @@ -0,0 +1,38 @@ +import type { ValidateCandidatesByGeneratorOptions } from '@/bundlers/shared/generator-css' +import type { SourceCandidateStore } from '@/bundlers/shared/source-candidates/types-and-cache' +import { validateCandidatesByGenerator } from '@/bundlers/shared/generator-css' +import { cleanUrl } from '../utils' + +interface FrameworkModuleCandidateRegistrarOptions { + cacheCurrent: () => void + debug: ValidateCandidatesByGeneratorOptions['debug'] + getCssHandlerOptions: (id: string) => ValidateCandidatesByGeneratorOptions['cssHandlerOptions'] + getGeneratorPlatform: () => ValidateCandidatesByGeneratorOptions['generatorPlatform'] + invalidateRecordedGeneratorCandidates: () => void + opts: ValidateCandidatesByGeneratorOptions['opts'] + runtimeState: ValidateCandidatesByGeneratorOptions['runtimeState'] + sourceCandidateCollector: Pick + styleHandler: ValidateCandidatesByGeneratorOptions['styleHandler'] +} + +export function createFrameworkModuleCandidateRegistrar(options: FrameworkModuleCandidateRegistrarOptions) { + return async (id: string, source: string) => { + await options.runtimeState.readyPromise + const candidates = await options.sourceCandidateCollector.syncModuleSource(id, source) + options.invalidateRecordedGeneratorCandidates() + options.cacheCurrent() + const cssHandlerOptions = options.getCssHandlerOptions(id) + return validateCandidatesByGenerator({ + candidates, + cssHandlerOptions, + cssUserHandlerOptions: cssHandlerOptions, + debug: options.debug, + file: cleanUrl(id), + generatorPlatform: options.getGeneratorPlatform(), + opts: options.opts, + rawSource: '', + runtimeState: options.runtimeState, + styleHandler: options.styleHandler, + }) + } +} diff --git a/packages/weapp-tailwindcss/src/uni-app-x/vite.ts b/packages/weapp-tailwindcss/src/uni-app-x/vite.ts index c2839866f..c6ea1a79d 100644 --- a/packages/weapp-tailwindcss/src/uni-app-x/vite.ts +++ b/packages/weapp-tailwindcss/src/uni-app-x/vite.ts @@ -57,6 +57,7 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug disabledDefaultTemplateHandler, isIosPlatform: providedIosPlatform, mainCssChunkMatcher, + registerModuleGraphCandidates, runtimeState, styleHandler, syncSourceCandidatesForHotUpdate, @@ -266,13 +267,19 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug nativeLocalStyleModuleIds.add(cleanUrl(id)) } harmonyApply.rememberSource(code, id, true) + const enableComponentLocalStyle = shouldEnableComponentLocalStyle() + const enablePageLocalStyle = shouldEnablePageLocalStyleForFile(id) const resolvedConfig = getResolvedConfig() const shouldForceRefresh = resolvedConfig?.command === 'serve' || resolvedConfig?.command === 'build' - const currentRuntimeSet: Set = await ensureRuntimeClassSet(shouldForceRefresh) + const moduleGraphCandidates = enableComponentLocalStyle || enablePageLocalStyle + ? await registerModuleGraphCandidates?.(id, code) + : undefined + const runtimeSet = await ensureRuntimeClassSet(shouldForceRefresh) + const currentRuntimeSet: Set = moduleGraphCandidates?.size + ? new Set([...runtimeSet, ...moduleGraphCandidates]) + : runtimeSet nativeHmrReloader.remember(currentRuntimeSet) const transformUVue = await loadTransformUVue() - const enableComponentLocalStyle = shouldEnableComponentLocalStyle() - const enablePageLocalStyle = shouldEnablePageLocalStyleForFile(id) const transformOptions = omitUndefined({ ...(customAttributesEntities.length > 0 ? { customAttributesEntities } : {}), ...(disabledDefaultTemplateHandler ? { disabledDefaultTemplateHandler } : {}), diff --git a/packages/weapp-tailwindcss/src/uni-app-x/vite/plugin-options.ts b/packages/weapp-tailwindcss/src/uni-app-x/vite/plugin-options.ts index 958993e24..62dfa8568 100644 --- a/packages/weapp-tailwindcss/src/uni-app-x/vite/plugin-options.ts +++ b/packages/weapp-tailwindcss/src/uni-app-x/vite/plugin-options.ts @@ -11,6 +11,7 @@ export interface CreateUniAppXPluginsOptions { customAttributesEntities: ICustomAttributesEntities disabledDefaultTemplateHandler: boolean | undefined mainCssChunkMatcher: NonNullable + registerModuleGraphCandidates?: ((id: string, source: string) => Promise>) | undefined runtimeState: { readyPromise: Promise } styleHandler: InternalUserDefinedOptions['styleHandler'] syncSourceCandidatesForHotUpdate?: ((ctx: HmrContext) => Promise) | undefined diff --git a/packages/weapp-tailwindcss/test/bundlers/vite-plugin.uni-app-x.unit.test.ts b/packages/weapp-tailwindcss/test/bundlers/vite-plugin.uni-app-x.unit.test.ts index 110032147..0d86b89c4 100644 --- a/packages/weapp-tailwindcss/test/bundlers/vite-plugin.uni-app-x.unit.test.ts +++ b/packages/weapp-tailwindcss/test/bundlers/vite-plugin.uni-app-x.unit.test.ts @@ -194,6 +194,78 @@ describe('bundlers/vite WeappTailwindcss uni-app-x', () => { expect((bundle['index.asset.js'] as OutputAsset).source).toBe('js:console.log("text-[#565656]")') }, TEST_TIMEOUT_MS) + it('validates active local-style page candidates outside configured Tailwind sources', async () => { + const previousUtsPlatform = process.env.UNI_UTS_PLATFORM + process.env.UNI_UTS_PLATFORM = 'web' + const validatedCandidates = new Set([ + 'bg-[#eccc68]', + 'dark:bg-[#3498db]', + ]) + const validateCandidatesByGenerator = vi.fn(async () => validatedCandidates) + vi.doMock('@/bundlers/shared/generator-css', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateCandidatesByGenerator, + } + }) + + try { + const initialRuntimeSet = new Set(['bg-[#eccc68]']) + setCurrentContext(createContext({ + appType: 'uni-app-x', + uniAppX: { + enabled: true, + componentLocalStyles: { + enabled: true, + onlyWhenStyleIsolationVersion2: false, + }, + }, + tailwindRuntime: { + getClassSet: vi.fn(async () => initialRuntimeSet), + getClassSetSync: vi.fn(() => initialRuntimeSet), + extract: vi.fn(async () => ({ classSet: initialRuntimeSet })), + majorVersion: 4, + }, + })) + const currentContext = getCurrentContext() + const WeappTailwindcss = await loadWeappTailwindcssPlugin() + const plugins = WeappTailwindcss() + const postPlugin = plugins?.find(plugin => plugin.name === 'weapp-tailwindcss:adaptor:post') as Plugin + const nvuePlugin = plugins?.find(plugin => plugin.name === 'weapp-tailwindcss:uni-app-x:nvue') as Plugin + const source = '' + const id = '/virtual/weapp-tailwindcss-vite-test/src/pages/issue-1091.uvue' + + await (postPlugin.configResolved as any)?.call(postPlugin, { + command: 'build', + css: { postcss: { plugins: [] } }, + build: { outDir: 'dist' }, + root: '/virtual/weapp-tailwindcss-vite-test', + } as ResolvedConfig) + await getTransformHandler(nvuePlugin)?.call(nvuePlugin, source, id) + + expect(validateCandidatesByGenerator).toHaveBeenCalledWith(expect.objectContaining({ + candidates: expect.any(Set), + file: id, + })) + const extractedCandidates = validateCandidatesByGenerator.mock.calls[0]![0].candidates as Set + expect(extractedCandidates).toContain('dark:bg-[#3498db]') + const transformRuntimeSet = getTransformUVueMock().mock.calls.at(-1)?.[3] as Set + expect(transformRuntimeSet).toContain('bg-[#eccc68]') + expect(transformRuntimeSet).toContain('dark:bg-[#3498db]') + expect(transformRuntimeSet).not.toContain('dark:not-a-utility-[#bad]') + expect(currentContext.tailwindRuntime.extract).toHaveBeenCalled() + } + finally { + if (previousUtsPlatform === undefined) { + delete process.env.UNI_UTS_PLATFORM + } + else { + process.env.UNI_UTS_PLATFORM = previousUtsPlatform + } + } + }, TEST_TIMEOUT_MS) + it('skips clean uni-app-x js assets during generateBundle precheck', async () => { const runtimeSet = new Set(['text-[#424242]']) const syncMock = vi.fn(async () => runtimeSet) diff --git a/packages/weapp-tailwindcss/test/bundlers/vite-source-candidates.unit.test.ts b/packages/weapp-tailwindcss/test/bundlers/vite-source-candidates.unit.test.ts index 602b9dd58..61513e1bd 100644 --- a/packages/weapp-tailwindcss/test/bundlers/vite-source-candidates.unit.test.ts +++ b/packages/weapp-tailwindcss/test/bundlers/vite-source-candidates.unit.test.ts @@ -215,6 +215,51 @@ describe('bundlers/vite source candidates', () => { }])).toEqual(new Set(['bg-normal'])) }) + it('keeps active module graph candidates across positive source gaps and explicit exclusions', async () => { + const { createSourceCandidateCollector } = await import('@/bundlers/vite/source-candidates') + const createCollector = () => createSourceCandidateCollector({ + extractor: source => source.split(/\s+/).filter(Boolean), + }) + const root = '/project' + const pageFile = '/project/src/pages/index.uvue' + const subpageFile = '/project/src/sub/tailwindcss/index.uvue' + const excludedFile = '/project/uni_modules/example/index.uvue' + const entries = [ + { + base: root, + negated: false, + pattern: 'src/pages/**/*.{uts,uvue}', + }, + { + base: root, + negated: true, + pattern: 'uni_modules/**/*', + }, + ] + const collector = createCollector() + + await collector.sync(pageFile, 'bg-page') + await collector.syncModuleSource(subpageFile, 'dark:bg-[#3498db]') + await collector.syncModuleSource(excludedFile, 'bg-excluded') + + expect(collector.valuesForEntries(entries)).toEqual(new Set([ + 'bg-page', + 'dark:bg-[#3498db]', + ])) + expect(collector.sourcesForEntries(entries).get('dark:bg-[#3498db]')).toEqual(new Set([subpageFile])) + expect(collector.valuesForEntries([])).toEqual(new Set([ + 'dark:bg-[#3498db]', + 'bg-excluded', + ])) + + const restored = createCollector() + restored.restore(collector.snapshot()) + expect(restored.valuesForEntries(entries)).toEqual(new Set([ + 'bg-page', + 'dark:bg-[#3498db]', + ])) + }) + it('refreshes Tailwind v4 Vue arbitrary candidates after a source update', async () => { const { createSourceCandidateCollector } = await import('@/bundlers/vite/source-candidates') const root = await createTempDir('weapp-tw-vite-v4-vue-hmr-candidates')