From cb6095da050408b81e5a15f5e2d7aca8497e61b8 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Wed, 17 Jun 2026 18:08:26 +0800 Subject: [PATCH] fix: skip excluded vite js source chunks --- .changeset/issue-932-large-ts-build.md | 5 + .../src/bundlers/vite/generate-bundle.ts | 7 ++ .../vite/generate-bundle/js-processing.ts | 12 +++ .../vite/generate-bundle/js-source-scope.ts | 53 +++++++++++ .../bundlers/vite/generate-bundle/types.ts | 2 + .../src/bundlers/vite/index.ts | 3 + .../bundlers/vite-plugin.bundle.unit.test.ts | 91 +++++++++++++++++++ .../test/tailwindcss/source-scan.unit.test.ts | 14 +++ 8 files changed, 187 insertions(+) create mode 100644 .changeset/issue-932-large-ts-build.md create mode 100644 packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-source-scope.ts diff --git a/.changeset/issue-932-large-ts-build.md b/.changeset/issue-932-large-ts-build.md new file mode 100644 index 0000000000..df6c5fd6f1 --- /dev/null +++ b/.changeset/issue-932-large-ts-build.md @@ -0,0 +1,5 @@ +--- +"weapp-tailwindcss": patch +--- + +修复 Vite 构建中 Tailwind v4 `@source not` 排除大型 TS 文件后,仍然会进入 JS 转译解析导致构建明显变慢的问题。 diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts index e052e6695a..00e9e88da3 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts @@ -92,6 +92,8 @@ export function createGenerateBundleHook(context: GenerateBundleContext) { getSourceCandidates, getSourceCandidateSource, getSourceCandidateSources, + getSourceScanEntries, + isWatchLikeBuild, getSourceCandidatesForEntries, getSourceCandidateSourcesForEntries, waitForSourceCandidateSyncs, @@ -243,6 +245,10 @@ export function createGenerateBundleHook(context: GenerateBundleContext) { }) const jsEntries = snapshot.jsEntries const getJsEntry = createJsEntryResolver(jsEntries) + const sourceScanEntries = getSourceScanEntries?.() + const jsSourceScanEntries = buildCommand && !isWatchLikeBuild?.() + ? sourceScanEntries + : undefined const moduleGraphOptions = createBundleModuleGraphOptions(outDir, jsEntries) const hasCssAssetEntry = snapshot.entries.some(entry => entry.type === 'css' && entry.output.type === 'asset') const hasRuntimeAffectingChanges = hasRuntimeAffectingSourceChanges(snapshot.runtimeAffectingChangedByType) @@ -921,6 +927,7 @@ export function createGenerateBundleHook(context: GenerateBundleContext) { rememberProcessCacheKey, runtimeSignature, snapshot, + sourceScanEntries: jsSourceScanEntries, timeTask, transformRuntime, uniAppX, diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-processing.ts b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-processing.ts index 87ca525e9e..13ebea21df 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-processing.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-processing.ts @@ -2,6 +2,7 @@ import type { OutputAsset, OutputChunk } from 'rollup' import type { BundleSnapshot, BundleStateEntry } from '../bundle-state' import type { BundleMetrics } from './metrics' import type { GenerateBundleContext } from './types' +import type { TailwindSourceEntry } from '@/tailwindcss/source-scan' import type { CreateJsHandlerOptions, LinkedJsModuleResult } from '@/types' import path from 'node:path' import { createUniAppXBundleAssetSourceGetter, UNI_APP_X_STYLE_PLACEHOLDER_VERSION } from '@/uni-app-x/style-asset' @@ -10,6 +11,7 @@ import { processCachedTask } from '../../shared/cache' import { shouldSkipViteJsTransform } from '../js-precheck' import { resolveUniAppXJsTransformEnabled } from './js-handler-options' import { collectLinkedFileNames } from './js-linking' +import { isViteJsChunkWithinTailwindSourceScope } from './js-source-scope' import { measureElapsed } from './metrics' import { createJsHashSalt, createLinkedImpactSignature, getSnapshotHash } from './signatures' @@ -32,6 +34,7 @@ interface ProcessJsBundleEntryOptions { rememberProcessCacheKey: (cacheKey: string, hashKey?: string | number) => void runtimeSignature: string snapshot: BundleSnapshot + sourceScanEntries: TailwindSourceEntry[] | undefined timeTask: (name: string, task: () => Promise) => Promise transformRuntime: Set uniAppX: GenerateBundleContext['opts']['uniAppX'] @@ -58,6 +61,7 @@ export function processJsBundleEntry(options: ProcessJsBundleEntryOptions) { rememberProcessCacheKey, runtimeSignature, snapshot, + sourceScanEntries, timeTask, transformRuntime, uniAppX, @@ -111,6 +115,14 @@ export function processJsBundleEntry(options: ProcessJsBundleEntryOptions) { debug('js cache replay miss, fallback transform: %s', file) } const handlerOptions = createHandlerOptions(absoluteFile) + if (!isViteJsChunkWithinTailwindSourceScope(originalSource, sourceScanEntries)) { + metrics.js.elapsed += measureElapsed(start) + metrics.js.transformed++ + debug('js skip transform (outside tailwind source scan): %s', file) + return { + result: rawSource, + } + } if (!disableJsPrecheck && shouldSkipViteJsTransform(rawSource, handlerOptions)) { metrics.js.elapsed += measureElapsed(start) metrics.js.transformed++ diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-source-scope.ts b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-source-scope.ts new file mode 100644 index 0000000000..aa7ae435db --- /dev/null +++ b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/js-source-scope.ts @@ -0,0 +1,53 @@ +import type { OutputChunk } from 'rollup' +import type { TailwindSourceEntry } from '@/tailwindcss/source-scan' +import path from 'node:path' +import { isFileMatchedByTailwindSourceEntries } from '@/tailwindcss/source-scan' +import { cleanUrl } from '../utils' + +function hasExplicitSourceEntries(entries: TailwindSourceEntry[] | undefined) { + return entries !== undefined && entries.length > 0 +} + +function normalizeModuleId(id: string) { + const file = cleanUrl(id) + return path.isAbsolute(file) ? path.resolve(file) : undefined +} + +function collectChunkModuleIds(chunk: OutputChunk) { + const ids = new Set() + for (const id of chunk.moduleIds ?? []) { + const normalized = normalizeModuleId(id) + if (normalized) { + ids.add(normalized) + } + } + for (const id of Object.keys(chunk.modules ?? {})) { + const normalized = normalizeModuleId(id) + if (normalized) { + ids.add(normalized) + } + } + return ids +} + +export function isViteJsChunkWithinTailwindSourceScope( + chunk: OutputChunk, + entries: TailwindSourceEntry[] | undefined, +) { + if (!hasExplicitSourceEntries(entries)) { + return true + } + + const moduleIds = collectChunkModuleIds(chunk) + if (moduleIds.size === 0) { + return true + } + + for (const id of moduleIds) { + if (isFileMatchedByTailwindSourceEntries(id, entries)) { + return true + } + } + + return false +} diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/types.ts b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/types.ts index af3714a146..5eb3b8f56f 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/types.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/types.ts @@ -34,6 +34,8 @@ export interface GenerateBundleContext { getSourceCandidates?: () => Set getSourceCandidateSource?: (file: string) => string | undefined getSourceCandidateSources?: () => Iterable<[string, string]> + getSourceScanEntries?: () => TailwindSourceEntry[] | undefined + isWatchLikeBuild?: () => boolean getSourceCandidatesForEntries?: ((entries: TailwindSourceEntry[] | undefined, options?: SourceCandidateFilterOptions) => Set) | undefined getSourceCandidateSourcesForEntries?: ((entries: TailwindSourceEntry[] | undefined, options?: SourceCandidateFilterOptions) => Map>) | undefined waitForSourceCandidateSyncs?: () => Promise diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/index.ts b/packages/weapp-tailwindcss/src/bundlers/vite/index.ts index e5809acaae..51772b5071 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/index.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/index.ts @@ -295,6 +295,7 @@ export function WeappTailwindcss(options: UserDefinedOptions = {}): WeappTailwin recordedGeneratorCandidates = undefined } const getSourceCandidates = () => sourceCandidateCollector.values() + const getSourceScanEntries = () => sourceScanEntries const getSourceCandidatesForEntries = (entries: TailwindSourceEntry[] | undefined) => sourceCandidateCollector.valuesForEntries(entries) const getSourceCandidateSourcesForEntries = (entries: TailwindSourceEntry[] | undefined, options?: SourceCandidateFilterOptions) => sourceCandidateCollector.sourcesForEntries(entries, options) @@ -700,6 +701,8 @@ export function WeappTailwindcss(options: UserDefinedOptions = {}): WeappTailwin getSourceCandidates, getSourceCandidateSource: file => sourceCandidateCollector.source(file), getSourceCandidateSources: () => sourceCandidateCollector.sources(), + getSourceScanEntries, + isWatchLikeBuild, getSourceCandidatesForEntries, getSourceCandidateSourcesForEntries, waitForSourceCandidateSyncs, diff --git a/packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts b/packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts index e7287d8ee2..a999e5fa3a 100644 --- a/packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts +++ b/packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts @@ -10755,6 +10755,97 @@ const fallback = "bg-[#434332] px-[32px]" expect(linkedOptions?.moduleGraph?.resolve?.('./chunk.js', linkedOptions.filename ?? '')).toBe(linkedFile) }, TEST_TIMEOUT_MS) + it('skips js chunks whose modules are excluded by Tailwind v4 source entries', async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-issue-932-')) + createdDirs.push(rootDir) + const includedModule = path.resolve(rootDir, 'src/pages/index.ts') + const excludedModule = path.resolve(rootDir, 'src/generated/openapi-client.ts') + const sourceEntries = [ + { + base: path.resolve(rootDir, 'src'), + negated: false, + pattern: '**/*.{ts,wxml}', + }, + { + base: path.resolve(rootDir, 'src/generated'), + negated: true, + pattern: '**/*.ts', + }, + ] + + const runtimeSet = new Set(['text-[#111111]']) + const context = createContext({ + twPatcher: { + majorVersion: 4, + extract: vi.fn(async () => ({ classSet: runtimeSet })), + getClassSetSync: vi.fn(() => runtimeSet), + getClassSet: vi.fn(async () => runtimeSet), + }, + jsHandler: vi.fn((code: string) => ({ code: `js:${code}` })), + }) + + const generateBundle = createGenerateBundleHook({ + opts: context as any, + runtimeState: { + twPatcher: context.twPatcher as any, + readyPromise: Promise.resolve(), + }, + ensureRuntimeClassSet: vi.fn(async () => runtimeSet), + ensureBundleRuntimeClassSet: vi.fn(async () => runtimeSet), + debug: vi.fn(), + getResolvedConfig: () => ({ + command: 'build', + plugins: [], + root: rootDir, + css: { postcss: { plugins: [] } }, + build: { outDir: 'dist' }, + } as unknown as ResolvedConfig), + markCssAssetProcessed: vi.fn(), + isCssAssetProcessed: vi.fn(() => false), + isViteProcessedCssAsset: vi.fn(() => false), + recordCssAssetResult: vi.fn(), + recordViteProcessedCssAssetResult: vi.fn(), + getViteProcessedCssAssetResults: () => [], + getViteProcessedCssAssetResult: () => undefined, + getSourceCandidates: () => new Set(), + getSourceScanEntries: () => sourceEntries, + getSourceCandidatesForEntries: () => new Set(), + waitForSourceCandidateSyncs: vi.fn(async () => undefined), + rememberCssSource: vi.fn(), + refreshRememberedCssSource: vi.fn(), + getRememberedCssSources: () => new Map(), + getRememberedCssSignature: () => undefined, + setRememberedCssSignature: vi.fn(), + recordGeneratorCandidates: vi.fn(), + }) + + const includedChunk = { + ...createRollupChunk('const cls = "text-[#111111]"'), + moduleIds: [includedModule], + modules: { + [includedModule]: {}, + }, + } as OutputChunk + const excludedChunk = { + ...createRollupChunk('export const api = "text-[#222222]";\nexport const value = 1;'), + moduleIds: [excludedModule], + modules: { + [excludedModule]: {}, + }, + } as OutputChunk + const bundle = { + 'pages/index.js': includedChunk, + 'generated/openapi-client.js': excludedChunk, + } + + await generateBundle.call({ addWatchFile: vi.fn() }, {}, bundle) + + expect(context.jsHandler).toHaveBeenCalledTimes(1) + expect(context.jsHandler.mock.calls[0]?.[0]).toBe('const cls = "text-[#111111]"') + expect(includedChunk.code).toContain('js:') + expect(excludedChunk.code).toBe('export const api = "text-[#222222]";\nexport const value = 1;') + }, TEST_TIMEOUT_MS) + it('propagates linked js updates to asset entries', async () => { const WeappTailwindcss = await loadWeappTailwindcssPlugin() const rootDir = process.cwd() diff --git a/packages/weapp-tailwindcss/test/tailwindcss/source-scan.unit.test.ts b/packages/weapp-tailwindcss/test/tailwindcss/source-scan.unit.test.ts index daa98e4957..64cd7a6985 100644 --- a/packages/weapp-tailwindcss/test/tailwindcss/source-scan.unit.test.ts +++ b/packages/weapp-tailwindcss/test/tailwindcss/source-scan.unit.test.ts @@ -3,6 +3,8 @@ import { describe, expect, it } from 'vitest' import { collectCssInlineSourceCandidates, expandTailwindSourceEntries, + isFileMatchedByTailwindSourceEntries, + resolveCssSourceEntries, resolveSourceScanPath, resolveTailwindSourceEntry, } from '@/tailwindcss/source-scan' @@ -39,6 +41,18 @@ describe('tailwindcss source scan', () => { }) }) + it('matches Tailwind v4 source entries while honoring negated generated ts globs', async () => { + const root = path.resolve('/project') + const entries = await resolveCssSourceEntries(postcss.parse([ + '@import "tailwindcss" source(none);', + '@source "./src/**/*.{ts,wxml}";', + '@source not "./src/generated/**/*.ts";', + ].join('\n')), root, '**/*') + + expect(isFileMatchedByTailwindSourceEntries(path.join(root, 'src/pages/index.ts'), entries)).toBe(true) + expect(isFileMatchedByTailwindSourceEntries(path.join(root, 'src/generated/openapi-client.ts'), entries)).toBe(false) + }) + it('keeps empty brace parts for Tailwind v4 inline source variants', () => { const inlineCandidates = collectCssInlineSourceCandidates(postcss.parse([ '@source inline("{hover:,focus:,}underline p-{2..6..2}");',