Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/issue-932-large-ts-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"weapp-tailwindcss": patch
---

修复 Vite 构建中 Tailwind v4 `@source not` 排除大型 TS 文件后,仍然会进入 JS 转译解析导致构建明显变慢的问题。
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export function createGenerateBundleHook(context: GenerateBundleContext) {
getSourceCandidates,
getSourceCandidateSource,
getSourceCandidateSources,
getSourceScanEntries,
isWatchLikeBuild,
getSourceCandidatesForEntries,
getSourceCandidateSourcesForEntries,
waitForSourceCandidateSyncs,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -921,6 +927,7 @@ export function createGenerateBundleHook(context: GenerateBundleContext) {
rememberProcessCacheKey,
runtimeSignature,
snapshot,
sourceScanEntries: jsSourceScanEntries,
timeTask,
transformRuntime,
uniAppX,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand All @@ -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<void>) => Promise<void>
transformRuntime: Set<string>
uniAppX: GenerateBundleContext['opts']['uniAppX']
Expand All @@ -58,6 +61,7 @@ export function processJsBundleEntry(options: ProcessJsBundleEntryOptions) {
rememberProcessCacheKey,
runtimeSignature,
snapshot,
sourceScanEntries,
timeTask,
transformRuntime,
uniAppX,
Expand Down Expand Up @@ -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++
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>()
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface GenerateBundleContext {
getSourceCandidates?: () => Set<string>
getSourceCandidateSource?: (file: string) => string | undefined
getSourceCandidateSources?: () => Iterable<[string, string]>
getSourceScanEntries?: () => TailwindSourceEntry[] | undefined
isWatchLikeBuild?: () => boolean
getSourceCandidatesForEntries?: ((entries: TailwindSourceEntry[] | undefined, options?: SourceCandidateFilterOptions) => Set<string>) | undefined
getSourceCandidateSourcesForEntries?: ((entries: TailwindSourceEntry[] | undefined, options?: SourceCandidateFilterOptions) => Map<string, Set<string>>) | undefined
waitForSourceCandidateSyncs?: () => Promise<void>
Expand Down
3 changes: 3 additions & 0 deletions packages/weapp-tailwindcss/src/bundlers/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,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)
Expand Down Expand Up @@ -730,6 +731,8 @@ export function WeappTailwindcss(options: UserDefinedOptions = {}): WeappTailwin
getSourceCandidates,
getSourceCandidateSource: file => sourceCandidateCollector.source(file),
getSourceCandidateSources: () => sourceCandidateCollector.sources(),
getSourceScanEntries,
isWatchLikeBuild,
getSourceCandidatesForEntries,
getSourceCandidateSourcesForEntries,
waitForSourceCandidateSyncs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(),
getSourceScanEntries: () => sourceEntries,
getSourceCandidatesForEntries: () => new Set<string>(),
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { describe, expect, it } from 'vitest'
import {
collectCssInlineSourceCandidates,
expandTailwindSourceEntries,
isFileMatchedByTailwindSourceEntries,
resolveCssSourceEntries,
resolveSourceScanPath,
resolveTailwindSourceEntry,
} from '@/tailwindcss/source-scan'
Expand Down Expand Up @@ -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}");',
Expand Down
Loading