Skip to content

Commit b1d596d

Browse files
committed
fix(uni-app-x): 修复 important utility 跨端编译
Refs #1113
1 parent d2c2907 commit b1d596d

12 files changed

Lines changed: 311 additions & 38 deletions

File tree

.changeset/clever-issues-1113.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"weapp-tailwindcss": patch
3+
"@weapp-tailwindcss/postcss": patch
4+
---
5+
6+
修复 uni-app x 中 important utility 与局部 Sass `@apply` 在 Web、Android Vapor 等目标间编译不一致的问题。

packages/postcss/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"postcss-pxtrans": "^1.0.4",
9595
"postcss-rem-to-responsive-pixel": "catalog:postcssRem",
9696
"postcss-rule-unit-converter": "^0.2.3",
97+
"postcss-scss": "^4.0.9",
9798
"postcss-selector-parser": "catalog:buildUtilities",
9899
"postcss-value-parser": "^4.2.0",
99100
"tailwindcss-config": "workspace:*"

packages/postcss/src/compat/uni-app-x.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import type { Result as PostcssResult, Rule } from 'postcss'
33
import type { Node, Pseudo } from 'postcss-selector-parser'
44
import type { IStyleHandlerOptions } from '../types'
5+
import { splitCandidateTokens } from '@tailwindcss-mangle/engine'
56
import postcss from 'postcss'
7+
import scssSyntax from 'postcss-scss'
8+
9+
/** native Sass 可解析、PostCSS 阶段再还原的 important utility 标记。 */
10+
export const UNI_APP_X_IMPORTANT_APPLY_MARKER = '__weapp_tw_important__'
611

712
const UNI_APP_X_BASE_CARRIER_SELECTORS = new Set([
813
'*',
@@ -18,6 +23,72 @@ const REQUIRED_TW_VAR_RE = /var\(\s*(--tw-[\w-]+)\s*\)/g
1823
const CLASS_SELECTOR_RE = /\.[\w-]+/
1924
const SELECTOR_WHITESPACE_RE = /\s+/g
2025

26+
function rewriteImportantApplyUtility(utility: string, marker: string) {
27+
if (utility.startsWith('!') && !utility.startsWith('\\!')) {
28+
return `${utility.slice(1)}${marker}`
29+
}
30+
if (utility.endsWith('!') && !utility.endsWith('\\!')) {
31+
return `${utility.slice(0, -1)}${marker}`
32+
}
33+
return utility
34+
}
35+
36+
function rewriteApplyParams(params: string, marker: string) {
37+
const candidates = splitCandidateTokens(params)
38+
if (candidates.length === 0) {
39+
return params
40+
}
41+
let result = params
42+
for (const candidate of candidates) {
43+
const rewritten = rewriteImportantApplyUtility(candidate, marker)
44+
if (rewritten !== candidate) {
45+
result = result.replace(candidate, rewritten)
46+
}
47+
}
48+
return result
49+
}
50+
51+
/** 将 Sass 不可直接解析的 important utility 改写成跨预处理器中间形式。 */
52+
export function normalizeUniAppXImportantApplyForSass(css: string) {
53+
try {
54+
const root = postcss.parse(css, { from: undefined, syntax: scssSyntax })
55+
let changed = false
56+
root.walkAtRules('apply', (rule) => {
57+
const params = rewriteApplyParams(rule.params, UNI_APP_X_IMPORTANT_APPLY_MARKER)
58+
if (params !== rule.params) {
59+
rule.params = params
60+
changed = true
61+
}
62+
})
63+
return changed ? root.toString() : css
64+
}
65+
catch {
66+
return css
67+
}
68+
}
69+
70+
/** 在 Tailwind/PostCSS 处理前还原 native important utility 标记。 */
71+
export function restoreUniAppXImportantApplyMarker(css: string) {
72+
if (!css.includes(UNI_APP_X_IMPORTANT_APPLY_MARKER)) {
73+
return css
74+
}
75+
try {
76+
const root = postcss.parse(css)
77+
let changed = false
78+
root.walkAtRules('apply', (rule) => {
79+
if (!rule.params.includes(UNI_APP_X_IMPORTANT_APPLY_MARKER)) {
80+
return
81+
}
82+
rule.params = rule.params.split(UNI_APP_X_IMPORTANT_APPLY_MARKER).join('!')
83+
changed = true
84+
})
85+
return changed ? root.toString() : css
86+
}
87+
catch {
88+
return css
89+
}
90+
}
91+
2192
interface TwDefaultDeclaration {
2293
prop: string
2394
value: string

packages/postcss/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export {
4444
type TailwindcssRpxToRemOptions,
4545
} from './compat/tailwindcss-rpx'
4646
export { normalizeTailwindcssV4InfinityCalcCss } from './compat/tailwindcss-v4'
47+
export {
48+
normalizeUniAppXImportantApplyForSass,
49+
restoreUniAppXImportantApplyMarker,
50+
UNI_APP_X_IMPORTANT_APPLY_MARKER,
51+
} from './compat/uni-app-x'
4752
export {
4853
type NormalizedWebCssCompatOptions,
4954
normalizeWebCssCompatOptions,

packages/postcss/test/uni-app-x.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import fs from 'fs-extra'
22
import path from 'pathe'
3+
import { normalizeUniAppXImportantApplyForSass, restoreUniAppXImportantApplyMarker } from '@/compat/uni-app-x'
34
import { applyUniAppXUvueCompatibility } from '@/compat/uni-app-x-uvue'
45
import { createStyleHandler, postcss } from '@/index'
56

67
const INVALID_UNI_APP_X_BASE_SELECTOR_RE = /(^|,)\s*(?:\*|view|text|::before|::after|:before|:after|::backdrop)\s*(?=,|\{)/m
78

89
describe('uni-app-x', () => {
10+
it('round-trips important apply utilities across Sass and PostCSS', () => {
11+
const source = '.probe { @apply !mt-6 mt-6! text-sm; }'
12+
const sassSafe = normalizeUniAppXImportantApplyForSass(source)
13+
14+
expect(sassSafe).toContain('@apply mt-6__weapp_tw_important__ mt-6__weapp_tw_important__ text-sm;')
15+
expect(restoreUniAppXImportantApplyMarker(sassSafe)).toContain('@apply mt-6! mt-6! text-sm;')
16+
})
17+
918
it('accepts Web local important apply syntax', async () => {
1019
const styleHandler = createStyleHandler({
1120
appType: 'uni-app-x',

packages/weapp-tailwindcss/src/uni-app-x/component-local-style.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { NodePath } from '@babel/traverse'
22
import type { StringLiteral, TemplateElement } from '@babel/types'
33
import { splitCandidateTokens } from '@tailwindcss-mangle/engine'
4+
import { UNI_APP_X_IMPORTANT_APPLY_MARKER } from '@weapp-tailwindcss/postcss'
45
import MagicString from 'magic-string'
56
import { analyzeSource, babelParse } from '@/js/babel'
67
import { isClassContextLiteralPath } from '@/js/class-context'
@@ -46,8 +47,12 @@ function createAlias(fileId: string, utility: string, index: number) {
4647
return `wtu-${createStableHash(`${fileId}:${utility}`)}-${index.toString(36)}`
4748
}
4849

49-
function serializeApplyUtility(utility: string, options: { web?: boolean } = {}) {
50-
const importantSuffix = options.web ? '!' : '#{\'!\'}'
50+
function serializeApplyUtility(utility: string, options: { native?: boolean, web?: boolean } = {}) {
51+
const importantSuffix = options.native
52+
? UNI_APP_X_IMPORTANT_APPLY_MARKER
53+
: options.web
54+
? '!'
55+
: '#{\'!\'}'
5156
if (utility.startsWith('!') && !utility.startsWith('\\!')) {
5257
return `${utility.slice(1)}${importantSuffix}`
5358
}
@@ -283,14 +288,14 @@ export class UniAppXComponentLocalStyleCollector {
283288
return this.aliasByUtility.size > 0
284289
}
285290

286-
toStyleBlock(options: { web?: boolean } = {}) {
291+
toStyleBlock(options: { native?: boolean, web?: boolean } = {}) {
287292
if (!this.hasStyles()) {
288293
return ''
289294
}
290295
return `<style scoped>\n${this.toStyleRules(options)}</style>\n`
291296
}
292297

293-
toStyleRules(options: { web?: boolean } = {}) {
298+
toStyleRules(options: { native?: boolean, web?: boolean } = {}) {
294299
if (!this.hasStyles()) {
295300
return ''
296301
}

packages/weapp-tailwindcss/src/uni-app-x/transform.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { SourceMapInput } from 'rollup'
33
import type { TransformResult } from 'vite'
44
import type { CreateJsHandlerOptions, ICustomAttributesEntities, JsHandler } from '@/types'
55
import { NodeTypes, parse as parseTemplate } from '@vue/compiler-dom'
6+
import { normalizeUniAppXImportantApplyForSass } from '@weapp-tailwindcss/postcss'
67
import MagicString from 'magic-string'
78
import { generateCode, replaceWxml } from '@/wxml'
89
import { createAttributeMatcher } from '@/wxml/custom-attributes'
@@ -130,6 +131,7 @@ interface TransformUVueOptions {
130131
enableComponentLocalStyle?: boolean
131132
enablePageLocalStyle?: boolean
132133
pageMatcher?: (id: string) => boolean
134+
native?: boolean
133135
webCustomAttributeDeep?: boolean
134136
onWebLocalStyleRules?: (rules: string) => void
135137
}
@@ -273,6 +275,14 @@ export function transformUVue(
273275
const matchCustomAttribute = createAttributeMatcher(customAttributesEntities)
274276
const ms = new MagicString(code)
275277
const descriptor = parseSfc(code)
278+
if (options.native || options.onWebLocalStyleRules) {
279+
for (const style of descriptor.styles) {
280+
const normalized = normalizeUniAppXImportantApplyForSass(style.content)
281+
if (normalized !== style.content) {
282+
ms.update(style.start, style.end, normalized)
283+
}
284+
}
285+
}
276286
const localStyleCollector = shouldEnableLocalStyle(id, options)
277287
? new UniAppXComponentLocalStyleCollector(id, runtimeSet)
278288
: undefined
@@ -372,10 +382,14 @@ export function transformUVue(
372382
}
373383
else if (scopedStyle) {
374384
const separator = scopedStyle.content.endsWith('\n') ? '' : '\n'
375-
ms.appendLeft(scopedStyle.end, `${separator}${localStyleCollector.toStyleRules()}`)
385+
ms.appendLeft(scopedStyle.end, `${separator}${localStyleCollector.toStyleRules({ native: options.native })}`)
376386
}
377387
else {
378-
ms.append(`\n${localStyleCollector.toStyleBlock({ web: Boolean(options.onWebLocalStyleRules) })}`)
388+
// 新增的局部样式块会单独进入预处理器,important utility 统一使用中间标记。
389+
ms.append(`\n${localStyleCollector.toStyleBlock({
390+
native: options.native || Boolean(options.onWebLocalStyleRules),
391+
web: Boolean(options.onWebLocalStyleRules),
392+
})}`)
379393
}
380394
}
381395
}

packages/weapp-tailwindcss/src/uni-app-x/vite.ts

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import type {
1111
} from '@/types'
1212
import path from 'node:path'
1313
import process from 'node:process'
14+
import {
15+
normalizeUniAppXImportantApplyForSass,
16+
restoreUniAppXImportantApplyMarker,
17+
} from '@weapp-tailwindcss/postcss'
1418
import { processCachedTask } from '@/bundlers/shared/cache'
1519
import { hasTailwindApplyDirective, hasTailwindRootDirectives } from '@/bundlers/shared/generator-css/directives'
1620
import { toAbsoluteOutputPath } from '@/bundlers/shared/module-graph'
21+
import { extractSfcStyleBlocks } from '@/bundlers/vite/generate-bundle/sfc-style-source'
1722
import { parseVueRequest } from '@/bundlers/vite/query'
1823
import { cleanUrl, formatPostcssSourceMap, isCSSRequest, normalizePath } from '@/bundlers/vite/utils'
1924
import { logger } from '@/logger'
@@ -46,6 +51,9 @@ function loadTransformUVue(): Promise<TransformUVue> {
4651
}
4752
const UVUE_NVUE_QUERY_RE = /\.(?:uvue|nvue)(?:\?.*)?$/
4853
const UVUE_NVUE_RE = /\.(?:uvue|nvue)$/
54+
function hasUniAppXImportantApply(source: string) {
55+
return extractSfcStyleBlocks(source).some(style => normalizeUniAppXImportantApplyForSass(style.source) !== style.source)
56+
}
4957
function resolveUniAppXJsTransformEnabled(uniAppX: InternalUserDefinedOptions['uniAppX'] | undefined) {
5058
return uniAppX === undefined ? true : isUniAppXEnabled(uniAppX)
5159
}
@@ -92,6 +100,7 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
92100
}
93101
}>()
94102
const nativeLocalStyleModuleIds = new Set<string>()
103+
const knownSfcSources = new Map<string, string>()
95104
const webLocalStyle = createUniAppXWebLocalStyleBridge(isWebGeneratorTarget)
96105
let componentLocalStyleEnabled: boolean | undefined
97106
const isNativeAppBuildTarget = createUniAppXNativeBuildTargetResolver(getResolvedConfig)
@@ -169,7 +178,10 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
169178
if (isCssModuleExport(code)) {
170179
return
171180
}
172-
const sourceCode = normalizeRelativeTailwindReferences(code, id)
181+
const sourceCode = normalizeRelativeTailwindReferences(
182+
restoreUniAppXImportantApplyMarker(code),
183+
id,
184+
)
173185
const hasTailwindRoot = hasTailwindRootDirectives(sourceCode, { importFallback: true })
174186
const hasTailwindApply = hasTailwindApplyDirective(sourceCode)
175187
const shouldGenerateCss = hasTailwindRoot || hasTailwindApply
@@ -230,21 +242,45 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
230242
const cssPrePlugin: Plugin = {
231243
name: 'weapp-tailwindcss:uni-app-x:css:pre',
232244
enforce: 'pre',
233-
async transform(code, id) {
234-
if (!isEnabled()) {
235-
return
236-
}
237-
await runtimeState.readyPromise
238-
const { query } = parseVueRequest(id)
239-
const styleCode = query.vue && query.type === 'style' ? webLocalStyle.appendToStyle(code, id) : code
240-
const preprocessor = resolvePreprocessorTransform(styleCode, id, query.lang, {
241-
isIosPlatform,
242-
isNativeAppStyleTarget: isNativeAppStyleTarget(),
243-
})
244-
if (preprocessor) {
245-
return preprocessor.result ?? (styleCode !== code ? { code: styleCode, map: null } : undefined)
246-
}
247-
return transformStyle(styleCode, id, query, this)
245+
load: {
246+
order: 'pre',
247+
async handler(id) {
248+
const { filename, query } = parseVueRequest(id)
249+
if (!query.vue || query.type !== 'style' || !UVUE_NVUE_RE.test(filename)) {
250+
return
251+
}
252+
const source = knownSfcSources.get(filename)
253+
const style = source ? extractSfcStyleBlocks(source)[query.index ?? 0] : undefined
254+
if (!style) {
255+
return
256+
}
257+
const normalized = normalizeUniAppXImportantApplyForSass(style.source)
258+
return normalized === style.source ? undefined : { code: normalized, map: null }
259+
},
260+
},
261+
transform: {
262+
order: 'pre',
263+
async handler(code, id) {
264+
if (!isEnabled()) {
265+
return
266+
}
267+
await runtimeState.readyPromise
268+
const { query } = parseVueRequest(id)
269+
const styleCode = query.vue && query.type === 'style' ? webLocalStyle.appendToStyle(code, id) : code
270+
// Vite 热更新会绕过 SFC 主模块,直接把原始样式交给预处理器;先移除
271+
// Sass 无法解析的 important utility 后,再由后续 CSS 阶段还原。
272+
const preprocessorCode = query.vue && query.type === 'style'
273+
? normalizeUniAppXImportantApplyForSass(styleCode)
274+
: styleCode
275+
const preprocessor = resolvePreprocessorTransform(preprocessorCode, id, query.lang, {
276+
isIosPlatform,
277+
isNativeAppStyleTarget: isNativeAppStyleTarget(),
278+
})
279+
if (preprocessor) {
280+
return preprocessor.result ?? (preprocessorCode !== code ? { code: preprocessorCode, map: null } : undefined)
281+
}
282+
return transformStyle(preprocessorCode, id, query, this)
283+
},
248284
},
249285
}
250286
const cssPlugin: Plugin = {
@@ -261,6 +297,7 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
261297
const cssPlugins = [cssPlugin, cssPrePlugin]
262298

263299
async function transformSfc(code: string, id: string, context: { addWatchFile?: (id: string) => void }) {
300+
knownSfcSources.set(cleanUrl(id), code)
264301
if (isNativeAppBuildTarget(id)) {
265302
nativeLocalStyleModuleIds.add(id)
266303
nativeLocalStyleModuleIds.add(cleanUrl(id))
@@ -285,6 +322,7 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
285322
...(disabledDefaultTemplateHandler ? { disabledDefaultTemplateHandler } : {}),
286323
...(enableComponentLocalStyle ? { enableComponentLocalStyle } : {}),
287324
...(enablePageLocalStyle ? { enablePageLocalStyle } : {}),
325+
native: true,
288326
pageMatcher: resolvedUniAppXOptions.componentLocalStyles.pageMatcher,
289327
...(isWebGeneratorTarget() && customAttributesEntities.length > 0 ? { webCustomAttributeDeep: true } : {}),
290328
...(isWebGeneratorTarget() ? { onWebLocalStyleRules: (rules: string) => webLocalStyle.remember(id, rules) } : {}),
@@ -322,7 +360,7 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
322360
},
323361
},
324362
handleHotUpdate: {
325-
order: 'post',
363+
order: 'pre',
326364
async handler(ctx) {
327365
if (!isEnabled() || getResolvedConfig()?.command !== 'serve') {
328366
return
@@ -331,7 +369,12 @@ export function createUniAppXPlugins(options: CreateUniAppXPluginsOptions): Plug
331369
return
332370
}
333371
if (isWebGeneratorTarget() && UVUE_NVUE_RE.test(ctx.file) && typeof ctx.read === 'function') {
334-
await transformSfc(await ctx.read(), ctx.file, this)
372+
const source = await ctx.read()
373+
if (hasUniAppXImportantApply(source)) {
374+
ctx.server.ws.send({ type: 'full-reload', path: ctx.file })
375+
return []
376+
}
377+
await transformSfc(source, ctx.file, this)
335378
}
336379
return webLocalStyle.handleHotUpdate(ctx) ?? nativeHmrReloader.handleHotUpdate(ctx)
337380
},
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<view class="!mt-6 mt-6!">important utility</view>
3+
</template>
4+
5+
<style lang="scss">
6+
.important-apply {
7+
@apply !mt-6 mt-6!;
8+
}
9+
</style>

0 commit comments

Comments
 (0)