Skip to content

Commit 069dbb7

Browse files
committed
refactor: split vite bundle flow and normalize css selectors
1 parent 8e41f8e commit 069dbb7

12 files changed

Lines changed: 1008 additions & 795 deletions

File tree

packages/weapp-tailwindcss/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@
214214
"magic-string": "0.30.21",
215215
"micromatch": "^4.0.8",
216216
"postcss-load-config": "^6.0.1",
217+
"postcss-selector-parser": "^7.1.2",
217218
"semver": "~7.8.4",
218219
"tailwindcss-config": "workspace:*",
219220
"tailwindcss-patch": "catalog:tailwindcssPatch",

packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts

Lines changed: 11 additions & 769 deletions
Large diffs are not rendered by default.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { InternalUserDefinedOptions } from '@/types'
2+
import { existsSync } from 'node:fs'
3+
import path from 'node:path'
4+
import { resolveTailwindV4CssSourceBase } from '@/tailwindcss/source-scan'
5+
import { slash } from '../utils'
6+
import { normalizeCssSourceForCompare } from './css-output'
7+
import { hasMatchingStyleFileBase, isMatchingCssSourceFile } from './style-matching'
8+
9+
function isPackageJsonImportRequest(request: string) {
10+
return request.startsWith('#')
11+
}
12+
13+
function normalizeMatchedCssSourcePath(file: string | undefined) {
14+
if (!file || !path.isAbsolute(file)) {
15+
return undefined
16+
}
17+
return path.resolve(file.replace(/[?#].*$/, ''))
18+
}
19+
20+
function collectConfiguredTailwindV4CssSources(opts: InternalUserDefinedOptions) {
21+
const patcherCssSources = ((opts.tailwindcssPatcherOptions as any)?.tailwindcss?.v4?.cssSources ?? []) as NonNullable<NonNullable<InternalUserDefinedOptions['tailwindcss']>['v4']>['cssSources'] | undefined
22+
return [
23+
...(opts.tailwindcss?.v4?.cssSources ?? []),
24+
...(patcherCssSources ?? []),
25+
]
26+
}
27+
28+
function collectConfiguredCssEntries(opts: InternalUserDefinedOptions) {
29+
const patcherCssEntries = ((opts.tailwindcssPatcherOptions as any)?.tailwindcss?.v4?.cssEntries ?? []) as string[] | undefined
30+
return [
31+
...(opts.cssEntries ?? []),
32+
...(opts.tailwindcss?.v4?.cssEntries ?? []),
33+
...(patcherCssEntries ?? []),
34+
].filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)
35+
}
36+
37+
function collectCssConfigBaseCandidates(
38+
source: string,
39+
file: string,
40+
outputRoot: string,
41+
opts: InternalUserDefinedOptions,
42+
) {
43+
const candidates: string[] = []
44+
const seen = new Set<string>()
45+
const addCandidate = (candidate: string | undefined) => {
46+
if (!candidate) {
47+
return
48+
}
49+
const normalized = path.resolve(candidate)
50+
if (seen.has(normalized)) {
51+
return
52+
}
53+
seen.add(normalized)
54+
candidates.push(normalized)
55+
}
56+
57+
addCandidate(path.dirname(path.resolve(outputRoot, file.replace(/[?#].*$/, ''))))
58+
59+
const normalizedSource = normalizeCssSourceForCompare(source)
60+
const patcherProjectRoot = typeof opts.tailwindcssPatcherOptions?.projectRoot === 'string'
61+
? opts.tailwindcssPatcherOptions.projectRoot
62+
: undefined
63+
const sourceBaseFallback = opts.tailwindcss?.v4?.base
64+
?? patcherProjectRoot
65+
?? opts.tailwindcssBasedir
66+
?? outputRoot
67+
const sourceRoot = opts.tailwindcssBasedir ?? patcherProjectRoot
68+
const configuredCssEntries = collectConfiguredCssEntries(opts)
69+
for (const cssEntry of configuredCssEntries) {
70+
const resolvedCssEntry = path.resolve(cssEntry)
71+
if (
72+
configuredCssEntries.length === 1
73+
|| isMatchingCssSourceFile(file, resolvedCssEntry, outputRoot)
74+
|| hasMatchingStyleFileBase(file, resolvedCssEntry, outputRoot, sourceRoot)
75+
) {
76+
addCandidate(path.dirname(resolvedCssEntry))
77+
}
78+
}
79+
for (const cssSource of collectConfiguredTailwindV4CssSources(opts)) {
80+
const cssSourceFile = normalizeMatchedCssSourcePath(cssSource.file)
81+
const cssSourceCss = typeof cssSource.css === 'string'
82+
? normalizeCssSourceForCompare(cssSource.css)
83+
: undefined
84+
if (
85+
cssSourceFile
86+
&& !isMatchingCssSourceFile(file, cssSourceFile, outputRoot)
87+
&& cssSourceCss !== normalizedSource
88+
) {
89+
continue
90+
}
91+
addCandidate(cssSourceFile ? path.dirname(cssSourceFile) : undefined)
92+
addCandidate(resolveTailwindV4CssSourceBase(cssSource, sourceBaseFallback))
93+
}
94+
95+
return candidates
96+
}
97+
98+
export function normalizeRelativeCssConfigDirectives(
99+
source: string,
100+
file: string,
101+
outputRoot: string,
102+
opts: InternalUserDefinedOptions,
103+
) {
104+
if (!source.includes('@config')) {
105+
return source
106+
}
107+
108+
const baseCandidates = collectCssConfigBaseCandidates(source, file, outputRoot, opts)
109+
110+
return source.replace(/@config\s+(["'])(.+?)\1\s*;?/g, (full, quote: string, request: string) => {
111+
if (path.isAbsolute(request) || isPackageJsonImportRequest(request)) {
112+
return full
113+
}
114+
115+
for (const base of baseCandidates) {
116+
const configFile = path.resolve(base, request)
117+
if (existsSync(configFile)) {
118+
return `@config ${quote}${slash(configFile)}${quote};`
119+
}
120+
}
121+
122+
return full
123+
})
124+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { InternalUserDefinedOptions } from '@/types'
2+
import path from 'node:path'
3+
import postcss from 'postcss'
4+
import { normalizeOutputPathKey } from '../../shared/module-graph'
5+
import { isCSSRequest } from '../utils'
6+
7+
export const SOURCE_STYLE_OUTPUT_EXT_RE = /\.(?:less|sass|scss|styl|stylus|pcss|postcss)$/i
8+
export const CSS_SOURCE_OUTPUT_EXT_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss)$/i
9+
export const MINI_PROGRAM_STYLE_OUTPUT_EXT_RE = /\.(?:wx|ac|jx|tt|q|ty)ss$/i
10+
11+
const SOURCE_STYLE_NON_CSS_SYNTAX_RE = /(?:^|\n)\s*(?:\/\/|\$[\w-]+\s*:|@(?:use|forward|mixin|include|function)\b)/
12+
13+
export function resolveReplayCssOutputFile(rootDir: string, file: string) {
14+
const nextFile = path.isAbsolute(file) ? path.relative(rootDir, file) : file
15+
const normalizedFile = normalizeOutputPathKey(nextFile)
16+
if (
17+
normalizedFile.length === 0
18+
|| normalizedFile === '.'
19+
|| normalizedFile === '..'
20+
|| normalizedFile.startsWith('../')
21+
) {
22+
return normalizeOutputPathKey(path.basename(file))
23+
}
24+
return normalizedFile
25+
}
26+
27+
export function resolveViteCssOutputFile(
28+
file: string,
29+
opts: InternalUserDefinedOptions,
30+
isWebGeneratorTarget: boolean,
31+
preserveCssExtension = false,
32+
) {
33+
if (
34+
isWebGeneratorTarget
35+
|| preserveCssExtension
36+
|| opts.cssMatcher(file)
37+
|| !SOURCE_STYLE_OUTPUT_EXT_RE.test(file)
38+
|| !isCSSRequest(file)
39+
) {
40+
return file
41+
}
42+
return file.replace(SOURCE_STYLE_OUTPUT_EXT_RE, '.wxss')
43+
}
44+
45+
export function resolveViteCssPipelineOutputFile(
46+
file: string,
47+
_opts: Pick<InternalUserDefinedOptions, 'cssMatcher'>,
48+
rootDir: string,
49+
isWebGeneratorTarget = false,
50+
preserveCssExtension = false,
51+
) {
52+
const normalizedFile = resolveReplayCssOutputFile(rootDir, file)
53+
if (
54+
isWebGeneratorTarget
55+
|| preserveCssExtension
56+
|| MINI_PROGRAM_STYLE_OUTPUT_EXT_RE.test(normalizedFile)
57+
|| !CSS_SOURCE_OUTPUT_EXT_RE.test(normalizedFile)
58+
|| !isCSSRequest(normalizedFile)
59+
) {
60+
return normalizedFile
61+
}
62+
return normalizedFile.replace(CSS_SOURCE_OUTPUT_EXT_RE, '.wxss')
63+
}
64+
65+
export function canProcessViteSourceStyleAsCss(source: string, file: string) {
66+
if (SOURCE_STYLE_NON_CSS_SYNTAX_RE.test(source)) {
67+
return false
68+
}
69+
try {
70+
postcss.parse(source, { from: file })
71+
return true
72+
}
73+
catch {
74+
return false
75+
}
76+
}
77+
78+
export function normalizeCssSourceForCompare(css: string) {
79+
return css.trim()
80+
}
81+
82+
export function stripStyleFileExtension(file: string) {
83+
const normalized = file.replace(/[?#].*$/, '')
84+
const ext = path.extname(normalized)
85+
return ext ? normalized.slice(0, -ext.length) : normalized
86+
}
87+
88+
export function isAppOriginCssFile(file: string) {
89+
return path.basename(stripStyleFileExtension(file)) === 'app-origin'
90+
}
91+
92+
export function isMainAppCssFile(file: string) {
93+
return path.basename(stripStyleFileExtension(file)) === 'app'
94+
}
95+
96+
export function isMainStyleEntryCssFile(file: string) {
97+
const basename = path.basename(stripStyleFileExtension(file))
98+
return basename === 'app' || basename === 'main'
99+
}
100+
101+
export function isTailwindEntryCssFile(file: string) {
102+
return path.basename(stripStyleFileExtension(file)) === 'tailwind'
103+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import type { OutputAsset } from 'rollup'
2+
import type { RememberedCssSource } from './types'
3+
import type { InternalUserDefinedOptions } from '@/types'
4+
import { hasBundlerGeneratedCssMarker, parseBundlerGeneratedCssMarkerBlocks } from '../../shared/generated-css-marker'
5+
import { hasTailwindApplyDirective } from '../../shared/generator-css/directives'
6+
import { normalizeOutputPathKey } from '../../shared/module-graph'
7+
import { isAppOriginCssFile, isMainAppCssFile, resolveViteCssPipelineOutputFile } from './css-output'
8+
import { hasTailwindGenerationSource } from './sfc-style-source'
9+
import { scoreMatchingStyleFileBase } from './style-matching'
10+
11+
export function createRememberedCssRuntimeSignature(cssRuntimeSignature: string, cssRuntimeAffectingHash: string) {
12+
return `${cssRuntimeSignature}:${cssRuntimeAffectingHash}`
13+
}
14+
15+
export function resolveRememberedCssSourceForTest(
16+
sources: Iterable<[string, RememberedCssSource]> | undefined,
17+
outputFile: string,
18+
file: string,
19+
originalSource: OutputAsset,
20+
outputRoot: string,
21+
sourceRoot: string | undefined,
22+
) {
23+
return findRememberedCssSource(sources, outputFile, file, originalSource, outputRoot, sourceRoot)
24+
}
25+
26+
function findRememberedCssSource(
27+
sources: Iterable<[string, RememberedCssSource]> | undefined,
28+
outputFile: string,
29+
file: string,
30+
originalSource: OutputAsset,
31+
outputRoot: string,
32+
sourceRoot: string | undefined,
33+
) {
34+
const matched = findRememberedCssSources(sources, outputFile, file, originalSource, outputRoot, sourceRoot)
35+
return matched.length === 1 ? matched[0] : undefined
36+
}
37+
38+
export function findRememberedCssSources(
39+
sources: Iterable<[string, RememberedCssSource]> | undefined,
40+
outputFile: string,
41+
file: string,
42+
originalSource: OutputAsset,
43+
outputRoot: string,
44+
sourceRoot: string | undefined,
45+
) {
46+
if (!sources) {
47+
return []
48+
}
49+
const rememberedSources = [...sources].map(([, remembered]) => remembered)
50+
const source = typeof originalSource.source === 'string'
51+
? originalSource.source
52+
: originalSource.source.toString()
53+
const markerFiles = new Set(parseBundlerGeneratedCssMarkerBlocks(source)
54+
.filter(block => block.bundler === 'vite' && typeof block.file === 'string' && block.file.length > 0)
55+
.map(block => normalizeOutputPathKey(block.file!)))
56+
if (markerFiles.size > 0) {
57+
const markerMatched = rememberedSources.filter(remembered =>
58+
markerFiles.has(normalizeOutputPathKey(remembered.sourceFile.replace(/[?#].*$/, ''))),
59+
)
60+
if (markerMatched.length > 0) {
61+
return markerMatched
62+
}
63+
}
64+
const originalFiles = [
65+
file,
66+
originalSource.originalFileName,
67+
...(originalSource.originalFileNames ?? []),
68+
].filter((item): item is string => typeof item === 'string' && item.length > 0)
69+
70+
const sourceMatched = rememberedSources.filter(remembered =>
71+
originalFiles.some(originalFile => normalizeOutputPathKey(remembered.sourceFile) === normalizeOutputPathKey(originalFile)),
72+
)
73+
if (sourceMatched.length > 0) {
74+
return sourceMatched
75+
}
76+
77+
const outputMatched = rememberedSources.filter(remembered =>
78+
normalizeOutputPathKey(remembered.outputFile) === normalizeOutputPathKey(outputFile),
79+
)
80+
if (outputMatched.length > 0) {
81+
return outputMatched
82+
}
83+
84+
const shouldUseRememberedApplyFallback = !hasBundlerGeneratedCssMarker(source)
85+
&& !hasTailwindGenerationSource(source)
86+
if (shouldUseRememberedApplyFallback && !rememberedSources.some(remembered => hasTailwindApplyDirective(remembered.rawSource))) {
87+
return []
88+
}
89+
90+
const scoredMatches = rememberedSources
91+
.filter(remembered => !shouldUseRememberedApplyFallback || hasTailwindApplyDirective(remembered.rawSource))
92+
.filter(remembered => !(isMainAppCssFile(outputFile) && isAppOriginCssFile(remembered.outputFile)))
93+
.map(remembered => ({
94+
remembered,
95+
score: Math.max(
96+
scoreMatchingStyleFileBase(outputFile, remembered.sourceFile, outputRoot, sourceRoot),
97+
scoreMatchingStyleFileBase(outputFile, remembered.outputFile, outputRoot, sourceRoot),
98+
),
99+
}))
100+
.filter(match => match.score > 0)
101+
.sort((a, b) => b.score - a.score)
102+
const bestScore = scoredMatches[0]?.score
103+
return bestScore
104+
? scoredMatches.filter(match => match.score === bestScore).map(match => match.remembered)
105+
: []
106+
}
107+
108+
export function mergeRememberedCssSources(
109+
sources: RememberedCssSource[],
110+
outputFile: string,
111+
) {
112+
if (sources.length <= 1) {
113+
return sources[0]
114+
}
115+
const seen = new Set<string>()
116+
const rawSources: string[] = []
117+
for (const source of sources) {
118+
const key = `${source.sourceFile}\0${source.rawSource}`
119+
if (seen.has(key)) {
120+
continue
121+
}
122+
seen.add(key)
123+
rawSources.push(source.rawSource)
124+
}
125+
return {
126+
outputFile,
127+
rawSource: rawSources.join('\n'),
128+
sourceFile: sources[0]?.sourceFile ?? outputFile,
129+
}
130+
}
131+
132+
export function collectRememberedCssReplayGroups(
133+
sources: Iterable<[string, RememberedCssSource]> | undefined,
134+
opts: Pick<InternalUserDefinedOptions, 'cssMatcher'>,
135+
rootDir: string,
136+
isWebGeneratorTarget: boolean,
137+
preserveCssExtension: boolean,
138+
) {
139+
const groups = new Map<string, Array<{ key: string, remembered: RememberedCssSource }>>()
140+
for (const [key, remembered] of sources ?? []) {
141+
const outputFile = resolveViteCssPipelineOutputFile(
142+
remembered.outputFile,
143+
opts,
144+
rootDir,
145+
isWebGeneratorTarget,
146+
preserveCssExtension,
147+
)
148+
const outputKey = normalizeOutputPathKey(outputFile)
149+
const group = groups.get(outputKey) ?? []
150+
group.push({ key, remembered })
151+
groups.set(outputKey, group)
152+
}
153+
return groups
154+
}

0 commit comments

Comments
 (0)